Skip to content

Commit 8f1a3c9

Browse files
authored
Merge pull request #4978 from mahf708/claude/add-cache-clean-option-poTqB
Reset only the CMake cache (CMakeCache.txt and the small bookkeeping files in CMakeFiles/) without touching compiled object files. This is useful when CMake needs to re-configure (e.g. after editing macros or toolchain settings) but a full --clean-all is too expensive. Adds: - --clean-cache flag in CIME/Tools/case.build (mutually exclusive with the existing --clean-* options) - build._clean_cache_impl and a clean_cache=True path through build.clean, logged under a distinct "build.clean_cache" phase - pytest unit tests covering the happy path, no-op when no cmake-bld dir exists, missing CMakeCache.txt, and dispatch routing in build.clean feat(build): add --clean-cache option to case.build
2 parents 6e9074e + 743e8f7 commit 8f1a3c9

3 files changed

Lines changed: 215 additions & 4 deletions

File tree

CIME/Tools/case.build

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@ other options are specific to one mode or the other:
4040
./case.build --clean ...
4141
or:
4242
./case.build --clean-depends ...
43+
44+
To reset only the CMake cache (forcing CMake to re-configure on the next
45+
build) while preserving compiled object files:
46+
./case.build --clean-cache
4347
"""
4448

4549
from standard_script_setup import *
@@ -49,6 +53,7 @@ from CIME.case import Case
4953
from CIME.utils import find_system_test, get_model
5054
from CIME.test_status import *
5155

56+
5257
###############################################################################
5358
def parse_command_line(args, description):
5459
###############################################################################
@@ -154,6 +159,15 @@ def parse_command_line(args, description):
154159
"files in the source tree or in SourceMods.",
155160
)
156161

162+
mutex_group.add_argument(
163+
"--clean-cache",
164+
action="store_true",
165+
help="Remove the CMake cache (CMakeCache.txt) so CMake re-configures\n"
166+
"on the next build. Compiled object files are preserved, so this is\n"
167+
"much cheaper than --clean-all when you only need CMake to pick up\n"
168+
"configuration changes.",
169+
)
170+
157171
args = CIME.utils.parse_args_and_handle_standard_logging_options(args, parser)
158172

159173
clean_depends = (
@@ -177,6 +191,7 @@ def parse_command_line(args, description):
177191
args.clean_all,
178192
buildlist,
179193
clean_depends,
194+
args.clean_cache,
180195
not args.skip_provenance_check,
181196
args.separate_builds,
182197
args.ninja,
@@ -196,6 +211,7 @@ def _main_func(description):
196211
clean_all,
197212
buildlist,
198213
clean_depends,
214+
clean_cache,
199215
save_build_provenance,
200216
separate_builds,
201217
ninja,
@@ -207,12 +223,18 @@ def _main_func(description):
207223
with Case(caseroot, read_only=False, record=True) as case:
208224
testname = case.get_value("TESTCASE")
209225

210-
if cleanlist is not None or clean_all or clean_depends is not None:
226+
if (
227+
cleanlist is not None
228+
or clean_all
229+
or clean_depends is not None
230+
or clean_cache
231+
):
211232
build.clean(
212233
case,
213234
cleanlist=cleanlist,
214235
clean_all=clean_all,
215236
clean_depends=clean_depends,
237+
clean_cache=clean_cache,
216238
)
217239
elif testname is not None:
218240
logging.warning(

CIME/build.py

Lines changed: 55 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -976,6 +976,53 @@ def _create_build_metadata_for_component(config_dir, libroot, bldroot, case):
976976
return "" if cmake_args is None else cmake_args
977977

978978

979+
###############################################################################
980+
def _clean_cache_impl(case):
981+
###############################################################################
982+
"""Remove the CMake cache so the next build re-configures CMake without
983+
discarding compiled object files.
984+
985+
Only ``CMakeCache.txt`` and the small CMake-generated bookkeeping files in
986+
``CMakeFiles`` (``cmake.check_cache``, ``CMakeCache*.txt``) are removed;
987+
per-target object directories are left in place so incremental builds
988+
continue to work.
989+
"""
990+
exeroot = os.path.abspath(case.get_value("EXEROOT"))
991+
case.load_env()
992+
bldroot = os.path.join(exeroot, "cmake-bld")
993+
if not os.path.isdir(bldroot):
994+
logging.info("No cmake build directory found at {}".format(bldroot))
995+
return
996+
997+
cache_file = os.path.join(bldroot, "CMakeCache.txt")
998+
if os.path.isfile(cache_file):
999+
logging.info("removing {}".format(cache_file))
1000+
os.remove(cache_file)
1001+
else:
1002+
logging.info("no CMakeCache.txt found at {}".format(cache_file))
1003+
1004+
# Also remove the small CMake bookkeeping files that reference the cache,
1005+
# but preserve per-target object directories under CMakeFiles/.
1006+
cmake_files_dir = os.path.join(bldroot, "CMakeFiles")
1007+
if os.path.isdir(cmake_files_dir):
1008+
stale = ["cmake.check_cache"]
1009+
stale += [
1010+
os.path.basename(p)
1011+
for p in glob.glob(os.path.join(cmake_files_dir, "CMakeCache*.txt"))
1012+
]
1013+
for name in stale:
1014+
path = os.path.join(cmake_files_dir, name)
1015+
if os.path.isfile(path):
1016+
logging.info("removing {}".format(path))
1017+
os.remove(path)
1018+
1019+
# unlink Locked files directory so env_build.xml can be modified
1020+
unlock_file("env_build.xml", case.get_value("CASEROOT"))
1021+
1022+
case.set_value("BUILD_COMPLETE", "FALSE")
1023+
case.flush()
1024+
1025+
9791026
###############################################################################
9801027
def _clean_impl(case, cleanlist, clean_all, clean_depends):
9811028
###############################################################################
@@ -1342,12 +1389,17 @@ def case_build(
13421389

13431390

13441391
###############################################################################
1345-
def clean(case, cleanlist=None, clean_all=False, clean_depends=None):
1392+
def clean(case, cleanlist=None, clean_all=False, clean_depends=None, clean_cache=False):
13461393
###############################################################################
1347-
functor = lambda: _clean_impl(case, cleanlist, clean_all, clean_depends)
1394+
if clean_cache:
1395+
functor = lambda: _clean_cache_impl(case)
1396+
phase = "build.clean_cache"
1397+
else:
1398+
functor = lambda: _clean_impl(case, cleanlist, clean_all, clean_depends)
1399+
phase = "build.clean"
13481400
return run_and_log_case_status(
13491401
functor,
1350-
"build.clean",
1402+
phase,
13511403
caseroot=case.get_value("CASEROOT"),
13521404
gitinterface=case._gitinterface,
13531405
)
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
#!/usr/bin/env python3
2+
3+
"""Unit tests for ``build._clean_cache_impl``.
4+
5+
These tests exercise the ``--clean-cache`` flag added to ``case.build``,
6+
which removes ``CMakeCache.txt`` and the small CMake bookkeeping files in
7+
``CMakeFiles/`` while preserving compiled object files so the next build can
8+
incrementally re-configure CMake without rebuilding everything.
9+
"""
10+
11+
import os
12+
from unittest import mock
13+
14+
import pytest
15+
16+
from CIME import build
17+
18+
19+
def _make_case(exeroot, caseroot):
20+
"""Build a minimal ``Case`` mock that ``_clean_cache_impl`` understands."""
21+
case = mock.MagicMock()
22+
case.get_value.side_effect = lambda key: {
23+
"EXEROOT": str(exeroot),
24+
"CASEROOT": str(caseroot),
25+
}[key]
26+
return case
27+
28+
29+
@pytest.fixture
30+
def cmake_bld(tmp_path):
31+
"""Create a fake ``cmake-bld`` tree with a populated CMakeFiles/ directory."""
32+
exeroot = tmp_path / "bld"
33+
bldroot = exeroot / "cmake-bld"
34+
cmake_files = bldroot / "CMakeFiles"
35+
target_dir = cmake_files / "atm.dir"
36+
target_dir.mkdir(parents=True)
37+
38+
# The cache file itself.
39+
(bldroot / "CMakeCache.txt").write_text("# fake cache\n")
40+
41+
# CMake bookkeeping files that reference the cache.
42+
(cmake_files / "cmake.check_cache").write_text("")
43+
(cmake_files / "CMakeCacheCopy.txt").write_text("")
44+
45+
# A precious object file that must survive a cache clean.
46+
object_file = target_dir / "foo.f90.o"
47+
object_file.write_bytes(b"\x7fELF")
48+
49+
return exeroot, object_file
50+
51+
52+
@mock.patch("CIME.build.unlock_file")
53+
def test_clean_cache_removes_cache_and_bookkeeping(unlock_file, tmp_path, cmake_bld):
54+
"""Happy path: cache + check-cache files removed, objects preserved."""
55+
exeroot, object_file = cmake_bld
56+
case = _make_case(exeroot, tmp_path)
57+
58+
build._clean_cache_impl(case)
59+
60+
bldroot = exeroot / "cmake-bld"
61+
assert not (bldroot / "CMakeCache.txt").exists()
62+
assert not (bldroot / "CMakeFiles" / "cmake.check_cache").exists()
63+
assert not (bldroot / "CMakeFiles" / "CMakeCacheCopy.txt").exists()
64+
65+
# Object files must be preserved.
66+
assert object_file.exists()
67+
# CMakeFiles/ itself must remain (per-target object directories live there).
68+
assert (bldroot / "CMakeFiles" / "atm.dir").is_dir()
69+
70+
case.set_value.assert_any_call("BUILD_COMPLETE", "FALSE")
71+
case.flush.assert_called_once()
72+
unlock_file.assert_called_once_with("env_build.xml", str(tmp_path))
73+
74+
75+
@mock.patch("CIME.build.unlock_file")
76+
def test_clean_cache_no_build_dir_is_noop(unlock_file, tmp_path):
77+
"""If the cmake-bld directory does not exist, the call should be a no-op."""
78+
exeroot = tmp_path / "bld" # intentionally not created
79+
case = _make_case(exeroot, tmp_path)
80+
81+
build._clean_cache_impl(case)
82+
83+
case.set_value.assert_not_called()
84+
case.flush.assert_not_called()
85+
unlock_file.assert_not_called()
86+
87+
88+
@mock.patch("CIME.build.unlock_file")
89+
def test_clean_cache_missing_cache_file_still_resets_state(unlock_file, tmp_path):
90+
"""A cmake-bld dir without CMakeCache.txt still resets BUILD_COMPLETE."""
91+
exeroot = tmp_path / "bld"
92+
(exeroot / "cmake-bld").mkdir(parents=True)
93+
case = _make_case(exeroot, tmp_path)
94+
95+
build._clean_cache_impl(case)
96+
97+
case.set_value.assert_any_call("BUILD_COMPLETE", "FALSE")
98+
case.flush.assert_called_once()
99+
unlock_file.assert_called_once_with("env_build.xml", str(tmp_path))
100+
101+
102+
def test_clean_dispatches_to_clean_cache_when_requested(tmp_path):
103+
"""``build.clean(..., clean_cache=True)`` must route to the cache impl."""
104+
exeroot = tmp_path / "bld"
105+
(exeroot / "cmake-bld").mkdir(parents=True)
106+
case = _make_case(exeroot, tmp_path)
107+
case._gitinterface = None
108+
109+
with mock.patch("CIME.build._clean_cache_impl") as cache_impl, mock.patch(
110+
"CIME.build._clean_impl"
111+
) as full_impl, mock.patch("CIME.build.run_and_log_case_status") as runner:
112+
runner.side_effect = lambda functor, *a, **kw: functor()
113+
114+
build.clean(case, clean_cache=True)
115+
116+
cache_impl.assert_called_once_with(case)
117+
full_impl.assert_not_called()
118+
# The phase name should be distinct from the regular clean phase so
119+
# case status logs make the operation traceable.
120+
assert runner.call_args.args[1] == "build.clean_cache"
121+
122+
123+
def test_clean_dispatches_to_clean_impl_by_default(tmp_path):
124+
"""Without ``clean_cache``, the legacy clean path must still be used."""
125+
case = _make_case(tmp_path / "bld", tmp_path)
126+
case._gitinterface = None
127+
128+
with mock.patch("CIME.build._clean_cache_impl") as cache_impl, mock.patch(
129+
"CIME.build._clean_impl"
130+
) as full_impl, mock.patch("CIME.build.run_and_log_case_status") as runner:
131+
runner.side_effect = lambda functor, *a, **kw: functor()
132+
133+
build.clean(case, clean_all=True)
134+
135+
full_impl.assert_called_once_with(case, None, True, None)
136+
cache_impl.assert_not_called()
137+
assert runner.call_args.args[1] == "build.clean"

0 commit comments

Comments
 (0)