From 2d0eb3589d78cac4bc111f8146225bf7cea86dab Mon Sep 17 00:00:00 2001 From: David Rebbe Date: Tue, 15 Sep 2026 23:03:52 -0400 Subject: [PATCH] fix: scope Linux dependency build cleanup --- build_libicsneo.py | 9 ++-- tests/test_build_libicsneo.py | 80 +++++++++++++++++++++++++++++++++++ 2 files changed, 86 insertions(+), 3 deletions(-) create mode 100644 tests/test_build_libicsneo.py diff --git a/build_libicsneo.py b/build_libicsneo.py index 93d7b16a..1ef6907f 100644 --- a/build_libicsneo.py +++ b/build_libicsneo.py @@ -30,7 +30,7 @@ # icspb bootstraps protobuf from source at configure time. Keep the # bootstrap OUTSIDE the libicsneo build dir so it survives the per-python -# `git clean` in _build_libicsneo_linux and is reused across all +# build-directory cleanup in _build_libicsneo_linux and is reused across all # cibuildwheel builds in a job (it self-partitions by -, # so sharing one root across archs is safe). Building protobuf once per # arch instead of once per python matters most under QEMU aarch64. @@ -125,8 +125,11 @@ def _cmake_ninja_args(): def _build_libicsneo_linux(): print("Cleaning libicsneo...") - subprocess.check_output(["git", "clean", "-xdf"], cwd="libicsneo") - subprocess.check_output(["mkdir", "-p", "libicsneo/build"]) + # Source archives have no parent Git checkout. Reset only the CMake + # build tree, preserving the source checkout and protobuf bootstrap. + if os.path.exists(LIBICSNEO_BUILD): + shutil.rmtree(LIBICSNEO_BUILD) + os.makedirs(LIBICSNEO_BUILD, exist_ok=True) print("cmake libicsneo...") subprocess.check_output( diff --git a/tests/test_build_libicsneo.py b/tests/test_build_libicsneo.py new file mode 100644 index 00000000..d4cf142c --- /dev/null +++ b/tests/test_build_libicsneo.py @@ -0,0 +1,80 @@ +"""Build orchestration checks without downloads, compilers, or hardware.""" + +import importlib.util +import os +from pathlib import Path +import shutil +import subprocess +import tempfile +import unittest +from unittest import mock + + +class TestLinuxBuild(unittest.TestCase): + def setUp(self): + self.temp = tempfile.TemporaryDirectory() + self.addCleanup(self.temp.cleanup) + self.root = Path(self.temp.name).resolve() + # Model an unpacked source archive: the build script has no parent .git. + script = self.root / "build_libicsneo.py" + shutil.copyfile(Path(__file__).resolve().parents[1] / script.name, script) + previous = Path.cwd() + os.chdir(self.root) + self.addCleanup(os.chdir, previous) + spec = importlib.util.spec_from_file_location("archive_build", script) + self.module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(self.module) + self.build = Path(self.module.LIBICSNEO_BUILD) + self.source = Path(self.module.LIBICSNEO_SOURCE) + self.source.mkdir(parents=True) + subprocess.run(["git", "init", "--quiet", str(self.source)], check=True) + self.real_check_output = subprocess.check_output + + def run_build(self, stale=None): + commands = [] + + def command(args, **kwargs): + if args[0] != "cmake": + return self.real_check_output(args, **kwargs) + self.assertTrue(self.build.is_dir()) + if stale is not None: + self.assertFalse(stale.exists()) + commands.append(args) + return b"" + + with mock.patch.object(self.module.subprocess, "check_output", side_effect=command): + self.module._build_libicsneo_linux() + self.assertEqual(len(commands), 2) + self.assertEqual(Path(commands[0][commands[0].index("-S") + 1]), self.source) + self.assertEqual(commands[0][commands[0].index("-B") + 1], self.module.LIBICSNEO_BUILD) + self.assertEqual(commands[1][1:3], ["--build", self.module.LIBICSNEO_BUILD]) + self.assertFalse((self.root / "libicsneo" / "build").exists()) + + def test_fresh_archive_build(self): + self.run_build() + + def test_rebuild_cleans_only_build_directory(self): + self.build.mkdir() + stale = self.build / "CMakeCache.txt" + stale.write_text("stale build environment") + preserved = [ + self.source / "untracked-source.txt", + Path(self.module.LIBICSNEO_INSTALL) / "installed.txt", + Path(self.module.ICSPB_BOOTSTRAP_DIR) / "protobuf.txt", + self.root / "libicsneo" / "other-version" / "keep.txt", + ] + for path in preserved: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text("keep") + self.run_build(stale) + for path in preserved: + self.assertEqual(path.read_text(), "keep") + self.assertTrue((self.source / ".git").is_dir()) + + def test_cleanup_failure_stops_before_cmake(self): + self.build.mkdir() + with mock.patch.object(self.module.shutil, "rmtree", side_effect=PermissionError("busy")): + with mock.patch.object(self.module.subprocess, "check_output") as command: + with self.assertRaises(PermissionError): + self.module._build_libicsneo_linux() + command.assert_not_called()