From 110b30d54577f94bd63110e2fdd00ee461440787 Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Wed, 20 May 2026 11:20:11 -0700 Subject: [PATCH] install: consume Codex package archives (#23636) ## 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 prefer `codex-package-.tar.gz` plus `codex-package_SHA256SUMS` introduced in https://github.com/openai/codex/pull/23635, authenticate the checksum manifest against GitHub release metadata, verify the selected package archive against the authenticated manifest, and install the package archive directly. ## Compatibility Notes Package installs still leave a compatibility command at `current/codex` for managed daemon flows, while visible command shims point at `bin/codex` inside the package layout. Recent releases that predate package archives still publish per-platform npm artifacts, so both installers keep a legacy platform npm fallback for those versions and verify those archives against release metadata directly. Releases old enough to publish only the single root `codex-npm-.tgz` archive are intentionally out of scope. The installers fail clearly when neither package archives nor per-platform npm archives are present. On Windows, the runtime helper lookups now recognize package-layout installs where `codex.exe` runs from `bin/`, so `codex-command-runner.exe` and `codex-windows-sandbox-setup.exe` resolve from the top-level `codex-resources/` directory. The direct-sibling and older sibling-resource fallbacks are preserved. ## Test plan - `sh -n scripts/install/install.sh` - `bash -n scripts/install/install.sh` - `pwsh -NoProfile -Command '$tokens=$null; $errors=$null; $null = [System.Management.Automation.Language.Parser]::ParseFile("scripts/install/install.ps1", [ref]$tokens, [ref]$errors); if ($errors.Count) { $errors | Format-List *; exit 1 }'` - `HOME="$home_dir" CODEX_HOME="$tmp_dir/codex-home" CODEX_INSTALL_DIR="$bin_dir" PATH="$bin_dir:$PATH" sh scripts/install/install.sh --release 0.125.0` - Verified the 0.125.0 isolated install leaves the visible command pointed at `current/codex` and includes the legacy `codex-resources/rg` payload. - `cargo test -p codex-windows-sandbox` - `just fix -p codex-windows-sandbox` --- [//]: # (BEGIN SAPLING FOOTER) Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/openai/codex/pull/23636). * #23638 * #23637 * __->__ #23636 --- .../src/helper_materialization.rs | 57 ++++- codex-rs/windows-sandbox-rs/src/setup.rs | 68 ++++-- scripts/install/install.ps1 | 181 +++++++++++++--- scripts/install/install.sh | 199 +++++++++++++++--- 4 files changed, 426 insertions(+), 79 deletions(-) diff --git a/codex-rs/windows-sandbox-rs/src/helper_materialization.rs b/codex-rs/windows-sandbox-rs/src/helper_materialization.rs index 8f0b5cb83b..3540f4c463 100644 --- a/codex-rs/windows-sandbox-rs/src/helper_materialization.rs +++ b/codex-rs/windows-sandbox-rs/src/helper_materialization.rs @@ -2,6 +2,7 @@ use anyhow::Context; use anyhow::Result; use anyhow::anyhow; use std::collections::HashMap; +use std::ffi::OsStr; use std::fs; use std::io::Write; use std::path::Path; @@ -15,6 +16,7 @@ use crate::logging::log_note; use crate::sandbox_bin_dir; const DEV_BUILD_VERSION_SENTINEL: &str = "0.0.0"; +const BIN_DIRNAME: &str = "bin"; const RESOURCES_DIRNAME: &str = "codex-resources"; #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] @@ -188,12 +190,21 @@ fn sibling_source_path(kind: HelperExecutable) -> Result { fn source_path_for_exe(exe: &Path, file_name: &str) -> Option { let dir = exe.parent()?; let direct_candidate = dir.join(file_name); - if direct_candidate.exists() { + if direct_candidate.is_file() { return Some(direct_candidate); } + if dir.file_name() == Some(OsStr::new(BIN_DIRNAME)) + && let Some(package_dir) = dir.parent() + { + let package_resource_candidate = package_dir.join(RESOURCES_DIRNAME).join(file_name); + if package_resource_candidate.is_file() { + return Some(package_resource_candidate); + } + } + let resource_candidate = dir.join(RESOURCES_DIRNAME).join(file_name); - resource_candidate.exists().then_some(resource_candidate) + resource_candidate.is_file().then_some(resource_candidate) } fn helper_destination_for_source( @@ -345,6 +356,7 @@ fn destination_is_fresh(source: &Path, destination: &Path) -> Result { #[cfg(test)] mod tests { + use super::BIN_DIRNAME; use super::CopyOutcome; use super::DEV_BUILD_VERSION_SENTINEL; use super::HelperExecutable; @@ -463,6 +475,47 @@ mod tests { assert_eq!(resolved, helper); } + #[test] + fn helper_source_lookup_checks_package_resource_dir_for_bin_exe() { + let tmp = TempDir::new().expect("tempdir"); + let package_dir = tmp.path().join("package"); + let bin_dir = package_dir.join(BIN_DIRNAME); + let resources_dir = package_dir.join(RESOURCES_DIRNAME); + fs::create_dir_all(&bin_dir).expect("create bin dir"); + fs::create_dir_all(&resources_dir).expect("create resources dir"); + let exe = bin_dir.join("codex.exe"); + let helper = resources_dir.join("codex-command-runner.exe"); + fs::write(&exe, b"codex").expect("write exe"); + fs::write(&helper, b"runner").expect("write helper"); + + let resolved = source_path_for_exe(&exe, /*file_name*/ "codex-command-runner.exe") + .expect("helper path"); + + assert_eq!(resolved, helper); + } + + #[test] + fn helper_source_lookup_prefers_package_resource_dir_over_bin_resource_dir() { + let tmp = TempDir::new().expect("tempdir"); + let package_dir = tmp.path().join("package"); + let bin_dir = package_dir.join(BIN_DIRNAME); + let package_resources_dir = package_dir.join(RESOURCES_DIRNAME); + let bin_resources_dir = bin_dir.join(RESOURCES_DIRNAME); + fs::create_dir_all(&package_resources_dir).expect("create package resources dir"); + fs::create_dir_all(&bin_resources_dir).expect("create bin resources dir"); + let exe = bin_dir.join("codex.exe"); + let package_helper = package_resources_dir.join("codex-command-runner.exe"); + let bin_helper = bin_resources_dir.join("codex-command-runner.exe"); + fs::write(&exe, b"codex").expect("write exe"); + fs::write(&package_helper, b"package runner").expect("write package helper"); + fs::write(&bin_helper, b"bin runner").expect("write bin helper"); + + let resolved = source_path_for_exe(&exe, /*file_name*/ "codex-command-runner.exe") + .expect("helper path"); + + assert_eq!(resolved, package_helper); + } + #[test] fn helper_source_lookup_prefers_direct_sibling_over_resource_dir() { let tmp = TempDir::new().expect("tempdir"); diff --git a/codex-rs/windows-sandbox-rs/src/setup.rs b/codex-rs/windows-sandbox-rs/src/setup.rs index 6f4dd86019..3da5933ee2 100644 --- a/codex-rs/windows-sandbox-rs/src/setup.rs +++ b/codex-rs/windows-sandbox-rs/src/setup.rs @@ -3,6 +3,7 @@ use serde::Serialize; use std::collections::BTreeSet; use std::collections::HashMap; use std::collections::HashSet; +use std::ffi::OsStr; use std::ffi::c_void; use std::os::windows::process::CommandExt; use std::path::Path; @@ -39,9 +40,11 @@ use windows_sys::Win32::Security::SECURITY_NT_AUTHORITY; pub const SETUP_VERSION: u32 = 5; pub const OFFLINE_USERNAME: &str = "CodexSandboxOffline"; pub const ONLINE_USERNAME: &str = "CodexSandboxOnline"; +const BIN_DIRNAME: &str = "bin"; const ERROR_CANCELLED: u32 = 1223; const SECURITY_BUILTIN_DOMAIN_RID: u32 = 0x0000_0020; const DOMAIN_ALIAS_RID_ADMINS: u32 = 0x0000_0220; +const RESOURCES_DIRNAME: &str = "codex-resources"; const USERPROFILE_ROOT_EXCLUSIONS: &[&str] = &[ ".ssh", ".tsh", @@ -579,26 +582,40 @@ fn quote_arg(arg: &str) -> String { fn find_setup_exe() -> PathBuf { if let Ok(exe) = std::env::current_exe() - && let Some(dir) = exe.parent() + && let Some(setup_exe) = find_setup_exe_for_current_exe(&exe) { - let candidate = dir.join("codex-windows-sandbox-setup.exe"); - if candidate.exists() { - return candidate; - } - - // Standalone installs keep Windows helper binaries under - // `codex-resources/` next to `codex.exe`, so elevation needs to probe - // that sibling folder before falling back to PATH. - let resource_candidate = dir - .join("codex-resources") - .join("codex-windows-sandbox-setup.exe"); - if resource_candidate.exists() { - return resource_candidate; - } + return setup_exe; } PathBuf::from("codex-windows-sandbox-setup.exe") } +fn find_setup_exe_for_current_exe(exe: &Path) -> Option { + let dir = exe.parent()?; + let candidate = dir.join("codex-windows-sandbox-setup.exe"); + if candidate.is_file() { + return Some(candidate); + } + + if dir.file_name() == Some(OsStr::new(BIN_DIRNAME)) + && let Some(package_dir) = dir.parent() + { + let package_resource_candidate = package_dir + .join(RESOURCES_DIRNAME) + .join("codex-windows-sandbox-setup.exe"); + if package_resource_candidate.is_file() { + return Some(package_resource_candidate); + } + } + + // Older standalone installs keep Windows helper binaries under + // `codex-resources/` next to `codex.exe`, so elevation still probes that + // sibling folder before falling back to PATH. + let resource_candidate = dir + .join(RESOURCES_DIRNAME) + .join("codex-windows-sandbox-setup.exe"); + resource_candidate.is_file().then_some(resource_candidate) +} + fn report_helper_failure( codex_home: &Path, cleared_report: bool, @@ -959,8 +976,11 @@ fn filter_sensitive_write_roots(mut roots: Vec, codex_home: &Path) -> V #[cfg(test)] mod tests { + use super::BIN_DIRNAME; + use super::RESOURCES_DIRNAME; use super::WINDOWS_PLATFORM_DEFAULT_READ_ROOTS; use super::build_payload_roots; + use super::find_setup_exe_for_current_exe; use super::gather_legacy_full_read_roots; use super::gather_read_roots; use super::loopback_proxy_port_from_url; @@ -1000,6 +1020,24 @@ mod tests { ); } + #[test] + fn setup_exe_lookup_checks_package_resource_dir_for_bin_exe() { + let tmp = TempDir::new().expect("tempdir"); + let package_dir = tmp.path().join("package"); + let bin_dir = package_dir.join(BIN_DIRNAME); + let resources_dir = package_dir.join(RESOURCES_DIRNAME); + fs::create_dir_all(&bin_dir).expect("create bin dir"); + fs::create_dir_all(&resources_dir).expect("create resources dir"); + let exe = bin_dir.join("codex.exe"); + let setup_exe = resources_dir.join("codex-windows-sandbox-setup.exe"); + fs::write(&exe, b"codex").expect("write exe"); + fs::write(&setup_exe, b"setup").expect("write setup"); + + let resolved = find_setup_exe_for_current_exe(&exe).expect("setup exe"); + + assert_eq!(resolved, setup_exe); + } + #[test] fn loopback_proxy_url_parsing_rejects_non_loopback_and_zero_port() { assert_eq!( diff --git a/scripts/install/install.ps1 b/scripts/install/install.ps1 index ed4a3e1a32..8ebc4fbb82 100644 --- a/scripts/install/install.ps1 +++ b/scripts/install/install.ps1 @@ -55,7 +55,7 @@ function Normalize-Version { return $RawVersion } -function Get-ReleaseAssetMetadata { +function Find-ReleaseAssetMetadata { param( [string]$AssetName, [string]$ResolvedVersion @@ -64,7 +64,7 @@ function Get-ReleaseAssetMetadata { $release = Invoke-RestMethod -Uri "https://api.github.com/repos/openai/codex/releases/tags/rust-v$ResolvedVersion" $asset = $release.assets | Where-Object { $_.name -eq $AssetName } | Select-Object -First 1 if ($null -eq $asset) { - throw "Could not find release asset $AssetName for Codex $ResolvedVersion." + return $null } $digestMatch = [regex]::Match([string]$asset.digest, "^sha256:([0-9a-fA-F]{64})$") @@ -78,6 +78,20 @@ function Get-ReleaseAssetMetadata { } } +function Get-ReleaseAssetMetadata { + param( + [string]$AssetName, + [string]$ResolvedVersion + ) + + $metadata = Find-ReleaseAssetMetadata -AssetName $AssetName -ResolvedVersion $ResolvedVersion + if ($null -eq $metadata) { + throw "Could not find release asset $AssetName for Codex $ResolvedVersion." + } + + return $metadata +} + function Test-ArchiveDigest { param( [string]$ArchivePath, @@ -86,10 +100,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 +221,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,14 +485,37 @@ function Ensure-Junction { throw "Refusing to replace file at $LinkPath with a junction." } -function Test-ReleaseIsComplete { +function Test-PackageContentsAreComplete { param( - [string]$ReleaseDir, - [string]$ExpectedVersion, - [string]$ExpectedTarget + [string]$PackageDir ) - if (-not (Test-Path -LiteralPath $ReleaseDir -PathType Container)) { + 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-LegacyPlatformNpmContentsAreComplete { + param( + [string]$PackageDir + ) + + if (-not (Test-Path -LiteralPath $PackageDir -PathType Container)) { return $false } @@ -467,11 +526,38 @@ function Test-ReleaseIsComplete { "codex-resources\rg.exe" ) foreach ($name in $expectedFiles) { - if (-not (Test-Path -LiteralPath (Join-Path $ReleaseDir $name) -PathType Leaf)) { + 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 + } + } + "LegacyPlatformNpm" { + if (-not (Test-LegacyPlatformNpmContentsAreComplete -PackageDir $ReleaseDir)) { + return $false + } + } + default { + throw "Unknown Codex installer layout: $Layout" + } + } + return (Split-Path -Leaf $ReleaseDir) -eq "$ExpectedVersion-$ExpectedTarget" } @@ -637,7 +723,21 @@ 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" +$packageMetadata = Find-ReleaseAssetMetadata -AssetName $packageAsset -ResolvedVersion $resolvedVersion +$checksumMetadata = Find-ReleaseAssetMetadata -AssetName $checksumAsset -ResolvedVersion $resolvedVersion +$installLayout = "Package" +if ($null -eq $packageMetadata -or $null -eq $checksumMetadata) { + $packageAsset = "codex-npm-$npmTag-$resolvedVersion.tgz" + $packageMetadata = Find-ReleaseAssetMetadata -AssetName $packageAsset -ResolvedVersion $resolvedVersion + if ($null -ne $packageMetadata) { + $installLayout = "LegacyPlatformNpm" + } else { + throw "Could not find Codex package or platform npm release assets for Codex $resolvedVersion." + } + $checksumMetadata = $null +} $tempDir = Join-Path ([System.IO.Path]::GetTempPath()) ("codex-install-" + [System.Guid]::NewGuid().ToString("N")) New-Item -ItemType Directory -Force -Path $tempDir | Out-Null @@ -645,40 +745,58 @@ try { Invoke-WithInstallLock -LockPath $lockPath -Script { Remove-StaleInstallArtifacts -ReleasesDir $releasesDir - if (-not (Test-ReleaseIsComplete -ReleaseDir $releaseDir -ExpectedVersion $resolvedVersion -ExpectedTarget $target)) { + 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 - $extractDir = Join-Path $tempDir "extract" + $checksumPath = Join-Path $tempDir $checksumAsset $stagingDir = Join-Path $releasesDir ".staging.$releaseName.$PID" - $assetMetadata = Get-ReleaseAssetMetadata -AssetName $packageAsset -ResolvedVersion $resolvedVersion Write-Step "Downloading Codex CLI" - Invoke-WebRequest -Uri $assetMetadata.Url -OutFile $archivePath - Test-ArchiveDigest -ArchivePath $archivePath -ExpectedDigest $assetMetadata.Sha256 + if ($installLayout -eq "Package") { + Invoke-WebRequest -Uri $checksumMetadata.Url -OutFile $checksumPath + Test-ArchiveDigest -ArchivePath $checksumPath -ExpectedDigest $checksumMetadata.Sha256 + $expectedPackageDigest = Get-PackageArchiveDigest -ManifestPath $checksumPath -AssetName $packageAsset + } else { + $expectedPackageDigest = $packageMetadata.Sha256 + } + 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 + 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" - } + $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]) + 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) { @@ -691,10 +809,15 @@ try { 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 $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)) { diff --git a/scripts/install/install.sh b/scripts/install/install.sh index 2fc585d7e9..cc4fc91345 100755 --- a/scripts/install/install.sh +++ b/scripts/install/install.sh @@ -120,24 +120,29 @@ release_metadata_url() { printf 'https://api.github.com/repos/openai/codex/releases/tags/rust-v%s\n' "$resolved_version" } -release_asset_digest() { +release_asset_digest_or_empty() { asset="$1" resolved_version="$2" release_json="$(download_text "$(release_metadata_url "$resolved_version")")" digest="$(printf '%s\n' "$release_json" | awk -v asset="$asset" ' - { - if ($0 ~ "\"name\":[[:space:]]*\"" asset "\"") { + /"name":[[:space:]]*"[^"]+"/ { + name = $0 + sub(/^.*"name":[[:space:]]*"/, "", name) + sub(/".*$/, "", name) + if (name == asset) { in_asset = 1 asset_depth = depth } + } - if (in_asset && /"digest":[[:space:]]*"[^"]+"/) { - sub(/^.*"digest":[[:space:]]*"/, "") - sub(/".*$/, "") - digest = $0 - } + in_asset && /"digest":[[:space:]]*"[^"]+"/ { + digest = $0 + sub(/^.*"digest":[[:space:]]*"/, "", digest) + sub(/".*$/, "", digest) + } + { line = $0 opens = gsub(/\{/, "{", line) closes = gsub(/\}/, "}", line) @@ -147,6 +152,7 @@ release_asset_digest() { in_asset = 0 } } + END { if (digest != "") { print digest @@ -159,12 +165,56 @@ release_asset_digest() { printf '%s\n' "${digest#sha256:}" ;; *) - echo "Could not find SHA-256 digest for release asset $asset." >&2 - exit 1 + return 1 ;; esac } +release_asset_exists() { + asset="$1" + resolved_version="$2" + + release_asset_digest_or_empty "$asset" "$resolved_version" >/dev/null 2>&1 +} + +release_asset_digest() { + asset="$1" + resolved_version="$2" + + digest="$(release_asset_digest_or_empty "$asset" "$resolved_version" || true)" + if [ -z "$digest" ]; then + echo "Could not find SHA-256 digest for release asset $asset." >&2 + exit 1 + fi + + printf '%s\n' "$digest" +} + +package_archive_digest() { + asset="$1" + manifest_path="$2" + + digest="$(awk -v asset="$asset" ' + $2 == asset && $1 ~ /^[0-9a-fA-F]{64}$/ { + print tolower($1) + found = 1 + exit + } + END { + if (!found) { + exit 1 + } + } + ' "$manifest_path" 2>/dev/null || true)" + + 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() { path="$1" @@ -193,7 +243,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 expected digest." >&2 echo "expected: $expected_digest" >&2 echo "actual: $actual_digest" >&2 exit 1 @@ -441,6 +491,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" @@ -584,18 +640,43 @@ handle_conflicting_install() { fi } -install_release() { +install_package_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" + 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 + ln -sf "bin/codex" "$stage_release/codex" + + if [ -e "$release_dir" ] || [ -L "$release_dir" ]; then + rm -rf "$release_dir" + fi + mv "$stage_release" "$release_dir" +} + +install_legacy_platform_npm_release() { + release_dir="$1" + archive_path="$2" + target="$3" + stage_release="$RELEASES_DIR/.staging.$(basename "$release_dir").$$" + extract_dir="$tmp_dir/extract" + vendor_root="$extract_dir/package/vendor/$target" + + mkdir -p "$RELEASES_DIR" + rm -rf "$stage_release" "$extract_dir" + mkdir -p "$stage_release/codex-resources" "$extract_dir" + tar -xzf "$archive_path" -C "$extract_dir" + 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" + chmod 0755 "$stage_release/codex" "$stage_release/codex-resources/rg" if [ -f "$vendor_root/codex-resources/bwrap" ]; then cp "$vendor_root/codex-resources/bwrap" "$stage_release/codex-resources/bwrap" chmod 0755 "$stage_release/codex-resources/bwrap" @@ -611,15 +692,34 @@ release_dir_is_complete() { release_dir="$1" expected_version="$2" expected_target="$3" + layout="$4" [ -d "$release_dir" ] && - [ -x "$release_dir/codex" ] && - [ -x "$release_dir/codex-resources/rg" ] && - [ "$(basename "$release_dir")" = "$expected_version-$expected_target" ] && - case "$expected_target" in - *linux*) [ -x "$release_dir/codex-resources/bwrap" ] ;; - *) true ;; - esac + [ "$(basename "$release_dir")" = "$expected_version-$expected_target" ] || + return 1 + + case "$layout" in + package) + [ -f "$release_dir/codex-package.json" ] && + [ -x "$release_dir/bin/codex" ] && + [ -x "$release_dir/codex" ] && + [ -x "$release_dir/codex-path/rg" ] || + return 1 + ;; + legacy-platform-npm) + [ -x "$release_dir/codex" ] && + [ -x "$release_dir/codex-resources/rg" ] || + return 1 + ;; + *) + return 1 + ;; + esac + + case "$layout:$expected_target" in + package:*linux* | legacy-platform-npm:*linux*) [ -x "$release_dir/codex-resources/bwrap" ] ;; + *) true ;; + esac } update_current_link() { @@ -629,11 +729,23 @@ update_current_link() { replace_path_with_symlink "$CURRENT_LINK" "$release_dir" "$tmp_link" } +release_codex_relative_path() { + release_dir="$1" + + if [ -x "$release_dir/bin/codex" ]; then + printf 'bin/codex\n' + else + printf 'codex\n' + fi +} + update_visible_command() { + release_dir="$1" mkdir -p "$BIN_DIR" tmp_link="$BIN_DIR/.codex.$$" + codex_relative_path="$(release_codex_relative_path "$release_dir")" - replace_path_with_symlink "$BIN_PATH" "$CURRENT_LINK/codex" "$tmp_link" + replace_path_with_symlink "$BIN_PATH" "$CURRENT_LINK/$codex_relative_path" "$tmp_link" } verify_visible_command() { @@ -700,8 +812,21 @@ else fi resolved_version="$(resolve_version)" -asset="codex-npm-$npm_tag-$resolved_version.tgz" +package_asset="codex-package-$vendor_target.tar.gz" +checksum_asset="codex-package_SHA256SUMS" +if release_asset_exists "$package_asset" "$resolved_version" && + release_asset_exists "$checksum_asset" "$resolved_version"; then + install_layout="package" + asset="$package_asset" +elif release_asset_exists "codex-npm-$npm_tag-$resolved_version.tgz" "$resolved_version"; then + install_layout="legacy-platform-npm" + asset="codex-npm-$npm_tag-$resolved_version.tgz" +else + echo "Could not find Codex package or platform npm release assets for Codex $resolved_version." >&2 + exit 1 +fi 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)" @@ -730,27 +855,35 @@ trap cleanup EXIT INT TERM acquire_install_lock cleanup_stale_install_artifacts -if ! release_dir_is_complete "$release_dir" "$resolved_version" "$vendor_target"; then +if ! release_dir_is_complete "$release_dir" "$resolved_version" "$vendor_target" "$install_layout"; then if [ -e "$release_dir" ] || [ -L "$release_dir" ]; then warn "Found incomplete existing release at $release_dir; reinstalling." 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")" + if [ "$install_layout" = "package" ]; then + checksum_digest="$(release_asset_digest "$checksum_asset" "$resolved_version")" + download_file "$checksum_url" "$checksum_path" + verify_archive_digest "$checksum_path" "$checksum_digest" + expected_digest="$(package_archive_digest "$asset" "$checksum_path")" + else + expected_digest="$(release_asset_digest "$asset" "$resolved_version")" + fi 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" + if [ "$install_layout" = "package" ]; then + install_package_release "$release_dir" "$archive_path" + else + install_legacy_platform_npm_release "$release_dir" "$archive_path" "$vendor_target" + fi fi update_current_link "$release_dir" -update_visible_command +update_visible_command "$release_dir" add_to_path verify_visible_command release_install_lock