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-<target>.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-<version>.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
This commit is contained in:
Michael Bolin
2026-05-20 11:20:11 -07:00
committed by GitHub
parent c5bd131567
commit 110b30d545
4 changed files with 426 additions and 79 deletions

View File

@@ -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<PathBuf> {
fn source_path_for_exe(exe: &Path, file_name: &str) -> Option<PathBuf> {
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<bool> {
#[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");

View File

@@ -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<PathBuf> {
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<PathBuf>, 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!(

View File

@@ -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)) {

View File

@@ -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