install: consume Codex package archives

## Summary

Standalone installs should exercise the same canonical package archive layout that release builds produce, rather than unpacking npm platform packages and reconstructing a parallel install tree.

This updates `install.sh` and `install.ps1` to download `codex-package-<target>.tar.gz` plus `codex-package_SHA256SUMS`, verify the selected archive against the manifest, install the archive directly, and point visible command shims at `bin/codex` inside the package layout. Existing installs are still version-detected through the old root-level `codex` path as a fallback during migration.

## Test plan

- `sh -n scripts/install/install.sh`
- `pwsh -NoProfile -Command '<PowerShell parser smoke test for scripts/install/install.ps1>'`
This commit is contained in:
Michael Bolin
2026-05-19 23:22:29 -07:00
parent 10fe8f900d
commit 9dd45d0217
2 changed files with 103 additions and 103 deletions

View File

@@ -86,10 +86,27 @@ function Test-ArchiveDigest {
$actualDigest = (Get-FileHash -LiteralPath $ArchivePath -Algorithm SHA256).Hash.ToLowerInvariant()
if ($actualDigest -ne $ExpectedDigest) {
throw "Downloaded Codex archive checksum did not match release metadata. Expected $ExpectedDigest but got $actualDigest."
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,
@@ -190,6 +207,11 @@ function Get-CurrentInstalledVersion {
[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
@@ -449,6 +471,31 @@ function Ensure-Junction {
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",
"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-ReleaseIsComplete {
param(
[string]$ReleaseDir,
@@ -456,22 +503,10 @@ function Test-ReleaseIsComplete {
[string]$ExpectedTarget
)
if (-not (Test-Path -LiteralPath $ReleaseDir -PathType Container)) {
if (-not (Test-PackageContentsAreComplete -PackageDir $ReleaseDir)) {
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 $ReleaseDir $name) -PathType Leaf)) {
return $false
}
}
return (Split-Path -Leaf $ReleaseDir) -eq "$ExpectedVersion-$ExpectedTarget"
}
@@ -584,17 +619,14 @@ if (-not [Environment]::Is64BitOperatingSystem) {
$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"
@@ -637,7 +669,8 @@ Write-Step "Resolved version: $resolvedVersion"
$conflictingInstall = Get-ConflictingInstall -VisibleBinDir $visibleBinDir
$oldStandaloneBackup = $null
$packageAsset = "codex-npm-$npmTag-$resolvedVersion.tgz"
$packageAsset = "codex-package-$target.tar.gz"
$checksumAsset = "codex-package_SHA256SUMS"
$tempDir = Join-Path ([System.IO.Path]::GetTempPath()) ("codex-install-" + [System.Guid]::NewGuid().ToString("N"))
New-Item -ItemType Directory -Force -Path $tempDir | Out-Null
@@ -651,34 +684,26 @@ try {
}
$archivePath = Join-Path $tempDir $packageAsset
$extractDir = Join-Path $tempDir "extract"
$checksumPath = Join-Path $tempDir $checksumAsset
$stagingDir = Join-Path $releasesDir ".staging.$releaseName.$PID"
$assetMetadata = Get-ReleaseAssetMetadata -AssetName $packageAsset -ResolvedVersion $resolvedVersion
$packageMetadata = Get-ReleaseAssetMetadata -AssetName $packageAsset -ResolvedVersion $resolvedVersion
$checksumMetadata = Get-ReleaseAssetMetadata -AssetName $checksumAsset -ResolvedVersion $resolvedVersion
Write-Step "Downloading Codex CLI"
Invoke-WebRequest -Uri $assetMetadata.Url -OutFile $archivePath
Test-ArchiveDigest -ArchivePath $archivePath -ExpectedDigest $assetMetadata.Sha256
Invoke-WebRequest -Uri $checksumMetadata.Url -OutFile $checksumPath
Test-ArchiveDigest -ArchivePath $checksumPath -ExpectedDigest $checksumMetadata.Sha256
$expectedPackageDigest = Get-PackageArchiveDigest -ManifestPath $checksumPath -AssetName $packageAsset
Invoke-WebRequest -Uri $packageMetadata.Url -OutFile $archivePath
Test-ArchiveDigest -ArchivePath $archivePath -ExpectedDigest $expectedPackageDigest
New-Item -ItemType Directory -Force -Path $extractDir | Out-Null
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
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])
tar -xzf $archivePath -C $stagingDir
if (-not (Test-PackageContentsAreComplete -PackageDir $stagingDir)) {
throw "Downloaded Codex package archive did not contain the expected package layout."
}
if (Test-Path -LiteralPath $releaseDir) {
@@ -691,10 +716,11 @@ try {
Ensure-Junction -LinkPath $currentDir -TargetPath $releaseDir -InstallerOwnedTargetPrefix $releasesDir
$visibleParent = Split-Path -Parent $visibleBinDir
$currentBinDir = Join-Path $currentDir "bin"
New-Item -ItemType Directory -Force -Path $visibleParent | Out-Null
$oldStandaloneBackup = Move-OldStandaloneBinIfApproved -VisibleBinDir $visibleBinDir -DefaultVisibleBinDir $defaultVisibleBinDir
try {
Ensure-Junction -LinkPath $visibleBinDir -TargetPath $currentDir -InstallerOwnedTargetPrefix $standaloneRoot
Ensure-Junction -LinkPath $visibleBinDir -TargetPath $currentBinDir -InstallerOwnedTargetPrefix $standaloneRoot
Test-VisibleCodexCommand -VisibleBinDir $visibleBinDir
} catch {
if ($null -ne $oldStandaloneBackup -and (Test-Path -LiteralPath $oldStandaloneBackup)) {

View File

@@ -114,55 +114,29 @@ release_url_for_asset() {
printf 'https://github.com/openai/codex/releases/download/rust-v%s/%s\n' "$resolved_version" "$asset"
}
release_metadata_url() {
resolved_version="$1"
printf 'https://api.github.com/repos/openai/codex/releases/tags/rust-v%s\n' "$resolved_version"
}
release_asset_digest() {
package_archive_digest() {
asset="$1"
resolved_version="$2"
release_json="$(download_text "$(release_metadata_url "$resolved_version")")"
manifest_path="$2"
digest="$(printf '%s\n' "$release_json" | awk -v asset="$asset" '
{
if ($0 ~ "\"name\":[[:space:]]*\"" asset "\"") {
in_asset = 1
asset_depth = depth
}
if (in_asset && /"digest":[[:space:]]*"[^"]+"/) {
sub(/^.*"digest":[[:space:]]*"/, "")
sub(/".*$/, "")
digest = $0
}
line = $0
opens = gsub(/\{/, "{", line)
closes = gsub(/\}/, "}", line)
depth += opens - closes
if (in_asset && depth < asset_depth) {
in_asset = 0
}
digest="$(awk -v asset="$asset" '
$2 == asset && $1 ~ /^[0-9a-fA-F]{64}$/ {
print tolower($1)
found = 1
exit
}
END {
if (digest != "") {
print digest
if (!found) {
exit 1
}
}
')"
' "$manifest_path" 2>/dev/null || true)"
case "$digest" in
sha256:????????????????????????????????????????????????????????????????)
printf '%s\n' "${digest#sha256:}"
;;
*)
echo "Could not find SHA-256 digest for release asset $asset." >&2
exit 1
;;
esac
if [ -z "$digest" ]; then
echo "Could not find SHA-256 digest for $asset in codex-package_SHA256SUMS." >&2
exit 1
fi
printf '%s\n' "$digest"
}
file_sha256() {
@@ -193,7 +167,7 @@ verify_archive_digest() {
actual_digest="$(file_sha256 "$archive_path")"
if [ "$actual_digest" != "$expected_digest" ]; then
echo "Downloaded Codex archive checksum did not match release metadata." >&2
echo "Downloaded Codex archive checksum did not match release checksum manifest." >&2
echo "expected: $expected_digest" >&2
echo "actual: $actual_digest" >&2
exit 1
@@ -441,6 +415,12 @@ version_from_binary() {
}
current_installed_version() {
version="$(version_from_binary "$CURRENT_LINK/bin/codex" || true)"
if [ -n "$version" ]; then
printf '%s\n' "$version"
return 0
fi
version="$(version_from_binary "$CURRENT_LINK/codex" || true)"
if [ -n "$version" ]; then
printf '%s\n' "$version"
@@ -586,18 +566,15 @@ handle_conflicting_install() {
install_release() {
release_dir="$1"
vendor_root="$2"
archive_path="$2"
stage_release="$RELEASES_DIR/.staging.$(basename "$release_dir").$$"
mkdir -p "$RELEASES_DIR"
rm -rf "$stage_release"
mkdir -p "$stage_release/codex-resources"
cp "$vendor_root/codex/codex" "$stage_release/codex"
cp "$vendor_root/path/rg" "$stage_release/codex-resources/rg"
chmod 0755 "$stage_release/codex"
chmod 0755 "$stage_release/codex-resources/rg"
if [ -f "$vendor_root/codex-resources/bwrap" ]; then
cp "$vendor_root/codex-resources/bwrap" "$stage_release/codex-resources/bwrap"
mkdir -p "$stage_release"
tar -xzf "$archive_path" -C "$stage_release"
chmod 0755 "$stage_release/bin/codex" "$stage_release/codex-path/rg"
if [ -f "$stage_release/codex-resources/bwrap" ]; then
chmod 0755 "$stage_release/codex-resources/bwrap"
fi
@@ -613,8 +590,9 @@ release_dir_is_complete() {
expected_target="$3"
[ -d "$release_dir" ] &&
[ -x "$release_dir/codex" ] &&
[ -x "$release_dir/codex-resources/rg" ] &&
[ -f "$release_dir/codex-package.json" ] &&
[ -x "$release_dir/bin/codex" ] &&
[ -x "$release_dir/codex-path/rg" ] &&
[ "$(basename "$release_dir")" = "$expected_version-$expected_target" ] &&
case "$expected_target" in
*linux*) [ -x "$release_dir/codex-resources/bwrap" ] ;;
@@ -633,7 +611,7 @@ update_visible_command() {
mkdir -p "$BIN_DIR"
tmp_link="$BIN_DIR/.codex.$$"
replace_path_with_symlink "$BIN_PATH" "$CURRENT_LINK/codex" "$tmp_link"
replace_path_with_symlink "$BIN_PATH" "$CURRENT_LINK/bin/codex" "$tmp_link"
}
verify_visible_command() {
@@ -679,29 +657,27 @@ fi
if [ "$os" = "darwin" ]; then
if [ "$arch" = "aarch64" ]; then
npm_tag="darwin-arm64"
vendor_target="aarch64-apple-darwin"
platform_label="macOS (Apple Silicon)"
else
npm_tag="darwin-x64"
vendor_target="x86_64-apple-darwin"
platform_label="macOS (Intel)"
fi
else
if [ "$arch" = "aarch64" ]; then
npm_tag="linux-arm64"
vendor_target="aarch64-unknown-linux-musl"
platform_label="Linux (ARM64)"
else
npm_tag="linux-x64"
vendor_target="x86_64-unknown-linux-musl"
platform_label="Linux (x64)"
fi
fi
resolved_version="$(resolve_version)"
asset="codex-npm-$npm_tag-$resolved_version.tgz"
asset="codex-package-$vendor_target.tar.gz"
checksum_asset="codex-package_SHA256SUMS"
download_url="$(release_url_for_asset "$asset" "$resolved_version")"
checksum_url="$(release_url_for_asset "$checksum_asset" "$resolved_version")"
release_name="$resolved_version-$vendor_target"
release_dir="$RELEASES_DIR/$release_name"
current_version="$(current_installed_version)"
@@ -736,18 +712,16 @@ if ! release_dir_is_complete "$release_dir" "$resolved_version" "$vendor_target"
fi
archive_path="$tmp_dir/$asset"
extract_dir="$tmp_dir/extract"
checksum_path="$tmp_dir/$checksum_asset"
step "Downloading Codex CLI"
expected_digest="$(release_asset_digest "$asset" "$resolved_version")"
download_file "$checksum_url" "$checksum_path"
expected_digest="$(package_archive_digest "$asset" "$checksum_path")"
download_file "$download_url" "$archive_path"
verify_archive_digest "$archive_path" "$expected_digest"
mkdir -p "$extract_dir"
tar -xzf "$archive_path" -C "$extract_dir"
step "Installing standalone package to $release_dir"
install_release "$release_dir" "$extract_dir/package/vendor/$vendor_target"
install_release "$release_dir" "$archive_path"
fi
update_current_link "$release_dir"
update_visible_command