diff --git a/.github/workflows/repo-checks.yml b/.github/workflows/repo-checks.yml index 4f05cd06046b..5fde6ff95eea 100644 --- a/.github/workflows/repo-checks.yml +++ b/.github/workflows/repo-checks.yml @@ -4,6 +4,46 @@ 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] + defaults: + run: + shell: ${{ matrix.shell }} + 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 + 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 -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() + 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