Skip to content

Commit 26381ee

Browse files
authored
Merge pull request #89 from Point72/tkp/vcp
Enable specific vcpkg hash checkout
2 parents 46431f8 + b4882a2 commit 26381ee

File tree

2 files changed

+212
-3
lines changed

2 files changed

+212
-3
lines changed

hatch_cpp/tests/test_vcpkg_ref.py

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
"""Tests for vcpkg ref/branch checkout support."""
2+
3+
from __future__ import annotations
4+
5+
from pathlib import Path
6+
7+
from hatch_cpp.toolchains.vcpkg import (
8+
HatchCppVcpkgConfiguration,
9+
_read_vcpkg_ref_from_gitmodules,
10+
)
11+
12+
13+
class TestReadVcpkgRefFromGitmodules:
14+
"""Tests for the _read_vcpkg_ref_from_gitmodules helper."""
15+
16+
def test_no_gitmodules_file(self, tmp_path, monkeypatch):
17+
monkeypatch.chdir(tmp_path)
18+
assert _read_vcpkg_ref_from_gitmodules(Path("vcpkg")) is None
19+
20+
def test_gitmodules_without_vcpkg_submodule(self, tmp_path, monkeypatch):
21+
monkeypatch.chdir(tmp_path)
22+
(tmp_path / ".gitmodules").write_text('[submodule "other"]\n\tpath = other\n\turl = https://github.com/example/other.git\n')
23+
assert _read_vcpkg_ref_from_gitmodules(Path("vcpkg")) is None
24+
25+
def test_gitmodules_vcpkg_without_branch(self, tmp_path, monkeypatch):
26+
monkeypatch.chdir(tmp_path)
27+
(tmp_path / ".gitmodules").write_text('[submodule "vcpkg"]\n\tpath = vcpkg\n\turl = https://github.com/microsoft/vcpkg.git\n')
28+
assert _read_vcpkg_ref_from_gitmodules(Path("vcpkg")) is None
29+
30+
def test_gitmodules_vcpkg_with_branch(self, tmp_path, monkeypatch):
31+
monkeypatch.chdir(tmp_path)
32+
(tmp_path / ".gitmodules").write_text(
33+
'[submodule "vcpkg"]\n\tpath = vcpkg\n\turl = https://github.com/microsoft/vcpkg.git\n\tbranch = 2024.01.12\n'
34+
)
35+
assert _read_vcpkg_ref_from_gitmodules(Path("vcpkg")) == "2024.01.12"
36+
37+
def test_gitmodules_vcpkg_with_commit_sha_branch(self, tmp_path, monkeypatch):
38+
monkeypatch.chdir(tmp_path)
39+
sha = "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2"
40+
(tmp_path / ".gitmodules").write_text(
41+
f'[submodule "vcpkg"]\n\tpath = vcpkg\n\turl = https://github.com/microsoft/vcpkg.git\n\tbranch = {sha}\n'
42+
)
43+
assert _read_vcpkg_ref_from_gitmodules(Path("vcpkg")) == sha
44+
45+
def test_gitmodules_custom_vcpkg_root(self, tmp_path, monkeypatch):
46+
monkeypatch.chdir(tmp_path)
47+
(tmp_path / ".gitmodules").write_text(
48+
'[submodule "deps/vcpkg"]\n\tpath = deps/vcpkg\n\turl = https://github.com/microsoft/vcpkg.git\n\tbranch = 2024.06.15\n'
49+
)
50+
# Default vcpkg root won't match
51+
assert _read_vcpkg_ref_from_gitmodules(Path("vcpkg")) is None
52+
# Custom root matches
53+
assert _read_vcpkg_ref_from_gitmodules(Path("deps/vcpkg")) == "2024.06.15"
54+
55+
def test_gitmodules_multiple_submodules(self, tmp_path, monkeypatch):
56+
monkeypatch.chdir(tmp_path)
57+
(tmp_path / ".gitmodules").write_text(
58+
'[submodule "other"]\n'
59+
"\tpath = other\n"
60+
"\turl = https://github.com/example/other.git\n"
61+
"\tbranch = main\n"
62+
'[submodule "vcpkg"]\n'
63+
"\tpath = vcpkg\n"
64+
"\turl = https://github.com/microsoft/vcpkg.git\n"
65+
"\tbranch = 2024.01.12\n"
66+
)
67+
assert _read_vcpkg_ref_from_gitmodules(Path("vcpkg")) == "2024.01.12"
68+
69+
70+
class TestVcpkgRefConfig:
71+
"""Tests for vcpkg_ref configuration field."""
72+
73+
def test_default_vcpkg_ref_is_none(self):
74+
cfg = HatchCppVcpkgConfiguration()
75+
assert cfg.vcpkg_ref is None
76+
77+
def test_explicit_vcpkg_ref(self):
78+
cfg = HatchCppVcpkgConfiguration(vcpkg_ref="2024.01.12")
79+
assert cfg.vcpkg_ref == "2024.01.12"
80+
81+
def test_explicit_vcpkg_ref_commit_sha(self):
82+
sha = "a1b2c3d4e5f6"
83+
cfg = HatchCppVcpkgConfiguration(vcpkg_ref=sha)
84+
assert cfg.vcpkg_ref == sha
85+
86+
87+
class TestResolveVcpkgRef:
88+
"""Tests for _resolve_vcpkg_ref priority logic."""
89+
90+
def test_explicit_ref_takes_priority_over_gitmodules(self, tmp_path, monkeypatch):
91+
monkeypatch.chdir(tmp_path)
92+
(tmp_path / ".gitmodules").write_text(
93+
'[submodule "vcpkg"]\n\tpath = vcpkg\n\turl = https://github.com/microsoft/vcpkg.git\n\tbranch = 2024.01.12\n'
94+
)
95+
cfg = HatchCppVcpkgConfiguration(vcpkg_ref="my-custom-tag")
96+
assert cfg._resolve_vcpkg_ref() == "my-custom-tag"
97+
98+
def test_falls_back_to_gitmodules(self, tmp_path, monkeypatch):
99+
monkeypatch.chdir(tmp_path)
100+
(tmp_path / ".gitmodules").write_text(
101+
'[submodule "vcpkg"]\n\tpath = vcpkg\n\turl = https://github.com/microsoft/vcpkg.git\n\tbranch = 2024.01.12\n'
102+
)
103+
cfg = HatchCppVcpkgConfiguration()
104+
assert cfg._resolve_vcpkg_ref() == "2024.01.12"
105+
106+
def test_returns_none_when_no_ref(self, tmp_path, monkeypatch):
107+
monkeypatch.chdir(tmp_path)
108+
cfg = HatchCppVcpkgConfiguration()
109+
assert cfg._resolve_vcpkg_ref() is None
110+
111+
112+
class TestVcpkgGenerate:
113+
"""Tests that generate() includes the checkout command when a ref is set."""
114+
115+
def _make_vcpkg_env(self, tmp_path):
116+
"""Create a minimal vcpkg.json so generate() produces commands."""
117+
(tmp_path / "vcpkg.json").write_text("{}")
118+
119+
def test_generate_with_explicit_ref(self, tmp_path, monkeypatch):
120+
monkeypatch.chdir(tmp_path)
121+
self._make_vcpkg_env(tmp_path)
122+
123+
cfg = HatchCppVcpkgConfiguration(vcpkg_ref="2024.01.12")
124+
commands = cfg.generate(None)
125+
126+
assert any("git clone" in cmd for cmd in commands)
127+
assert any("git -C vcpkg checkout 2024.01.12" in cmd for cmd in commands)
128+
# checkout must come after clone but before bootstrap
129+
clone_idx = next(i for i, c in enumerate(commands) if "git clone" in c)
130+
checkout_idx = next(i for i, c in enumerate(commands) if "checkout" in c)
131+
bootstrap_idx = next(i for i, c in enumerate(commands) if "bootstrap" in c)
132+
assert clone_idx < checkout_idx < bootstrap_idx
133+
134+
def test_generate_with_gitmodules_ref(self, tmp_path, monkeypatch):
135+
monkeypatch.chdir(tmp_path)
136+
self._make_vcpkg_env(tmp_path)
137+
(tmp_path / ".gitmodules").write_text(
138+
'[submodule "vcpkg"]\n\tpath = vcpkg\n\turl = https://github.com/microsoft/vcpkg.git\n\tbranch = 2024.06.15\n'
139+
)
140+
141+
cfg = HatchCppVcpkgConfiguration()
142+
commands = cfg.generate(None)
143+
144+
assert any("git -C vcpkg checkout 2024.06.15" in cmd for cmd in commands)
145+
146+
def test_generate_without_ref(self, tmp_path, monkeypatch):
147+
monkeypatch.chdir(tmp_path)
148+
self._make_vcpkg_env(tmp_path)
149+
150+
cfg = HatchCppVcpkgConfiguration()
151+
commands = cfg.generate(None)
152+
153+
assert not any("checkout" in cmd for cmd in commands)
154+
assert any("git clone" in cmd for cmd in commands)
155+
156+
def test_generate_skips_clone_when_vcpkg_root_exists(self, tmp_path, monkeypatch):
157+
monkeypatch.chdir(tmp_path)
158+
self._make_vcpkg_env(tmp_path)
159+
(tmp_path / "vcpkg").mkdir()
160+
161+
cfg = HatchCppVcpkgConfiguration(vcpkg_ref="2024.01.12")
162+
commands = cfg.generate(None)
163+
164+
# When vcpkg_root already exists, no clone or checkout happens
165+
assert not any("git clone" in cmd for cmd in commands)
166+
assert not any("checkout" in cmd for cmd in commands)
167+
assert any("vcpkg" in cmd and "install" in cmd for cmd in commands)
168+
169+
def test_generate_no_vcpkg_json(self, tmp_path, monkeypatch):
170+
monkeypatch.chdir(tmp_path)
171+
# No vcpkg.json => no commands at all
172+
cfg = HatchCppVcpkgConfiguration(vcpkg_ref="2024.01.12")
173+
commands = cfg.generate(None)
174+
assert commands == []

