runtime: detect Codex package layout (#23596)

## Why

The package-builder stack now creates a canonical Codex package
directory where the entrypoint lives under `bin/`, bundled helper
resources live under `codex-resources/`, and bundled PATH-style tools
live under `codex-path/`. That layout is not specific to the standalone
installer: npm, brew, install scripts, and manually unpacked artifacts
should all be able to use the same package shape.

The Rust runtime still only knew about the legacy standalone release
layout, where resources sit next to the executable. A packaged binary
therefore would not identify its package root or prefer the bundled `rg`
from `codex-path/`.

## What changed

- Adds `CodexPackageLayout` to `codex-install-context` and detects it
from an executable path shaped like `<package>/bin/<entrypoint>` when
`<package>/codex-package.json` is present.
- Splits `InstallContext` into an install `method` plus an optional
package layout so the layout is shared across npm, bun, brew,
standalone, and other launch contexts.
- Stores package-layout paths as `AbsolutePathBuf` values.
- Keeps `codex-resources/` and `codex-path/` optional so Codex can still
run with degraded behavior if sidecar directories are missing.
- Updates `InstallContext::rg_command()` to prefer bundled
`codex-path/rg` or `rg.exe`, then fall back to the legacy standalone
resources location, then system `rg`.
- Updates `codex doctor` reporting so package installs show package,
bin, resources, and path directories, and so bundled search detection
recognizes `codex-path/` for any install method.

## Test plan

- `cargo test -p codex-install-context`
- `cargo test -p codex-cli`
- `cargo test -p codex-tui
update_action::tests::maps_install_context_to_update_action`
- `just bazel-lock-check`
This commit is contained in:
Michael Bolin
2026-05-19 23:13:49 -07:00
committed by GitHub
parent 57a68fb9e3
commit cfa16fcc2e
7 changed files with 467 additions and 125 deletions
+1
View File
@@ -2997,6 +2997,7 @@ dependencies = [
name = "codex-install-context" name = "codex-install-context"
version = "0.0.0" version = "0.0.0"
dependencies = [ dependencies = [
"codex-utils-absolute-path",
"codex-utils-home-dir", "codex-utils-home-dir",
"pretty_assertions", "pretty_assertions",
"tempfile", "tempfile",
+62 -15
View File
@@ -39,7 +39,9 @@ use codex_core::config::ConfigBuilder;
use codex_core::config::ConfigOverrides; use codex_core::config::ConfigOverrides;
use codex_core::config::find_codex_home; use codex_core::config::find_codex_home;
use codex_features::FEATURES; use codex_features::FEATURES;
use codex_install_context::CodexPackageLayout;
use codex_install_context::InstallContext; use codex_install_context::InstallContext;
use codex_install_context::InstallMethod;
use codex_install_context::StandalonePlatform; use codex_install_context::StandalonePlatform;
use codex_login::AuthDotJson; use codex_login::AuthDotJson;
use codex_login::AuthManager; use codex_login::AuthManager;
@@ -812,7 +814,10 @@ fn installation_check(show_details: bool) -> DoctorCheck {
fn doctor_install_context(current_exe: Option<&Path>) -> InstallContext { fn doctor_install_context(current_exe: Option<&Path>) -> InstallContext {
if inherited_managed_env_for_cargo_binary(current_exe) { if inherited_managed_env_for_cargo_binary(current_exe) {
InstallContext::Other InstallContext {
method: InstallMethod::Other,
package_layout: None,
}
} else { } else {
InstallContext::current().clone() InstallContext::current().clone()
} }
@@ -843,8 +848,8 @@ fn inherited_managed_env_for_cargo_binary(current_exe: Option<&Path>) -> bool {
} }
fn describe_install_context(context: &InstallContext) -> String { fn describe_install_context(context: &InstallContext) -> String {
match context { match &context.method {
InstallContext::Standalone { InstallMethod::Standalone {
release_dir, release_dir,
resources_dir, resources_dir,
platform, platform,
@@ -853,22 +858,63 @@ fn describe_install_context(context: &InstallContext) -> String {
StandalonePlatform::Unix => "unix", StandalonePlatform::Unix => "unix",
StandalonePlatform::Windows => "windows", StandalonePlatform::Windows => "windows",
}; };
let resources = resources_dir match &context.package_layout {
.as_ref() Some(package_layout) => {
.map(|path| path.display().to_string()) let resources = display_optional_path(package_layout.resources_dir.as_deref());
.unwrap_or_else(|| "none".to_string()); let path = display_optional_path(package_layout.path_dir.as_deref());
format!(
"standalone ({platform}, package {}, bin {}, resources {resources}, path {path})",
package_layout.package_dir.display(),
package_layout.bin_dir.display()
)
}
None => {
let resources = display_optional_path(resources_dir.as_deref());
format!(
"standalone ({platform}, release {}, resources {resources})",
release_dir.display()
)
}
}
}
InstallMethod::Npm => {
describe_method_with_package_layout("npm", context.package_layout.as_ref())
}
InstallMethod::Bun => {
describe_method_with_package_layout("bun", context.package_layout.as_ref())
}
InstallMethod::Brew => {
describe_method_with_package_layout("brew", context.package_layout.as_ref())
}
InstallMethod::Other => {
describe_method_with_package_layout("other", context.package_layout.as_ref())
}
}
}
fn describe_method_with_package_layout(
method: &str,
package_layout: Option<&CodexPackageLayout>,
) -> String {
match package_layout {
Some(package_layout) => {
let resources = display_optional_path(package_layout.resources_dir.as_deref());
let path = display_optional_path(package_layout.path_dir.as_deref());
format!( format!(
"standalone ({platform}, release {}, resources {resources})", "{method} (package {}, bin {}, resources {resources}, path {path})",
release_dir.display() package_layout.package_dir.display(),
package_layout.bin_dir.display()
) )
} }
InstallContext::Npm => "npm".to_string(), None => method.to_string(),
InstallContext::Bun => "bun".to_string(),
InstallContext::Brew => "brew".to_string(),
InstallContext::Other => "other".to_string(),
} }
} }
fn display_optional_path(path: Option<&Path>) -> String {
path.map(|path| path.display().to_string())
.unwrap_or_else(|| "none".to_string())
}
#[derive(Debug, PartialEq, Eq)] #[derive(Debug, PartialEq, Eq)]
enum NpmRootCheck { enum NpmRootCheck {
Match { Match {
@@ -2791,13 +2837,14 @@ fn path_readiness(details: &mut Vec<String>, label: &str, path: &Path) {
} }
fn standalone_release_cache_details(details: &mut Vec<String>) { fn standalone_release_cache_details(details: &mut Vec<String>) {
let InstallContext::Standalone { release_dir, .. } = InstallContext::current() else { let context = InstallContext::current();
let InstallMethod::Standalone { release_dir, .. } = &context.method else {
return; return;
}; };
let Some(releases_dir) = release_dir.parent() else { let Some(releases_dir) = release_dir.parent() else {
return; return;
}; };
let Ok(entries) = std::fs::read_dir(releases_dir) else { let Ok(entries) = std::fs::read_dir(&releases_dir) else {
return; return;
}; };
let release_count = entries.filter_map(Result::ok).count(); let release_count = entries.filter_map(Result::ok).count();
+29 -15
View File
@@ -3,12 +3,13 @@
//! Runtime diagnostics answer provenance questions that are hard to infer from //! Runtime diagnostics answer provenance questions that are hard to infer from
//! user reports: which binary is running, which install channel it resembles, //! user reports: which binary is running, which install channel it resembles,
//! which platform it targets, and whether the search command comes from bundled //! which platform it targets, and whether the search command comes from bundled
//! standalone resources or from PATH. //! package files or from PATH.
use std::env; use std::env;
use std::process::Command; use std::process::Command;
use codex_install_context::InstallContext; use codex_install_context::InstallContext;
use codex_install_context::InstallMethod;
use super::CheckStatus; use super::CheckStatus;
use super::DoctorCheck; use super::DoctorCheck;
@@ -49,9 +50,10 @@ pub(super) fn runtime_check() -> DoctorCheck {
/// Verifies that the search command selected by the install context is usable. /// Verifies that the search command selected by the install context is usable.
/// ///
/// Standalone installs should point at a bundled ripgrep binary, while local or /// Package-layout installs should point at a bundled ripgrep binary, while local
/// package-managed installs usually resolve rg from PATH. A warning here means /// installs without that layout usually resolve rg from PATH. A warning here
/// features that depend on file search may degrade even when the CLI launches. /// means features that depend on file search may degrade even when the CLI
/// launches.
pub(super) fn search_check() -> DoctorCheck { pub(super) fn search_check() -> DoctorCheck {
let current_exe = env::current_exe().ok(); let current_exe = env::current_exe().ok();
let install_context = doctor_install_context(current_exe.as_deref()); let install_context = doctor_install_context(current_exe.as_deref());
@@ -109,28 +111,40 @@ pub(super) fn search_check() -> DoctorCheck {
}; };
let mut check = DoctorCheck::new("runtime.search", "search", status, summary).details(details); let mut check = DoctorCheck::new("runtime.search", "search", status, summary).details(details);
if status != CheckStatus::Ok { if status != CheckStatus::Ok {
check = check.remediation("Install ripgrep or repair the bundled standalone resources."); check = check.remediation("Install ripgrep or repair the bundled Codex package.");
} }
check check
} }
fn install_method_name(context: &InstallContext) -> &'static str { fn install_method_name(context: &InstallContext) -> &'static str {
match context { match &context.method {
InstallContext::Standalone { .. } => "standalone", InstallMethod::Standalone { .. } => "standalone",
InstallContext::Npm => "npm", InstallMethod::Npm => "npm",
InstallContext::Bun => "bun", InstallMethod::Bun => "bun",
InstallContext::Brew => "brew", InstallMethod::Brew => "brew",
InstallContext::Other => "local build", InstallMethod::Other => "local build",
} }
} }
fn search_provider(context: &InstallContext) -> &'static str { fn search_provider(context: &InstallContext) -> &'static str {
match context { let rg_command = context.rg_command();
InstallContext::Standalone { let from_package_layout = context
.package_layout
.as_ref()
.and_then(|package_layout| package_layout.path_dir.as_ref())
.is_some_and(|path_dir| rg_command.starts_with(path_dir));
let from_legacy_standalone = matches!(
&context.method,
InstallMethod::Standalone {
resources_dir: Some(resources_dir), resources_dir: Some(resources_dir),
.. ..
} if context.rg_command().starts_with(resources_dir) => "bundled", } if rg_command.starts_with(resources_dir)
_ => "system", );
if from_package_layout || from_legacy_standalone {
"bundled"
} else {
"system"
} }
} }
+21 -14
View File
@@ -10,6 +10,7 @@ use std::path::Path;
use codex_core::config::Config; use codex_core::config::Config;
use codex_install_context::InstallContext; use codex_install_context::InstallContext;
use codex_install_context::InstallMethod;
use serde::Deserialize; use serde::Deserialize;
use super::CheckStatus; use super::CheckStatus;
@@ -129,22 +130,22 @@ fn push_cached_version_details(details: &mut Vec<String>, version_file: &Path) {
} }
fn update_action_label(context: &InstallContext) -> &'static str { fn update_action_label(context: &InstallContext) -> &'static str {
match context { match &context.method {
InstallContext::Npm => "npm install -g @openai/codex", InstallMethod::Npm => "npm install -g @openai/codex",
InstallContext::Bun => "bun install -g @openai/codex", InstallMethod::Bun => "bun install -g @openai/codex",
InstallContext::Brew => "brew upgrade --cask codex", InstallMethod::Brew => "brew upgrade --cask codex",
InstallContext::Standalone { .. } => "standalone installer", InstallMethod::Standalone { .. } => "standalone installer",
InstallContext::Other => "manual or unknown", InstallMethod::Other => "manual or unknown",
} }
} }
fn fetch_latest_version(context: &InstallContext) -> Result<String, String> { fn fetch_latest_version(context: &InstallContext) -> Result<String, String> {
match context { match &context.method {
InstallContext::Brew => fetch_homebrew_cask_version(), InstallMethod::Brew => fetch_homebrew_cask_version(),
InstallContext::Npm InstallMethod::Npm
| InstallContext::Bun | InstallMethod::Bun
| InstallContext::Standalone { .. } | InstallMethod::Standalone { .. }
| InstallContext::Other => fetch_latest_github_release_version(), | InstallMethod::Other => fetch_latest_github_release_version(),
} }
} }
@@ -216,11 +217,17 @@ mod tests {
#[test] #[test]
fn update_action_labels_install_contexts() { fn update_action_labels_install_contexts() {
assert_eq!( assert_eq!(
update_action_label(&InstallContext::Npm), update_action_label(&InstallContext {
method: InstallMethod::Npm,
package_layout: None,
}),
"npm install -g @openai/codex" "npm install -g @openai/codex"
); );
assert_eq!( assert_eq!(
update_action_label(&InstallContext::Other), update_action_label(&InstallContext {
method: InstallMethod::Other,
package_layout: None,
}),
"manual or unknown" "manual or unknown"
); );
} }
+1
View File
@@ -13,6 +13,7 @@ doctest = false
workspace = true workspace = true
[dependencies] [dependencies]
codex-utils-absolute-path = { workspace = true }
codex-utils-home-dir = { workspace = true } codex-utils-home-dir = { workspace = true }
[dev-dependencies] [dev-dependencies]
+311 -61
View File
@@ -1,7 +1,13 @@
use std::ffi::OsStr;
use std::path::Path; use std::path::Path;
use std::path::PathBuf; use std::path::PathBuf;
use std::sync::OnceLock; use std::sync::OnceLock;
use codex_utils_absolute_path::AbsolutePathBuf;
const BIN_DIRNAME: &str = "bin";
const PACKAGE_METADATA_FILENAME: &str = "codex-package.json";
const PATH_DIRNAME: &str = "codex-path";
const RELEASES_DIRNAME: &str = "releases"; const RELEASES_DIRNAME: &str = "releases";
const RESOURCES_DIRNAME: &str = "codex-resources"; const RESOURCES_DIRNAME: &str = "codex-resources";
const STANDALONE_PACKAGES_DIRNAME: &str = "standalone"; const STANDALONE_PACKAGES_DIRNAME: &str = "standalone";
@@ -14,14 +20,34 @@ pub enum StandalonePlatform {
} }
#[derive(Clone, Debug, Eq, PartialEq)] #[derive(Clone, Debug, Eq, PartialEq)]
pub enum InstallContext { pub struct CodexPackageLayout {
/// The package root that contains the metadata file and layout directories.
pub package_dir: AbsolutePathBuf,
/// Directory containing the Codex entrypoint executable.
pub bin_dir: AbsolutePathBuf,
/// Directory containing managed helper binaries and data files, when present.
pub resources_dir: Option<AbsolutePathBuf>,
/// Folder that should be prepended to the PATH, when present.
pub path_dir: Option<AbsolutePathBuf>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct InstallContext {
pub method: InstallMethod,
pub package_layout: Option<CodexPackageLayout>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum InstallMethod {
Standalone { Standalone {
/// The managed standalone release directory, for example /// The managed standalone release directory. Legacy installs use paths
/// such as
/// `~/.codex/packages/standalone/releases/0.111.0-x86_64-unknown-linux-musl`. /// `~/.codex/packages/standalone/releases/0.111.0-x86_64-unknown-linux-musl`.
release_dir: PathBuf, /// Package-layout installs use the package root that contains `bin/`,
/// The bundled resource directory that sits next to the executable when /// `codex-resources/`, and `codex-path/`.
/// this install ships managed dependencies. release_dir: AbsolutePathBuf,
resources_dir: Option<PathBuf>, /// The bundled resource directory for managed dependencies.
resources_dir: Option<AbsolutePathBuf>,
/// The platform of the standalone release, either `Unix` or `Windows`. /// The platform of the standalone release, either `Unix` or `Windows`.
platform: StandalonePlatform, platform: StandalonePlatform,
}, },
@@ -62,28 +88,21 @@ impl InstallContext {
managed_by_bun: bool, managed_by_bun: bool,
codex_home: Option<&Path>, codex_home: Option<&Path>,
) -> Self { ) -> Self {
if managed_by_npm { let package_layout = current_exe.and_then(CodexPackageLayout::from_exe);
return Self::Npm; let method = if managed_by_npm {
} InstallMethod::Npm
} else if managed_by_bun {
InstallMethod::Bun
} else if let Some(exe_path) = current_exe {
install_method_from_exe(exe_path, codex_home, package_layout.as_ref(), is_macos)
} else {
InstallMethod::Other
};
if managed_by_bun { Self {
return Self::Bun; method,
package_layout,
} }
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 { pub fn current() -> &'static Self {
@@ -101,53 +120,102 @@ impl InstallContext {
} }
pub fn rg_command(&self) -> PathBuf { pub fn rg_command(&self) -> PathBuf {
match self { if let Some(package_layout) = &self.package_layout
Self::Standalone { && let Some(path_dir) = &package_layout.path_dir
resources_dir: Some(resources_dir), {
.. let bundled_rg = path_dir.join(default_rg_command());
} => { if bundled_rg.exists() {
let bundled_rg = resources_dir.join(default_rg_command()); return bundled_rg.into_path_buf();
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(),
} }
if let InstallMethod::Standalone {
resources_dir: Some(resources_dir),
..
} = &self.method
{
let bundled_rg = resources_dir.join(default_rg_command());
if bundled_rg.exists() {
return bundled_rg.into_path_buf();
}
}
default_rg_command()
} }
} }
fn standalone_install_context( impl CodexPackageLayout {
fn from_exe(exe_path: &Path) -> Option<Self> {
let canonical_exe = canonical_absolute_path(exe_path)?;
let bin_dir = canonical_exe.parent()?;
if bin_dir.file_name() != Some(OsStr::new(BIN_DIRNAME)) {
return None;
}
let package_dir = bin_dir.parent()?;
if !package_dir.join(PACKAGE_METADATA_FILENAME).is_file() {
return None;
}
Some(Self {
resources_dir: existing_dir(package_dir.join(RESOURCES_DIRNAME)),
path_dir: existing_dir(package_dir.join(PATH_DIRNAME)),
package_dir,
bin_dir,
})
}
}
fn install_method_from_exe(
exe_path: &Path, exe_path: &Path,
codex_home: Option<&Path>, codex_home: Option<&Path>,
) -> Option<InstallContext> { package_layout: Option<&CodexPackageLayout>,
let canonical_exe = std::fs::canonicalize(exe_path).ok()?; is_macos: bool,
let canonical_codex_home = std::fs::canonicalize(codex_home?).ok()?; ) -> InstallMethod {
let release_dir = canonical_exe.parent()?.to_path_buf(); if let Some(standalone_method) = standalone_install_method(exe_path, codex_home, package_layout)
{
return standalone_method;
}
if is_macos && (exe_path.starts_with("/opt/homebrew") || exe_path.starts_with("/usr/local")) {
InstallMethod::Brew
} else {
InstallMethod::Other
}
}
fn standalone_install_method(
exe_path: &Path,
codex_home: Option<&Path>,
package_layout: Option<&CodexPackageLayout>,
) -> Option<InstallMethod> {
let canonical_codex_home = canonical_absolute_path(codex_home?)?;
let release_dir = if let Some(package_layout) = package_layout {
package_layout.package_dir.clone()
} else {
canonical_absolute_path(exe_path)?.parent()?
};
let releases_root = canonical_codex_home let releases_root = canonical_codex_home
.join("packages") .join("packages")
.join(STANDALONE_PACKAGES_DIRNAME) .join(STANDALONE_PACKAGES_DIRNAME)
.join(RELEASES_DIRNAME); .join(RELEASES_DIRNAME);
if !release_dir.starts_with(releases_root) { if !release_dir.starts_with(releases_root.as_path()) {
return None; return None;
} }
let resources_dir = release_dir.join(RESOURCES_DIRNAME); let resources_dir = release_dir.join(RESOURCES_DIRNAME);
Some(InstallContext::Standalone { Some(InstallMethod::Standalone {
release_dir, release_dir,
resources_dir: resources_dir.is_dir().then_some(resources_dir), resources_dir: resources_dir.is_dir().then_some(resources_dir),
platform: standalone_platform(), platform: standalone_platform(),
}) })
} }
fn canonical_absolute_path(path: &Path) -> Option<AbsolutePathBuf> {
let canonical_path = std::fs::canonicalize(path).ok()?;
AbsolutePathBuf::from_absolute_path(canonical_path).ok()
}
fn standalone_platform() -> StandalonePlatform { fn standalone_platform() -> StandalonePlatform {
if cfg!(windows) { if cfg!(windows) {
StandalonePlatform::Windows StandalonePlatform::Windows
@@ -156,6 +224,10 @@ fn standalone_platform() -> StandalonePlatform {
} }
} }
fn existing_dir(path: AbsolutePathBuf) -> Option<AbsolutePathBuf> {
path.is_dir().then_some(path)
}
fn default_rg_command() -> PathBuf { fn default_rg_command() -> PathBuf {
if cfg!(windows) { if cfg!(windows) {
PathBuf::from("rg.exe") PathBuf::from("rg.exe")
@@ -181,8 +253,10 @@ mod tests {
let exe_path = release_dir.join(if cfg!(windows) { "codex.exe" } else { "codex" }); let exe_path = release_dir.join(if cfg!(windows) { "codex.exe" } else { "codex" });
fs::write(&exe_path, "")?; fs::write(&exe_path, "")?;
fs::write(resources_dir.join(default_rg_command()), "")?; fs::write(resources_dir.join(default_rg_command()), "")?;
let canonical_release_dir = release_dir.canonicalize()?; let canonical_release_dir =
let canonical_resources_dir = resources_dir.canonicalize()?; AbsolutePathBuf::from_absolute_path(release_dir.canonicalize()?)?;
let canonical_resources_dir =
AbsolutePathBuf::from_absolute_path(resources_dir.canonicalize()?)?;
let context = InstallContext::from_exe_with_codex_home( let context = InstallContext::from_exe_with_codex_home(
/*is_macos*/ false, /*is_macos*/ false,
@@ -193,10 +267,13 @@ mod tests {
); );
assert_eq!( assert_eq!(
context, context,
InstallContext::Standalone { InstallContext {
release_dir: canonical_release_dir, method: InstallMethod::Standalone {
resources_dir: Some(canonical_resources_dir), release_dir: canonical_release_dir,
platform: standalone_platform(), resources_dir: Some(canonical_resources_dir),
platform: standalone_platform(),
},
package_layout: None,
} }
); );
Ok(()) Ok(())
@@ -223,6 +300,161 @@ mod tests {
Ok(()) Ok(())
} }
#[test]
fn detects_package_layout_independently_from_install_method() -> std::io::Result<()> {
let package_dir = tempfile::tempdir()?;
let bin_dir = package_dir.path().join(BIN_DIRNAME);
let resources_dir = package_dir.path().join(RESOURCES_DIRNAME);
let path_dir = package_dir.path().join(PATH_DIRNAME);
fs::create_dir_all(&bin_dir)?;
fs::create_dir_all(&resources_dir)?;
fs::create_dir_all(&path_dir)?;
fs::write(package_dir.path().join(PACKAGE_METADATA_FILENAME), "{}")?;
let exe_path = bin_dir.join(if cfg!(windows) { "codex.exe" } else { "codex" });
fs::write(&exe_path, "")?;
fs::write(path_dir.join(default_rg_command()), "")?;
let canonical_package_dir =
AbsolutePathBuf::from_absolute_path(package_dir.path().canonicalize()?)?;
let canonical_bin_dir = AbsolutePathBuf::from_absolute_path(bin_dir.canonicalize()?)?;
let canonical_resources_dir =
AbsolutePathBuf::from_absolute_path(resources_dir.canonicalize()?)?;
let canonical_path_dir = AbsolutePathBuf::from_absolute_path(path_dir.canonicalize()?)?;
let package_layout = CodexPackageLayout {
package_dir: canonical_package_dir,
bin_dir: canonical_bin_dir,
resources_dir: Some(canonical_resources_dir),
path_dir: Some(canonical_path_dir.clone()),
};
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*/ None,
);
assert_eq!(
context,
InstallContext {
method: InstallMethod::Other,
package_layout: Some(package_layout),
}
);
assert_eq!(
context.rg_command(),
canonical_path_dir
.join(default_rg_command())
.into_path_buf()
);
Ok(())
}
#[test]
fn standalone_package_layout_keeps_standalone_install_method() -> std::io::Result<()> {
let codex_home = tempfile::tempdir()?;
let package_dir = codex_home
.path()
.join("packages/standalone/releases/1.2.3-x86_64-unknown-linux-musl");
let bin_dir = package_dir.join(BIN_DIRNAME);
let resources_dir = package_dir.join(RESOURCES_DIRNAME);
let path_dir = package_dir.join(PATH_DIRNAME);
fs::create_dir_all(&bin_dir)?;
fs::create_dir_all(&resources_dir)?;
fs::create_dir_all(&path_dir)?;
fs::write(package_dir.join(PACKAGE_METADATA_FILENAME), "{}")?;
let exe_path = bin_dir.join(if cfg!(windows) { "codex.exe" } else { "codex" });
fs::write(&exe_path, "")?;
fs::write(path_dir.join(default_rg_command()), "")?;
let canonical_package_dir =
AbsolutePathBuf::from_absolute_path(package_dir.canonicalize()?)?;
let canonical_bin_dir = AbsolutePathBuf::from_absolute_path(bin_dir.canonicalize()?)?;
let canonical_resources_dir =
AbsolutePathBuf::from_absolute_path(resources_dir.canonicalize()?)?;
let canonical_path_dir = AbsolutePathBuf::from_absolute_path(path_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 {
method: InstallMethod::Standalone {
release_dir: canonical_package_dir.clone(),
resources_dir: Some(canonical_resources_dir.clone()),
platform: standalone_platform(),
},
package_layout: Some(CodexPackageLayout {
package_dir: canonical_package_dir,
bin_dir: canonical_bin_dir,
resources_dir: Some(canonical_resources_dir),
path_dir: Some(canonical_path_dir.clone()),
}),
}
);
assert_eq!(
context.rg_command(),
canonical_path_dir
.join(default_rg_command())
.into_path_buf()
);
Ok(())
}
#[test]
fn npm_managed_package_keeps_package_layout() -> std::io::Result<()> {
let package_dir = tempfile::tempdir()?;
let bin_dir = package_dir.path().join(BIN_DIRNAME);
let path_dir = package_dir.path().join(PATH_DIRNAME);
fs::create_dir_all(&bin_dir)?;
fs::create_dir_all(&path_dir)?;
fs::write(package_dir.path().join(PACKAGE_METADATA_FILENAME), "{}")?;
let exe_path = bin_dir.join(if cfg!(windows) { "codex.exe" } else { "codex" });
fs::write(&exe_path, "")?;
fs::write(path_dir.join(default_rg_command()), "")?;
let canonical_path_dir = AbsolutePathBuf::from_absolute_path(path_dir.canonicalize()?)?;
let context = InstallContext::from_exe_with_codex_home(
/*is_macos*/ false,
/*current_exe*/ Some(&exe_path),
/*managed_by_npm*/ true,
/*managed_by_bun*/ false,
/*codex_home*/ None,
);
assert_eq!(context.method, InstallMethod::Npm);
assert!(context.package_layout.is_some());
assert_eq!(
context.rg_command(),
canonical_path_dir
.join(default_rg_command())
.into_path_buf()
);
Ok(())
}
#[test]
fn standalone_package_rg_falls_back_when_codex_path_is_missing() -> std::io::Result<()> {
let package_dir = tempfile::tempdir()?;
let bin_dir = package_dir.path().join(BIN_DIRNAME);
fs::create_dir_all(&bin_dir)?;
fs::write(package_dir.path().join(PACKAGE_METADATA_FILENAME), "{}")?;
let exe_path = bin_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*/ None,
);
assert_eq!(context.rg_command(), default_rg_command());
Ok(())
}
#[test] #[test]
fn npm_and_bun_take_precedence() { fn npm_and_bun_take_precedence() {
let npm_context = InstallContext::from_exe_with_codex_home( let npm_context = InstallContext::from_exe_with_codex_home(
@@ -232,7 +464,13 @@ mod tests {
/*managed_by_bun*/ false, /*managed_by_bun*/ false,
/*codex_home*/ None, /*codex_home*/ None,
); );
assert_eq!(npm_context, InstallContext::Npm); assert_eq!(
npm_context,
InstallContext {
method: InstallMethod::Npm,
package_layout: None,
}
);
let bun_context = InstallContext::from_exe_with_codex_home( let bun_context = InstallContext::from_exe_with_codex_home(
/*is_macos*/ false, /*is_macos*/ false,
@@ -241,7 +479,13 @@ mod tests {
/*managed_by_bun*/ true, /*managed_by_bun*/ true,
/*codex_home*/ None, /*codex_home*/ None,
); );
assert_eq!(bun_context, InstallContext::Bun); assert_eq!(
bun_context,
InstallContext {
method: InstallMethod::Bun,
package_layout: None,
}
);
} }
#[test] #[test]
@@ -253,6 +497,12 @@ mod tests {
/*managed_by_bun*/ false, /*managed_by_bun*/ false,
/*codex_home*/ None, /*codex_home*/ None,
); );
assert_eq!(context, InstallContext::Brew); assert_eq!(
context,
InstallContext {
method: InstallMethod::Brew,
package_layout: None,
}
);
} }
} }
+42 -20
View File
@@ -1,6 +1,8 @@
#[cfg(any(not(debug_assertions), test))] #[cfg(any(not(debug_assertions), test))]
use codex_install_context::InstallContext; use codex_install_context::InstallContext;
#[cfg(any(not(debug_assertions), test))] #[cfg(any(not(debug_assertions), test))]
use codex_install_context::InstallMethod;
#[cfg(any(not(debug_assertions), test))]
use codex_install_context::StandalonePlatform; use codex_install_context::StandalonePlatform;
/// Update action the CLI should perform after the TUI exits. /// Update action the CLI should perform after the TUI exits.
@@ -21,15 +23,15 @@ pub enum UpdateAction {
impl UpdateAction { impl UpdateAction {
#[cfg(any(not(debug_assertions), test))] #[cfg(any(not(debug_assertions), test))]
pub(crate) fn from_install_context(context: &InstallContext) -> Option<Self> { pub(crate) fn from_install_context(context: &InstallContext) -> Option<Self> {
match context { match &context.method {
InstallContext::Npm => Some(UpdateAction::NpmGlobalLatest), InstallMethod::Npm => Some(UpdateAction::NpmGlobalLatest),
InstallContext::Bun => Some(UpdateAction::BunGlobalLatest), InstallMethod::Bun => Some(UpdateAction::BunGlobalLatest),
InstallContext::Brew => Some(UpdateAction::BrewUpgrade), InstallMethod::Brew => Some(UpdateAction::BrewUpgrade),
InstallContext::Standalone { platform, .. } => Some(match platform { InstallMethod::Standalone { platform, .. } => Some(match platform {
StandalonePlatform::Unix => UpdateAction::StandaloneUnix, StandalonePlatform::Unix => UpdateAction::StandaloneUnix,
StandalonePlatform::Windows => UpdateAction::StandaloneWindows, StandalonePlatform::Windows => UpdateAction::StandaloneWindows,
}), }),
InstallContext::Other => None, InstallMethod::Other => None,
} }
} }
@@ -66,42 +68,62 @@ pub fn get_update_action() -> Option<UpdateAction> {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use codex_utils_absolute_path::AbsolutePathBuf;
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
use std::path::PathBuf;
#[test] #[test]
fn maps_install_context_to_update_action() { fn maps_install_context_to_update_action() {
let native_release_dir = PathBuf::from("/tmp/native-release"); let native_release_dir =
AbsolutePathBuf::from_absolute_path(std::env::temp_dir().join("native-release"))
.expect("temp dir path should be absolute");
assert_eq!( assert_eq!(
UpdateAction::from_install_context(&InstallContext::Other), UpdateAction::from_install_context(&InstallContext {
method: InstallMethod::Other,
package_layout: None,
}),
None None
); );
assert_eq!( assert_eq!(
UpdateAction::from_install_context(&InstallContext::Npm), UpdateAction::from_install_context(&InstallContext {
method: InstallMethod::Npm,
package_layout: None,
}),
Some(UpdateAction::NpmGlobalLatest) Some(UpdateAction::NpmGlobalLatest)
); );
assert_eq!( assert_eq!(
UpdateAction::from_install_context(&InstallContext::Bun), UpdateAction::from_install_context(&InstallContext {
method: InstallMethod::Bun,
package_layout: None,
}),
Some(UpdateAction::BunGlobalLatest) Some(UpdateAction::BunGlobalLatest)
); );
assert_eq!( assert_eq!(
UpdateAction::from_install_context(&InstallContext::Brew), UpdateAction::from_install_context(&InstallContext {
method: InstallMethod::Brew,
package_layout: None,
}),
Some(UpdateAction::BrewUpgrade) Some(UpdateAction::BrewUpgrade)
); );
assert_eq!( assert_eq!(
UpdateAction::from_install_context(&InstallContext::Standalone { UpdateAction::from_install_context(&InstallContext {
platform: StandalonePlatform::Unix, method: InstallMethod::Standalone {
release_dir: native_release_dir.clone(), platform: StandalonePlatform::Unix,
resources_dir: Some(native_release_dir.join("codex-resources")), release_dir: native_release_dir.clone(),
resources_dir: Some(native_release_dir.join("codex-resources")),
},
package_layout: None,
}), }),
Some(UpdateAction::StandaloneUnix) Some(UpdateAction::StandaloneUnix)
); );
assert_eq!( assert_eq!(
UpdateAction::from_install_context(&InstallContext::Standalone { UpdateAction::from_install_context(&InstallContext {
platform: StandalonePlatform::Windows, method: InstallMethod::Standalone {
release_dir: native_release_dir.clone(), platform: StandalonePlatform::Windows,
resources_dir: Some(native_release_dir.join("codex-resources")), release_dir: native_release_dir.clone(),
resources_dir: Some(native_release_dir.join("codex-resources")),
},
package_layout: None,
}), }),
Some(UpdateAction::StandaloneWindows) Some(UpdateAction::StandaloneWindows)
); );