From ce7fbb373b14b37a5d163735c395e355e272d618 Mon Sep 17 00:00:00 2001 From: Benjamin Carlsson Date: Fri, 11 Sep 2026 21:44:43 +0000 Subject: [PATCH] Bundle native voice runtimes in Windows releases (#44922) ## Why Windows release packages need the voice helper and native audio libraries. Realtime TLS connections on fresh Windows installations also need platform certificate validation so Windows can retrieve missing trusted roots on demand. ## What changed - Build and sign the voice helper and audio DLLs for Windows x64 and ARM64, bundle a pinned Microsoft CRT DLL, and verify signatures and runtime receipts before packaging. - Add verified, pinned Cygwin and native build tools plus MSVC linker, compiler, and path handling fixes for the Windows Bazel builds. - Include voice resources in primary release archives and WinGet packages. Preserve WinGet executable names, update manifest hashes, and recognize the package root through matching entrypoint metadata. Keep Python runtime wheels voice-free to preserve their existing Windows support floor. - Use Windows platform TLS validation for realtime WebSockets when no custom CA bundle is configured, preserving custom CA behavior. ## Testing Add coverage for build-input integrity and unsafe paths, signed Windows runtime assembly, WinGet file and hash preservation, package discovery, and TLS trust selection, untrusted certificate rejection, and hostname validation. GitOrigin-RevId: 423da35872fa5549d69fd4ca97d922bb49599386 --- .../scripts/build-codex-package-archive.sh | 7 +- .github/scripts/run-bazel-ci.sh | 3 + .github/scripts/setup-voice-windows.ps1 | 96 ++++++ .github/scripts/test_voice_cygwin_inputs.py | 83 +++++ .github/scripts/voice-cygwin-inputs.py | 98 ++++++ .github/scripts/voice-cygwin-snapshot.json | 145 +++++++++ .github/scripts/voice_windows_tools.py | 80 +++++ .github/scripts/watch_voice_bazel.py | 79 +++++ .github/workflows/rust-release-windows.yml | 283 +++++++++++++++--- MODULE.bazel | 2 + MODULE.bazel.lock | 2 + codex-rs/Cargo.lock | 31 ++ codex-rs/codex-api/Cargo.toml | 3 + .../endpoint/realtime_websocket/methods.rs | 11 + .../codex-api/tests/realtime_websocket_tls.rs | 167 +++++++++++ codex-rs/http-client/Cargo.toml | 1 + codex-rs/http-client/src/custom_ca.rs | 4 + .../http-client/src/custom_ca_tls_tests.rs | 79 +++++ codex-rs/http-client/src/lib.rs | 6 + codex-rs/http-client/src/windows_tls.rs | 22 ++ codex-rs/http-client/src/windows_tls_tests.rs | 58 ++++ codex-rs/install-context/src/bundle_tests.rs | 28 ++ codex-rs/install-context/src/lib.rs | 15 + patches/BUILD.bazel | 2 + patches/ring_windows_msvc_include_dirs.patch | 20 +- .../rules_foreign_cc_make_msvc_stdint.patch | 13 + patches/rules_rs_windows_msvc_linker.patch | 7 +- ...les_rust_windows_execroot_separators.patch | 52 ++++ ...s_rust_windows_msvc_direct_link_args.patch | 2 +- scripts/build_winget_package.py | 37 +++ scripts/codex_package/test_layout.py | 66 ++++ third_party/voice/NOTICE.md | 10 + third_party/voice/README.md | 18 +- third_party/voice/assemble_package.py | 1 + third_party/voice/release_runtime.py | 2 + third_party/voice/test_assemble_package.py | 41 +++ third_party/voice/windows-crt.json | 18 ++ third_party/voice/windows_crt.py | 83 +++++ 38 files changed, 1629 insertions(+), 46 deletions(-) create mode 100644 .github/scripts/setup-voice-windows.ps1 create mode 100644 .github/scripts/test_voice_cygwin_inputs.py create mode 100644 .github/scripts/voice-cygwin-inputs.py create mode 100644 .github/scripts/voice-cygwin-snapshot.json create mode 100644 .github/scripts/voice_windows_tools.py create mode 100644 .github/scripts/watch_voice_bazel.py create mode 100644 codex-rs/codex-api/tests/realtime_websocket_tls.rs create mode 100644 codex-rs/http-client/src/custom_ca_tls_tests.rs create mode 100644 codex-rs/http-client/src/windows_tls.rs create mode 100644 codex-rs/http-client/src/windows_tls_tests.rs create mode 100644 patches/rules_foreign_cc_make_msvc_stdint.patch create mode 100644 patches/rules_rust_windows_execroot_separators.patch create mode 100644 scripts/build_winget_package.py create mode 100644 third_party/voice/windows-crt.json create mode 100644 third_party/voice/windows_crt.py diff --git a/.github/scripts/build-codex-package-archive.sh b/.github/scripts/build-codex-package-archive.sh index 1ac54c0cce..ecac99f533 100644 --- a/.github/scripts/build-codex-package-archive.sh +++ b/.github/scripts/build-codex-package-archive.sh @@ -117,8 +117,8 @@ if [[ -z "$target" || -z "$bundle" || -z "$entrypoint_dir" || -z "$archive_dir" usage >&2 exit 1 fi -if [[ ( -n "$voice_release_dir" || -n "$release_version" ) && ( -z "$voice_release_dir" || -z "$release_version" || "$bundle" != "primary" || ( "$target" != *-apple-darwin && "$target" != *-unknown-linux-musl ) ) ]]; then - echo "Voice resources require a primary macOS or Linux release package version" >&2 +if [[ ( -n "$voice_release_dir" || -n "$release_version" ) && ( -z "$voice_release_dir" || -z "$release_version" || "$bundle" != "primary" || ( "$target" != *-apple-darwin && "$target" != *-unknown-linux-musl && "$target" != *-pc-windows-msvc ) ) ]]; then + echo "Voice resources require a primary supported release package version" >&2 exit 1 fi @@ -221,9 +221,10 @@ if [[ -n "$voice_release_dir" ]]; then fi voice_package="${RUNNER_TEMP:-/tmp}/${archive_stem}-voice-${target}" rm -rf "$voice_package" + voice_helper="${voice_release_dir%/}/codex-voice-host${exe_suffix}" "$python_bin" "${repo_root}/third_party/voice/assemble_package.py" \ --package "$package_dir" \ - --helper "${voice_release_dir%/}/codex-voice-host" \ + --helper "$voice_helper" \ --runtime "${voice_release_dir%/}/runtime" \ --voice-target "$voice_target" \ --build-commit "$(git -C "$repo_root" rev-parse HEAD)" \ diff --git a/.github/scripts/run-bazel-ci.sh b/.github/scripts/run-bazel-ci.sh index 89f937a998..5825893fbc 100755 --- a/.github/scripts/run-bazel-ci.sh +++ b/.github/scripts/run-bazel-ci.sh @@ -47,6 +47,9 @@ if [[ $# -eq 0 ]]; then fi bazel_startup_args=() +if [[ -n "${BAZEL_HOST_JVM_ARG:-}" ]]; then + bazel_startup_args+=("--host_jvm_args=${BAZEL_HOST_JVM_ARG}") +fi if [[ -n "${BAZEL_OUTPUT_USER_ROOT:-}" ]]; then bazel_startup_args+=("--output_user_root=${BAZEL_OUTPUT_USER_ROOT}") fi diff --git a/.github/scripts/setup-voice-windows.ps1 b/.github/scripts/setup-voice-windows.ps1 new file mode 100644 index 0000000000..a24d29b1f0 --- /dev/null +++ b/.github/scripts/setup-voice-windows.ps1 @@ -0,0 +1,96 @@ +# CI-only prerequisites. Never install these tools into a Codex package. +param( + [Parameter(Mandatory = $true)][string]$Target, + [Parameter(Mandatory = $true)][string]$SnapshotArchive +) + +$ErrorActionPreference = "Stop" +$PkgHashes = @{ + "x86_64-pc-windows-msvc" = @("x64", "5604cf25ef38bb6a09520cff25ae9f0ecd8c2443053b15e40df4ad1eae0e4405") + "aarch64-pc-windows-msvc" = @("arm64", "d5752ce2ac2296c8abb91fb12c12e75ee99ba8a18b52ce5febf325847b6f062b") +} +if (-not $PkgHashes.ContainsKey($Target)) { throw "Unsupported target: $Target" } +$Root = Join-Path $env:RUNNER_TEMP "voice-windows-build-tools" +$Evidence = Join-Path $Root "evidence" +$Cygwin = Join-Path $Root "cygwin" +$Cache = Join-Path $Root "cache" +New-Item -ItemType Directory -Path $Root, $Evidence | Out-Null +$ManifestPath = Join-Path $PSScriptRoot "voice-cygwin-snapshot.json" +$Manifest = Get-Content -Raw $ManifestPath | ConvertFrom-Json +$SnapshotTool = Join-Path $PSScriptRoot "voice-cygwin-inputs.py" +& python $SnapshotTool extract --archive $SnapshotArchive --directory (Join-Path $Cache $Manifest.cacheDirectory) +if ($LASTEXITCODE -ne 0) { throw "Cygwin snapshot could not be verified and extracted" } +Copy-Item $ManifestPath (Join-Path $Evidence "cygwin-snapshot.json") + +# Official https://cygwin.com/setup/sha512.sum and pkgconf release asset digests. +# The complete package closure is pinned separately in cygwin-snapshot.json. +$Inputs = @( + @{ + url = "https://cygwin.com/setup/setup-2.937.x86_64.exe" + file = "setup.exe" + algorithm = "SHA512" + digest = "6acea47c59781c9e7f544a18d53935d59df6e44d5d52ac95ee165671b8e388820455eebf30ccc7d254b00c3d0eb694269a0f8dc84b17944c1f355dac9c5aafcc" + }, + @{ + url = "https://github.com/pkgconf/pkgconf/releases/download/pkgconf-3.0.6/pkgconf-$($PkgHashes[$Target][0])-3.0.6.msi" + file = "pkgconf.msi" + algorithm = "SHA256" + digest = $PkgHashes[$Target][1] + } +) +foreach ($InputFile in $Inputs) { + $Destination = Join-Path $Root $InputFile.file + Invoke-WebRequest -Uri $InputFile.url -OutFile $Destination + $Actual = (Get-FileHash -Path $Destination -Algorithm $InputFile.algorithm).Hash.ToLowerInvariant() + if ($Actual -ne $InputFile.digest) { throw "Build input digest mismatch: $($InputFile.file)" } +} +$Inputs | ConvertTo-Json | Set-Content (Join-Path $Evidence "bootstrap-inputs.json") +$Packages = $Manifest.requestedPackages -join "," +# Only setup.xz + setup.xz.sig are present: local mode still verifies that +# signature. Never substitute an unsigned setup.ini or enable a network fallback. +$Setup = Start-Process -FilePath (Join-Path $Root "setup.exe") -Wait -PassThru -ArgumentList @( + "--quiet-mode", "--no-admin", "--no-shortcuts", "--only-site", + "--local-install", "--no-version-check", + "--root", "`"$Cygwin`"", "--local-package-dir", "`"$Cache`"", + "--packages", $Packages +) +if ($Setup.ExitCode -ne 0) { throw "Cygwin setup failed: $($Setup.ExitCode)" } +Copy-Item (Join-Path $Cygwin "var/log/setup.log*") $Evidence +Copy-Item (Join-Path $Cygwin "etc/setup/installed.db") $Evidence +& (Join-Path $Cygwin "bin/cygcheck.exe") -cd | Set-Content (Join-Path $Evidence "packages.txt") +if ($LASTEXITCODE -ne 0) { throw "Cannot inventory Cygwin packages" } +& python $SnapshotTool check-installed --inventory (Join-Path $Evidence "packages.txt") +if ($LASTEXITCODE -ne 0) { throw "Installed Cygwin package set differs from the snapshot" } +Get-ChildItem $Cache -File -Recurse | ForEach-Object { + @{ file = [IO.Path]::GetRelativePath($Cache, $_.FullName); bytes = $_.Length; + sha512 = (Get-FileHash $_.FullName -Algorithm SHA512).Hash.ToLowerInvariant() } +} | ConvertTo-Json | Set-Content (Join-Path $Evidence "package-cache.json") + +# Administrative extraction uses TARGETDIR, not a normal MSI installation: +# no product registration or system PATH modification. Fail rather than fall +# back to an ambient executable or ordinary installer if the payload is absent. +$Image = Join-Path $Root "pkgconf-image" +$Extract = Start-Process msiexec.exe -Wait -PassThru -ArgumentList @( + "/a", "`"$(Join-Path $Root 'pkgconf.msi')`"", "/qn", "/norestart", + "TARGETDIR=`"$Image`"", "/L*v", "`"$(Join-Path $Evidence 'pkgconf-extract.log')`"" +) +if ($Extract.ExitCode -ne 0) { throw "pkgconf extraction failed: $($Extract.ExitCode)" } +$Candidates = @(Get-ChildItem $Image -Filter pkgconf.exe -File -Recurse) +if ($Candidates.Count -ne 1) { throw "Expected exactly one native pkgconf executable" } +# Bazel shell-quotes execpaths containing spaces; Cargo executes PKG_CONFIG +# directly. Keep the executable and its support files at a space-free execpath. +$Native = Join-Path $Image "native" +if (Test-Path $Native) { throw "Native pkgconf destination must be fresh" } +Move-Item -LiteralPath $Candidates[0].Directory.FullName -Destination $Native +$PkgConfig = Join-Path $Native "pkgconf.exe" +$Version = & $PkgConfig --version +if ($LASTEXITCODE -ne 0 -or $Version -ne "3.0.6") { throw "Unexpected native pkgconf version" } +@{ target = $Target; cygwin_root = $Cygwin; pkg_config = $PkgConfig; + pkgconf_version = $Version; requested_packages = $Packages } | + ConvertTo-Json | Set-Content (Join-Path $Evidence "tools.json") +# Declare complete installed support trees only after snapshot and tool checks. +& python (Join-Path $PSScriptRoot "voice_windows_tools.py") --root $Root --target $Target --pkg-config $PkgConfig +if ($LASTEXITCODE -ne 0) { throw "Cannot declare Windows Bazel tool inputs" } +"VOICE_WINDOWS_BAZEL_REPOSITORY=$Root" | Out-File $env:GITHUB_ENV -Encoding utf8 -Append +"VOICE_CYGWIN_ROOT=$Cygwin" | Out-File $env:GITHUB_ENV -Encoding utf8 -Append +"VOICE_PKG_CONFIG=$PkgConfig" | Out-File $env:GITHUB_ENV -Encoding utf8 -Append diff --git a/.github/scripts/test_voice_cygwin_inputs.py b/.github/scripts/test_voice_cygwin_inputs.py new file mode 100644 index 0000000000..3b5a4d31ae --- /dev/null +++ b/.github/scripts/test_voice_cygwin_inputs.py @@ -0,0 +1,83 @@ +"""Offline checks for the public Windows build-input bootstrap.""" + +import hashlib +import importlib.util +import io +from pathlib import Path +import tarfile +import tempfile +import unittest + + +SCRIPT = Path(__file__).with_name("voice-cygwin-inputs.py") +SPEC = importlib.util.spec_from_file_location("voice_cygwin_inputs", SCRIPT) +inputs = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(inputs) + + +class VoiceCygwinInputsTests(unittest.TestCase): + def test_input_paths_cannot_escape_the_local_cache(self): + manifest = { + "metadata": [ + {"file": "x86_64/setup.xz"}, + {"file": "x86_64/setup.xz.sig"}, + ], + "packages": [{"file": "../outside.tar.xz"}], + } + with self.assertRaisesRegex(ValueError, "unsafe Cygwin input"): + inputs.records(manifest) + + def test_extract_verifies_archive_and_every_input_before_offline_installation(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + payloads = { + "x86_64/setup.xz": b"metadata", + "x86_64/setup.xz.sig": b"signature", + "x86_64/release/bash/bash.tar.xz": b"package", + } + records = [ + { + "file": name, + "bytes": len(data), + "sha512": hashlib.sha512(data).hexdigest(), + } + for name, data in payloads.items() + ] + archive = root / "inputs.tar.gz" + + def write_archive(contents): + with tarfile.open(archive, "w:gz") as bundle: + for name, data in contents.items(): + info = tarfile.TarInfo(name) + info.size = len(data) + bundle.addfile(info, io.BytesIO(data)) + return { + "bytes": archive.stat().st_size, + "sha256": hashlib.sha256(archive.read_bytes()).hexdigest(), + } + + manifest = { + "metadata": records[:2], + "packages": records[2:], + "archive": write_archive(payloads), + } + destination = root / "cache" + inputs.extract(manifest, archive, destination) + self.assertEqual( + { + record["file"]: (destination / record["file"]).read_bytes() + for record in records + }, + payloads, + ) + + (destination / records[2]["file"]).unlink() + payloads[records[2]["file"]] = b"wrong" + manifest["archive"] = write_archive(payloads) + with self.assertRaisesRegex(ValueError, "Cygwin input mismatch"): + inputs.extract(manifest, archive, root / "rejected") + self.assertFalse((root / "rejected").exists()) + + +if __name__ == "__main__": + unittest.main() diff --git a/.github/scripts/voice-cygwin-inputs.py b/.github/scripts/voice-cygwin-inputs.py new file mode 100644 index 0000000000..11b2ebda7d --- /dev/null +++ b/.github/scripts/voice-cygwin-inputs.py @@ -0,0 +1,98 @@ +"""Verify the pinned Cygwin build-input archive before offline installation.""" + +import argparse +import hashlib +import json +from pathlib import Path, PurePosixPath +import shutil +import tarfile + + +def records(manifest): + metadata = manifest["metadata"] + if {item["file"] for item in metadata} != { + "x86_64/setup.xz", + "x86_64/setup.xz.sig", + }: + raise ValueError("signed setup metadata is required") + entries = metadata + manifest["packages"] + expected = {entry["file"]: entry for entry in entries} + if len(expected) != len(entries): + raise ValueError("duplicate Cygwin input") + for name in expected: + path = PurePosixPath(name) + if ( + not path.parts + or path.is_absolute() + or ".." in path.parts + or path.as_posix() != name + or "\\" in name + or ":" in name + ): + raise ValueError(f"unsafe Cygwin input: {name}") + return expected + + +def extract(manifest, archive, destination): + expected = records(manifest) + pin = manifest["archive"] + with archive.open("rb") as stream: + if ( + archive.stat().st_size != pin["bytes"] + or hashlib.file_digest(stream, "sha256").hexdigest() != pin["sha256"] + ): + raise ValueError("Cygwin archive digest or size mismatch") + stream.seek(0) + with tarfile.open(fileobj=stream, mode="r:gz") as bundle: + members = bundle.getmembers() + if len(members) != len(expected) or {m.name for m in members} != set( + expected + ): + raise ValueError("missing, extra, or duplicate Cygwin inputs") + for member in members: + if not member.isfile() or member.issparse(): + raise ValueError(f"non-regular Cygwin input: {member.name}") + with bundle.extractfile(member) as source: + if ( + member.size != expected[member.name]["bytes"] + or hashlib.file_digest(source, "sha512").hexdigest() + != expected[member.name]["sha512"] + ): + raise ValueError(f"Cygwin input mismatch: {member.name}") + destination.mkdir(parents=True) + try: + bundle.extractall(destination, members=members, filter="data") + except BaseException: + shutil.rmtree(destination) + raise + + +def check_installed(manifest, inventory): + installed = sorted( + tuple(line.split()) + for line in inventory.read_text().splitlines() + if len(line.split()) == 2 and not line.startswith("Package ") + ) + expected = sorted((item["name"], item["version"]) for item in manifest["packages"]) + if installed != expected: + raise ValueError("installed Cygwin packages differ from reviewed inputs") + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("command", choices=("extract", "check-installed")) + parser.add_argument("--archive", type=Path) + parser.add_argument("--directory", type=Path) + parser.add_argument("--inventory", type=Path) + args = parser.parse_args() + pinned = json.loads( + Path(__file__).with_name("voice-cygwin-snapshot.json").read_text() + ) + if args.command == "extract": + if args.archive is None or args.directory is None: + parser.error("extract requires --archive and --directory") + extract(pinned, args.archive, args.directory) + else: + if args.inventory is None: + parser.error("check-installed requires --inventory") + check_installed(pinned, args.inventory) diff --git a/.github/scripts/voice-cygwin-snapshot.json b/.github/scripts/voice-cygwin-snapshot.json new file mode 100644 index 0000000000..8679adca1e --- /dev/null +++ b/.github/scripts/voice-cygwin-snapshot.json @@ -0,0 +1,145 @@ +{ + "schemaVersion": 1, + "sourceMirror": "https://mirrors.kernel.org/sourceware/cygwin/", + "cacheDirectory": "https%3a%2f%2fmirrors.kernel.org%2fsourceware%2fcygwin%2f", + "requestedPackages": [ + "bash", + "make", + "automake1.18", + "coreutils", + "diffutils", + "findutils", + "grep", + "sed", + "gawk" + ], + "signingFingerprint": "56405CF6FCC81574682A5D561A698DE9E2E56300", + "archive": { + "tag": "voice-cygwin-108b38cf67cbb731", + "name": "cygwin-build-inputs.tar.gz", + "bytes": 62276721, + "sha256": "108b38cf67cbb731801e5ca8d8412b8f349961e4e71d8e3d5a1768f1a8d15c50" + }, + "metadata": [ + { + "file": "x86_64/setup.xz", + "bytes": 3598392, + "sha512": "306bffefa1e86178d9c6ff78a8add2e1af3c96f7cd698d62b58d0237a568e1a9f1b30016ed5891f9fe1d1c8a1e58089c3d9cb8989eb7bed758a928f212078e25" + }, + { + "file": "x86_64/setup.xz.sig", + "bytes": 566, + "sha512": "7c466da6446bbef67762ff4038dd4986018ed09432bd6ee6ca9e33150874dbb9d61c7e4b06bd925f932a55fda345401224689bbaddc135d7300651f67ec8160b" + } + ], + "sourceArchive": { + "name": "cygwin-build-sources.tar", + "bytes": 368824320, + "sha256": "13a42ea4e6465f77c32f91e4162f41bcfeab7c0eca47701bb6a8b9aca1eb99f4" + }, + "packages": [ + {"name": "_autorebase", "version": "001091-1", "file": "noarch/release/_autorebase/_autorebase-001091-1.tar.zst", "bytes": 4696, "sha512": "a9416ce992ca57f3a6208f46517520d21dfac61988ac84cbb5b17b76e053a0845c15368ccb8ca9c483d8935f9e3db81568433c0d08d0265ec5e6916ea30f2831"}, + {"name": "alternatives", "version": "1.31-1", "file": "x86_64/release/alternatives/alternatives-1.31-1.tar.xz", "bytes": 123748, "sha512": "94adf3376e7fcaf937594be718f54a99f2a2b66367eb2bb294b6fd4a933a9515954a9e9f309ed74cc62fbb353adb053d8500bbaf42994e4355f7180664afa62c"}, + {"name": "autoconf", "version": "15-3", "file": "noarch/release/autoconf/autoconf-15-3-noarch.tar.zst", "bytes": 2396, "sha512": "edaa6c97c5b50e138acef4b80406bfdd0988da3360cc622be2541593f6e926a4fbe1088ff608ddadfacf275b3924605d54f59e140cc7c79b31f4885e5878ad6f"}, + {"name": "autoconf2.1", "version": "2.13-12", "file": "noarch/release/autoconf2.1/autoconf2.1-2.13-12.tar.bz2", "bytes": 204911, "sha512": "96f8f592fff4665512c0023d6a7844fdc281ba99dc98dee9e55c37523b3a1279ae8fd31559fa6426edb1da7559a483b3eae99ae7911d50646ec60338ef209c18"}, + {"name": "autoconf2.5", "version": "2.69-5", "file": "noarch/release/autoconf2.5/autoconf2.5-2.69-5.tar.zst", "bytes": 341597, "sha512": "3739719f29ad3ab1c1045b72ccbfe6c58e3f7696ac063c2ff4e09e11e1a9015f289fa8fc267a62b43cb2f6b682b83d3b4b6450040e733f9f41d086dc4a2f36b9"}, + {"name": "autoconf2.7", "version": "2.73-1", "file": "noarch/release/autoconf2.7/autoconf2.7-2.73-1-noarch.tar.zst", "bytes": 906344, "sha512": "0493d68a7ea9cd61e403b6b7086b5302bad5bc1cc35715349f59605a795ac7a9f901c444cb387e5f44d1860de3e5d0fcfceee4ee2726d68904cfc5e2db6e90e9"}, + {"name": "automake1.18", "version": "1.18.1-1", "file": "noarch/release/automake1.18/automake1.18-1.18.1-1-noarch.tar.zst", "bytes": 921189, "sha512": "416ef8041505ecddc23e9e72394ef3a81311c6c378b8b6d348e989584c77924d40aa427b47ab4d3a9a2b83438d13995bb7c6302afb73af3d3af7f2e9061579c0"}, + {"name": "base-cygwin", "version": "3.8-2", "file": "noarch/release/base-cygwin/base-cygwin-3.8-2.tar.xz", "bytes": 1260, "sha512": "e03949b4ae6b692858e1d29fb764afbda990c07a6b70b0eb7219fc12f28dfba3cf7e757a7570c31e315be79f091b012d6a6ddafdb4b0b8961afe9af9f4977fab"}, + {"name": "base-files", "version": "4.3-3", "file": "noarch/release/base-files/base-files-4.3-3.tar.zst", "bytes": 46481, "sha512": "dfafbe28fbe89d9b72a2eeb10901712e04de71d9089a85cfd2b8d45ffd739bdd49dadb7b57d098f01b8902456f5b839e9d4a10dbd6a8001573e035fde6797b63"}, + {"name": "bash", "version": "5.2.21-1", "file": "x86_64/release/bash/bash-5.2.21-1.tar.xz", "bytes": 1650932, "sha512": "c3a5594f1c248dd27a3b5aa10c31fb1651072eae878479d3d8cab09c28354ce4509a2fe230f176ad200a6484a561bc10c2dc9c74c4e9f03164190dac2e683d65"}, + {"name": "bzip2", "version": "1.0.8-2", "file": "x86_64/release/bzip2/bzip2-1.0.8-2-x86_64.tar.xz", "bytes": 36396, "sha512": "86e4b0b21b64d17b9fb60260a787626125102b0cb786de67893cb1d39a109457637bdb93956c992429f1135f64b07def64dd0116682a6190b635ba12f81395d7"}, + {"name": "ca-certificates", "version": "2025.2.80_v9.0.304-1", "file": "noarch/release/ca-certificates/ca-certificates-2025.2.80_v9.0.304-1-noarch.tar.zst", "bytes": 902180, "sha512": "49d08f490db12855f8794cd5cfc721940504d36deffdd3b9aea0fc75909e27471f795d48c739630498fd4baeecb76ee0adb5e5c4cecab4db2ea39b03391a02d0"}, + {"name": "coreutils", "version": "9.0-1", "file": "x86_64/release/coreutils/coreutils-9.0-1.tar.xz", "bytes": 2767296, "sha512": "530f47640d878f5953580f3556c492a93786191abe573322026b70206a46c7f17d05927784db3e7f2c044d1555f35941880316f7288b887eaec770a9eda4c687"}, + {"name": "crypto-policies", "version": "20190218-1", "file": "noarch/release/crypto-policies/crypto-policies-20190218-1.tar.xz", "bytes": 14032, "sha512": "44d50ff08528df786eeb26ec1d312445ec92d77ecc9f448a3b741f7647e187f11eabdc0b38f8ab8de803e21d2e610f67eb44de7554f3fa83f6e8172e609b0054"}, + {"name": "cygutils", "version": "1.4.17-4", "file": "x86_64/release/cygutils/cygutils-1.4.17-4-x86_64.tar.xz", "bytes": 90580, "sha512": "a9c63f8ac86e529fa8809c9637bb80651721003160726e2058c361687e5df6377c4cdbea2d023677b23e1d166bbce37d5a412f7d1135baf075ae2631fdeb77b2"}, + {"name": "cygwin", "version": "3.6.10-1", "file": "x86_64/release/cygwin/cygwin-3.6.10-1-x86_64.tar.xz", "bytes": 1626060, "sha512": "50e5a100f5229eecba279799363a30d599f8c6ad06967eabc4fc8ed9105736576a522539abc250110282cf18da8536d4e408823cc6c0d2abd8692a91d9fd3341"}, + {"name": "dash", "version": "0.5.12-5", "file": "x86_64/release/dash/dash-0.5.12-5.tar.xz", "bytes": 79612, "sha512": "8891165224ca8531ad7b601309bc1a74f8b68dad24721060fd3db5d12ae504e3c96bbd0217006bbcbe0650d13283f04c1f58412bde54c40f19acb5291dfaced9"}, + {"name": "diffutils", "version": "3.12-1", "file": "x86_64/release/diffutils/diffutils-3.12-1-x86_64.tar.xz", "bytes": 431400, "sha512": "2a9d8fa0acd5a6108f74afe515f55a2a7cade6fa7f96368e46a6f33cf3b49d270b50dc3cee0320740c8f1729edcbf8dd3824f4b268261fba7aeed0d75a4e943c"}, + {"name": "editrights", "version": "1.04-1", "file": "x86_64/release/editrights/editrights-1.04-1.tar.xz", "bytes": 7112, "sha512": "aadc188201441ee1ff5710df0e5550c8ca5cb65421d025c5845921510560a2875c317d1f35b6ea8abbb5077172c0de7fc8f2f55ae95dc8d1e59d26c963c93aa7"}, + {"name": "file", "version": "5.46-1", "file": "x86_64/release/file/file-5.46-1-x86_64.tar.xz", "bytes": 840940, "sha512": "e4b52477d353557d36af625a6a55c97a11ae41053ce17602624210df2a6d2683f46250c2b9f6db5a9f9e5fcccb7b0be4b24705a36d196cee677bf47d90dfee99"}, + {"name": "findutils", "version": "4.11.0-1", "file": "x86_64/release/findutils/findutils-4.11.0-1-x86_64.tar.xz", "bytes": 768172, "sha512": "cbb1359fd81c55f98da1485f84f11f905a7a3fe9ce1676424e86da4d7f0effa5fc298546bd5c62c9707e8ddf93c31597d6da01e60be0610c50b9b22ef9475814"}, + {"name": "gawk", "version": "5.4.0-1", "file": "x86_64/release/gawk/gawk-5.4.0-1-x86_64.tar.xz", "bytes": 1496256, "sha512": "3604c99921d0cff387279aec1d3b332e8afcd2746b052304c0511221efdb6ae6d4a3117b36593e44adc43c67b6f6c4c48bccf6b686e2e1a5cac644c89613d829"}, + {"name": "getent", "version": "2.18.90-5", "file": "x86_64/release/getent/getent-2.18.90-5.tar.xz", "bytes": 17864, "sha512": "b860f3d01f32da7312a0ce118bdaa53e2418c53f0ac5cfd52b3e4267f353fa21ae27fd2649eb9c6bff48b3d10a0a9f7bcaa2d644f770f0f3916f5d969beedd65"}, + {"name": "gettext-locale-alias", "version": "0.26-1", "file": "x86_64/release/gettext/gettext-locale-alias/gettext-locale-alias-0.26-1-x86_64.tar.xz", "bytes": 1596, "sha512": "289feafbe00598a2451527b64b0187ab62115577ceaa960774b020270e5a8c5bff4038f389a44c7d9eb870f7e2741c7bb2b27be507843cc8c4ae5c33bc74c8f3"}, + {"name": "grep", "version": "3.12-1", "file": "x86_64/release/grep/grep-3.12-1-x86_64.tar.xz", "bytes": 418788, "sha512": "2ba28b5d93c352bc3f3f0f232e2052e6651b71cc535841f4ff8cedb23e61e9e4c030d7d5516ed9c797074e1a59cbf3a56732ca678cb0e986e86dcbbf25955976"}, + {"name": "groff", "version": "1.24.1-1", "file": "x86_64/release/groff/groff-1.24.1-1-x86_64.tar.zst", "bytes": 5878965, "sha512": "9e8aaab0e6f464df3fec6b96fed6679673091aaeafaa463ed518081cb156ef7441fb5f9341a251f602334862640ea0b0fc1b158746c08a1e2529cceeb37e9802"}, + {"name": "gzip", "version": "1.14-1", "file": "x86_64/release/gzip/gzip-1.14-1-x86_64.tar.xz", "bytes": 164124, "sha512": "57995d2e90bf435ffff8f61cd79c4970ad7260e0c407ab52d4dbacb0540a74c7f8fd0d8bd905316eff31525d9b0367284f9f250950f78f179e5260ce59be3a98"}, + {"name": "hostname", "version": "3.13-1", "file": "x86_64/release/hostname/hostname-3.13-1.tar.bz2", "bytes": 14024, "sha512": "75114326fa4575b8341434658a881feb81a6565e7e0adf8fd100dde87022d3818b5fc3df430bd9b3298d818a1d23f6b6c4bc7ff33efb3ded5f0ea0bf118c1aae"}, + {"name": "info", "version": "7.3-1", "file": "x86_64/release/texinfo/info/info-7.3-1-x86_64.tar.zst", "bytes": 607040, "sha512": "3af0e7c2edc34a068d88974c6c1b0f529663f4bbce145b5a33f3b7e90671fd908fb4a544e033b98a6906d3244c64a18732104c5f1d323c0ccac8a5134e13f411"}, + {"name": "ipc-utils", "version": "1.1-1", "file": "x86_64/release/ipc-utils/ipc-utils-1.1-1.tar.xz", "bytes": 11120, "sha512": "374ef40c871d00eb3b4774c383bb85872443a2e8b9abf74b8b9b9b27984598abfb6284716c2d2cd264181e6d012cf7dc8cd04f6bab91f9b083f72e9b51e6226b"}, + {"name": "less", "version": "704-1", "file": "x86_64/release/less/less-704-1-x86_64.tar.xz", "bytes": 171024, "sha512": "2d1ecf29272e7c1781bb0df5b0f0531b009972edc8d246d63e17edf7d8314109f0a8f38668b371c6f5e1dc1f948d63384eecdca5409eacb927c4f267f89f4bfe"}, + {"name": "libargp", "version": "20250917-2", "file": "x86_64/release/libargp/libargp-20250917-2-x86_64.tar.xz", "bytes": 19508, "sha512": "a83ef7c1809cae1b10a50e228643a640ea14e1be324429b9f53be9a4c8cee03ddd0efb46b24e159493dfa36569e73b636bc897943d61cdb90511e5e4cbb54257"}, + {"name": "libattr1", "version": "2.5.2-2", "file": "x86_64/release/attr/libattr1/libattr1-2.5.2-2-x86_64.tar.xz", "bytes": 6160, "sha512": "345b8f84b9c4df2eeea5e211dbaff5e0382aaee788435e1f4cc69f546e1603bbb45e9baec02a37d935674fcd55fb782d924c3a1540d2f829bde4969c96945ce4"}, + {"name": "libblkid1", "version": "2.40.2-2", "file": "x86_64/release/util-linux/libblkid1/libblkid1-2.40.2-2.tar.xz", "bytes": 120936, "sha512": "d931c47cce5a7caf18799a16ed526e0f0d0f82c0f6d0af952420d50d88f1e0f0ea9d6c07be72f9d0baa06a3299b2db8c3bd3c43d13d94400edc816e7c00da148"}, + {"name": "libbz2_1", "version": "1.0.8-2", "file": "x86_64/release/bzip2/libbz2_1/libbz2_1-1.0.8-2-x86_64.tar.xz", "bytes": 26884, "sha512": "d15d88e8e0fae69f7c4c3025515e748c273d758a0f470312162cec46c4cf0d340d2ef036e3a4f4d03b5edf4508b4927834ffa0f9aa45585a5f3bc3b3163ff675"}, + {"name": "libcrypt2", "version": "4.5.2-1", "file": "x86_64/release/libxcrypt/libcrypt2/libcrypt2-4.5.2-1-x86_64.tar.xz", "bytes": 106104, "sha512": "7668d29aaebba8ca94e48e810d92108db72b067b2786cea8dcf4a049c29f20f0aeb8653a6e9524869c69e0b1df9d4d0ac499a052a86f0d44178e8a8167002c97"}, + {"name": "libdb18.1", "version": "18.1.40-1", "file": "x86_64/release/db/libdb18.1/libdb18.1-18.1.40-1.tar.xz", "bytes": 733872, "sha512": "51955ae95823c54051b5345a7dbe6927521411754dff416ad6af2061bd3c9b2230ca2ea88f9b921b4b09b7654c7dac924b0ae08af708a93604cc193d6a18f4ff"}, + {"name": "libfdisk1", "version": "2.40.2-2", "file": "x86_64/release/util-linux/libfdisk1/libfdisk1-2.40.2-2.tar.xz", "bytes": 160264, "sha512": "0c8e6854eb72bb8cc51fc33565ee0d36a747e520e98e074a1d2400672a45651807b53be0f5a1c97efa0ce34975063f86953c9465312dcfdaba8104edb1298d62"}, + {"name": "libffi8", "version": "3.8.0-1", "file": "x86_64/release/libffi/libffi8/libffi8-3.8.0-1-x86_64.tar.xz", "bytes": 196924, "sha512": "935a0567f740a314d70ecd4adc8c4cf5ad37c6fb43f2d26dee882d4fda71f08efb2646bee10cee6e2000f52e25cce87fa29ce07665ba6d22532e0daa76bc887c"}, + {"name": "libgc1", "version": "8.2.12-1", "file": "x86_64/release/libgc/libgc1/libgc1-8.2.12-1-x86_64.tar.xz", "bytes": 62392, "sha512": "faf226ea2b58949da714e2c50f1aa95738082c3499e2ae4828af5173bdd617248f000bf4868636b31a2b76b3a905057ab307c4f6a0d7112f23422531a97be92d"}, + {"name": "libgcc1", "version": "14.4.0-1", "file": "x86_64/release/gcc/libgcc1/libgcc1-14.4.0-1-x86_64.tar.zst", "bytes": 59166, "sha512": "5ac8a56e5eee285efcd6dbd05a473e795e181021d7194b8a98dfe2fa201a71f0abe2908e0b4c81490eb5ef472efaeac8e56a61a5f289eee4ad6e90e52a844ff8"}, + {"name": "libgdbm6", "version": "1.26-1", "file": "x86_64/release/gdbm/libgdbm6/libgdbm6-1.26-1-x86_64.tar.xz", "bytes": 25436, "sha512": "8ed666a5b978b40f8cfc69d270d47cd674a31eb41bac499f783137eedfde6aa02ba4346de3d5dc3184d9af10ec80f57730c7517a88732616f4c6e0ef637d4bce"}, + {"name": "libgdbm_compat4", "version": "1.26-1", "file": "x86_64/release/gdbm/libgdbm_compat4/libgdbm_compat4-1.26-1-x86_64.tar.xz", "bytes": 3892, "sha512": "fd026dd74c495c1ee67b790269e77e8525ad78d52d4895923443feab55f7bb32dc07b115d490143c7e0277167d9cdbef5dad4707ecc7ca6df13a9f1a5184f8ca"}, + {"name": "libgmp10", "version": "6.3.0-1", "file": "x86_64/release/gmp/libgmp10/libgmp10-6.3.0-1.tar.zst", "bytes": 254680, "sha512": "947cd56e1945ddf180622b453f17e6655b4ee3e79fcc91238370a8826b88cd07c31d939f1ed7b87d91344d46fee90af2560105267a9207dcca800cba72459191"}, + {"name": "libguile3.0_1", "version": "3.0.11-1", "file": "x86_64/release/guile3.0/libguile3.0_1/libguile3.0_1-3.0.11-1-x86_64.tar.xz", "bytes": 6969492, "sha512": "a559914ed5d62d0a701e4ce3810276436cf1ce54d579c72fc72c3b4688f2af71ef93f3cdae7089ad7f85107c7ca5c87f2e497cac6f2550b38df1bc6ba7f16230"}, + {"name": "libiconv2", "version": "1.19-2", "file": "x86_64/release/libiconv/libiconv2/libiconv2-1.19-2-x86_64.tar.xz", "bytes": 555664, "sha512": "a1b23e24e0a495e7163923bcf40a4b933403e0826cfc0d9bde785bc92c909b8c4fb94b1e6b2c0242f4328a723855e72ff3baab9f557ef855b7fe7a7d901b0943"}, + {"name": "libintl8", "version": "0.22.5-1", "file": "x86_64/release/gettext/libintl8/libintl8-0.22.5-1.tar.xz", "bytes": 43256, "sha512": "370a19af888bc7c9ed95cf8ade0d36d5e7cdb634abdebcba260cad61fa9a4cc05b5e748e315c9709f66861e8756b227d6757d529846591189e19ee2b2d00002d"}, + {"name": "liblastlog2", "version": "2.40.2-2", "file": "x86_64/release/util-linux/liblastlog2/liblastlog2-2.40.2-2.tar.xz", "bytes": 4908, "sha512": "89350921fe76c9065662dd9d72dd464d2f404f1690e806210cbb828ae8325ea5f3f9909d8eb04c8342b80417a4e1e22c3569241515762ad081ecd58d56927c02"}, + {"name": "liblz4_1", "version": "1.9.4-1", "file": "x86_64/release/lz4/liblz4_1/liblz4_1-1.9.4-1.tar.xz", "bytes": 53296, "sha512": "70e6b0f3af2c5e04089c955b46c13293ab935ee4683e4690da0cebfffba4ba91614004aede99dd804f54e4def93461159f55d151e7fbb647d1ca97ca29e619cc"}, + {"name": "liblzma5", "version": "5.8.3-1", "file": "x86_64/release/xz/liblzma5/liblzma5-5.8.3-1-x86_64.tar.zst", "bytes": 87321, "sha512": "7399c6d117f23788a7edb11cf7f5eec77724aad2bf7cf8a437001fc1515941f6ca415f42363981be35361290f43cd60cd04e15bb1b01cffc0fd275f3167037e0"}, + {"name": "libmpfr6", "version": "4.2.2-1", "file": "x86_64/release/mpfr/libmpfr6/libmpfr6-4.2.2-1.tar.zst", "bytes": 284416, "sha512": "8203dc61bf646041c71f7fece9fe59422ded96ac28dd24826ffb00a9607f274f3226673f9bee441f32b424a7e26c2f178281bd7836db26b0253d37da208d06a8"}, + {"name": "libncursesw10", "version": "6.5+20240427-1", "file": "x86_64/release/ncurses/libncursesw10/libncursesw10-6.5+20240427-1.tar.xz", "bytes": 393928, "sha512": "08102f7a7083cb5663fecc46d819f2ece4123444a8f94c42300f03f7eebd40395f556c44b22ab58bf57321b44d9b8fb8b19358e535c7500d36e52d48b05dcca8"}, + {"name": "libp11-kit0", "version": "0.26.5-1", "file": "x86_64/release/p11-kit/libp11-kit0/libp11-kit0-0.26.5-1-x86_64.tar.xz", "bytes": 212700, "sha512": "dd64f129fe71233e26e0845b6711db5cf2eb2e6d2d050f07b6aec0d9dd54d980e87390614744fbf022cf57aa2b3f7edbdc30c548c2f9d480188c22bcde0c2eba"}, + {"name": "libpcre1", "version": "8.45-1", "file": "x86_64/release/pcre/libpcre1/libpcre1-8.45-1.tar.zst", "bytes": 166711, "sha512": "85c793ed3622a00b811773b94c45376882513e267b9f1d2a3efa0bc42ff86a4e4bebd8362a193d302e66ae9e5c9b93befc1bb1096c0fd397e2cd0f5632c808dd"}, + {"name": "libpcre2_8_0", "version": "10.47-1", "file": "x86_64/release/pcre2/libpcre2_8_0/libpcre2_8_0-10.47-1-x86_64.tar.zst", "bytes": 237134, "sha512": "a98b38e1591c19404a5794cef488810747d86cc27ce5ee326e706cfa7069cf088d8a64571933f4c1f7a611fd8c232008837271bebe2e81c04a3f6ec571f0009e"}, + {"name": "libpipeline1", "version": "1.5.8-1", "file": "x86_64/release/libpipeline/libpipeline1/libpipeline1-1.5.8-1-x86_64.tar.xz", "bytes": 24880, "sha512": "1d062ac4909d37fbac3ce66b07ac691df0ae5acbd01f08c5019a5ed897cb2bbef95f9e347c5bc71185ae3317d89383306e46f2c59acae5d28a18fa65f66d4e80"}, + {"name": "libpopt-common", "version": "1.19-1", "file": "x86_64/release/popt/libpopt-common/libpopt-common-1.19-1.tar.xz", "bytes": 18536, "sha512": "4d69e6d7e196cdb4caad558c5d7543f706a4c6d7053476be861be07672506e378084126f22903deab8113557f757d2a07578f4a790ae316a2ea50ba612148a43"}, + {"name": "libpopt0", "version": "1.19-1", "file": "x86_64/release/popt/libpopt0/libpopt0-1.19-1.tar.xz", "bytes": 18748, "sha512": "e820a7bac13cdd3bff07673ac54dcb80f342c6a32917035f218ef255bdb707d654ee6a1684242d7ce8439eebf050ade6566f27f5118896fbf8e0171173b5bfee"}, + {"name": "libreadline7", "version": "8.2-2", "file": "x86_64/release/readline/libreadline7/libreadline7-8.2-2.tar.xz", "bytes": 113340, "sha512": "1d27576cf49b056f35f90ad933e39c5d946bc271ddcd109daa98f95b97123b428256827889ff45ca8a76409cdb1afd0146e99c9b70150b302538356512acfe38"}, + {"name": "libreadline8", "version": "8.3-1", "file": "x86_64/release/readline/libreadline8/libreadline8-8.3-1-x86_64.tar.xz", "bytes": 120084, "sha512": "64142c5f305b9836c5be9a4271e4163d8b3e739a0451c0d152373eeb3338342f7cf5691e4437b8e6d0a8404c30ea7a21832e342e7a1ca3b5f3e8e3403051b96e"}, + {"name": "libsmartcols1", "version": "2.40.2-2", "file": "x86_64/release/util-linux/libsmartcols1/libsmartcols1-2.40.2-2.tar.xz", "bytes": 96124, "sha512": "71ded6f02d264343d068b90450d96f4fdeb4087f278b990709aaa814c16123f4ae947714e5f10020260c30a6d78865c2d35d9ffe57a51b038815db52c3274317"}, + {"name": "libsqlite3_0", "version": "3.49.1-1", "file": "x86_64/release/sqlite3/libsqlite3_0/libsqlite3_0-3.49.1-1.tar.xz", "bytes": 686212, "sha512": "cd81686fe5778a94ab91e4aebdd2a229cb963318ce7b393e0a0299e29ed7648c24135733c1978cbebbb926813d7f994b9e38318ede12c72fe79ad404dbfdeaf2"}, + {"name": "libssl3", "version": "3.5.7-1", "file": "x86_64/release/openssl/libssl3/libssl3-3.5.7-1-x86_64.tar.zst", "bytes": 2081790, "sha512": "18161f1292404554742ced4beaa10548c7e1bbfb9398ec0b733e632401dfaa49c33336ad4b1e80e2b7bd77c20f00c10196c5804c327e37f641d49da2288837b8"}, + {"name": "libstdc++6", "version": "14.4.0-1", "file": "x86_64/release/gcc/libstdc++6/libstdc++6-14.4.0-1-x86_64.tar.zst", "bytes": 651767, "sha512": "3d86409193c952618eee7e2dea32bdeb97f22ee800e13cc46f98b5095d4d0b1b31ac4336dc5819abb2d553ad1c2151619e9f6f337dfe921ba80fda1323f6f4e3"}, + {"name": "libtasn1_6", "version": "4.21.0-1", "file": "x86_64/release/libtasn1/libtasn1_6/libtasn1_6-4.21.0-1-x86_64.tar.xz", "bytes": 32072, "sha512": "d28713b27d5d41408b29caed785e29380da4d8b4ccdc1c479ae13906e6765bedc6d6b9e6c1c22fd2111d678fcf1663187d3de27d7ad2e104cd48c38086189e36"}, + {"name": "libuchardet0", "version": "0.0.8-1", "file": "x86_64/release/uchardet/libuchardet0/libuchardet0-0.0.8-1.tar.zst", "bytes": 72637, "sha512": "1fad5711590a82a8386c2492286dd9c0ad7cbe086aab249d07ad27277757327a6a67264e9718aef7d30307515462fe67d64f23582f08f0ced85a19cc8e0afe23"}, + {"name": "libunistring-devel", "version": "1.4.1-1", "file": "x86_64/release/libunistring/libunistring-devel/libunistring-devel-1.4.1-1-x86_64.tar.xz", "bytes": 43332, "sha512": "14e067719e5c0a973950955a05685c18d6fa97df5defa8d0915125ea3af1d01ac921c2a84565a49cc7fb785b15a8be4640ee209f3f0d2e13cb542eb6c75b51f9"}, + {"name": "libunistring5", "version": "1.4.1-1", "file": "x86_64/release/libunistring/libunistring5/libunistring5-1.4.1-1-x86_64.tar.xz", "bytes": 498220, "sha512": "cbd454f5068bfb9291d440a3b03b0c58437aa0342a199007d5cce6fa006b6b5ea4d3ba10c136fb0257ffb0d0199db73484275c632506e4ec31c81e1746667b80"}, + {"name": "libuuid1", "version": "2.40.2-2", "file": "x86_64/release/util-linux/libuuid1/libuuid1-2.40.2-2.tar.xz", "bytes": 12532, "sha512": "3ba0c5d5a401c88796077e9f45164532408882f2f5d9f0ab99b2fdf6eb915917250124cde3653a7c53b648866e091aa3baf3304d0b243337f0d4e254a7631f55"}, + {"name": "libzstd1", "version": "1.5.7-1", "file": "x86_64/release/zstd/libzstd1/libzstd1-1.5.7-1.tar.zst", "bytes": 253786, "sha512": "f89cdaa004487ec8c4796b8f56887c81f6f497873c1495b9cb585ffe6c8d6e6b8ad4b52617d5238075f965f1fe001da287d9a00d121590274b2d87672b415a0f"}, + {"name": "login", "version": "1.13-1", "file": "x86_64/release/login/login-1.13-1.tar.xz", "bytes": 17264, "sha512": "66acc3255962a27af8d701a8c02846f3b9180b7b1d65d946f6fb27cd24aeedc8e6a9a2601b419e981c94b356ef376a5c565ea31a4a220e5add9d70ae501a11d1"}, + {"name": "m4", "version": "1.4.21-1", "file": "x86_64/release/m4/m4-1.4.21-1-x86_64.tar.xz", "bytes": 363204, "sha512": "0d86c88d4bbbfc59e136bba3101ca3714139fba2ab7fea2883bc773ca884c3876f8a8f3a2ea8b05c29fd5280c1fb2d686401346f2761160917edf0818a2b232b"}, + {"name": "make", "version": "4.4.1-2", "file": "x86_64/release/make/make-4.4.1-2.tar.xz", "bytes": 598004, "sha512": "9a89a0f4ceadf5c2d0390029d16274fbed0190d2410ad6beb76e39b7153f38d0d1169ad0abf57c96929e6ab0523a0a76c345868e9ae46f9cd3d708d23f6fa3e6"}, + {"name": "man-db", "version": "2.13.1-1", "file": "x86_64/release/man-db/man-db-2.13.1-1-x86_64.tar.zst", "bytes": 1301518, "sha512": "a1e8a0d6d5a75465e32be83c13e685a73f084fa41abc5500d9437837a2c9de2f897827d3516c1076fa6cdd07a3d1aaaa4751f3b393fcfe64c370ff8f6b8b7f84"}, + {"name": "mintty", "version": "3.8.3-1", "file": "x86_64/release/mintty/mintty-3.8.3-1-x86_64.tar.xz", "bytes": 1190356, "sha512": "79068708c81a7bf6abba3ad1cd0550a2c3eb6f6eea21adade8b0d2b77339cd2519716f528159da9f0972f93bb624867e394d782c3e3b0b7e841a8db985395b92"}, + {"name": "ncurses", "version": "6.5+20240427-1", "file": "x86_64/release/ncurses/ncurses-6.5+20240427-1.tar.xz", "bytes": 111396, "sha512": "e15664bdab3f4d82bac6481deaa66476adbe34404937079983b5165930fa18cec3aa0345b93fbb2c390f8304ef20f041cd47d268552b92d01a51adb6abde0fc5"}, + {"name": "openssl", "version": "3.5.7-1", "file": "x86_64/release/openssl/openssl-3.5.7-1-x86_64.tar.zst", "bytes": 1392875, "sha512": "c50821b61eec47d58d1624a4a3d5a4013c80a8b48cdc803d81a656e6859079d321b34fa0ff97db4c093735a76b71a68c8a7f12184b2c4f0677014e910d1ba9d3"}, + {"name": "p11-kit", "version": "0.26.5-1", "file": "x86_64/release/p11-kit/p11-kit-0.26.5-1-x86_64.tar.xz", "bytes": 332724, "sha512": "1f057de789a5553f04649c80e6f78385135b57de7ba078e9dd51f2db60afee37717bb6de9dcf0a330cf72fce1359e08c0186988632b5f858992ebf95739bb840"}, + {"name": "p11-kit-trust", "version": "0.26.5-1", "file": "x86_64/release/p11-kit/p11-kit-trust/p11-kit-trust-0.26.5-1-x86_64.tar.xz", "bytes": 110096, "sha512": "2f98ae11e42f69fa4626638f7cefe2ba2fed808ae618093c1e6c568f8a8be79e6690b40e39e20df02a8d363c371cca8b8320fea7b288e8561cd299f15cc5b796"}, + {"name": "perl", "version": "5.44.0-1", "file": "x86_64/release/perl/perl-5.44.0-1-x86_64.tar.zst", "bytes": 5129424, "sha512": "42f80064ff94ea778319a24e011d4a7236944c79d9db22e0ed64a7ca7d9b65bc2df5998130eb64c50aa95c173937776cc42665174c5717e84fe6dca523368f27"}, + {"name": "perl-Algorithm-Diff", "version": "1.2010-5", "file": "noarch/release/perl-Algorithm-Diff/perl-Algorithm-Diff-1.2010-5-noarch.tar.zst", "bytes": 31580, "sha512": "d840b605043448ceab8ded77313e12653c81fd7db08062a99010fb19b334429c15628b1a0a805f88110e7aae4dcd3f667b515c18870ba873169533663f4374de"}, + {"name": "perl-Archive-Zip", "version": "1.680.0-1", "file": "noarch/release/perl-Archive-Zip/perl-Archive-Zip-1.680.0-1-noarch.tar.zst", "bytes": 85917, "sha512": "de23da6dac8aa8c8218c141c845723d092f6eaf2f1e67ced96d7b0385206c2e335b150bb7066bd0c7d257125ad1b3fc7691686dc535c56429b4a0f4535b6c73a"}, + {"name": "perl-Class-Inspector", "version": "1.360.0-1", "file": "noarch/release/perl-Class-Inspector/perl-Class-Inspector-1.360.0-1-noarch.tar.zst", "bytes": 21261, "sha512": "299489c9d0bc86fe8ba625c0ec0530f0e6ecacab3f67b2826760413f6bc58252c6c11f45583063cfcf0944f9127ed2bece9168fd76220e735841f374a57943f5"}, + {"name": "perl-Devel-Cycle", "version": "1.120.0-1", "file": "noarch/release/perl-Devel-Cycle/perl-Devel-Cycle-1.120.0-1-noarch.tar.zst", "bytes": 8884, "sha512": "308211af5b57106629b168be340fd682e23670fd97cc7c7f80c3bac48223984c1a226148c99cf5d904b09e352f1008f13bd4645897223a162a22af819aa7af11"}, + {"name": "perl-File-ShareDir", "version": "1.118.0-1", "file": "noarch/release/perl-File-ShareDir/perl-File-ShareDir-1.118.0-1-noarch.tar.zst", "bytes": 19857, "sha512": "7cb0f5fe4c327e9f870fc9c303f5e7e1483d2d2d012dbb5487f4b08f350170b7eb0a345766bd0a809bb4b8fa138788112eed2528b92718e817a5f21a9b82774d"}, + {"name": "perl-Text-Diff", "version": "1.450.0-1", "file": "noarch/release/perl-Text-Diff/perl-Text-Diff-1.450.0-1-noarch.tar.zst", "bytes": 30982, "sha512": "c19c80564d50d1d8efd44b156013aa5620b79205fd291897b938735c49214c84867a1337dfe9e7857f97e6f924ec919f5a0197df2d3bfeae78e0ec9dc8b9a6ac"}, + {"name": "perl-Win32-API", "version": "0.840.0-1", "file": "x86_64/release/perl-Win32-API/perl-Win32-API-0.840.0-1-x86_64.tar.zst", "bytes": 84514, "sha512": "758b1881d55942e91365780bede1dca63838995ad6574f9a242c15f613e7e3211a029e9bd3a3bd24d834ca6838e7a85c2812411783423ba23ccf808d094b6890"}, + {"name": "perl_autorebase", "version": "5.44.0-1", "file": "x86_64/release/perl/perl_autorebase/perl_autorebase-5.44.0-1-x86_64.tar.zst", "bytes": 175, "sha512": "55cd94604cfa3e2491df45e5a3715f32b2dee8298a656ca2da8de2eba4096329c595228a8c950760e9641b2c6097309571350c0ffe1c8d13759eb76ec841a4f8"}, + {"name": "perl_base", "version": "5.44.0-1", "file": "x86_64/release/perl/perl_base/perl_base-5.44.0-1-x86_64.tar.zst", "bytes": 3772189, "sha512": "3039e7a60c950cd682d8372f948ec83f7ebea27e9b425ab7406ed2eff0bebb80f7622423cf82b9ab07e34deb183bdf1b56fe129c8306bd2cfb04e7cd96beb992"}, + {"name": "rebase", "version": "4.6.6-1", "file": "x86_64/release/rebase/rebase-4.6.6-1.tar.xz", "bytes": 248832, "sha512": "9f3751c4687b09f3c286cf5c4522a0d976fce2d79ab471ab37560b15f14af84f7d13162b6102c02d0d60784448ff0586e76e9291675af4fa0c9320a3713633da"}, + {"name": "run", "version": "1.3.4-2", "file": "x86_64/release/run/run-1.3.4-2.tar.xz", "bytes": 34492, "sha512": "7e3156bbddc5f934fde3530879aa5186c9fdccb757ba848fc958e79e96534b336975f2882e413d5c18174628033a422157d25c48f8b5e90f0f1555ff2fa6343f"}, + {"name": "sed", "version": "4.10-1", "file": "x86_64/release/sed/sed-4.10-1-x86_64.tar.xz", "bytes": 359252, "sha512": "da5475344a1693f90fb560773ccca48eaf13af0e6bbcada9dcea658862bf7f46043b2d7365de0e2e235fb9a874fe784ad83bc29eb2fdbf3207cd8ad5e127b8b9"}, + {"name": "tar", "version": "1.35-2", "file": "x86_64/release/tar/tar-1.35-2.tar.zst", "bytes": 997616, "sha512": "007c7df12bc27ca14862a925680e2b2ff616a5149262b153889e6f580f3db492f98da49b5f34a8c4234194ee25021b67e9a84c987c96c5d6baa7ee0b3ea02425"}, + {"name": "terminfo", "version": "6.5+20240427-1", "file": "x86_64/release/ncurses/terminfo/terminfo-6.5+20240427-1.tar.xz", "bytes": 94760, "sha512": "128fb04f188bbf2af3060416193c9879a9cbbf7e5486febf851f35c3d9d00b9d61a0e2bf4be575d2308fd665a224ce5674485dcfd071125c3a8d199a4b88a042"}, + {"name": "texinfo", "version": "7.3-1", "file": "x86_64/release/texinfo/texinfo-7.3-1-x86_64.tar.zst", "bytes": 1853455, "sha512": "a7a0da042d05bbf2978857fcad1887552db9d77bf745fb8a17e156a2616500cf711adce107610f008ddff5556d02823f148d2bf146bd7eaa8a9466f9300cf2e6"}, + {"name": "tzcode", "version": "2026c-1", "file": "x86_64/release/tzcode/tzcode-2026c-1-x86_64.tar.xz", "bytes": 185512, "sha512": "85ab0f5d8d7c02e38dbd945c99ee6841774d84c486f857cd8484605068441b2028eea1f42a7e36c8fd6801c454f8f786a16f9caeb422afb30e5fcf324434878b"}, + {"name": "tzdata", "version": "2026c-1", "file": "noarch/release/tzdata/tzdata-2026c-1-noarch.tar.xz", "bytes": 184092, "sha512": "567adb6da01050a84b3785a1cb518e62a9c838cfe55e8ab33610c6ee64355677486329de5781361d08f03cd355acd64f94c64ec2a2686d48538813cc405b638c"}, + {"name": "util-linux", "version": "2.40.2-2", "file": "x86_64/release/util-linux/util-linux-2.40.2-2.tar.xz", "bytes": 2053568, "sha512": "4f3473472e32bff586401ec31f1423237c9cb112ae1140cdd8b2797040c5c182632f49f5354eaaa409d2dacba7767d5baa8b8926d7638e2725ad80065b46c10b"}, + {"name": "vim-minimal", "version": "9.1.1825-1", "file": "x86_64/release/vim/vim-minimal/vim-minimal-9.1.1825-1-x86_64.tar.xz", "bytes": 794064, "sha512": "6a252210e2ef08a7038d6e5bc8e5312e0c25d8328ab0aefd7f292eb79b7a32dd699f15f3f293cfbb83687e87cb3f68a335e969b117f55e6ff99b34ebdfbc8119"}, + {"name": "which", "version": "2.23-1", "file": "x86_64/release/which/which-2.23-1-x86_64.tar.xz", "bytes": 31108, "sha512": "98dffe802e1137d7b54359314797fd10080e6c261f37668e32e46aa7b1587b4148a1647b4f4a00cd0d23a9e90f153d5c2081d0ffa4ccea657b73aaa31b538d6e"}, + {"name": "xz", "version": "5.8.3-1", "file": "x86_64/release/xz/xz-5.8.3-1-x86_64.tar.zst", "bytes": 767298, "sha512": "8918389508ade8237226170b6dd02104938ec53b2bb15b310f5005dd64f954dfd67a5391757ba8602afe2a423f901894204771d6ae335402095c074d098daa76"}, + {"name": "zlib0", "version": "1.3.2-1", "file": "x86_64/release/zlib/zlib0/zlib0-1.3.2-1-x86_64.tar.zst", "bytes": 46254, "sha512": "d79ba6098cadf6bf2f32f35d9cd54729293c64b3c9daabb9d080f52861a19db71544d5710fdbd61ff45e7c2ae4eecb3f2e3b8a1e3bc05695b8cfe01c7639c53a"}, + {"name": "zstd", "version": "1.5.7-1", "file": "x86_64/release/zstd/zstd-1.5.7-1.tar.zst", "bytes": 394600, "sha512": "5de9a2fdee75345a35592444d467a1b70504f09f3b2ee5aaf8af8a9d9fda1a4e77d37c5c4c4fee9b96995ef78f9708e6c02adea7a70d9f80f1238e507b2bc0fc"} + ] +} diff --git a/.github/scripts/voice_windows_tools.py b/.github/scripts/voice_windows_tools.py new file mode 100644 index 0000000000..7dc26ee3fc --- /dev/null +++ b/.github/scripts/voice_windows_tools.py @@ -0,0 +1,80 @@ +"""Expose verified Windows build-tool installations as local Bazel inputs. + +This declares the installed support trees; the caller authenticates their +inputs and verifies the installed package inventory first. +""" + +import argparse +import itertools +import json +from pathlib import Path +import stat + + +def export_repository(root: Path, target: str, pkg_config: Path): + if target not in ("x86_64-pc-windows-msvc", "aarch64-pc-windows-msvc"): + raise ValueError("unsupported Windows target") + root = root.resolve(strict=True) + pkg_config = pkg_config.resolve(strict=True) + names = { + "shell": "cygwin/bin/bash.exe", + "make": "cygwin/bin/make.exe", + "cygpath": "cygwin/bin/cygpath.exe", + "automake": "cygwin/bin/automake-1.18", + "pkg_config": pkg_config.relative_to(root).as_posix(), + } + if not names["pkg_config"].startswith("pkgconf-image/"): + raise ValueError("pkg-config must belong to the extracted image") + outputs = ("BUILD.bazel", "MODULE.bazel", "voice-tools.json") + if any((root / name).exists() for name in outputs): + raise ValueError("Bazel tool repository outputs must be fresh") + # A nested package boundary would silently remove files from Bazel's glob. + # Reparse points could escape the installation or hide support directories. + for directory in ("cygwin", "pkgconf-image"): + tree = root / directory + if not tree.is_dir(): + raise ValueError(f"missing tool tree: {directory}") + for path in itertools.chain((tree,), tree.rglob("*")): + info = path.lstat() + if ( + stat.S_ISLNK(info.st_mode) + or getattr(info, "st_file_attributes", 0) + & stat.FILE_ATTRIBUTE_REPARSE_POINT + or not (stat.S_ISDIR(info.st_mode) or stat.S_ISREG(info.st_mode)) + or path.name.casefold() in ("build", "build.bazel") + ): + raise ValueError(f"unsupported tool-tree entry: {path}") + if any(not (root / name).is_file() for name in names.values()): + raise ValueError("required Windows build tool missing") + definitions = [ + 'package(default_visibility = ["//visibility:public"])', + 'filegroup(name = "cygwin", srcs = glob(["cygwin/**"], allow_empty = False))', + 'filegroup(name = "pkgconf", srcs = glob(["pkgconf-image/**"], allow_empty = False))', + 'filegroup(name = "tools", srcs = [":cygwin", ":pkgconf", "voice-tools.json"])', + ] + definitions.extend( + f"filegroup(name = {json.dumps(name)}, srcs = [{json.dumps(path)}])" + for name, path in names.items() + ) + metadata = { + "schemaVersion": 1, + "target": target, + "cygwinArchitecture": "x86_64", + "tools": names, + } + for name, contents in ( + ("BUILD.bazel", "\n".join(definitions) + "\n"), + ("MODULE.bazel", 'module(name = "voice_windows_tools")\n'), + ("voice-tools.json", json.dumps(metadata, indent=2) + "\n"), + ): + with (root / name).open("x", encoding="utf-8") as output: + output.write(contents) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--root", type=Path, required=True) + parser.add_argument("--target", required=True) + parser.add_argument("--pkg-config", type=Path, required=True) + args = parser.parse_args() + export_repository(args.root, args.target, args.pkg_config) diff --git a/.github/scripts/watch_voice_bazel.py b/.github/scripts/watch_voice_bazel.py new file mode 100644 index 0000000000..d27b7a2664 --- /dev/null +++ b/.github/scripts/watch_voice_bazel.py @@ -0,0 +1,79 @@ +"""Capture one bounded diagnostic when the ARM64 voice build goes silent.""" + +import os +from pathlib import Path +import shutil +import subprocess +import sys +import threading +import time + + +def diagnose(): + root = Path(os.environ.get("BAZEL_OUTPUT_BASE", "")) + if not root.is_absolute(): + print("Bazel output base unavailable; skipping diagnostics", flush=True) + return + for relative in ("command.log", "server/jvm.out"): + try: + with (root / relative).open("rb") as source: + source.seek(max(0, source.seek(0, 2) - 16384)) + print( + f"Bazel {relative} tail:\n{source.read(16384).decode(errors='replace')}", + flush=True, + ) + except OSError as error: + print(f"Cannot read {relative}: {error}", flush=True) + try: + pid = str(int((root / "server/server.pid.txt").read_text().strip())) + jstack = shutil.which("jstack") + if not jstack: + print("jstack unavailable; log tails retained", flush=True) + return + result = subprocess.run([jstack, pid], capture_output=True, timeout=20) + print( + f"jstack exit {result.returncode}:\n{(result.stdout + result.stderr)[-65536:].decode(errors='replace')}", + flush=True, + ) + except (OSError, ValueError, subprocess.TimeoutExpired) as error: + print(f"Cannot capture JVM stacks: {error}", flush=True) + + +def main(): + with subprocess.Popen( + sys.argv[1:], stdout=subprocess.PIPE, stderr=subprocess.STDOUT + ) as process: + last_output = time.monotonic() + + def forward(): + nonlocal last_output + for line in process.stdout: + last_output = time.monotonic() + sys.stdout.buffer.write(line) + sys.stdout.buffer.flush() + + reader = threading.Thread(target=forward) + reader.start() + captured = False + while process.poll() is None: + if ( + not captured + and os.environ.get("VOICE_ARCH") == "aarch64" + and time.monotonic() - last_output >= 600 + ): + captured = True + print( + "ARM64 Bazel silent for ten minutes; capturing diagnostics", + flush=True, + ) + diagnose() + time.sleep(1) + reader.join() + if process.returncode and not captured: + print("Bazel command failed; capturing diagnostics", flush=True) + diagnose() + return process.returncode + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/.github/workflows/rust-release-windows.yml b/.github/workflows/rust-release-windows.yml index e9a200ff0f..7b529f8cf7 100644 --- a/.github/workflows/rust-release-windows.yml +++ b/.github/workflows/rust-release-windows.yml @@ -109,6 +109,8 @@ jobs: for binary in ${{ matrix.binaries }}; do build_args+=(--bin "$binary") done + STABLE_GIT_COMMIT="$(git rev-parse HEAD)" + export STABLE_GIT_COMMIT cargo build --target "$target" --release --timings "${build_args[@]}" - name: Upload Cargo timings @@ -146,6 +148,134 @@ jobs: path: | ${{ env.CARGO_TARGET_DIR }}/${{ matrix.target }}/release/staged-${{ matrix.bundle }}/* + build-windows-voice: + name: Build Windows voice - ${{ matrix.target }} + runs-on: ${{ matrix.runs_on }} + timeout-minutes: 120 + permissions: + contents: read + strategy: + fail-fast: false + matrix: + include: + - target: x86_64-pc-windows-msvc + arch: x86_64 + python_arch: x64 + runs_on: + group: ${{ github.event.repository.name }}-runners + labels: ${{ github.event.repository.name }}-windows-x64 + - target: aarch64-pc-windows-msvc + arch: aarch64 + python_arch: arm64 + runs_on: + group: ${{ github.event.repository.name }}-runners + labels: ${{ github.event.repository.name }}-windows-arm64 + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + with: + python-version: "3.12" + architecture: ${{ matrix.python_arch }} + - name: Prepare Bazel CI + uses: ./.github/actions/prepare-bazel-ci + with: + target: ${{ matrix.target }} + cache-scope: release-voice-windows + - uses: ./.github/actions/setup-msvc-env + with: + target: ${{ matrix.target }} + - name: Refresh Bazel PATH for the selected MSVC target + shell: pwsh + run: ./.github/scripts/compute-bazel-windows-path.ps1 + - name: Download pinned Windows build inputs + shell: pwsh + env: + GH_TOKEN: ${{ github.token }} + run: | + $manifest = Get-Content -Raw .github/scripts/voice-cygwin-snapshot.json | ConvertFrom-Json + $pin = $manifest.archive + $source = $manifest.sourceArchive + $directory = Join-Path $env:RUNNER_TEMP "voice-cygwin-snapshot" + New-Item -ItemType Directory -Path $directory | Out-Null + gh release download $pin.tag --repo "${{ github.repository }}" --pattern $pin.name --pattern $source.name --dir $directory + if ($LASTEXITCODE -ne 0) { throw "Cannot obtain the public Cygwin inputs and source" } + $sourceFile = Join-Path $directory $source.name + if ((Get-Item $sourceFile).Length -ne $source.bytes -or + (Get-FileHash $sourceFile -Algorithm SHA256).Hash.ToLowerInvariant() -ne $source.sha256) { + throw "Public Cygwin source archive mismatch" + } + "VOICE_CYGWIN_ARCHIVE=$(Join-Path $directory $pin.name)" | Out-File $env:GITHUB_ENV -Encoding utf8 -Append + - name: Verify and install pinned public build tools + shell: pwsh + run: | + & ./.github/scripts/setup-voice-windows.ps1 -Target "${{ matrix.target }}" -SnapshotArchive $env:VOICE_CYGWIN_ARCHIVE + if (-not $env:SystemRoot) { throw "Windows SystemRoot is required for native audio actions" } + "VOICE_WINDOWS_SYSTEM_ROOT=$env:SystemRoot" | Out-File $env:GITHUB_ENV -Encoding utf8 -Append + $hostArch = $env:PROCESSOR_ARCHITEW6432 + if (-not $hostArch) { $hostArch = $env:PROCESSOR_ARCHITECTURE } + if (-not $hostArch) { throw "Windows host architecture is required" } + "VOICE_WINDOWS_HOST_ARCH=$hostArch" | Out-File $env:GITHUB_ENV -Encoding utf8 -Append + - name: Build same-commit native runtime and helper + shell: bash + env: + ARCH: ${{ matrix.arch }} + VOICE_ARCH: ${{ matrix.arch }} + # Avoid Java JIT crashes in the Windows ARM64 voice build. + BAZEL_HOST_JVM_ARG: ${{ matrix.arch == 'aarch64' && '-Xint' || '' }} + BUILDBUDDY_API_KEY: "" + run: | + (cd codex-rs && cargo update --workspace) + python .github/scripts/watch_voice_bazel.py "$(cygpath -w "$BASH")" ./.github/scripts/run-bazel-ci.sh \ + --remote-download-toplevel \ + --print-failed-action-summary \ + --windows-msvc-host-platform \ + -- \ + test -c opt \ + --platforms=//:local_windows_msvc \ + --extra_toolchains=@local_config_cc//:cc-toolchain-x64_windows,@local_config_cc//:cc-toolchain-arm64_windows,//third_party/voice:windows_pkg_config_toolchain,//third_party/voice:windows_cmake_toolchain \ + --repo_env=BAZEL_DO_NOT_DETECT_CPP_TOOLCHAIN=0 \ + --repo_env=BAZEL_MSVC_RUNTIME_VISUAL_STUDIO_EULA=1 \ + --inject_repository="voice_windows_tools=$VOICE_WINDOWS_BAZEL_REPOSITORY" \ + --//third_party/voice:windows_installed_tools=@voice_windows_tools//:tools \ + --action_env="SystemRoot=$VOICE_WINDOWS_SYSTEM_ROOT" \ + --host_action_env="SystemRoot=$VOICE_WINDOWS_SYSTEM_ROOT" \ + --action_env="PROCESSOR_ARCHITECTURE=$VOICE_WINDOWS_HOST_ARCH" \ + --host_action_env="PROCESSOR_ARCHITECTURE=$VOICE_WINDOWS_HOST_ARCH" \ + --output_groups=default,receipt,sdk \ + --workspace_status_command=./scripts/workspace-status.cmd \ + --build_metadata=COMMIT_SHA="$GITHUB_SHA" \ + -- \ + "//third_party/voice:native_runtime_windows_$ARCH" \ + @rules_rust//cargo/private/cargo_build_script_runner:test \ + //codex-rs/voice-host:codex-voice-host + - name: Stage verified voice build output + shell: bash + env: + TARGET: ${{ matrix.target }} + ARCH: ${{ matrix.arch }} + run: | + set -euo pipefail + runtime="bazel-bin/third_party/voice/native_runtime_windows_${ARCH}" + PYTHONPATH=third_party/voice python - "$runtime" "$TARGET" <<'PY' + from pathlib import Path + import sys + from package_runtime import runtime_files + runtime_files(Path(sys.argv[1]).resolve(strict=True), sys.argv[2]) + PY + output="voice-unsigned/${TARGET}" + mkdir -p "$output" + cp -R "$runtime" "$output/runtime" + cp bazel-bin/codex-rs/voice-host/codex-voice-host.exe "$output/codex-voice-host.exe" + chmod -R u+w "$output" + tar -C "$output" -czf "voice-unsigned-${TARGET}.tar.gz" runtime codex-voice-host.exe + - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + with: + name: voice-${{ matrix.target }}-unsigned + path: voice-unsigned-${{ matrix.target }}.tar.gz + if-no-files-found: error + build-windows-symbols: needs: - build-windows-binaries @@ -193,12 +323,13 @@ jobs: build-windows: needs: - build-windows-binaries + - build-windows-voice name: Build - ${{ matrix.runner }} - ${{ matrix.target }} runs-on: ${{ matrix.runs_on }} environment: name: azure-artifact-signing deployment: false - timeout-minutes: 90 + timeout-minutes: 120 permissions: contents: read id-token: write @@ -265,6 +396,76 @@ jobs: account-name: ${{ secrets.AZURE_ARTIFACT_SIGNING_ACCOUNT_NAME }} certificate-profile-name: ${{ secrets.AZURE_ARTIFACT_SIGNING_CERTIFICATE_PROFILE_NAME }} + - name: Download matching native voice build + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: voice-${{ matrix.target }}-unsigned + path: ${{ runner.temp }}/unsigned-voice + + - name: Stage voice files for signing + id: voice_files + shell: bash + run: | + set -euo pipefail + target="${{ matrix.target }}" + unsigned="${RUNNER_TEMP}/unsigned-voice/extracted" + signed="${GITHUB_WORKSPACE}/signed-voice/${target}" + mkdir -p "$unsigned" "$signed" + tar -xzf "$(cygpath -u "${RUNNER_TEMP}/unsigned-voice/voice-unsigned-${target}.tar.gz")" -C "$(cygpath -u "$unsigned")" + python "${GITHUB_WORKSPACE}/third_party/voice/release_runtime.py" stage \ + --source "$unsigned/runtime" --target "$target" --output "$signed/runtime" + cp "$unsigned/codex-voice-host.exe" "$signed/codex-voice-host.exe" + chmod -R u+w "$signed" + { + echo 'files<> "$GITHUB_OUTPUT" + + - name: Sign Windows voice helper and native DLLs + uses: azure/trusted-signing-action@1d365fec12862c4aa68fcac418143d73f0cea293 # v0.5.11 + with: + endpoint: ${{ secrets.AZURE_ARTIFACT_SIGNING_ENDPOINT }} + trusted-signing-account-name: ${{ secrets.AZURE_ARTIFACT_SIGNING_ACCOUNT_NAME }} + certificate-profile-name: ${{ secrets.AZURE_ARTIFACT_SIGNING_CERTIFICATE_PROFILE_NAME }} + exclude-environment-credential: true + exclude-workload-identity-credential: true + exclude-managed-identity-credential: true + exclude-shared-token-cache-credential: true + exclude-visual-studio-credential: true + exclude-visual-studio-code-credential: true + exclude-azure-cli-credential: false + exclude-azure-powershell-credential: true + exclude-azure-developer-cli-credential: true + exclude-interactive-browser-credential: true + cache-dependencies: false + files: ${{ steps.voice_files.outputs.files }} + + - uses: ./.github/actions/setup-msvc-env + with: + target: ${{ matrix.target }} + + - name: Seal and verify signed voice output + shell: pwsh + run: | + $target = "${{ matrix.target }}" + $signed = Join-Path $env:GITHUB_WORKSPACE "signed-voice/$target" + python (Join-Path $env:GITHUB_WORKSPACE 'third_party/voice/windows_crt.py') --root (Join-Path $signed 'runtime') --target $target --helper (Join-Path $signed 'codex-voice-host.exe') + if ($LASTEXITCODE -ne 0) { throw 'Cannot stage verified Microsoft CRT' } + $files = @(Join-Path $signed "codex-voice-host.exe") + @( + Get-ChildItem (Join-Path $signed "runtime/bin") -Filter *.dll -File | + ForEach-Object FullName + ) + if ($files.Count -lt 8) { throw "Voice DLL closure is incomplete" } + foreach ($file in $files) { + $signature = Get-AuthenticodeSignature $file + if ($signature.Status -ne 'Valid') { throw "Unsigned voice file: $file ($($signature.Status))" } + } + python (Join-Path $env:GITHUB_WORKSPACE 'third_party/voice/release_runtime.py') seal --target $target --output (Join-Path $signed 'runtime') + if ($LASTEXITCODE -ne 0) { throw 'Cannot seal signed voice runtime' } + "VOICE_RELEASE_DIR=$signed" | Out-File $env:GITHUB_ENV -Encoding utf8 -Append + - name: Stage artifacts shell: bash run: | @@ -282,22 +483,34 @@ jobs: set -euo pipefail target="${{ matrix.target }}" archive_script="${GITHUB_WORKSPACE}/.github/scripts/build-codex-package-archive.sh" - temp_root="${RUNNER_TEMP}/codex-package-archives" + voice_args=(--voice-release-dir "$VOICE_RELEASE_DIR") + voice_args+=(--release-version "${GITHUB_REF_NAME#rust-v}") + bash "$archive_script" \ + --target "$target" --bundle primary \ + --entrypoint-dir "$CARGO_TARGET_DIR/$target/release" \ + --archive-dir "dist/$target" "${voice_args[@]}" + bash "$archive_script" \ + --target "$target" --bundle app-server \ + --entrypoint-dir "$CARGO_TARGET_DIR/$target/release" \ + --archive-dir "dist/$target" - # The package helper rewrites cached DotSlash executables. Keep the - # concurrent processes in separate temp roots because Windows cannot - # replace an executable while another process is using it. - mkdir -p "$temp_root/primary" "$temp_root/app-server" - printf '%s\0' primary app-server | - xargs -0 -P0 -I{} env \ - TMPDIR="$temp_root/{}" \ - TMP="$temp_root/{}" \ - TEMP="$temp_root/{}" \ - bash "$archive_script" \ - --target "$target" \ - --bundle "{}" \ - --entrypoint-dir "$CARGO_TARGET_DIR/$target/release" \ - --archive-dir "dist/$target" + - name: Verify packaged Windows voice closure + shell: pwsh + run: | + $target = "${{ matrix.target }}" + $package = Join-Path $env:RUNNER_TEMP "verify-voice-archive-$target" + New-Item -ItemType Directory -Path $package | Out-Null + tar -xzf "dist/$target/codex-package-$target.tar.gz" -C $package + if ($LASTEXITCODE -ne 0) { throw "Cannot extract the final Windows package" } + $voice = Join-Path $package 'codex-resources/voice' + $helper = Join-Path $voice 'bin/codex-voice-host.exe' + if (-not (Test-Path $helper) -or -not (Test-Path (Join-Path $voice 'bin/gstreamer-1.0-0.dll'))) { + throw 'Signed Windows voice files are missing from the canonical package' + } + $signature = Get-AuthenticodeSignature $helper + if ($signature.Status -ne 'Valid') { throw "Packaged helper signature: $($signature.Status)" } + python -c 'import sys; from pathlib import Path; sys.path.insert(0, sys.argv[3]); from package_runtime import runtime_files; runtime_files(Path(sys.argv[1]), sys.argv[2], public_release=True)' $voice $target (Join-Path $env:GITHUB_WORKSPACE 'third_party/voice') + if ($LASTEXITCODE -ne 0) { throw 'Packaged runtime receipt verification failed' } - name: Build Python runtime wheel shell: bash @@ -320,12 +533,25 @@ jobs: python -m venv "${RUNNER_TEMP}/python-runtime-build-venv" "${RUNNER_TEMP}/python-runtime-build-venv/Scripts/python.exe" -m pip install build + # The wheel's Windows support floor predates the native voice + # runtime's verified device/VC++ prerequisites. Keep it voice-free. + wheel_archives="${RUNNER_TEMP}/voice-free-wheel/${{ matrix.target }}" + bash "${GITHUB_WORKSPACE}/.github/scripts/build-codex-package-archive.sh" \ + --target "${{ matrix.target }}" --bundle primary \ + --entrypoint-dir "$CARGO_TARGET_DIR/${{ matrix.target }}/release" \ + --archive-dir "$wheel_archives" + python - "$wheel_archives/codex-package-${{ matrix.target }}.tar.gz" <<'PY' + import sys + import tarfile + with tarfile.open(sys.argv[1]) as archive: + assert not any("codex-resources/voice/" in member.name for member in archive) + PY stage_dir="${RUNNER_TEMP}/openai-codex-cli-bin-${{ matrix.target }}" wheel_dir="${GITHUB_WORKSPACE}/python-runtime-dist/${{ matrix.target }}" python "${GITHUB_WORKSPACE}/sdk/python/scripts/update_sdk_artifacts.py" \ stage-runtime \ "$stage_dir" \ - "dist/${{ matrix.target }}/codex-package-${{ matrix.target }}.tar.gz" \ + "$wheel_archives/codex-package-${{ matrix.target }}.tar.gz" \ --codex-version "${GITHUB_REF_NAME}" \ --platform-tag "$platform_tag" "${RUNNER_TEMP}/python-runtime-build-venv/Scripts/python.exe" -m build --wheel --outdir "$wheel_dir" "$stage_dir" @@ -378,25 +604,12 @@ jobs: # Must run from inside the dest dir so 7z does not embed the # directory path inside the zip. if [[ "$base" == "codex-${target}.exe" ]]; then - # Bundle the sandbox helper binaries into the main codex zip so - # WinGet installs include the required helpers next to codex.exe. - # Fall back to the single-binary zip if the helpers are missing - # to avoid breaking releases. + # Preserve WinGet executable paths while including the signed + # runtime, resources, and package metadata from the canonical tar. bundle_dir="$(mktemp -d)" - runner_src="$dest/codex-command-runner-${target}.exe" - setup_src="$dest/codex-windows-sandbox-setup-${target}.exe" - if [[ -f "$runner_src" && -f "$setup_src" ]]; then - cp "$dest/$base" "$bundle_dir/$base" - cp "$runner_src" "$bundle_dir/codex-command-runner.exe" - cp "$setup_src" "$bundle_dir/codex-windows-sandbox-setup.exe" - # Use an absolute path so bundle zips land in the real dist - # dir even when 7z runs from a temp directory. - (cd "$bundle_dir" && 7z a "$repo_root/$dest/${base}.zip" .) - else - echo "warning: missing sandbox binaries; falling back to single-binary zip" - echo "warning: expected $runner_src and $setup_src" - (cd "$dest" && 7z a "${base}.zip" "$base") - fi + tar -xzf "$dest/codex-package-${target}.tar.gz" -C "$bundle_dir" + python "${GITHUB_WORKSPACE}/scripts/build_winget_package.py" "$(cygpath -w "$bundle_dir")" + (cd "$bundle_dir" && 7z a "$repo_root/$dest/${base}.zip" .) rm -rf "$bundle_dir" else (cd "$dest" && 7z a "${base}.zip" "$base") diff --git a/MODULE.bazel b/MODULE.bazel index e014c6d32c..7854947538 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -165,6 +165,7 @@ single_version_override( patches = [ "//patches:rules_foreign_cc_make_cppflags.patch", "//patches:rules_foreign_cc_make_xcompile.patch", + "//patches:rules_foreign_cc_make_msvc_stdint.patch", ], version = "0.15.1", ) @@ -208,6 +209,7 @@ rules_rust.patch( "//patches:rules_rust_windows_process_wrapper_skip_temp_outputs.patch", # Group build-script argument files to avoid Windows command-line limits. "//patches:rules_rust_group_build_script_arg_files.patch", + "//patches:rules_rust_windows_execroot_separators.patch", ], strip = 1, ) diff --git a/MODULE.bazel.lock b/MODULE.bazel.lock index d09b0fb5bb..f6208f82d0 100644 --- a/MODULE.bazel.lock +++ b/MODULE.bazel.lock @@ -1663,6 +1663,8 @@ "rustls-native-certs_0.8.3": "{\"dependencies\":[{\"name\":\"openssl-probe\",\"req\":\"^0.2\",\"target\":\"cfg(all(unix, not(target_os = \\\"macos\\\")))\"},{\"features\":[\"std\"],\"name\":\"pki-types\",\"package\":\"rustls-pki-types\",\"req\":\"^1.10\"},{\"kind\":\"dev\",\"name\":\"ring\",\"req\":\"^0.17\"},{\"kind\":\"dev\",\"name\":\"rustls\",\"req\":\"^0.23\"},{\"kind\":\"dev\",\"name\":\"rustls-webpki\",\"req\":\"^0.103\"},{\"name\":\"schannel\",\"req\":\"^0.1\",\"target\":\"cfg(windows)\"},{\"name\":\"security-framework\",\"req\":\"^3\",\"target\":\"cfg(target_os = \\\"macos\\\")\"},{\"kind\":\"dev\",\"name\":\"serial_test\",\"req\":\"^3\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3.5\"},{\"kind\":\"dev\",\"name\":\"untrusted\",\"req\":\"^0.9\"},{\"kind\":\"dev\",\"name\":\"webpki-roots\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"x509-parser\",\"req\":\"^0.18\"}],\"features\":{}}", "rustls-pki-types_1.14.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"crabgrind\",\"req\":\"=0.1.9\",\"target\":\"cfg(all(target_os = \\\"linux\\\", target_arch = \\\"x86_64\\\"))\"},{\"name\":\"web-time\",\"optional\":true,\"req\":\"^1\",\"target\":\"cfg(all(target_family = \\\"wasm\\\", target_os = \\\"unknown\\\"))\"},{\"name\":\"zeroize\",\"optional\":true,\"req\":\"^1\"}],\"features\":{\"alloc\":[\"dep:zeroize\"],\"default\":[\"alloc\"],\"std\":[\"alloc\"],\"web\":[\"web-time\"]}}", "rustls-pki-types_1.14.1": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"crabgrind\",\"req\":\"=0.1.9\",\"target\":\"cfg(all(target_os = \\\"linux\\\", target_arch = \\\"x86_64\\\"))\"},{\"name\":\"web-time\",\"optional\":true,\"req\":\"^1\",\"target\":\"cfg(all(target_family = \\\"wasm\\\", target_os = \\\"unknown\\\"))\"},{\"name\":\"zeroize\",\"optional\":true,\"req\":\"^1\"}],\"features\":{\"alloc\":[\"dep:zeroize\"],\"default\":[\"alloc\"],\"std\":[\"alloc\"],\"web\":[\"web-time\"]}}", + "rustls-platform-verifier-android_0.1.1": "{\"dependencies\":[],\"features\":{}}", + "rustls-platform-verifier_0.7.0": "{\"dependencies\":[{\"name\":\"android_logger\",\"optional\":true,\"req\":\"^0.15\",\"target\":\"cfg(target_os = \\\"android\\\")\"},{\"name\":\"base64\",\"optional\":true,\"req\":\"^0.22\"},{\"name\":\"core-foundation\",\"req\":\"^0.10\",\"target\":\"cfg(any(target_vendor = \\\"apple\\\"))\"},{\"name\":\"core-foundation-sys\",\"req\":\"^0.8\",\"target\":\"cfg(any(target_vendor = \\\"apple\\\"))\"},{\"default_features\":false,\"name\":\"jni\",\"req\":\"^0.22\",\"target\":\"cfg(target_os = \\\"android\\\")\"},{\"default_features\":false,\"name\":\"jni\",\"optional\":true,\"req\":\"^0.22.4\"},{\"name\":\"log\",\"req\":\"^0.4\"},{\"name\":\"once_cell\",\"req\":\"^1.9\",\"target\":\"cfg(target_os = \\\"android\\\")\"},{\"name\":\"once_cell\",\"optional\":true,\"req\":\"^1.9\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"rustls\",\"req\":\"^0.23.27\"},{\"default_features\":false,\"features\":[\"ring\"],\"kind\":\"dev\",\"name\":\"rustls\",\"req\":\"^0.23\"},{\"name\":\"rustls-native-certs\",\"req\":\"^0.8\",\"target\":\"cfg(all(unix, not(target_os = \\\"android\\\"), not(target_vendor = \\\"apple\\\"), not(target_arch = \\\"wasm32\\\")))\"},{\"name\":\"rustls-platform-verifier-android\",\"req\":\"^0.1.0\",\"target\":\"cfg(target_os = \\\"android\\\")\"},{\"name\":\"security-framework\",\"req\":\"^3.5.0\",\"target\":\"cfg(any(target_vendor = \\\"apple\\\"))\"},{\"name\":\"security-framework-sys\",\"req\":\"^2.15\",\"target\":\"cfg(any(target_vendor = \\\"apple\\\"))\"},{\"default_features\":false,\"name\":\"webpki\",\"package\":\"rustls-webpki\",\"req\":\"^0.103\",\"target\":\"cfg(all(unix, not(target_os = \\\"android\\\"), not(target_vendor = \\\"apple\\\"), not(target_arch = \\\"wasm32\\\")))\"},{\"default_features\":false,\"name\":\"webpki\",\"package\":\"rustls-webpki\",\"req\":\"^0.103\",\"target\":\"cfg(target_arch = \\\"wasm32\\\")\"},{\"default_features\":false,\"name\":\"webpki\",\"package\":\"rustls-webpki\",\"req\":\"^0.103\",\"target\":\"cfg(target_os = \\\"android\\\")\"},{\"name\":\"webpki-root-certs\",\"req\":\"^1\",\"target\":\"cfg(target_arch = \\\"wasm32\\\")\"},{\"kind\":\"dev\",\"name\":\"webpki-root-certs\",\"req\":\"^1\"},{\"default_features\":false,\"features\":[\"Win32_Foundation\",\"Win32_Security_Cryptography\"],\"name\":\"windows-sys\",\"req\":\">=0.52.0, <0.62.0\",\"target\":\"cfg(windows)\"}],\"features\":{\"cert-logging\":[\"base64\"],\"dbg\":[],\"docsrs\":[\"jni\",\"once_cell\"],\"ffi-testing\":[\"android_logger\",\"rustls/ring\"]}}", "rustls-webpki_0.103.13": "{\"dependencies\":[{\"default_features\":false,\"name\":\"aws-lc-rs\",\"optional\":true,\"req\":\"^1.14\"},{\"kind\":\"dev\",\"name\":\"base64\",\"req\":\"^0.22\"},{\"kind\":\"dev\",\"name\":\"bencher\",\"req\":\"^0.1.5\"},{\"kind\":\"dev\",\"name\":\"bzip2\",\"req\":\"^0.6\"},{\"kind\":\"dev\",\"name\":\"once_cell\",\"req\":\"^1.17.2\"},{\"default_features\":false,\"name\":\"pki-types\",\"package\":\"rustls-pki-types\",\"req\":\"^1.12\"},{\"default_features\":false,\"features\":[\"aws_lc_rs\"],\"kind\":\"dev\",\"name\":\"rcgen\",\"req\":\"^0.14.2\"},{\"default_features\":false,\"name\":\"ring\",\"optional\":true,\"req\":\"^0.17\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0\"},{\"name\":\"untrusted\",\"req\":\"^0.9\"},{\"kind\":\"dev\",\"name\":\"x509-parser\",\"req\":\"^0.18.1\"}],\"features\":{\"alloc\":[\"ring?/alloc\",\"pki-types/alloc\"],\"aws-lc-rs\":[\"dep:aws-lc-rs\",\"aws-lc-rs/aws-lc-sys\",\"aws-lc-rs/prebuilt-nasm\"],\"aws-lc-rs-fips\":[\"dep:aws-lc-rs\",\"aws-lc-rs/fips\"],\"aws-lc-rs-unstable\":[\"aws-lc-rs\",\"aws-lc-rs/unstable\"],\"default\":[\"std\"],\"ring\":[\"dep:ring\"],\"std\":[\"alloc\",\"pki-types/std\"]}}", "rustls_0.23.36": "{\"dependencies\":[{\"default_features\":false,\"name\":\"aws-lc-rs\",\"optional\":true,\"req\":\"^1.14\"},{\"kind\":\"dev\",\"name\":\"base64\",\"req\":\"^0.22\"},{\"kind\":\"dev\",\"name\":\"bencher\",\"req\":\"^0.1.5\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"brotli\",\"optional\":true,\"req\":\"^8\"},{\"name\":\"brotli-decompressor\",\"optional\":true,\"req\":\"^5.0.0\"},{\"kind\":\"dev\",\"name\":\"env_logger\",\"req\":\"^0.11\"},{\"default_features\":false,\"features\":[\"default-hasher\",\"inline-more\"],\"name\":\"hashbrown\",\"optional\":true,\"req\":\"^0.15\"},{\"kind\":\"dev\",\"name\":\"hex\",\"req\":\"^0.4\"},{\"name\":\"log\",\"optional\":true,\"req\":\"^0.4.8\"},{\"kind\":\"dev\",\"name\":\"log\",\"req\":\"^0.4.8\"},{\"kind\":\"dev\",\"name\":\"macro_rules_attribute\",\"req\":\"^0.2\"},{\"kind\":\"dev\",\"name\":\"num-bigint\",\"req\":\"^0.4.4\"},{\"default_features\":false,\"features\":[\"alloc\",\"race\"],\"name\":\"once_cell\",\"req\":\"^1.16\"},{\"features\":[\"alloc\"],\"name\":\"pki-types\",\"package\":\"rustls-pki-types\",\"req\":\"^1.12\"},{\"default_features\":false,\"features\":[\"pem\",\"aws_lc_rs\"],\"kind\":\"dev\",\"name\":\"rcgen\",\"req\":\"^0.14\"},{\"name\":\"ring\",\"optional\":true,\"req\":\"^0.17\"},{\"kind\":\"build\",\"name\":\"rustversion\",\"optional\":true,\"req\":\"^1.0.6\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1\"},{\"default_features\":false,\"name\":\"subtle\",\"req\":\"^2.5.0\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"time\",\"req\":\"^0.3.6\"},{\"default_features\":false,\"features\":[\"alloc\"],\"name\":\"webpki\",\"package\":\"rustls-webpki\",\"req\":\"^0.103.5\"},{\"kind\":\"dev\",\"name\":\"webpki-roots\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"x509-parser\",\"req\":\"^0.17\"},{\"name\":\"zeroize\",\"req\":\"^1.8\"},{\"name\":\"zlib-rs\",\"optional\":true,\"req\":\"^0.5\"}],\"features\":{\"aws-lc-rs\":[\"aws_lc_rs\"],\"aws_lc_rs\":[\"dep:aws-lc-rs\",\"webpki/aws-lc-rs\",\"aws-lc-rs/aws-lc-sys\",\"aws-lc-rs/prebuilt-nasm\"],\"brotli\":[\"dep:brotli\",\"dep:brotli-decompressor\",\"std\"],\"custom-provider\":[],\"default\":[\"aws_lc_rs\",\"logging\",\"prefer-post-quantum\",\"std\",\"tls12\"],\"fips\":[\"aws_lc_rs\",\"aws-lc-rs?/fips\",\"webpki/aws-lc-rs-fips\"],\"logging\":[\"log\"],\"prefer-post-quantum\":[\"aws_lc_rs\"],\"read_buf\":[\"rustversion\",\"std\"],\"ring\":[\"dep:ring\",\"webpki/ring\"],\"std\":[\"webpki/std\",\"pki-types/std\",\"once_cell/std\"],\"tls12\":[],\"zlib\":[\"dep:zlib-rs\"]}}", "rustls_0.23.40": "{\"dependencies\":[{\"default_features\":false,\"name\":\"aws-lc-rs\",\"optional\":true,\"req\":\"^1.14\"},{\"kind\":\"dev\",\"name\":\"base64\",\"req\":\"^0.22\"},{\"kind\":\"dev\",\"name\":\"bencher\",\"req\":\"^0.1.5\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"brotli\",\"optional\":true,\"req\":\"^8\"},{\"name\":\"brotli-decompressor\",\"optional\":true,\"req\":\"^5.0.0\"},{\"kind\":\"dev\",\"name\":\"env_logger\",\"req\":\"^0.11\"},{\"default_features\":false,\"features\":[\"default-hasher\",\"inline-more\"],\"name\":\"hashbrown\",\"optional\":true,\"req\":\"^0.15\"},{\"kind\":\"dev\",\"name\":\"hex\",\"req\":\"^0.4\"},{\"name\":\"log\",\"optional\":true,\"req\":\"^0.4.8\"},{\"kind\":\"dev\",\"name\":\"log\",\"req\":\"^0.4.8\"},{\"kind\":\"dev\",\"name\":\"macro_rules_attribute\",\"req\":\"^0.2\"},{\"kind\":\"dev\",\"name\":\"num-bigint\",\"req\":\"^0.4.4\"},{\"default_features\":false,\"features\":[\"alloc\",\"race\"],\"name\":\"once_cell\",\"req\":\"^1.16\"},{\"features\":[\"alloc\"],\"name\":\"pki-types\",\"package\":\"rustls-pki-types\",\"req\":\"^1.12\"},{\"default_features\":false,\"features\":[\"pem\",\"aws_lc_rs\"],\"kind\":\"dev\",\"name\":\"rcgen\",\"req\":\"^0.14\"},{\"name\":\"ring\",\"optional\":true,\"req\":\"^0.17\"},{\"kind\":\"build\",\"name\":\"rustversion\",\"optional\":true,\"req\":\"^1.0.6\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1\"},{\"default_features\":false,\"name\":\"subtle\",\"req\":\"^2.5.0\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"time\",\"req\":\"^0.3.6\"},{\"default_features\":false,\"features\":[\"alloc\"],\"name\":\"webpki\",\"package\":\"rustls-webpki\",\"req\":\"^0.103.5\"},{\"kind\":\"dev\",\"name\":\"webpki-roots\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"x509-parser\",\"req\":\"^0.17\"},{\"name\":\"zeroize\",\"req\":\"^1.8\"},{\"name\":\"zlib-rs\",\"optional\":true,\"req\":\"^0.6\"}],\"features\":{\"aws-lc-rs\":[\"aws_lc_rs\"],\"aws_lc_rs\":[\"dep:aws-lc-rs\",\"webpki/aws-lc-rs\",\"aws-lc-rs/aws-lc-sys\",\"aws-lc-rs/prebuilt-nasm\"],\"brotli\":[\"dep:brotli\",\"dep:brotli-decompressor\",\"std\"],\"custom-provider\":[],\"default\":[\"aws_lc_rs\",\"logging\",\"prefer-post-quantum\",\"std\",\"tls12\"],\"fips\":[\"aws_lc_rs\",\"aws-lc-rs?/fips\",\"webpki/aws-lc-rs-fips\"],\"logging\":[\"log\"],\"prefer-post-quantum\":[\"aws_lc_rs\"],\"read_buf\":[\"rustversion\",\"std\"],\"ring\":[\"dep:ring\",\"webpki/ring\"],\"std\":[\"webpki/std\",\"pki-types/std\",\"once_cell/std\"],\"tls12\":[],\"zlib\":[\"dep:zlib-rs\"]}}", diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index c769f0c1ee..222fe09245 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -2147,10 +2147,13 @@ dependencies = [ "futures", "http 1.4.0", "pretty_assertions", + "rcgen", "regex-lite", + "rustls", "schemars 0.8.22", "serde", "serde_json", + "tempfile", "thiserror 2.0.18", "tokio", "tokio-test", @@ -3699,6 +3702,7 @@ dependencies = [ "rustls", "rustls-native-certs", "rustls-pki-types", + "rustls-platform-verifier", "serde", "serde_json", "sha2 0.10.9", @@ -13428,6 +13432,33 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rustls-platform-verifier" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0" +dependencies = [ + "core-foundation 0.10.1", + "core-foundation-sys", + "jni", + "log", + "once_cell", + "rustls", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki", + "security-framework 3.5.1", + "security-framework-sys", + "webpki-root-certs", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls-platform-verifier-android" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" + [[package]] name = "rustls-webpki" version = "0.103.13" diff --git a/codex-rs/codex-api/Cargo.toml b/codex-rs/codex-api/Cargo.toml index 01cacbe2b4..da3f1b7fcb 100644 --- a/codex-rs/codex-api/Cargo.toml +++ b/codex-rs/codex-api/Cargo.toml @@ -34,6 +34,9 @@ uuid = { workspace = true } anyhow = { workspace = true } assert_matches = { workspace = true } pretty_assertions = { workspace = true } +rcgen = { workspace = true } +rustls = { workspace = true } +tempfile = { workspace = true } tokio-test = { workspace = true } wiremock = { workspace = true } diff --git a/codex-rs/codex-api/src/endpoint/realtime_websocket/methods.rs b/codex-rs/codex-api/src/endpoint/realtime_websocket/methods.rs index 293305ea53..fe31a1dbef 100644 --- a/codex-rs/codex-api/src/endpoint/realtime_websocket/methods.rs +++ b/codex-rs/codex-api/src/endpoint/realtime_websocket/methods.rs @@ -965,6 +965,17 @@ impl RealtimeWebsocketClient { let connector = maybe_build_rustls_client_config_with_custom_ca() .map_err(|err| ApiError::Stream(format!("failed to configure websocket TLS: {err}")))? .map(tokio_tungstenite::Connector::Rustls); + // A fresh Windows install may not have downloaded the server's trusted root yet. + // Use platform validation only for system trust, preserving custom CA semantics. + #[cfg(windows)] + let connector = match connector { + Some(connector) => Some(connector), + None => Some(tokio_tungstenite::Connector::Rustls( + codex_http_client::build_windows_platform_tls_config().map_err(|err| { + ApiError::Stream(format!("failed to configure websocket TLS: {err}")) + })?, + )), + }; let (stream, response) = tokio_tungstenite::connect_async_tls_with_config( request, Some(websocket_config()), diff --git a/codex-rs/codex-api/tests/realtime_websocket_tls.rs b/codex-rs/codex-api/tests/realtime_websocket_tls.rs new file mode 100644 index 0000000000..2fe132e24f --- /dev/null +++ b/codex-rs/codex-api/tests/realtime_websocket_tls.rs @@ -0,0 +1,167 @@ +#![allow(clippy::expect_used)] +//! Exercise realtime TLS selection without mutating the test process's trust environment. + +use std::io; +use std::net::TcpListener; +use std::process::Command; +use std::sync::Arc; +use std::time::Duration; +use std::time::Instant; + +use codex_api::Provider; +use codex_api::RealtimeEventParser; +use codex_api::RealtimeOutputModality; +use codex_api::RealtimeSessionConfig; +use codex_api::RealtimeSessionMode; +use codex_api::RealtimeWebsocketClient; +use codex_api::RetryConfig; +use codex_protocol::protocol::RealtimeVoice; +use http::HeaderMap; +use pretty_assertions::assert_eq; + +const ADDRESS_ENV: &str = "CODEX_TEST_REALTIME_TLS_ADDRESS"; +const TRUST_ENV: &str = "CODEX_TEST_REALTIME_TLS_TRUST"; + +#[test] +fn realtime_tls_selects_system_and_custom_trust() { + if let Ok(address) = std::env::var(ADDRESS_ENV) { + let runtime = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .unwrap(); + runtime.block_on(check_connection(address)); + return; + } + + codex_utils_rustls_provider::ensure_rustls_crypto_provider(); + let certificate = rcgen::generate_simple_self_signed(vec!["localhost".to_string()]).unwrap(); + let temp = tempfile::TempDir::new().unwrap(); + let ca = temp.path().join("ca.pem"); + std::fs::write(&ca, certificate.cert.pem()).unwrap(); + let config = Arc::new( + rustls::ServerConfig::builder() + .with_no_client_auth() + .with_single_cert( + vec![certificate.cert.der().clone()], + certificate.signing_key.into(), + ) + .unwrap(), + ); + + for trust in ["system", "custom"] { + let listener = TcpListener::bind("127.0.0.1:0").unwrap(); + let address = listener.local_addr().unwrap(); + listener.set_nonblocking(/*nonblocking*/ true).unwrap(); + let config = config.clone(); + let server = std::thread::spawn(move || { + let deadline = Instant::now() + Duration::from_secs(/*secs*/ 30); + let stream = loop { + match listener.accept() { + Ok((stream, _)) => break stream, + Err(error) if error.kind() == io::ErrorKind::WouldBlock => { + assert!(Instant::now() < deadline, "TLS client did not connect"); + std::thread::sleep(Duration::from_millis(/*millis*/ 10)); + } + Err(error) => panic!("TLS accept failed: {error}"), + } + }; + stream.set_nonblocking(/*nonblocking*/ false).unwrap(); + stream + .set_read_timeout(Some(Duration::from_secs(/*secs*/ 30))) + .unwrap(); + stream + .set_write_timeout(Some(Duration::from_secs(/*secs*/ 30))) + .unwrap(); + let tls = + rustls::StreamOwned::new(rustls::ServerConnection::new(config).unwrap(), stream); + match tungstenite::accept(tls) { + Ok(mut socket) => { + assert_eq!(trust, "custom", "system trust must reject the generated CA"); + let message = socket.read().unwrap().into_text().unwrap(); + let update: serde_json::Value = serde_json::from_str(&message).unwrap(); + assert_eq!(update["type"], "session.update"); + } + Err(_) => assert_eq!(trust, "system", "custom CA must allow WSS"), + } + }); + // Re-execute this test so each connection uses its own CA environment, including on Windows. + let mut child = Command::new(std::env::current_exe().unwrap()); + child + .args([ + "--exact", + "realtime_tls_selects_system_and_custom_trust", + "--nocapture", + ]) + .env(ADDRESS_ENV, format!("localhost:{}", address.port())) + .env(TRUST_ENV, trust) + .env_remove("CODEX_CA_CERTIFICATE") + .env_remove("SSL_CERT_FILE"); + if trust == "custom" { + child.env("CODEX_CA_CERTIFICATE", &ca).env( + "SSL_CERT_FILE", + temp.path().join("missing-lower-priority-ca.pem"), + ); + } + let output = child.output().unwrap(); + server.join().unwrap(); + assert!( + output.status.success(), + "{trust}: {}\n{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + } +} + +async fn check_connection(address: String) { + let client = RealtimeWebsocketClient::new(Provider { + name: "local TLS test".into(), + base_url: format!("https://{address}"), + query_params: None, + headers: HeaderMap::new(), + retry: RetryConfig { + max_attempts: 1, + base_delay: Duration::from_millis(/*millis*/ 1), + retry_429: false, + retry_5xx: false, + retry_transport: false, + }, + stream_idle_timeout: Duration::from_secs(/*secs*/ 5), + }); + let result = tokio::time::timeout( + Duration::from_secs(/*secs*/ 20), + client.connect( + RealtimeSessionConfig { + instructions: "TLS test".into(), + initial_items: Vec::new(), + delegation_ack_filler: None, + model: Some("realtime-test-model".into()), + session_id: None, + event_parser: RealtimeEventParser::V1, + session_mode: RealtimeSessionMode::Conversational, + output_modality: RealtimeOutputModality::Audio, + voice: RealtimeVoice::Cove, + }, + HeaderMap::new(), + HeaderMap::new(), + ), + ) + .await + .expect("TLS connection should finish"); + match std::env::var(TRUST_ENV) + .expect("trust scenario should be set") + .as_str() + { + "custom" => { + result.expect("configured CA should permit realtime WSS"); + } + "system" => { + let error = match result { + Ok(_) => panic!("system trust accepted an untrusted certificate"), + Err(error) => error.to_string(), + }; + assert!(error.to_lowercase().contains("certificate"), "{error}"); + } + other => panic!("unknown trust scenario: {other}"), + } +} diff --git a/codex-rs/http-client/Cargo.toml b/codex-rs/http-client/Cargo.toml index 2d54ca7158..a390c46ab2 100644 --- a/codex-rs/http-client/Cargo.toml +++ b/codex-rs/http-client/Cargo.toml @@ -28,6 +28,7 @@ zstd = { workspace = true } system-configuration = { workspace = true } [target.'cfg(target_os = "windows")'.dependencies] +rustls-platform-verifier = "0.7.0" windows-sys = { version = "0.52", features = [ "Win32_Foundation", "Win32_Networking_WinHttp", diff --git a/codex-rs/http-client/src/custom_ca.rs b/codex-rs/http-client/src/custom_ca.rs index 5a2e51c320..6ca705d0ab 100644 --- a/codex-rs/http-client/src/custom_ca.rs +++ b/codex-rs/http-client/src/custom_ca.rs @@ -818,3 +818,7 @@ mod tests { )); } } + +#[cfg(test)] +#[path = "custom_ca_tls_tests.rs"] +mod tls_tests; diff --git a/codex-rs/http-client/src/custom_ca_tls_tests.rs b/codex-rs/http-client/src/custom_ca_tls_tests.rs new file mode 100644 index 0000000000..fb3c78fe14 --- /dev/null +++ b/codex-rs/http-client/src/custom_ca_tls_tests.rs @@ -0,0 +1,79 @@ +//! Handshake coverage for the custom trust path retained by Windows realtime connections. + +use super::CODEX_CA_CERT_ENV; +use super::ConfiguredCaBundle; +use super::build_rustls_client_config; +use pretty_assertions::assert_eq; +use rcgen::BasicConstraints; +use rcgen::CertificateParams; +use rcgen::CertifiedIssuer; +use rcgen::IsCa; +use rcgen::KeyPair; +use std::sync::Arc; + +#[test] +fn custom_intermediate_trust_preserves_hostname_validation() { + let mut params = CertificateParams::default(); + params.is_ca = IsCa::Ca(BasicConstraints::Unconstrained); + let root = CertifiedIssuer::self_signed(params.clone(), KeyPair::generate().unwrap()).unwrap(); + let intermediate = + CertifiedIssuer::signed_by(params, KeyPair::generate().unwrap(), &root).unwrap(); + let leaf_key = KeyPair::generate().unwrap(); + let leaf = CertificateParams::new(vec!["localhost".to_string()]) + .unwrap() + .signed_by(&leaf_key, &intermediate) + .unwrap(); + let temp = tempfile::TempDir::new().unwrap(); + let path = temp.path().join("intermediate.pem"); + std::fs::write(&path, intermediate.pem()).unwrap(); + let config = build_rustls_client_config(Some(&ConfiguredCaBundle { + source_env: CODEX_CA_CERT_ENV, + path, + })) + .unwrap(); + let server = Arc::new( + rustls::ServerConfig::builder() + .with_no_client_auth() + .with_single_cert(vec![leaf.der().clone()], leaf_key.into()) + .unwrap(), + ); + + assert_eq!( + handshake(config.clone(), server.clone(), "localhost"), + Ok(()) + ); + let error = handshake(config, server, "wrong.example").unwrap_err(); + assert!( + matches!( + error, + rustls::Error::InvalidCertificate( + rustls::CertificateError::NotValidForName + | rustls::CertificateError::NotValidForNameContext { .. } + ) + ), + "{error:?}" + ); +} + +fn handshake( + config: Arc, + server: Arc, + hostname: &'static str, +) -> Result<(), rustls::Error> { + let mut client = rustls::ClientConnection::new(config, hostname.try_into().unwrap()).unwrap(); + let mut server = rustls::ServerConnection::new(server).unwrap(); + for _ in 0..10 { + let mut bytes = Vec::new(); + client.write_tls(&mut bytes).unwrap(); + server.read_tls(&mut bytes.as_slice()).unwrap(); + server.process_new_packets()?; + bytes.clear(); + server.write_tls(&mut bytes).unwrap(); + client.read_tls(&mut bytes.as_slice()).unwrap(); + client.process_new_packets()?; + if !client.is_handshaking() { + return Ok(()); + } + } + panic!("handshake did not complete"); +} diff --git a/codex-rs/http-client/src/lib.rs b/codex-rs/http-client/src/lib.rs index cb7f1209e2..d3e8ac85a6 100644 --- a/codex-rs/http-client/src/lib.rs +++ b/codex-rs/http-client/src/lib.rs @@ -57,3 +57,9 @@ pub use crate::transport::ByteStream; pub use crate::transport::HttpTransport; pub use crate::transport::ReqwestTransport; pub use crate::transport::StreamResponse; + +#[cfg(windows)] +mod windows_tls; + +#[cfg(windows)] +pub use crate::windows_tls::build_windows_platform_tls_config; diff --git a/codex-rs/http-client/src/windows_tls.rs b/codex-rs/http-client/src/windows_tls.rs new file mode 100644 index 0000000000..5b42b37fc3 --- /dev/null +++ b/codex-rs/http-client/src/windows_tls.rs @@ -0,0 +1,22 @@ +//! Windows certificate validation for callers without a custom CA bundle. +//! +//! Unlike a snapshot of installed roots, Windows can retrieve missing trusted roots on demand. +//! Callers must retain the custom-CA configuration path when a bundle is configured. + +use std::sync::Arc; + +use codex_utils_rustls_provider::ensure_rustls_crypto_provider; +use rustls::ClientConfig; +use rustls_platform_verifier::ConfigVerifierExt; + +/// Builds a TLS configuration that validates server certificates using Windows trust policy. +/// +/// This does not load Codex custom CA settings; callers must check those before selecting it. +pub fn build_windows_platform_tls_config() -> Result, rustls::Error> { + ensure_rustls_crypto_provider(); + ClientConfig::with_platform_verifier().map(Arc::new) +} + +#[cfg(test)] +#[path = "windows_tls_tests.rs"] +mod tests; diff --git a/codex-rs/http-client/src/windows_tls_tests.rs b/codex-rs/http-client/src/windows_tls_tests.rs new file mode 100644 index 0000000000..6ddf3b50f7 --- /dev/null +++ b/codex-rs/http-client/src/windows_tls_tests.rs @@ -0,0 +1,58 @@ +//! Certificate rejection coverage for Windows platform TLS. + +use super::build_windows_platform_tls_config; +use std::sync::Arc; + +#[test] +fn platform_tls_rejects_a_self_signed_server() { + let client_config = + build_windows_platform_tls_config().expect("configure platform verification"); + let certified_key = rcgen::generate_simple_self_signed(vec!["localhost".to_string()]) + .expect("generate untrusted certificate"); + let server_config = rustls::ServerConfig::builder() + .with_no_client_auth() + .with_single_cert( + vec![certified_key.cert.der().clone()], + certified_key.signing_key.into(), + ) + .expect("configure test server"); + let mut client = rustls::ClientConnection::new( + client_config, + "localhost".try_into().expect("valid server name"), + ) + .expect("create client"); + let mut server = rustls::ServerConnection::new(Arc::new(server_config)).expect("create server"); + + // Drive the real handshake without opening ports or changing system trust. + for _ in 0..10 { + let mut outbound = Vec::new(); + client + .write_tls(&mut outbound) + .expect("write client records"); + server + .read_tls(&mut outbound.as_slice()) + .expect("read client records"); + server + .process_new_packets() + .expect("process client records"); + outbound.clear(); + server + .write_tls(&mut outbound) + .expect("write server records"); + client + .read_tls(&mut outbound.as_slice()) + .expect("read server records"); + if let Err(error) = client.process_new_packets() { + assert!( + matches!(error, rustls::Error::InvalidCertificate(_)), + "{error:?}" + ); + return; + } + assert!( + client.is_handshaking(), + "untrusted server must not complete TLS" + ); + } + panic!("expected certificate rejection during handshake"); +} diff --git a/codex-rs/install-context/src/bundle_tests.rs b/codex-rs/install-context/src/bundle_tests.rs index 74a416639b..cf49aa69b0 100644 --- a/codex-rs/install-context/src/bundle_tests.rs +++ b/codex-rs/install-context/src/bundle_tests.rs @@ -46,3 +46,31 @@ fn bundle_executable_preserves_package_layout_and_install_method() -> std::io::R ); Ok(()) } + +#[cfg(windows)] +#[test] +fn winget_root_requires_metadata_for_the_actual_executable() -> std::io::Result<()> { + let temp = tempfile::tempdir()?; + let package = canonical_absolute_path(temp.path()).unwrap(); + let name = "codex-x86_64-pc-windows-msvc.exe"; + let executable = package.join(name); + fs::write(&executable, "signed CLI")?; + for directory in [RESOURCES_DIRNAME, PATH_DIRNAME] { + fs::create_dir_all(package.join(directory))?; + } + assert_eq!(CodexPackageLayout::from_exe(executable.as_path()), None); + for entrypoint in ["other.exe", "bin/codex.exe", name] { + fs::write( + package.join(PACKAGE_METADATA_FILENAME), + serde_json::json!({"layoutVersion": 1, "entrypoint": entrypoint}).to_string(), + )?; + let expected = (entrypoint == name).then(|| CodexPackageLayout { + package_dir: package.clone(), + bin_dir: package.clone(), + resources_dir: Some(package.join(RESOURCES_DIRNAME)), + path_dir: Some(package.join(PATH_DIRNAME)), + }); + assert_eq!(CodexPackageLayout::from_exe(executable.as_path()), expected); + } + Ok(()) +} diff --git a/codex-rs/install-context/src/lib.rs b/codex-rs/install-context/src/lib.rs index 699add63db..1a9be86a11 100644 --- a/codex-rs/install-context/src/lib.rs +++ b/codex-rs/install-context/src/lib.rs @@ -246,6 +246,21 @@ impl CodexPackageLayout { fn from_exe(exe_path: &Path) -> Option { let canonical_exe = canonical_absolute_path(exe_path)?; let exe_dir = canonical_exe.parent()?; + // WinGet preserves a target-qualified executable at the package root. + // Only recognize that layout when metadata names this exact executable. + #[cfg(windows)] + if let Ok(contents) = std::fs::read(exe_dir.join(PACKAGE_METADATA_FILENAME)) + && let Ok(metadata) = serde_json::from_slice::(&contents) + && metadata["layoutVersion"] == 1 + && metadata["entrypoint"].as_str().map(OsStr::new) == canonical_exe.file_name() + { + return Some(Self { + resources_dir: existing_dir(exe_dir.join(RESOURCES_DIRNAME)), + path_dir: existing_dir(exe_dir.join(PATH_DIRNAME)), + package_dir: exe_dir.clone(), + bin_dir: exe_dir, + }); + } match exe_dir.file_name() { Some(name) if name == OsStr::new(BIN_DIRNAME) => Self::from_package_bin_dir(exe_dir), Some(name) if name == OsStr::new(RESOURCES_DIRNAME) => { diff --git a/patches/BUILD.bazel b/patches/BUILD.bazel index 2afcf05d1e..7b58251d8a 100644 --- a/patches/BUILD.bazel +++ b/patches/BUILD.bazel @@ -6,11 +6,13 @@ exports_files([ "llvm_windows_mingw_compat.patch", "rules_rust_build_script_tools_transition.patch", "rules_rust_group_build_script_arg_files.patch", + "rules_rust_windows_execroot_separators.patch", "rules_rust_windows_msvc_direct_link_args.patch", "rules_rust_windows_process_wrapper_skip_temp_outputs.patch", "rules_cc_rusty_v8_custom_libcxx.patch", "rules_foreign_cc_make_cppflags.patch", "rules_foreign_cc_make_xcompile.patch", + "rules_foreign_cc_make_msvc_stdint.patch", "rules_rs_build_script_deps_annotation.patch", "rules_rs_windows_msvc_linker.patch", "rules_rs_zlib_snapshot_urls.patch", diff --git a/patches/ring_windows_msvc_include_dirs.patch b/patches/ring_windows_msvc_include_dirs.patch index 06f944ad34..7bb49cbc2c 100644 --- a/patches/ring_windows_msvc_include_dirs.patch +++ b/patches/ring_windows_msvc_include_dirs.patch @@ -1,8 +1,7 @@ diff --git a/build.rs b/build.rs -index 9843ad8aa..573075489 100644 --- a/build.rs +++ b/build.rs -@@ -346,7 +346,28 @@ fn ring_build_rs_main(c_root_dir: &Path, core_name_and_version: &str) { +@@ -346,7 +346,27 @@ // we want to optimize for minimizing the build tools required: No Perl, // no nasm, etc. let generated_dir = if !is_git { @@ -31,7 +30,22 @@ index 9843ad8aa..573075489 100644 } else { generate_sources_and_preassemble( &out_dir, -@@ -569,6 +591,15 @@ fn configure_cc(c: &mut cc::Build, target: &Target, c_root_dir: &Path, include_d +@@ -561,7 +581,13 @@ + let compiler = c.get_compiler(); + // FIXME: On Windows AArch64 we currently must use Clang to compile C code + let compiler = if target.os == WINDOWS && target.arch == AARCH64 && !compiler.is_like_clang() { +- let _ = c.compiler("clang"); ++ // Keep inherited MSVC flags compatible with the selected Clang driver. ++ let clang = if compiler.is_like_msvc() { ++ "clang-cl" ++ } else { ++ "clang" ++ }; ++ let _ = c.compiler(clang); + c.get_compiler() + } else { + compiler +@@ -569,6 +595,15 @@ let _ = c.include(c_root_dir.join("include")); let _ = c.include(include_dir); diff --git a/patches/rules_foreign_cc_make_msvc_stdint.patch b/patches/rules_foreign_cc_make_msvc_stdint.patch new file mode 100644 index 0000000000..f3af72841a --- /dev/null +++ b/patches/rules_foreign_cc_make_msvc_stdint.patch @@ -0,0 +1,13 @@ +diff --git a/foreign_cc/built_tools/make_build.bzl b/foreign_cc/built_tools/make_build.bzl +--- a/foreign_cc/built_tools/make_build.bzl ++++ b/foreign_cc/built_tools/make_build.bzl +@@ -35,6 +35,9 @@ + build_str += " gcc" + dist_dir = "GccRel" + else: ++ # Modern MSVC provides stdint.h, including the intmax_t types that ++ # Make's legacy Windows config otherwise replaces with macros. ++ build_str = 'export CL="$${CL:-}$$ /DHAVE_STDINT_H=1"\n' + build_str + dist_dir = "WinRel" + + script = [ diff --git a/patches/rules_rs_windows_msvc_linker.patch b/patches/rules_rs_windows_msvc_linker.patch index 66feb78569..5056ade96b 100644 --- a/patches/rules_rs_windows_msvc_linker.patch +++ b/patches/rules_rs_windows_msvc_linker.patch @@ -1,13 +1,14 @@ -# What: use Rust's bundled direct linker for Windows x86_64 MSVC toolchains. -# Scope: standard and bootstrap rules_rs Rust toolchains targeting x86_64 MSVC. +# What: use Rust's bundled direct linker for Windows x86_64 and ARM64 MSVC toolchains. +# Scope: standard and bootstrap rules_rs Rust toolchains targeting x86_64 and ARM64 MSVC. diff --git a/rs/toolchains/declare_rustc_toolchains.bzl b/rs/toolchains/declare_rustc_toolchains.bzl --- a/rs/toolchains/declare_rustc_toolchains.bzl +++ b/rs/toolchains/declare_rustc_toolchains.bzl -@@ -88,6 +88,7 @@ def declare_rustc_toolchains( +@@ -88,6 +88,8 @@ def declare_rustc_toolchains( llvm_cov = "@llvm//tools:llvm-cov", llvm_profdata = "@llvm//tools:llvm-profdata", linker = select({ ++ "@rules_rs//rs/platforms/config:aarch64-pc-windows-msvc": "{}rust-lld".format(rustc_repo_label), + "@rules_rs//rs/platforms/config:x86_64-pc-windows-msvc": "{}rust-lld".format(rustc_repo_label), "@platforms//cpu:wasm32": "{}rust-lld".format(rustc_repo_label), "@platforms//cpu:wasm64": "{}rust-lld".format(rustc_repo_label), diff --git a/patches/rules_rust_windows_execroot_separators.patch b/patches/rules_rust_windows_execroot_separators.patch new file mode 100644 index 0000000000..187b37f48c --- /dev/null +++ b/patches/rules_rust_windows_execroot_separators.patch @@ -0,0 +1,52 @@ +--- a/cargo/private/cargo_build_script_runner/lib.rs ++++ b/cargo/private/cargo_build_script_runner/lib.rs +@@ -337,9 +337,18 @@ + return "${pwd}".to_owned(); + } + +- value ++ let redacted = value + .replace(&format!("{exec_root}/"), "${pwd}/") +- .replace(&format!("{exec_root}\\"), "${pwd}\\") ++ .replace(&format!("{exec_root}\\"), "${pwd}\\"); ++ #[cfg(windows)] ++ let redacted = { ++ // pkg-config can emit forward slashes for the same Windows root. ++ let root = exec_root.replace('\\', "/"); ++ redacted ++ .replace(&format!("{root}/"), "${pwd}/") ++ .replace(&format!("{root}\\"), "${pwd}\\") ++ }; ++ redacted + } + + /// Redact for env vars: uses the generic `${out_dir}` token, resolved +@@ -630,6 +639,28 @@ + link_search_paths: + "-L${pwd}/${bazel-out/cfg/bin/pkg/_bs.out_dir}\n-L${pwd}/other/path".to_owned(), + } ++ ); ++ } ++ ++ #[cfg(windows)] ++ #[test] ++ fn windows_include_metadata_redacts_both_root_spellings() { ++ let root = r"D:\o\execroot\_main"; ++ let forward = root.replace('\\', "/"); ++ let outputs = vec![BuildScriptOutput::DepEnv(format!( ++ "INCLUDE={forward}/include;{root}\\lib;{forward}-other/include" ++ ))]; ++ assert_eq!( ++ BuildScriptOutput::outputs_to_dep_env(&outputs, "glib", root), ++ format!("DEP_GLIB_INCLUDE=${{pwd}}/include;${{pwd}}\\lib;{forward}-other/include") ++ ); ++ let directory = std::env::current_dir().unwrap(); ++ let directory = directory.to_str().unwrap(); ++ let forward = directory.replace('\\', "/"); ++ let outputs = vec![BuildScriptOutput::DepEnv(format!("INCLUDE={forward}/."))]; ++ assert_eq!( ++ BuildScriptOutput::nonhermetic_absolute_paths(&outputs, directory, ""), ++ Vec::::new() + ); + } + diff --git a/patches/rules_rust_windows_msvc_direct_link_args.patch b/patches/rules_rust_windows_msvc_direct_link_args.patch index aa5fb274e1..a8eba2ba6e 100644 --- a/patches/rules_rust_windows_msvc_direct_link_args.patch +++ b/patches/rules_rust_windows_msvc_direct_link_args.patch @@ -4,7 +4,7 @@ use_bpf_linker = toolchain.target_arch in ("bpfeb", "bpfel") and toolchain.linker - if not ld or toolchain.linker_preference == "rust" or use_bpf_linker: + use_windows_msvc_linker = ( -+ toolchain.target_arch == "x86_64" and ++ toolchain.target_arch in ("x86_64", "aarch64") and + toolchain.target_os == "windows" and + toolchain.target_abi == "msvc" and + toolchain.linker != None and diff --git a/scripts/build_winget_package.py b/scripts/build_winget_package.py new file mode 100644 index 0000000000..a5d547048e --- /dev/null +++ b/scripts/build_winget_package.py @@ -0,0 +1,37 @@ +"""Adapt an extracted signed Windows package to WinGet's existing root filenames.""" + +import argparse +import json +import shutil +from pathlib import Path + + +def prepare_winget_package(package: Path) -> None: + metadata_path = package / "codex-package.json" + metadata = json.loads(metadata_path.read_text(encoding="utf-8")) + target = metadata["target"] + if target not in ("x86_64-pc-windows-msvc", "aarch64-pc-windows-msvc"): + raise ValueError("WinGet requires a Windows package") + if metadata["entrypoint"] != "bin/codex.exe": + raise ValueError("WinGet requires the canonical Codex entrypoint") + entrypoint = f"codex-{target}.exe" + manifest_path = package / "codex-resources/voice/manifest.json" + manifest = json.loads(manifest_path.read_text(encoding="utf-8")) + manifest["sha256"][entrypoint] = manifest["sha256"].pop("bin/codex.exe") + (package / "bin/codex.exe").rename(package / entrypoint) + (package / "bin/codex-code-mode-host.exe").rename( + package / "codex-code-mode-host.exe" + ) + # Keep resources in place for package-aware discovery and provide the root + # filenames declared by the existing WinGet portable installer manifest. + for helper in ("codex-command-runner.exe", "codex-windows-sandbox-setup.exe"): + shutil.copy2(package / "codex-resources" / helper, package / helper) + metadata["entrypoint"] = entrypoint + for path, value in ((metadata_path, metadata), (manifest_path, manifest)): + path.write_text(json.dumps(value, indent=2) + "\n", encoding="utf-8") + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("package", type=Path) + prepare_winget_package(parser.parse_args().package) diff --git a/scripts/codex_package/test_layout.py b/scripts/codex_package/test_layout.py index b0810cac99..b2df9601c1 100644 --- a/scripts/codex_package/test_layout.py +++ b/scripts/codex_package/test_layout.py @@ -1,12 +1,15 @@ #!/usr/bin/env python3 from pathlib import Path +import hashlib +import json import sys import tempfile import unittest sys.path.insert(0, str(Path(__file__).resolve().parents[1])) +from build_winget_package import prepare_winget_package from codex_package.layout import build_package_dir from codex_package.layout import validate_package_dir from codex_package.targets import PACKAGE_VARIANTS @@ -15,6 +18,69 @@ from codex_package.targets import TARGET_SPECS class PackageLayoutTest(unittest.TestCase): + def test_winget_preserves_signed_files_and_voice_hashes(self) -> None: + for target in ("x86_64-pc-windows-msvc", "aarch64-pc-windows-msvc"): + with self.subTest(target=target), tempfile.TemporaryDirectory() as temp: + package = Path(temp) + files = { + "bin/codex.exe": b"signed CLI", + "bin/codex-code-mode-host.exe": b"signed code mode host", + "codex-resources/codex-command-runner.exe": b"signed runner", + "codex-resources/codex-windows-sandbox-setup.exe": b"signed setup", + "codex-resources/voice/bin/codex-voice-host.exe": b"signed voice host", + "codex-resources/voice/bin/gstreamer-1.0-0.dll": b"signed audio DLL", + "codex-resources/voice/NOTICE.md": b"license notices", + "codex-path/rg.exe": b"ripgrep", + } + for name, contents in files.items(): + path = package / name + path.parent.mkdir(parents=True, exist_ok=True) + path.write_bytes(contents) + metadata = { + "layoutVersion": 1, + "target": target, + "entrypoint": "bin/codex.exe", + } + (package / "codex-package.json").write_text(json.dumps(metadata)) + manifest = { + "schemaVersion": 1, + "sha256": { + name: hashlib.sha256(contents).hexdigest() + for name, contents in files.items() + if name == "bin/codex.exe" + or name.startswith("codex-resources/voice/") + }, + } + manifest_path = package / "codex-resources/voice/manifest.json" + manifest_path.write_text(json.dumps(manifest)) + prepare_winget_package(package) + entrypoint = f"codex-{target}.exe" + files[entrypoint] = files.pop("bin/codex.exe") + files["codex-code-mode-host.exe"] = files.pop( + "bin/codex-code-mode-host.exe" + ) + for helper in ( + "codex-command-runner.exe", + "codex-windows-sandbox-setup.exe", + ): + files[helper] = files[f"codex-resources/{helper}"] + actual = { + str(path.relative_to(package)).replace("\\", "/"): path.read_bytes() + for path in package.rglob("*") + if path.is_file() + } + actual_metadata = json.loads(actual.pop("codex-package.json")) + actual_manifest = json.loads( + actual.pop("codex-resources/voice/manifest.json") + ) + self.assertEqual(actual, files) + metadata["entrypoint"] = entrypoint + self.assertEqual(actual_metadata, metadata) + manifest["sha256"][entrypoint] = manifest["sha256"].pop("bin/codex.exe") + self.assertEqual(actual_manifest, manifest) + for name, digest in actual_manifest["sha256"].items(): + self.assertEqual(hashlib.sha256(actual[name]).hexdigest(), digest) + def test_macos_package_preserves_prebuilt_resource_binaries(self) -> None: for variant_name in ("codex", "codex-app-server"): for target in ("aarch64-apple-darwin", "x86_64-apple-darwin"): diff --git a/third_party/voice/NOTICE.md b/third_party/voice/NOTICE.md index c5b79744a8..3de0cff39b 100644 --- a/third_party/voice/NOTICE.md +++ b/third_party/voice/NOTICE.md @@ -15,3 +15,13 @@ projection and package scripts are in the public Codex source tree under platform-specific runtime directories. Replacements must be compatible with the package and, on macOS, have valid code signatures. Build tools listed in `sources.json` are build inputs, not bundled runtime libraries. + +Windows packages also contain an unmodified Microsoft Visual C++ runtime DLL, +copyright Microsoft Corporation, under Microsoft's applicable software license +terms, separately from the open-source audio libraries. `windows-crt.json` +records its version, official download, hashes, and redistribution references. +The Apache and LGPL licenses for other components do not license this DLL. +Microsoft's runtime terms and separate developer redistribution terms apply +to the Microsoft component; the runtime terms alone do not grant redistribution. +Only retail redistributable files are included; Microsoft signatures are retained. +App-local runtime security updates must be delivered with Codex updates. diff --git a/third_party/voice/README.md b/third_party/voice/README.md index 48ea1fa46c..6cbddf369f 100644 --- a/third_party/voice/README.md +++ b/third_party/voice/README.md @@ -249,7 +249,23 @@ runtime inspection and SDK export. These targets are MSVC-only. They require native Windows execution of the matching architecture; they are not cross builds. The generic Rust-consumer aliases are connected separately, after these inputs. -Provide the complete installed Cygwin/pkgconf repository explicitly. The default +Provide the complete installed Cygwin/pkgconf repository explicitly. For a public +Windows build, `.github/scripts/setup-voice-windows.ps1 -Target +x86_64-pc-windows-msvc -SnapshotArchive ` (under `public/` in +codex-internal) verifies the pinned archive and every input size and SHA-512 +digest, then runs the offline installer against its signed metadata. ARM64 uses +`aarch64-pc-windows-msvc` and the same Cygwin x64 tools under emulation. The +installed tool tree stays in the CI temporary directory and is never added to +a Codex package. The upstream mirror's signed metadata changes over time, so +the archived snapshot must be supplied separately. Private CI validates this +public bootstrap against its existing pinned archive. Public release CI obtains +the same hash-pinned build inputs from the public `openai/codex` release named +by `voice-cygwin-snapshot.json`. That release also makes the corresponding +upstream source archives available under `cygwin-build-sources.tar`, with its +own size and SHA-256 pin. These Cygwin tools run only on the build runner; +neither archive is included in the user's Codex package. + +The default `windows_installed_tools` label setting is empty and fails if a Windows action needs it. This keeps ordinary public dependency queries independent of private provisioning; it does not silently omit tools from a requested Windows build. diff --git a/third_party/voice/assemble_package.py b/third_party/voice/assemble_package.py index af6cc8ca00..a8077d4c5f 100644 --- a/third_party/voice/assemble_package.py +++ b/third_party/voice/assemble_package.py @@ -114,6 +114,7 @@ def assemble( for relative in ( "NOTICE.md", "sources.json", + *(("windows-crt.json",) if suffix else ()), "licenses/LGPL-2.1.txt", "licenses/Opus.txt", "licenses/PCRE2.md", diff --git a/third_party/voice/release_runtime.py b/third_party/voice/release_runtime.py index 6b705ca314..a02cce3448 100644 --- a/third_party/voice/release_runtime.py +++ b/third_party/voice/release_runtime.py @@ -15,6 +15,8 @@ def stage(source: Path, destination: Path, target: str) -> None: "x86_64-apple-darwin", "aarch64-unknown-linux-gnu", "x86_64-unknown-linux-gnu", + "aarch64-pc-windows-msvc", + "x86_64-pc-windows-msvc", }: raise ValueError("unsupported public release voice runtime target") source = source.resolve(strict=True) diff --git a/third_party/voice/test_assemble_package.py b/third_party/voice/test_assemble_package.py index 7758bd4b12..a9ac9a3588 100644 --- a/third_party/voice/test_assemble_package.py +++ b/third_party/voice/test_assemble_package.py @@ -308,6 +308,47 @@ class AssembleTests(unittest.TestCase): digest(staged / "runtime.json"), ) + def test_windows_release_packages_signed_receipt_and_exe_helper(self): + self.commit = "b" * 40 + for target in ("x86_64-pc-windows-msvc", "aarch64-pc-windows-msvc"): + with self.subTest(target=target): + runtime, _ = self.make_runtime(target, "bin/gst{}.dll") + staged = self.root / f"signed-{target}" + stage(runtime, staged, target) + library = staged / "bin/gio-2.0-0.dll" + library.write_bytes(library.read_bytes() + b"signed") + seal(staged, target) + self.metadata.update( + target=target, + entrypoint="bin/codex.exe", + version="0.154.0-beta.2", + ) + (self.package / "bin/codex.exe").write_bytes(b"unchanged app") + (self.package / "codex-package.json").write_text( + json.dumps(self.metadata) + ) + output = self.root / f"windows-{target}" + assemble( + self.package, + self.helper, + target, + self.commit, + output, + runtime=staged, + release_version="0.154.0-beta.2", + ) + voice = output / "codex-resources/voice" + self.assertEqual( + (voice / "bin/codex-voice-host.exe").read_bytes(), + self.helper.read_bytes(), + ) + self.assertEqual( + (voice / "bin/gio-2.0-0.dll").read_bytes(), library.read_bytes() + ) + self.assertTrue( + runtime_files(voice.resolve(), target, public_release=True) + ) + def test_rejects_invalid_runtime_receipts_before_creating_package(self): runtime, original = self.make_runtime() changes = [ diff --git a/third_party/voice/windows-crt.json b/third_party/voice/windows-crt.json new file mode 100644 index 0000000000..c198cb9ea4 --- /dev/null +++ b/third_party/voice/windows-crt.json @@ -0,0 +1,18 @@ +{ + "version": "14.50.35719", + "license": "https://visualstudio.microsoft.com/license-terms/vs2026-ga-visualcpp-v14-redist-runtime/", + "developerLicense": "https://visualstudio.microsoft.com/license-terms/vs2026-ga-community/", + "redistribution": "https://learn.microsoft.com/en-us/visualstudio/releases/2026/redistribution", + "x86_64-pc-windows-msvc": { + "url": "https://download.visualstudio.microsoft.com/download/pr/a424e95b-20f6-4af6-844f-9d9a806080a7/a4ab48822362df6e478eb4ed33bf825a07a6faacd6879c1e3d59929cd38b984b/Microsoft.VC.14.50.18.0.CRT.Redist.X64.base.vsix", + "sha256": "a4ab48822362df6e478eb4ed33bf825a07a6faacd6879c1e3d59929cd38b984b", + "member": "Contents/VC/Redist/MSVC/14.50.35710/x64/Microsoft.VC145.CRT/vcruntime140.dll", + "dllSha256": "184146852727a9db4eea06178716bec3cdbb1015c911f6b0f915b184ad7775b2" + }, + "aarch64-pc-windows-msvc": { + "url": "https://download.visualstudio.microsoft.com/download/pr/a424e95b-20f6-4af6-844f-9d9a806080a7/3544badcbcf09e77c6ec32ed866aae59471a39d8a84611d9ae016861ff60a42f/Microsoft.VC.14.50.18.0.CRT.Redist.ARM64.base.vsix", + "sha256": "3544badcbcf09e77c6ec32ed866aae59471a39d8a84611d9ae016861ff60a42f", + "member": "Contents/VC/Redist/MSVC/14.50.35710/arm64/Microsoft.VC145.CRT/vcruntime140.dll", + "dllSha256": "6d987d8cb2a47cff9c29c1fcbb853e7ef273c545f7d822728f7d2d448ca18758" + } +} diff --git a/third_party/voice/windows_crt.py b/third_party/voice/windows_crt.py new file mode 100644 index 0000000000..a927c1b19e --- /dev/null +++ b/third_party/voice/windows_crt.py @@ -0,0 +1,83 @@ +"""Add pinned, unmodified Microsoft retail CRT files to Windows release staging.""" + +import argparse +import hashlib +import io +import json +from pathlib import Path +import re +import subprocess +import urllib.request +import zipfile + +from windows_runtime import EXTERNAL_IMPORTS, inspect + + +def stage(root: Path, target: str, helper: Path): + pin = json.loads(Path(__file__).with_name("windows-crt.json").read_text())[target] + with urllib.request.urlopen(pin["url"], timeout=90) as response: + archive = response.read(6 * 1024 * 1024 + 1) + if hashlib.sha256(archive).hexdigest() != pin["sha256"]: + raise ValueError("Microsoft CRT archive digest mismatch") + with zipfile.ZipFile(io.BytesIO(archive)) as source: + member = source.getinfo(pin["member"]) + if member.file_size > 1024 * 1024: + raise ValueError("CRT member exceeds size limit") + data = source.read(member) + if hashlib.sha256(data).hexdigest() != pin["dllSha256"]: + raise ValueError("Microsoft CRT DLL digest mismatch") + # Extract exactly one retail member, never debug_nonredist or installer files. + with (root / "bin/vcruntime140.dll").open("xb") as output: + output.write(data) + files = list((root / "bin").glob("*.dll")) + bundled = {path.name.lower() for path in files} + imports = set() + for path in files: + if path.name.lower() != "vcruntime140.dll": + imports.update(inspect(path, target).imports) + # Only Microsoft's hash-pinned CRT may use ARM64X rather than plain ARM64. + for path in (helper, root / "bin/vcruntime140.dll"): + result = subprocess.run( + ["dumpbin", "/nologo", "/dependents", str(path)], + check=True, + capture_output=True, + timeout=30, + ) + imports.update( + re.findall( + r"(?mi)^ +([a-z0-9_+.-]+\.dll)\s*$", result.stdout.decode("ascii") + ) + ) + # Additional Windows OS imports used by the Rust helper, not bundled CRTs. + system = (EXTERNAL_IMPORTS - {"vcruntime140.dll"}) | { + "api-ms-win-core-synch-l1-2-0.dll", + "api-ms-win-core-winrt-error-l1-1-0.dll", + "bcrypt.dll", + "bcryptprimitives.dll", + "combase.dll", + "mmdevapi.dll", + "oleaut32.dll", + "ntdll.dll", + "userenv.dll", + "dbghelp.dll", + } + missing = {name.lower() for name in imports} - bundled - system + if missing: + raise ValueError(f"Unbundled Windows imports: {sorted(missing)}") + manifest_path = root / "runtime.json" + manifest = json.loads(manifest_path.read_text()) + if manifest.get("target") != target or manifest.get("developmentOnly") is not True: + raise ValueError("expected staged development runtime") + manifest["libraries"].append( + {"path": "bin/vcruntime140.dll", "sha256": pin["dllSha256"]} + ) + manifest_path.write_text(json.dumps(manifest, indent=2) + "\n") + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--root", type=Path, required=True) + parser.add_argument("--target", required=True) + parser.add_argument("--helper", type=Path, required=True) + args = parser.parse_args() + stage(args.root, args.target, args.helper)