hatch_cpp/toolchains/vcpkg.py

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import annotations
22

3+
import configparser
34
from pathlib import Path
45
from platform import machine as platform_machine
56
from sys import platform as sys_platform
@@ -38,14 +39,45 @@
3839
}
3940

4041

42+
def _read_vcpkg_ref_from_gitmodules(vcpkg_root: Path) -> Optional[str]:
43+
"""Read the branch/ref for vcpkg from .gitmodules if it exists.
44+
45+
Looks for a submodule whose path matches ``vcpkg_root`` and returns
46+
its ``branch`` value when present.
47+
"""
48+
gitmodules_path = Path(".gitmodules")
49+
if not gitmodules_path.exists():
50+
return None
51+
52+
parser = configparser.ConfigParser()
53+
parser.read(str(gitmodules_path))
54+
55+
for section in parser.sections():
56+
if parser.get(section, "path", fallback=None) == str(vcpkg_root):
57+
return parser.get(section, "branch", fallback=None)
58+
59+
return None
60+
61+
4162
class HatchCppVcpkgConfiguration(BaseModel):
4263
vcpkg: Optional[str] = Field(default="vcpkg.json")
4364
vcpkg_root: Optional[Path] = Field(default=Path("vcpkg"))
4465
vcpkg_repo: Optional[str] = Field(default="https://github.com/microsoft/vcpkg.git")
4566
vcpkg_triplet: Optional[VcpkgTriplet] = Field(default=None)
67+
vcpkg_ref: Optional[str] = Field(
68+
default=None,
69+
description="Branch, tag, or commit SHA to checkout after cloning vcpkg. "
70+
"If not set, falls back to the branch specified in .gitmodules for the vcpkg submodule.",
71+
)
4672

