From d955e47c168e3e29ea14f98d733a9db38430816e Mon Sep 17 00:00:00 2001 From: Agriya Khetarpal <74401230+agriyakhetarpal@users.noreply.github.com> Date: Mon, 7 Jul 2025 13:07:49 +0530 Subject: [PATCH 01/19] Add `clean_cache` to command-line arguments --- cibuildwheel/options.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cibuildwheel/options.py b/cibuildwheel/options.py index 7c98f1e21..e1ad8be14 100644 --- a/cibuildwheel/options.py +++ b/cibuildwheel/options.py @@ -63,6 +63,7 @@ class CommandLineArguments: allow_empty: bool debug_traceback: bool enable: list[str] + clean_cache: bool @classmethod def defaults(cls) -> Self: @@ -77,6 +78,7 @@ def defaults(cls) -> Self: print_build_identifiers=False, debug_traceback=False, enable=[], + clean_cache=False, ) From 7a19069938e5cc293b2361e2ea48a6c17551bbac Mon Sep 17 00:00:00 2001 From: Agriya Khetarpal <74401230+agriyakhetarpal@users.noreply.github.com> Date: Mon, 7 Jul 2025 13:08:06 +0530 Subject: [PATCH 02/19] Add `--clean-cache` to parser --- cibuildwheel/__main__.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/cibuildwheel/__main__.py b/cibuildwheel/__main__.py index 934d80c4e..98f4d2f2d 100644 --- a/cibuildwheel/__main__.py +++ b/cibuildwheel/__main__.py @@ -180,6 +180,12 @@ def main_inner(global_options: GlobalOptions) -> None: help="Print the build identifiers matched by the current invocation and exit.", ) + parser.add_argument( + "--clean-cache", + action="store_true", + help="Clear the cibuildwheel cache and exit.", + ) + parser.add_argument( "--allow-empty", action="store_true", From f705697e4f32bc3ed5af5b19e700b62674f89d4b Mon Sep 17 00:00:00 2001 From: Agriya Khetarpal <74401230+agriyakhetarpal@users.noreply.github.com> Date: Mon, 7 Jul 2025 13:08:14 +0530 Subject: [PATCH 03/19] Add logic --- cibuildwheel/__main__.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/cibuildwheel/__main__.py b/cibuildwheel/__main__.py index 98f4d2f2d..a0e28d216 100644 --- a/cibuildwheel/__main__.py +++ b/cibuildwheel/__main__.py @@ -203,6 +203,19 @@ def main_inner(global_options: GlobalOptions) -> None: global_options.print_traceback_on_error = args.debug_traceback + if args.clean_cache: + if CIBW_CACHE_PATH.exists(): + print(f"Clearing cache directory: {CIBW_CACHE_PATH}") + try: + shutil.rmtree(CIBW_CACHE_PATH) + print("Cache cleared successfully.") + except OSError as e: + print(f"Error clearing cache: {e}", file=sys.stderr) + sys.exit(1) + else: + print(f"Cache directory does not exist: {CIBW_CACHE_PATH}") + sys.exit(0) + args.package_dir = args.package_dir.resolve() # This are always relative to the base directory, even in SDist builds From 103944064b71d0ddbfed768ebac2aec60f2152fb Mon Sep 17 00:00:00 2001 From: Agriya Khetarpal <74401230+agriyakhetarpal@users.noreply.github.com> Date: Mon, 7 Jul 2025 14:02:06 +0530 Subject: [PATCH 04/19] Add test to clean cache when it exists --- unit_test/main_tests/main_options_test.py | 27 +++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/unit_test/main_tests/main_options_test.py b/unit_test/main_tests/main_options_test.py index 731d10adc..1fbbd0df8 100644 --- a/unit_test/main_tests/main_options_test.py +++ b/unit_test/main_tests/main_options_test.py @@ -1,3 +1,5 @@ +import errno +import shutil import sys import tomllib from fnmatch import fnmatch @@ -5,6 +7,7 @@ import pytest +import cibuildwheel.__main__ as main_module from cibuildwheel.__main__ import main from cibuildwheel.environment import ParsedEnvironment from cibuildwheel.frontend import _split_config_settings @@ -421,6 +424,30 @@ def test_debug_traceback(monkeypatch, method, capfd): assert "Traceback (most recent call last)" in err +def test_clean_cache_when_cache_exists(tmp_path, monkeypatch, capfd): + monkeypatch.undo() + fake_cache_dir = (tmp_path / "cibw_cache").resolve() + monkeypatch.setattr(main_module, "CIBW_CACHE_PATH", fake_cache_dir) + + fake_cache_dir.mkdir(parents=True, exist_ok=True) + assert fake_cache_dir.exists() + + dummy_file = fake_cache_dir / "dummy.txt" + dummy_file.write_text("hello") + + monkeypatch.setattr(sys, "argv", ["cibuildwheel", "--clean-cache"]) + + with pytest.raises(SystemExit) as e: + main_module.main() + + assert e.value.code == 0 + + out, err = capfd.readouterr() + assert f"Clearing cache directory: {fake_cache_dir}" in out + assert "Cache cleared successfully." in out + assert not fake_cache_dir.exists() + + @pytest.mark.parametrize("method", ["unset", "command_line", "env_var"]) def test_enable(method, intercepted_build_args, monkeypatch): monkeypatch.delenv("CIBW_ENABLE", raising=False) From 810c7fa37442f8a96c04f163563baa7b4ff523c7 Mon Sep 17 00:00:00 2001 From: Agriya Khetarpal <74401230+agriyakhetarpal@users.noreply.github.com> Date: Mon, 7 Jul 2025 14:03:29 +0530 Subject: [PATCH 05/19] Ad test to run cache cleaning when none exists --- unit_test/main_tests/main_options_test.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/unit_test/main_tests/main_options_test.py b/unit_test/main_tests/main_options_test.py index 1fbbd0df8..be9b64010 100644 --- a/unit_test/main_tests/main_options_test.py +++ b/unit_test/main_tests/main_options_test.py @@ -448,6 +448,22 @@ def test_clean_cache_when_cache_exists(tmp_path, monkeypatch, capfd): assert not fake_cache_dir.exists() +def test_clean_cache_when_cache_does_not_exist(tmp_path, monkeypatch, capfd): + monkeypatch.undo() + fake_cache_dir = (tmp_path / "nonexistent_cache").resolve() + monkeypatch.setattr(main_module, "CIBW_CACHE_PATH", fake_cache_dir) + + monkeypatch.setattr(sys, "argv", ["cibuildwheel", "--clean-cache"]) + + with pytest.raises(SystemExit) as e: + main_module.main() + + assert e.value.code == 0 + + out, err = capfd.readouterr() + assert f"Cache directory does not exist: {fake_cache_dir}" in out + + @pytest.mark.parametrize("method", ["unset", "command_line", "env_var"]) def test_enable(method, intercepted_build_args, monkeypatch): monkeypatch.delenv("CIBW_ENABLE", raising=False) From 0687cc1a590a150baefa83c1d2c7ffb3b8345d38 Mon Sep 17 00:00:00 2001 From: Agriya Khetarpal <74401230+agriyakhetarpal@users.noreply.github.com> Date: Mon, 7 Jul 2025 14:03:43 +0530 Subject: [PATCH 06/19] Add test to run cache cleaning on unwritable path --- unit_test/main_tests/main_options_test.py | 25 +++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/unit_test/main_tests/main_options_test.py b/unit_test/main_tests/main_options_test.py index be9b64010..a8a6fb722 100644 --- a/unit_test/main_tests/main_options_test.py +++ b/unit_test/main_tests/main_options_test.py @@ -464,6 +464,31 @@ def test_clean_cache_when_cache_does_not_exist(tmp_path, monkeypatch, capfd): assert f"Cache directory does not exist: {fake_cache_dir}" in out +def test_clean_cache_with_error(tmp_path, monkeypatch, capfd): + monkeypatch.undo() + fake_cache_dir = (tmp_path / "cibw_cache").resolve() + monkeypatch.setattr(main_module, "CIBW_CACHE_PATH", fake_cache_dir) + + fake_cache_dir.mkdir(parents=True, exist_ok=True) + assert fake_cache_dir.exists() + + monkeypatch.setattr(sys, "argv", ["cibuildwheel", "--clean-cache"]) + + def fake_rmtree(path): # noqa: ARG001 + raise OSError(errno.EACCES, "Permission denied") + + monkeypatch.setattr(shutil, "rmtree", fake_rmtree) + + with pytest.raises(SystemExit) as e: + main_module.main() + + assert e.value.code == 1 + + out, err = capfd.readouterr() + assert f"Clearing cache directory: {fake_cache_dir}" in out + assert "Error clearing cache:" in err + + @pytest.mark.parametrize("method", ["unset", "command_line", "env_var"]) def test_enable(method, intercepted_build_args, monkeypatch): monkeypatch.delenv("CIBW_ENABLE", raising=False) From 5196729ad95f6028bb1f91bba0d6d2de37316a7d Mon Sep 17 00:00:00 2001 From: Agriya Khetarpal <74401230+agriyakhetarpal@users.noreply.github.com> Date: Mon, 7 Jul 2025 14:03:52 +0530 Subject: [PATCH 07/19] Use `main()` instead --- unit_test/main_tests/main_options_test.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/unit_test/main_tests/main_options_test.py b/unit_test/main_tests/main_options_test.py index a8a6fb722..d8a5902ff 100644 --- a/unit_test/main_tests/main_options_test.py +++ b/unit_test/main_tests/main_options_test.py @@ -438,7 +438,7 @@ def test_clean_cache_when_cache_exists(tmp_path, monkeypatch, capfd): monkeypatch.setattr(sys, "argv", ["cibuildwheel", "--clean-cache"]) with pytest.raises(SystemExit) as e: - main_module.main() + main() assert e.value.code == 0 @@ -456,7 +456,7 @@ def test_clean_cache_when_cache_does_not_exist(tmp_path, monkeypatch, capfd): monkeypatch.setattr(sys, "argv", ["cibuildwheel", "--clean-cache"]) with pytest.raises(SystemExit) as e: - main_module.main() + main() assert e.value.code == 0 @@ -480,7 +480,7 @@ def fake_rmtree(path): # noqa: ARG001 monkeypatch.setattr(shutil, "rmtree", fake_rmtree) with pytest.raises(SystemExit) as e: - main_module.main() + main() assert e.value.code == 1 From 59a085d3a5bf7c828ce54911e23c4b9584ef1675 Mon Sep 17 00:00:00 2001 From: Henry Schreiner Date: Sun, 13 Jul 2025 22:48:36 -0700 Subject: [PATCH 08/19] tests: move command tests Signed-off-by: Henry Schreiner --- unit_test/main_commands_test.py | 70 +++++++++++++++++++++++ unit_test/main_tests/main_options_test.py | 68 ---------------------- 2 files changed, 70 insertions(+), 68 deletions(-) create mode 100644 unit_test/main_commands_test.py diff --git a/unit_test/main_commands_test.py b/unit_test/main_commands_test.py new file mode 100644 index 000000000..c247e6505 --- /dev/null +++ b/unit_test/main_commands_test.py @@ -0,0 +1,70 @@ +import errno +import shutil +import sys + +import pytest + +import cibuildwheel.__main__ as main_module +from cibuildwheel.__main__ import main + + +def test_clean_cache_when_cache_exists(tmp_path, monkeypatch, capfd): + fake_cache_dir = (tmp_path / "cibw_cache").resolve() + monkeypatch.setattr(main_module, "CIBW_CACHE_PATH", fake_cache_dir) + + fake_cache_dir.mkdir(parents=True, exist_ok=True) + assert fake_cache_dir.exists() + + dummy_file = fake_cache_dir / "dummy.txt" + dummy_file.write_text("hello") + + monkeypatch.setattr(sys, "argv", ["cibuildwheel", "--clean-cache"]) + + with pytest.raises(SystemExit) as e: + main() + + assert e.value.code == 0 + + out, err = capfd.readouterr() + assert f"Clearing cache directory: {fake_cache_dir}" in out + assert "Cache cleared successfully." in out + assert not fake_cache_dir.exists() + + +def test_clean_cache_when_cache_does_not_exist(tmp_path, monkeypatch, capfd): + fake_cache_dir = (tmp_path / "nonexistent_cache").resolve() + monkeypatch.setattr(main_module, "CIBW_CACHE_PATH", fake_cache_dir) + + monkeypatch.setattr(sys, "argv", ["cibuildwheel", "--clean-cache"]) + + with pytest.raises(SystemExit) as e: + main() + + assert e.value.code == 0 + + out, err = capfd.readouterr() + assert f"Cache directory does not exist: {fake_cache_dir}" in out + + +def test_clean_cache_with_error(tmp_path, monkeypatch, capfd): + fake_cache_dir = (tmp_path / "cibw_cache").resolve() + monkeypatch.setattr(main_module, "CIBW_CACHE_PATH", fake_cache_dir) + + fake_cache_dir.mkdir(parents=True, exist_ok=True) + assert fake_cache_dir.exists() + + monkeypatch.setattr(sys, "argv", ["cibuildwheel", "--clean-cache"]) + + def fake_rmtree(path): # noqa: ARG001 + raise OSError(errno.EACCES, "Permission denied") + + monkeypatch.setattr(shutil, "rmtree", fake_rmtree) + + with pytest.raises(SystemExit) as e: + main() + + assert e.value.code == 1 + + out, err = capfd.readouterr() + assert f"Clearing cache directory: {fake_cache_dir}" in out + assert "Error clearing cache:" in err diff --git a/unit_test/main_tests/main_options_test.py b/unit_test/main_tests/main_options_test.py index d8a5902ff..731d10adc 100644 --- a/unit_test/main_tests/main_options_test.py +++ b/unit_test/main_tests/main_options_test.py @@ -1,5 +1,3 @@ -import errno -import shutil import sys import tomllib from fnmatch import fnmatch @@ -7,7 +5,6 @@ import pytest -import cibuildwheel.__main__ as main_module from cibuildwheel.__main__ import main from cibuildwheel.environment import ParsedEnvironment from cibuildwheel.frontend import _split_config_settings @@ -424,71 +421,6 @@ def test_debug_traceback(monkeypatch, method, capfd): assert "Traceback (most recent call last)" in err -def test_clean_cache_when_cache_exists(tmp_path, monkeypatch, capfd): - monkeypatch.undo() - fake_cache_dir = (tmp_path / "cibw_cache").resolve() - monkeypatch.setattr(main_module, "CIBW_CACHE_PATH", fake_cache_dir) - - fake_cache_dir.mkdir(parents=True, exist_ok=True) - assert fake_cache_dir.exists() - - dummy_file = fake_cache_dir / "dummy.txt" - dummy_file.write_text("hello") - - monkeypatch.setattr(sys, "argv", ["cibuildwheel", "--clean-cache"]) - - with pytest.raises(SystemExit) as e: - main() - - assert e.value.code == 0 - - out, err = capfd.readouterr() - assert f"Clearing cache directory: {fake_cache_dir}" in out - assert "Cache cleared successfully." in out - assert not fake_cache_dir.exists() - - -def test_clean_cache_when_cache_does_not_exist(tmp_path, monkeypatch, capfd): - monkeypatch.undo() - fake_cache_dir = (tmp_path / "nonexistent_cache").resolve() - monkeypatch.setattr(main_module, "CIBW_CACHE_PATH", fake_cache_dir) - - monkeypatch.setattr(sys, "argv", ["cibuildwheel", "--clean-cache"]) - - with pytest.raises(SystemExit) as e: - main() - - assert e.value.code == 0 - - out, err = capfd.readouterr() - assert f"Cache directory does not exist: {fake_cache_dir}" in out - - -def test_clean_cache_with_error(tmp_path, monkeypatch, capfd): - monkeypatch.undo() - fake_cache_dir = (tmp_path / "cibw_cache").resolve() - monkeypatch.setattr(main_module, "CIBW_CACHE_PATH", fake_cache_dir) - - fake_cache_dir.mkdir(parents=True, exist_ok=True) - assert fake_cache_dir.exists() - - monkeypatch.setattr(sys, "argv", ["cibuildwheel", "--clean-cache"]) - - def fake_rmtree(path): # noqa: ARG001 - raise OSError(errno.EACCES, "Permission denied") - - monkeypatch.setattr(shutil, "rmtree", fake_rmtree) - - with pytest.raises(SystemExit) as e: - main() - - assert e.value.code == 1 - - out, err = capfd.readouterr() - assert f"Clearing cache directory: {fake_cache_dir}" in out - assert "Error clearing cache:" in err - - @pytest.mark.parametrize("method", ["unset", "command_line", "env_var"]) def test_enable(method, intercepted_build_args, monkeypatch): monkeypatch.delenv("CIBW_ENABLE", raising=False) From 3e5a78cc3141b231ba58b9b9ba2a85a8727beab0 Mon Sep 17 00:00:00 2001 From: Agriya Khetarpal <74401230+agriyakhetarpal@users.noreply.github.com> Date: Sun, 17 Aug 2025 23:04:29 +0530 Subject: [PATCH 09/19] Create cibuildwheel cache sentinel file --- cibuildwheel/__main__.py | 15 ++++++++++++++- cibuildwheel/util/file.py | 14 ++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/cibuildwheel/__main__.py b/cibuildwheel/__main__.py index a0e28d216..8909f964c 100644 --- a/cibuildwheel/__main__.py +++ b/cibuildwheel/__main__.py @@ -25,7 +25,7 @@ from cibuildwheel.platforms import ALL_PLATFORM_MODULES, get_build_identifiers from cibuildwheel.selector import BuildSelector, EnableGroup, selector_matches from cibuildwheel.typing import PLATFORMS, PlatformName -from cibuildwheel.util.file import CIBW_CACHE_PATH +from cibuildwheel.util.file import CIBW_CACHE_PATH, ensure_cache_sentinel from cibuildwheel.util.helpers import strtobool @@ -205,6 +205,18 @@ def main_inner(global_options: GlobalOptions) -> None: if args.clean_cache: if CIBW_CACHE_PATH.exists(): + sentinel_file = CIBW_CACHE_PATH / ".cibuildwheel_cached" + if not sentinel_file.exists(): + print( + f"Error: {CIBW_CACHE_PATH} does not appear to be a cibuildwheel cache directory.", + file=sys.stderr, + ) + print( + "Only directories with a .cibuildwheel_cached sentinel file can be cleaned.", + file=sys.stderr, + ) + sys.exit(1) + print(f"Clearing cache directory: {CIBW_CACHE_PATH}") try: shutil.rmtree(CIBW_CACHE_PATH) @@ -379,6 +391,7 @@ def build_in_directory(args: CommandLineArguments) -> None: # create the cache dir before it gets printed & builds performed CIBW_CACHE_PATH.mkdir(parents=True, exist_ok=True) + ensure_cache_sentinel(CIBW_CACHE_PATH) print_preamble(platform=platform, options=options, identifiers=identifiers) diff --git a/cibuildwheel/util/file.py b/cibuildwheel/util/file.py index c0fdc2c19..bd4a62f02 100644 --- a/cibuildwheel/util/file.py +++ b/cibuildwheel/util/file.py @@ -20,6 +20,20 @@ ).resolve() +def ensure_cache_sentinel(cache_path: Path) -> None: + """Create a sentinel file to mark a cibuildwheel cache directory. + + We help prevent accidental deletion of non-cache directories with this. + + Args: + cache_path: The cache directory path where the sentinel should be created + """ + if cache_path.exists(): + sentinel_file = cache_path / ".cibuildwheel_cached" + if not sentinel_file.exists(): + sentinel_file.write_text("# Created by cibuildwheel automatically\n") + + def download(url: str, dest: Path) -> None: print(f"+ Download {url} to {dest}") dest_dir = dest.parent From 05b8a1dc40d33f007e519ae95765750f0a828aa7 Mon Sep 17 00:00:00 2001 From: Agriya Khetarpal <74401230+agriyakhetarpal@users.noreply.github.com> Date: Sun, 17 Aug 2025 23:05:01 +0530 Subject: [PATCH 10/19] Add sentinel to existing tests + add new test --- unit_test/main_commands_test.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/unit_test/main_commands_test.py b/unit_test/main_commands_test.py index c247e6505..6bfff897b 100644 --- a/unit_test/main_commands_test.py +++ b/unit_test/main_commands_test.py @@ -15,6 +15,9 @@ def test_clean_cache_when_cache_exists(tmp_path, monkeypatch, capfd): fake_cache_dir.mkdir(parents=True, exist_ok=True) assert fake_cache_dir.exists() + cibw_sentinel = fake_cache_dir / ".cibuildwheel_cached" + cibw_sentinel.write_text("# Created by cibuildwheel automatically", encoding="utf-8") + dummy_file = fake_cache_dir / "dummy.txt" dummy_file.write_text("hello") @@ -53,6 +56,9 @@ def test_clean_cache_with_error(tmp_path, monkeypatch, capfd): fake_cache_dir.mkdir(parents=True, exist_ok=True) assert fake_cache_dir.exists() + cibw_sentinel = fake_cache_dir / ".cibuildwheel_cached" + cibw_sentinel.write_text("# Created by cibuildwheel automatically\n") + monkeypatch.setattr(sys, "argv", ["cibuildwheel", "--clean-cache"]) def fake_rmtree(path): # noqa: ARG001 @@ -68,3 +74,21 @@ def fake_rmtree(path): # noqa: ARG001 out, err = capfd.readouterr() assert f"Clearing cache directory: {fake_cache_dir}" in out assert "Error clearing cache:" in err + + +def test_clean_cache_without_sentinel(tmp_path, monkeypatch, capfd): + fake_cache_dir = (tmp_path / "not_a_cache").resolve() + monkeypatch.setattr(main_module, "CIBW_CACHE_PATH", fake_cache_dir) + + fake_cache_dir.mkdir(parents=True, exist_ok=True) + + monkeypatch.setattr(sys, "argv", ["cibuildwheel", "--clean-cache"]) + + with pytest.raises(SystemExit) as e: + main() + + assert e.value.code == 1 + + out, err = capfd.readouterr() + assert "does not appear to be a cibuildwheel cache directory" in err + assert fake_cache_dir.exists() From 1a267b683207056646ff22a7b8abb6c8fdf3f70d Mon Sep 17 00:00:00 2001 From: Agriya Khetarpal <74401230+agriyakhetarpal@users.noreply.github.com> Date: Mon, 18 Aug 2025 21:22:48 +0530 Subject: [PATCH 11/19] Add signature and switch to `CACHEDIR.TAG` Co-authored-by: Henry Schreiner --- cibuildwheel/__main__.py | 6 ++---- cibuildwheel/util/file.py | 5 +++-- unit_test/main_commands_test.py | 10 ++++++---- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/cibuildwheel/__main__.py b/cibuildwheel/__main__.py index e9e7d72f8..97fe9bd7d 100644 --- a/cibuildwheel/__main__.py +++ b/cibuildwheel/__main__.py @@ -208,10 +208,8 @@ def main_inner(global_options: GlobalOptions) -> None: if not sentinel_file.exists(): print( f"Error: {CIBW_CACHE_PATH} does not appear to be a cibuildwheel cache directory.", - file=sys.stderr, - ) - print( - "Only directories with a .cibuildwheel_cached sentinel file can be cleaned.", + "Only directories with a CACHEDIR.TAG sentinel file can be cleaned.", + sep="\n", file=sys.stderr, ) sys.exit(1) diff --git a/cibuildwheel/util/file.py b/cibuildwheel/util/file.py index bd4a62f02..e614a80b1 100644 --- a/cibuildwheel/util/file.py +++ b/cibuildwheel/util/file.py @@ -29,9 +29,10 @@ def ensure_cache_sentinel(cache_path: Path) -> None: cache_path: The cache directory path where the sentinel should be created """ if cache_path.exists(): - sentinel_file = cache_path / ".cibuildwheel_cached" + sentinel_file = cache_path / "CACHEDIR.TAG" if not sentinel_file.exists(): - sentinel_file.write_text("# Created by cibuildwheel automatically\n") + sentinel_file.write_text("Signature: 8a477f597d28d172789f06886806bc55\n" + "# This file is a cache directory tag created by cibuildwheel.\n") def download(url: str, dest: Path) -> None: diff --git a/unit_test/main_commands_test.py b/unit_test/main_commands_test.py index 6bfff897b..0c12adb3a 100644 --- a/unit_test/main_commands_test.py +++ b/unit_test/main_commands_test.py @@ -15,8 +15,9 @@ def test_clean_cache_when_cache_exists(tmp_path, monkeypatch, capfd): fake_cache_dir.mkdir(parents=True, exist_ok=True) assert fake_cache_dir.exists() - cibw_sentinel = fake_cache_dir / ".cibuildwheel_cached" - cibw_sentinel.write_text("# Created by cibuildwheel automatically", encoding="utf-8") + cibw_sentinel = fake_cache_dir / "CACHEDIR.TAG" + cibw_sentinel.write_text("Signature: 8a477f597d28d172789f06886806bc55\n" + "# This file is a cache directory tag created by cibuildwheel.\n", encoding="utf-8") dummy_file = fake_cache_dir / "dummy.txt" dummy_file.write_text("hello") @@ -56,8 +57,9 @@ def test_clean_cache_with_error(tmp_path, monkeypatch, capfd): fake_cache_dir.mkdir(parents=True, exist_ok=True) assert fake_cache_dir.exists() - cibw_sentinel = fake_cache_dir / ".cibuildwheel_cached" - cibw_sentinel.write_text("# Created by cibuildwheel automatically\n") + cibw_sentinel = fake_cache_dir / "CACHEDIR.TAG" + cibw_sentinel.write_text("Signature: 8a477f597d28d172789f06886806bc55\n" + "# This file is a cache directory tag created by cibuildwheel.\n") monkeypatch.setattr(sys, "argv", ["cibuildwheel", "--clean-cache"]) From 47e870b458d3ad5b071343d76c5b7924463cf014 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 18 Aug 2025 15:53:19 +0000 Subject: [PATCH 12/19] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- cibuildwheel/util/file.py | 6 ++++-- unit_test/main_commands_test.py | 13 +++++++++---- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/cibuildwheel/util/file.py b/cibuildwheel/util/file.py index e614a80b1..442861421 100644 --- a/cibuildwheel/util/file.py +++ b/cibuildwheel/util/file.py @@ -31,8 +31,10 @@ def ensure_cache_sentinel(cache_path: Path) -> None: if cache_path.exists(): sentinel_file = cache_path / "CACHEDIR.TAG" if not sentinel_file.exists(): - sentinel_file.write_text("Signature: 8a477f597d28d172789f06886806bc55\n" - "# This file is a cache directory tag created by cibuildwheel.\n") + sentinel_file.write_text( + "Signature: 8a477f597d28d172789f06886806bc55\n" + "# This file is a cache directory tag created by cibuildwheel.\n" + ) def download(url: str, dest: Path) -> None: diff --git a/unit_test/main_commands_test.py b/unit_test/main_commands_test.py index 0c12adb3a..77c5f7aaf 100644 --- a/unit_test/main_commands_test.py +++ b/unit_test/main_commands_test.py @@ -16,8 +16,11 @@ def test_clean_cache_when_cache_exists(tmp_path, monkeypatch, capfd): assert fake_cache_dir.exists() cibw_sentinel = fake_cache_dir / "CACHEDIR.TAG" - cibw_sentinel.write_text("Signature: 8a477f597d28d172789f06886806bc55\n" - "# This file is a cache directory tag created by cibuildwheel.\n", encoding="utf-8") + cibw_sentinel.write_text( + "Signature: 8a477f597d28d172789f06886806bc55\n" + "# This file is a cache directory tag created by cibuildwheel.\n", + encoding="utf-8", + ) dummy_file = fake_cache_dir / "dummy.txt" dummy_file.write_text("hello") @@ -58,8 +61,10 @@ def test_clean_cache_with_error(tmp_path, monkeypatch, capfd): assert fake_cache_dir.exists() cibw_sentinel = fake_cache_dir / "CACHEDIR.TAG" - cibw_sentinel.write_text("Signature: 8a477f597d28d172789f06886806bc55\n" - "# This file is a cache directory tag created by cibuildwheel.\n") + cibw_sentinel.write_text( + "Signature: 8a477f597d28d172789f06886806bc55\n" + "# This file is a cache directory tag created by cibuildwheel.\n" + ) monkeypatch.setattr(sys, "argv", ["cibuildwheel", "--clean-cache"]) From eb9f33a5420b3cdb83ea3c60811db15746527d47 Mon Sep 17 00:00:00 2001 From: Agriya Khetarpal <74401230+agriyakhetarpal@users.noreply.github.com> Date: Mon, 18 Aug 2025 21:25:34 +0530 Subject: [PATCH 13/19] Another instance to replace with `CACHEDIR.TAG` --- cibuildwheel/__main__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cibuildwheel/__main__.py b/cibuildwheel/__main__.py index 97fe9bd7d..869dd29b5 100644 --- a/cibuildwheel/__main__.py +++ b/cibuildwheel/__main__.py @@ -204,7 +204,7 @@ def main_inner(global_options: GlobalOptions) -> None: if args.clean_cache: if CIBW_CACHE_PATH.exists(): - sentinel_file = CIBW_CACHE_PATH / ".cibuildwheel_cached" + sentinel_file = CIBW_CACHE_PATH / "CACHEDIR.TAG" if not sentinel_file.exists(): print( f"Error: {CIBW_CACHE_PATH} does not appear to be a cibuildwheel cache directory.", From 698c5f7a117bb2def96e36f50c545b0b137426ef Mon Sep 17 00:00:00 2001 From: Agriya Khetarpal <74401230+agriyakhetarpal@users.noreply.github.com> Date: Mon, 18 Aug 2025 21:58:29 +0530 Subject: [PATCH 14/19] Verify `CACHEDIR.TAG` signature --- cibuildwheel/__main__.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/cibuildwheel/__main__.py b/cibuildwheel/__main__.py index 869dd29b5..37510c9d6 100644 --- a/cibuildwheel/__main__.py +++ b/cibuildwheel/__main__.py @@ -214,6 +214,22 @@ def main_inner(global_options: GlobalOptions) -> None: ) sys.exit(1) + # Verify signature to ensure it's a proper cache dir + # See https://bford.info/cachedir/ for more + try: + sentinel_content = sentinel_file.read_text(encoding="utf-8") + if not sentinel_content.startswith("Signature: 8a477f597d28d172789f06886806bc55"): + print( + f"Error: {sentinel_file} does not contain a valid cache directory signature.", + "For safety, only properly tagged cache directories can be cleaned.", + sep="\n", + file=sys.stderr, + ) + sys.exit(1) + except OSError as e: + print(f"Error reading cache directory tag: {e}", file=sys.stderr) + sys.exit(1) + print(f"Clearing cache directory: {CIBW_CACHE_PATH}") try: shutil.rmtree(CIBW_CACHE_PATH) From 8023c76571e1e82f36fc9aeff359c979144b0f00 Mon Sep 17 00:00:00 2001 From: Agriya Khetarpal <74401230+agriyakhetarpal@users.noreply.github.com> Date: Mon, 18 Aug 2025 22:01:35 +0530 Subject: [PATCH 15/19] Simplify code branching and control flow --- cibuildwheel/__main__.py | 73 +++++++++++++++++++++------------------- 1 file changed, 38 insertions(+), 35 deletions(-) diff --git a/cibuildwheel/__main__.py b/cibuildwheel/__main__.py index 37510c9d6..574e5fcba 100644 --- a/cibuildwheel/__main__.py +++ b/cibuildwheel/__main__.py @@ -203,42 +203,45 @@ def main_inner(global_options: GlobalOptions) -> None: global_options.print_traceback_on_error = args.debug_traceback if args.clean_cache: - if CIBW_CACHE_PATH.exists(): - sentinel_file = CIBW_CACHE_PATH / "CACHEDIR.TAG" - if not sentinel_file.exists(): - print( - f"Error: {CIBW_CACHE_PATH} does not appear to be a cibuildwheel cache directory.", - "Only directories with a CACHEDIR.TAG sentinel file can be cleaned.", - sep="\n", - file=sys.stderr, - ) - sys.exit(1) - - # Verify signature to ensure it's a proper cache dir - # See https://bford.info/cachedir/ for more - try: - sentinel_content = sentinel_file.read_text(encoding="utf-8") - if not sentinel_content.startswith("Signature: 8a477f597d28d172789f06886806bc55"): - print( - f"Error: {sentinel_file} does not contain a valid cache directory signature.", - "For safety, only properly tagged cache directories can be cleaned.", - sep="\n", - file=sys.stderr, - ) - sys.exit(1) - except OSError as e: - print(f"Error reading cache directory tag: {e}", file=sys.stderr) - sys.exit(1) - - print(f"Clearing cache directory: {CIBW_CACHE_PATH}") - try: - shutil.rmtree(CIBW_CACHE_PATH) - print("Cache cleared successfully.") - except OSError as e: - print(f"Error clearing cache: {e}", file=sys.stderr) - sys.exit(1) - else: + if not CIBW_CACHE_PATH.exists(): print(f"Cache directory does not exist: {CIBW_CACHE_PATH}") + sys.exit(0) + + sentinel_file = CIBW_CACHE_PATH / "CACHEDIR.TAG" + if not sentinel_file.exists(): + print( + f"Error: {CIBW_CACHE_PATH} does not appear to be a cibuildwheel cache directory.", + "Only directories with a CACHEDIR.TAG sentinel file can be cleaned.", + sep="\n", + file=sys.stderr, + ) + sys.exit(1) + + # Verify signature to ensure it's a proper cache dir + # See https://bford.info/cachedir/ for more + try: + sentinel_content = sentinel_file.read_text(encoding="utf-8") + except OSError as e: + print(f"Error reading cache directory tag: {e}", file=sys.stderr) + sys.exit(1) + + if not sentinel_content.startswith("Signature: 8a477f597d28d172789f06886806bc55"): + print( + f"Error: {sentinel_file} does not contain a valid cache directory signature.", + "For safety, only properly signed cache directories can be cleaned.", + sep="\n", + file=sys.stderr, + ) + sys.exit(1) + + print(f"Clearing cache directory: {CIBW_CACHE_PATH}") + try: + shutil.rmtree(CIBW_CACHE_PATH) + print("Cache cleared successfully.") + except OSError as e: + print(f"Error clearing cache: {e}", file=sys.stderr) + sys.exit(1) + sys.exit(0) args.package_dir = args.package_dir.resolve() From eced16248223a660696bdd4b545ec93029bafed4 Mon Sep 17 00:00:00 2001 From: Agriya Khetarpal <74401230+agriyakhetarpal@users.noreply.github.com> Date: Mon, 18 Aug 2025 22:05:38 +0530 Subject: [PATCH 16/19] Add test --- unit_test/main_commands_test.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/unit_test/main_commands_test.py b/unit_test/main_commands_test.py index 77c5f7aaf..96305f3ae 100644 --- a/unit_test/main_commands_test.py +++ b/unit_test/main_commands_test.py @@ -99,3 +99,24 @@ def test_clean_cache_without_sentinel(tmp_path, monkeypatch, capfd): out, err = capfd.readouterr() assert "does not appear to be a cibuildwheel cache directory" in err assert fake_cache_dir.exists() + + +def test_clean_cache_with_invalid_signature(tmp_path, monkeypatch, capfd): + fake_cache_dir = (tmp_path / "fake_cache").resolve() + monkeypatch.setattr(main_module, "CIBW_CACHE_PATH", fake_cache_dir) + + fake_cache_dir.mkdir(parents=True, exist_ok=True) + + cibw_sentinel = fake_cache_dir / "CACHEDIR.TAG" + cibw_sentinel.write_text("Invalid signature\n# This is not a real cache directory tag") + + monkeypatch.setattr(sys, "argv", ["cibuildwheel", "--clean-cache"]) + + with pytest.raises(SystemExit) as e: + main() + + assert e.value.code == 1 + + out, err = capfd.readouterr() + assert "does not contain a valid cache directory signature" in err + assert fake_cache_dir.exists() From a571ca7d17a4c978155b657bad04d4901ecc74d4 Mon Sep 17 00:00:00 2001 From: Agriya Khetarpal <74401230+agriyakhetarpal@users.noreply.github.com> Date: Tue, 19 Aug 2025 10:22:49 +0530 Subject: [PATCH 17/19] Add link to `CACHEDIR.TAG` specification --- cibuildwheel/util/file.py | 2 ++ unit_test/main_commands_test.py | 7 ++++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/cibuildwheel/util/file.py b/cibuildwheel/util/file.py index 442861421..0e7037d92 100644 --- a/cibuildwheel/util/file.py +++ b/cibuildwheel/util/file.py @@ -34,6 +34,8 @@ def ensure_cache_sentinel(cache_path: Path) -> None: sentinel_file.write_text( "Signature: 8a477f597d28d172789f06886806bc55\n" "# This file is a cache directory tag created by cibuildwheel.\n" + "# For information about cache directory tags, see:\n" + "# https://www.brynosaurus.com/cachedir/", ) diff --git a/unit_test/main_commands_test.py b/unit_test/main_commands_test.py index 96305f3ae..d3d09a7e5 100644 --- a/unit_test/main_commands_test.py +++ b/unit_test/main_commands_test.py @@ -18,7 +18,9 @@ def test_clean_cache_when_cache_exists(tmp_path, monkeypatch, capfd): cibw_sentinel = fake_cache_dir / "CACHEDIR.TAG" cibw_sentinel.write_text( "Signature: 8a477f597d28d172789f06886806bc55\n" - "# This file is a cache directory tag created by cibuildwheel.\n", + "# This file is a cache directory tag created by cibuildwheel.\n" + "# For information about cache directory tags, see:\n" + "# https://www.brynosaurus.com/cachedir/", encoding="utf-8", ) @@ -64,6 +66,9 @@ def test_clean_cache_with_error(tmp_path, monkeypatch, capfd): cibw_sentinel.write_text( "Signature: 8a477f597d28d172789f06886806bc55\n" "# This file is a cache directory tag created by cibuildwheel.\n" + "# For information about cache directory tags, see:\n" + "# https://www.brynosaurus.com/cachedir/", + encoding="utf-8", ) monkeypatch.setattr(sys, "argv", ["cibuildwheel", "--clean-cache"]) From 7e8695c66e6121c8731e60e2c40471e1ddc67766 Mon Sep 17 00:00:00 2001 From: Agriya Khetarpal <74401230+agriyakhetarpal@users.noreply.github.com> Date: Tue, 19 Aug 2025 10:23:07 +0530 Subject: [PATCH 18/19] Write with UTF-8 encoding --- cibuildwheel/util/file.py | 1 + 1 file changed, 1 insertion(+) diff --git a/cibuildwheel/util/file.py b/cibuildwheel/util/file.py index 0e7037d92..a70afef43 100644 --- a/cibuildwheel/util/file.py +++ b/cibuildwheel/util/file.py @@ -36,6 +36,7 @@ def ensure_cache_sentinel(cache_path: Path) -> None: "# This file is a cache directory tag created by cibuildwheel.\n" "# For information about cache directory tags, see:\n" "# https://www.brynosaurus.com/cachedir/", + encoding="utf-8" ) From 18f8bc6aa99b326551df1d11fa3d8ef9d84173ad Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 19 Aug 2025 04:54:21 +0000 Subject: [PATCH 19/19] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- cibuildwheel/util/file.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cibuildwheel/util/file.py b/cibuildwheel/util/file.py index a70afef43..b93a5e50a 100644 --- a/cibuildwheel/util/file.py +++ b/cibuildwheel/util/file.py @@ -36,7 +36,7 @@ def ensure_cache_sentinel(cache_path: Path) -> None: "# This file is a cache directory tag created by cibuildwheel.\n" "# For information about cache directory tags, see:\n" "# https://www.brynosaurus.com/cachedir/", - encoding="utf-8" + encoding="utf-8", )