From 955fa324da18cb6047f6c48ef80326e2e5240924 Mon Sep 17 00:00:00 2001 From: Channing Conger Date: Thu, 9 Jul 2026 20:38:47 +0000 Subject: [PATCH] fix code mode host resource installation --- .github/workflows/rust-release.yml | 8 +- codex-rs/Cargo.lock | 1 + codex-rs/code-mode/Cargo.toml | 1 + codex-rs/code-mode/src/remote_session.rs | 38 +----- .../code-mode/src/remote_session_tests.rs | 51 -------- codex-rs/install-context/src/lib.rs | 110 ++++++++++++++++++ scripts/codex_package/README.md | 6 +- scripts/codex_package/layout.py | 4 +- scripts/codex_package/test_layout.py | 79 ++++++++----- scripts/install/install.ps1 | 2 +- scripts/install/install.sh | 6 +- sdk/python/scripts/update_sdk_artifacts.py | 4 +- .../test_artifact_workflow_and_binaries.py | 8 +- 13 files changed, 189 insertions(+), 129 deletions(-) diff --git a/.github/workflows/rust-release.yml b/.github/workflows/rust-release.yml index 9fdd66637b..ff5f48f029 100644 --- a/.github/workflows/rust-release.yml +++ b/.github/workflows/rust-release.yml @@ -352,13 +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-code-mode-host" + 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-code-mode-host" \ + "$bundle_root/codex-resources/codex-code-mode-host" \ "$bundle_root/codex-resources/bwrap" - tar -C "$bundle_root" -cf - codex codex-code-mode-host 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 @@ -1054,7 +1054,7 @@ jobs: mkdir -p "$package_dir" tar -xzf "${packaged_dir}/${package_stem}-${target}.tar.gz" -C "$package_dir" verify_signed_binary "${package_dir}/bin/${package_entrypoint}" "$package_entrypoint" - verify_signed_binary "${package_dir}/bin/codex-code-mode-host" "codex-code-mode-host" + verify_signed_binary "${package_dir}/codex-resources/codex-code-mode-host" "codex-code-mode-host" if [[ "${{ matrix.verify_dmg }}" != "true" ]]; then exit 0 diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index db4350e1f9..3701633a40 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -2491,6 +2491,7 @@ name = "codex-code-mode" version = "0.0.0" dependencies = [ "codex-code-mode-protocol", + "codex-install-context", "codex-protocol", "deno_core_icudata", "futures", diff --git a/codex-rs/code-mode/Cargo.toml b/codex-rs/code-mode/Cargo.toml index 011fee6ebc..59bd117bb6 100644 --- a/codex-rs/code-mode/Cargo.toml +++ b/codex-rs/code-mode/Cargo.toml @@ -17,6 +17,7 @@ workspace = true [dependencies] codex-code-mode-protocol = { workspace = true } +codex-install-context = { workspace = true } codex-protocol = { workspace = true } deno_core_icudata = { workspace = true } futures = { workspace = true } diff --git a/codex-rs/code-mode/src/remote_session.rs b/codex-rs/code-mode/src/remote_session.rs index ae0a82e6dc..2efda2e2c3 100644 --- a/codex-rs/code-mode/src/remote_session.rs +++ b/codex-rs/code-mode/src/remote_session.rs @@ -1,5 +1,3 @@ -use std::ffi::OsString; -use std::io; use std::path::PathBuf; use std::sync::Arc; use std::sync::Mutex as StdMutex; @@ -18,6 +16,7 @@ use codex_code_mode_protocol::StartedCell; use codex_code_mode_protocol::WaitOutcome; use codex_code_mode_protocol::WaitRequest; use codex_code_mode_protocol::host::SessionId; +use codex_install_context::InstallContext; use tokio::sync::Semaphore; use tokio::sync::watch; @@ -29,8 +28,6 @@ use crate::NoopCodeModeSessionDelegate; mod connection; -const CODE_MODE_HOST_PATH_ENV: &str = "CODEX_CODE_MODE_HOST_PATH"; - type ShutdownResultReceiver = watch::Receiver>>; /// Creates code-mode sessions backed by one lazily spawned process host. @@ -66,7 +63,7 @@ impl ProcessOwnedCodeModeSessionProvider { impl Default for ProcessOwnedCodeModeSessionProvider { fn default() -> Self { - Self::with_host_program(default_host_program()) + Self::with_host_program(InstallContext::current().code_mode_host_program()) } } @@ -195,7 +192,9 @@ impl ProcessOwnedCodeModeSession { pub fn new() -> Self { Self::with_process_host( Arc::new(NoopCodeModeSessionDelegate), - Arc::new(OwnedProcessHost::new(default_host_program())), + Arc::new(OwnedProcessHost::new( + InstallContext::current().code_mode_host_program(), + )), ) } @@ -494,33 +493,6 @@ impl CodeModeSession for ProcessOwnedCodeModeSession { } } -fn default_host_program() -> PathBuf { - resolve_host_program( - std::env::var_os(CODE_MODE_HOST_PATH_ENV), - std::env::current_exe(), - ) -} - -fn resolve_host_program( - override_path: Option, - current_exe: io::Result, -) -> PathBuf { - if let Some(path) = override_path { - return PathBuf::from(path); - } - let executable_name = if cfg!(windows) { - "codex-code-mode-host.exe" - } else { - "codex-code-mode-host" - }; - if let Ok(current_exe) = current_exe - && let Some(parent) = current_exe.parent() - { - return parent.join(executable_name); - } - PathBuf::from(executable_name) -} - #[cfg(test)] #[path = "remote_session_tests.rs"] mod tests; diff --git a/codex-rs/code-mode/src/remote_session_tests.rs b/codex-rs/code-mode/src/remote_session_tests.rs index e48ac2d71e..b7b262e31a 100644 --- a/codex-rs/code-mode/src/remote_session_tests.rs +++ b/codex-rs/code-mode/src/remote_session_tests.rs @@ -1,5 +1,3 @@ -use std::io; -use std::path::PathBuf; use std::sync::Arc; use codex_code_mode_protocol::CodeModeSessionProvider; @@ -10,7 +8,6 @@ use pretty_assertions::assert_eq; use super::ProcessOwnedCodeModeSession; use super::ProcessOwnedCodeModeSessionProvider; -use super::resolve_host_program; use crate::NoopCodeModeSessionDelegate; #[test] @@ -23,54 +20,6 @@ fn provider_reuses_its_live_process_host() { assert!(Arc::ptr_eq(&first, &second)); } -#[test] -fn host_program_override_takes_precedence() { - assert_eq!( - resolve_host_program( - Some("custom-code-mode-host".into()), - Ok(PathBuf::from("/opt/codex/bin/codex")), - ), - PathBuf::from("custom-code-mode-host") - ); -} - -#[test] -fn host_program_is_next_to_the_main_executable_even_when_missing() { - let executable_name = if cfg!(windows) { - "codex-code-mode-host.exe" - } else { - "codex-code-mode-host" - }; - - assert_eq!( - resolve_host_program( - /*override_path*/ None, - Ok(PathBuf::from("/opt/codex/bin/codex")), - ), - PathBuf::from("/opt/codex/bin").join(executable_name) - ); -} - -#[test] -fn host_program_falls_back_to_its_name_when_main_executable_is_unknown() { - let executable_name = if cfg!(windows) { - "codex-code-mode-host.exe" - } else { - "codex-code-mode-host" - }; - - assert_eq!( - resolve_host_program( - /*override_path*/ None, - Err(io::Error::new( - io::ErrorKind::NotFound, - "missing executable" - )), - ), - PathBuf::from(executable_name) - ); -} - #[tokio::test] async fn provider_falls_back_to_in_process_session_when_host_is_missing() { let provider = ProcessOwnedCodeModeSessionProvider::with_host_program( diff --git a/codex-rs/install-context/src/lib.rs b/codex-rs/install-context/src/lib.rs index 61dabb0e02..362ca71b99 100644 --- a/codex-rs/install-context/src/lib.rs +++ b/codex-rs/install-context/src/lib.rs @@ -1,4 +1,6 @@ use std::ffi::OsStr; +use std::ffi::OsString; +use std::io; use std::path::Path; use std::path::PathBuf; use std::sync::OnceLock; @@ -6,6 +8,7 @@ use std::sync::OnceLock; use codex_utils_absolute_path::AbsolutePathBuf; const BIN_DIRNAME: &str = "bin"; +const CODE_MODE_HOST_PATH_ENV: &str = "CODEX_CODE_MODE_HOST_PATH"; const PACKAGE_METADATA_FILENAME: &str = "codex-package.json"; const PATH_DIRNAME: &str = "codex-path"; const RELEASES_DIRNAME: &str = "releases"; @@ -147,6 +150,37 @@ impl InstallContext { default_rg_command() } + /// Resolves the code-mode host executable for the current installation. + pub fn code_mode_host_program(&self) -> PathBuf { + self.resolve_code_mode_host_program( + std::env::var_os(CODE_MODE_HOST_PATH_ENV), + std::env::current_exe(), + ) + } + + fn resolve_code_mode_host_program( + &self, + override_path: Option, + current_exe: io::Result, + ) -> PathBuf { + if let Some(path) = override_path { + return PathBuf::from(path); + } + + let executable_name = code_mode_host_executable_name(); + if let Some(path) = self.bundled_resource(executable_name) { + return path.into_path_buf(); + } + + if let Ok(current_exe) = current_exe + && let Some(parent) = current_exe.parent() + { + return parent.join(executable_name); + } + + PathBuf::from(executable_name) + } + pub fn bundled_resource(&self, file_name: impl AsRef) -> Option { if let Some(package_layout) = &self.package_layout && let Some(resources_dir) = &package_layout.resources_dir @@ -279,6 +313,14 @@ fn default_rg_command() -> PathBuf { } } +fn code_mode_host_executable_name() -> &'static str { + if cfg!(windows) { + "codex-code-mode-host.exe" + } else { + "codex-code-mode-host" + } +} + fn zsh_resource_path() -> PathBuf { PathBuf::from(ZSH_DIRNAME).join(BIN_DIRNAME).join("zsh") } @@ -291,6 +333,74 @@ mod tests { const TEST_RESOURCE_NAME: &str = "codex-test-helper"; + #[test] + fn code_mode_host_program_prefers_override_then_bundled_resource() -> 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); + fs::create_dir_all(&bin_dir)?; + fs::create_dir_all(&resources_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 host_path = resources_dir.join(code_mode_host_executable_name()); + fs::write(&host_path, "")?; + let canonical_host_path = host_path.canonicalize()?; + let context = InstallContext::from_exe( + /*is_macos*/ false, + /*current_exe*/ Some(&exe_path), + /*method_override*/ None, + ); + + assert_eq!( + context.resolve_code_mode_host_program( + Some("custom-code-mode-host".into()), + Ok(exe_path.clone()), + ), + PathBuf::from("custom-code-mode-host") + ); + assert_eq!( + context.resolve_code_mode_host_program(/*override_path*/ None, Ok(exe_path),), + canonical_host_path + ); + Ok(()) + } + + #[test] + fn code_mode_host_program_uses_adjacent_binary_for_legacy_layout() { + let context = InstallContext { + method: InstallMethod::Other, + package_layout: None, + }; + + assert_eq!( + context.resolve_code_mode_host_program( + /*override_path*/ None, + Ok(PathBuf::from("/opt/codex/bin/codex")), + ), + PathBuf::from("/opt/codex/bin").join(code_mode_host_executable_name()) + ); + } + + #[test] + fn code_mode_host_program_falls_back_to_executable_name() { + let context = InstallContext { + method: InstallMethod::Other, + package_layout: None, + }; + + assert_eq!( + context.resolve_code_mode_host_program( + /*override_path*/ None, + Err(io::Error::new( + io::ErrorKind::NotFound, + "missing executable" + )), + ), + PathBuf::from(code_mode_host_executable_name()) + ); + } + #[test] fn detects_standalone_install_from_release_layout() -> std::io::Result<()> { let codex_home = tempfile::tempdir()?; diff --git a/scripts/codex_package/README.md b/scripts/codex_package/README.md index 7850bbd354..5c299948b5 100644 --- a/scripts/codex_package/README.md +++ b/scripts/codex_package/README.md @@ -10,10 +10,10 @@ The builder creates a canonical Codex package directory: . ├── codex-package.json ├── bin -│ ├── [.exe] -│ └── codex-code-mode-host[.exe] +│ └── [.exe] ├── codex-resources │ ├── bwrap # Linux only +│ ├── codex-code-mode-host[.exe] │ ├── zsh/bin/zsh # supported Unix targets only │ ├── codex-command-runner.exe # Windows only │ └── codex-windows-sandbox-setup.exe # Windows only @@ -53,7 +53,7 @@ entrypoint should pass `--entrypoint-bin` so the package contains that exact binary instead of rebuilding it. Release jobs should likewise pass `--code-mode-host-bin` so the package contains -the signed host executable beside the signed entrypoint. +the signed host executable in `codex-resources`. Release jobs that already built package resource binaries should also pass the corresponding resource flags: `--bwrap-bin` for Linux packages, and diff --git a/scripts/codex_package/layout.py b/scripts/codex_package/layout.py index f4fef6912c..e69055ac37 100644 --- a/scripts/codex_package/layout.py +++ b/scripts/codex_package/layout.py @@ -53,7 +53,7 @@ def build_package_dir( ) copy_executable( inputs.code_mode_host_bin, - bin_dir / f"codex-code-mode-host{spec.exe_suffix}", + resources_dir / f"codex-code-mode-host{spec.exe_suffix}", is_windows=spec.is_windows, ) copy_executable(inputs.rg_bin, path_dir / spec.rg_name, is_windows=spec.is_windows) @@ -135,7 +135,7 @@ def validate_package_dir( required_files = [ Path("bin") / variant.entrypoint_name(spec), - Path("bin") / f"codex-code-mode-host{spec.exe_suffix}", + Path("codex-resources") / f"codex-code-mode-host{spec.exe_suffix}", Path("codex-path") / spec.rg_name, ] executable_files = list(required_files) diff --git a/scripts/codex_package/test_layout.py b/scripts/codex_package/test_layout.py index 157bce5e9c..4de8d17dde 100644 --- a/scripts/codex_package/test_layout.py +++ b/scripts/codex_package/test_layout.py @@ -15,36 +15,61 @@ from codex_package.targets import TARGET_SPECS class PackageLayoutTest(unittest.TestCase): - def test_app_server_package_places_code_mode_host_beside_entrypoint(self) -> None: + def test_app_server_package_places_code_mode_host_in_resources(self) -> None: with tempfile.TemporaryDirectory() as temp_dir: - root = Path(temp_dir) - package_dir = root / "package" - package_dir.mkdir() - inputs = PackageInputs( - entrypoint_bin=touch_executable(root / "codex-app-server"), - code_mode_host_bin=touch_executable(root / "codex-code-mode-host"), - rg_bin=touch_executable(root / "rg"), - zsh_bin=None, - bwrap_bin=touch_executable(root / "bwrap"), - codex_command_runner_bin=None, - codex_windows_sandbox_setup_bin=None, - ) + for target in ( + "x86_64-unknown-linux-musl", + "x86_64-pc-windows-msvc", + ): + with self.subTest(target=target): + root = Path(temp_dir) / target + root.mkdir() + package_dir = root / "package" + package_dir.mkdir() + spec = TARGET_SPECS[target] + inputs = PackageInputs( + entrypoint_bin=touch_executable(root / "codex-app-server"), + code_mode_host_bin=touch_executable( + root / "codex-code-mode-host" + ), + rg_bin=touch_executable(root / "rg"), + zsh_bin=None, + bwrap_bin=( + touch_executable(root / "bwrap") if spec.is_linux else None + ), + codex_command_runner_bin=( + touch_executable(root / "codex-command-runner.exe") + if spec.is_windows + else None + ), + codex_windows_sandbox_setup_bin=( + touch_executable(root / "codex-windows-sandbox-setup.exe") + if spec.is_windows + else None + ), + ) - build_package_dir( - package_dir, - "1.2.3", - PACKAGE_VARIANTS["codex-app-server"], - TARGET_SPECS["x86_64-unknown-linux-musl"], - inputs, - ) - validate_package_dir( - package_dir, - PACKAGE_VARIANTS["codex-app-server"], - TARGET_SPECS["x86_64-unknown-linux-musl"], - include_zsh=False, - ) + build_package_dir( + package_dir, + "1.2.3", + PACKAGE_VARIANTS["codex-app-server"], + spec, + inputs, + ) + validate_package_dir( + package_dir, + PACKAGE_VARIANTS["codex-app-server"], + spec, + include_zsh=False, + ) - self.assertTrue((package_dir / "bin" / "codex-code-mode-host").is_file()) + self.assertTrue( + ( + package_dir + / "codex-resources" + / f"codex-code-mode-host{spec.exe_suffix}" + ).is_file() + ) def touch_executable(path: Path) -> Path: diff --git a/scripts/install/install.ps1 b/scripts/install/install.ps1 index e27d34b223..484d5be1c8 100644 --- a/scripts/install/install.ps1 +++ b/scripts/install/install.ps1 @@ -538,8 +538,8 @@ function Test-PackageContentsAreComplete { $expectedFiles = @( "codex-package.json", "bin\codex.exe", - "bin\codex-code-mode-host.exe", "codex-path\rg.exe", + "codex-resources\codex-code-mode-host.exe", "codex-resources\codex-command-runner.exe", "codex-resources\codex-windows-sandbox-setup.exe" ) diff --git a/scripts/install/install.sh b/scripts/install/install.sh index 0c52542a7a..e7e6524caf 100755 --- a/scripts/install/install.sh +++ b/scripts/install/install.sh @@ -771,12 +771,11 @@ install_package_release() { tar -xzf "$archive_path" -C "$stage_release" chmod 0755 \ "$stage_release/bin/codex" \ - "$stage_release/bin/codex-code-mode-host" \ + "$stage_release/codex-resources/codex-code-mode-host" \ "$stage_release/codex-path/rg" if [ -f "$stage_release/codex-resources/bwrap" ]; then chmod 0755 "$stage_release/codex-resources/bwrap" fi - ln -sf "bin/codex" "$stage_release/codex" if [ -e "$release_dir" ] || [ -L "$release_dir" ]; then rm -rf "$release_dir" @@ -825,8 +824,7 @@ release_dir_is_complete() { package) [ -f "$release_dir/codex-package.json" ] && [ -x "$release_dir/bin/codex" ] && - [ -x "$release_dir/bin/codex-code-mode-host" ] && - [ -x "$release_dir/codex" ] && + [ -x "$release_dir/codex-resources/codex-code-mode-host" ] && [ -x "$release_dir/codex-path/rg" ] || return 1 ;; diff --git a/sdk/python/scripts/update_sdk_artifacts.py b/sdk/python/scripts/update_sdk_artifacts.py index 1c6ec59402..fa7542aa29 100755 --- a/sdk/python/scripts/update_sdk_artifacts.py +++ b/sdk/python/scripts/update_sdk_artifacts.py @@ -280,9 +280,9 @@ def _validate_codex_package_layout(package_dir: Path, package_archive: Path) -> package_binary = package_dir / "bin" / runtime_binary_name() if not package_binary.is_file(): missing_entries.append(str(Path("bin") / runtime_binary_name())) - code_mode_host = package_dir / "bin" / runtime_code_mode_host_name() + code_mode_host = package_dir / "codex-resources" / runtime_code_mode_host_name() if not code_mode_host.is_file(): - missing_entries.append(str(Path("bin") / runtime_code_mode_host_name())) + missing_entries.append(str(Path("codex-resources") / runtime_code_mode_host_name())) if missing_entries: missing = ", ".join(missing_entries) raise RuntimeError(f"Missing Codex package layout entries in {package_archive}: {missing}") diff --git a/sdk/python/tests/test_artifact_workflow_and_binaries.py b/sdk/python/tests/test_artifact_workflow_and_binaries.py index ad1262e836..108c574519 100644 --- a/sdk/python/tests/test_artifact_workflow_and_binaries.py +++ b/sdk/python/tests/test_artifact_workflow_and_binaries.py @@ -57,7 +57,9 @@ def _write_fake_codex_package(package_dir: Path, script) -> Path: (package_dir / "codex-path").mkdir() (package_dir / "codex-package.json").write_text('{"variant":"codex"}\n') (package_dir / "bin" / script.runtime_binary_name()).write_text("fake codex\n") - (package_dir / "bin" / script.runtime_code_mode_host_name()).write_text("fake code mode host\n") + (package_dir / "codex-resources" / script.runtime_code_mode_host_name()).write_text( + "fake code mode host\n" + ) (package_dir / "codex-resources" / "bwrap").write_text("fake bwrap\n") (package_dir / "codex-path" / "rg").write_text("fake rg\n") return package_dir @@ -683,7 +685,9 @@ def test_stage_runtime_release_copies_package_layout_and_sets_version( assert { "metadata": (package_root / "codex-package.json").read_text(), "codex": (package_root / "bin" / script.runtime_binary_name()).read_text(), - "code_mode_host": (package_root / "bin" / script.runtime_code_mode_host_name()).read_text(), + "code_mode_host": ( + package_root / "codex-resources" / script.runtime_code_mode_host_name() + ).read_text(), "bwrap": (package_root / "codex-resources" / "bwrap").read_text(), "rg": (package_root / "codex-path" / "rg").read_text(), } == {