4773
# TODO: overlay
4874

75+
def _resolve_vcpkg_ref(self) -> Optional[str]:
76+
"""Return the ref to checkout: explicit config takes priority, then .gitmodules."""
77+
if self.vcpkg_ref is not None:
78+
return self.vcpkg_ref
79+
return _read_vcpkg_ref_from_gitmodules(self.vcpkg_root)
80+
4981
def generate(self, config):
5082
commands = []
5183

@@ -57,9 +89,12 @@ def generate(self, config):
5789
if self.vcpkg and Path(self.vcpkg).exists():
5890
if not Path(self.vcpkg_root).exists():
5991
commands.append(f"git clone {self.vcpkg_repo} {self.vcpkg_root}")
60-
commands.append(
61-
f"./{self.vcpkg_root / 'bootstrap-vcpkg.sh' if sys_platform != 'win32' else self.vcpkg_root / 'sbootstrap-vcpkg.bat'}"
62-
)
92+
93+
ref = self._resolve_vcpkg_ref()
94+
if ref is not None:
95+
commands.append(f"git -C {self.vcpkg_root} checkout {ref}")
96+
97+
commands.append(f"./{self.vcpkg_root / 'bootstrap-vcpkg.sh' if sys_platform != 'win32' else self.vcpkg_root / 'bootstrap-vcpkg.bat'}")
6398
commands.append(f"./{self.vcpkg_root / 'vcpkg'} install --triplet {self.vcpkg_triplet}")
6499

65100
return commands

0 commit comments

Comments
 (0)