Skip to content

Commit afe486c

Browse files
agriyakhetarpalhenryiiipre-commit-ci[bot]
authored
feat: add a --clean-cache command to clean up locations specified at CIBW_CACHE_PATH (#2489)
* Add `clean_cache` to command-line arguments * Add `--clean-cache` to parser * Add logic * Add test to clean cache when it exists * Ad test to run cache cleaning when none exists * Add test to run cache cleaning on unwritable path * Use `main()` instead * tests: move command tests Signed-off-by: Henry Schreiner <[email protected]> * Create cibuildwheel cache sentinel file * Add sentinel to existing tests + add new test * Add signature and switch to `CACHEDIR.TAG` Co-authored-by: Henry Schreiner <[email protected]> * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Another instance to replace with `CACHEDIR.TAG` * Verify `CACHEDIR.TAG` signature * Simplify code branching and control flow * Add test * Add link to `CACHEDIR.TAG` specification * Write with UTF-8 encoding * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Signed-off-by: Henry Schreiner <[email protected]> Co-authored-by: Henry Schreiner <[email protected]> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent 1c1ba8a commit afe486c

File tree

4 files changed

+199
-1
lines changed

4 files changed

+199
-1
lines changed

cibuildwheel/__main__.py

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
from cibuildwheel.platforms import ALL_PLATFORM_MODULES, get_build_identifiers, native_platform
2424
from cibuildwheel.selector import BuildSelector, EnableGroup, selector_matches
2525
from cibuildwheel.typing import PLATFORMS, PlatformName
26-
from cibuildwheel.util.file import CIBW_CACHE_PATH
26+
from cibuildwheel.util.file import CIBW_CACHE_PATH, ensure_cache_sentinel
2727
from cibuildwheel.util.helpers import strtobool
2828
from cibuildwheel.util.resources import read_all_configs
2929

@@ -179,6 +179,12 @@ def main_inner(global_options: GlobalOptions) -> None:
179179
help="Print the build identifiers matched by the current invocation and exit.",
180180
)
181181

182+
parser.add_argument(
183+
"--clean-cache",
184+
action="store_true",
185+
help="Clear the cibuildwheel cache and exit.",
186+
)
187+
182188
parser.add_argument(
183189
"--allow-empty",
184190
action="store_true",
@@ -196,6 +202,48 @@ def main_inner(global_options: GlobalOptions) -> None:
196202

197203
global_options.print_traceback_on_error = args.debug_traceback
198204

205+
if args.clean_cache:
206+
if not CIBW_CACHE_PATH.exists():
207+
print(f"Cache directory does not exist: {CIBW_CACHE_PATH}")
208+
sys.exit(0)
209+
210+
sentinel_file = CIBW_CACHE_PATH / "CACHEDIR.TAG"
211+
if not sentinel_file.exists():
212+
print(
213+
f"Error: {CIBW_CACHE_PATH} does not appear to be a cibuildwheel cache directory.",
214+
"Only directories with a CACHEDIR.TAG sentinel file can be cleaned.",
215+
sep="\n",
216+
file=sys.stderr,
217+
)
218+
sys.exit(1)
219+
220+
# Verify signature to ensure it's a proper cache dir
221+
# See https://bford.info/cachedir/ for more
222+
try:
223+
sentinel_content = sentinel_file.read_text(encoding="utf-8")
224+
except OSError as e:
225+
print(f"Error reading cache directory tag: {e}", file=sys.stderr)
226+
sys.exit(1)
227+
228+
if not sentinel_content.startswith("Signature: 8a477f597d28d172789f06886806bc55"):
229+
print(
230+
f"Error: {sentinel_file} does not contain a valid cache directory signature.",
231+
"For safety, only properly signed cache directories can be cleaned.",
232+
sep="\n",
233+
file=sys.stderr,
234+
)
235+
sys.exit(1)
236+
237+
print(f"Clearing cache directory: {CIBW_CACHE_PATH}")
238+
try:
239+
shutil.rmtree(CIBW_CACHE_PATH)
240+
print("Cache cleared successfully.")
241+
except OSError as e:
242+
print(f"Error clearing cache: {e}", file=sys.stderr)
243+
sys.exit(1)
244+
245+
sys.exit(0)
246+
199247
args.package_dir = args.package_dir.resolve()
200248

201249
# This are always relative to the base directory, even in SDist builds
@@ -309,6 +357,7 @@ def build_in_directory(args: CommandLineArguments) -> None:
309357

310358
# create the cache dir before it gets printed & builds performed
311359
CIBW_CACHE_PATH.mkdir(parents=True, exist_ok=True)
360+
ensure_cache_sentinel(CIBW_CACHE_PATH)
312361

313362
print_preamble(platform=platform, options=options, identifiers=identifiers)
314363

cibuildwheel/options.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ class CommandLineArguments:
6363
allow_empty: bool
6464
debug_traceback: bool
6565
enable: list[str]
66+
clean_cache: bool
6667

6768
@classmethod
6869
def defaults(cls) -> Self:
@@ -77,6 +78,7 @@ def defaults(cls) -> Self:
7778
print_build_identifiers=False,
7879
debug_traceback=False,
7980
enable=[],
81+
clean_cache=False,
8082
)
8183

