mirror of
https://github.com/openai/codex.git
synced 2026-09-06 15:29:32 +00:00
runtime: detect Codex package layout
This commit is contained in:
1
codex-rs/Cargo.lock
generated
1
codex-rs/Cargo.lock
generated
@@ -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",
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ doctest = false
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
codex-utils-absolute-path = { workspace = true }
|
||||
codex-utils-home-dir = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user