Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 29 additions & 9 deletions build.py
Original file line number Diff line number Diff line change
Expand Up @@ -697,6 +697,30 @@ def _cmake_cache_value(project: str, key: str) -> str:
return values[0].strip()


def _msvc_crt_dir(vc_root: Path, redist_version: str) -> Path:
x64_dir = vc_root / "Redist" / "MSVC" / redist_version / "x64"
try:
entries = tuple(x64_dir.iterdir())
except OSError as error:
raise SystemExit(f"cannot inspect matching x64 MSVC redist {x64_dir}: {error}") from error

candidates: list[Path] = []
for entry in entries:
if re.fullmatch(r"Microsoft\.VC[0-9]+\.CRT", entry.name, re.IGNORECASE) is None:
continue
if entry.is_symlink() or (hasattr(entry, "is_junction") and entry.is_junction()):
continue
if entry.is_dir():
candidates.append(entry)
if len(candidates) != 1:
names = sorted(entry.name for entry in candidates)
raise SystemExit(
"matching x64 MSVC redist must contain exactly one CRT family; "
f"found {names} under {x64_dir}"
)
return candidates[0]


def _msvc_runtime_sources(
project: str, runtime_names: tuple[str, ...]
) -> tuple[Path, tuple[_AppLocalRuntimeFile, ...]]:
Expand Down Expand Up @@ -734,18 +758,14 @@ def _msvc_runtime_sources(
if re.fullmatch(r"[0-9]+(?:\.[0-9]+){2,3}", redist_version) is None:
raise SystemExit(f"invalid MSVC redist version in {version_file}: {redist_version!r}")

crt_dir = (
vc_root
/ "Redist"
/ "MSVC"
/ redist_version
/ "x64"
/ "Microsoft.VC143.CRT"
)
crt_dir = _msvc_crt_dir(vc_root, redist_version)
try:
resolved_x64_dir = crt_dir.parent.resolve(strict=True)
resolved_crt_dir = crt_dir.resolve(strict=True)
except OSError as error:
raise SystemExit(f"matching x64 VC143 redist directory missing: {crt_dir}") from error
raise SystemExit(f"cannot resolve x64 MSVC redist directory: {crt_dir}") from error
if resolved_crt_dir.parent != resolved_x64_dir:
raise SystemExit(f"x64 MSVC runtime escapes its matching Redist directory: {crt_dir}")

files: list[_AppLocalRuntimeFile] = []
for name in runtime_names:
Expand Down
80 changes: 67 additions & 13 deletions scripts/test_build_runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,13 @@ def _snapshot(directory: Path) -> dict[str, bytes]:


class _RuntimeFixture:
def __init__(self) -> None:
def __init__(
self,
*,
toolset_version: str = "14.44.35207",
redist_version: str = "14.44.35112",
crt_family: str = "143",
) -> None:
self.temp = tempfile.TemporaryDirectory()
self.base = Path(self.temp.name)
self.root = self.base / "repo"
Expand All @@ -57,7 +63,7 @@ def __init__(self) -> None:
self.vc
/ "Tools"
/ "MSVC"
/ "14.44.35207"
/ toolset_version
/ "bin"
/ "Hostx64"
/ "x64"
Expand All @@ -69,17 +75,8 @@ def __init__(self) -> None:
self.vc / "Auxiliary" / "Build" / "Microsoft.VCRedistVersion.default.txt"
)
version_file.parent.mkdir(parents=True)
version_file.write_text("14.44.35112\n", encoding="utf-8")
self.crt = (
self.vc
/ "Redist"
/ "MSVC"
/ "14.44.35112"
/ "x64"
/ "Microsoft.VC143.CRT"
)
for index, name in enumerate(_RUNTIME_NAMES):
_write_pe(self.crt / name, marker=f"source-{index}".encode())
version_file.write_text(f"{redist_version}\n", encoding="utf-8")
self.crt = self.add_crt_family(crt_family, redist_version)

cache = self.app / "build" / "windows" / "x64" / "CMakeCache.txt"
cache.parent.mkdir(parents=True, exist_ok=True)
Expand All @@ -90,6 +87,19 @@ def __init__(self) -> None:
)
self.extra_app_imports: tuple[str, ...] = ()

def add_crt_family(self, family: str, version: str = "14.44.35112") -> Path:
crt = (
self.vc
/ "Redist"
/ "MSVC"
/ version
/ "x64"
/ f"Microsoft.VC{family}.CRT"
)
for index, name in enumerate(_RUNTIME_NAMES):
_write_pe(crt / name, marker=f"{version}-source-{index}".encode())
return crt

def close(self) -> None:
self.temp.cleanup()

Expand Down Expand Up @@ -156,6 +166,50 @@ def record_sign(paths: list[Path], dry: bool) -> None:
for name in _RUNTIME_NAMES:
self.assertEqual((fixture.bundle / name).read_bytes(), (fixture.crt / name).read_bytes())

def test_stages_vs2026_vc145_runtime(self) -> None:
fixture = _RuntimeFixture(
toolset_version="14.51.36231",
redist_version="14.51.36231",
crt_family="145",
)
self.addCleanup(fixture.close)

with fixture.patched(), mock.patch.object(gore_build, "sign_paths"):
plan = gore_build._sign_and_stage_app_local_runtime(
"gore-mod-manager", fixture.bundle, dry=False
)

self.assertIsNotNone(plan)
for name in _RUNTIME_NAMES:
self.assertEqual((fixture.bundle / name).read_bytes(), (fixture.crt / name).read_bytes())

def test_rejects_missing_or_ambiguous_crt_family_before_signing(self) -> None:
def missing(fixture: _RuntimeFixture) -> None:
shutil.rmtree(fixture.crt)

def ambiguous(fixture: _RuntimeFixture) -> None:
fixture.add_crt_family("145")

for label, mutate in (("missing", missing), ("ambiguous", ambiguous)):
with self.subTest(label=label):
fixture = _RuntimeFixture()
try:
before = _snapshot(fixture.bundle)
mutate(fixture)
with fixture.patched(), mock.patch.object(
gore_build, "sign_paths"
) as signer:
with self.assertRaisesRegex(
SystemExit, "exactly one CRT family"
):
gore_build._sign_and_stage_app_local_runtime(
"gore-mod-manager", fixture.bundle, dry=False
)
signer.assert_not_called()
self.assertEqual(_snapshot(fixture.bundle), before)
finally:
fixture.close()

def test_invalid_sources_and_closure_leave_bundle_unchanged(self) -> None:
def missing_source(fixture: _RuntimeFixture) -> None:
(fixture.crt / "vcruntime140_1.dll").unlink()
Expand Down
Loading