codex 安装脚本分析
codex 安装脚本是一个非常典型且易于阅读的 PowerShell 脚本安装程序。
一、 为什么在控制面板的“软件卸载”中找不到 Codex?
在 Windows 中,一个软件想要出现在控制面板的“程序和功能”(或设置中的“应用和功能”)列表里,它的安装程序必须在 Windows 的注册表(Registry)中写入特定的卸载信息(通常在 HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall 或 HKCU 下)。
通过分析这个脚本,可以看出它是一个“绿色/便携式”(Standalone)安装脚本,它完全没有对注册表进行这种写入操作。它所做的事情非常简单直接:
- 下载压缩包。
- 解压到你的用户文件夹里。
- 创建文件夹快捷方式(Junctions)。
- 修改你的用户环境变量(PATH)。
因为它没有向系统注册自己,所以控制面板里自然找不到它。如果你想“卸载”它,只需要手动删除相关文件夹,并把环境变量里的路径删掉即可。
二、 如何知道 Codex 的安装位置?
根据脚本中的变量定义,除非你在安装前自己设置了环境变量($env:CODEX_HOME 或 $env:CODEX_INSTALL_DIR),否则它会默认安装在以下两个隐蔽的目录中:
1. 实际文件存放位置(核心程序库):
- 默认路径:
C:\Users\<你的用户名>\.codex\packages\standalone\ - 脚本对应逻辑:
$codexHome = Join-Path $env:USERPROFILE ".codex"
2. 可执行文件(快捷方式/目录联接)的对外暴露位置:
- 默认路径:
C:\Users\<你的用户名>\AppData\Local\Programs\OpenAI\Codex\bin - 脚本对应逻辑:
$defaultVisibleBinDir = Join-Path $env:LOCALAPPDATA "Programs\OpenAI\Codex\bin"
最快的查找方法:
由于脚本在最后把安装路径加到了系统的环境变量中,你只需打开任意一个命令提示符(CMD)或 PowerShell,输入以下命令即可直接看到它在哪里:
# 在 PowerShell 中:
Get-Command codex
# 在 CMD 中:
where codex
三、 如何了解 Codex 的安装详情(脚本到底做了什么)?
通读脚本逻辑,它的安装流程可以分为以下 5 个主要步骤:
1. 解析版本与下载地址 (Resolve-Release)
脚本首先会去 [releases.openai.com/codex](https://releases.openai.com/codex) 或 GitHub Releases API 检查当前最新的版本号(或者你指定的特定版本),并匹配你的电脑架构(比如 Windows x64 还是 ARM64),找到对应的 .tar.gz 或 .tgz 压缩包的下载链接。
2. 下载与校验 (Invoke-WebRequestWithFallback / Test-ArchiveDigest)
脚本会在你的系统的临时文件夹(Temp)里建一个专属目录,把压缩包和 SHA256 校验文件下载下来。下载完成后,它会自动计算压缩包的哈希值,确保文件在下载过程中没有损坏或被篡改。
3. 解压与存放 (Remove-StaleInstallArtifacts / tar -xzf)
它会将旧的、失败的临时安装残留清理掉,然后调用系统自带的 tar 命令把压缩包解压到 C:\Users\<你的用户名>\.codex\packages\standalone\releases\<版本号-架构> 目录下。
4. 建立目录联接/软链接 (Ensure-Junction)
为了方便以后升级(不用每次都去改环境变量),脚本玩了一个聪明的“指针”魔法(Directory Junctions):
- 它创建了一个叫
current的文件夹快捷方式,指向刚刚下载好的具体版本文件夹。 - 它又在
%LOCALAPPDATA%\Programs\OpenAI\Codex\bin创建了一个快捷方式,指向current里的bin目录。
这样做的好处是,以后再升级,只需要把current重新指向新版本的文件夹就行了,外面的路径永远不用变。
5. 解决冲突与配置环境变量 (Maybe-HandleConflictingInstall / Environment::SetEnvironmentVariable)
- 冲突检查:它会检查你是不是以前用
npm或bun全局安装过@openai/codex。如果有,它会弹窗问你是否要卸载旧的,以免系统不知道该运行哪一个。 - 写环境变量:最后,它将
%LOCALAPPDATA%\Programs\OpenAI\Codex\bin写入你当前用户的PATH环境变量。这一步是为了让你在以后打开任何终端时,只需要敲codex就能直接运行软件,而不用输入长长的一串绝对路径。
codex 完整安装脚本
[CmdletBinding()]
param(
[string]$Release = $env:CODEX_RELEASE
)
Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"
$ProgressPreference = "SilentlyContinue"
if ([string]::IsNullOrWhiteSpace($Release)) {
$Release = "latest"
}
$NonInteractive = $env:CODEX_NON_INTERACTIVE -match "^(?i:1|true|yes)$"
$DefaultPreferReleasesOpenAICom = $true
$PreferReleasesOpenAICom = if ([string]::IsNullOrWhiteSpace($env:CODEX_INSTALLER_USE_RELEASES_OPENAI_COM)) {
$DefaultPreferReleasesOpenAICom
} else {
$env:CODEX_INSTALLER_USE_RELEASES_OPENAI_COM -match "^(?i:1|true|yes)$"
}
$ReleasesBaseUri = "https://releases.openai.com/codex"
$ReleasesMetadataTimeoutSec = 30
$ReleasesAssetTimeoutSec = 300
function Write-Step {
param(
[string]$Message
)
Write-Host "==> $Message"
}
function Write-WarningStep {
param(
[string]$Message
)
Write-Warning $Message
}
function Prompt-YesNo {
param(
[string]$Prompt
)
if ($NonInteractive) {
return $false
}
if ([Console]::IsInputRedirected -or [Console]::IsOutputRedirected) {
return $false
}
$choice = Read-Host "$Prompt [y/N]"
return $choice -match "^(?i:y(?:es)?)$"
}
function Normalize-Version {
param(
[string]$RawVersion
)
if ([string]::IsNullOrWhiteSpace($RawVersion) -or $RawVersion -eq "latest") {
return "latest"
}
if ($RawVersion.StartsWith("rust-v")) {
return $RawVersion.Substring(6)
}
if ($RawVersion.StartsWith("v")) {
return $RawVersion.Substring(1)
}
return $RawVersion
}
function Assert-ValidReleaseVersion {
param(
[string]$Version
)
if ($Version -cne "latest" -and $Version -cnotmatch "^[0-9]+\.[0-9]+\.[0-9]+(?:-alpha(?:\.[0-9]+){0,2}|-beta(?:\.[0-9]+)?)?$") {
throw "Invalid Codex release version: $Version. Expected latest or x.y.z[-alpha[.N[.M]]|-beta[.N]]."
}
}
function Find-ReleaseAssetMetadata {
param(
[string]$AssetName,
[object]$ReleaseMetadata,
[string]$Url = $null,
[string]$FallbackUrl = $null
)
$asset = $ReleaseMetadata.assets | Where-Object { $_.name -eq $AssetName } | Select-Object -First 1
if ($null -eq $asset) {
return $null
}
$digestMatch = [regex]::Match([string]$asset.digest, "^sha256:([0-9a-fA-F]{64})$")
if (-not $digestMatch.Success) {
throw "Could not find SHA-256 digest for release asset $AssetName."
}
return [PSCustomObject]@{
Url = if ([string]::IsNullOrWhiteSpace($Url)) { $asset.browser_download_url } else { $Url }
FallbackUrl = $FallbackUrl
Sha256 = $digestMatch.Groups[1].Value.ToLowerInvariant()
}
}
function Invoke-WebRequestWithFallback {
param(
[object]$Metadata,
[string]$OutFile,
[string]$ExpectedDigest,
[string]$AssetName,
[string]$ReleaseVersion,
[string]$RequiredManifestAsset
)
try {
if ($Metadata.Url.StartsWith("$ReleasesBaseUri/", [System.StringComparison]::OrdinalIgnoreCase)) {
Invoke-WebRequest -UseBasicParsing -Uri $Metadata.Url -OutFile $OutFile -TimeoutSec $ReleasesAssetTimeoutSec
} else {
Invoke-WebRequest -UseBasicParsing -Uri $Metadata.Url -OutFile $OutFile
}
Test-ArchiveDigest -ArchivePath $OutFile -ExpectedDigest $ExpectedDigest
if (-not [string]::IsNullOrWhiteSpace($RequiredManifestAsset)) {
$null = Get-PackageArchiveDigest -ManifestPath $OutFile -AssetName $RequiredManifestAsset
}
} catch {
if ([string]::IsNullOrWhiteSpace($Metadata.FallbackUrl)) {
throw
}
Write-WarningStep "Could not download or verify $($Metadata.Url); retrying from GitHub Releases."
Invoke-WebRequest -UseBasicParsing -Uri $Metadata.FallbackUrl -OutFile $OutFile
try {
Test-ArchiveDigest -ArchivePath $OutFile -ExpectedDigest $ExpectedDigest
if (-not [string]::IsNullOrWhiteSpace($RequiredManifestAsset)) {
$null = Get-PackageArchiveDigest -ManifestPath $OutFile -AssetName $RequiredManifestAsset
}
} catch {
$githubRelease = Resolve-ReleaseFromGitHub -NormalizedVersion $ReleaseVersion
$githubAssetMetadata = Find-ReleaseAssetMetadata -AssetName $AssetName -ReleaseMetadata $githubRelease.Metadata
if ($null -eq $githubAssetMetadata) {
throw "Could not find GitHub release metadata for asset $AssetName."
}
Test-ArchiveDigest -ArchivePath $OutFile -ExpectedDigest $githubAssetMetadata.Sha256
if (-not [string]::IsNullOrWhiteSpace($RequiredManifestAsset)) {
$null = Get-PackageArchiveDigest -ManifestPath $OutFile -AssetName $RequiredManifestAsset
}
}
}
}
function Resolve-ReleaseAssetSelection {
param(
[object]$ResolvedRelease,
[string]$Target,
[string]$NpmTag
)
$version = $ResolvedRelease.Version
$releaseMetadata = $ResolvedRelease.Metadata
$packageAsset = "codex-package-$Target.tar.gz"
$checksumAsset = "codex-package_SHA256SUMS"
$packageUrl = $null
$packageFallbackUrl = $null
$checksumUrl = $null
$checksumFallbackUrl = $null
if ($ResolvedRelease.Source -eq "ReleasesOpenAICom") {
$packageUrl = "$ReleasesBaseUri/releases/$version/$packageAsset"
$packageFallbackUrl = "https://github.com/openai/codex/releases/download/rust-v$version/$packageAsset"
$checksumUrl = "$ReleasesBaseUri/releases/$version/$checksumAsset"
$checksumFallbackUrl = "https://github.com/openai/codex/releases/download/rust-v$version/$checksumAsset"
}
$packageMetadata = Find-ReleaseAssetMetadata -AssetName $packageAsset -ReleaseMetadata $releaseMetadata -Url $packageUrl -FallbackUrl $packageFallbackUrl
$checksumMetadata = Find-ReleaseAssetMetadata -AssetName $checksumAsset -ReleaseMetadata $releaseMetadata -Url $checksumUrl -FallbackUrl $checksumFallbackUrl
if ($null -ne $packageMetadata -and $null -ne $checksumMetadata) {
return [PSCustomObject]@{
PackageAsset = $packageAsset
PackageMetadata = $packageMetadata
ChecksumMetadata = $checksumMetadata
InstallLayout = "Package"
}
}
$packageAsset = "codex-npm-$NpmTag-$version.tgz"
$packageUrl = $null
$packageFallbackUrl = $null
if ($ResolvedRelease.Source -eq "ReleasesOpenAICom") {
$packageUrl = "$ReleasesBaseUri/releases/$version/$packageAsset"
$packageFallbackUrl = "https://github.com/openai/codex/releases/download/rust-v$version/$packageAsset"
}
$packageMetadata = Find-ReleaseAssetMetadata -AssetName $packageAsset -ReleaseMetadata $releaseMetadata -Url $packageUrl -FallbackUrl $packageFallbackUrl
if ($null -eq $packageMetadata) {
throw "Could not find Codex package or platform npm release assets for Codex $version."
}
return [PSCustomObject]@{
PackageAsset = $packageAsset
PackageMetadata = $packageMetadata
ChecksumMetadata = $null
InstallLayout = "LegacyPlatformNpm"
}
}
function Test-ArchiveDigest {
param(
[string]$ArchivePath,
[string]$ExpectedDigest
)
$actualDigest = (Get-FileHash -LiteralPath $ArchivePath -Algorithm SHA256).Hash.ToLowerInvariant()
if ($actualDigest -ne $ExpectedDigest) {
throw "Downloaded Codex archive checksum did not match expected digest. Expected $ExpectedDigest but got $actualDigest."
}
}
function Get-PackageArchiveDigest {
param(
[string]$ManifestPath,
[string]$AssetName
)
$escapedAssetName = [regex]::Escape($AssetName)
foreach ($line in Get-Content -LiteralPath $ManifestPath) {
$match = [regex]::Match($line, "^\s*([0-9a-fA-F]{64})\s+$escapedAssetName\s*$")
if ($match.Success) {
return $match.Groups[1].Value.ToLowerInvariant()
}
}
throw "Could not find SHA-256 digest for $AssetName in codex-package_SHA256SUMS."
}
function Path-Contains {
param(
[string]$PathValue,
[string]$Entry
)
if ([string]::IsNullOrWhiteSpace($PathValue)) {
return $false
}
$needle = $Entry.TrimEnd("\")
foreach ($segment in $PathValue.Split(";", [System.StringSplitOptions]::RemoveEmptyEntries)) {
if ($segment.TrimEnd("\") -ieq $needle) {
return $true
}
}
return $false
}
function Prepend-PathEntry {
param(
[string]$PathValue,
[string]$Entry
)
$needle = $Entry.TrimEnd("\")
$segments = @($Entry)
if (-not [string]::IsNullOrWhiteSpace($PathValue)) {
$segments += $PathValue.Split(";", [System.StringSplitOptions]::RemoveEmptyEntries) |
Where-Object { $_.TrimEnd("\") -ine $needle }
}
return ($segments -join ";")
}
function Invoke-WithInstallLock {
param(
[string]$LockPath,
[scriptblock]$Script
)
New-Item -ItemType Directory -Force -Path (Split-Path -Parent $LockPath) | Out-Null
$lock = $null
while ($null -eq $lock) {
try {
$lock = [System.IO.File]::Open(
$LockPath,
[System.IO.FileMode]::OpenOrCreate,
[System.IO.FileAccess]::ReadWrite,
[System.IO.FileShare]::None
)
} catch [System.IO.IOException] {
Start-Sleep -Milliseconds 250
}
}
try {
& $Script
} finally {
$lock.Dispose()
}
}
function Remove-StaleInstallArtifacts {
param(
[string]$ReleasesDir
)
if (Test-Path -LiteralPath $ReleasesDir -PathType Container) {
Get-ChildItem -LiteralPath $ReleasesDir -Force -Directory -Filter ".staging.*" -ErrorAction SilentlyContinue |
Remove-Item -Recurse -Force -ErrorAction SilentlyContinue
}
}
function Resolve-VersionFromReleaseMetadata {
param(
[object]$ReleaseMetadata
)
if (-not $ReleaseMetadata.tag_name) {
throw "Failed to resolve the latest Codex release version."
}
$resolvedVersion = Normalize-Version -RawVersion $ReleaseMetadata.tag_name
Assert-ValidReleaseVersion -Version $resolvedVersion
return $resolvedVersion
}
function Resolve-ReleaseFromGitHub {
param(
[string]$NormalizedVersion
)
if ($NormalizedVersion -eq "latest") {
$requestedRelease = "latest"
$metadataUri = "https://api.github.com/repos/openai/codex/releases/latest"
} else {
$resolvedVersion = $NormalizedVersion
$requestedRelease = $resolvedVersion
$metadataUri = "https://api.github.com/repos/openai/codex/releases/tags/rust-v$resolvedVersion"
}
try {
$releaseMetadata = Invoke-RestMethod -Uri $metadataUri
} catch {
throw "Could not fetch GitHub release metadata for Codex $requestedRelease. GitHub API may be unavailable or rate limited. $($_.Exception.Message)"
}
if ($NormalizedVersion -eq "latest") {
$resolvedVersion = Resolve-VersionFromReleaseMetadata -ReleaseMetadata $releaseMetadata
}
return [PSCustomObject]@{
Version = $resolvedVersion
Metadata = $releaseMetadata
Source = "GitHub"
}
}
function Resolve-ReleaseFromReleases {
param(
[string]$NormalizedVersion
)
$metadataUri = if ($NormalizedVersion -eq "latest") {
"$ReleasesBaseUri/channels/latest"
} else {
"$ReleasesBaseUri/releases/$NormalizedVersion/release.json"
}
try {
$metadataResponse = Invoke-WebRequest -UseBasicParsing -Uri $metadataUri -TimeoutSec $ReleasesMetadataTimeoutSec
$releaseMetadata = [string]$metadataResponse.Content | ConvertFrom-Json -ErrorAction Stop
$resolvedVersion = Resolve-VersionFromReleaseMetadata -ReleaseMetadata $releaseMetadata
if ($NormalizedVersion -ne "latest" -and $resolvedVersion -cne $NormalizedVersion) {
throw "Release metadata version did not match requested Codex version $NormalizedVersion."
}
$resolvedRelease = [PSCustomObject]@{
Version = $resolvedVersion
Metadata = $releaseMetadata
Source = "ReleasesOpenAICom"
}
$null = Resolve-ReleaseAssetSelection -ResolvedRelease $resolvedRelease -Target $target -NpmTag $npmTag
} catch {
return $null
}
return $resolvedRelease
}
function Resolve-Release {
$normalizedVersion = Normalize-Version -RawVersion $Release
Assert-ValidReleaseVersion -Version $normalizedVersion
if ($PreferReleasesOpenAICom) {
$release = Resolve-ReleaseFromReleases -NormalizedVersion $normalizedVersion
if ($null -ne $release) {
return $release
}
Write-WarningStep "releases.openai.com is unavailable; falling back to GitHub Releases."
}
return Resolve-ReleaseFromGitHub -NormalizedVersion $normalizedVersion
}
function Get-VersionFromBinary {
param(
[string]$CodexPath
)
if (-not (Test-Path -LiteralPath $CodexPath -PathType Leaf)) {
return $null
}
try {
$versionOutput = & $CodexPath --version 2>$null
} catch {
return $null
}
if ($versionOutput -match '([0-9][0-9A-Za-z.+-]*)$') {
return $matches[1]
}
return $null
}
function Get-CurrentInstalledVersion {
param(
[string]$StandaloneCurrentDir
)
$standaloneVersion = Get-VersionFromBinary -CodexPath (Join-Path $StandaloneCurrentDir "bin\codex.exe")
if (-not [string]::IsNullOrWhiteSpace($standaloneVersion)) {
return $standaloneVersion
}
$standaloneVersion = Get-VersionFromBinary -CodexPath (Join-Path $StandaloneCurrentDir "codex.exe")
if (-not [string]::IsNullOrWhiteSpace($standaloneVersion)) {
return $standaloneVersion
}
return $null
}
function Test-OldStandaloneBinLayout {
param(
[string]$VisibleBinDir,
[string]$DefaultVisibleBinDir
)
if (-not $VisibleBinDir.Equals($DefaultVisibleBinDir, [System.StringComparison]::OrdinalIgnoreCase)) {
return $false
}
if (-not (Test-Path -LiteralPath $VisibleBinDir -PathType Container)) {
return $false
}
$item = Get-Item -LiteralPath $VisibleBinDir -Force
if ($item.Attributes -band [IO.FileAttributes]::ReparsePoint) {
return $false
}
$requiredFiles = @("codex.exe", "rg.exe")
foreach ($fileName in $requiredFiles) {
if (-not (Test-Path -LiteralPath (Join-Path $VisibleBinDir $fileName) -PathType Leaf)) {
return $false
}
}
$knownFiles = @(
"codex.exe",
"rg.exe",
"codex-command-runner.exe",
"codex-windows-sandbox.exe",
"codex-windows-sandbox-setup.exe"
)
foreach ($child in Get-ChildItem -LiteralPath $VisibleBinDir -Force) {
if ($child.PSIsContainer) {
return $false
}
if ($knownFiles -notcontains $child.Name) {
return $false
}
}
return $true
}
function Move-OldStandaloneBinIfApproved {
param(
[string]$VisibleBinDir,
[string]$DefaultVisibleBinDir
)
if (-not (Test-OldStandaloneBinLayout -VisibleBinDir $VisibleBinDir -DefaultVisibleBinDir $DefaultVisibleBinDir)) {
return $null
}
Write-Step "We found an older Codex install at $VisibleBinDir"
Write-WarningStep "To continue, Codex needs to update the install at this path."
if (-not (Prompt-YesNo "Replace it with the current Codex setup now?")) {
throw "Cannot replace older standalone install without confirmation: $VisibleBinDir"
}
$backupDir = "$VisibleBinDir.backup.$([DateTimeOffset]::UtcNow.ToUnixTimeSeconds()).$PID"
Write-Step "Moving older standalone install to $backupDir"
Move-Item -LiteralPath $VisibleBinDir -Destination $backupDir
return $backupDir
}
function Add-JunctionSupportType {
if (([System.Management.Automation.PSTypeName]'CodexInstaller.Junction').Type) {
return
}
Add-Type -TypeDefinition @"
using System;
using System.ComponentModel;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;
using Microsoft.Win32.SafeHandles;
namespace CodexInstaller
{
public static class Junction
{
private const uint GENERIC_WRITE = 0x40000000;
private const uint FILE_SHARE_READ = 0x00000001;
private const uint FILE_SHARE_WRITE = 0x00000002;
private const uint FILE_SHARE_DELETE = 0x00000004;
private const uint OPEN_EXISTING = 3;
private const uint FILE_FLAG_BACKUP_SEMANTICS = 0x02000000;
private const uint FILE_FLAG_OPEN_REPARSE_POINT = 0x00200000;
private const uint FSCTL_SET_REPARSE_POINT = 0x000900A4;
private const uint IO_REPARSE_TAG_MOUNT_POINT = 0xA0000003;
private const int HeaderLength = 20;
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
private static extern SafeFileHandle CreateFileW(
string lpFileName,
uint dwDesiredAccess,
uint dwShareMode,
IntPtr lpSecurityAttributes,
uint dwCreationDisposition,
uint dwFlagsAndAttributes,
IntPtr hTemplateFile);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool DeviceIoControl(
SafeFileHandle hDevice,
uint dwIoControlCode,
byte[] lpInBuffer,
int nInBufferSize,
IntPtr lpOutBuffer,
int nOutBufferSize,
out int lpBytesReturned,
IntPtr lpOverlapped);
public static void SetTarget(string linkPath, string targetPath)
{
string substituteName = "\\??\\" + Path.GetFullPath(targetPath);
byte[] substituteNameBytes = Encoding.Unicode.GetBytes(substituteName);
if (substituteNameBytes.Length > ushort.MaxValue - HeaderLength) {
throw new ArgumentException("Junction target path is too long.", "targetPath");
}
byte[] reparseBuffer = new byte[substituteNameBytes.Length + HeaderLength];
WriteUInt32(reparseBuffer, 0, IO_REPARSE_TAG_MOUNT_POINT);
WriteUInt16(reparseBuffer, 4, checked((ushort)(substituteNameBytes.Length + 12)));
WriteUInt16(reparseBuffer, 8, 0);
WriteUInt16(reparseBuffer, 10, checked((ushort)substituteNameBytes.Length));
WriteUInt16(reparseBuffer, 12, checked((ushort)(substituteNameBytes.Length + 2)));
WriteUInt16(reparseBuffer, 14, 0);
Buffer.BlockCopy(substituteNameBytes, 0, reparseBuffer, 16, substituteNameBytes.Length);
using (SafeFileHandle handle = CreateFileW(
linkPath,
GENERIC_WRITE,
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
IntPtr.Zero,
OPEN_EXISTING,
FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT,
IntPtr.Zero))
{
if (handle.IsInvalid) {
throw new Win32Exception(Marshal.GetLastWin32Error());
}
int bytesReturned;
if (!DeviceIoControl(
handle,
FSCTL_SET_REPARSE_POINT,
reparseBuffer,
reparseBuffer.Length,
IntPtr.Zero,
0,
out bytesReturned,
IntPtr.Zero))
{
throw new Win32Exception(Marshal.GetLastWin32Error());
}
}
}
private static void WriteUInt16(byte[] buffer, int offset, ushort value)
{
buffer[offset] = (byte)value;
buffer[offset + 1] = (byte)(value >> 8);
}
private static void WriteUInt32(byte[] buffer, int offset, uint value)
{
buffer[offset] = (byte)value;
buffer[offset + 1] = (byte)(value >> 8);
buffer[offset + 2] = (byte)(value >> 16);
buffer[offset + 3] = (byte)(value >> 24);
}
}
}
"@
}
function Set-JunctionTarget {
param(
[string]$LinkPath,
[string]$TargetPath
)
Add-JunctionSupportType
[CodexInstaller.Junction]::SetTarget($LinkPath, $TargetPath)
}
function Test-IsJunction {
param(
[string]$Path
)
if (-not (Test-Path -LiteralPath $Path)) {
return $false
}
$item = Get-Item -LiteralPath $Path -Force
return ($item.Attributes -band [IO.FileAttributes]::ReparsePoint) -and $item.LinkType -eq "Junction"
}
function Ensure-Junction {
param(
[string]$LinkPath,
[string]$TargetPath,
[string]$InstallerOwnedTargetPrefix
)
if (-not (Test-Path -LiteralPath $LinkPath)) {
New-Item -ItemType Junction -Path $LinkPath -Target $TargetPath | Out-Null
return
}
$item = Get-Item -LiteralPath $LinkPath -Force
if (Test-IsJunction -Path $LinkPath) {
$existingTarget = [string]$item.Target
if (-not [string]::IsNullOrWhiteSpace($InstallerOwnedTargetPrefix)) {
$ownedTargetPrefix = $InstallerOwnedTargetPrefix.TrimEnd("\\")
if (-not $existingTarget.StartsWith($ownedTargetPrefix, [System.StringComparison]::OrdinalIgnoreCase)) {
throw "Refusing to retarget junction at $LinkPath because it is not managed by this installer."
}
}
if ($existingTarget.Equals($TargetPath, [System.StringComparison]::OrdinalIgnoreCase)) {
return
}
# Keep the path itself in place and only retarget the junction. That
# avoids a gap where current or the visible bin path disappears during
# an update.
Set-JunctionTarget -LinkPath $LinkPath -TargetPath $TargetPath
return
}
if ($item.Attributes -band [IO.FileAttributes]::ReparsePoint) {
throw "Refusing to replace non-junction reparse point at $LinkPath."
}
if ($item.PSIsContainer) {
if ((Get-ChildItem -LiteralPath $LinkPath -Force | Select-Object -First 1) -ne $null) {
throw "Refusing to replace non-empty directory at $LinkPath with a junction."
}
Remove-Item -LiteralPath $LinkPath -Force
New-Item -ItemType Junction -Path $LinkPath -Target $TargetPath | Out-Null
return
}
throw "Refusing to replace file at $LinkPath with a junction."
}
function Test-PackageContentsAreComplete {
param(
[string]$PackageDir
)
if (-not (Test-Path -LiteralPath $PackageDir -PathType Container)) {
return $false
}
$expectedFiles = @(
"codex-package.json",
"bin\codex.exe",
"bin\codex-code-mode-host.exe",
"codex-path\rg.exe",
"codex-resources\codex-command-runner.exe",
"codex-resources\codex-windows-sandbox-setup.exe"
)
foreach ($name in $expectedFiles) {
if (-not (Test-Path -LiteralPath (Join-Path $PackageDir $name) -PathType Leaf)) {
return $false
}
}
return $true
}
function Test-LegacyPlatformNpmContentsAreComplete {
param(
[string]$PackageDir
)
if (-not (Test-Path -LiteralPath $PackageDir -PathType Container)) {
return $false
}
$expectedFiles = @(
"codex.exe",
"codex-resources\codex-command-runner.exe",
"codex-resources\codex-windows-sandbox-setup.exe",
"codex-resources\rg.exe"
)
foreach ($name in $expectedFiles) {
if (-not (Test-Path -LiteralPath (Join-Path $PackageDir $name) -PathType Leaf)) {
return $false
}
}
return $true
}
function Test-ReleaseIsComplete {
param(
[string]$ReleaseDir,
[string]$ExpectedVersion,
[string]$ExpectedTarget,
[string]$Layout
)
switch ($Layout) {
"Package" {
if (-not (Test-PackageContentsAreComplete -PackageDir $ReleaseDir)) {
return $false
}
$codexPath = Join-Path $ReleaseDir "bin\codex.exe"
}
"LegacyPlatformNpm" {
if (-not (Test-LegacyPlatformNpmContentsAreComplete -PackageDir $ReleaseDir)) {
return $false
}
$codexPath = Join-Path $ReleaseDir "codex.exe"
}
default {
throw "Unknown Codex installer layout: $Layout"
}
}
return (Split-Path -Leaf $ReleaseDir) -eq "$ExpectedVersion-$ExpectedTarget" -and
(Get-VersionFromBinary -CodexPath $codexPath) -ceq $ExpectedVersion
}
function Get-ExistingCodexCommand {
$existing = Get-Command codex -ErrorAction SilentlyContinue
if ($null -eq $existing) {
return $null
}
return $existing.Source
}
function Get-ExistingCodexManager {
param(
[string]$ExistingPath,
[string]$VisibleBinDir
)
if ([string]::IsNullOrWhiteSpace($ExistingPath)) {
return $null
}
if ($ExistingPath.StartsWith($VisibleBinDir, [System.StringComparison]::OrdinalIgnoreCase)) {
return $null
}
if ($ExistingPath -match "\\.bun\\") {
return "bun"
}
if ($ExistingPath -match "node_modules" -or $ExistingPath -match "\\npm\\") {
return "npm"
}
return $null
}
function Get-ConflictingInstall {
param(
[string]$VisibleBinDir
)
$existingPath = Get-ExistingCodexCommand
$manager = Get-ExistingCodexManager -ExistingPath $existingPath -VisibleBinDir $VisibleBinDir
if ($null -eq $manager) {
return $null
}
Write-Step "Detected existing $manager-managed Codex at $existingPath"
Write-WarningStep "Multiple managed Codex installs can be ambiguous because PATH order decides which one runs."
return [PSCustomObject]@{
Manager = $manager
Path = $existingPath
}
}
function Maybe-HandleConflictingInstall {
param(
[object]$Conflict
)
if ($null -eq $Conflict) {
return
}
$manager = $Conflict.Manager
$uninstallArgs = if ($manager -eq "bun") {
@("remove", "-g", "@openai/codex")
} else {
@("uninstall", "-g", "@openai/codex")
}
$uninstallCommand = if ($manager -eq "bun") { "bun" } else { "npm" }
if (Prompt-YesNo "Uninstall the existing $manager-managed Codex now?") {
Write-Step "Running: $uninstallCommand $($uninstallArgs -join ' ')"
try {
& $uninstallCommand @uninstallArgs
} catch {
Write-WarningStep "Failed to uninstall the existing $manager-managed Codex. Continuing with the standalone install."
}
} else {
Write-WarningStep "Leaving the existing $manager-managed Codex installed. PATH order will determine which codex runs."
}
}
function Test-VisibleCodexCommand {
param(
[string]$VisibleBinDir
)
$codexCommand = Join-Path $VisibleBinDir "codex.exe"
& $codexCommand --version *> $null
if ($LASTEXITCODE -ne 0) {
throw "Installed Codex command failed verification: $codexCommand --version"
}
}
if ($env:OS -ne "Windows_NT") {
Write-Error "install.ps1 supports Windows only. Use install.sh on macOS or Linux."
exit 1
}
if (-not [Environment]::Is64BitOperatingSystem) {
Write-Error "Codex requires a 64-bit version of Windows."
exit 1
}
$architecture = [System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture
$target = $null
$platformLabel = $null
$npmTag = $null
switch ($architecture) {
"Arm64" {
$target = "aarch64-pc-windows-msvc"
$platformLabel = "Windows (ARM64)"
$npmTag = "win32-arm64"
}
"X64" {
$target = "x86_64-pc-windows-msvc"
$platformLabel = "Windows (x64)"
$npmTag = "win32-x64"
}
default {
Write-Error "Unsupported architecture: $architecture"
exit 1
}
}
$codexHome = if ([string]::IsNullOrWhiteSpace($env:CODEX_HOME)) {
Join-Path $env:USERPROFILE ".codex"
} else {
$env:CODEX_HOME
}
$standaloneRoot = Join-Path $codexHome "packages\standalone"
$releasesDir = Join-Path $standaloneRoot "releases"
$currentDir = Join-Path $standaloneRoot "current"
$lockPath = Join-Path $standaloneRoot "install.lock"
$defaultVisibleBinDir = Join-Path $env:LOCALAPPDATA "Programs\OpenAI\Codex\bin"
if ([string]::IsNullOrWhiteSpace($env:CODEX_INSTALL_DIR)) {
$visibleBinDir = $defaultVisibleBinDir
} else {
$visibleBinDir = $env:CODEX_INSTALL_DIR
}
$currentVersion = Get-CurrentInstalledVersion -StandaloneCurrentDir $currentDir
$resolvedRelease = Resolve-Release
$resolvedVersion = $resolvedRelease.Version
$releaseMetadata = $resolvedRelease.Metadata
$releaseName = "$resolvedVersion-$target"
$releaseDir = Join-Path $releasesDir $releaseName
if (-not [string]::IsNullOrWhiteSpace($currentVersion) -and $currentVersion -ne $resolvedVersion) {
Write-Step "Updating Codex CLI from $currentVersion to $resolvedVersion"
} elseif (-not [string]::IsNullOrWhiteSpace($currentVersion)) {
Write-Step "Updating Codex CLI"
} else {
Write-Step "Installing Codex CLI"
}
Write-Step "Detected platform: $platformLabel"
Write-Step "Resolved version: $resolvedVersion"
$conflictingInstall = Get-ConflictingInstall -VisibleBinDir $visibleBinDir
$oldStandaloneBackup = $null
$checksumAsset = "codex-package_SHA256SUMS"
$assetSelection = Resolve-ReleaseAssetSelection -ResolvedRelease $resolvedRelease -Target $target -NpmTag $npmTag
$packageAsset = $assetSelection.PackageAsset
$packageMetadata = $assetSelection.PackageMetadata
$checksumMetadata = $assetSelection.ChecksumMetadata
$installLayout = $assetSelection.InstallLayout
$tempDir = Join-Path ([System.IO.Path]::GetTempPath()) ("codex-install-" + [System.Guid]::NewGuid().ToString("N"))
New-Item -ItemType Directory -Force -Path $tempDir | Out-Null
try {
Invoke-WithInstallLock -LockPath $lockPath -Script {
Remove-StaleInstallArtifacts -ReleasesDir $releasesDir
if (-not (Test-ReleaseIsComplete -ReleaseDir $releaseDir -ExpectedVersion $resolvedVersion -ExpectedTarget $target -Layout $installLayout)) {
if (Test-Path -LiteralPath $releaseDir) {
Write-WarningStep "Found incomplete existing release at $releaseDir. Reinstalling."
}
$archivePath = Join-Path $tempDir $packageAsset
$checksumPath = Join-Path $tempDir $checksumAsset
$stagingDir = Join-Path $releasesDir ".staging.$releaseName.$PID"
Write-Step "Downloading Codex CLI"
if ($installLayout -eq "Package") {
Invoke-WebRequestWithFallback -Metadata $checksumMetadata -OutFile $checksumPath -ExpectedDigest $checksumMetadata.Sha256 -AssetName $checksumAsset -ReleaseVersion $resolvedVersion -RequiredManifestAsset $packageAsset
$expectedPackageDigest = Get-PackageArchiveDigest -ManifestPath $checksumPath -AssetName $packageAsset
} else {
$expectedPackageDigest = $packageMetadata.Sha256
}
Invoke-WebRequestWithFallback -Metadata $packageMetadata -OutFile $archivePath -ExpectedDigest $expectedPackageDigest -AssetName $packageAsset -ReleaseVersion $resolvedVersion
New-Item -ItemType Directory -Force -Path $releasesDir | Out-Null
if (Test-Path -LiteralPath $stagingDir) {
Remove-Item -LiteralPath $stagingDir -Recurse -Force
}
New-Item -ItemType Directory -Force -Path $stagingDir | Out-Null
if ($installLayout -eq "Package") {
tar -xzf $archivePath -C $stagingDir
if (-not (Test-PackageContentsAreComplete -PackageDir $stagingDir)) {
throw "Downloaded Codex package archive did not contain the expected package layout."
}
} else {
$extractDir = Join-Path $tempDir "extract"
New-Item -ItemType Directory -Force -Path $extractDir | Out-Null
tar -xzf $archivePath -C $extractDir
$vendorRoot = Join-Path $extractDir "package/vendor/$target"
$resourcesDir = Join-Path $stagingDir "codex-resources"
New-Item -ItemType Directory -Force -Path $resourcesDir | Out-Null
$copyMap = @{
"codex/codex.exe" = "codex.exe"
"codex/codex-command-runner.exe" = "codex-resources\codex-command-runner.exe"
"codex/codex-windows-sandbox-setup.exe" = "codex-resources\codex-windows-sandbox-setup.exe"
"path/rg.exe" = "codex-resources\rg.exe"
}
foreach ($relativeSource in $copyMap.Keys) {
Copy-Item -LiteralPath (Join-Path $vendorRoot $relativeSource) -Destination (Join-Path $stagingDir $copyMap[$relativeSource])
}
if (-not (Test-LegacyPlatformNpmContentsAreComplete -PackageDir $stagingDir)) {
throw "Downloaded Codex npm archive did not contain the expected legacy platform package layout."
}
}
if (Test-Path -LiteralPath $releaseDir) {
Remove-Item -LiteralPath $releaseDir -Recurse -Force
}
Move-Item -LiteralPath $stagingDir -Destination $releaseDir
}
if (-not (Test-ReleaseIsComplete -ReleaseDir $releaseDir -ExpectedVersion $resolvedVersion -ExpectedTarget $target -Layout $installLayout)) {
throw "Installed Codex command did not report expected version $resolvedVersion."
}
New-Item -ItemType Directory -Force -Path $standaloneRoot | Out-Null
Ensure-Junction -LinkPath $currentDir -TargetPath $releaseDir -InstallerOwnedTargetPrefix $releasesDir
$visibleParent = Split-Path -Parent $visibleBinDir
$currentBinDir = if ($installLayout -eq "Package") {
Join-Path $currentDir "bin"
} else {
$currentDir
}
New-Item -ItemType Directory -Force -Path $visibleParent | Out-Null
$oldStandaloneBackup = Move-OldStandaloneBinIfApproved -VisibleBinDir $visibleBinDir -DefaultVisibleBinDir $defaultVisibleBinDir
try {
Ensure-Junction -LinkPath $visibleBinDir -TargetPath $currentBinDir -InstallerOwnedTargetPrefix $standaloneRoot
Test-VisibleCodexCommand -VisibleBinDir $visibleBinDir
} catch {
if ($null -ne $oldStandaloneBackup -and (Test-Path -LiteralPath $oldStandaloneBackup)) {
if (Test-Path -LiteralPath $visibleBinDir) {
Remove-Item -LiteralPath $visibleBinDir -Recurse -Force
}
Move-Item -LiteralPath $oldStandaloneBackup -Destination $visibleBinDir
}
throw
}
if ($null -ne $oldStandaloneBackup) {
Remove-Item -LiteralPath $oldStandaloneBackup -Recurse -Force
}
}
} finally {
Remove-Item -Recurse -Force $tempDir -ErrorAction SilentlyContinue
}
Maybe-HandleConflictingInstall -Conflict $conflictingInstall
$userPath = [Environment]::GetEnvironmentVariable("Path", "User")
$prioritizeVisibleBin = $null -ne $conflictingInstall
if ($prioritizeVisibleBin) {
$newUserPath = Prepend-PathEntry -PathValue $userPath -Entry $visibleBinDir
if ($newUserPath -cne $userPath) {
[Environment]::SetEnvironmentVariable("Path", $newUserPath, "User")
Write-Step "PATH updated for future PowerShell sessions."
} else {
Write-Step "$visibleBinDir is already first on PATH."
}
} elseif (-not (Path-Contains -PathValue $userPath -Entry $visibleBinDir)) {
if ([string]::IsNullOrWhiteSpace($userPath)) {
$newUserPath = $visibleBinDir
} else {
$newUserPath = "$visibleBinDir;$userPath"
}
[Environment]::SetEnvironmentVariable("Path", $newUserPath, "User")
Write-Step "PATH updated for future PowerShell sessions."
} elseif (Path-Contains -PathValue $env:Path -Entry $visibleBinDir) {
Write-Step "$visibleBinDir is already on PATH."
} else {
Write-Step "PATH is already configured for future PowerShell sessions."
}
if ($prioritizeVisibleBin) {
$env:Path = Prepend-PathEntry -PathValue $env:Path -Entry $visibleBinDir
} elseif (-not (Path-Contains -PathValue $env:Path -Entry $visibleBinDir)) {
if ([string]::IsNullOrWhiteSpace($env:Path)) {
$env:Path = $visibleBinDir
} else {
$env:Path = "$visibleBinDir;$env:Path"
}
}
Write-Step "Current PowerShell session: codex"
Write-Step "Future PowerShell windows: open a new PowerShell window and run: codex"
Write-Host "Codex CLI $resolvedVersion installed successfully."
$codexCommand = Join-Path $visibleBinDir "codex.exe"
if (Prompt-YesNo "Start Codex now?") {
Write-Step "Launching Codex"
& $codexCommand
}

浙公网安备 33010602011771号