Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
1e5abe5
feat: significantly improve native installer
efrazer-oai Mar 8, 2026
32ac634
fix: tighten native install context model
efrazer-oai Mar 8, 2026
02249b4
Cleaning up UpdateAction
efrazer-oai Mar 8, 2026
27af29d
adding bazel build file & fixing an install script bug
efrazer-oai Mar 8, 2026
231320f
manually fixing annoying codex nits
efrazer-oai Mar 8, 2026
8effe7f
build fix on release.
efrazer-oai Mar 8, 2026
623b08e
fixing flaky test
efrazer-oai Mar 8, 2026
74b8140
fix: harden native installer recovery and PATH updates
efrazer-oai Mar 8, 2026
e884b88
fix: improve native installer launch UX
efrazer-oai Mar 11, 2026
de550e2
fix: improve standalone installer behavior
efrazer-oai Mar 16, 2026
8798d16
fix: repair CI regressions in native installer branch
efrazer-oai Mar 16, 2026
14eee82
fix: canonicalize codex home for standalone detection
efrazer-oai Apr 7, 2026
a791658
codex: fix CI failure on PR #17022
efrazer-oai Apr 7, 2026
71d331a
fix: address native installer review comments
efrazer-oai Apr 7, 2026
319edba
fix: update unified exec test path join
efrazer-oai Apr 7, 2026
a3872be
refactor: inline standalone release-root check
efrazer-oai Apr 7, 2026
7b28752
fix: run standalone windows updater without cmd
efrazer-oai Apr 7, 2026
19bcbd4
fix: harden standalone installer activation and updates
efrazer-oai Apr 15, 2026
cada50f
fix: clean stale installer artifacts before activation
efrazer-oai Apr 15, 2026
e1851ee
fix: simplify windows updater branch and drop plan doc
efrazer-oai Apr 15, 2026
75957c1
fix: retarget installer-owned junctions in place
efrazer-oai Apr 15, 2026
3b1036b
fix: restore legacy windows bin on migration failure
efrazer-oai Apr 15, 2026
a186044
fix: use durable unix installer locks
efrazer-oai Apr 15, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions codex-rs/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions codex-rs/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ members = [
"arg0",
"feedback",
"features",
"install-context",
"codex-backend-openapi-models",
"code-mode",
"cloud-requirements",
Expand Down Expand Up @@ -131,6 +132,7 @@ codex-execpolicy = { path = "execpolicy" }
codex-experimental-api-macros = { path = "codex-experimental-api-macros" }
codex-features = { path = "features" }
codex-feedback = { path = "feedback" }
codex-install-context = { path = "install-context" }
codex-file-search = { path = "file-search" }
codex-git-utils = { path = "git-utils" }
codex-hooks = { path = "hooks" }
Expand Down
2 changes: 1 addition & 1 deletion codex-rs/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Codex CLI (Rust Implementation)

We provide Codex CLI as a standalone, native executable to ensure a zero-dependency install.
We provide Codex CLI as a standalone executable to ensure a zero-dependency install.

## Installing Codex

Expand Down
17 changes: 13 additions & 4 deletions codex-rs/cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -490,10 +490,19 @@ fn run_update_action(action: UpdateAction) -> anyhow::Result<()> {
let status = {
#[cfg(windows)]
{
// On Windows, run via cmd.exe so .CMD/.BAT are correctly resolved (PATHEXT semantics).
std::process::Command::new("cmd")
.args(["/C", &cmd_str])
.status()?
if action == UpdateAction::StandaloneWindows {
let (cmd, args) = action.command_args();
// Run the standalone PowerShell installer with PowerShell
// itself. Routing this through `cmd.exe /C` would parse
// PowerShell metacharacters like `|` before PowerShell sees
// the installer command.
std::process::Command::new(cmd).args(args).status()?
} else {
// On Windows, run via cmd.exe so .CMD/.BAT are correctly resolved (PATHEXT semantics).
std::process::Command::new("cmd")
.args(["/C", &cmd_str])
.status()?
}
}
#[cfg(not(windows))]
{
Expand Down
2 changes: 1 addition & 1 deletion codex-rs/core/tests/suite/unified_exec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ async fn create_workspace_directory(
test: &TestCodex,
rel_path: impl AsRef<std::path::Path>,
) -> Result<std::path::PathBuf> {
let abs_path = test.config.cwd.join(rel_path.as_ref())?;
let abs_path = test.config.cwd.join(rel_path.as_ref());
test.fs()
.create_directory(&abs_path, CreateDirectoryOptions { recursive: true })
.await?;
Expand Down
6 changes: 6 additions & 0 deletions codex-rs/install-context/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
load("//:defs.bzl", "codex_rust_crate")

codex_rust_crate(
name = "install-context",
crate_name = "codex_install_context",
)
19 changes: 19 additions & 0 deletions codex-rs/install-context/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
[package]
name = "codex-install-context"
version.workspace = true
edition.workspace = true
license.workspace = true

[lib]
name = "codex_install_context"
path = "src/lib.rs"

[lints]
workspace = true

[dependencies]
codex-utils-home-dir = { workspace = true }

[dev-dependencies]
pretty_assertions = { workspace = true }
tempfile = { workspace = true }
258 changes: 258 additions & 0 deletions codex-rs/install-context/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,258 @@
use std::path::Path;
use std::path::PathBuf;
use std::sync::OnceLock;

const RELEASES_DIRNAME: &str = "releases";
const RESOURCES_DIRNAME: &str = "codex-resources";
const STANDALONE_PACKAGES_DIRNAME: &str = "standalone";
static INSTALL_CONTEXT: OnceLock<InstallContext> = OnceLock::new();

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum StandalonePlatform {
Unix,
Windows,
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub enum InstallContext {
Standalone {
/// The managed standalone release directory, for example
/// `~/.codex/packages/standalone/releases/0.111.0-x86_64-unknown-linux-musl`.
release_dir: PathBuf,
/// The bundled resource directory that sits next to the executable when
/// this install ships managed dependencies.
resources_dir: Option<PathBuf>,
/// The platform of the standalone release, either `Unix` or `Windows`.
platform: StandalonePlatform,
},
/// A Codex binary launched through the npm-managed `codex.js` shim.
Npm,
/// A Codex binary launched through the bun-managed `codex.js` shim.
Bun,
/// A Codex binary that appears to come from a Homebrew install prefix.
Brew,
/// Any other execution environment.
///
/// This commonly covers `cargo run`, app-bundled Codex binaries, custom
/// internal launchers, and tests that execute Codex from an arbitrary path.
Other,
}

impl InstallContext {
pub fn from_exe(
is_macos: bool,
current_exe: Option<&Path>,
managed_by_npm: bool,
managed_by_bun: bool,
) -> Self {
let codex_home = codex_utils_home_dir::find_codex_home().ok();
Self::from_exe_with_codex_home(
is_macos,
current_exe,
managed_by_npm,
managed_by_bun,
codex_home.as_deref(),
)
}

fn from_exe_with_codex_home(
is_macos: bool,
current_exe: Option<&Path>,
managed_by_npm: bool,
managed_by_bun: bool,
codex_home: Option<&Path>,
) -> Self {
if managed_by_npm {
return Self::Npm;
}

if managed_by_bun {
return Self::Bun;
}

if let Some(exe_path) = current_exe
&& let Some(standalone_context) = standalone_install_context(exe_path, codex_home)
{
return standalone_context;
}

if is_macos
&& let Some(exe_path) = current_exe
&& (exe_path.starts_with("/opt/homebrew") || exe_path.starts_with("/usr/local"))
{
return Self::Brew;
}

Self::Other
}

pub fn current() -> &'static Self {
INSTALL_CONTEXT.get_or_init(|| {
let current_exe = std::env::current_exe().ok();
let managed_by_npm = std::env::var_os("CODEX_MANAGED_BY_NPM").is_some();
let managed_by_bun = std::env::var_os("CODEX_MANAGED_BY_BUN").is_some();
Self::from_exe(
cfg!(target_os = "macos"),
current_exe.as_deref(),
managed_by_npm,
managed_by_bun,
)
})
}

pub fn rg_command(&self) -> PathBuf {
match self {
Self::Standalone {
resources_dir: Some(resources_dir),
..
} => {
let bundled_rg = resources_dir.join(default_rg_command());
if bundled_rg.exists() {
bundled_rg
} else {
default_rg_command()
}
}
Self::Standalone {
resources_dir: None,
..
}
| Self::Npm
| Self::Bun
| Self::Brew
| Self::Other => default_rg_command(),
}
}
}

fn standalone_install_context(
exe_path: &Path,
codex_home: Option<&Path>,
) -> Option<InstallContext> {
let canonical_exe = std::fs::canonicalize(exe_path).ok()?;
let canonical_codex_home = std::fs::canonicalize(codex_home?).ok()?;
let release_dir = canonical_exe.parent()?.to_path_buf();
let releases_root = canonical_codex_home
.join("packages")
.join(STANDALONE_PACKAGES_DIRNAME)
.join(RELEASES_DIRNAME);
if !release_dir.starts_with(releases_root) {
return None;
}

let resources_dir = release_dir.join(RESOURCES_DIRNAME);
Some(InstallContext::Standalone {
release_dir,
resources_dir: resources_dir.is_dir().then_some(resources_dir),
platform: standalone_platform(),
})
}

fn standalone_platform() -> StandalonePlatform {
if cfg!(windows) {
StandalonePlatform::Windows
} else {
StandalonePlatform::Unix
}
}

fn default_rg_command() -> PathBuf {
if cfg!(windows) {
PathBuf::from("rg.exe")
} else {
PathBuf::from("rg")
}
}

#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
use std::fs;

#[test]
fn detects_standalone_install_from_release_layout() -> std::io::Result<()> {
let codex_home = tempfile::tempdir()?;
let release_dir = codex_home
.path()
.join("packages/standalone/releases/1.2.3-x86_64-unknown-linux-musl");
let resources_dir = release_dir.join(RESOURCES_DIRNAME);
fs::create_dir_all(&resources_dir)?;
let exe_path = release_dir.join(if cfg!(windows) { "codex.exe" } else { "codex" });
fs::write(&exe_path, "")?;
fs::write(resources_dir.join(default_rg_command()), "")?;
let canonical_release_dir = release_dir.canonicalize()?;
let canonical_resources_dir = resources_dir.canonicalize()?;

let context = InstallContext::from_exe_with_codex_home(
/*is_macos*/ false,
/*current_exe*/ Some(&exe_path),
/*managed_by_npm*/ false,
/*managed_by_bun*/ false,
/*codex_home*/ Some(codex_home.path()),
);
assert_eq!(
context,
InstallContext::Standalone {
release_dir: canonical_release_dir,
resources_dir: Some(canonical_resources_dir),
platform: standalone_platform(),
}
);
Ok(())
}

#[test]
fn standalone_rg_falls_back_when_resources_are_missing() -> std::io::Result<()> {
let codex_home = tempfile::tempdir()?;
let release_dir = codex_home
.path()
.join("packages/standalone/releases/1.2.3-x86_64-unknown-linux-musl");
fs::create_dir_all(&release_dir)?;
let exe_path = release_dir.join(if cfg!(windows) { "codex.exe" } else { "codex" });
fs::write(&exe_path, "")?;

let context = InstallContext::from_exe_with_codex_home(
/*is_macos*/ false,
/*current_exe*/ Some(&exe_path),
/*managed_by_npm*/ false,
/*managed_by_bun*/ false,
/*codex_home*/ Some(codex_home.path()),
);
assert_eq!(context.rg_command(), default_rg_command());
Ok(())
}

#[test]
fn npm_and_bun_take_precedence() {
let npm_context = InstallContext::from_exe_with_codex_home(
/*is_macos*/ false,
/*current_exe*/ Some(Path::new("/tmp/codex")),
/*managed_by_npm*/ true,
/*managed_by_bun*/ false,
/*codex_home*/ None,
);
assert_eq!(npm_context, InstallContext::Npm);

let bun_context = InstallContext::from_exe_with_codex_home(
/*is_macos*/ false,
/*current_exe*/ Some(Path::new("/tmp/codex")),
/*managed_by_npm*/ false,
/*managed_by_bun*/ true,
/*codex_home*/ None,
);
assert_eq!(bun_context, InstallContext::Bun);
}

#[test]
fn brew_is_detected_on_macos_prefixes() {
let context = InstallContext::from_exe_with_codex_home(
/*is_macos*/ true,
/*current_exe*/ Some(Path::new("/opt/homebrew/bin/codex")),
/*managed_by_npm*/ false,
/*managed_by_bun*/ false,
/*codex_home*/ None,
);
assert_eq!(context, InstallContext::Brew);
}
}
1 change: 1 addition & 0 deletions codex-rs/tui/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ codex-ansi-escape = { workspace = true }
codex-app-server-client = { workspace = true }
codex-app-server-protocol = { workspace = true }
codex-arg0 = { workspace = true }
codex-install-context = { workspace = true }
codex-chatgpt = { workspace = true }
codex-cloud-requirements = { workspace = true }
codex-config = { workspace = true }
Expand Down
Loading
Loading