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 <henryschreineriii@gmail.com> * 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 <HenrySchreinerIII@gmail.com> * [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 <henryschreineriii@gmail.com> Co-authored-by: Henry Schreiner <henryschreineriii@gmail.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
This commit is contained in:
co-authored by
Henry Schreiner
pre-commit-ci[bot]
parent
1c1ba8a22e
commit
afe486c21f
@@ -23,7 +23,7 @@ from cibuildwheel.options import CommandLineArguments, Options, compute_options
|
|||||||
from cibuildwheel.platforms import ALL_PLATFORM_MODULES, get_build_identifiers, native_platform
|
from cibuildwheel.platforms import ALL_PLATFORM_MODULES, get_build_identifiers, native_platform
|
||||||
from cibuildwheel.selector import BuildSelector, EnableGroup, selector_matches
|
from cibuildwheel.selector import BuildSelector, EnableGroup, selector_matches
|
||||||
from cibuildwheel.typing import PLATFORMS, PlatformName
|
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
|
from cibuildwheel.util.helpers import strtobool
|
||||||
from cibuildwheel.util.resources import read_all_configs
|
from cibuildwheel.util.resources import read_all_configs
|
||||||
|
|
||||||
@@ -179,6 +179,12 @@ def main_inner(global_options: GlobalOptions) -> None:
|
|||||||
help="Print the build identifiers matched by the current invocation and exit.",
|
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(
|
parser.add_argument(
|
||||||
"--allow-empty",
|
"--allow-empty",
|
||||||
action="store_true",
|
action="store_true",
|
||||||
@@ -196,6 +202,48 @@ def main_inner(global_options: GlobalOptions) -> None:
|
|||||||
|
|
||||||
global_options.print_traceback_on_error = args.debug_traceback
|
global_options.print_traceback_on_error = args.debug_traceback
|
||||||
|
|
||||||
|
if args.clean_cache:
|
||||||
|
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()
|
args.package_dir = args.package_dir.resolve()
|
||||||
|
|
||||||
# This are always relative to the base directory, even in SDist builds
|
# This are always relative to the base directory, even in SDist builds
|
||||||
@@ -309,6 +357,7 @@ def build_in_directory(args: CommandLineArguments) -> None:
|
|||||||
|
|
||||||
# create the cache dir before it gets printed & builds performed
|
# create the cache dir before it gets printed & builds performed
|
||||||
CIBW_CACHE_PATH.mkdir(parents=True, exist_ok=True)
|
CIBW_CACHE_PATH.mkdir(parents=True, exist_ok=True)
|
||||||
|
ensure_cache_sentinel(CIBW_CACHE_PATH)
|
||||||
|
|
||||||
print_preamble(platform=platform, options=options, identifiers=identifiers)
|
print_preamble(platform=platform, options=options, identifiers=identifiers)
|
||||||
|
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ class CommandLineArguments:
|
|||||||
allow_empty: bool
|
allow_empty: bool
|
||||||
debug_traceback: bool
|
debug_traceback: bool
|
||||||
enable: list[str]
|
enable: list[str]
|
||||||
|
clean_cache: bool
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def defaults(cls) -> Self:
|
def defaults(cls) -> Self:
|
||||||
@@ -77,6 +78,7 @@ class CommandLineArguments:
|
|||||||
print_build_identifiers=False,
|
print_build_identifiers=False,
|
||||||
debug_traceback=False,
|
debug_traceback=False,
|
||||||
enable=[],
|
enable=[],
|
||||||
|
clean_cache=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,26 @@ CIBW_CACHE_PATH: Final[Path] = Path(
|
|||||||
).resolve()
|
).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 / "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"
|
||||||
|
"# For information about cache directory tags, see:\n"
|
||||||
|
"# https://www.brynosaurus.com/cachedir/",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def download(url: str, dest: Path) -> None:
|
def download(url: str, dest: Path) -> None:
|
||||||
print(f"+ Download {url} to {dest}")
|
print(f"+ Download {url} to {dest}")
|
||||||
dest_dir = dest.parent
|
dest_dir = dest.parent
|
||||||
|
|||||||
@@ -0,0 +1,127 @@
|
|||||||
|
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()
|
||||||
|
|
||||||
|
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"
|
||||||
|
"# For information about cache directory tags, see:\n"
|
||||||
|
"# https://www.brynosaurus.com/cachedir/",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
|
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"
|
||||||
|
"# For information about cache directory tags, see:\n"
|
||||||
|
"# https://www.brynosaurus.com/cachedir/",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
|
|
||||||
|
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()
|
||||||
Reference in New Issue
Block a user