#!/bin/sh set -eu RELEASE="${CODEX_RELEASE:-latest}" NON_INTERACTIVE="${CODEX_NON_INTERACTIVE:-false}" DAEMON_ONLY="${CODEX_INSTALL_DAEMON_ONLY:-0}" DEFAULT_PREFER_RELEASES_OPENAI_COM="true" PREFER_RELEASES_OPENAI_COM="${CODEX_INSTALLER_USE_RELEASES_OPENAI_COM:-$DEFAULT_PREFER_RELEASES_OPENAI_COM}" RELEASES_BASE_URL="https://releases.openai.com/codex" RELEASES_CONNECT_TIMEOUT=10 RELEASES_METADATA_TIMEOUT=30 RELEASES_ASSET_TIMEOUT=300 release_source="github" BIN_DIR="${CODEX_INSTALL_DIR:-$HOME/.local/bin}" BIN_PATH="$BIN_DIR/codex" CODE_MODE_HOST_BIN_PATH="$BIN_DIR/codex-code-mode-host" CODEX_HOME_DIR="${CODEX_HOME:-$HOME/.codex}" STANDALONE_ROOT="$CODEX_HOME_DIR/packages/standalone" if [ "$DAEMON_ONLY" = "1" ]; then STANDALONE_ROOT="$CODEX_HOME_DIR/packages/app-server-daemon" fi RELEASES_DIR="$STANDALONE_ROOT/releases" CURRENT_LINK="$STANDALONE_ROOT/current" if [ "${CODEX_INSTALL_DEFER_SELECTION:-0}" = "1" ]; then if [ "$DAEMON_ONLY" != "1" ]; then echo "Deferred selection requires a daemon-only installation." >&2 exit 1 fi CURRENT_LINK="$STANDALONE_ROOT/.migration-current" fi AUTO_UPDATE_VERSION="$STANDALONE_ROOT/auto-update-version" LOCK_FILE="$STANDALONE_ROOT/install.lock" LOCK_DIR="$STANDALONE_ROOT/install.lock.d" LOCK_STALE_AFTER_SECS=600 path_action="already" path_profile="" conflict_manager="" conflict_path="" lock_kind="" tmp_dir="" step() { printf '==> %s\n' "$1" } warn() { printf 'WARNING: %s\n' "$1" >&2 } normalize_version() { case "$1" in "" | latest) printf 'latest\n' ;; rust-v*) printf '%s\n' "${1#rust-v}" ;; v*) printf '%s\n' "${1#v}" ;; *) printf '%s\n' "$1" ;; esac } validate_version() { version="$1" if [ "$version" = "latest" ]; then return fi if ! printf '%s\n' "$version" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+(-alpha(\.[0-9]+){0,2}|-beta(\.[0-9]+)?)?$'; then echo "Invalid Codex release version: $version. Expected latest or x.y.z[-alpha[.N[.M]]|-beta[.N]]." >&2 return 1 fi } parse_args() { while [ "$#" -gt 0 ]; do case "$1" in --release) if [ "$#" -lt 2 ]; then echo "--release requires a value." >&2 exit 1 fi RELEASE="$2" shift ;; --help | -h) cat <&2 exit 1 ;; esac shift done } download_file() { url="$1" output="$2" if command -v curl >/dev/null 2>&1; then case "$url" in "$RELEASES_BASE_URL"/*) curl -fsSL --connect-timeout "$RELEASES_CONNECT_TIMEOUT" --max-time "$RELEASES_ASSET_TIMEOUT" "$url" -o "$output" ;; *) curl -fsSL "$url" -o "$output" ;; esac return fi if command -v wget >/dev/null 2>&1; then case "$url" in "$RELEASES_BASE_URL"/*) wget -q -t 1 -T "$RELEASES_ASSET_TIMEOUT" -O "$output" "$url" ;; *) wget -q -O "$output" "$url" ;; esac return fi echo "curl or wget is required to install Codex." >&2 exit 1 } download_text() { url="$1" if command -v curl >/dev/null 2>&1; then case "$url" in "$RELEASES_BASE_URL"/*) curl -fsSL --connect-timeout "$RELEASES_CONNECT_TIMEOUT" --max-time "$RELEASES_METADATA_TIMEOUT" "$url" ;; *) curl -fsSL "$url" ;; esac return fi if command -v wget >/dev/null 2>&1; then case "$url" in "$RELEASES_BASE_URL"/*) wget -q -t 1 -T "$RELEASES_METADATA_TIMEOUT" -O - "$url" ;; *) wget -q -O - "$url" ;; esac return fi echo "curl or wget is required to install Codex." >&2 exit 1 } download_file_with_fallback() { primary_url="$1" fallback_url="$2" output="$3" expected_digest="$4" fallback_asset="$5" required_manifest_asset="${6:-}" if download_file "$primary_url" "$output" && verify_archive_digest "$output" "$expected_digest" && { [ -z "$required_manifest_asset" ] || package_archive_digest "$required_manifest_asset" "$output" >/dev/null; }; then return fi if [ -z "$fallback_url" ]; then return 1 fi warn "Could not download or verify $primary_url; retrying from GitHub Releases." download_file "$fallback_url" "$output" if verify_archive_digest "$output" "$expected_digest" && { [ -z "$required_manifest_asset" ] || package_archive_digest "$required_manifest_asset" "$output" >/dev/null; }; then return fi resolve_release_from_github "$resolved_version" fallback_digest="$(release_asset_digest "$fallback_asset")" verify_archive_digest "$output" "$fallback_digest" if [ -n "$required_manifest_asset" ]; then package_archive_digest "$required_manifest_asset" "$output" >/dev/null fi } parse_release_metadata() { # Bound awk's record size so compact, single-line JSON stays fast on every # supported awk implementation. JSON strings cannot contain literal newlines, # so the record boundaries inserted by fold do not change the document. LC_ALL=C fold -b -w 4096 | LC_ALL=C awk ' function finish_string(value) { if (object_depth == 1 && (key == "tag_name" || key == "version" || key == "target" || key == "entrypoint")) { print key "\t" value } else if (object_depth == asset_object_depth) { if (key == "name") { asset_name = value } else if (key == "digest") { asset_digest = value } } expecting_value = 0 key = "" } { for (i = 1; i <= length($0); i++) { char = substr($0, i, 1) if (in_string) { if (escaped) { token = token "\\" char escaped = 0 } else if (char == "\\") { escaped = 1 } else if (char == "\"") { in_string = 0 if (string_is_value) { finish_string(token) } else { pending_key = token } } else { token = token char } continue } if (char == "\"") { in_string = 1 token = "" escaped = 0 string_is_value = expecting_value } else if (char == ":" && pending_key != "") { key = pending_key pending_key = "" expecting_value = 1 } else if (char == "{") { object_depth++ if (assets_array_depth != 0 && array_depth == assets_array_depth && asset_object_depth == 0) { asset_object_depth = object_depth asset_name = "" asset_digest = "" } expecting_value = 0 key = "" } else if (char == "}") { if (object_depth == asset_object_depth) { if (asset_name != "" && asset_digest != "") { print "asset\t" asset_name "\t" asset_digest } asset_object_depth = 0 asset_name = "" asset_digest = "" } object_depth-- expecting_value = 0 key = "" pending_key = "" } else if (char == "[") { array_depth++ if (expecting_value && key == "assets" && object_depth == 1) { assets_array_depth = array_depth } expecting_value = 0 key = "" } else if (char == "]") { if (array_depth == assets_array_depth) { assets_array_depth = 0 } array_depth-- expecting_value = 0 key = "" pending_key = "" } else if (char == ",") { expecting_value = 0 key = "" pending_key = "" } } } END { if (in_string || object_depth != 0 || array_depth != 0) { exit 1 } } ' } release_url_for_asset() { asset="$1" resolved_version="$2" printf 'https://github.com/openai/codex/releases/download/rust-v%s/%s\n' "$resolved_version" "$asset" } releases_url_for_asset() { asset="$1" resolved_version="$2" printf '%s/releases/%s/%s\n' "$RELEASES_BASE_URL" "$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" } parse_downloaded_release_metadata() { requested_release="$1" source_name="$2" if ! release_metadata="$(printf '%s\n' "$release_json" | parse_release_metadata)"; then echo "Could not parse $source_name release metadata for Codex $requested_release." >&2 return 1 fi } resolve_metadata_version() { release_tag="$(printf '%s\n' "$release_metadata" | awk -F '\t' '$1 == "tag_name" { print $2; exit }')" case "$release_tag" in rust-v*) metadata_version="${release_tag#rust-v}" ;; *) metadata_version="" ;; esac if [ -z "$metadata_version" ]; then echo "Failed to resolve the latest Codex release version." >&2 return 1 fi validate_version "$metadata_version" } resolve_release_from_github() { normalized_version="$1" if [ "$normalized_version" = "latest" ]; then requested_release="latest" metadata_url="https://api.github.com/repos/openai/codex/releases/latest" else resolved_version="$normalized_version" requested_release="$resolved_version" metadata_url="$(release_metadata_url "$resolved_version")" fi if ! release_json="$(download_text "$metadata_url")"; then echo "Could not fetch GitHub release metadata for Codex $requested_release. GitHub API may be unavailable or rate limited." >&2 exit 1 fi parse_downloaded_release_metadata "$requested_release" "GitHub" if [ "$normalized_version" = "latest" ]; then resolve_metadata_version resolved_version="$metadata_version" fi release_source="github" } resolve_release_from_releases() { normalized_version="$1" if [ "$normalized_version" = "latest" ]; then requested_release="latest" metadata_url="$RELEASES_BASE_URL/channels/latest" else requested_release="$normalized_version" metadata_url="$RELEASES_BASE_URL/releases/$normalized_version/release.json" fi if ! release_json="$(download_text "$metadata_url")"; then return 1 fi if ! parse_downloaded_release_metadata "$requested_release" "releases.openai.com"; then return 1 fi if ! resolve_metadata_version; then return 1 fi if [ "$normalized_version" != "latest" ] && [ "$metadata_version" != "$normalized_version" ]; then echo "Release metadata version did not match requested Codex version $normalized_version." >&2 return 1 fi resolved_version="$metadata_version" release_source="releases.openai.com" } resolve_release() { normalized_version="$(normalize_version "$RELEASE")" validate_version "$normalized_version" case "$PREFER_RELEASES_OPENAI_COM" in 1 | [Tt][Rr][Uu][Ee] | [Yy][Ee][Ss]) if resolve_release_from_releases "$normalized_version" && select_release_assets; then return fi warn "releases.openai.com is unavailable; falling back to GitHub Releases." ;; esac resolve_release_from_github "$normalized_version" select_release_assets } release_asset_digest_or_empty() { asset="$1" digest="$(printf '%s\n' "$release_metadata" | awk -F '\t' -v asset="$asset" ' $1 == "asset" && $2 == asset { print $3 exit } ')" case "$digest" in sha256:????????????????????????????????????????????????????????????????) digest="${digest#sha256:}" case "$digest" in *[!0-9a-fA-F]*) return 1 ;; esac printf '%s\n' "$digest" ;; *) return 1 ;; esac } release_asset_exists() { asset="$1" release_asset_digest_or_empty "$asset" >/dev/null 2>&1 } release_asset_digest() { asset="$1" digest="$(release_asset_digest_or_empty "$asset" || true)" if [ -z "$digest" ]; then echo "Could not find SHA-256 digest for release asset $asset." >&2 exit 1 fi printf '%s\n' "$digest" } select_release_assets() { package_asset="codex-package-$vendor_target.tar.gz" checksum_asset="codex-package_SHA256SUMS" download_fallback_url="" checksum_fallback_url="" if release_asset_exists "$package_asset" && release_asset_exists "$checksum_asset"; then install_layout="package" asset="$package_asset" elif release_asset_exists "codex-npm-$npm_tag-$resolved_version.tgz"; 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 return 1 fi if [ "$release_source" = "releases.openai.com" ]; then download_url="$(releases_url_for_asset "$asset" "$resolved_version")" download_fallback_url="$(release_url_for_asset "$asset" "$resolved_version")" if [ "$install_layout" = "package" ]; then checksum_url="$(releases_url_for_asset "$checksum_asset" "$resolved_version")" checksum_fallback_url="$(release_url_for_asset "$checksum_asset" "$resolved_version")" fi else download_url="$(release_url_for_asset "$asset" "$resolved_version")" if [ "$install_layout" = "package" ]; then checksum_url="$(release_url_for_asset "$checksum_asset" "$resolved_version")" fi fi } package_archive_digest() { asset="$1" manifest_path="$2" digest="$(awk -v asset="$asset" ' $2 == asset && length($1) == 64 && $1 !~ /[^0-9a-fA-F]/ { 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 return 1 fi printf '%s\n' "$digest" } file_sha256() { path="$1" if command -v sha256sum >/dev/null 2>&1; then sha256sum "$path" | awk '{print $1}' return fi if command -v shasum >/dev/null 2>&1; then shasum -a 256 "$path" | awk '{print $1}' return fi if command -v openssl >/dev/null 2>&1; then openssl dgst -sha256 "$path" | sed 's/^.*= //' return fi echo "sha256sum, shasum, or openssl is required to verify the Codex download." >&2 exit 1 } verify_archive_digest() { archive_path="$1" expected_digest="$2" actual_digest="$(file_sha256 "$archive_path")" if [ "$actual_digest" != "$expected_digest" ]; then echo "Downloaded Codex archive checksum did not match expected digest." >&2 echo "expected: $expected_digest" >&2 echo "actual: $actual_digest" >&2 return 1 fi } require_command() { if ! command -v "$1" >/dev/null 2>&1; then echo "$1 is required to install Codex." >&2 exit 1 fi } pick_profile() { # Use the same shell-specific split Homebrew documents because there is no # universal startup file across macOS/Linux login and interactive shells. case "$os:${SHELL:-}" in darwin:*/zsh) printf '%s\n' "$HOME/.zprofile" ;; darwin:*/bash) printf '%s\n' "$HOME/.bash_profile" ;; linux:*/zsh) printf '%s\n' "$HOME/.zshrc" ;; linux:*/bash) printf '%s\n' "$HOME/.bashrc" ;; *) printf '%s\n' "$HOME/.profile" ;; esac } add_to_path() { path_action="already" path_profile="" case ":$PATH:" in *":$BIN_DIR:"*) if [ -z "$conflict_manager" ]; then return fi ;; esac profile="$(pick_profile)" path_profile="$profile" begin_marker="# >>> Codex installer >>>" end_marker="# <<< Codex installer <<<" path_line="export PATH=\"$BIN_DIR:\$PATH\"" if [ -f "$profile" ] && grep -F "$begin_marker" "$profile" >/dev/null 2>&1; then if grep -F "$path_line" "$profile" >/dev/null 2>&1; then path_action="configured" return fi if grep -F "$end_marker" "$profile" >/dev/null 2>&1; then rewrite_path_block "$profile" "$begin_marker" "$end_marker" "$path_line" path_action="updated" return fi fi append_path_block "$profile" "$begin_marker" "$end_marker" "$path_line" path_action="added" } append_path_block() { profile="$1" begin_marker="$2" end_marker="$3" path_line="$4" { printf '\n%s\n' "$begin_marker" printf '%s\n' "$path_line" printf '%s\n' "$end_marker" } >>"$profile" } rewrite_path_block() { profile="$1" begin_marker="$2" end_marker="$3" path_line="$4" tmp_profile="$tmp_dir/profile.$$.tmp" awk -v begin="$begin_marker" -v end="$end_marker" -v line="$path_line" ' BEGIN { in_block = 0 replaced = 0 } $0 == begin { if (!replaced) { print begin print line print end replaced = 1 } in_block = 1 next } in_block { if ($0 == end) { in_block = 0 } next } { print } END { if (in_block != 0) { exit 1 } } ' "$profile" >"$tmp_profile" mv "$tmp_profile" "$profile" } mkdir_lock_is_stale() { [ -d "$LOCK_DIR" ] || return 1 pid="$(cat "$LOCK_DIR/pid" 2>/dev/null || true)" started_at="$(cat "$LOCK_DIR/started_at" 2>/dev/null || true)" now="$(date +%s 2>/dev/null || printf '0')" case "$started_at" in ''|*[!0-9]*) started_at=0 ;; esac if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then return 1 fi if [ "$started_at" -eq 0 ] || [ "$now" -eq 0 ]; then return 0 fi [ $((now - started_at)) -ge "$LOCK_STALE_AFTER_SECS" ] } acquire_install_lock() { mkdir -p "$STANDALONE_ROOT" if [ "$os" = "darwin" ] && command -v lockf >/dev/null 2>&1; then : >>"$LOCK_FILE" exec 9<>"$LOCK_FILE" lockf 9 lock_kind="lockf" return fi if command -v flock >/dev/null 2>&1; then exec 9>"$LOCK_FILE" flock 9 lock_kind="flock" return fi while ! mkdir "$LOCK_DIR" 2>/dev/null; do if mkdir_lock_is_stale; then warn "Removing stale installer lock at $LOCK_DIR" rm -rf "$LOCK_DIR" continue fi sleep 1 done printf '%s\n' "$$" >"$LOCK_DIR/pid" date +%s >"$LOCK_DIR/started_at" 2>/dev/null || true lock_kind="mkdir" } release_install_lock() { if [ "$lock_kind" = "mkdir" ]; then rm -rf "$LOCK_DIR" 2>/dev/null || true elif [ "$lock_kind" = "flock" ] || [ "$lock_kind" = "lockf" ]; then exec 9>&- 2>/dev/null || true fi lock_kind="" } cleanup_stale_install_artifacts() { mkdir -p "$RELEASES_DIR" "$STANDALONE_ROOT" find "$RELEASES_DIR" -mindepth 1 -maxdepth 1 -name '.staging.*' -exec rm -rf {} + find "$STANDALONE_ROOT" -mindepth 1 -maxdepth 1 -name '.current.*' -exec rm -f {} + if [ "$DAEMON_ONLY" != "1" ] && [ -d "$BIN_DIR" ]; then find "$BIN_DIR" -mindepth 1 -maxdepth 1 -name '.codex.*' -exec rm -f {} + fi } record_standalone_releases_in_use() { # /proc also catches old clients with no session records. On macOS lsof # checks every open file in the release, including running helper programs. if [ "$os" = "linux" ]; then [ -r /proc/self/mountinfo ] && [ -L /proc/self/exe ] || return 1 # hidepid can omit other users' processes entirely; do not assume an # incomplete process listing proves an older release is unused. if grep -E '(^|[ ,])hidepid=[^0, ]' /proc/self/mountinfo >/dev/null; then return 1 fi : > "$tmp_dir/in-use" || return 1 for process in /proc/[0-9]*; do if executable="$(readlink "$process/exe" 2>/dev/null)"; then printf 'n%s\n' "$executable" >> "$tmp_dir/in-use" || return 1 elif [ -d "$process" ]; then # Even same-user ssh-agent/systemd processes may deny exe inspection. # Only an unreadable Codex or bundled helper prevents safe pruning. process_name="$(cat "$process/comm" 2>/dev/null)" || { [ ! -d "$process" ] && continue return 1 } # Linux truncates comm to 15 bytes, including the code-mode host. case "$process_name" in codex* | rg | bwrap) # A zombie has no files left open, even if its parent never reaps it. grep -Eq '^State:[[:space:]]*Z' "$process/status" 2>/dev/null && continue [ ! -d "$process" ] && continue return 1 ;; esac fi done else # macOS lsof silently omits other users' processes, even with no errors. # ps can still report their executable names. As on Linux, decline to # prune when an uninspectable Codex or helper may be using a release. current_uid="$(id -u)" || return 1 ps -axo uid=,comm= > "$tmp_dir/processes" || return 1 [ -s "$tmp_dir/processes" ] || return 1 while read -r process_uid process_name; do [ "$process_uid" = "$current_uid" ] && continue case "${process_name##*/}" in codex* | rg | bwrap) return 1 ;; esac done < "$tmp_dir/processes" if /usr/sbin/lsof -nP -F n +D "$releases" > "$tmp_dir/in-use" 2> "$tmp_dir/lsof-error"; then : else # lsof returns 1 for an empty result as well as for errors. [ ! -s "$tmp_dir/in-use" ] && [ ! -s "$tmp_dir/lsof-error" ] || return 1 fi [ ! -s "$tmp_dir/lsof-error" ] || return 1 fi } # Run under the install lock, after the visible command has been verified. # Only retire ordinary, manifest-owned packages; never retry a retired tree. prune_standalone_releases() ( [ ! -L "$CODEX_HOME_DIR/packages" ] && [ ! -L "$STANDALONE_ROOT" ] && [ ! -L "$RELEASES_DIR" ] || return 1 releases="$(cd -P "$RELEASES_DIR" && pwd)" || return 1 # Local process inspection cannot establish whether a network home is in # use on another host. df -l excludes remotely mounted filesystems. df -lP "$releases" > /dev/null 2> "$tmp_dir/df-error" || return 1 [ ! -s "$tmp_dir/df-error" ] || return 1 selected="$(cd -P "$CURRENT_LINK" && pwd)" || return 1 history="$(cat "$STANDALONE_ROOT/selection-history")" || return 1 previous="$(printf '%s\n' "$history" | sed -n '2p')" case "$previous" in */* | . | ..) return 1 ;; esac identity="$(LC_ALL=C stat -c '%d %i %.9Z' "$CURRENT_LINK" 2>/dev/null || LC_ALL=C stat -f '%d %i %Fc' "$CURRENT_LINK")" || return 1 [ "$selected" = "$releases/${selected##*/}" ] && [ "$history" = "$(printf '%s\n%s\n%s' "${selected##*/}" "$previous" "$identity")" ] || return 1 record_standalone_releases_in_use || return 1 restore_candidate="" # A stopped install must not strand a late-starting release in retirement. trap 'if [ -n "$restore_candidate" ] && [ -d "$retired/release" ] && [ ! -e "$candidate" ] && [ ! -L "$candidate" ]; then mv "$retired/release" "$candidate" && rmdir "$retired" fi' EXIT trap 'exit 1' HUP INT TERM for candidate in "$releases"/*; do [ -d "$candidate" ] && [ ! -L "$candidate" ] && [ "$candidate" != "$selected" ] && [ "${candidate##*/}" != "$previous" ] || continue grep -F "n$candidate/" "$tmp_dir/in-use" >/dev/null && continue [ -f "$candidate/codex-package.json" ] && [ ! -L "$candidate/codex-package.json" ] || continue metadata="$(parse_release_metadata < "$candidate/codex-package.json")" || continue version="$(printf '%s\n' "$metadata" | awk -F '\t' '$1 == "version" {print $2}')" target="$(printf '%s\n' "$metadata" | awk -F '\t' '$1 == "target" {print $2}')" entrypoint="$(printf '%s\n' "$metadata" | awk -F '\t' '$1 == "entrypoint" {print $2}')" [ "${candidate##*/}" = "$version-$target" ] && [ "$version" != "latest" ] && [ "$entrypoint" = "bin/codex" ] && [ -f "$candidate/bin/codex" ] || continue validate_version "$version" >/dev/null 2>&1 || continue case "$target" in aarch64-apple-darwin | x86_64-apple-darwin | aarch64-unknown-linux-musl | x86_64-unknown-linux-musl) ;; *) continue ;; esac # Reject symlinks (except the installer's own alias), special files and # mounts before rm -r. find -xdev alone would not protect nested mounts. [ "$(readlink "$candidate/codex" 2>/dev/null)" = "bin/codex" ] || continue if ! find "$candidate" -xdev ! -type f ! -type d ! -path "$candidate/codex" -print > "$tmp_dir/unowned"; then continue fi [ ! -s "$tmp_dir/unowned" ] || continue if [ "$os" = "linux" ]; then # mountinfo escapes spaces, tabs, newlines, and backslashes in paths. if ! CODEX_PRUNE_RELEASE="$candidate" LC_ALL=C awk ' BEGIN { release = ENVIRON["CODEX_PRUNE_RELEASE"] } { path = $5; gsub(/\\040/, " ", path); gsub(/\\011/, "\t", path) gsub(/\\012/, "\n", path); gsub(/\\134/, "\\", path) if (path == release || index(path, release "/") == 1) found = 1 } END { exit found ? 1 : 0 } ' /proc/self/mountinfo; then continue fi else device="$(stat -f '%d' "$releases")" || return 1 if ! find "$candidate" -xdev -exec stat -f '%d' {} + > "$tmp_dir/devices"; then continue fi grep -vxF "$device" "$tmp_dir/devices" >/dev/null && continue fi retired="$(mktemp -d "$releases/.retired.XXXXXXXX")" || return 1 restore_candidate=1 if mv "$candidate" "$retired/release"; then # Block new launches at the original path before the final use check. # Some systems report the old executable path, so check both names. if ! record_standalone_releases_in_use || grep -F -e "n$candidate/" -e "n$retired/release/" "$tmp_dir/in-use" >/dev/null; then [ ! -e "$candidate" ] && [ ! -L "$candidate" ] || return 1 mv "$retired/release" "$candidate" && rmdir "$retired" || return 1 restore_candidate="" continue fi # Once proved unused, never restore a possibly partially deleted tree. restore_candidate="" find "$retired" -xdev -depth -delete || return 1 else restore_candidate="" rmdir "$retired" return 1 fi done ) replace_path_with_symlink() { link_path="$1" link_target="$2" tmp_link="$3" rm -f "$tmp_link" ln -s "$link_target" "$tmp_link" if mv -Tf "$tmp_link" "$link_path" 2>/dev/null; then return fi if mv -hf "$tmp_link" "$link_path" 2>/dev/null; then return fi rm -f "$link_path" mv -f "$tmp_link" "$link_path" } version_from_binary() { codex_path="$1" if [ ! -x "$codex_path" ]; then return 1 fi "$codex_path" --version 2>/dev/null | sed -n 's/.* \([0-9][0-9A-Za-z.+-]*\)$/\1/p' | head -n 1 } 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" return 0 fi return 0 } resolve_existing_codex() { command -v codex 2>/dev/null || true } classify_existing_codex() { existing_path="$1" if [ -z "$existing_path" ] || [ "$existing_path" = "$BIN_PATH" ]; then return 1 fi case "$existing_path" in /opt/homebrew/* | /usr/local/*) if [ "$os" = "darwin" ]; then printf 'brew\n' return 0 fi ;; esac if [ -f "$existing_path" ] && grep -F "#!/usr/bin/env node" "$existing_path" >/dev/null 2>&1; then case "$existing_path" in *".bun"*) printf 'bun\n' ;; *) printf 'npm\n' ;; esac return 0 fi return 1 } prompt_yes_no() { prompt="$1" case "$NON_INTERACTIVE" in 1 | [Tt][Rr][Uu][Ee] | [Yy][Ee][Ss]) return 1 ;; esac if ( : /dev/null; then printf '%s [y/N] ' "$prompt" >/dev/tty if ! IFS= read -r answer /dev/null || true)" = \ "$CURRENT_LINK/bin/codex-code-mode-host" ]; then rm -f "$CODE_MODE_HOST_BIN_PATH" fi } verify_visible_command() { "$BIN_PATH" --version >/dev/null if [ "$os" = "darwin" ] && [ "$install_layout" = "package" ]; then [ -x "$CODE_MODE_HOST_BIN_PATH" ] fi } parse_args "$@" require_command mktemp require_command tar case "$(uname -s)" in Darwin) os="darwin" ;; Linux) os="linux" ;; *) echo "install.sh supports macOS and Linux. Use install.ps1 on Windows." >&2 exit 1 ;; esac case "$(uname -m)" in x86_64 | amd64) arch="x86_64" ;; arm64 | aarch64) arch="aarch64" ;; *) echo "Unsupported architecture: $(uname -m)" >&2 exit 1 ;; esac if [ "$os" = "darwin" ] && [ "$arch" = "x86_64" ]; then if [ "$(sysctl -n sysctl.proc_translated 2>/dev/null || true)" = "1" ]; then arch="aarch64" fi 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 resolve_release release_name="$resolved_version-$vendor_target" release_dir="$RELEASES_DIR/$release_name" current_version="$(current_installed_version)" if [ -n "$current_version" ] && [ "$current_version" != "$resolved_version" ]; then step "Updating Codex CLI from $current_version to $resolved_version" elif [ -n "$current_version" ]; then step "Updating Codex CLI" else step "Installing Codex CLI" fi step "Detected platform: $platform_label" step "Resolved version: $resolved_version" if [ "$DAEMON_ONLY" != "1" ]; then detect_conflicting_install fi tmp_dir="$(mktemp -d)" cleanup() { release_install_lock if [ -n "$tmp_dir" ]; then rm -rf "$tmp_dir" fi } trap cleanup EXIT trap 'exit 130' INT trap 'exit 143' TERM acquire_install_lock if [ "${CODEX_INSTALL_DEFER_SELECTION:-0}" = "1" ] && { [ -e "$STANDALONE_ROOT/current" ] || [ -L "$STANDALONE_ROOT/current" ]; }; then echo "A dedicated daemon is already selected; retry the update." >&2 exit 1 fi updater_record="$CODEX_HOME_DIR/app-server-daemon/app-server-updater.pid" if [ "$DAEMON_ONLY" = "1" ]; then updater_record="$CODEX_HOME_DIR/app-server-daemon/daemon-updater.pid" fi old_updater_parent="false" if [ "${CODEX_INSTALL_IF_LATEST:-}" != "1" ] && [ "${CODEX_INSTALL_IF_CURRENT:-}" != "1" ] && [ -f "$updater_record" ]; then updater_pid="$(sed -n 's/.*"pid"[[:space:]]*:[[:space:]]*\([0-9][0-9]*\).*/\1/p' "$updater_record" | head -n 1)" recorded_start="$(sed -n 's/.*"processStartTime"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$updater_record" | head -n 1)" if [ -r "/proc/$$/stat" ]; then parent_pid="$(sed 's/^.*) //' "/proc/$$/stat" | awk '{ print $2 }')" else parent_pid="$(ps -p "$$" -o ppid= 2>/dev/null)" || parent_pid="" parent_pid="$(printf '%s' "$parent_pid" | tr -d ' ')" fi if [ -n "$updater_pid" ] && [ "$updater_pid" = "$parent_pid" ]; then actual_details="$(ps -p "$updater_pid" -o stat= -o lstart= 2>/dev/null)" || actual_details="" actual_start="$(printf '%s' "$actual_details" | sed 's/^[^[:space:]]*[[:space:]]*//; s/[[:space:]]*$//')" if [ -n "$recorded_start" ] && [ "$recorded_start" = "$actual_start" ]; then old_updater_parent="true" fi fi if [ "$RELEASE" = "latest" ] && [ -n "$updater_pid" ] && { [ -z "$parent_pid" ] || { [ "$updater_pid" = "$parent_pid" ] && { [ -z "$recorded_start" ] || [ -z "$actual_start" ]; }; }; } && kill -0 "$updater_pid" 2>/dev/null; then warn "Cannot verify whether an older updater launched this installer; skipping latest update." exit 0 fi fi if [ "${CODEX_INSTALL_IF_LATEST:-}" = "1" ] || [ "${CODEX_INSTALL_IF_CURRENT:-}" = "1" ] || [ "$old_updater_parent" = "true" ]; then guarded_release="${CODEX_UPDATE_FROM_RELEASE:-}" if [ "$old_updater_parent" = "true" ]; then guarded_release="$(cat "$AUTO_UPDATE_VERSION" 2>/dev/null || true)" fi current_release_dir="$(cd -P "$CURRENT_LINK" 2>/dev/null && pwd)" || exit 0 releases_dir="$(cd -P "$RELEASES_DIR" 2>/dev/null && pwd)" || exit 0 if [ "$RELEASE" != "latest" ] || [ -z "$guarded_release" ] || [ "$current_release_dir" != "$releases_dir/$guarded_release" ]; then if [ "${CODEX_INSTALL_IF_CURRENT:-}" = "1" ]; then echo "Daemon selection changed; retry the update." >&2 exit 1 fi exit 0 fi # An explicit daemon update may leave a local or pinned release. Scheduled # updates still require the selected release to follow the latest channel. if [ "${CODEX_INSTALL_IF_CURRENT:-}" != "1" ] && [ "$(cat "$AUTO_UPDATE_VERSION" 2>/dev/null || true)" != "$guarded_release" ]; then exit 0 fi fi cleanup_stale_install_artifacts if ! release_dir_is_complete "$release_dir" "$resolved_version" "$vendor_target" "$install_layout"; then if [ -e "$release_dir" ] || [ -L "$release_dir" ]; then if [ "$DAEMON_ONLY" = "1" ]; then echo "Refusing to overwrite existing daemon release $release_dir." >&2 exit 1 fi warn "Found incomplete existing release at $release_dir; reinstalling." fi archive_path="$tmp_dir/$asset" checksum_path="$tmp_dir/$checksum_asset" step "Downloading Codex CLI" if [ "$install_layout" = "package" ]; then checksum_digest="$(release_asset_digest "$checksum_asset")" download_file_with_fallback "$checksum_url" "$checksum_fallback_url" "$checksum_path" "$checksum_digest" "$checksum_asset" "$asset" expected_digest="$(package_archive_digest "$asset" "$checksum_path")" else expected_digest="$(release_asset_digest "$asset")" fi download_file_with_fallback "$download_url" "$download_fallback_url" "$archive_path" "$expected_digest" "$asset" step "Installing standalone package to $release_dir" 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 if ! release_dir_is_complete "$release_dir" "$resolved_version" "$vendor_target" "$install_layout"; then echo "Installed Codex command did not report expected version $resolved_version." >&2 exit 1 fi if [ "$DAEMON_ONLY" = "1" ] && [ "${CODEX_INSTALL_DEFER_SELECTION:-0}" != "1" ]; then installed_codex="$release_dir/codex" if [ "$install_layout" = "package" ]; then installed_codex="$release_dir/bin/codex" fi if ! "$installed_codex" app-server daemon pid-update-loop --check-package-ownership >/dev/null 2>&1; then echo "The production release does not support daemon-owned packages; the current selection was left unchanged." >&2 exit 1 fi fi if [ "${CODEX_INSTALL_DEFER_SELECTION:-0}" != "1" ]; then # A no-op must not replace the selection link: history is bound to it. selected_dir="$(cd -P "$release_dir" && pwd)" previous_dir="$(cd -P "$CURRENT_LINK" 2>/dev/null && pwd)" || previous_dir="" if [ "$previous_dir" != "$selected_dir" ]; then update_current_link "$release_dir" if ! ( history="$STANDALONE_ROOT/selection-history" # GNU and BSD stat both inspect the link itself by default. selection_id="$(LC_ALL=C stat -c '%d %i %.9Z' "$CURRENT_LINK" 2>/dev/null || LC_ALL=C stat -f '%d %i %Fc' "$CURRENT_LINK")" printf '%s\n%s\n%s\n' "$release_name" "${previous_dir##*/}" "$selection_id" > "$history.tmp.$$" mv -f "$history.tmp.$$" "$history" ); then warn "Could not record installation selection; pruning will be skipped." fi fi else # Do not publish history for a migration until Rust has committed the cutover. update_current_link "$release_dir" fi if [ "$RELEASE" = "latest" ]; then printf '%s' "$release_name" > "$AUTO_UPDATE_VERSION.tmp.$$" mv -f "$AUTO_UPDATE_VERSION.tmp.$$" "$AUTO_UPDATE_VERSION" else rm -f "$AUTO_UPDATE_VERSION" fi if [ "$DAEMON_ONLY" = "1" ]; then release_install_lock exit 0 fi update_visible_command "$release_dir" add_to_path verify_visible_command if ! prune_standalone_releases; then warn "Could not safely inspect old Codex releases; skipped remaining cleanup." fi release_install_lock handle_conflicting_install case "$path_action" in added) print_launch_instructions ;; updated) print_launch_instructions ;; configured) print_launch_instructions ;; *) step "$BIN_DIR is already on PATH" print_launch_instructions ;; esac printf 'Codex CLI %s installed successfully.\n' "$resolved_version" maybe_launch_codex_now