diff --git a/.github/workflows/rust-release.yml b/.github/workflows/rust-release.yml index 2ddad10bf2..ff5f48f029 100644 --- a/.github/workflows/rust-release.yml +++ b/.github/workflows/rust-release.yml @@ -352,11 +352,13 @@ jobs: rm -rf "$bundle_root" mkdir -p "$bundle_root/codex-resources" cp "$dest/codex-${{ matrix.target }}" "$bundle_root/codex" + cp "$dest/codex-code-mode-host-${{ matrix.target }}" "$bundle_root/codex-resources/codex-code-mode-host" cp "$dest/bwrap-${{ matrix.target }}" "$bundle_root/codex-resources/bwrap" chmod 0755 \ "$bundle_root/codex" \ + "$bundle_root/codex-resources/codex-code-mode-host" \ "$bundle_root/codex-resources/bwrap" - tar -C "$bundle_root" -cf - codex codex-resources/bwrap | + tar -C "$bundle_root" -cf - codex codex-resources/codex-code-mode-host codex-resources/bwrap | zstd -T0 -19 -o "$dest/codex-${{ matrix.target }}-bundle.tar.zst" fi diff --git a/codex-rs/cli/src/doctor.rs b/codex-rs/cli/src/doctor.rs index 7f06a611bc..57f3a914c6 100644 --- a/codex-rs/cli/src/doctor.rs +++ b/codex-rs/cli/src/doctor.rs @@ -936,6 +936,14 @@ fn describe_install_context(context: &InstallContext) -> String { } } } + InstallMethod::DirectBundle { + bundle_dir, + resources_dir, + } => format!( + "direct bundle (root {}, resources {})", + bundle_dir.display(), + resources_dir.display() + ), InstallMethod::Npm => { describe_method_with_package_layout("npm", context.package_layout.as_ref()) } diff --git a/codex-rs/cli/src/doctor/runtime.rs b/codex-rs/cli/src/doctor/runtime.rs index d805c7c6b2..87da645896 100644 --- a/codex-rs/cli/src/doctor/runtime.rs +++ b/codex-rs/cli/src/doctor/runtime.rs @@ -119,6 +119,7 @@ pub(super) fn search_check() -> DoctorCheck { fn install_method_name(context: &InstallContext) -> &'static str { match &context.method { InstallMethod::Standalone { .. } => "standalone", + InstallMethod::DirectBundle { .. } => "direct bundle", InstallMethod::Npm => "npm", InstallMethod::Bun => "bun", InstallMethod::Pnpm => "pnpm", diff --git a/codex-rs/cli/src/doctor/updates.rs b/codex-rs/cli/src/doctor/updates.rs index b13de06d14..9b19278c25 100644 --- a/codex-rs/cli/src/doctor/updates.rs +++ b/codex-rs/cli/src/doctor/updates.rs @@ -136,6 +136,7 @@ fn update_action_label(context: &InstallContext) -> &'static str { InstallMethod::Pnpm => "pnpm add -g @openai/codex", InstallMethod::Brew => "brew upgrade --cask codex", InstallMethod::Standalone { .. } => "standalone installer", + InstallMethod::DirectBundle { .. } => "manual direct bundle", InstallMethod::Other => "manual or unknown", } } @@ -147,6 +148,7 @@ fn fetch_latest_version(context: &InstallContext) -> Result { | InstallMethod::Bun | InstallMethod::Pnpm | InstallMethod::Standalone { .. } + | InstallMethod::DirectBundle { .. } | InstallMethod::Other => fetch_latest_github_release_version(), } } diff --git a/codex-rs/install-context/src/lib.rs b/codex-rs/install-context/src/lib.rs index e660718c56..7960d59c12 100644 --- a/codex-rs/install-context/src/lib.rs +++ b/codex-rs/install-context/src/lib.rs @@ -52,6 +52,12 @@ pub enum InstallMethod { /// The platform of the standalone release, either `Unix` or `Windows`. platform: StandalonePlatform, }, + /// A directly extracted Linux bundle with `codex` at its root and managed + /// resources under `codex-resources/`. + DirectBundle { + bundle_dir: AbsolutePathBuf, + resources_dir: AbsolutePathBuf, + }, /// A Codex binary launched through the npm-managed `codex.js` shim. Npm, /// A Codex binary launched through the bun-managed `codex.js` shim. @@ -136,7 +142,8 @@ impl InstallContext { if let InstallMethod::Standalone { resources_dir: Some(resources_dir), .. - } = &self.method + } + | InstallMethod::DirectBundle { resources_dir, .. } = &self.method { let bundled_rg = resources_dir.join(default_rg_command()); if bundled_rg.is_file() { @@ -147,7 +154,7 @@ impl InstallContext { default_rg_command() } - /// Returns the code-mode host when it is part of the detected package layout. + /// Returns the code-mode host when it is part of the detected installation. pub fn code_mode_host_program(&self) -> Option { self.bundled_resource(code_mode_host_executable_name()) .map(AbsolutePathBuf::into_path_buf) @@ -166,7 +173,8 @@ impl InstallContext { if let InstallMethod::Standalone { resources_dir: Some(resources_dir), .. - } = &self.method + } + | InstallMethod::DirectBundle { resources_dir, .. } = &self.method { let resource = resources_dir.join(file_name); if resource.is_file() { @@ -225,6 +233,9 @@ fn install_method_from_exe( { return standalone_method; } + if let Some(direct_bundle_method) = direct_bundle_install_method(exe_path, package_layout) { + return direct_bundle_method; + } if is_macos && (exe_path.starts_with("/opt/homebrew") || exe_path.starts_with("/usr/local")) { InstallMethod::Brew @@ -233,6 +244,31 @@ fn install_method_from_exe( } } +fn direct_bundle_install_method( + exe_path: &Path, + package_layout: Option<&CodexPackageLayout>, +) -> Option { + if !cfg!(target_os = "linux") || package_layout.is_some() { + return None; + } + + let canonical_exe = canonical_absolute_path(exe_path)?; + if canonical_exe.file_name() != Some(OsStr::new("codex")) { + return None; + } + + let bundle_dir = canonical_exe.parent()?; + let resources_dir = bundle_dir.join(RESOURCES_DIRNAME); + if !resources_dir.is_dir() { + return None; + } + + Some(InstallMethod::DirectBundle { + bundle_dir, + resources_dir, + }) +} + fn standalone_install_method( exe_path: &Path, codex_home: Option<&Path>, @@ -338,6 +374,49 @@ mod tests { assert_eq!(context.code_mode_host_program(), None); } + #[cfg(target_os = "linux")] + #[test] + fn detects_direct_bundle_and_resolves_resources() -> std::io::Result<()> { + let bundle_dir = tempfile::tempdir()?; + let resources_dir = bundle_dir.path().join(RESOURCES_DIRNAME); + fs::create_dir_all(&resources_dir)?; + let exe_path = bundle_dir.path().join("codex"); + let host_path = resources_dir.join(code_mode_host_executable_name()); + fs::write(&exe_path, "")?; + fs::write(&host_path, "")?; + let canonical_bundle_dir = + AbsolutePathBuf::from_absolute_path(bundle_dir.path().canonicalize()?)?; + let canonical_resources_dir = + AbsolutePathBuf::from_absolute_path(resources_dir.canonicalize()?)?; + + let context = InstallContext::from_exe_with_codex_home( + /*is_macos*/ false, + /*current_exe*/ Some(&exe_path), + /*method_override*/ None, + /*codex_home*/ None, + ); + + assert_eq!( + context, + InstallContext { + method: InstallMethod::DirectBundle { + bundle_dir: canonical_bundle_dir, + resources_dir: canonical_resources_dir.clone(), + }, + package_layout: None, + } + ); + assert_eq!( + context.code_mode_host_program(), + Some( + canonical_resources_dir + .join(code_mode_host_executable_name()) + .into_path_buf() + ) + ); + Ok(()) + } + #[test] fn detects_standalone_install_from_release_layout() -> std::io::Result<()> { let codex_home = tempfile::tempdir()?; diff --git a/codex-rs/linux-sandbox/src/bundled_bwrap.rs b/codex-rs/linux-sandbox/src/bundled_bwrap.rs index 972be9dac3..096d254d90 100644 --- a/codex-rs/linux-sandbox/src/bundled_bwrap.rs +++ b/codex-rs/linux-sandbox/src/bundled_bwrap.rs @@ -226,15 +226,20 @@ mod tests { } #[test] - fn finds_legacy_standalone_bundled_bwrap_next_to_exe_resources() { + fn finds_direct_bundle_bwrap_from_install_context() { let temp_dir = tempdir().expect("temp dir"); let exe = temp_dir.path().join("codex"); let expected_bwrap = temp_dir.path().join("codex-resources").join("bwrap"); write_executable(&exe); write_executable(&expected_bwrap); + let context = InstallContext::from_exe( + /*is_macos*/ false, + /*current_exe*/ Some(&exe), + /*method_override*/ None, + ); assert_eq!( - find_legacy_for_exe(&exe), + find_for_install_context(&context), Some(AbsolutePathBuf::from_absolute_path(&expected_bwrap).expect("absolute")) ); } diff --git a/codex-rs/tui/src/update_action.rs b/codex-rs/tui/src/update_action.rs index 0fe4408444..3d5cb6fe16 100644 --- a/codex-rs/tui/src/update_action.rs +++ b/codex-rs/tui/src/update_action.rs @@ -34,7 +34,7 @@ impl UpdateAction { StandalonePlatform::Unix => UpdateAction::StandaloneUnix, StandalonePlatform::Windows => UpdateAction::StandaloneWindows, }), - InstallMethod::Other => None, + InstallMethod::DirectBundle { .. } | InstallMethod::Other => None, } }