mirror of
https://github.com/openai/codex.git
synced 2026-10-30 19:18:49 +00:00
Move daemon packages out of the standalone CLI installation (#45546)
## Why The daemon shares the standalone CLI package, coupling daemon updates to the visible CLI installation. A dedicated package lets daemon updates preserve the CLI selection and shell profile. ## What changed - Add dedicated daemon package selection and separate daemon and updater PID files, with diagnostics for the new files. - Make `codex app-server daemon update` migrate eligible legacy stable latest-channel installations to a dedicated package. Prepare and validate the release and its package-ownership support before switching; preserve settings and whether the daemon was running. Scheduled updates keep legacy installations in place. - Add daemon-only and deferred-selection modes to the shell and PowerShell installers, leaving the visible CLI installation unchanged. - Coordinate migration with installer locks, refresh package selection after acquiring lifecycle locks, and reject package changes during update downloads. ## Testing Add coverage for running and stopped migrations, incompatible installers and releases, package-selection races, daemon-only updates, and a capability probe that does not start an updater. Add a shell installer regression test verifying that daemon installation preserves the visible CLI and shell profile. GitOrigin-RevId: 1ec0f358f8f6ebde7daddb74e880cc8d2235d647
This commit is contained in:
@@ -903,9 +903,15 @@ $codexHome = if ([string]::IsNullOrWhiteSpace($env:CODEX_HOME)) {
|
||||
} else {
|
||||
$env:CODEX_HOME
|
||||
}
|
||||
$standaloneRoot = Join-Path $codexHome "packages\standalone"
|
||||
$daemonOnly = $env:CODEX_INSTALL_DAEMON_ONLY -eq "1"
|
||||
$standaloneRoot = Join-Path $codexHome $(if ($daemonOnly) { "packages\app-server-daemon" } else { "packages\standalone" })
|
||||
$releasesDir = Join-Path $standaloneRoot "releases"
|
||||
$currentDir = Join-Path $standaloneRoot "current"
|
||||
$deferSelection = $env:CODEX_INSTALL_DEFER_SELECTION -eq "1"
|
||||
if ($deferSelection) {
|
||||
if (-not $daemonOnly) { throw "Deferred selection requires a daemon-only installation." }
|
||||
$currentDir = Join-Path $standaloneRoot ".migration-current"
|
||||
}
|
||||
$autoUpdateVersion = Join-Path $standaloneRoot "auto-update-version"
|
||||
$lockPath = Join-Path $standaloneRoot "install.lock"
|
||||
|
||||
@@ -933,7 +939,7 @@ if (-not [string]::IsNullOrWhiteSpace($currentVersion) -and $currentVersion -ne
|
||||
Write-Step "Detected platform: $platformLabel"
|
||||
Write-Step "Resolved version: $resolvedVersion"
|
||||
|
||||
$conflictingInstall = Get-ConflictingInstall -VisibleBinDir $visibleBinDir
|
||||
$conflictingInstall = if ($daemonOnly) { $null } else { Get-ConflictingInstall -VisibleBinDir $visibleBinDir }
|
||||
$oldStandaloneBackup = $null
|
||||
|
||||
$checksumAsset = "codex-package_SHA256SUMS"
|
||||
@@ -949,6 +955,7 @@ $guardRejected = $false
|
||||
try {
|
||||
Invoke-WithInstallLock -LockPath $lockPath -Script {
|
||||
$updaterRecord = Join-Path $codexHome "app-server-daemon\app-server-updater.pid"
|
||||
if ($daemonOnly) { $updaterRecord = Join-Path $codexHome "app-server-daemon\daemon-updater.pid" }
|
||||
$oldUpdaterParent = $false
|
||||
if ($Release -eq "latest" -and $env:CODEX_INSTALL_IF_LATEST -ne "1" -and (Test-Path -LiteralPath $updaterRecord)) {
|
||||
$updaterPid = $null
|
||||
@@ -997,6 +1004,7 @@ try {
|
||||
|
||||
if (-not (Test-ReleaseIsComplete -ReleaseDir $releaseDir -ExpectedVersion $resolvedVersion -ExpectedTarget $target -Layout $installLayout)) {
|
||||
if (Test-Path -LiteralPath $releaseDir) {
|
||||
if ($daemonOnly) { throw "Refusing to overwrite existing daemon release $releaseDir." }
|
||||
Write-WarningStep "Found incomplete existing release at $releaseDir. Reinstalling."
|
||||
}
|
||||
|
||||
@@ -1058,6 +1066,9 @@ try {
|
||||
}
|
||||
|
||||
New-Item -ItemType Directory -Force -Path $standaloneRoot | Out-Null
|
||||
if ($deferSelection -and (Get-Item -LiteralPath (Join-Path $standaloneRoot "current") -Force -ErrorAction SilentlyContinue)) {
|
||||
throw "A dedicated daemon is already selected; retry the update."
|
||||
}
|
||||
Ensure-Junction -LinkPath $currentDir -TargetPath $releaseDir -InstallerOwnedTargetPrefix $releasesDir
|
||||
if ($Release -eq "latest") {
|
||||
$tempMarker = "$autoUpdateVersion.tmp.$PID"
|
||||
@@ -1069,6 +1080,7 @@ try {
|
||||
}
|
||||
}
|
||||
|
||||
if ($daemonOnly) { return }
|
||||
$visibleParent = Split-Path -Parent $visibleBinDir
|
||||
$currentBinDir = if ($installLayout -eq "Package") {
|
||||
Join-Path $currentDir "bin"
|
||||
@@ -1096,7 +1108,7 @@ try {
|
||||
} finally {
|
||||
Remove-Item -Recurse -Force $tempDir -ErrorAction SilentlyContinue
|
||||
}
|
||||
if ($guardRejected) { return }
|
||||
if ($guardRejected -or $daemonOnly) { return }
|
||||
|
||||
Maybe-HandleConflictingInstall -Conflict $conflictingInstall
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ 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"
|
||||
@@ -17,8 +18,18 @@ 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"
|
||||
@@ -742,7 +753,7 @@ cleanup_stale_install_artifacts() {
|
||||
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 [ -d "$BIN_DIR" ]; then
|
||||
if [ "$DAEMON_ONLY" != "1" ] && [ -d "$BIN_DIR" ]; then
|
||||
find "$BIN_DIR" -mindepth 1 -maxdepth 1 -name '.codex.*' -exec rm -f {} +
|
||||
fi
|
||||
}
|
||||
@@ -1140,7 +1151,9 @@ fi
|
||||
step "Detected platform: $platform_label"
|
||||
step "Resolved version: $resolved_version"
|
||||
|
||||
detect_conflicting_install
|
||||
if [ "$DAEMON_ONLY" != "1" ]; then
|
||||
detect_conflicting_install
|
||||
fi
|
||||
|
||||
tmp_dir="$(mktemp -d)"
|
||||
cleanup() {
|
||||
@@ -1154,7 +1167,15 @@ 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" ] && [ -f "$updater_record" ]; then
|
||||
updater_pid="$(sed -n 's/.*"pid"[[:space:]]*:[[:space:]]*\([0-9][0-9]*\).*/\1/p' "$updater_record" | head -n 1)"
|
||||
@@ -1197,6 +1218,10 @@ 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
|
||||
|
||||
@@ -1231,6 +1256,10 @@ if [ "$RELEASE" = "latest" ]; then
|
||||
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
|
||||
|
||||
@@ -156,6 +156,40 @@ class InstallShTest(unittest.TestCase):
|
||||
],
|
||||
)
|
||||
|
||||
def test_daemon_install_preserves_visible_cli_and_profile(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
root = Path(temp_dir).resolve()
|
||||
archive, checksum, metadata = create_package_release(root)
|
||||
options = dict(
|
||||
metadata_json=metadata,
|
||||
archive_path=archive,
|
||||
checksum_path=checksum,
|
||||
force_macos=True,
|
||||
)
|
||||
pinned, _ = run_installer_in(root, VERSION, **options)
|
||||
self.assertEqual(pinned.returncode, 0, pinned.stderr)
|
||||
visible = root / "install-bin/codex"
|
||||
original = visible.resolve()
|
||||
pending = visible.with_name(".codex.12345")
|
||||
pending.symlink_to(original)
|
||||
profile = root / "home/.profile"
|
||||
profile.write_text("unchanged profile\n")
|
||||
|
||||
installed, _ = run_installer_in(root, "latest", daemon_only=True, **options)
|
||||
self.assertEqual(installed.returncode, 0, installed.stderr)
|
||||
daemon = root / "codex-home/packages/app-server-daemon"
|
||||
release_name = f"{VERSION}-aarch64-apple-darwin"
|
||||
self.assertEqual(
|
||||
(daemon / "current").resolve(), daemon / "releases" / release_name
|
||||
)
|
||||
self.assertEqual((daemon / "auto-update-version").read_text(), release_name)
|
||||
self.assertEqual(visible.resolve(), original)
|
||||
self.assertTrue(pending.is_symlink())
|
||||
self.assertEqual(profile.read_text(), "unchanged profile\n")
|
||||
self.assertFalse(
|
||||
(root / "codex-home/packages/standalone/auto-update-version").exists()
|
||||
)
|
||||
|
||||
def test_explicit_release_pins_even_the_current_latest_version(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
root = Path(temp_dir)
|
||||
@@ -637,6 +671,7 @@ def run_installer_in(
|
||||
update_guard_from_release: str | None = None,
|
||||
old_updater_parent_pid: int | None = None,
|
||||
fail_ps: bool = False,
|
||||
daemon_only: bool = False,
|
||||
) -> tuple[subprocess.CompletedProcess[str], list[str]]:
|
||||
bin_dir = root / "bin"
|
||||
bin_dir.mkdir(exist_ok=True)
|
||||
@@ -771,6 +806,7 @@ def run_installer_in(
|
||||
"CODEX_INSTALL_DIR": str(root / "install-bin"),
|
||||
"CODEX_NON_INTERACTIVE": "1",
|
||||
"CODEX_RELEASE": release,
|
||||
"CODEX_INSTALL_DAEMON_ONLY": "1" if daemon_only else "0",
|
||||
"CODEX_TEST_ARCHIVE_PATH": str(archive_path or ""),
|
||||
"CODEX_TEST_CHECKSUM_PATH": str(checksum_path or ""),
|
||||
"CODEX_TEST_RELEASES_CHECKSUM_PATH": str(
|
||||
|
||||
Reference in New Issue
Block a user