diff --git a/hatch_cpp/tests/test_vcpkg_ref.py b/hatch_cpp/tests/test_vcpkg_ref.py index 02d862e..f300f31 100644 --- a/hatch_cpp/tests/test_vcpkg_ref.py +++ b/hatch_cpp/tests/test_vcpkg_ref.py @@ -156,14 +156,66 @@ def test_generate_without_ref(self, tmp_path, monkeypatch): def test_generate_skips_clone_when_vcpkg_root_exists(self, tmp_path, monkeypatch): monkeypatch.chdir(tmp_path) self._make_vcpkg_env(tmp_path) - (tmp_path / "vcpkg").mkdir() + vcpkg_root = tmp_path / "vcpkg" + vcpkg_root.mkdir() + # Existing bootstrap script and executable mean no clone/bootstrap is needed. + (vcpkg_root / "bootstrap-vcpkg.sh").write_text("#!/bin/sh\nexit 0\n") + (vcpkg_root / "vcpkg").write_text("#!/bin/sh\nexit 0\n") cfg = HatchCppVcpkgConfiguration(vcpkg_ref="2024.01.12") + monkeypatch.setattr(cfg, "_is_vcpkg_working", lambda: True) commands = cfg.generate(None) - # When vcpkg_root already exists, no clone or checkout happens + # When vcpkg_root already exists with a working executable, no clone or bootstrap happens. assert not any("git clone" in cmd for cmd in commands) assert not any("checkout" in cmd for cmd in commands) + assert not any("bootstrap-vcpkg" in cmd for cmd in commands) + assert any("vcpkg" in cmd and "install" in cmd for cmd in commands) + + def test_generate_reclones_when_vcpkg_root_exists_but_empty(self, tmp_path, monkeypatch): + monkeypatch.chdir(tmp_path) + self._make_vcpkg_env(tmp_path) + (tmp_path / "vcpkg").mkdir() + + cfg = HatchCppVcpkgConfiguration(vcpkg_ref="2024.01.12") + commands = cfg.generate(None) + + assert any(cmd.startswith('rm -rf "vcpkg"') for cmd in commands) + assert any("git clone" in cmd for cmd in commands) + assert any("checkout 2024.01.12" in cmd for cmd in commands) + assert any("bootstrap-vcpkg" in cmd for cmd in commands) + assert any("vcpkg" in cmd and "install" in cmd for cmd in commands) + + def test_generate_bootstraps_when_vcpkg_executable_missing(self, tmp_path, monkeypatch): + monkeypatch.chdir(tmp_path) + self._make_vcpkg_env(tmp_path) + vcpkg_root = tmp_path / "vcpkg" + vcpkg_root.mkdir() + (vcpkg_root / "bootstrap-vcpkg.sh").write_text("#!/bin/sh\nexit 0\n") + + cfg = HatchCppVcpkgConfiguration(vcpkg_ref="2024.01.12") + commands = cfg.generate(None) + + assert not any("git clone" in cmd for cmd in commands) + assert any("bootstrap-vcpkg" in cmd for cmd in commands) + assert any("vcpkg" in cmd and "install" in cmd for cmd in commands) + + def test_generate_reclones_when_vcpkg_exists_but_not_working(self, tmp_path, monkeypatch): + monkeypatch.chdir(tmp_path) + self._make_vcpkg_env(tmp_path) + vcpkg_root = tmp_path / "vcpkg" + vcpkg_root.mkdir() + (vcpkg_root / "bootstrap-vcpkg.sh").write_text("#!/bin/sh\nexit 0\n") + (vcpkg_root / "vcpkg").write_text("#!/bin/sh\nexit 1\n") + + cfg = HatchCppVcpkgConfiguration(vcpkg_ref="2024.01.12") + monkeypatch.setattr(cfg, "_is_vcpkg_working", lambda: False) + commands = cfg.generate(None) + + assert any(cmd.startswith('rm -rf "vcpkg"') for cmd in commands) + assert any("git clone" in cmd for cmd in commands) + assert any("checkout 2024.01.12" in cmd for cmd in commands) + assert any("bootstrap-vcpkg" in cmd for cmd in commands) assert any("vcpkg" in cmd and "install" in cmd for cmd in commands) def test_generate_no_vcpkg_json(self, tmp_path, monkeypatch): diff --git a/hatch_cpp/toolchains/vcpkg.py b/hatch_cpp/toolchains/vcpkg.py index a93be72..530df98 100644 --- a/hatch_cpp/toolchains/vcpkg.py +++ b/hatch_cpp/toolchains/vcpkg.py @@ -1,6 +1,7 @@ from __future__ import annotations import configparser +import subprocess from pathlib import Path from platform import machine as platform_machine from sys import platform as sys_platform @@ -78,6 +79,39 @@ def _resolve_vcpkg_ref(self) -> Optional[str]: return self.vcpkg_ref return _read_vcpkg_ref_from_gitmodules(self.vcpkg_root) + def _bootstrap_script_path(self) -> Path: + return self.vcpkg_root / ("bootstrap-vcpkg.bat" if sys_platform == "win32" else "bootstrap-vcpkg.sh") + + def _vcpkg_executable_path(self) -> Path: + if sys_platform == "win32": + return self.vcpkg_root / "vcpkg.exe" + return self.vcpkg_root / "vcpkg" + + def _delete_dir_command(self, path: Path) -> str: + if sys_platform == "win32": + return f'rmdir /s /q "{path}"' + return f'rm -rf "{path}"' + + def _is_vcpkg_working(self) -> bool: + vcpkg_executable = self._vcpkg_executable_path() + if not vcpkg_executable.exists(): + return False + try: + result = subprocess.run([str(vcpkg_executable), "version"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=False) + return result.returncode == 0 + except OSError: + return False + + def _clone_checkout_bootstrap_commands(self) -> list[str]: + commands = [f"git clone {self.vcpkg_repo} {self.vcpkg_root}"] + + ref = self._resolve_vcpkg_ref() + if ref is not None: + commands.append(f"git -C {self.vcpkg_root} checkout {ref}") + + commands.append(f"./{self._bootstrap_script_path()}") + return commands + def generate(self, config): commands = [] @@ -87,14 +121,24 @@ def generate(self, config): raise ValueError(f"Could not determine vcpkg triplet for platform {sys_platform} and architecture {platform_machine()}") if self.vcpkg and Path(self.vcpkg).exists(): - if not Path(self.vcpkg_root).exists(): - commands.append(f"git clone {self.vcpkg_repo} {self.vcpkg_root}") - - ref = self._resolve_vcpkg_ref() - if ref is not None: - commands.append(f"git -C {self.vcpkg_root} checkout {ref}") + vcpkg_root = Path(self.vcpkg_root) + bootstrap_script = self._bootstrap_script_path() + + if not vcpkg_root.exists(): + commands.extend(self._clone_checkout_bootstrap_commands()) + else: + is_empty_dir = vcpkg_root.is_dir() and not any(vcpkg_root.iterdir()) + if is_empty_dir: + commands.append(self._delete_dir_command(vcpkg_root)) + commands.extend(self._clone_checkout_bootstrap_commands()) + else: + vcpkg_executable = self._vcpkg_executable_path() + if not vcpkg_executable.exists(): + commands.append(f"./{bootstrap_script}") + elif not self._is_vcpkg_working(): + commands.append(self._delete_dir_command(vcpkg_root)) + commands.extend(self._clone_checkout_bootstrap_commands()) - commands.append(f"./{self.vcpkg_root / 'bootstrap-vcpkg.sh' if sys_platform != 'win32' else self.vcpkg_root / 'bootstrap-vcpkg.bat'}") commands.append(f"./{self.vcpkg_root / 'vcpkg'} install --triplet {self.vcpkg_triplet}") return commands