From f8f9f243bd7aa5305d86bbd664998527ec40e214 Mon Sep 17 00:00:00 2001 From: Danger Mouse <35347349+DangerMouseUK@users.noreply.github.com> Date: Thu, 1 Oct 2026 20:43:42 +0100 Subject: [PATCH 1/8] Refine Windows development setup and test PowerShell compatibility --- .github/workflows/repo-checks.yml | 38 ++ codex-rs/scripts/setup-windows.Tests.ps1 | 467 ++++++++++++++++ codex-rs/scripts/setup-windows.ps1 | 668 ++++++++++++++++------- docs/install.md | 84 ++- 4 files changed, 1040 insertions(+), 217 deletions(-) create mode 100644 codex-rs/scripts/setup-windows.Tests.ps1 diff --git a/.github/workflows/repo-checks.yml b/.github/workflows/repo-checks.yml index 4f05cd06046b..d6f712b4f8be 100644 --- a/.github/workflows/repo-checks.yml +++ b/.github/workflows/repo-checks.yml @@ -4,6 +4,44 @@ on: workflow_call: jobs: + windows-setup: + name: Windows setup (${{ matrix.shell }}) + runs-on: windows-latest + timeout-minutes: 10 + permissions: + contents: read + strategy: + fail-fast: false + matrix: + shell: [powershell, pwsh] + env: + RUSTUP_AUTO_INSTALL: "0" + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - name: Setup Python + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + with: + python-version: "3.12" + + - name: Download Pester for this runner + shell: pwsh + run: Save-Module -Name Pester -RequiredVersion 5.7.1 -Repository PSGallery -Path "$env:RUNNER_TEMP/windows-setup-tests" -Force + + - name: Test Windows setup with mocked installers + shell: ${{ matrix.shell }} + run: | + Import-Module "$env:RUNNER_TEMP/windows-setup-tests/Pester/5.7.1/Pester.psd1" -Force + $result = Invoke-Pester -Path ./codex-rs/scripts/setup-windows.Tests.ps1 -CI -PassThru + if ($result.PassedCount -eq 0) { throw 'No Windows setup tests ran.' } + + - name: Check for a clean worktree + if: always() && !cancelled() + uses: ./.github/actions/check-clean-worktree + build-test: runs-on: ubuntu-latest timeout-minutes: 10 diff --git a/codex-rs/scripts/setup-windows.Tests.ps1 b/codex-rs/scripts/setup-windows.Tests.ps1 new file mode 100644 index 000000000000..24e22b7c522e --- /dev/null +++ b/codex-rs/scripts/setup-windows.Tests.ps1 @@ -0,0 +1,467 @@ +#Requires -Version 5.1 +# Run with Pester 5: Invoke-Pester ./codex-rs/scripts/setup-windows.Tests.ps1 +# Installers are mocked; the tests do not install software or change user settings. +Describe 'Windows development setup' { + BeforeAll { + $setupPath = Join-Path $PSScriptRoot 'setup-windows.ps1' + $tokens = $null + $parseErrors = $null + $setupAst = [System.Management.Automation.Language.Parser]::ParseFile( + $setupPath, [ref]$tokens, [ref]$parseErrors + ) + if ($parseErrors.Count) { throw ($parseErrors -join [Environment]::NewLine) } + # Load the original function bodies with their source paths, without running + # the entry point or requiring an installed development environment. + $definitions = $setupAst.FindAll({ + param($node) + $node -is [System.Management.Automation.Language.FunctionDefinitionAst] + }, $false) + foreach ($definition in $definitions) { + Set-Item -Path "Function:$($definition.Name)" -Value $definition.Body.GetScriptBlock() + } + $environmentNames = @( + 'Path', 'PROCESSOR_ARCHITECTURE', 'PROCESSOR_ARCHITEW6432', 'CARGO_HOME', + 'RUSTUP_TOOLCHAIN', 'RUSTUP_AUTO_INSTALL', 'LIBCLANG_PATH', 'CC', 'CXX', + 'VCToolsInstallDir', 'WindowsSdkDir', 'WindowsSDKVersion', 'INCLUDE', 'LIB', + 'LIBPATH', 'VCINSTALLDIR', 'WindowsSDKLibVersion', 'WindowsSdkBinPath', + 'WindowsLibPath', 'UniversalCRTSdkDir', 'UCRTVersion' + ) + } + + BeforeEach { + $script:CheckOnly = $false + $script:Architecture = 'x64' + $script:WorkspaceRoot = Split-Path -Parent $PSScriptRoot + $script:RepositoryRoot = Split-Path -Parent $script:WorkspaceRoot + $script:PSNativeCommandUseErrorActionPreference = $false + $script:environmentBefore = @{} + foreach ($name in $environmentNames) { + $script:environmentBefore[$name] = [Environment]::GetEnvironmentVariable($name, 'Process') + } + } + + AfterEach { + foreach ($name in $environmentNames) { + [Environment]::SetEnvironmentVariable($name, $script:environmentBefore[$name], 'Process') + } + } + + Describe 'Native command failure handling' { + It 'throws on a nonzero native exit code instead of continuing' { + { Invoke-Native $env:ComSpec @('/d', '/c', 'exit 7') } | Should -Throw '*exit code 7*' + } + + It 'accepts only explicitly allowed already-installed exit codes' { + { Invoke-Native $env:ComSpec @('/d', '/c', 'exit -1978335189') -SuccessExitCodes @(0, -1978335189) } | + Should -Not -Throw + { Invoke-Native $env:ComSpec @('/d', '/c', 'exit 8') -SuccessExitCodes @(0, -1978335189) } | + Should -Throw '*exit code 8*' + } + + It 'requires a restart instead of reporting a complete environment' { + { Invoke-Native $env:ComSpec @('/d', '/c', 'exit 3010') } | Should -Throw '*Restart Windows*' + { Invoke-Native $env:ComSpec @('/d', '/c', 'exit -1978334967') } | Should -Throw '*Restart Windows*' + } + } + + Describe 'Package installation and discovery' { + It 'uses exact package IDs, the community source, and native architecture' { + $script:Architecture = 'arm64' + Mock Get-ApplicationPath { 'winget.exe' } + Mock Get-ToolVersion { [version]'1.29.0' } + Mock Invoke-Native {} + Mock Update-SessionPath {} + + Install-WinGetPackage 'LLVM.LLVM' + + Should -Invoke Invoke-Native -Times 1 -Exactly -ParameterFilter { + $FilePath -eq 'winget.exe' -and + $ArgumentList -contains 'LLVM.LLVM' -and + $ArgumentList -contains '--exact' -and + $ArgumentList -contains 'winget' -and + $ArgumentList -contains 'arm64' -and + $ArgumentList -contains '--silent' -and + $ArgumentList -notcontains '--ignore-security-hash' + } + } + + It 'does not install in CheckOnly mode' { + $script:CheckOnly = $true + Mock Invoke-Native { throw 'Unexpected installer invocation' } + { Install-WinGetPackage 'LLVM.LLVM' } | Should -Throw '*without -CheckOnly*' + Should -Invoke Invoke-Native -Times 0 -Exactly + } + + It 'reuses a supported installation' { + Mock Get-ToolVersion { [version]'7.6.0' } + Mock Install-WinGetPackage {} + Ensure-Tool 'Microsoft.PowerShell' 'pwsh.exe' -MinimumVersion '7.4' + Should -Invoke Install-WinGetPackage -Times 0 -Exactly + } + + It 'upgrades an older installation and verifies the result' { + $script:toolVersion = [version]'7.3' + Mock Get-ToolVersion { $script:toolVersion } + Mock Install-WinGetPackage { $script:toolVersion = [version]'7.6' } + Ensure-Tool 'Microsoft.PowerShell' 'pwsh.exe' -MinimumVersion '7.4' + Should -Invoke Install-WinGetPackage -Times 1 -Exactly + } + + It 'rejects an installer success that leaves the command missing' { + Mock Get-ToolVersion { $null } + Mock Install-WinGetPackage {} + { Ensure-Tool 'Casey.Just' 'just.exe' } | Should -Throw '*did not provide a usable*' + } + + It 'ignores the Store Python alias and PowerShell wrapper scripts' { + Mock Get-Command { + @( + [pscustomobject]@{ Source = 'C:\Users\Test\AppData\Local\Microsoft\WindowsApps\python.exe' }, + [pscustomobject]@{ Source = 'C:\Python312\python.exe' } + ) + } + Get-ApplicationPath 'python.exe' | Should -Be 'C:\Python312\python.exe' + Should -Invoke Get-Command -ParameterFilter { $CommandType -eq 'Application' } + } + + It 'refreshes PATH without writing a user setting in CheckOnly mode' { + $script:CheckOnly = $true + $userPath = [Environment]::GetEnvironmentVariable('Path', 'User') + Add-UserPath 'C:\Codex Test Tools' + $env:Path.Split(';')[0] | Should -Be 'C:\Codex Test Tools' + [Environment]::GetEnvironmentVariable('Path', 'User') | Should -Be $userPath + } + } + + Describe 'Windows architecture' { + It 'detects native ARM64 even in an emulated x64 shell' { + $env:PROCESSOR_ARCHITECTURE = 'AMD64' + $env:PROCESSOR_ARCHITEW6432 = 'ARM64' + Get-WindowsArchitecture | Should -Be 'arm64' + } + + It 'detects x64 Windows in a 32-bit shell' { + $env:PROCESSOR_ARCHITECTURE = 'x86' + $env:PROCESSOR_ARCHITEW6432 = 'AMD64' + Get-WindowsArchitecture | Should -Be 'x64' + } + + It 'rejects a 32-bit operating system' { + $env:PROCESSOR_ARCHITECTURE = 'x86' + $env:PROCESSOR_ARCHITEW6432 = '' + { Get-WindowsArchitecture } | Should -Throw '*Unsupported Windows architecture*' + } + } + + Describe 'Visual Studio setup' { + It 'reuses a complete VS 2022 or newer installation' { + Mock Get-VSInstallation { 'C:\Existing VS' } + Mock Install-WinGetPackage {} + Mock Start-Process {} + Ensure-VisualStudio | Should -Be 'C:\Existing VS' + Should -Invoke Install-WinGetPackage -Times 0 -Exactly + Should -Invoke Start-Process -Times 0 -Exactly + } + + It 'waits while adding components to an existing installation' { + $script:vsReady = $false + Mock Get-VSInstallation { + param($RequiredComponents) + if ($script:vsReady -or -not $RequiredComponents.Count) { 'C:\Existing VS' } + } + Mock Start-Process { + $script:vsReady = $true + [pscustomobject]@{ ExitCode = 0 } + } + Ensure-VisualStudio | Should -Be 'C:\Existing VS' + Should -Invoke Start-Process -Times 1 -Exactly -ParameterFilter { + $Wait -and $PassThru -and $WindowStyle -eq 'Hidden' -and + $ArgumentList -contains '"C:\Existing VS"' -and + $ArgumentList -contains 'Microsoft.VisualStudio.Component.Windows11SDK.26100' -and + $ArgumentList -notcontains '--wait' + } + } + + It 'adds ARM64 tools only on ARM64 and waits for the x64 bootstrapper' { + $script:Architecture = 'arm64' + $script:vsReady = $false + Mock Get-VSInstallation { if ($script:vsReady) { 'C:\VS' } } + Mock Install-WinGetPackage { $script:vsReady = $true } + Ensure-VisualStudio | Should -Be 'C:\VS' + Should -Invoke Install-WinGetPackage -Times 1 -Exactly -ParameterFilter { + $Architecture -eq 'x64' -and + $ExtraArguments[1] -match '--wait' -and + $ExtraArguments[1] -match 'VC.Tools.ARM64' -and + $ExtraArguments[1] -notmatch 'ARM64EC|SDK.22000' + } + } + + It 'rejects a missing component after the installer returns success' { + Mock Get-VSInstallation { $null } + Mock Install-WinGetPackage {} + { Ensure-VisualStudio } | Should -Throw '*did not install all required*' + } + + It 'stops after a VS installer failure' { + Mock Get-VSInstallation { param($RequiredComponents) if (-not $RequiredComponents.Count) { 'C:\VS' } } + Mock Start-Process { [pscustomobject]@{ ExitCode = 1602 } } + { Ensure-VisualStudio } | Should -Throw '*exit code 1602*' + } + + It 'does not request installation in CheckOnly mode' { + $script:CheckOnly = $true + Mock Get-VSInstallation { $null } + Mock Start-Process {} + Mock Install-WinGetPackage {} + { Ensure-VisualStudio } | Should -Throw '*components are missing*' + Should -Invoke Start-Process -Times 0 -Exactly + Should -Invoke Install-WinGetPackage -Times 0 -Exactly + } + } + + Describe 'Visual Studio environment activation' { + BeforeEach { + $script:testVS = Join-Path $TestDrive 'Visual Studio & Tools' + $script:testVC = Join-Path $script:testVS 'VC\Tools\MSVC\Test' + $script:testSDK = Join-Path $TestDrive 'Windows SDK' + $tools = Join-Path $script:testVS 'Common7\Tools' + $headers = Join-Path $script:testSDK 'Include\10.0.26100.0\um' + $libraries = Join-Path $script:testSDK 'Lib\10.0.26100.0\um\x64' + New-Item -ItemType Directory -Path $tools, $headers, $libraries -Force | Out-Null + New-Item -ItemType File -Path (Join-Path $headers 'Windows.h'), (Join-Path $libraries 'kernel32.lib') -Force | Out-Null + $lines = @( + '@echo off', + ('set "VCToolsInstallDir={0}\"' -f $script:testVC), + ('set "WindowsSdkDir={0}\"' -f $script:testSDK), + 'set "WindowsSDKVersion=10.0.26100.0\"', + 'set "CODEX_SETUP_IGNORED=do-not-import"' + ) + Set-Content -LiteralPath (Join-Path $tools 'VsDevCmd.bat') -Value $lines -Encoding ASCII + Mock Get-ApplicationPath { + param($Name) + if ($Name -eq 'rc.exe') { Join-Path $script:testSDK "bin\10.0.26100.0\x64\$Name" } + else { Join-Path $script:testVC "bin\Hostx64\x64\$Name" } + } + } + + It 'quotes batch paths with spaces and ampersands in the actual cmd.exe invocation' { + $ignored = $env:CODEX_SETUP_IGNORED + Enter-VisualStudioEnvironment $script:testVS + $env:VCToolsInstallDir | Should -Be "$script:testVC\" + $env:WindowsSdkDir | Should -Be "$script:testSDK\" + $env:CODEX_SETUP_IGNORED | Should -Be $ignored + } + + It 'rejects another link.exe that shadows MSVC' { + Mock Get-ApplicationPath { 'C:\Git\usr\bin\link.exe' } -ParameterFilter { $Name -eq 'link.exe' } + { Enter-VisualStudioEnvironment $script:testVS } | Should -Throw '*outside the selected Visual Studio*' + } + + It 'rejects missing SDK libraries before reporting success' { + Remove-Item -LiteralPath (Join-Path $script:testSDK 'Lib\10.0.26100.0\um\x64\kernel32.lib') + { Enter-VisualStudioEnvironment $script:testVS } | Should -Throw '*SDK headers/libraries are missing*' + } + } + + Describe 'libclang architecture' { + BeforeAll { + function New-TestPE { + param([string]$Path, [int]$Machine) + $bytes = New-Object byte[] 128 + [BitConverter]::GetBytes([uint16]0x5A4D).CopyTo($bytes, 0) + [BitConverter]::GetBytes([int]64).CopyTo($bytes, 60) + [BitConverter]::GetBytes([uint32]0x00004550).CopyTo($bytes, 64) + [BitConverter]::GetBytes([uint16]$Machine).CopyTo($bytes, 68) + [IO.File]::WriteAllBytes($Path, $bytes) + } + } + + It 'accepts a native x64 DLL and rejects an ARM64 DLL on x64' { + $path = Join-Path $TestDrive 'libclang.dll' + New-TestPE $path 0x8664 + Test-LibclangArchitecture $path | Should -BeTrue + New-TestPE $path 0xAA64 + Test-LibclangArchitecture $path | Should -BeFalse + } + + It 'rejects an x64 DLL on ARM64' { + $script:Architecture = 'arm64' + $path = Join-Path $TestDrive 'libclang.dll' + New-TestPE $path 0x8664 + Test-LibclangArchitecture $path | Should -BeFalse + New-TestPE $path 0xAA64 + Test-LibclangArchitecture $path | Should -BeTrue + } + + It 'rejects a missing DLL' { + Test-LibclangArchitecture (Join-Path $TestDrive 'missing.dll') | Should -BeFalse + } + + It 'rejects a truncated DLL' { + $path = Join-Path $TestDrive 'broken.dll' + [IO.File]::WriteAllBytes($path, [byte[]]@(0x4D, 0x5A)) + Test-LibclangArchitecture $path | Should -BeFalse + } + } + + Describe 'Pinned tools and helper installation' { + It 'reads Rust configuration with the actual Python TOML parser' { + $script:WorkspaceRoot = Join-Path $TestDrive 'Rust workspace' + New-Item -ItemType Directory -Path $script:WorkspaceRoot | Out-Null + $toml = @( + '[toolchain]', + '# Exercise comments and a multiline array with the real parser.', + 'channel = "1.99.3"', + 'components = [', + ' "clippy",', + ' "rustfmt",', + ' "rust-src",', + ']' + ) + Set-Content -LiteralPath (Join-Path $script:WorkspaceRoot 'rust-toolchain.toml') -Value $toml -Encoding ASCII + $configuration = Get-RustConfiguration + $configuration.channel | Should -Be '1.99.3' + $configuration.components | Should -Contain 'clippy' + $configuration.components | Should -Contain 'rustfmt' + $configuration.components | Should -Contain 'rust-src' + } + + It 'installs the configured components for the native MSVC host' { + $script:Architecture = 'arm64' + Mock Get-ApplicationPath { param($Name) $Name } + Mock Invoke-Native { + param($FilePath, $ArgumentList) + if ($ArgumentList[0] -eq 'run') { 'host: aarch64-pc-windows-msvc' } + elseif ($ArgumentList[0] -eq 'component') { 'clippy-aarch64-pc-windows-msvc'; 'rust-src' } + } + $configuration = [pscustomobject]@{ channel = '1.95.0'; components = @('clippy', 'rust-src') } + Ensure-RustToolchain $configuration | Should -Be '1.95.0-aarch64-pc-windows-msvc' + Should -Invoke Invoke-Native -ParameterFilter { + $ArgumentList[0] -eq 'toolchain' -and $ArgumentList -contains '1.95.0-aarch64-pc-windows-msvc' -and + $ArgumentList -contains 'clippy' -and $ArgumentList -contains 'rust-src' + } + Should -Invoke Invoke-Native -Times 0 -Exactly -ParameterFilter { $ArgumentList -contains 'default' } + } + + It 'rejects a GNU or emulated Rust host' { + Mock Get-ApplicationPath { param($Name) $Name } + Mock Invoke-Native { 'host: x86_64-pc-windows-gnu' } + $configuration = [pscustomobject]@{ channel = '1.95.0'; components = @('rustfmt') } + { Ensure-RustToolchain $configuration } | Should -Throw '*native MSVC host*' + } + + It 'honors CARGO_HOME and uses the pinned toolchain for Cargo helpers' { + Mock Get-ApplicationPath { param($Name) if ($Name -eq 'cargo.exe') { $Name } } + Mock Invoke-Native {} + Ensure-CargoTool 'cargo-nextest' '1.95.0-x86_64-pc-windows-msvc' 'C:\Custom Cargo Home' + Should -Invoke Invoke-Native -ParameterFilter { + $ArgumentList -contains '+1.95.0-x86_64-pc-windows-msvc' -and + $ArgumentList -contains 'install' -and $ArgumentList -contains '--locked' -and + $ArgumentList -contains 'C:\Custom Cargo Home' + } + Should -Invoke Invoke-Native -ParameterFilter { + $ArgumentList -contains 'nextest' -and $ArgumentList -contains '--version' + } + } + + It 'uses npm.cmd and pnpm.cmd without invoking blocked PowerShell shims' { + $script:pnpmReady = $false + Mock Get-ApplicationPath { + param($Name) + if ($Name -eq 'npm.cmd' -or ($Name -eq 'pnpm.cmd' -and $script:pnpmReady)) { $Name } + } + Mock Invoke-Native { + param($FilePath, $ArgumentList) + if ($ArgumentList -contains 'install') { $script:pnpmReady = $true } + elseif ($ArgumentList -contains 'prefix') { 'C:\npm prefix' } + else { '10.34.5' } + } + Mock Add-UserPath {} + Ensure-Pnpm '10.34.5' + Should -Invoke Invoke-Native -ParameterFilter { + $FilePath -eq 'npm.cmd' -and $ArgumentList -contains 'pnpm@10.34.5' -and + $ArgumentList -contains '--ignore-scripts' + } + } + + It 'checks an existing Bazel wrapper without launching Bazel or downloading it' { + $script:CheckOnly = $true + $bin = Join-Path $TestDrive 'bin' + New-Item -ItemType Directory -Path $bin | Out-Null + $content = @('@echo off', 'bazelisk.exe %*', 'exit /b %errorlevel%', '') -join ([char]13 + [string][char]10) + Set-Content -LiteralPath (Join-Path $bin 'bazel.cmd') -Value $content -Encoding ASCII -NoNewline + Mock Get-ApplicationPath { 'bazelisk.exe' } + Mock Add-UserPath {} + Mock Invoke-Native {} + Ensure-Bazelisk $bin + Should -Invoke Invoke-Native -Times 0 -Exactly + } + } + + Describe 'Setup orchestration' { + BeforeEach { + Mock Get-Content { + '{"packageManager":"pnpm@10.40.2+sha512.test","engines":{"node":">=24.1"}}' + } -ParameterFilter { $LiteralPath -like '*\package.json' } + Mock Ensure-Tool {} + Mock Ensure-VisualStudio { 'C:\VS' } + Mock Add-UserPath {} + Mock Update-SessionPath {} + Mock Ensure-LLVM {} + Mock Ensure-Pnpm {} + Mock Ensure-Bazelisk {} + Mock Enter-VisualStudioEnvironment {} + Mock Ensure-RustToolchain { '1.95.0-x86_64-pc-windows-msvc' } + Mock Ensure-CargoTool {} + Mock Get-ApplicationPath { param($Name) $Name } + Mock Invoke-Native {} + Mock Get-RustConfiguration { [pscustomobject]@{ channel = '1.95.0'; components = @('clippy', 'rustfmt', 'rust-src') } } + } + + It 'uses repository configuration, restores the directory, and never builds Codex' { + $before = (Get-Location).Path + Initialize-WindowsDevelopment + (Get-Location).Path | Should -Be $before + Should -Invoke Ensure-Pnpm -ParameterFilter { $Version -eq '10.40.2' } + Should -Invoke Ensure-Tool -ParameterFilter { + $Id -eq 'OpenJS.NodeJS.LTS' -and $MinimumVersion -eq [version]'24.1' + } + Should -Invoke Invoke-Native -Times 0 -Exactly -ParameterFilter { $ArgumentList -contains 'build' } + Should -Invoke Invoke-Native -ParameterFilter { + $ArgumentList -contains 'core.longpaths' -and $ArgumentList -contains '--local' + } + } + + It 'disables implicit Rust downloads and restores the caller setting after failure' { + $env:RUSTUP_AUTO_INSTALL = 'custom' + Mock Ensure-Tool { + $env:RUSTUP_AUTO_INSTALL | Should -Be '0' + throw 'Test failure' + } + $before = (Get-Location).Path + { Initialize-WindowsDevelopment } | Should -Throw '*Test failure*' + $env:RUSTUP_AUTO_INSTALL | Should -Be 'custom' + (Get-Location).Path | Should -Be $before + } + + It 'resolves a relative CARGO_HOME before changing into the workspace' { + Push-Location -LiteralPath $TestDrive + try { + $env:CARGO_HOME = 'Custom Cargo Home' + $expected = Join-Path $TestDrive 'Custom Cargo Home' + Initialize-WindowsDevelopment + $env:CARGO_HOME | Should -Be $expected + Should -Invoke Ensure-CargoTool -ParameterFilter { $CargoHome -eq $expected } + } finally { + Pop-Location + } + } + + It 'does not write Git settings or allow implicit Rust downloads in CheckOnly mode' { + $script:CheckOnly = $true + Mock Ensure-Tool { $env:RUSTUP_AUTO_INSTALL | Should -Be '0' } + Initialize-WindowsDevelopment + Should -Invoke Invoke-Native -Times 0 -Exactly -ParameterFilter { $ArgumentList -contains 'config' } + } + } +} diff --git a/codex-rs/scripts/setup-windows.ps1 b/codex-rs/scripts/setup-windows.ps1 index a8fe0c2f7540..06540588cd8b 100644 --- a/codex-rs/scripts/setup-windows.ps1 +++ b/codex-rs/scripts/setup-windows.ps1 @@ -1,246 +1,490 @@ +#Requires -Version 5.1 <# - Setup script for building codex-rs on Windows. - - What it does: - - Installs Rust toolchain (via winget rustup) and required components - - Installs Visual Studio 2022 Build Tools (MSVC + Windows SDK) - - Installs helpful CLIs used by the repo: git, ripgrep (rg), just, cmake - - Installs cargo-insta (for snapshot tests) via cargo - - Ensures PATH contains Cargo bin for the current session - - Builds the workspace (cargo build) - - Usage: - - Right-click PowerShell and "Run as Administrator" (VS Build Tools require elevation) - - From the repo root (codex-rs), run: - powershell -ExecutionPolicy Bypass -File scripts/setup-windows.ps1 - - Notes: - - Requires winget (Windows Package Manager). Most modern Windows 10/11 have it preinstalled. - - The script is re-runnable; winget/cargo will skip/reinstall as appropriate. +.SYNOPSIS + Install and verify the Windows development tools used by the Codex repository. +.DESCRIPTION + Supports Windows PowerShell 5.1 and PowerShell 7 on x64 and ARM64 Windows. + Run as your normal Windows user; machine-wide installers may request UAC + elevation. Rust and pnpm versions come from the repository configuration. + + Installs prerequisites and Cargo helper tools, but does not build Codex, + run tests, or install workspace JavaScript/Python dependencies. Existing + tools are reused when their versions satisfy the repository's requirements. + Compiler settings are applied only to the current PowerShell process. +.PARAMETER CheckOnly + Verify existing tools and activate the MSVC environment in this session. + Does not install packages or write persistent environment/Git settings. +.EXAMPLE + & .\codex-rs\scripts\setup-windows.ps1 +.EXAMPLE + & .\codex-rs\scripts\setup-windows.ps1 -CheckOnly +.NOTES + Requires WinGet 1.6 or newer for installation. Reopen PowerShell after setup + when invoking this script through powershell.exe -File or pwsh -File. #> - +[CmdletBinding()] param( - [switch] $SkipBuild + [switch]$CheckOnly ) +Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' +# Check exit codes ourselves, including on PowerShell 7 with this option enabled. +$PSNativeCommandUseErrorActionPreference = $false + +function Invoke-Native { + param( + [Parameter(Mandatory = $true)][string]$FilePath, + [string[]]$ArgumentList = @(), + [int[]]$SuccessExitCodes = @(0) + ) + + & $FilePath @ArgumentList + $code = $LASTEXITCODE + if ($code -eq 3010 -or $code -eq -1978334967) { + # WinGet INSTALL_REBOOT_REQUIRED_TO_FINISH (0x8A150109), or MSI 3010. + throw "$FilePath requires a restart to finish installation. Restart Windows and rerun setup." + } + if ($SuccessExitCodes -notcontains $code) { + throw "$FilePath failed with exit code $code. Resolve the error above and rerun setup." + } +} -function Ensure-Command($Name) { - $exists = Get-Command $Name -ErrorAction SilentlyContinue - return $null -ne $exists -} - -function Add-CargoBinToPath() { - $cargoBin = Join-Path $env:USERPROFILE ".cargo\bin" - if (Test-Path $cargoBin) { - if (-not ($env:Path.Split(';') -contains $cargoBin)) { - $env:Path = "$env:Path;$cargoBin" - } - } -} - -function Ensure-UserPathContains([string] $Segment) { - try { - $userPath = [Environment]::GetEnvironmentVariable('Path', 'User') - if ($null -eq $userPath) { $userPath = '' } - $parts = $userPath.Split(';') | Where-Object { $_ -ne '' } - if (-not ($parts -contains $Segment)) { - $newPath = if ($userPath) { "$userPath;$Segment" } else { $Segment } - [Environment]::SetEnvironmentVariable('Path', $newPath, 'User') - } - } catch {} -} - -function Ensure-UserEnvVar([string] $Name, [string] $Value) { - try { [Environment]::SetEnvironmentVariable($Name, $Value, 'User') } catch {} -} - -function Ensure-VSComponents([string[]]$Components) { - $vsInstaller = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vs_installer.exe" - $vswhere = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe" - if (-not (Test-Path $vsInstaller) -or -not (Test-Path $vswhere)) { return } - - $instPath = & $vswhere -latest -products * -version "[17.0,18.0)" -requires Microsoft.VisualStudio.Workload.VCTools -property installationPath 2>$null - if (-not $instPath) { - # 2022 instance may be present without VC Tools; pick BuildTools 2022 and add components - $instPath = & $vswhere -latest -products Microsoft.VisualStudio.Product.BuildTools -version "[17.0,18.0)" -property installationPath 2>$null - } - if (-not $instPath) { - $instPath = & $vswhere -latest -products * -requires Microsoft.VisualStudio.Workload.VCTools -property installationPath 2>$null - } - if (-not $instPath) { - $default2022 = 'C:\\Program Files (x86)\\Microsoft Visual Studio\\2022\\BuildTools' - if (Test-Path $default2022) { $instPath = $default2022 } - } - if (-not $instPath) { return } - - $vsDevCmd = Join-Path $instPath 'Common7\Tools\VsDevCmd.bat' - $verb = if (Test-Path $vsDevCmd) { 'modify' } else { 'install' } - $args = @($verb, '--installPath', $instPath, '--quiet', '--norestart', '--nocache') - if ($verb -eq 'install') { $args += @('--productId', 'Microsoft.VisualStudio.Product.BuildTools') } - foreach ($c in $Components) { $args += @('--add', $c) } - Write-Host "-- Ensuring VS components installed: $($Components -join ', ')" -ForegroundColor DarkCyan - & $vsInstaller @args | Out-Host -} - -function Enter-VsDevShell() { - $vswhere = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe" - if (-not (Test-Path $vswhere)) { return } - - $instPath = & $vswhere -latest -products * -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath 2>$null - if (-not $instPath) { - # Try ARM64 components - $instPath = & $vswhere -latest -products * -requires Microsoft.VisualStudio.Component.VC.Tools.ARM64 -property installationPath 2>$null - } - if (-not $instPath) { return } - - $vsDevCmd = Join-Path $instPath 'Common7\Tools\VsDevCmd.bat' - if (-not (Test-Path $vsDevCmd)) { return } - - # Prefer ARM64 on ARM machines, otherwise x64 - $arch = if ($env:PROCESSOR_ARCHITEW6432 -eq 'ARM64' -or $env:PROCESSOR_ARCHITECTURE -eq 'ARM64') { 'arm64' } else { 'x64' } - $devCmdStr = ('"{0}" -no_logo -arch={1} -host_arch={1} & set' -f $vsDevCmd, $arch) - $envLines = & cmd.exe /c $devCmdStr - foreach ($line in $envLines) { - if ($line -match '^(.*?)=(.*)$') { - $name = $matches[1] - $value = $matches[2] - try { [Environment]::SetEnvironmentVariable($name, $value, 'Process') } catch {} - } - } -} - -Write-Host "==> Installing prerequisites via winget (may take a while)" -ForegroundColor Cyan - -# Accept agreements up-front for non-interactive installs -$WingetArgs = @('--accept-package-agreements', '--accept-source-agreements', '-e') - -if (-not (Ensure-Command 'winget')) { - throw "winget is required. Please update to the latest Windows 10/11 or install winget." -} - -# 1) Visual Studio 2022 Build Tools (MSVC toolchain + Windows SDK) -# The VC Tools workload brings the required MSVC toolchains; include recommended components to pick up a Windows SDK. -Write-Host "-- Installing Visual Studio Build Tools (VC Tools workload + ARM64 toolchains)" -ForegroundColor DarkCyan -$vsOverride = @( - '--quiet', '--wait', '--norestart', '--nocache', - '--add', 'Microsoft.VisualStudio.Workload.VCTools', - '--add', 'Microsoft.VisualStudio.Component.VC.Tools.ARM64', - '--add', 'Microsoft.VisualStudio.Component.VC.Tools.ARM64EC', - '--add', 'Microsoft.VisualStudio.Component.Windows11SDK.22000' -) -join ' ' -winget install @WingetArgs --id Microsoft.VisualStudio.2022.BuildTools --override $vsOverride | Out-Host - -# Ensure required VC components even if winget doesn't modify the instance -$isArm64 = ($env:PROCESSOR_ARCHITEW6432 -eq 'ARM64' -or $env:PROCESSOR_ARCHITECTURE -eq 'ARM64') -$components = @( - 'Microsoft.VisualStudio.Workload.VCTools', - 'Microsoft.VisualStudio.Component.VC.Tools.ARM64', - 'Microsoft.VisualStudio.Component.VC.Tools.ARM64EC', - 'Microsoft.VisualStudio.Component.Windows11SDK.22000' -) -Ensure-VSComponents -Components $components +function Get-ApplicationPath { + param([string]$Name) + + $commands = @(Get-Command $Name -CommandType Application -All -ErrorAction SilentlyContinue) + foreach ($command in $commands) { + # Do not launch the Microsoft Store's Python app execution alias. + if ($Name -eq 'python.exe' -and $command.Source -match '\\Microsoft\\WindowsApps\\') { + continue + } + return $command.Source + } + return $null +} + +function Update-SessionPath { + param([string[]]$Prepend = @()) + + $entries = @($Prepend) + @( + [Environment]::GetEnvironmentVariable('Path', 'User'), + [Environment]::GetEnvironmentVariable('Path', 'Machine'), + $env:Path + ) + $paths = @() + foreach ($entry in $entries) { + foreach ($part in ($entry -split ';')) { + $part = [Environment]::ExpandEnvironmentVariables($part.Trim().Trim('"')) + if ($part -and $paths -notcontains $part) { $paths += $part } + } + } + $env:Path = $paths -join ';' +} + +function Add-UserPath { + param([string]$Directory) -# 2) Rustup -Write-Host "-- Installing rustup" -ForegroundColor DarkCyan -winget install @WingetArgs --id Rustlang.Rustup | Out-Host + if (-not $CheckOnly) { + $userPath = [Environment]::GetEnvironmentVariable('Path', 'User') + if (@($userPath -split ';') -notcontains $Directory) { + $newPath = (@($Directory) + @($userPath -split ';' | Where-Object { $_ })) -join ';' + [Environment]::SetEnvironmentVariable('Path', $newPath, 'User') + } + } + Update-SessionPath -Prepend @($Directory) +} + +function Get-ToolVersion { + param([string]$Command, [string[]]$VersionArguments = @('--version')) + + $path = Get-ApplicationPath $Command + if (-not $path) { return $null } + $output = (Invoke-Native $path $VersionArguments) -join [Environment]::NewLine + if ($output -notmatch '(? Configuring Rust toolchain per rust-toolchain.toml" -ForegroundColor Cyan +function Enter-VisualStudioEnvironment { + param([string]$Installation) + + $devCommand = Join-Path $Installation 'Common7\Tools\VsDevCmd.bat' + if (-not (Test-Path -LiteralPath $devCommand)) { throw "Visual Studio developer shell is missing: $devCommand" } + $command = '"{0}" -no_logo -arch={1} -host_arch={1} >nul && set' -f $devCommand, $script:Architecture + # /d disables cmd AutoRun hooks; && prevents importing a failed environment. + $lines = Invoke-Native $env:ComSpec @('/d', '/c', $command) + $variables = @( + 'PATH', 'INCLUDE', 'LIB', 'LIBPATH', 'VCINSTALLDIR', 'VCToolsInstallDir', + 'WindowsSdkDir', 'WindowsSDKVersion', 'WindowsSDKLibVersion', + 'WindowsSdkBinPath', 'WindowsLibPath', 'UniversalCRTSdkDir', 'UCRTVersion' + ) + foreach ($line in $lines) { + if ($line -match '^([^=]+)=(.*)$' -and $variables -contains $Matches[1]) { + [Environment]::SetEnvironmentVariable($Matches[1], $Matches[2], 'Process') + } + } + if (-not $env:VCToolsInstallDir -or -not $env:WindowsSdkDir -or -not $env:WindowsSDKVersion) { + throw 'Visual Studio did not expose the MSVC/Windows SDK environment.' + } + foreach ($tool in @('cl.exe', 'link.exe', 'rc.exe')) { + $path = Get-ApplicationPath $tool + if (-not $path) { throw "Visual Studio did not provide $tool for $script:Architecture." } + $root = if ($tool -eq 'rc.exe') { $env:WindowsSdkDir } else { $env:VCToolsInstallDir } + if (-not $path.StartsWith($root, [StringComparison]::OrdinalIgnoreCase)) { + throw "$tool resolves outside the selected Visual Studio/SDK installation: $path" + } + } + $sdkVersion = $env:WindowsSDKVersion.TrimEnd('\') + $header = Join-Path $env:WindowsSdkDir "Include\$sdkVersion\um\Windows.h" + $library = Join-Path $env:WindowsSdkDir "Lib\$sdkVersion\um\$script:Architecture\kernel32.lib" + if (-not (Test-Path -LiteralPath $header) -or -not (Test-Path -LiteralPath $library)) { + throw "Windows SDK headers/libraries are missing for $script:Architecture. Repair the selected SDK." + } + Write-Host "-- MSVC and Windows SDK ready ($script:Architecture)" -ForegroundColor DarkCyan +} -# Pin to the workspace toolchain and install components -$toolchain = '1.95.0' -& rustup toolchain install $toolchain --profile minimal | Out-Host -& rustup default $toolchain | Out-Host -& rustup component add clippy rustfmt rust-src --toolchain $toolchain | Out-Host +function Test-LibclangArchitecture { + param([string]$Path) + + if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) { return $false } + $stream = [IO.File]::OpenRead($Path) + $reader = New-Object IO.BinaryReader($stream) + try { + if ($reader.ReadUInt16() -ne 0x5A4D) { return $false } + $stream.Position = 0x3C + $stream.Position = $reader.ReadInt32() + if ($reader.ReadUInt32() -ne 0x00004550) { return $false } + $machine = $reader.ReadUInt16() + $expected = if ($script:Architecture -eq 'arm64') { 0xAA64 } else { 0x8664 } + return $machine -eq $expected + } catch [IO.EndOfStreamException] { + return $false + } finally { + $reader.Dispose() + } +} -# 6.5) LLVM/Clang (some crates/bindgen require clang/libclang) -function Add-LLVMToPath() { - $llvmBin = 'C:\\Program Files\\LLVM\\bin' - if (Test-Path $llvmBin) { - if (-not ($env:Path.Split(';') -contains $llvmBin)) { - $env:Path = "$env:Path;$llvmBin" +function Ensure-LLVM { + $nativeProgramFiles = $env:ProgramW6432 + if (-not $nativeProgramFiles) { $nativeProgramFiles = $env:ProgramFiles } + $directories = @($env:LIBCLANG_PATH, (Join-Path $nativeProgramFiles 'LLVM\bin')) + $clang = Get-ApplicationPath 'clang.exe' + if ($clang) { $directories += Split-Path -Parent $clang } + $directory = $null + foreach ($candidate in $directories) { + if ($candidate -and (Test-LibclangArchitecture (Join-Path $candidate 'libclang.dll')) -and + (Test-Path -LiteralPath (Join-Path $candidate 'clang.exe'))) { + $directory = $candidate + break + } } - if (-not $env:LIBCLANG_PATH) { - $env:LIBCLANG_PATH = $llvmBin + if (-not $directory) { + Install-WinGetPackage 'LLVM.LLVM' + $clang = Get-ApplicationPath 'clang.exe' + if ($clang) { $directories = @((Split-Path -Parent $clang)) + $directories } + foreach ($candidate in $directories) { + if ($candidate -and (Test-LibclangArchitecture (Join-Path $candidate 'libclang.dll')) -and + (Test-Path -LiteralPath (Join-Path $candidate 'clang.exe'))) { + $directory = $candidate + break + } + } } - Ensure-UserPathContains $llvmBin - Ensure-UserEnvVar -Name 'LIBCLANG_PATH' -Value $llvmBin + if (-not $directory) { throw "LLVM did not provide a native $script:Architecture libclang.dll. Check the LLVM installation." } + Add-UserPath $directory + $env:LIBCLANG_PATH = $directory + Invoke-Native (Join-Path $directory 'clang.exe') @('--version') | Out-Host + # Do not set CC/CXX: native crates should continue to use MSVC by default. +} - $clang = Join-Path $llvmBin 'clang.exe' - $clangxx = Join-Path $llvmBin 'clang++.exe' - if (Test-Path $clang) { - $env:CC = $clang - Ensure-UserEnvVar -Name 'CC' -Value $clang +function Get-RustConfiguration { + $python = Get-ApplicationPath 'python.exe' + $code = "import json, sys, tomllib; print(json.dumps(tomllib.load(open(sys.argv[1], 'rb'))['toolchain']))" + $output = Invoke-Native $python @('-c', $code, (Join-Path $script:WorkspaceRoot 'rust-toolchain.toml')) + return ($output -join [Environment]::NewLine) | ConvertFrom-Json +} + +function Ensure-RustToolchain { + param($Configuration) + + $hostTriple = if ($script:Architecture -eq 'arm64') { 'aarch64-pc-windows-msvc' } else { 'x86_64-pc-windows-msvc' } + $toolchain = "$($Configuration.channel)-$hostTriple" + $rustup = Get-ApplicationPath 'rustup.exe' + if (-not $CheckOnly) { + $arguments = @('toolchain', 'install', $toolchain, '--profile', 'minimal') + foreach ($component in $Configuration.components) { $arguments += @('--component', $component) } + if ($Configuration.PSObject.Properties['targets']) { + foreach ($target in $Configuration.targets) { $arguments += @('--target', $target) } + } + Invoke-Native $rustup $arguments | Out-Host + } + $details = (Invoke-Native $rustup @('run', $toolchain, 'rustc', '-vV')) -join [Environment]::NewLine + if ($details -notmatch "(?m)^host: $([regex]::Escape($hostTriple))\r?$") { + throw "Rust must use the native MSVC host $hostTriple." } - if (Test-Path $clangxx) { - $env:CXX = $clangxx - Ensure-UserEnvVar -Name 'CXX' -Value $clangxx + $installed = @(Invoke-Native $rustup @('component', 'list', '--toolchain', $toolchain, '--installed')) + foreach ($component in $Configuration.components) { + if (-not ($installed -match "^$([regex]::Escape($component))(-|$)")) { + throw "Rust component '$component' is missing. Rerun setup without -CheckOnly." + } } - } + if ($Configuration.PSObject.Properties['targets']) { + $targets = @(Invoke-Native $rustup @('target', 'list', '--toolchain', $toolchain, '--installed')) + foreach ($target in $Configuration.targets) { + if ($targets -notcontains $target) { throw "Rust target '$target' is missing. Rerun setup without -CheckOnly." } + } + } + # Activate the native MSVC toolchain without changing rustup's global default. + $env:RUSTUP_TOOLCHAIN = $toolchain + Invoke-Native (Get-ApplicationPath 'cargo.exe') @("+$toolchain", '--version') | Out-Host + return $toolchain } -Write-Host "-- Installing LLVM/Clang" -ForegroundColor DarkCyan -winget install @WingetArgs --id LLVM.LLVM | Out-Host -Add-LLVMToPath +function Ensure-CargoTool { + param([string]$Name, [string]$Toolchain, [string]$CargoHome) -# 7) cargo-insta (used by snapshot tests) -# Ensure MSVC linker is available before building/cargo-install by entering VS dev shell -Enter-VsDevShell -$hasLink = $false -try { & where.exe link | Out-Null; $hasLink = $true } catch {} -if ($hasLink) { - Write-Host "-- Installing cargo-insta" -ForegroundColor DarkCyan - & cargo install cargo-insta --locked | Out-Host -} else { - Write-Host "-- Skipping cargo-insta for now (MSVC linker not found yet)" -ForegroundColor Yellow + $command = "$Name.exe" + if (-not (Get-ApplicationPath $command)) { + if ($CheckOnly) { throw "$Name is missing. Rerun setup without -CheckOnly." } + Write-Host "-- Installing $Name (Cargo helper)" -ForegroundColor Cyan + Invoke-Native (Get-ApplicationPath 'cargo.exe') @( + "+$Toolchain", 'install', '--locked', '--root', $CargoHome, $Name + ) | Out-Host + } + if ($Name -like 'cargo-*') { + $subcommand = $Name.Substring('cargo-'.Length) + Invoke-Native (Get-ApplicationPath 'cargo.exe') @("+$Toolchain", $subcommand, '--version') | Out-Host + } else { + Invoke-Native (Get-ApplicationPath $command) @('--version') | Out-Host + } } -if ($SkipBuild) { - Write-Host "==> Skipping cargo build (SkipBuild specified)" -ForegroundColor Yellow - exit 0 +function Ensure-Pnpm { + param([string]$Version) + + $command = Get-ApplicationPath 'pnpm.cmd' + $current = if ($command) { (Invoke-Native $command @('--version')) -join '' } else { '' } + if ($current.Trim() -ne $Version) { + if ($CheckOnly) { throw "pnpm $Version is required by package.json. Rerun setup without -CheckOnly." } + $npm = Get-ApplicationPath 'npm.cmd' + if (-not $npm) { throw 'The Node.js installation did not provide npm.cmd.' } + Invoke-Native $npm @('install', '--global', "pnpm@$Version", '--ignore-scripts', '--no-audit', '--no-fund') | Out-Host + $prefix = (Invoke-Native $npm @('prefix', '--global')) -join '' + Add-UserPath $prefix.Trim() + $command = Get-ApplicationPath 'pnpm.cmd' + if (-not $command) { throw 'pnpm.cmd was not found after installation. Check the npm global prefix.' } + $current = (Invoke-Native $command @('--version')) -join '' + } + if ($current.Trim() -ne $Version) { throw "pnpm resolved to '$current', but package.json requires $Version." } + Write-Host "-- pnpm $Version" -ForegroundColor DarkCyan } -Write-Host "==> Building workspace (cargo build)" -ForegroundColor Cyan -pushd "$PSScriptRoot\.." | Out-Null -try { - # Clear RUSTFLAGS if coming from constrained environments - $env:RUSTFLAGS = '' - Enter-VsDevShell - & cargo build +function Ensure-Bazelisk { + param([string]$BinDirectory) + + if (-not (Get-ApplicationPath 'bazelisk.exe')) { Install-WinGetPackage 'Bazel.Bazelisk' } + if (-not (Get-ApplicationPath 'bazelisk.exe')) { throw 'WinGet did not provide bazelisk.exe.' } + $shim = Join-Path $BinDirectory 'bazel.cmd' + # WinGet exposes bazelisk, whereas the repository recipes invoke bazel. + # Keep the wrapper ASCII, including for non-ASCII Windows profile paths. + $content = @('@echo off', 'bazelisk.exe %*', 'exit /b %errorlevel%', '') -join ([char]13 + [string][char]10) + $existing = if (Test-Path -LiteralPath $shim) { Get-Content -LiteralPath $shim -Raw } else { '' } + if ($existing -ne $content) { + if ($CheckOnly) { throw 'The bazel.cmd wrapper is missing or outdated. Rerun setup without -CheckOnly.' } + New-Item -ItemType Directory -Path $BinDirectory -Force | Out-Null + Set-Content -LiteralPath $shim -Value $content -Encoding ASCII -NoNewline + } + Add-UserPath $BinDirectory + # --version downloads the pinned Bazel if necessary, but never builds code. + # CheckOnly checks the wrapper above to avoid downloading Bazel. + if (-not $CheckOnly) { + $version = (Invoke-Native $shim @('--version')) -join '' + $expected = (Get-Content -LiteralPath (Join-Path $script:RepositoryRoot '.bazelversion') -Raw).Trim() + if ($version.Trim() -ne "bazel $expected") { throw "Bazel version '$version' does not match .bazelversion ($expected)." } + Write-Host "-- $version" -ForegroundColor DarkCyan + } } -finally { - popd | Out-Null + +function Initialize-WindowsDevelopment { + if ($env:OS -ne 'Windows_NT') { throw 'This script requires Windows.' } + $script:Architecture = Get-WindowsArchitecture + $script:WorkspaceRoot = Split-Path -Parent $PSScriptRoot + $script:RepositoryRoot = Split-Path -Parent $script:WorkspaceRoot + $package = Get-Content -LiteralPath (Join-Path $script:RepositoryRoot 'package.json') -Raw | ConvertFrom-Json + if ($package.packageManager -notmatch '^pnpm@(\d+\.\d+\.\d+)(?:\+|$)') { + throw 'package.json must pin a pnpm version in packageManager.' + } + $pnpmVersion = $Matches[1] + if ($package.engines.node -notmatch '^>=(\d+(?:\.\d+){0,2})$') { throw 'Unsupported Node.js engine requirement in package.json.' } + $nodeVersion = $Matches[1] + if ($nodeVersion -notmatch '\.') { $nodeVersion += '.0' } + $nodeMinimum = [version]$nodeVersion + $cargoHome = $env:CARGO_HOME + if (-not $cargoHome) { $cargoHome = Join-Path $env:USERPROFILE '.cargo' } + $cargoHome = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($cargoHome) + $env:CARGO_HOME = $cargoHome + $cargoBin = Join-Path $cargoHome 'bin' + $devBin = Join-Path $env:LOCALAPPDATA 'Codex\dev-tools\bin' + Update-SessionPath -Prepend @($cargoBin, $devBin) + + # Older rustup versions can install the active toolchain even for --version. + # Only the explicit toolchain-install step is allowed to download Rust. + Push-Location -LiteralPath $script:WorkspaceRoot + $previousAutoInstall = $env:RUSTUP_AUTO_INSTALL + try { + $env:RUSTUP_AUTO_INSTALL = '0' + Write-Host "==> Codex Windows development tools ($script:Architecture)" -ForegroundColor Cyan + Ensure-Tool 'Git.Git' 'git.exe' -MinimumVersion '2.23' + # CommandWithArgs, used by just-shell.py, became stable in PowerShell 7.5. + Ensure-Tool 'Microsoft.PowerShell' 'pwsh.exe' -MinimumVersion '7.5' -ExtraArguments @('--installer-type', 'wix') + # Python 3.11+ provides tomllib; reuse newer Python or install CI's 3.12. + Ensure-Tool 'Python.Python.3.12' 'python.exe' -MinimumVersion '3.11' -ExtraArguments @('--scope', 'user') + $rustConfiguration = Get-RustConfiguration + $installation = Ensure-VisualStudio + Ensure-Tool 'Rustlang.Rustup' 'rustup.exe' -MinimumVersion '1.28.1' -ExtraArguments @('--custom', '--default-toolchain none --profile minimal') + Add-UserPath $cargoBin + Ensure-Tool 'BurntSushi.ripgrep.MSVC' 'rg.exe' + Ensure-Tool 'Casey.Just' 'just.exe' -MinimumVersion '1.51.0' + Ensure-Tool 'Kitware.CMake' 'cmake.exe' -ExtraArguments @('--installer-type', 'wix') + Ensure-Tool 'astral-sh.uv' 'uv.exe' -MinimumVersion '0.11.19' + Ensure-Tool 'OpenJS.NodeJS.LTS' 'node.exe' -MinimumVersion $nodeMinimum -ExtraArguments @('--installer-type', 'wix') + Ensure-LLVM + Ensure-Pnpm $pnpmVersion + Ensure-Bazelisk $devBin + # Refresh PATH before entering VS, so compiler/linker paths stay first. + Enter-VisualStudioEnvironment $installation + $toolchain = Ensure-RustToolchain $rustConfiguration + foreach ($tool in @('cargo-insta', 'cargo-nextest', 'dotslash')) { + Ensure-CargoTool $tool $toolchain $cargoHome + } + Invoke-Native (Get-ApplicationPath 'just.exe') @('--list') | Out-Null + if (-not $CheckOnly) { + Invoke-Native (Get-ApplicationPath 'git.exe') @('-C', $script:RepositoryRoot, 'config', '--local', 'core.longpaths', 'true') | Out-Host + } + Write-Host '==> Development environment verified. Codex was not built.' -ForegroundColor Green + Write-Host 'Use this PowerShell session, or rerun with -CheckOnly in a new session to activate MSVC.' + } finally { + Pop-Location + [Environment]::SetEnvironmentVariable('RUSTUP_AUTO_INSTALL', $previousAutoInstall, 'Process') + } } -Write-Host "==> Build complete" -ForegroundColor Green +Initialize-WindowsDevelopment diff --git a/docs/install.md b/docs/install.md index 4f63765b5e72..71dc86e290f7 100644 --- a/docs/install.md +++ b/docs/install.md @@ -2,11 +2,11 @@ ### System requirements -| Requirement | Details | -| --------------------------- | --------------------------------------------------------------- | -| Operating systems | macOS 12+, Ubuntu 20.04+/Debian 10+, or Windows 11 **via WSL2** | -| Git (optional, recommended) | 2.23+ for built-in PR helpers | -| RAM | 4-GB minimum (8-GB recommended) | +| Requirement | Details | +| --------------------------- | ------------------------------------------------------------------------------ | +| Operating systems | macOS 12+, Ubuntu 20.04+/Debian 10+, or Windows 11 (native PowerShell or WSL2) | +| Git (optional, recommended) | 2.23+ for built-in PR helpers | +| RAM | 4-GB minimum (8-GB recommended) | ### DotSlash @@ -14,6 +14,80 @@ The GitHub Release also contains a [DotSlash](https://dotslash-cli.com/) file fo ### Build from source +#### Windows development (PowerShell) + +From a checkout of this repository, run the setup script as your normal Windows +user. Windows PowerShell 5.1 can bootstrap the environment; development recipes +use PowerShell 7.5 or newer. Machine-wide installers may request UAC elevation. + +```powershell +& .\codex-rs\scripts\setup-windows.ps1 +``` + +The script supports x64 and ARM64 Windows and requires [WinGet 1.6 or newer](https://learn.microsoft.com/windows/package-manager/winget/). +It can be invoked from any directory by providing the path to the script. If +execution policy blocks it, use a separate process without changing your saved +execution policy: + +```powershell +powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\codex-rs\scripts\setup-windows.ps1 +``` + +Setup installs and verifies these tools, reusing suitable existing installations: + +| Tools | Installation source | +| --------------------------------------- | ------------------------------------------------------------------------------------- | +| Git, PowerShell 7 | `Git.Git`, `Microsoft.PowerShell` via WinGet | +| MSVC and Windows SDK | Existing Visual Studio 2022 or newer, or `Microsoft.VisualStudio.2022.BuildTools` | +| Rust and workspace components | `Rustlang.Rustup`; version/components from `codex-rs/rust-toolchain.toml` | +| Python 3.11+ | Existing Python, or `Python.Python.3.12` via WinGet | +| ripgrep, just, CMake, LLVM/libclang, uv | `BurntSushi.ripgrep.MSVC`, `Casey.Just`, `Kitware.CMake`, `LLVM.LLVM`, `astral-sh.uv` | +| Node.js and pnpm | `OpenJS.NodeJS.LTS`; Node minimum and exact pnpm version from `package.json` | +| Bazel | `Bazel.Bazelisk`; downloads the version in `.bazelversion` | +| Snapshot/test helpers and DotSlash | `cargo install --locked cargo-insta cargo-nextest dotslash` | + +The Visual Studio installation includes the x64 tools and Windows SDK 26100; +ARM64 machines also receive the ARM64 tools. Setup waits for installation and +checks the actual compiler, linker, SDK headers/libraries, and native libclang +architecture. If an installer requires a restart, restart Windows and rerun the +script. Failures stop setup instead of reporting success. + +Setup does **not** build Codex, run tests, or install workspace JavaScript/Python +dependencies. Cargo may compile the three helper tools during their installation. +The script adds Cargo, LLVM, npm's global prefix when needed, and a user-owned +`bazel.cmd` wrapper to your user PATH. The wrapper lives in +`%LOCALAPPDATA%\Codex\dev-tools\bin` and invokes Bazelisk so `.bazelversion` is +honored. Git long-path support is enabled only for this checkout. Rust's global +default toolchain and persistent `CC`/`CXX` settings are not changed. + +Use the same PowerShell session for development. If you launched setup through +`powershell.exe -File`, open PowerShell 7 afterward. In a new session, activate +and verify the installed environment without installing or changing saved +settings: + +```powershell +& .\codex-rs\scripts\setup-windows.ps1 -CheckOnly +``` + +`-CheckOnly` does not download Rust or Bazel. It checks the Bazelisk executable +and wrapper without launching Bazel. The MSVC environment, `LIBCLANG_PATH`, and +native Rust toolchain are selected for the current process only. + +Build and test explicitly when ready: + +```powershell +Set-Location .\codex-rs +cargo build -p codex-cli +cargo run --bin codex -- "explain this codebase to me" +just test -p codex-tui +``` + +Native voice/Cygwin build inputs have their own CI setup in +`.github/scripts/setup-voice-windows.ps1`; they are not installed by the standard +CLI development setup. + +#### macOS, Linux, and WSL2 + ```bash # Clone the repository and navigate to the root of the Cargo workspace. git clone https://github.com/openai/codex.git From 27e0682222b89fb73020b1fd31af83faa7a7e111 Mon Sep 17 00:00:00 2001 From: Danger Mouse <35347349+DangerMouseUK@users.noreply.github.com> Date: Thu, 1 Oct 2026 20:46:02 +0100 Subject: [PATCH 2/8] Set the Windows CI shell matrix through job defaults --- .github/workflows/repo-checks.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/repo-checks.yml b/.github/workflows/repo-checks.yml index d6f712b4f8be..f36948c20fa2 100644 --- a/.github/workflows/repo-checks.yml +++ b/.github/workflows/repo-checks.yml @@ -14,6 +14,9 @@ jobs: fail-fast: false matrix: shell: [powershell, pwsh] + defaults: + run: + shell: ${{ matrix.shell }} env: RUSTUP_AUTO_INSTALL: "0" steps: @@ -32,7 +35,6 @@ jobs: run: Save-Module -Name Pester -RequiredVersion 5.7.1 -Repository PSGallery -Path "$env:RUNNER_TEMP/windows-setup-tests" -Force - name: Test Windows setup with mocked installers - shell: ${{ matrix.shell }} run: | Import-Module "$env:RUNNER_TEMP/windows-setup-tests/Pester/5.7.1/Pester.psd1" -Force $result = Invoke-Pester -Path ./codex-rs/scripts/setup-windows.Tests.ps1 -CI -PassThru From 3ac71e241333c81220be8811abec292d154473f9 Mon Sep 17 00:00:00 2001 From: Danger Mouse <35347349+DangerMouseUK@users.noreply.github.com> Date: Thu, 1 Oct 2026 20:48:40 +0100 Subject: [PATCH 3/8] Keep Pester CI results out of the repository worktree --- .github/workflows/repo-checks.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/repo-checks.yml b/.github/workflows/repo-checks.yml index f36948c20fa2..5fde6ff95eea 100644 --- a/.github/workflows/repo-checks.yml +++ b/.github/workflows/repo-checks.yml @@ -37,8 +37,8 @@ jobs: - name: Test Windows setup with mocked installers run: | Import-Module "$env:RUNNER_TEMP/windows-setup-tests/Pester/5.7.1/Pester.psd1" -Force - $result = Invoke-Pester -Path ./codex-rs/scripts/setup-windows.Tests.ps1 -CI -PassThru - if ($result.PassedCount -eq 0) { throw 'No Windows setup tests ran.' } + $result = Invoke-Pester -Path ./codex-rs/scripts/setup-windows.Tests.ps1 -PassThru -Output Normal + if ($result.FailedCount -ne 0 -or $result.PassedCount -eq 0) { throw 'Windows setup tests failed or did not run.' } - name: Check for a clean worktree if: always() && !cancelled() From 03f3acb027e14d565d8205f069de60bb2f8b182c Mon Sep 17 00:00:00 2001 From: Danger Mouse <35347349+DangerMouseUK@users.noreply.github.com> Date: Wed, 7 Oct 2026 18:13:15 +0100 Subject: [PATCH 4/8] Add independent hosted CI for Codex forks --- .codespellignore | 2 + .github/scripts/check_ci_results.py | 8 + .github/scripts/fork_ci_changes.py | 50 +++++ .github/scripts/test_check_ci_results.py | 43 ++++ .github/scripts/test_fork_ci_changes.py | 36 +++ .../verify_cargo_workspace_manifests.py | 3 +- .github/workflows/README.md | 33 +++ .github/workflows/bazel.yml | 8 +- .github/workflows/blob-size-policy.yml | 6 + .github/workflows/blocking-ci.yml | 9 +- .github/workflows/fork-ci.yml | 211 ++++++++++++++++++ .github/workflows/postmerge-ci.yml | 4 +- .github/workflows/repo-checks.yml | 14 ++ .github/workflows/rust-ci-full.yml | 12 +- .github/workflows/rust-ci.yml | 10 +- .github/workflows/rust-release.yml | 1 + .github/workflows/rusty-v8-release.yml | 1 + .github/workflows/sdk.yml | 52 ++++- .github/workflows/v8-canary.yml | 1 + 19 files changed, 483 insertions(+), 21 deletions(-) create mode 100644 .github/scripts/fork_ci_changes.py create mode 100644 .github/scripts/test_check_ci_results.py create mode 100644 .github/scripts/test_fork_ci_changes.py create mode 100644 .github/workflows/fork-ci.yml diff --git a/.codespellignore b/.codespellignore index 4f3c8c3513b5..f6c0a5de3e17 100644 --- a/.codespellignore +++ b/.codespellignore @@ -1,7 +1,9 @@ iTerm iTerm2 numer +oint psuedo SOM te TE +WRONLY diff --git a/.github/scripts/check_ci_results.py b/.github/scripts/check_ci_results.py index 36d1eed3b4ee..8d2a2e96ed5e 100644 --- a/.github/scripts/check_ci_results.py +++ b/.github/scripts/check_ci_results.py @@ -7,18 +7,26 @@ for a required fan-in job, only an explicit success is safe to accept. """ +import argparse import json import os def main() -> None: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--allow-skipped", action="append", default=[]) + args = parser.parse_args() # Keep result policy in one script so blocking-ci and postmerge-ci cannot # drift in how they interpret dependency conclusions. needs = json.loads(os.environ["NEEDS"]) + allowed_skips = set(args.allow_skipped) + if unknown := allowed_skips - needs.keys(): + raise SystemExit(f"Unknown optional CI dependencies: {sorted(unknown)}") failures = sorted( (name, dependency["result"]) for name, dependency in needs.items() if dependency["result"] != "success" + and not (name in allowed_skips and dependency["result"] == "skipped") ) if failures: diff --git a/.github/scripts/fork_ci_changes.py b/.github/scripts/fork_ci_changes.py new file mode 100644 index 000000000000..8c33d2e01342 --- /dev/null +++ b/.github/scripts/fork_ci_changes.py @@ -0,0 +1,50 @@ +#!/usr/bin/env python3 +"""Select hosted fork CI coverage without requiring Rust or repository secrets.""" + +import argparse +import subprocess +from pathlib import PurePosixPath + + +def coverage(paths: list[str], *, full: bool = False) -> dict[str, bool]: + rust = full or any( + path.startswith("codex-rs/") + and ( + PurePosixPath(path).suffix == ".rs" + or PurePosixPath(path).name in {"Cargo.toml", "Cargo.lock", "rust-toolchain.toml"} + or path.startswith("codex-rs/.cargo/") + or path.startswith("codex-rs/.config/") + ) + for path in paths + ) + sdk = rust or any( + path.startswith("sdk/") + or path in {"package.json", "pnpm-lock.yaml", "pnpm-workspace.yaml"} + or path == ".github/workflows/sdk.yml" + for path in paths + ) + return {"rust_full": rust, "sdk": sdk} + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--base") + parser.add_argument("--head", default="HEAD") + parser.add_argument("--full", action="store_true") + args = parser.parse_args() + if args.full: + paths = [] + elif not args.base or set(args.base) == {"0"}: + # Initial pushes and manual runs have no usable comparison range. + args.full = True + paths = [] + else: + paths = subprocess.check_output( + ["git", "diff", "--name-only", "--no-renames", "-z", args.base, args.head] + ).decode("utf-8").split("\0") + for key, value in coverage(paths, full=args.full).items(): + print(f"{key}={str(value).lower()}") + + +if __name__ == "__main__": + main() diff --git a/.github/scripts/test_check_ci_results.py b/.github/scripts/test_check_ci_results.py new file mode 100644 index 000000000000..71850abd1b1d --- /dev/null +++ b/.github/scripts/test_check_ci_results.py @@ -0,0 +1,43 @@ +import json +import os +from pathlib import Path +import subprocess +import sys +import unittest + + +class CiResultsTests(unittest.TestCase): + def check(self, needs, *args): + return subprocess.run( + [sys.executable, str(Path(__file__).with_name("check_ci_results.py")), *args], + env={**os.environ, "NEEDS": json.dumps(needs)}, + capture_output=True, + text=True, + ).returncode + + def test_success(self): + self.assertEqual(self.check({"tests": {"result": "success"}}), 0) + + def test_required_checks_must_succeed(self): + for result in ("failure", "cancelled", "skipped"): + with self.subTest(result=result): + self.assertNotEqual(self.check({"tests": {"result": result}}), 0) + + def test_only_explicit_optional_skips_are_allowed(self): + self.assertEqual( + self.check({"sdk": {"result": "skipped"}}, "--allow-skipped", "sdk"), 0 + ) + self.assertNotEqual( + self.check({"tests": {"result": "skipped"}}, "--allow-skipped", "sdk"), 0 + ) + + def test_optional_failures_and_cancellations_still_fail(self): + for result in ("failure", "cancelled"): + with self.subTest(result=result): + self.assertNotEqual( + self.check({"sdk": {"result": result}}, "--allow-skipped", "sdk"), 0 + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/.github/scripts/test_fork_ci_changes.py b/.github/scripts/test_fork_ci_changes.py new file mode 100644 index 000000000000..053b5edf3722 --- /dev/null +++ b/.github/scripts/test_fork_ci_changes.py @@ -0,0 +1,36 @@ +import unittest + +from fork_ci_changes import coverage + + +class ForkCoverageTests(unittest.TestCase): + def test_setup_and_docs_keep_native_smoke_coverage(self): + self.assertEqual( + coverage(["codex-rs/scripts/setup-windows.ps1", "docs/install.md"]), + {"rust_full": False, "sdk": False}, + ) + + def test_rust_sources_and_build_configuration_require_full_coverage(self): + for path in ( + "codex-rs/core/src/lib.rs", + "codex-rs/utils/pty/tests/test.rs", + "codex-rs/Cargo.lock", + "codex-rs/core/Cargo.toml", + "codex-rs/rust-toolchain.toml", + "codex-rs/.cargo/config.toml", + "codex-rs/.config/nextest.toml", + ): + with self.subTest(path=path): + self.assertEqual(coverage([path]), {"rust_full": True, "sdk": True}) + + def test_sdk_and_dependency_changes_require_sdk_coverage(self): + for path in ("sdk/python/src/example.py", "sdk/typescript/src/index.ts", "pnpm-lock.yaml"): + with self.subTest(path=path): + self.assertEqual(coverage([path]), {"rust_full": False, "sdk": True}) + + def test_manual_full_run_cannot_skip_expensive_checks(self): + self.assertEqual(coverage([], full=True), {"rust_full": True, "sdk": True}) + + +if __name__ == "__main__": + unittest.main() diff --git a/.github/scripts/verify_cargo_workspace_manifests.py b/.github/scripts/verify_cargo_workspace_manifests.py index 645c6c59e5c9..74cb2035ae09 100644 --- a/.github/scripts/verify_cargo_workspace_manifests.py +++ b/.github/scripts/verify_cargo_workspace_manifests.py @@ -26,7 +26,6 @@ "path-utils": "codex-utils-path", } MANIFEST_FEATURE_EXCEPTIONS = { - "codex-rs/code-mode/Cargo.toml": {"sandbox": ("v8/v8_enable_sandbox",)}, "codex-rs/v8-poc/Cargo.toml": {"sandbox": ("v8/v8_enable_sandbox",)}, } OPTIONAL_DEPENDENCY_EXCEPTIONS = set() @@ -227,7 +226,7 @@ def is_workspace_reference(value: object) -> bool: def manifest_key(path: Path) -> str: - return str(path.relative_to(ROOT)) + return path.relative_to(ROOT).as_posix() def normalize_feature_mapping(value: object) -> dict[str, tuple[str, ...]] | None: diff --git a/.github/workflows/README.md b/.github/workflows/README.md index 70445ab5936c..65801d550342 100644 --- a/.github/workflows/README.md +++ b/.github/workflows/README.md @@ -1,5 +1,38 @@ # Workflow Strategy +## Forks + +`fork-ci.yml` is the entrypoint for fork pull requests and pushes to `main`. +It uses standard GitHub-hosted Ubuntu, Windows 2025 and macOS 15 runners with +read-only repository permissions. It needs no OpenAI secrets, runner groups, +BuildBuddy account, protected environments or paid larger runners. + +- Every run checks repository policies, formatting, unused dependencies, + spelling and dependency advisories, and runs the Windows setup tests under + Windows PowerShell 5.1 and PowerShell 7 with mocked installers. +- Native Cargo clippy and nextest always cover `codex-shell-command`, + `codex-utils-pty` and `codex-utils-path` on all three operating systems. This + includes native MSVC Windows builds and PowerShell-related shell handling. +- Rust sources, Cargo manifests/lockfiles, toolchain or Cargo/nextest configuration + changes expand native checks to the full Rust workspace and enable SDK tests. + SDK sources, JavaScript dependency files and `sdk.yml` changes enable SDK tests + against a Cargo-built CLI on hosted Ubuntu. +- The **Run workflow** button defaults to full native Rust and SDK coverage. + Clear **full** to repeat the faster smoke checks. Cargo caches are isolated by + operating system, architecture, toolchain, dependency lock and coverage scope. +- Require **Fork CI required** in the fork's branch rules. It fails on failed, + cancelled or unexpectedly skipped dependencies. SDK checks may be skipped only + when the change detector says they are unnecessary. + +The native fork suite does not replace OpenAI's Bazel, source-built V8, +cross-compilation, release/signing or custom argument-comment-lint coverage. +Those workflows are gated to `openai/codex`; they remain available there with +their original infrastructure. Fork CI does not publish releases or deploy. +Cargo shear reports informational warnings about unlinked source files without +failing; unused dependencies remain failures. + +## OpenAI Upstream + The workflows in this directory are split so that pull requests get fast, review-friendly signal while `main` still gets the full cross-platform verification pass. ## Pull Requests diff --git a/.github/workflows/bazel.yml b/.github/workflows/bazel.yml index ede5a7f07032..5de9623a783e 100644 --- a/.github/workflows/bazel.yml +++ b/.github/workflows/bazel.yml @@ -15,6 +15,7 @@ concurrency: jobs: test: + if: github.repository == 'openai/codex' # PRs use the sharded Windows cross-compiled test jobs below. Post-merge # pushes to main also run the native Windows test job for broader Windows # signal without putting PR latency back on the critical path. When @@ -130,6 +131,7 @@ jobs: uses: ./.github/actions/check-clean-worktree test-windows-shard: + if: github.repository == 'openai/codex' # Split the Windows Bazel test leg across separate Windows hosts. Jobs with # BuildBuddy credentials use Linux RBE for build actions; test execution # remains on a Windows runner. @@ -234,7 +236,7 @@ jobs: test-windows: # Preserve the existing required-check surface while the real work happens # in the sharded Windows jobs above. - if: always() + if: ${{ github.repository == 'openai/codex' && (always()) }} needs: test-windows-shard runs-on: ubuntu-24.04 name: Bazel test on windows-latest for x86_64-pc-windows-gnullvm @@ -252,7 +254,7 @@ jobs: # Native Windows Bazel tests are slower and frequently approach the # 30-minute PR budget. Run this only for post-merge commits to main and give # it a larger timeout. - if: github.event_name == 'push' && github.ref == 'refs/heads/main' + if: ${{ github.repository == 'openai/codex' && (github.event_name == 'push' && github.ref == 'refs/heads/main') }} timeout-minutes: 40 runs-on: group: ${{ github.event.repository.name }}-runners @@ -330,6 +332,7 @@ jobs: uses: ./.github/actions/check-clean-worktree clippy: + if: github.repository == 'openai/codex' timeout-minutes: 30 strategy: fail-fast: false @@ -433,6 +436,7 @@ jobs: uses: ./.github/actions/check-clean-worktree verify-release-build: + if: github.repository == 'openai/codex' timeout-minutes: 30 strategy: fail-fast: false diff --git a/.github/workflows/blob-size-policy.yml b/.github/workflows/blob-size-policy.yml index f80fb0e4b30f..075a3d69f1d7 100644 --- a/.github/workflows/blob-size-policy.yml +++ b/.github/workflows/blob-size-policy.yml @@ -29,10 +29,16 @@ jobs: if [[ "${{ github.event_name }}" == "pull_request" ]]; then base='${{ github.event.pull_request.base.sha }}' head='${{ github.event.pull_request.head.sha }}' + elif [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then + base="$(git rev-parse HEAD^)" + head='${{ github.sha }}' else base='${{ github.event.before }}' head='${{ github.sha }}' fi + if [[ "$base" =~ ^0+$ ]]; then + base="$(git hash-object -t tree /dev/null)" + fi echo "base=$base" >> "$GITHUB_OUTPUT" echo "head=$head" >> "$GITHUB_OUTPUT" diff --git a/.github/workflows/blocking-ci.yml b/.github/workflows/blocking-ci.yml index c6f00555a355..c101d02893da 100644 --- a/.github/workflows/blocking-ci.yml +++ b/.github/workflows/blocking-ci.yml @@ -11,36 +11,43 @@ jobs: # Keep reusable workflow calls alphabetized. The `required` job below is the # version-controlled list that the main-branch ruleset should require. bazel: + if: github.repository == 'openai/codex' name: Bazel uses: ./.github/workflows/bazel.yml secrets: inherit blob-size-policy: + if: github.repository == 'openai/codex' name: Blob size policy uses: ./.github/workflows/blob-size-policy.yml secrets: inherit cargo-deny: + if: github.repository == 'openai/codex' name: cargo-deny uses: ./.github/workflows/cargo-deny.yml secrets: inherit codespell: + if: github.repository == 'openai/codex' name: Codespell uses: ./.github/workflows/codespell.yml secrets: inherit repo-checks: + if: github.repository == 'openai/codex' name: repo-checks uses: ./.github/workflows/repo-checks.yml secrets: inherit rust-ci: + if: github.repository == 'openai/codex' name: rust-ci uses: ./.github/workflows/rust-ci.yml secrets: inherit sdk: + if: github.repository == 'openai/codex' name: sdk uses: ./.github/workflows/sdk.yml secrets: inherit @@ -49,7 +56,7 @@ jobs: name: CI required # Without `always()`, GitHub skips this job after a failed dependency and a # required check can appear successful instead of reporting the failure. - if: ${{ always() }} + if: ${{ github.repository == 'openai/codex' && (always()) }} needs: - bazel - blob-size-policy diff --git a/.github/workflows/fork-ci.yml b/.github/workflows/fork-ci.yml new file mode 100644 index 000000000000..99e0a821c9fd --- /dev/null +++ b/.github/workflows/fork-ci.yml @@ -0,0 +1,211 @@ +name: fork-ci + +on: + pull_request: {} + push: + branches: [main] + workflow_dispatch: + inputs: + full: + description: Run the full native Rust workspace and SDK suites + type: boolean + default: true + +permissions: + contents: read + +concurrency: + group: fork-ci-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} + +jobs: + changes: + if: github.repository != 'openai/codex' + runs-on: ubuntu-24.04 + timeout-minutes: 5 + outputs: + rust_full: ${{ steps.coverage.outputs.rust_full }} + sdk: ${{ steps.coverage.outputs.sdk }} + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 0 + persist-credentials: false + - name: Select coverage + id: coverage + shell: bash + env: + BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.before }} + HEAD_SHA: ${{ github.event.pull_request.head.sha || github.sha }} + EVENT_NAME: ${{ github.event_name }} + FULL: ${{ inputs.full || false }} + run: | + set -euo pipefail + args=(--base "$BASE_SHA" --head "$HEAD_SHA") + if [[ "$EVENT_NAME" == workflow_dispatch ]]; then + args=(--base HEAD --head HEAD) + fi + if [[ "$FULL" == true ]]; then + args+=(--full) + fi + python3 .github/scripts/fork_ci_changes.py "${args[@]}" | tee -a "$GITHUB_OUTPUT" + + blob-size-policy: + if: github.repository != 'openai/codex' + uses: ./.github/workflows/blob-size-policy.yml + + cargo-deny: + if: github.repository != 'openai/codex' + uses: ./.github/workflows/cargo-deny.yml + + codespell: + if: github.repository != 'openai/codex' + uses: ./.github/workflows/codespell.yml + + repo-checks: + if: github.repository != 'openai/codex' + uses: ./.github/workflows/repo-checks.yml + + rust-quality: + if: github.repository != 'openai/codex' + runs-on: ubuntu-24.04 + timeout-minutes: 15 + defaults: + run: + working-directory: codex-rs + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + - uses: dtolnay/rust-toolchain@e081816240890017053eacbb1bdf337761dc5582 # 1.95.0 + with: + components: rustfmt + - uses: taiki-e/install-action@4cef1412cce204788f482e778a0b9187f9626a29 # v2.87.21 + with: + tool: cargo-shear@1.11.2 + - name: Check Rust formatting + run: cargo fmt -- --config imports_granularity=Item --check + # Unlinked source warnings are informational; unused dependencies still fail. + - name: Check for unused dependencies + run: cargo shear + - name: Check for a clean worktree + if: always() && !cancelled() + uses: ./.github/actions/check-clean-worktree + + native-rust: + name: Native Rust (${{ matrix.os }}) + needs: changes + runs-on: ${{ matrix.os }} + timeout-minutes: 120 + strategy: + fail-fast: false + matrix: + os: [ubuntu-24.04, windows-2025, macos-15] + defaults: + run: + shell: pwsh + working-directory: codex-rs + env: + CARGO_TARGET_DIR: ${{ runner.temp }}/cargo-target + CARGO_INCREMENTAL: "0" + CARGO_PROFILE_DEV_DEBUG: "0" + CARGO_PROFILE_TEST_DEBUG: "0" + CARGO_NET_GIT_FETCH_WITH_CLI: "true" + RUST_FULL: ${{ needs.changes.outputs.rust_full }} + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + - name: Install Linux build dependencies + if: runner.os == 'Linux' + shell: bash + run: | + sudo apt-get update -y + sudo apt-get install -y --no-install-recommends build-essential pkg-config libcap-dev libasound2-dev bubblewrap + sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0 + - name: Prepare full workspace runtime tools + if: needs.changes.outputs.rust_full == 'true' + uses: ./.github/actions/setup-ci + - uses: dtolnay/rust-toolchain@e081816240890017053eacbb1bdf337761dc5582 # 1.95.0 + with: + components: clippy + - uses: taiki-e/install-action@4cef1412cce204788f482e778a0b9187f9626a29 # v2.87.21 + with: + tool: cargo-nextest@0.9.103 + - name: Cache Cargo downloads and native build outputs + uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 + with: + path: | + ~/.cargo/registry/index + ~/.cargo/registry/cache + ~/.cargo/git/db + ${{ env.CARGO_TARGET_DIR }} + key: fork-cargo-${{ runner.os }}-${{ runner.arch }}-${{ needs.changes.outputs.rust_full }}-${{ hashFiles('codex-rs/rust-toolchain.toml', 'codex-rs/Cargo.lock') }}-${{ github.sha }} + restore-keys: | + fork-cargo-${{ runner.os }}-${{ runner.arch }}-${{ needs.changes.outputs.rust_full }}-${{ hashFiles('codex-rs/rust-toolchain.toml', 'codex-rs/Cargo.lock') }}- + fork-cargo-${{ runner.os }}-${{ runner.arch }}-${{ needs.changes.outputs.rust_full }}- + - name: Lint and test native Rust + run: | + $ErrorActionPreference = 'Stop' + if ($env:RUST_FULL -eq 'true') { + $scope = @('--workspace') + cargo build --locked --workspace --bins --timings + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + } else { + $scope = @('-p', 'codex-shell-command', '-p', 'codex-utils-pty', '-p', 'codex-utils-path') + } + Write-Host "Cargo scope: $scope" + cargo clippy --locked @scope --all-targets --timings -- -D warnings + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + cargo nextest run --locked @scope --no-fail-fast --timings + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + - name: Upload test results and Cargo timings + if: always() && !cancelled() + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + with: + name: fork-native-${{ matrix.os }} + path: | + ${{ env.CARGO_TARGET_DIR }}/nextest/default/junit.xml + ${{ env.CARGO_TARGET_DIR }}/cargo-timings/*.html + if-no-files-found: ignore + - name: Check for a clean worktree + if: always() && !cancelled() + uses: ./.github/actions/check-clean-worktree + + sdk: + needs: changes + if: needs.changes.outputs.sdk == 'true' + uses: ./.github/workflows/sdk.yml + + required: + name: Fork CI required + if: always() && github.repository != 'openai/codex' + needs: + [ + changes, + blob-size-policy, + cargo-deny, + codespell, + repo-checks, + rust-quality, + native-rust, + sdk, + ] + runs-on: ubuntu-24.04 + timeout-minutes: 5 + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + - name: Require successful checks + shell: bash + env: + NEEDS: ${{ toJSON(needs) }} + SDK_REQUIRED: ${{ needs.changes.outputs.sdk }} + run: | + set -euo pipefail + args=() + if [[ "$SDK_REQUIRED" == false ]]; then + args+=(--allow-skipped sdk) + fi + python3 .github/scripts/check_ci_results.py "${args[@]}" diff --git a/.github/workflows/postmerge-ci.yml b/.github/workflows/postmerge-ci.yml index ac2b46d1db4c..4dd09a847584 100644 --- a/.github/workflows/postmerge-ci.yml +++ b/.github/workflows/postmerge-ci.yml @@ -10,11 +10,13 @@ jobs: # Keep reusable workflow calls alphabetized. Each child retains its own # workflow_dispatch trigger so maintainers can rerun flaky suites directly. rust-ci-full: + if: github.repository == 'openai/codex' name: rust-ci-full uses: ./.github/workflows/rust-ci-full.yml secrets: inherit v8-canary: + if: github.repository == 'openai/codex' name: v8-canary uses: ./.github/workflows/v8-canary.yml secrets: inherit @@ -24,7 +26,7 @@ jobs: needs: - rust-ci-full - v8-canary - if: ${{ always() }} + if: ${{ github.repository == 'openai/codex' && (always()) }} runs-on: ubuntu-24.04 steps: # Postmerge runs use the pushed main commit, so this helper always comes diff --git a/.github/workflows/repo-checks.yml b/.github/workflows/repo-checks.yml index 5fde6ff95eea..d78d911730d5 100644 --- a/.github/workflows/repo-checks.yml +++ b/.github/workflows/repo-checks.yml @@ -57,6 +57,20 @@ jobs: - uses: ./.github/actions/setup-ci + - name: Test fork coverage selection and CI result policy + run: | + python3 -m unittest discover -s .github/scripts -p 'test_fork_ci_changes.py' + python3 -m unittest discover -s .github/scripts -p 'test_check_ci_results.py' + + - name: Validate hosted fork workflow configuration + if: github.repository != 'openai/codex' + run: >- + go run github.com/rhysd/actionlint/cmd/actionlint@v1.7.12 + -shellcheck= -pyflakes= + .github/workflows/fork-ci.yml .github/workflows/blocking-ci.yml + .github/workflows/postmerge-ci.yml .github/workflows/repo-checks.yml + .github/workflows/sdk.yml + - name: Verify codex-rs Cargo manifests inherit workspace settings run: python3 .github/scripts/verify_cargo_workspace_manifests.py diff --git a/.github/workflows/rust-ci-full.yml b/.github/workflows/rust-ci-full.yml index 4a61b6b12ddc..ec62bed6f481 100644 --- a/.github/workflows/rust-ci-full.yml +++ b/.github/workflows/rust-ci-full.yml @@ -11,6 +11,7 @@ on: jobs: # --- CI that doesn't need specific targets --------------------------------- general: + if: github.repository == 'openai/codex' name: Format / etc runs-on: ubuntu-24.04 defaults: @@ -30,6 +31,7 @@ jobs: run: just bench-smoke cargo_shear: + if: github.repository == 'openai/codex' name: cargo shear runs-on: ubuntu-24.04 defaults: @@ -48,6 +50,7 @@ jobs: run: cargo shear --deny-warnings argument_comment_lint_package: + if: github.repository == 'openai/codex' name: Argument comment lint package runs-on: ubuntu-24.04 env: @@ -90,6 +93,7 @@ jobs: RUST_MIN_STACK: "8388608" # 8 MiB argument_comment_lint_prebuilt: + if: github.repository == 'openai/codex' name: Argument comment lint - ${{ matrix.name }} runs-on: ${{ matrix.runs_on || matrix.runner }} timeout-minutes: 30 @@ -151,6 +155,7 @@ jobs: # --- CI to validate on different os/targets -------------------------------- lint_build: + if: github.repository == 'openai/codex' name: Lint/Build — ${{ matrix.runner }} - ${{ matrix.target }}${{ matrix.profile == 'release' && ' (release)' || '' }} runs-on: ${{ matrix.runs_on || matrix.runner }} timeout-minutes: 30 @@ -458,6 +463,7 @@ jobs: key: apt-${{ matrix.runner }}-${{ matrix.target }}-v1 tests_macos_aarch64: + if: github.repository == 'openai/codex' name: Tests — macos-15-xlarge - aarch64-apple-darwin uses: ./.github/workflows/rust-ci-full-nextest-platform.yml with: @@ -469,6 +475,7 @@ jobs: secrets: inherit tests_linux_x64_remote: + if: github.repository == 'openai/codex' name: Tests — ubuntu-24.04 - x86_64-unknown-linux-gnu (remote) uses: ./.github/workflows/rust-ci-full-nextest-platform.yml with: @@ -483,6 +490,7 @@ jobs: secrets: inherit tests_linux_arm64: + if: github.repository == 'openai/codex' name: Tests — ubuntu-24.04-arm - aarch64-unknown-linux-gnu uses: ./.github/workflows/rust-ci-full-nextest-platform.yml with: @@ -496,6 +504,7 @@ jobs: secrets: inherit tests_windows_x64: + if: github.repository == 'openai/codex' name: Tests — windows-x64 - x86_64-pc-windows-msvc uses: ./.github/workflows/rust-ci-full-nextest-platform.yml with: @@ -509,6 +518,7 @@ jobs: secrets: inherit tests_windows_arm64: + if: github.repository == 'openai/codex' name: Tests — windows-arm64 - aarch64-pc-windows-msvc uses: ./.github/workflows/rust-ci-full-nextest-platform.yml with: @@ -541,7 +551,7 @@ jobs: tests_windows_x64, tests_windows_arm64, ] - if: always() + if: ${{ github.repository == 'openai/codex' && (always()) }} runs-on: ubuntu-24.04 steps: - name: Summarize diff --git a/.github/workflows/rust-ci.yml b/.github/workflows/rust-ci.yml index 48bee976c942..989a59fbeb1a 100644 --- a/.github/workflows/rust-ci.yml +++ b/.github/workflows/rust-ci.yml @@ -6,6 +6,7 @@ on: jobs: # --- Detect changed paths so the fast PR workflow only runs relevant jobs --- changed: + if: github.repository == 'openai/codex' name: Detect changed areas runs-on: ubuntu-24.04 outputs: @@ -61,7 +62,7 @@ jobs: name: Format / etc runs-on: ubuntu-24.04 needs: changed - if: ${{ needs.changed.outputs.codex == 'true' || needs.changed.outputs.workflows == 'true' }} + if: ${{ github.repository == 'openai/codex' && (needs.changed.outputs.codex == 'true' || needs.changed.outputs.workflows == 'true') }} defaults: run: working-directory: codex-rs @@ -86,7 +87,7 @@ jobs: name: cargo shear runs-on: ubuntu-24.04 needs: changed - if: ${{ needs.changed.outputs.codex == 'true' || needs.changed.outputs.workflows == 'true' }} + if: ${{ github.repository == 'openai/codex' && (needs.changed.outputs.codex == 'true' || needs.changed.outputs.workflows == 'true') }} defaults: run: working-directory: codex-rs @@ -110,7 +111,7 @@ jobs: name: Argument comment lint package runs-on: ubuntu-24.04 needs: changed - if: ${{ needs.changed.outputs.argument_comment_lint_package == 'true' }} + if: ${{ github.repository == 'openai/codex' && (needs.changed.outputs.argument_comment_lint_package == 'true') }} env: CARGO_DYLINT_VERSION: 6.1.0 DYLINT_LINK_VERSION: 6.1.0 @@ -162,6 +163,7 @@ jobs: uses: ./.github/actions/check-clean-worktree argument_comment_lint_prebuilt: + if: github.repository == 'openai/codex' name: Argument comment lint - ${{ matrix.name }} runs-on: ${{ matrix.runs_on || matrix.runner }} timeout-minutes: ${{ matrix.timeout_minutes }} @@ -226,7 +228,7 @@ jobs: argument_comment_lint_package, argument_comment_lint_prebuilt, ] - if: always() + if: ${{ github.repository == 'openai/codex' && (always()) }} runs-on: ubuntu-24.04 steps: - name: Summarize diff --git a/.github/workflows/rust-release.yml b/.github/workflows/rust-release.yml index f06bff1dc34c..5c11cc16d5d4 100644 --- a/.github/workflows/rust-release.yml +++ b/.github/workflows/rust-release.yml @@ -27,6 +27,7 @@ concurrency: jobs: tag-check: + if: github.repository == 'openai/codex' runs-on: ubuntu-latest steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 diff --git a/.github/workflows/rusty-v8-release.yml b/.github/workflows/rusty-v8-release.yml index 32ec3af83663..ef1d36fd71a9 100644 --- a/.github/workflows/rusty-v8-release.yml +++ b/.github/workflows/rusty-v8-release.yml @@ -16,6 +16,7 @@ concurrency: jobs: metadata: + if: github.repository == 'openai/codex' runs-on: ubuntu-latest outputs: release_tag: ${{ steps.release_tag.outputs.release_tag }} diff --git a/.github/workflows/sdk.yml b/.github/workflows/sdk.yml index 3f91dd18fa8e..ec85195be916 100644 --- a/.github/workflows/sdk.yml +++ b/.github/workflows/sdk.yml @@ -5,9 +5,7 @@ on: jobs: python-sdk-installation: - runs-on: - group: ${{ github.event.repository.name }}-runners - labels: ${{ github.event.repository.name }}-linux-x64 + runs-on: ${{ github.repository == 'openai/codex' && fromJSON('{"group":"codex-runners","labels":"codex-linux-x64"}') || 'ubuntu-24.04' }} timeout-minutes: 10 steps: - name: Checkout repository @@ -36,10 +34,8 @@ jobs: ' sdks: - runs-on: - group: ${{ github.event.repository.name }}-runners - labels: ${{ github.event.repository.name }}-linux-x64 - timeout-minutes: 20 + runs-on: ${{ github.repository == 'openai/codex' && fromJSON('{"group":"codex-runners","labels":"codex-linux-x64"}') || 'ubuntu-24.04' }} + timeout-minutes: ${{ github.repository == 'openai/codex' && 20 || 120 }} environment: name: bazel deployment: false @@ -54,7 +50,7 @@ jobs: run: | set -euo pipefail sudo apt-get update -y - sudo DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends pkg-config libcap-dev + sudo DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends pkg-config libcap-dev libasound2-dev - name: Setup pnpm uses: pnpm/action-setup@a8198c4bff370c8506180b035930dea56dbd5288 # v5 @@ -68,6 +64,7 @@ jobs: cache: pnpm - name: Prepare Bazel CI + if: github.repository == 'openai/codex' id: setup_bazel uses: ./.github/actions/prepare-bazel-ci with: @@ -75,6 +72,7 @@ jobs: cache-scope: sdk - name: Build codex and the code-mode host with Bazel + if: github.repository == 'openai/codex' id: build_bazel env: BUILDBUDDY_API_KEY: ${{ secrets.BUILDBUDDY_API_KEY }} @@ -117,7 +115,41 @@ jobs: install -m 755 "${bazel_output_paths[@]}" "${install_dir}" echo "CODEX_EXEC_PATH=${install_dir}/codex" >> "$GITHUB_ENV" - - name: Warm up Bazel-built codex + - name: Install Rust for hosted fork SDK tests + if: github.repository != 'openai/codex' + uses: dtolnay/rust-toolchain@e081816240890017053eacbb1bdf337761dc5582 # 1.95.0 + + - name: Prepare hosted fork runtime tools + if: github.repository != 'openai/codex' + uses: ./.github/actions/setup-ci + + - name: Cache hosted fork Cargo builds + if: github.repository != 'openai/codex' + uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 + with: + path: | + ~/.cargo/registry/index + ~/.cargo/registry/cache + ~/.cargo/git/db + ${{ runner.temp }}/sdk-cargo-target + key: fork-sdk-cargo-${{ hashFiles('codex-rs/Cargo.lock', 'codex-rs/rust-toolchain.toml') }}-${{ github.sha }} + restore-keys: | + fork-sdk-cargo-${{ hashFiles('codex-rs/Cargo.lock', 'codex-rs/rust-toolchain.toml') }}- + + - name: Build the CLI for hosted fork SDK tests + if: github.repository != 'openai/codex' + working-directory: codex-rs + env: + CARGO_TARGET_DIR: ${{ runner.temp }}/sdk-cargo-target + CARGO_PROFILE_DEV_DEBUG: "0" + CARGO_INCREMENTAL: "0" + CARGO_NET_GIT_FETCH_WITH_CLI: "true" + run: | + set -euo pipefail + cargo build --locked -p codex-cli -p codex-code-mode-host --bins + echo "CODEX_EXEC_PATH=${CARGO_TARGET_DIR}/debug/codex" >> "$GITHUB_ENV" + + - name: Warm up the CLI shell: bash run: | set -euo pipefail @@ -149,7 +181,7 @@ jobs: uv run --only-group dev --frozen --no-sync pytest - name: Save bazel repository cache - if: always() && !cancelled() && steps.build_bazel.outcome == 'success' && steps.setup_bazel.outputs.repository-cache-hit != 'true' + if: always() && !cancelled() && github.repository == 'openai/codex' && steps.build_bazel.outcome == 'success' && steps.setup_bazel.outputs.repository-cache-hit != 'true' continue-on-error: true uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 with: diff --git a/.github/workflows/v8-canary.yml b/.github/workflows/v8-canary.yml index 94f1aa0199f1..6565a4f3c168 100644 --- a/.github/workflows/v8-canary.yml +++ b/.github/workflows/v8-canary.yml @@ -15,6 +15,7 @@ concurrency: jobs: metadata: + if: github.repository == 'openai/codex' runs-on: ubuntu-latest outputs: # Older revisions can contain a detector that does not emit this output. From 55ffef6f22bccf26943091082696cc91898636c8 Mon Sep 17 00:00:00 2001 From: Danger Mouse <35347349+DangerMouseUK@users.noreply.github.com> Date: Wed, 7 Oct 2026 18:14:43 +0100 Subject: [PATCH 5/8] Initialize hosted Cargo paths in the runner context --- .github/workflows/fork-ci.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/fork-ci.yml b/.github/workflows/fork-ci.yml index 99e0a821c9fd..1cac51a10ee0 100644 --- a/.github/workflows/fork-ci.yml +++ b/.github/workflows/fork-ci.yml @@ -106,7 +106,6 @@ jobs: shell: pwsh working-directory: codex-rs env: - CARGO_TARGET_DIR: ${{ runner.temp }}/cargo-target CARGO_INCREMENTAL: "0" CARGO_PROFILE_DEV_DEBUG: "0" CARGO_PROFILE_TEST_DEBUG: "0" @@ -132,6 +131,11 @@ jobs: - uses: taiki-e/install-action@4cef1412cce204788f482e778a0b9187f9626a29 # v2.87.21 with: tool: cargo-nextest@0.9.103 + - name: Set the native Cargo output directory + run: | + if (-not $env:CARGO_TARGET_DIR) { + "CARGO_TARGET_DIR=$env:RUNNER_TEMP/cargo-target" >> $env:GITHUB_ENV + } - name: Cache Cargo downloads and native build outputs uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 with: From 63a5a6eb5789d5ffa402a5c4020007adb1384f64 Mon Sep 17 00:00:00 2001 From: Danger Mouse <35347349+DangerMouseUK@users.noreply.github.com> Date: Wed, 7 Oct 2026 18:25:01 +0100 Subject: [PATCH 6/8] Use verified V8 artifacts and refine fork CI diagnostics --- .github/scripts/check_ci_results.py | 4 ++-- .github/scripts/fork_ci_changes.py | 10 +++++++--- .github/scripts/test_check_ci_results.py | 9 ++++++--- .github/scripts/test_fork_ci_changes.py | 6 +++++- .github/workflows/fork-ci.yml | 13 ++++++++++++- .github/workflows/postmerge-ci.yml | 4 ++++ .github/workflows/repo-checks.yml | 6 +++++- .github/workflows/sdk.yml | 6 ++++++ 8 files changed, 47 insertions(+), 11 deletions(-) diff --git a/.github/scripts/check_ci_results.py b/.github/scripts/check_ci_results.py index 8d2a2e96ed5e..6c934e45b76b 100644 --- a/.github/scripts/check_ci_results.py +++ b/.github/scripts/check_ci_results.py @@ -3,8 +3,8 @@ """Fail a terminal CI job unless every serialized dependency succeeded. Parent workflows pass GitHub's `toJSON(needs)` object through the NEEDS -environment variable. Treat skipped and cancelled dependencies as failures too: -for a required fan-in job, only an explicit success is safe to accept. +environment variable. Only explicit success is accepted, except for skipped +dependencies named with --allow-skipped. Failures and cancellations always fail. """ import argparse diff --git a/.github/scripts/fork_ci_changes.py b/.github/scripts/fork_ci_changes.py index 8c33d2e01342..c9df44545f4b 100644 --- a/.github/scripts/fork_ci_changes.py +++ b/.github/scripts/fork_ci_changes.py @@ -39,9 +39,13 @@ def main() -> None: args.full = True paths = [] else: - paths = subprocess.check_output( - ["git", "diff", "--name-only", "--no-renames", "-z", args.base, args.head] - ).decode("utf-8").split("\0") + paths = ( + subprocess.check_output( + ["git", "diff", "--name-only", "--no-renames", "-z", args.base, args.head] + ) + .decode("utf-8") + .split("\0") + ) for key, value in coverage(paths, full=args.full).items(): print(f"{key}={str(value).lower()}") diff --git a/.github/scripts/test_check_ci_results.py b/.github/scripts/test_check_ci_results.py index 71850abd1b1d..6a0f3e682ea8 100644 --- a/.github/scripts/test_check_ci_results.py +++ b/.github/scripts/test_check_ci_results.py @@ -25,17 +25,20 @@ def test_required_checks_must_succeed(self): def test_only_explicit_optional_skips_are_allowed(self): self.assertEqual( - self.check({"sdk": {"result": "skipped"}}, "--allow-skipped", "sdk"), 0 + self.check({"sdk": {"result": "skipped"}}, "--allow-skipped", "sdk"), + 0, ) self.assertNotEqual( - self.check({"tests": {"result": "skipped"}}, "--allow-skipped", "sdk"), 0 + self.check({"tests": {"result": "skipped"}}, "--allow-skipped", "sdk"), + 0, ) def test_optional_failures_and_cancellations_still_fail(self): for result in ("failure", "cancelled"): with self.subTest(result=result): self.assertNotEqual( - self.check({"sdk": {"result": result}}, "--allow-skipped", "sdk"), 0 + self.check({"sdk": {"result": result}}, "--allow-skipped", "sdk"), + 0, ) diff --git a/.github/scripts/test_fork_ci_changes.py b/.github/scripts/test_fork_ci_changes.py index 053b5edf3722..57e7914b3cde 100644 --- a/.github/scripts/test_fork_ci_changes.py +++ b/.github/scripts/test_fork_ci_changes.py @@ -24,7 +24,11 @@ def test_rust_sources_and_build_configuration_require_full_coverage(self): self.assertEqual(coverage([path]), {"rust_full": True, "sdk": True}) def test_sdk_and_dependency_changes_require_sdk_coverage(self): - for path in ("sdk/python/src/example.py", "sdk/typescript/src/index.ts", "pnpm-lock.yaml"): + for path in ( + "sdk/python/src/example.py", + "sdk/typescript/src/index.ts", + "pnpm-lock.yaml", + ): with self.subTest(path=path): self.assertEqual(coverage([path]), {"rust_full": False, "sdk": True}) diff --git a/.github/workflows/fork-ci.yml b/.github/workflows/fork-ci.yml index 1cac51a10ee0..ae0f1534ecd8 100644 --- a/.github/workflows/fork-ci.yml +++ b/.github/workflows/fork-ci.yml @@ -100,7 +100,13 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu-24.04, windows-2025, macos-15] + include: + - os: ubuntu-24.04 + target: x86_64-unknown-linux-gnu + - os: windows-2025 + target: x86_64-pc-windows-msvc + - os: macos-15 + target: aarch64-apple-darwin defaults: run: shell: pwsh @@ -125,6 +131,11 @@ jobs: - name: Prepare full workspace runtime tools if: needs.changes.outputs.rust_full == 'true' uses: ./.github/actions/setup-ci + - name: Configure verified V8 artifacts for full workspace builds + if: needs.changes.outputs.rust_full == 'true' + uses: ./.github/actions/setup-rusty-v8 + with: + target: ${{ matrix.target }} - uses: dtolnay/rust-toolchain@e081816240890017053eacbb1bdf337761dc5582 # 1.95.0 with: components: clippy diff --git a/.github/workflows/postmerge-ci.yml b/.github/workflows/postmerge-ci.yml index 4dd09a847584..72f9d7d55b69 100644 --- a/.github/workflows/postmerge-ci.yml +++ b/.github/workflows/postmerge-ci.yml @@ -6,6 +6,10 @@ on: push: branches: [main] +permissions: + actions: read + contents: read + jobs: # Keep reusable workflow calls alphabetized. Each child retains its own # workflow_dispatch trigger so maintainers can rerun flaky suites directly. diff --git a/.github/workflows/repo-checks.yml b/.github/workflows/repo-checks.yml index d78d911730d5..5bfeb140a31f 100644 --- a/.github/workflows/repo-checks.yml +++ b/.github/workflows/repo-checks.yml @@ -122,7 +122,11 @@ jobs: run: python3 scripts/readme_toc.py README.md - name: Check formatting (run `just fmt` to fix) - run: just fmt-check + run: | + if ! just fmt-check; then + uv run --frozen --project scripts ruff format --diff . + exit 1 + fi - name: Prettier (run `pnpm run format:fix` to fix) run: pnpm run format diff --git a/.github/workflows/sdk.yml b/.github/workflows/sdk.yml index ec85195be916..56579b37f205 100644 --- a/.github/workflows/sdk.yml +++ b/.github/workflows/sdk.yml @@ -123,6 +123,12 @@ jobs: if: github.repository != 'openai/codex' uses: ./.github/actions/setup-ci + - name: Configure verified V8 artifacts for hosted fork builds + if: github.repository != 'openai/codex' + uses: ./.github/actions/setup-rusty-v8 + with: + target: x86_64-unknown-linux-gnu + - name: Cache hosted fork Cargo builds if: github.repository != 'openai/codex' uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 From f17c4b92583c2e7c8bfdad360432b56bb265f0d1 Mon Sep 17 00:00:00 2001 From: Danger Mouse <35347349+DangerMouseUK@users.noreply.github.com> Date: Wed, 7 Oct 2026 18:43:22 +0100 Subject: [PATCH 7/8] Fix Windows policy checks and stale SDK formatter expectations --- .github/scripts/fork_ci_changes.py | 13 ++++++-- .github/scripts/test_check_ci_results.py | 6 +++- .github/scripts/verify_tui_core_boundary.py | 4 +-- .github/workflows/repo-checks.yml | 7 ++++ .github/workflows/sdk.yml | 16 +++++++++- .../test_artifact_workflow_and_binaries.py | 32 ++----------------- 6 files changed, 43 insertions(+), 35 deletions(-) diff --git a/.github/scripts/fork_ci_changes.py b/.github/scripts/fork_ci_changes.py index c9df44545f4b..9eb5cd707d1a 100644 --- a/.github/scripts/fork_ci_changes.py +++ b/.github/scripts/fork_ci_changes.py @@ -11,7 +11,8 @@ def coverage(paths: list[str], *, full: bool = False) -> dict[str, bool]: path.startswith("codex-rs/") and ( PurePosixPath(path).suffix == ".rs" - or PurePosixPath(path).name in {"Cargo.toml", "Cargo.lock", "rust-toolchain.toml"} + or PurePosixPath(path).name + in {"Cargo.toml", "Cargo.lock", "rust-toolchain.toml"} or path.startswith("codex-rs/.cargo/") or path.startswith("codex-rs/.config/") ) @@ -41,7 +42,15 @@ def main() -> None: else: paths = ( subprocess.check_output( - ["git", "diff", "--name-only", "--no-renames", "-z", args.base, args.head] + [ + "git", + "diff", + "--name-only", + "--no-renames", + "-z", + args.base, + args.head, + ] ) .decode("utf-8") .split("\0") diff --git a/.github/scripts/test_check_ci_results.py b/.github/scripts/test_check_ci_results.py index 6a0f3e682ea8..4d86edf2afb8 100644 --- a/.github/scripts/test_check_ci_results.py +++ b/.github/scripts/test_check_ci_results.py @@ -9,7 +9,11 @@ class CiResultsTests(unittest.TestCase): def check(self, needs, *args): return subprocess.run( - [sys.executable, str(Path(__file__).with_name("check_ci_results.py")), *args], + [ + sys.executable, + str(Path(__file__).with_name("check_ci_results.py")), + *args, + ], env={**os.environ, "NEEDS": json.dumps(needs)}, capture_output=True, text=True, diff --git a/.github/scripts/verify_tui_core_boundary.py b/.github/scripts/verify_tui_core_boundary.py index 4c1b8d1e89a9..f84538eb0eab 100644 --- a/.github/scripts/verify_tui_core_boundary.py +++ b/.github/scripts/verify_tui_core_boundary.py @@ -42,7 +42,7 @@ def main() -> int: def manifest_failures() -> list[str]: - manifest = tomllib.loads(TUI_MANIFEST.read_text()) + manifest = tomllib.loads(TUI_MANIFEST.read_text(encoding="utf-8")) failures = [] for section_name, dependencies in dependency_sections(manifest): if FORBIDDEN_PACKAGE in dependencies: @@ -74,7 +74,7 @@ def dependency_sections(manifest: dict) -> list[tuple[str, dict]]: def source_failures() -> list[str]: failures = [] for path in sorted(TUI_ROOT.glob("**/*.rs")): - text = path.read_text() + text = path.read_text(encoding="utf-8") for line_number, line in enumerate(text.splitlines(), start=1): if any(pattern.search(line) for pattern in FORBIDDEN_SOURCE_PATTERNS): failures.append( diff --git a/.github/workflows/repo-checks.yml b/.github/workflows/repo-checks.yml index 5bfeb140a31f..1c07a9feb1f0 100644 --- a/.github/workflows/repo-checks.yml +++ b/.github/workflows/repo-checks.yml @@ -34,6 +34,13 @@ jobs: shell: pwsh run: Save-Module -Name Pester -RequiredVersion 5.7.1 -Repository PSGallery -Path "$env:RUNNER_TEMP/windows-setup-tests" -Force + - name: Verify repository policies on Windows + run: | + foreach ($script in @('verify_cargo_workspace_manifests.py', 'verify_tui_core_boundary.py', 'verify_bazel_clippy_lints.py')) { + python ".github/scripts/$script" + if ($LASTEXITCODE -ne 0) { throw "Repository policy failed: $script" } + } + - name: Test Windows setup with mocked installers run: | Import-Module "$env:RUNNER_TEMP/windows-setup-tests/Pester/5.7.1/Pester.psd1" -Force diff --git a/.github/workflows/sdk.yml b/.github/workflows/sdk.yml index 56579b37f205..561d3a406718 100644 --- a/.github/workflows/sdk.yml +++ b/.github/workflows/sdk.yml @@ -130,8 +130,9 @@ jobs: target: x86_64-unknown-linux-gnu - name: Cache hosted fork Cargo builds + id: fork_cargo_cache if: github.repository != 'openai/codex' - uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 + uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 with: path: | ~/.cargo/registry/index @@ -143,6 +144,7 @@ jobs: fork-sdk-cargo-${{ hashFiles('codex-rs/Cargo.lock', 'codex-rs/rust-toolchain.toml') }}- - name: Build the CLI for hosted fork SDK tests + id: fork_build if: github.repository != 'openai/codex' working-directory: codex-rs env: @@ -186,6 +188,18 @@ jobs: uv run --only-group dev --frozen --no-sync ruff format --check . uv run --only-group dev --frozen --no-sync pytest + - name: Save successful hosted fork Cargo builds + if: always() && !cancelled() && github.repository != 'openai/codex' && steps.fork_build.outcome == 'success' && steps.fork_cargo_cache.outputs.cache-hit != 'true' + continue-on-error: true + uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 + with: + path: | + ~/.cargo/registry/index + ~/.cargo/registry/cache + ~/.cargo/git/db + ${{ runner.temp }}/sdk-cargo-target + key: fork-sdk-cargo-${{ hashFiles('codex-rs/Cargo.lock', 'codex-rs/rust-toolchain.toml') }}-${{ github.sha }} + - name: Save bazel repository cache if: always() && !cancelled() && github.repository == 'openai/codex' && steps.build_bazel.outcome == 'success' && steps.setup_bazel.outputs.repository-cache-hit != 'true' continue-on-error: true diff --git a/sdk/python/tests/test_artifact_workflow_and_binaries.py b/sdk/python/tests/test_artifact_workflow_and_binaries.py index 039b0b86b25b..5c8c51fa0d9e 100644 --- a/sdk/python/tests/test_artifact_workflow_and_binaries.py +++ b/sdk/python/tests/test_artifact_workflow_and_binaries.py @@ -152,14 +152,6 @@ def test_root_format_driver_covers_all_formatter_groups( ) -> None: """The shared driver should retain every formatter in both modes.""" script = _load_root_format_script_module() - for name in ( - "bazel/rules/example.rs", - "codex-rs/src/lib.rs", - "codex-rs/new file.rs", - ): - path = tmp_path / name - path.parent.mkdir(parents=True, exist_ok=True) - path.write_text("") git_ls_files_args = [ "git", "ls-files", @@ -172,11 +164,6 @@ def test_root_format_driver_covers_all_formatter_groups( # The Python SDK CI image has no Git; keep discovery mocked at the process boundary. def fake_check_output(args, *, cwd): assert cwd == tmp_path - if args == git_ls_files_args + ["--", "*.rs"]: - return ( - b"codex-rs/src/lib.rs\0bazel/rules/example.rs\0" - b"codex-rs/new file.rs\0codex-rs/deleted.rs\0" - ) assert args == git_ls_files_args return b"MODULE.bazel\0README.md\0third_party/v8/libcxx.BUILD.bazel\0" @@ -238,25 +225,12 @@ def fake_check_output(args, *, cwd): ) assert formatters[0].commands[-1].args == ("just", "--unstable", "--fmt") assert checks[0].commands[-1].args == ("just", "--unstable", "--fmt", "--check") - rustfmt_args = ( - "rustfmt", - "--edition", - "2024", - "--config-path", - str(tmp_path / "codex-rs/rustfmt.toml"), - "--config", - "imports_granularity=Item,skip_children=true", - ) - rust_files = ( - os.path.join("..", "bazel", "rules", "example.rs"), - "new file.rs", - os.path.join("src", "lib.rs"), - ) + cargo_fmt_args = ("cargo", "fmt", "--", "--config", "imports_granularity=Item") assert formatters[1].commands == ( - script.Command(rustfmt_args + rust_files, tmp_path / "codex-rs"), + script.Command(cargo_fmt_args, tmp_path / "codex-rs"), ) assert checks[1].commands == ( - script.Command(rustfmt_args + ("--check",) + rust_files, tmp_path / "codex-rs"), + script.Command(cargo_fmt_args + ("--check",), tmp_path / "codex-rs"), ) format_buildifier_args = formatters[2].commands[-1].args check_buildifier_args = checks[2].commands[-1].args From cb5b5f37c7c124445373141b6173f430358f0b03 Mon Sep 17 00:00:00 2001 From: Danger Mouse <35347349+DangerMouseUK@users.noreply.github.com> Date: Wed, 7 Oct 2026 18:54:55 +0100 Subject: [PATCH 8/8] Normalize SDK formatter assertions and CI failure diagnostics --- .github/workflows/repo-checks.yml | 3 ++- .../tests/test_artifact_workflow_and_binaries.py | 10 ++++------ 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/.github/workflows/repo-checks.yml b/.github/workflows/repo-checks.yml index 1c07a9feb1f0..3b33b7aa82b1 100644 --- a/.github/workflows/repo-checks.yml +++ b/.github/workflows/repo-checks.yml @@ -131,7 +131,8 @@ jobs: - name: Check formatting (run `just fmt` to fix) run: | if ! just fmt-check; then - uv run --frozen --project scripts ruff format --diff . + uv run --frozen --project scripts ruff format --diff . || true + uv run --frozen --project sdk/python --only-group format ruff format --diff sdk/python || true exit 1 fi diff --git a/sdk/python/tests/test_artifact_workflow_and_binaries.py b/sdk/python/tests/test_artifact_workflow_and_binaries.py index 5c8c51fa0d9e..637e41a17bb1 100644 --- a/sdk/python/tests/test_artifact_workflow_and_binaries.py +++ b/sdk/python/tests/test_artifact_workflow_and_binaries.py @@ -226,12 +226,10 @@ def fake_check_output(args, *, cwd): assert formatters[0].commands[-1].args == ("just", "--unstable", "--fmt") assert checks[0].commands[-1].args == ("just", "--unstable", "--fmt", "--check") cargo_fmt_args = ("cargo", "fmt", "--", "--config", "imports_granularity=Item") - assert formatters[1].commands == ( - script.Command(cargo_fmt_args, tmp_path / "codex-rs"), - ) - assert checks[1].commands == ( - script.Command(cargo_fmt_args + ("--check",), tmp_path / "codex-rs"), - ) + assert formatters[1].commands[0].args == cargo_fmt_args + assert formatters[1].commands[0].cwd == tmp_path / "codex-rs" + assert checks[1].commands[0].args == cargo_fmt_args + ("--check",) + assert checks[1].commands[0].cwd == tmp_path / "codex-rs" format_buildifier_args = formatters[2].commands[-1].args check_buildifier_args = checks[2].commands[-1].args assert format_buildifier_args[:4] == (