mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
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:
committed by
GitHub
Unverified
parent
57a68fb9e3
commit
cfa16fcc2e
@@ -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,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user