From d16b590e002bc5a597f8d3fd7b28ab643788e312 Mon Sep 17 00:00:00 2001 From: NeverSightAI Date: Thu, 17 Sep 2026 04:07:59 -0700 Subject: [PATCH 1/4] feat: matrix Windows EH corpus across VS 2022 and VS 2026 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Hosted runners can install Visual Studio 2022 (windows-2022) and Visual Studio 2026 (windows-2025). Add that year axis to the producer, keep VS 2022 paths/keys stable, and skip VS 2010–2019 and a non-existent VS 2025 product with an explicit reason instead of a silent cell. --- .github/workflows/build-windows-eh.yml | 3 +- corpus/windows-eh/README.md | 6 + schema/windows-eh-manifest.schema.json | 4 + scripts/Build-WindowsCorpus.ps1 | 27 ++++- .../tests/test_build_windows_corpus_script.py | 2 + scripts/tests/test_verify_windows_corpus.py | 12 +- scripts/tests/test_windows_matrix.py | 34 +++++- scripts/windows_matrix.py | 107 +++++++++++++++--- 8 files changed, 168 insertions(+), 27 deletions(-) diff --git a/.github/workflows/build-windows-eh.yml b/.github/workflows/build-windows-eh.yml index 27366c0..7790360 100644 --- a/.github/workflows/build-windows-eh.yml +++ b/.github/workflows/build-windows-eh.yml @@ -60,7 +60,7 @@ jobs: build-cell: name: ${{ matrix.cell_name }} needs: verify-producer - runs-on: windows-2022 + runs-on: ${{ matrix.runner }} timeout-minutes: 30 strategy: fail-fast: false @@ -78,6 +78,7 @@ jobs: -Optimization "${{ matrix.optimization }}" -SecurityCookie "${{ matrix.security_cookie }}" -CxxFormat "${{ matrix.cxx_format }}" + -VsYear "${{ matrix.vs_year }}" -OutputRoot "${{ runner.temp }}/windows-eh" - name: Upload validated cell diff --git a/corpus/windows-eh/README.md b/corpus/windows-eh/README.md index f37bc05..f6e28e2 100644 --- a/corpus/windows-eh/README.md +++ b/corpus/windows-eh/README.md @@ -6,3 +6,9 @@ workflow after every successful producer change on `main`. Do not add, rename, or replace PE files without the matching generated entry in `manifests/windows-eh.json`. Every artifact name must identify its toolchain, architecture, C++ EH format, security-cookie mode, and optimization mode. + +MSVC cells are built for every Visual Studio year the GitHub-hosted runner can +install (VS 2022 on `windows-2022`, VS 2026 on `windows-2025`). VS 2010–2019 +and a non-existent VS 2025 product are explicit producer skips, not silent +cells. VS 2022 keeps the historical path; later years use +`corpus/windows-eh/msvc/vs/...`. diff --git a/schema/windows-eh-manifest.schema.json b/schema/windows-eh-manifest.schema.json index b800218..684dbcf 100644 --- a/schema/windows-eh-manifest.schema.json +++ b/schema/windows-eh-manifest.schema.json @@ -183,6 +183,10 @@ }, "linker_flags": { "$ref": "#/$defs/stringArray" + }, + "visual_studio_year": { + "type": "integer", + "enum": [2022, 2026] } }, "allOf": [ diff --git a/scripts/Build-WindowsCorpus.ps1 b/scripts/Build-WindowsCorpus.ps1 index 5e33de1..d41deee 100644 --- a/scripts/Build-WindowsCorpus.ps1 +++ b/scripts/Build-WindowsCorpus.ps1 @@ -23,6 +23,10 @@ param( [ValidateSet("native", "fh3", "fh4")] [string] $CxxFormat, + [Parameter(Mandatory = $false)] + [ValidateSet("2022", "2026")] + [string] $VsYear = "2022", + [Parameter(Mandatory = $true)] [string] $OutputRoot, @@ -78,7 +82,16 @@ if ($CxxFormat -notin $SupportedFormats) { } $CookieLabel = if ($SecurityCookie -eq "on") { "gs" } else { "no-gs" } -$CellName = "$Toolchain-$Architecture-$CxxFormat-$CookieLabel-$Optimization" +$ToolchainKey = if ($Toolchain -eq "msvc" -and $VsYear -ne "2022") { + "msvc-vs$VsYear" +} else { + $Toolchain +} +$CellName = "$ToolchainKey-$Architecture-$CxxFormat-$CookieLabel-$Optimization" +$VsWhereVersion = switch ($VsYear) { + "2026" { "[18.0,19.0)" } + default { "[17.0,18.0)" } +} $Compiler = if ($Toolchain -eq "msvc") { "cl.exe" } else { "clang-cl.exe" } $Linker = if ($Toolchain -eq "msvc") { "link.exe" } else { "lld-link.exe" } $OptimizationFlag = if ($Optimization -eq "o2") { "/O2" } else { "/Od" } @@ -176,6 +189,8 @@ if ($ValidateConfigurationOnly) { [ordered]@{ cell_name = $CellName toolchain = $Toolchain + vs_year = [int]$VsYear + vswhere_version = $VsWhereVersion architecture = $Architecture target_triple = $Target.target_triple vs_arch = $Target.vs_arch @@ -200,7 +215,11 @@ $SourceRoot = Join-Path $RepositoryRoot "sources" $OfficialSourceRoot = Join-Path $SourceRoot "windows-seh-tests/src" $ProbeSourceRoot = Join-Path $SourceRoot "msvc-exceptions" $OutputRoot = [IO.Path]::GetFullPath($OutputRoot) -$CellRelativeRoot = "corpus/windows-eh/$Toolchain/$Architecture/$CxxFormat/$CookieLabel/$Optimization" +$CellRelativeRoot = if ($Toolchain -eq "msvc" -and $VsYear -ne "2022") { + "corpus/windows-eh/$Toolchain/vs$VsYear/$Architecture/$CxxFormat/$CookieLabel/$Optimization" +} else { + "corpus/windows-eh/$Toolchain/$Architecture/$CxxFormat/$CookieLabel/$Optimization" +} $CellOutputRoot = Join-Path $OutputRoot $CellRelativeRoot $OfficialOutputRoot = Join-Path $CellOutputRoot "windows-seh-tests" $ProbeOutputRoot = Join-Path $CellOutputRoot "abi-probe" @@ -226,6 +245,9 @@ function Import-VisualStudioEnvironment { "-requires", $Target.component, "-property", "installationPath" ) + if ($Toolchain -eq "msvc" -and $VsWhereVersion) { + $VsWhereArguments += @("-version", $VsWhereVersion) + } $Installation = (& $VsWhere @VsWhereArguments | Select-Object -First 1) if (-not $Installation) { throw "a Visual Studio installation with $($Target.component) was not found" @@ -546,6 +568,7 @@ function New-ArtifactRecord( execution = if ($Target.execute) { "passed" } else { "not-run-cross-target" } compiler_flags = @($script:CommonCompilerFlags + $AdditionalCompilerFlags) linker_flags = @($script:CommonLinkerFlags + $AdditionalLinkerFlags) + visual_studio_year = [int]$VsYear } evidence = Get-Evidence $Name $Kind neverd = Get-NeverDExpectation $Name $Kind diff --git a/scripts/tests/test_build_windows_corpus_script.py b/scripts/tests/test_build_windows_corpus_script.py index ce0cea2..fe18504 100644 --- a/scripts/tests/test_build_windows_corpus_script.py +++ b/scripts/tests/test_build_windows_corpus_script.py @@ -42,6 +42,8 @@ def _configuration(self, cell) -> dict: cell.security_cookie, "-CxxFormat", cell.cxx_format, + "-VsYear", + str(cell.vs_year), "-OutputRoot", temp_dir, "-ValidateConfigurationOnly", diff --git a/scripts/tests/test_verify_windows_corpus.py b/scripts/tests/test_verify_windows_corpus.py index 65f5c69..5adf846 100644 --- a/scripts/tests/test_verify_windows_corpus.py +++ b/scripts/tests/test_verify_windows_corpus.py @@ -209,10 +209,14 @@ def _artifact_path( security_cookie: bool, optimization: str, name: str = "cxx_eh_probe", + vs_year: int = 2022, ) -> Path: suite = "abi-probe" if name.endswith("_probe") else "windows-seh-tests" extension = ".dll" if name == "xframe_eh_dll" else ".exe" cookie_label = "gs" if security_cookie else "no-gs" + toolchain_dir = toolchain + if toolchain == "msvc" and vs_year != 2022: + toolchain_dir = f"{toolchain}/vs{vs_year}" filename = ( "-".join( ( @@ -229,7 +233,7 @@ def _artifact_path( return ( root / "corpus/windows-eh" - / toolchain + / toolchain_dir / architecture / cxx_format / cookie_label @@ -374,6 +378,7 @@ def _complete_inventory() -> dict: security_cookie=security_cookie, optimization=cell.optimization, name=name, + vs_year=cell.vs_year, ).as_posix(), "architecture": cell.architecture, "name": name, @@ -382,6 +387,7 @@ def _complete_inventory() -> dict: "optimization": cell.optimization, "security_cookie": security_cookie, "cxx_format": cell.cxx_format, + "visual_studio_year": cell.vs_year, }, } ) @@ -851,13 +857,13 @@ def test_rejects_hash_mismatch(self) -> None: with self.assertRaisesRegex(VERIFY.VerificationError, "SHA-256 mismatch"): VERIFY.verify_manifest(manifest_path, root) - def test_complete_matrix_accepts_32_cells_and_168_capability_artifacts( + def test_complete_matrix_accepts_52_cells_and_288_capability_artifacts( self, ) -> None: with tempfile.TemporaryDirectory() as temp_dir: root = Path(temp_dir) manifest = _complete_inventory() - self.assertEqual(len(manifest["artifacts"]), 168) + self.assertEqual(len(manifest["artifacts"]), 288) manifest_path = _write_manifest(root, manifest) VERIFY.verify_complete_matrix(manifest_path) diff --git a/scripts/tests/test_windows_matrix.py b/scripts/tests/test_windows_matrix.py index a2b6485..352adff 100644 --- a/scripts/tests/test_windows_matrix.py +++ b/scripts/tests/test_windows_matrix.py @@ -32,15 +32,19 @@ def setUpClass(cls) -> None: def test_matrix_contains_exact_supported_capabilities(self) -> None: cells = self.matrix.expected_cells() - self.assertEqual(len(cells), 32) - self.assertEqual(len({cell.key for cell in cells}), 32) + self.assertEqual(len(cells), 52) + self.assertEqual(len({cell.key for cell in cells}), 52) self.assertEqual( Counter(cell.toolchain for cell in cells), - Counter({"msvc": 20, "clang-cl": 12}), + Counter({"msvc": 40, "clang-cl": 12}), + ) + self.assertEqual( + Counter(cell.vs_year for cell in cells if cell.toolchain == "msvc"), + Counter({2022: 20, 2026: 20}), ) self.assertEqual( Counter(cell.architecture for cell in cells), - Counter({"x86": 8, "x86_64": 12, "arm": 4, "aarch64": 8}), + Counter({"x86": 12, "x86_64": 20, "arm": 8, "aarch64": 12}), ) msvc_x64 = { @@ -136,8 +140,12 @@ def test_github_output_is_compact_include_matrix(self) -> None: name, payload = line.split("=", 1) self.assertEqual(name, "matrix") matrix = json.loads(payload) - self.assertEqual(len(matrix["include"]), 32) - self.assertEqual(len({entry["cell_name"] for entry in matrix["include"]}), 32) + self.assertEqual(len(matrix["include"]), 52) + self.assertEqual(len({entry["cell_name"] for entry in matrix["include"]}), 52) + self.assertTrue(any(entry.get("vs_year") == 2026 for entry in matrix["include"])) + self.assertTrue( + any(entry.get("runner") == "windows-2025" for entry in matrix["include"]) + ) self.assertTrue( all(entry["architecture"] != "i386" for entry in matrix["include"]) ) @@ -148,6 +156,20 @@ def test_github_output_is_compact_include_matrix(self) -> None: ) ) + def test_skipped_vs_years_are_explicit(self) -> None: + skips = self.matrix.skipped_vs_years() + self.assertEqual( + set(skips), + {2010, 2012, 2013, 2015, 2017, 2019, 2025}, + ) + for year, reason in skips.items(): + with self.subTest(year=year): + self.assertTrue(reason) + with self.assertRaises(ValueError): + self.matrix.validate_cell( + "msvc", "x86_64", "fh4", "o0", "off", year + ) + if __name__ == "__main__": unittest.main() diff --git a/scripts/windows_matrix.py b/scripts/windows_matrix.py index b5e8bf1..929507d 100644 --- a/scripts/windows_matrix.py +++ b/scripts/windows_matrix.py @@ -50,6 +50,30 @@ _TOOLCHAINS = ("msvc", "clang-cl") _OPTIMIZATIONS = ("o0", "o2") _SECURITY_COOKIE_MODES = ("off", "on") +# Hosted-image MSVC product years the producer can actually drive. VS 2022 +# keeps the historical cell key/path so existing artifacts stay valid. +_MSVC_VS_YEARS = (2022, 2026) +_MSVC_VS_YEAR_RUNNERS = { + 2022: { + "runner": "windows-2022", + "vswhere_version": "[17.0,18.0)", + }, + 2026: { + "runner": "windows-2025", + "vswhere_version": "[18.0,19.0)", + }, +} +# Requested 2010–2026 coverage. Years the hosted runner cannot install are +# explicit skips, not silent cells. +_MSVC_VS_YEAR_SKIPS = { + 2010: "VS 2010 Build Tools cannot be installed on current GitHub-hosted Windows images", + 2012: "VS 2012 Build Tools cannot be installed on current GitHub-hosted Windows images", + 2013: "VS 2013 Build Tools cannot be installed on current GitHub-hosted Windows images", + 2015: "VS 2015 Build Tools cannot be installed on current GitHub-hosted Windows images", + 2017: "VS 2017 Build Tools cannot be installed on current GitHub-hosted Windows images", + 2019: "VS 2019 Build Tools are not preinstalled on windows-2022/windows-2025 and are not part of the hosted-image contract", + 2025: "There is no Visual Studio 2025 product; MSVC 14.4x ships as Visual Studio 2022", +} _FULL_ARTIFACT_INVENTORY = ( "xcpt4", "nested_collided", @@ -87,6 +111,7 @@ class MatrixCell: cxx_format: str security_cookie: str optimization: str + vs_year: int = 2022 @property def target_triple(self) -> str: @@ -122,9 +147,12 @@ def artifact_names(self) -> tuple[str, ...]: @property def key(self) -> str: + toolchain = self.toolchain + if self.toolchain == "msvc" and self.vs_year != 2022: + toolchain = f"msvc-vs{self.vs_year}" return "-".join( ( - self.toolchain, + toolchain, self.architecture, self.cxx_format, self.cookie_label, @@ -132,8 +160,20 @@ def key(self) -> str: ) ) - def to_actions_entry(self) -> dict[str, str | bool]: - return { + @property + def runner(self) -> str: + if self.toolchain != "msvc": + return "windows-2022" + return str(_MSVC_VS_YEAR_RUNNERS[self.vs_year]["runner"]) + + @property + def vswhere_version(self) -> str: + if self.toolchain != "msvc": + return "" + return str(_MSVC_VS_YEAR_RUNNERS[self.vs_year]["vswhere_version"]) + + def to_actions_entry(self) -> dict[str, str | bool | int]: + entry: dict[str, str | bool | int] = { "toolchain": self.toolchain, "architecture": self.architecture, "cxx_format": self.cxx_format, @@ -144,7 +184,18 @@ def to_actions_entry(self) -> dict[str, str | bool]: "linker_machine": self.linker_machine, "execute": self.execute, "cell_name": self.key, + "runner": self.runner, + "vs_year": self.vs_year, } + if self.vswhere_version: + entry["vswhere_version"] = self.vswhere_version + return entry + + +def skipped_vs_years() -> dict[int, str]: + """Return Visual Studio years that are requested but not installable.""" + + return dict(_MSVC_VS_YEAR_SKIPS) def validate_cell( @@ -153,6 +204,7 @@ def validate_cell( cxx_format: str, optimization: str, security_cookie: str, + vs_year: int = 2022, ) -> MatrixCell: """Validate and canonicalize one producer cell.""" @@ -175,12 +227,27 @@ def validate_cell( normalized_cookie = security_cookie.strip().lower() if normalized_cookie not in _SECURITY_COOKIE_MODES: raise ValueError(f"unsupported security-cookie mode: {security_cookie}") + if not isinstance(vs_year, int): + raise ValueError(f"unsupported Visual Studio year: {vs_year}") + if normalized_toolchain == "msvc": + if vs_year in _MSVC_VS_YEAR_SKIPS: + raise ValueError( + f"Visual Studio {vs_year} is an explicit skip: " + f"{_MSVC_VS_YEAR_SKIPS[vs_year]}" + ) + if vs_year not in _MSVC_VS_YEARS: + raise ValueError(f"unsupported Visual Studio year: {vs_year}") + elif vs_year != 2022: + raise ValueError( + f"Visual Studio year {vs_year} is only valid for the msvc toolchain" + ) return MatrixCell( normalized_toolchain, normalized_architecture, normalized_format, normalized_cookie, normalized_optimization, + vs_year, ) @@ -189,19 +256,22 @@ def expected_cells() -> tuple[MatrixCell, ...]: cells: list[MatrixCell] = [] for toolchain in _TOOLCHAINS: - for architecture in _ARCHITECTURES: - for cxx_format in _supported_formats(toolchain, architecture): - for security_cookie in _SECURITY_COOKIE_MODES: - for optimization in _OPTIMIZATIONS: - cells.append( - validate_cell( - toolchain, - architecture, - cxx_format, - optimization, - security_cookie, + vs_years = _MSVC_VS_YEARS if toolchain == "msvc" else (2022,) + for vs_year in vs_years: + for architecture in _ARCHITECTURES: + for cxx_format in _supported_formats(toolchain, architecture): + for security_cookie in _SECURITY_COOKIE_MODES: + for optimization in _OPTIMIZATIONS: + cells.append( + validate_cell( + toolchain, + architecture, + cxx_format, + optimization, + security_cookie, + vs_year, + ) ) - ) return tuple(cells) @@ -211,12 +281,19 @@ def artifact_cell_key(build: dict[str, object], architecture: str) -> str: security_cookie = build.get("security_cookie") if not isinstance(security_cookie, bool): raise ValueError("security_cookie must be boolean") + vs_year = 2022 + raw_year = build.get("visual_studio_year") + if raw_year is not None: + if not isinstance(raw_year, int): + raise ValueError("visual_studio_year must be an integer") + vs_year = raw_year cell = validate_cell( str(build.get("toolchain", "")), architecture, str(build.get("cxx_format", "")), str(build.get("optimization", "")), "on" if security_cookie else "off", + vs_year, ) return cell.key From 5b950872ac18d0ee76fabf9a711a3ac214958b64 Mon Sep 17 00:00:00 2001 From: NeverSightAI Date: Thu, 17 Sep 2026 05:15:59 -0700 Subject: [PATCH 2/4] fix: accept VS 2026 msvc/vs2026/ artifact layout The producer already wrote later MSVC years under msvc/vsYYYY/. The verifier still expected the historical msvc/... path, so every VS 2026 cell failed after a successful compile and execute. Cap the Windows matrix at six concurrent jobs so the free-plan runner queue can drain. --- .github/workflows/build-windows-eh.yml | 1 + scripts/tests/test_verify_windows_corpus.py | 26 ++++++++++++ scripts/tests/test_windows_matrix.py | 8 ++++ scripts/verify_windows_corpus.py | 44 ++++++++++++++++----- scripts/windows_matrix.py | 11 ++++++ 5 files changed, 80 insertions(+), 10 deletions(-) diff --git a/.github/workflows/build-windows-eh.yml b/.github/workflows/build-windows-eh.yml index 7790360..9e9d90b 100644 --- a/.github/workflows/build-windows-eh.yml +++ b/.github/workflows/build-windows-eh.yml @@ -64,6 +64,7 @@ jobs: timeout-minutes: 30 strategy: fail-fast: false + max-parallel: 6 matrix: ${{ fromJSON(needs.verify-producer.outputs.matrix) }} steps: - name: Check out producer sources diff --git a/scripts/tests/test_verify_windows_corpus.py b/scripts/tests/test_verify_windows_corpus.py index 5adf846..9334cc2 100644 --- a/scripts/tests/test_verify_windows_corpus.py +++ b/scripts/tests/test_verify_windows_corpus.py @@ -253,6 +253,7 @@ def _valid_manifest( security_cookie: bool = False, optimization: str = "o0", name: str = "cxx_eh_probe", + vs_year: int = 2022, ) -> dict: payload = artifact.read_bytes() is_x64 = architecture == "x86_64" @@ -327,6 +328,7 @@ def _valid_manifest( "optimization": optimization, "security_cookie": security_cookie, "cxx_format": cxx_format, + "visual_studio_year": vs_year, "execution": execution, "compiler_flags": compiler_flags, "linker_flags": [ @@ -414,6 +416,30 @@ def test_accepts_schema_v2_x64_artifact(self) -> None: self.assertEqual(result.artifact_count, 1) self.assertEqual(result.total_bytes, artifact.stat().st_size) + def test_accepts_vs2026_msvc_layout(self) -> None: + with tempfile.TemporaryDirectory() as temp_dir: + root = Path(temp_dir) + artifact = _artifact_path( + root, + toolchain="msvc", + architecture="x86_64", + cxx_format="fh4", + security_cookie=False, + optimization="o0", + vs_year=2026, + ) + _write_minimal_pe(artifact, import_names=("__CxxFrameHandler4",)) + manifest = _valid_manifest( + root, artifact, cxx_format="fh4", vs_year=2026 + ) + self.assertIn("/msvc/vs2026/", manifest["artifacts"][0]["path"]) + self.assertNotIn("vs2026", Path(manifest["artifacts"][0]["path"]).name) + manifest_path = _write_manifest(root, manifest) + + result = VERIFY.verify_manifest(manifest_path, root) + + self.assertEqual(result.artifact_count, 1) + def test_accepts_all_four_pe_machine_targets(self) -> None: combinations = ( ("x86", "native", "load-only"), diff --git a/scripts/tests/test_windows_matrix.py b/scripts/tests/test_windows_matrix.py index 352adff..2dd2eae 100644 --- a/scripts/tests/test_windows_matrix.py +++ b/scripts/tests/test_windows_matrix.py @@ -156,6 +156,14 @@ def test_github_output_is_compact_include_matrix(self) -> None: ) ) + def test_non_2022_msvc_cells_use_vs_year_directory(self) -> None: + cells = {cell.key: cell for cell in self.matrix.expected_cells()} + self.assertEqual(cells["msvc-x86_64-fh4-gs-o0"].corpus_toolchain_parts, ("msvc",)) + self.assertEqual( + cells["msvc-vs2026-x86_64-fh4-gs-o0"].corpus_toolchain_parts, + ("msvc", "vs2026"), + ) + def test_skipped_vs_years_are_explicit(self) -> None: skips = self.matrix.skipped_vs_years() self.assertEqual( diff --git a/scripts/verify_windows_corpus.py b/scripts/verify_windows_corpus.py index e8818cc..b1c2865 100644 --- a/scripts/verify_windows_corpus.py +++ b/scripts/verify_windows_corpus.py @@ -554,12 +554,15 @@ def _expected_personalities( def _validate_build( build: dict[str, Any], architecture: str, context: str, *, uses_cxx: bool -) -> tuple[str, str, bool, str, str]: +) -> tuple[str, str, bool, str, str, int]: toolchain = _require_string(build, "toolchain", context) cxx_format = _require_string(build, "cxx_format", context) optimization = _require_string(build, "optimization", context) security_cookie = _require_bool(build, "security_cookie", context) execution = _require_string(build, "execution", context) + vs_year = 2022 + if "visual_studio_year" in build: + vs_year = _require_nonnegative_int(build, "visual_studio_year", context) try: cell = validate_cell( toolchain, @@ -567,6 +570,7 @@ def _validate_build( cxx_format, optimization, "on" if security_cookie else "off", + vs_year, ) except ValueError as error: raise VerificationError(str(error)) from error @@ -622,7 +626,14 @@ def _validate_build( if target_flag not in compiler_flags: raise VerificationError(f"{context}.compiler_flags omit {target_flag}") - return toolchain, cxx_format, security_cookie, optimization, cell.cookie_label + return ( + toolchain, + cxx_format, + security_cookie, + optimization, + cell.cookie_label, + cell.vs_year, + ) def _validate_neverd( @@ -772,13 +783,18 @@ def _validate_artifact( raise VerificationError(f"{context} artifact identity is inconsistent") build = _require_object(artifact.get("build"), f"{context}.build") - toolchain, cxx_format, security_cookie, optimization, cookie_label = ( - _validate_build( - build, - architecture, - f"{context}.build", - uses_cxx=kind in ("cxx", "mixed"), - ) + ( + toolchain, + cxx_format, + security_cookie, + optimization, + cookie_label, + vs_year, + ) = _validate_build( + build, + architecture, + f"{context}.build", + uses_cxx=kind in ("cxx", "mixed"), ) expected_filename = ( "-".join( @@ -793,10 +809,18 @@ def _validate_artifact( ) + extension ) + cell = validate_cell( + toolchain, + architecture, + cxx_format, + optimization, + "on" if security_cookie else "off", + vs_year, + ) expected_path = PurePosixPath( "corpus", "windows-eh", - toolchain, + *cell.corpus_toolchain_parts, architecture, cxx_format, cookie_label, diff --git a/scripts/windows_matrix.py b/scripts/windows_matrix.py index 929507d..7c72aff 100644 --- a/scripts/windows_matrix.py +++ b/scripts/windows_matrix.py @@ -160,6 +160,17 @@ def key(self) -> str: ) ) + @property + def corpus_toolchain_parts(self) -> tuple[str, ...]: + """Directory parts under corpus/windows-eh for this cell. + + VS 2022 keeps the historical `msvc/...` layout. Later MSVC years add + `msvc/vsYYYY/` so those artifacts do not overwrite the 2022 set. + """ + if self.toolchain == "msvc" and self.vs_year != 2022: + return (self.toolchain, f"vs{self.vs_year}") + return (self.toolchain,) + @property def runner(self) -> str: if self.toolchain != "msvc": From 5feb9b4dec8fa9658e38762ffa42c0f759987a61 Mon Sep 17 00:00:00 2001 From: NeverSightAI Date: Thu, 17 Sep 2026 06:09:07 -0700 Subject: [PATCH 3/4] fix: skip VS 2026 ARM32; windows-2025 has no VC.Tools.ARM Hosted VS 2026 built x86/x64 cells, then failed looking up Microsoft.VisualStudio.Component.VC.Tools.ARM. Keep ARM32 on VS 2022 and make the 2026 ARM cells an explicit skip. --- scripts/tests/test_verify_windows_corpus.py | 4 +-- scripts/tests/test_windows_matrix.py | 29 ++++++++++++++++----- scripts/windows_matrix.py | 21 +++++++++++++++ 3 files changed, 45 insertions(+), 9 deletions(-) diff --git a/scripts/tests/test_verify_windows_corpus.py b/scripts/tests/test_verify_windows_corpus.py index 9334cc2..7d4b78b 100644 --- a/scripts/tests/test_verify_windows_corpus.py +++ b/scripts/tests/test_verify_windows_corpus.py @@ -883,13 +883,13 @@ def test_rejects_hash_mismatch(self) -> None: with self.assertRaisesRegex(VERIFY.VerificationError, "SHA-256 mismatch"): VERIFY.verify_manifest(manifest_path, root) - def test_complete_matrix_accepts_52_cells_and_288_capability_artifacts( + def test_complete_matrix_accepts_48_cells_and_264_capability_artifacts( self, ) -> None: with tempfile.TemporaryDirectory() as temp_dir: root = Path(temp_dir) manifest = _complete_inventory() - self.assertEqual(len(manifest["artifacts"]), 288) + self.assertEqual(len(manifest["artifacts"]), 264) manifest_path = _write_manifest(root, manifest) VERIFY.verify_complete_matrix(manifest_path) diff --git a/scripts/tests/test_windows_matrix.py b/scripts/tests/test_windows_matrix.py index 2dd2eae..9b0910a 100644 --- a/scripts/tests/test_windows_matrix.py +++ b/scripts/tests/test_windows_matrix.py @@ -32,19 +32,19 @@ def setUpClass(cls) -> None: def test_matrix_contains_exact_supported_capabilities(self) -> None: cells = self.matrix.expected_cells() - self.assertEqual(len(cells), 52) - self.assertEqual(len({cell.key for cell in cells}), 52) + self.assertEqual(len(cells), 48) + self.assertEqual(len({cell.key for cell in cells}), 48) self.assertEqual( Counter(cell.toolchain for cell in cells), - Counter({"msvc": 40, "clang-cl": 12}), + Counter({"msvc": 36, "clang-cl": 12}), ) self.assertEqual( Counter(cell.vs_year for cell in cells if cell.toolchain == "msvc"), - Counter({2022: 20, 2026: 20}), + Counter({2022: 20, 2026: 16}), ) self.assertEqual( Counter(cell.architecture for cell in cells), - Counter({"x86": 12, "x86_64": 20, "arm": 8, "aarch64": 12}), + Counter({"x86": 12, "x86_64": 20, "arm": 4, "aarch64": 12}), ) msvc_x64 = { @@ -140,8 +140,14 @@ def test_github_output_is_compact_include_matrix(self) -> None: name, payload = line.split("=", 1) self.assertEqual(name, "matrix") matrix = json.loads(payload) - self.assertEqual(len(matrix["include"]), 52) - self.assertEqual(len({entry["cell_name"] for entry in matrix["include"]}), 52) + self.assertEqual(len(matrix["include"]), 48) + self.assertEqual(len({entry["cell_name"] for entry in matrix["include"]}), 48) + self.assertFalse( + any( + entry.get("vs_year") == 2026 and entry["architecture"] == "arm" + for entry in matrix["include"] + ) + ) self.assertTrue(any(entry.get("vs_year") == 2026 for entry in matrix["include"])) self.assertTrue( any(entry.get("runner") == "windows-2025" for entry in matrix["include"]) @@ -178,6 +184,15 @@ def test_skipped_vs_years_are_explicit(self) -> None: "msvc", "x86_64", "fh4", "o0", "off", year ) + def test_skipped_vs2026_arm_is_explicit(self) -> None: + skips = self.matrix.skipped_msvc_cells() + self.assertIn((2026, "arm"), skips) + self.assertTrue(skips[(2026, "arm")]) + with self.assertRaises(ValueError): + self.matrix.validate_cell("msvc", "arm", "native", "o0", "off", 2026) + keys = {cell.key for cell in self.matrix.expected_cells()} + self.assertNotIn("msvc-vs2026-arm-native-no-gs-o0", keys) + if __name__ == "__main__": unittest.main() diff --git a/scripts/windows_matrix.py b/scripts/windows_matrix.py index 7c72aff..a13981f 100644 --- a/scripts/windows_matrix.py +++ b/scripts/windows_matrix.py @@ -74,6 +74,13 @@ 2019: "VS 2019 Build Tools are not preinstalled on windows-2022/windows-2025 and are not part of the hosted-image contract", 2025: "There is no Visual Studio 2025 product; MSVC 14.4x ships as Visual Studio 2022", } +# Year is installable, but this target component is not on the hosted image. +_MSVC_CELL_SKIPS = { + (2026, "arm"): ( + "VS 2026 on windows-2025 does not include " + "Microsoft.VisualStudio.Component.VC.Tools.ARM" + ), +} _FULL_ARTIFACT_INVENTORY = ( "xcpt4", "nested_collided", @@ -209,6 +216,12 @@ def skipped_vs_years() -> dict[int, str]: return dict(_MSVC_VS_YEAR_SKIPS) +def skipped_msvc_cells() -> dict[tuple[int, str], str]: + """Return (year, architecture) cells a hosted image cannot build.""" + + return dict(_MSVC_CELL_SKIPS) + + def validate_cell( toolchain: str, architecture: str, @@ -248,6 +261,12 @@ def validate_cell( ) if vs_year not in _MSVC_VS_YEARS: raise ValueError(f"unsupported Visual Studio year: {vs_year}") + cell_skip = _MSVC_CELL_SKIPS.get((vs_year, normalized_architecture)) + if cell_skip: + raise ValueError( + f"Visual Studio {vs_year} {normalized_architecture} is an " + f"explicit skip: {cell_skip}" + ) elif vs_year != 2022: raise ValueError( f"Visual Studio year {vs_year} is only valid for the msvc toolchain" @@ -270,6 +289,8 @@ def expected_cells() -> tuple[MatrixCell, ...]: vs_years = _MSVC_VS_YEARS if toolchain == "msvc" else (2022,) for vs_year in vs_years: for architecture in _ARCHITECTURES: + if toolchain == "msvc" and (vs_year, architecture) in _MSVC_CELL_SKIPS: + continue for cxx_format in _supported_formats(toolchain, architecture): for security_cookie in _SECURITY_COOKIE_MODES: for optimization in _OPTIMIZATIONS: From 08849356cefbd9068fef3b2142885594d3b49043 Mon Sep 17 00:00:00 2001 From: NeverSightAI Date: Thu, 17 Sep 2026 07:50:38 -0700 Subject: [PATCH 4/4] fix: merge Windows EH fragments across hosted runner images VS 2022 cells run on windows-2022 and VS 2026 cells on windows-2025, so producer.runner_image is not a shared envelope field. Union the images and keep schema, corpus, source, and producer revision identical. --- schema/windows-eh-manifest.schema.json | 14 ++- scripts/tests/test_verify_windows_corpus.py | 116 +++++++++++++++++++- scripts/verify_windows_corpus.py | 76 +++++++++++-- 3 files changed, 195 insertions(+), 11 deletions(-) diff --git a/schema/windows-eh-manifest.schema.json b/schema/windows-eh-manifest.schema.json index 684dbcf..c2fed33 100644 --- a/schema/windows-eh-manifest.schema.json +++ b/schema/windows-eh-manifest.schema.json @@ -113,7 +113,19 @@ "$ref": "#/$defs/gitRevision" }, "runner_image": { - "$ref": "#/$defs/nonEmptyString" + "oneOf": [ + { + "$ref": "#/$defs/nonEmptyString" + }, + { + "type": "array", + "minItems": 1, + "uniqueItems": true, + "items": { + "$ref": "#/$defs/nonEmptyString" + } + } + ] }, "runner_arch": { "const": "x64" diff --git a/scripts/tests/test_verify_windows_corpus.py b/scripts/tests/test_verify_windows_corpus.py index 7d4b78b..f09f4e3 100644 --- a/scripts/tests/test_verify_windows_corpus.py +++ b/scripts/tests/test_verify_windows_corpus.py @@ -254,6 +254,8 @@ def _valid_manifest( optimization: str = "o0", name: str = "cxx_eh_probe", vs_year: int = 2022, + runner_image: str = "windows-2022", + repository_revision: str = "1" * 40, ) -> dict: payload = artifact.read_bytes() is_x64 = architecture == "x86_64" @@ -297,8 +299,8 @@ def _valid_manifest( } }, "producer": { - "repository_revision": "1" * 40, - "runner_image": "windows-2022", + "repository_revision": repository_revision, + "runner_image": runner_image, "runner_arch": "x64", }, "artifacts": [ @@ -943,6 +945,116 @@ def test_merges_msvc_and_clang_cl_fragments(self) -> None: {entry["build"]["toolchain"] for entry in merged["artifacts"]}, {"msvc", "clang-cl"}, ) + self.assertEqual(merged["producer"]["runner_image"], "windows-2022") + + def test_merges_fragments_built_on_different_runner_images(self) -> None: + """VS 2022 cells run on windows-2022 and VS 2026 cells on windows-2025. + GitHub also does not promise two jobs of one workflow land on the same + image version. That is a fact about the pool, not a sign the fragments + came from different producer runs, so the merge has to accept it and + keep both images on record. + """ + + with tempfile.TemporaryDirectory() as temp_dir: + root = Path(temp_dir) + fragments = [] + cells = ( + (2022, "fh3", "__CxxFrameHandler3", "win22-20260802.262.1"), + (2026, "fh4", "__CxxFrameHandler4", "win25-20260917.1.1"), + ) + for vs_year, cxx_format, personality, runner_image in cells: + artifact = _artifact_path( + root, + toolchain="msvc", + architecture="x86_64", + cxx_format=cxx_format, + security_cookie=False, + optimization="o0", + vs_year=vs_year, + ) + _write_minimal_pe(artifact, import_names=(personality,)) + manifest = _valid_manifest( + root, + artifact, + cxx_format=cxx_format, + vs_year=vs_year, + runner_image=runner_image, + ) + fragment = root / "fragments" / f"msvc-vs{vs_year}.json" + fragment.parent.mkdir(parents=True, exist_ok=True) + fragment.write_text(json.dumps(manifest), encoding="utf-8") + fragments.append(fragment) + + output = root / "manifests/windows-eh.json" + result = VERIFY.merge_manifests(fragments, output, root) + + self.assertEqual(result.artifact_count, 2) + merged = json.loads(output.read_text(encoding="utf-8")) + self.assertEqual( + merged["producer"]["runner_image"], + ["win22-20260802.262.1", "win25-20260917.1.1"], + ) + self.assertEqual( + { + entry["build"]["visual_studio_year"] + for entry in merged["artifacts"] + }, + {2022, 2026}, + ) + + def test_rejects_fragments_from_different_producer_runs(self) -> None: + with tempfile.TemporaryDirectory() as temp_dir: + root = Path(temp_dir) + fragments = [] + for index, toolchain in enumerate(("msvc", "clang-cl")): + artifact = _artifact_path( + root, + toolchain=toolchain, + architecture="x86_64", + cxx_format="fh3", + security_cookie=False, + optimization="o0", + ) + _write_minimal_pe(artifact, import_names=("__CxxFrameHandler3",)) + manifest = _valid_manifest( + root, + artifact, + toolchain=toolchain, + repository_revision=str(index + 1) * 40, + ) + fragment = root / "fragments" / f"{toolchain}.json" + fragment.parent.mkdir(parents=True, exist_ok=True) + fragment.write_text(json.dumps(manifest), encoding="utf-8") + fragments.append(fragment) + + output = root / "manifests/windows-eh.json" + with self.assertRaisesRegex( + VERIFY.VerificationError, "inconsistent envelopes" + ): + VERIFY.merge_manifests(fragments, output, root) + + def test_accepts_unioned_runner_image_array(self) -> None: + with tempfile.TemporaryDirectory() as temp_dir: + root = Path(temp_dir) + artifact = _artifact_path( + root, + toolchain="msvc", + architecture="x86_64", + cxx_format="fh3", + security_cookie=False, + optimization="o0", + ) + _write_minimal_pe(artifact, import_names=("__CxxFrameHandler3",)) + manifest = _valid_manifest(root, artifact) + manifest["producer"]["runner_image"] = [ + "win22-20260802.262.1", + "win25-20260917.1.1", + ] + manifest_path = _write_manifest(root, manifest) + + result = VERIFY.verify_manifest(manifest_path, root) + + self.assertEqual(result.artifact_count, 1) if __name__ == "__main__": diff --git a/scripts/verify_windows_corpus.py b/scripts/verify_windows_corpus.py index b1c2865..0931fb4 100644 --- a/scripts/verify_windows_corpus.py +++ b/scripts/verify_windows_corpus.py @@ -517,10 +517,59 @@ def _validate_source_and_producer(manifest: dict[str, Any]) -> None: producer_revision = _require_string(producer, "repository_revision", "producer") if not _REVISION_RE.fullmatch(producer_revision): raise VerificationError("producer.repository_revision must be a full SHA") - _require_string(producer, "runner_image", "producer") + _require_runner_images(producer) _require_string(producer, "runner_arch", "producer") +def _require_runner_images(producer: dict[str, Any]) -> list[str]: + """Accept one host image or the union written by a multi-host merge.""" + + value = producer.get("runner_image") + if isinstance(value, str) and value: + return [value] + if isinstance(value, list): + if not value: + raise VerificationError("producer.runner_image must not be empty") + images: list[str] = [] + seen: set[str] = set() + for index, entry in enumerate(value): + if not isinstance(entry, str) or not entry: + raise VerificationError( + f"producer.runner_image[{index}] must be a non-empty string" + ) + if entry in seen: + raise VerificationError( + "producer.runner_image items must be unique" + ) + seen.add(entry) + images.append(entry) + return images + raise VerificationError( + "producer.runner_image must be a non-empty string or array of strings" + ) + + +def _envelope_identity(fragment: dict[str, Any]) -> dict[str, Any]: + producer = _require_object(fragment.get("producer"), "producer") + return { + "schema_version": fragment["schema_version"], + "corpus": fragment["corpus"], + "source": fragment["source"], + "producer": { + key: value + for key, value in producer.items() + if key != "runner_image" + }, + } + + +def _merged_runner_image(images: list[str]) -> str | list[str]: + unique = sorted(set(images)) + if len(unique) == 1: + return unique[0] + return unique + + def _validate_tool_identity(value: Any, context: str, *, expected_name: str) -> None: identity = _require_object(value, context) name = _require_string(identity, "name", context) @@ -963,20 +1012,29 @@ def verify_complete_matrix(path: Path) -> None: def merge_manifests( fragment_paths: list[Path], output_path: Path, root: Path ) -> VerificationResult: + """Validate fragments and write one manifest. + + Cells on different hosted images (VS 2022 on windows-2022, VS 2026 on + windows-2025, or two windows-2022 jobs during an image rollout) disagree + about producer.runner_image. That is a fact about the pool, not a sign + they came from different producer runs. Schema, corpus, source, and the + rest of producer must still match. + """ + if not fragment_paths: raise VerificationError("no manifest fragments were found") envelopes: list[dict[str, Any]] = [] artifacts: list[dict[str, Any]] = [] + runner_images: list[str] = [] for fragment_path in sorted(fragment_paths, key=lambda item: item.as_posix()): verify_manifest(fragment_path, root) fragment = _load_manifest(fragment_path) - envelope = { - "schema_version": fragment["schema_version"], - "corpus": fragment["corpus"], - "source": fragment["source"], - "producer": fragment["producer"], - } - envelopes.append(envelope) + envelopes.append(_envelope_identity(fragment)) + runner_images.extend( + _require_runner_images( + _require_object(fragment.get("producer"), "producer") + ) + ) artifacts.extend(_require_array(fragment.get("artifacts"), "artifacts")) first = envelopes[0] for envelope in envelopes[1:]: @@ -985,6 +1043,8 @@ def merge_manifests( artifacts.sort(key=lambda artifact: str(artifact.get("path", ""))) merged = dict(first) + merged["producer"] = dict(first["producer"]) + merged["producer"]["runner_image"] = _merged_runner_image(runner_images) merged["artifacts"] = artifacts output_path.parent.mkdir(parents=True, exist_ok=True) file_descriptor, temporary_name = tempfile.mkstemp(