8284

cibuildwheel/util/file.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,26 @@
2020
).resolve()
2121

2222

23+
def ensure_cache_sentinel(cache_path: Path) -> None:
24+
"""Create a sentinel file to mark a cibuildwheel cache directory.
25+
26+
We help prevent accidental deletion of non-cache directories with this.
27+
28+
Args:
29+
cache_path: The cache directory path where the sentinel should be created
30+
"""
31+
if cache_path.exists():
32+
sentinel_file = cache_path / "CACHEDIR.TAG"
33+
if not sentinel_file.exists():
34+
sentinel_file.write_text(
35+
"Signature: 8a477f597d28d172789f06886806bc55\n"
36+
"# This file is a cache directory tag created by cibuildwheel.\n"
37+
"# For information about cache directory tags, see:\n"
38+
"# https://www.brynosaurus.com/cachedir/",
39+
encoding="utf-8",
40+
)
41+
42+
2343
def download(url: str, dest: Path) -> None:
2444
print(f"+ Download {url} to {dest}")
2545
dest_dir = dest.parent

unit_test/main_commands_test.py

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
import errno
2+
import shutil
3+
import sys
4+
5+
import pytest
6+
7+
import cibuildwheel.__main__ as main_module
8+
from cibuildwheel.__main__ import main
9+
10+
11+
def test_clean_cache_when_cache_exists(tmp_path, monkeypatch, capfd):
12+
fake_cache_dir = (tmp_path / "cibw_cache").resolve()
13+
monkeypatch.setattr(main_module, "CIBW_CACHE_PATH", fake_cache_dir)
14+
15+
fake_cache_dir.mkdir(parents=True, exist_ok=True)
16+
assert fake_cache_dir.exists()
17+
18+
cibw_sentinel = fake_cache_dir / "CACHEDIR.TAG"
19+
cibw_sentinel.write_text(
20+
"Signature: 8a477f597d28d172789f06886806bc55\n"
21+
"# This file is a cache directory tag created by cibuildwheel.\n"
22+
"# For information about cache directory tags, see:\n"
23+
"# https://www.brynosaurus.com/cachedir/",
24+
encoding="utf-8",
25+
)
26+
27+
dummy_file = fake_cache_dir / "dummy.txt"
28+
dummy_file.write_text("hello")
29+
30+
monkeypatch.setattr(sys, "argv", ["cibuildwheel", "--clean-cache"])
31+
32+
with pytest.raises(SystemExit) as e:
33+
main()
34+
35+
assert e.value.code == 0
36+
37+
out, err = capfd.readouterr()
38+
assert f"Clearing cache directory: {fake_cache_dir}" in out
39+
assert "Cache cleared successfully." in out
40+
assert not fake_cache_dir.exists()
41+
42+
43+
def test_clean_cache_when_cache_does_not_exist(tmp_path, monkeypatch, capfd):
44+
fake_cache_dir = (tmp_path / "nonexistent_cache").resolve()
45+
monkeypatch.setattr(main_module, "CIBW_CACHE_PATH", fake_cache_dir)
46+
47+
monkeypatch.setattr(sys, "argv", ["cibuildwheel", "--clean-cache"])
48+
49+
with pytest.raises(SystemExit) as e:
50+
main()
51+
52+
assert e.value.code == 0
53+
54+
out, err = capfd.readouterr()
55+
assert f"Cache directory does not exist: {fake_cache_dir}" in out
56+
57+
58+
def test_clean_cache_with_error(tmp_path, monkeypatch, capfd):
59+
fake_cache_dir = (tmp_path / "cibw_cache").resolve()
60+
monkeypatch.setattr(main_module, "CIBW_CACHE_PATH", fake_cache_dir)
61+
62+
fake_cache_dir.mkdir(parents=True, exist_ok=True)
63+
assert fake_cache_dir.exists()
64+
65+
cibw_sentinel = fake_cache_dir / "CACHEDIR.TAG"
66+
cibw_sentinel.write_text(
67+
"Signature: 8a477f597d28d172789f06886806bc55\n"
68+
"# This file is a cache directory tag created by cibuildwheel.\n"
69+
"# For information about cache directory tags, see:\n"
70+
"# https://www.brynosaurus.com/cachedir/",
71+
encoding="utf-8",
72+
)
73+
74+
monkeypatch.setattr(sys, "argv", ["cibuildwheel", "--clean-cache"])
75+
76+
def fake_rmtree(path): # noqa: ARG001
77+
raise OSError(errno.EACCES, "Permission denied")
78+
79+
monkeypatch.setattr(shutil, "rmtree", fake_rmtree)
80+
81+
with pytest.raises(SystemExit) as e:
82+
main()
83+
84+
assert e.value.code == 1
85+
86+
out, err = capfd.readouterr()
87+
assert f"Clearing cache directory: {fake_cache_dir}" in out
88+
assert "Error clearing cache:" in err
89+
90+
91+
def test_clean_cache_without_sentinel(tmp_path, monkeypatch, capfd):
92+
fake_cache_dir = (tmp_path / "not_a_cache").resolve()
93+
monkeypatch.setattr(main_module, "CIBW_CACHE_PATH", fake_cache_dir)
94+
95+
fake_cache_dir.mkdir(parents=True, exist_ok=True)
96+
97+
monkeypatch.setattr(sys, "argv", ["cibuildwheel", "--clean-cache"])
98+
99+
with pytest.raises(SystemExit) as e:
100+
main()
101+
102+
assert e.value.code == 1
103+
104+
out, err = capfd.readouterr()
105+
assert "does not appear to be a cibuildwheel cache directory" in err
106+
assert fake_cache_dir.exists()
107+
108+
109+
def test_clean_cache_with_invalid_signature(tmp_path, monkeypatch, capfd):
110+
fake_cache_dir = (tmp_path / "fake_cache").resolve()
111+
monkeypatch.setattr(main_module, "CIBW_CACHE_PATH", fake_cache_dir)
112+
113+
fake_cache_dir.mkdir(parents=True, exist_ok=True)
114+
115+
cibw_sentinel = fake_cache_dir / "CACHEDIR.TAG"
116+
cibw_sentinel.write_text("Invalid signature\n# This is not a real cache directory tag")
117+
118+
monkeypatch.setattr(sys, "argv", ["cibuildwheel", "--clean-cache"])
119+
120+
with pytest.raises(SystemExit) as e:
121+
main()
122+
123+
assert e.value.code == 1
124+
125+
out, err = capfd.readouterr()
126+
assert "does not contain a valid cache directory signature" in err
127+
assert fake_cache_dir.exists()

0 commit comments

Comments
 (0)