mirror of
https://github.com/openai/codex.git
synced 2026-10-28 18:58:46 +00:00
## Why macOS `lsof +D` can return status 1 even when it finds open files, causing release cleanup to be skipped. It can also escape non-ASCII installation paths, preventing full-path matching from recognizing releases in use. ## What changed Accept `lsof` status 1 while preserving matches, and continue to reject other failures or any stderr. Match the installer-owned ASCII path suffix on macOS and recognize processes whose working directory is the release root. Apply these checks before and after moving a release for deletion. ## Testing Add coverage for `lsof` exit statuses, output, and errors. Exercise release retention with a non-ASCII installation path and, on macOS, a process holding the release directory open. GitOrigin-RevId: 06181467c7d1bb256b6781241a444976b890d7c9
1494 lines
42 KiB
Bash
Executable File
1494 lines
42 KiB
Bash
Executable File
#!/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 <<EOF
|
|
Usage: install.sh [--release VERSION]
|
|
|
|
Environment:
|
|
CODEX_RELEASE Version to install; overridden by --release.
|
|
CODEX_NON_INTERACTIVE Set to 1, true, or yes to skip prompts.
|
|
CODEX_INSTALLER_USE_RELEASES_OPENAI_COM
|
|
Set to 0, false, or no to use GitHub Releases.
|
|
EOF
|
|
exit 0
|
|
;;
|
|
*)
|
|
echo "Unknown argument: $1" >&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
|
|
# macOS lsof +D can return 1 with matches when some files are not open.
|
|
# Keep those matches, but reject other failures and any stderr below.
|
|
[ "$?" -eq 1 ] || return 1
|
|
fi
|
|
[ ! -s "$tmp_dir/lsof-error" ] || return 1
|
|
fi
|
|
}
|
|
|
|
release_is_unused() {
|
|
if [ "$os" = "linux" ]; then
|
|
path_field="n$1"
|
|
else
|
|
# lsof +D scopes the scan to this tree, but may escape bytes in CODEX_HOME.
|
|
# Match the installer-owned ASCII path, including the temporary retirement name.
|
|
path_field="/packages/${STANDALONE_ROOT##*/}/releases/${1#"$releases"/}"
|
|
fi
|
|
# Appending / also matches a process whose cwd is the release root itself.
|
|
LC_ALL=C RELEASE_PATH="$path_field/" awk 'index($0 "/", ENVIRON["RELEASE_PATH"]) { exit 1 }' "$tmp_dir/in-use"
|
|
}
|
|
|
|
# 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
|
|
release_is_unused "$candidate" || 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 ||
|
|
! release_is_unused "$candidate" || ! release_is_unused "$retired/release"; 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/tty ) 2>/dev/null; then
|
|
printf '%s [y/N] ' "$prompt" >/dev/tty
|
|
if ! IFS= read -r answer </dev/tty; then
|
|
return 1
|
|
fi
|
|
elif [ -t 0 ]; then
|
|
printf '%s [y/N] ' "$prompt"
|
|
if ! IFS= read -r answer; then
|
|
return 1
|
|
fi
|
|
else
|
|
return 1
|
|
fi
|
|
|
|
case "$answer" in
|
|
y | Y | yes | YES)
|
|
return 0
|
|
;;
|
|
*)
|
|
return 1
|
|
;;
|
|
esac
|
|
}
|
|
|
|
print_launch_instructions() {
|
|
case "$path_action" in
|
|
added)
|
|
step "Current terminal: export PATH=\"$BIN_DIR:\$PATH\" && codex"
|
|
step "Future terminals: open a new terminal and run: codex"
|
|
step "PATH was added to $path_profile"
|
|
;;
|
|
updated)
|
|
step "Current terminal: export PATH=\"$BIN_DIR:\$PATH\" && codex"
|
|
step "Future terminals: open a new terminal and run: codex"
|
|
step "PATH was updated in $path_profile"
|
|
;;
|
|
configured)
|
|
step "Current terminal: export PATH=\"$BIN_DIR:\$PATH\" && codex"
|
|
step "Future terminals: open a new terminal and run: codex"
|
|
step "PATH is already configured in $path_profile"
|
|
;;
|
|
*)
|
|
step "Current terminal: codex"
|
|
step "Future terminals: open a new terminal and run: codex"
|
|
;;
|
|
esac
|
|
}
|
|
|
|
maybe_launch_codex_now() {
|
|
if prompt_yes_no "Start Codex now?"; then
|
|
step "Launching Codex"
|
|
"$BIN_PATH"
|
|
fi
|
|
}
|
|
|
|
detect_conflicting_install() {
|
|
existing_path="$(resolve_existing_codex)"
|
|
manager="$(classify_existing_codex "$existing_path" || true)"
|
|
|
|
if [ -z "$manager" ]; then
|
|
return
|
|
fi
|
|
|
|
conflict_manager="$manager"
|
|
conflict_path="$existing_path"
|
|
step "Detected existing $manager-managed Codex at $existing_path"
|
|
warn "Multiple managed Codex installs can be ambiguous because PATH order decides which one runs."
|
|
}
|
|
|
|
handle_conflicting_install() {
|
|
if [ -z "$conflict_manager" ]; then
|
|
return
|
|
fi
|
|
|
|
case "$conflict_manager" in
|
|
brew)
|
|
uninstall_cmd="brew uninstall --cask codex"
|
|
;;
|
|
bun)
|
|
uninstall_cmd="bun remove -g @openai/codex"
|
|
;;
|
|
*)
|
|
uninstall_cmd="npm uninstall -g @openai/codex"
|
|
;;
|
|
esac
|
|
|
|
if prompt_yes_no "Uninstall the existing $conflict_manager-managed Codex now?"; then
|
|
step "Running: $uninstall_cmd"
|
|
if ! sh -c "$uninstall_cmd"; then
|
|
warn "Failed to uninstall the existing $conflict_manager-managed Codex. Continuing with the standalone install."
|
|
fi
|
|
else
|
|
warn "Leaving the existing $conflict_manager-managed Codex installed. PATH order will determine which codex runs."
|
|
fi
|
|
}
|
|
|
|
install_package_release() {
|
|
release_dir="$1"
|
|
archive_path="$2"
|
|
stage_release="$RELEASES_DIR/.staging.$(basename "$release_dir").$$"
|
|
|
|
mkdir -p "$RELEASES_DIR"
|
|
rm -rf "$stage_release"
|
|
mkdir -p "$stage_release"
|
|
tar -xzf "$archive_path" -C "$stage_release"
|
|
chmod 0755 \
|
|
"$stage_release/bin/codex" \
|
|
"$stage_release/bin/codex-code-mode-host" \
|
|
"$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" "$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"
|
|
fi
|
|
|
|
if [ -e "$release_dir" ] || [ -L "$release_dir" ]; then
|
|
rm -rf "$release_dir"
|
|
fi
|
|
mv "$stage_release" "$release_dir"
|
|
}
|
|
|
|
release_dir_is_complete() {
|
|
release_dir="$1"
|
|
expected_version="$2"
|
|
expected_target="$3"
|
|
layout="$4"
|
|
|
|
[ -d "$release_dir" ] &&
|
|
[ "$(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/bin/codex-code-mode-host" ] &&
|
|
[ -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" ] || return 1
|
|
;;
|
|
esac
|
|
|
|
installed_version="$(version_from_binary "$release_dir/bin/codex" || version_from_binary "$release_dir/codex" || true)"
|
|
[ "$installed_version" = "$expected_version" ]
|
|
}
|
|
|
|
update_current_link() {
|
|
release_dir="$1"
|
|
tmp_link="$STANDALONE_ROOT/.current.$$"
|
|
|
|
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_relative_path" "$tmp_link"
|
|
|
|
if [ "$os" = "darwin" ] && [ -x "$release_dir/bin/codex-code-mode-host" ]; then
|
|
replace_path_with_symlink \
|
|
"$CODE_MODE_HOST_BIN_PATH" \
|
|
"$CURRENT_LINK/bin/codex-code-mode-host" \
|
|
"$tmp_link"
|
|
elif [ "$(readlink "$CODE_MODE_HOST_BIN_PATH" 2>/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
|