runtime: detect Codex package layout

This commit is contained in:
Michael Bolin
2026-05-19 22:10:18 -07:00
parent 0f07206297
commit af339f3a15
7 changed files with 467 additions and 125 deletions

1
codex-rs/Cargo.lock generated
View File

@@ -2997,6 +2997,7 @@ dependencies = [
name = "codex-install-context"
version = "0.0.0"
dependencies = [
"codex-utils-absolute-path",
"codex-utils-home-dir",
"pretty_assertions",
"tempfile",

View File

@@ -39,7 +39,9 @@ use codex_core::config::ConfigBuilder;
use codex_core::config::ConfigOverrides;
use codex_core::config::find_codex_home;
use codex_features::FEATURES;
use codex_install_context::CodexPackageLayout;
use codex_install_context::InstallContext;
use codex_install_context::InstallMethod;
use codex_install_context::StandalonePlatform;
use codex_login::AuthDotJson;
use codex_login::AuthManager;
@@ -812,7 +814,10 @@ fn installation_check(show_details: bool) -> DoctorCheck {
fn doctor_install_context(current_exe: Option<&Path>) -> InstallContext {
if inherited_managed_env_for_cargo_binary(current_exe) {
InstallContext::Other
InstallContext {
method: InstallMethod::Other,
package_layout: None,
}
} else {
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 {
match context {
InstallContext::Standalone {
match &context.method {
InstallMethod::Standalone {
release_dir,
resources_dir,
platform,
@@ -853,22 +858,63 @@ fn describe_install_context(context: &InstallContext) -> String {
StandalonePlatform::Unix => "unix",
StandalonePlatform::Windows => "windows",
};
let resources = resources_dir
.as_ref()
.map(|path| path.display().to_string())
.unwrap_or_else(|| "none".to_string());
match &context.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!(
"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!(
"standalone ({platform}, release {}, resources {resources})",
release_dir.display()
"{method} (package {}, bin {}, resources {resources}, path {path})",
package_layout.package_dir.display(),
package_layout.bin_dir.display()
)
}
InstallContext::Npm => "npm".to_string(),
InstallContext::Bun => "bun".to_string(),
InstallContext::Brew => "brew".to_string(),
InstallContext::Other => "other".to_string(),
None => method.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)]
enum NpmRootCheck {
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>) {
let InstallContext::Standalone { release_dir, .. } = InstallContext::current() else {
let context = InstallContext::current();
let InstallMethod::Standalone { release_dir, .. } = &context.method else {
return;
};
let Some(releases_dir) = release_dir.parent() else {
return;
};
let Ok(entries) = std::fs::read_dir(releases_dir) else {
let Ok(entries) = std::fs::read_dir(&releases_dir) else {
return;
};
let release_count = entries.filter_map(Result::ok).count();

View File

@@ -3,12 +3,13 @@
//! Runtime diagnostics answer provenance questions that are hard to infer from
//! user reports: which binary is running, which install channel it resembles,
//! 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::process::Command;
use codex_install_context::InstallContext;
use codex_install_context::InstallMethod;
use super::CheckStatus;
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.
///
/// Standalone installs should point at a bundled ripgrep binary, while local or
/// package-managed installs usually resolve rg from PATH. A warning here means
/// features that depend on file search may degrade even when the CLI launches.
/// Package-layout installs should point at a bundled ripgrep binary, while local
/// installs without that layout usually resolve rg from PATH. A warning here
/// means features that depend on file search may degrade even when the CLI
/// launches.
pub(super) fn search_check() -> DoctorCheck {
let current_exe = env::current_exe().ok();
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);
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
}
fn install_method_name(context: &InstallContext) -> &'static str {
match context {
InstallContext::Standalone { .. } => "standalone",
InstallContext::Npm => "npm",
InstallContext::Bun => "bun",
InstallContext::Brew => "brew",
InstallContext::Other => "local build",
match &context.method {
InstallMethod::Standalone { .. } => "standalone",
InstallMethod::Npm => "npm",
InstallMethod::Bun => "bun",
InstallMethod::Brew => "brew",
InstallMethod::Other => "local build",
}
}
fn search_provider(context: &InstallContext) -> &'static str {
match context {
InstallContext::Standalone {
let rg_command = context.rg_command();
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),
..
} if context.rg_command().starts_with(resources_dir) => "bundled",
_ => "system",
} if rg_command.starts_with(resources_dir)
);
if from_package_layout || from_legacy_standalone {
"bundled"
} else {
"system"
}
}

View File

@@ -10,6 +10,7 @@ use std::path::Path;
use codex_core::config::Config;
use codex_install_context::InstallContext;
use codex_install_context::InstallMethod;
use serde::Deserialize;
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 {
match context {
InstallContext::Npm => "npm install -g @openai/codex",
InstallContext::Bun => "bun install -g @openai/codex",
InstallContext::Brew => "brew upgrade --cask codex",
InstallContext::Standalone { .. } => "standalone installer",
InstallContext::Other => "manual or unknown",
match &context.method {
InstallMethod::Npm => "npm install -g @openai/codex",
InstallMethod::Bun => "bun install -g @openai/codex",
InstallMethod::Brew => "brew upgrade --cask codex",
InstallMethod::Standalone { .. } => "standalone installer",
InstallMethod::Other => "manual or unknown",
}
}
fn fetch_latest_version(context: &InstallContext) -> Result<String, String> {
match context {
InstallContext::Brew => fetch_homebrew_cask_version(),
InstallContext::Npm
| InstallContext::Bun
| InstallContext::Standalone { .. }
| InstallContext::Other => fetch_latest_github_release_version(),
match &context.method {
InstallMethod::Brew => fetch_homebrew_cask_version(),
InstallMethod::Npm
| InstallMethod::Bun
| InstallMethod::Standalone { .. }
| InstallMethod::Other => fetch_latest_github_release_version(),
}
}
@@ -216,11 +217,17 @@ mod tests {
#[test]
fn update_action_labels_install_contexts() {
assert_eq!(
update_action_label(&InstallContext::Npm),
update_action_label(&InstallContext {
method: InstallMethod::Npm,
package_layout: None,
}),
"npm install -g @openai/codex"
);
assert_eq!(
update_action_label(&InstallContext::Other),
update_action_label(&InstallContext {
method: InstallMethod::Other,
package_layout: None,
}),
"manual or unknown"
);
}

View File

@@ -13,6 +13,7 @@ doctest = false
workspace = true
[dependencies]
codex-utils-absolute-path = { workspace = true }
codex-utils-home-dir = { workspace = true }
[dev-dependencies]

View File

@@ -1,7 +1,13 @@
use std::ffi::OsStr;
use std::path::Path;
use std::path::PathBuf;
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 RESOURCES_DIRNAME: &str = "codex-resources";
const STANDALONE_PACKAGES_DIRNAME: &str = "standalone";
@@ -14,14 +20,34 @@ pub enum StandalonePlatform {
}
#[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 {
/// 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`.
release_dir: PathBuf,
/// The bundled resource directory that sits next to the executable when
/// this install ships managed dependencies.
resources_dir: Option<PathBuf>,
/// Package-layout installs use the package root that contains `bin/`,
/// `codex-resources/`, and `codex-path/`.
release_dir: AbsolutePathBuf,
/// The bundled resource directory for managed dependencies.
resources_dir: Option<AbsolutePathBuf>,
/// The platform of the standalone release, either `Unix` or `Windows`.
platform: StandalonePlatform,
},
@@ -62,28 +88,21 @@ impl InstallContext {
managed_by_bun: bool,
codex_home: Option<&Path>,
) -> Self {
if managed_by_npm {
return Self::Npm;
}
let package_layout = current_exe.and_then(CodexPackageLayout::from_exe);
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 {
return Self::Bun;
Self {
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 {
@@ -101,53 +120,102 @@ impl InstallContext {
}
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()
}
if let Some(package_layout) = &self.package_layout
&& let Some(path_dir) = &package_layout.path_dir
{
let bundled_rg = path_dir.join(default_rg_command());
if bundled_rg.exists() {
return bundled_rg.into_path_buf();
}
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,
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();
package_layout: Option<&CodexPackageLayout>,
is_macos: bool,
) -> InstallMethod {
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
.join("packages")
.join(STANDALONE_PACKAGES_DIRNAME)
.join(RELEASES_DIRNAME);
if !release_dir.starts_with(releases_root) {
if !release_dir.starts_with(releases_root.as_path()) {
return None;
}
let resources_dir = release_dir.join(RESOURCES_DIRNAME);
Some(InstallContext::Standalone {
Some(InstallMethod::Standalone {
release_dir,
resources_dir: resources_dir.is_dir().then_some(resources_dir),
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 {
if cfg!(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 {
if cfg!(windows) {
PathBuf::from("rg.exe")
@@ -181,8 +253,10 @@ mod tests {
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 canonical_release_dir =
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(
/*is_macos*/ false,
@@ -193,10 +267,13 @@ mod tests {
);
assert_eq!(
context,
InstallContext::Standalone {
release_dir: canonical_release_dir,
resources_dir: Some(canonical_resources_dir),
platform: standalone_platform(),
InstallContext {
method: InstallMethod::Standalone {
release_dir: canonical_release_dir,
resources_dir: Some(canonical_resources_dir),
platform: standalone_platform(),
},
package_layout: None,
}
);
Ok(())
@@ -223,6 +300,161 @@ mod tests {
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]
fn npm_and_bun_take_precedence() {
let npm_context = InstallContext::from_exe_with_codex_home(
@@ -232,7 +464,13 @@ mod tests {
/*managed_by_bun*/ false,
/*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(
/*is_macos*/ false,
@@ -241,7 +479,13 @@ mod tests {
/*managed_by_bun*/ true,
/*codex_home*/ None,
);
assert_eq!(bun_context, InstallContext::Bun);
assert_eq!(
bun_context,
InstallContext {
method: InstallMethod::Bun,
package_layout: None,
}
);
}
#[test]
@@ -253,6 +497,12 @@ mod tests {
/*managed_by_bun*/ false,
/*codex_home*/ None,
);
assert_eq!(context, InstallContext::Brew);
assert_eq!(
context,
InstallContext {
method: InstallMethod::Brew,
package_layout: None,
}
);
}
}

View File

@@ -1,6 +1,8 @@
#[cfg(any(not(debug_assertions), test))]
use codex_install_context::InstallContext;
#[cfg(any(not(debug_assertions), test))]
use codex_install_context::InstallMethod;
#[cfg(any(not(debug_assertions), test))]
use codex_install_context::StandalonePlatform;
/// Update action the CLI should perform after the TUI exits.
@@ -21,15 +23,15 @@ pub enum UpdateAction {
impl UpdateAction {
#[cfg(any(not(debug_assertions), test))]
pub(crate) fn from_install_context(context: &InstallContext) -> Option<Self> {
match context {
InstallContext::Npm => Some(UpdateAction::NpmGlobalLatest),
InstallContext::Bun => Some(UpdateAction::BunGlobalLatest),
InstallContext::Brew => Some(UpdateAction::BrewUpgrade),
InstallContext::Standalone { platform, .. } => Some(match platform {
match &context.method {
InstallMethod::Npm => Some(UpdateAction::NpmGlobalLatest),
InstallMethod::Bun => Some(UpdateAction::BunGlobalLatest),
InstallMethod::Brew => Some(UpdateAction::BrewUpgrade),
InstallMethod::Standalone { platform, .. } => Some(match platform {
StandalonePlatform::Unix => UpdateAction::StandaloneUnix,
StandalonePlatform::Windows => UpdateAction::StandaloneWindows,
}),
InstallContext::Other => None,
InstallMethod::Other => None,
}
}
@@ -66,42 +68,62 @@ pub fn get_update_action() -> Option<UpdateAction> {
#[cfg(test)]
mod tests {
use super::*;
use codex_utils_absolute_path::AbsolutePathBuf;
use pretty_assertions::assert_eq;
use std::path::PathBuf;
#[test]
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!(
UpdateAction::from_install_context(&InstallContext::Other),
UpdateAction::from_install_context(&InstallContext {
method: InstallMethod::Other,
package_layout: None,
}),
None
);
assert_eq!(
UpdateAction::from_install_context(&InstallContext::Npm),
UpdateAction::from_install_context(&InstallContext {
method: InstallMethod::Npm,
package_layout: None,
}),
Some(UpdateAction::NpmGlobalLatest)
);
assert_eq!(
UpdateAction::from_install_context(&InstallContext::Bun),
UpdateAction::from_install_context(&InstallContext {
method: InstallMethod::Bun,
package_layout: None,
}),
Some(UpdateAction::BunGlobalLatest)
);
assert_eq!(
UpdateAction::from_install_context(&InstallContext::Brew),
UpdateAction::from_install_context(&InstallContext {
method: InstallMethod::Brew,
package_layout: None,
}),
Some(UpdateAction::BrewUpgrade)
);
assert_eq!(
UpdateAction::from_install_context(&InstallContext::Standalone {
platform: StandalonePlatform::Unix,
release_dir: native_release_dir.clone(),
resources_dir: Some(native_release_dir.join("codex-resources")),
UpdateAction::from_install_context(&InstallContext {
method: InstallMethod::Standalone {
platform: StandalonePlatform::Unix,
release_dir: native_release_dir.clone(),
resources_dir: Some(native_release_dir.join("codex-resources")),
},
package_layout: None,
}),
Some(UpdateAction::StandaloneUnix)
);
assert_eq!(
UpdateAction::from_install_context(&InstallContext::Standalone {
platform: StandalonePlatform::Windows,
release_dir: native_release_dir.clone(),
resources_dir: Some(native_release_dir.join("codex-resources")),
UpdateAction::from_install_context(&InstallContext {
method: InstallMethod::Standalone {
platform: StandalonePlatform::Windows,
release_dir: native_release_dir.clone(),
resources_dir: Some(native_release_dir.join("codex-resources")),
},
package_layout: None,
}),
Some(UpdateAction::StandaloneWindows)
);