From 9102255854eab4310d7d90ab64215bf3f2e06850 Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Mon, 28 Jul 2025 08:31:24 -0700 Subject: [PATCH] fix: move arg0 handling out of codex-linux-sandbox and into its own crate (#1697) --- codex-rs/Cargo.lock | 20 ++++++--- codex-rs/Cargo.toml | 1 + codex-rs/arg0/Cargo.toml | 18 ++++++++ codex-rs/arg0/src/lib.rs | 68 +++++++++++++++++++++++++++++++ codex-rs/cli/Cargo.toml | 2 +- codex-rs/cli/src/main.rs | 3 +- codex-rs/exec/Cargo.toml | 2 +- codex-rs/exec/src/main.rs | 3 +- codex-rs/linux-sandbox/Cargo.toml | 14 +++---- codex-rs/linux-sandbox/src/lib.rs | 65 +---------------------------- codex-rs/mcp-server/Cargo.toml | 2 +- codex-rs/mcp-server/src/main.rs | 3 +- codex-rs/tui/Cargo.toml | 2 +- codex-rs/tui/src/main.rs | 3 +- 14 files changed, 121 insertions(+), 85 deletions(-) create mode 100644 codex-rs/arg0/Cargo.toml create mode 100644 codex-rs/arg0/src/lib.rs diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index da3bd50a..f3903d6e 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -605,6 +605,17 @@ dependencies = [ "tree-sitter-bash", ] +[[package]] +name = "codex-arg0" +version = "0.0.0" +dependencies = [ + "anyhow", + "codex-core", + "codex-linux-sandbox", + "dotenvy", + "tokio", +] + [[package]] name = "codex-chatgpt" version = "0.0.0" @@ -628,11 +639,11 @@ dependencies = [ "anyhow", "clap", "clap_complete", + "codex-arg0", "codex-chatgpt", "codex-common", "codex-core", "codex-exec", - "codex-linux-sandbox", "codex-login", "codex-mcp-server", "codex-tui", @@ -709,9 +720,9 @@ dependencies = [ "anyhow", "chrono", "clap", + "codex-arg0", "codex-common", "codex-core", - "codex-linux-sandbox", "owo-colors", "serde_json", "shlex", @@ -761,7 +772,6 @@ dependencies = [ "clap", "codex-common", "codex-core", - "dotenvy", "landlock", "libc", "seccompiler", @@ -799,8 +809,8 @@ version = "0.0.0" dependencies = [ "anyhow", "assert_cmd", + "codex-arg0", "codex-core", - "codex-linux-sandbox", "mcp-types", "mcp_test_support", "pretty_assertions", @@ -826,10 +836,10 @@ dependencies = [ "base64 0.22.1", "clap", "codex-ansi-escape", + "codex-arg0", "codex-common", "codex-core", "codex-file-search", - "codex-linux-sandbox", "codex-login", "color-eyre", "crossterm", diff --git a/codex-rs/Cargo.toml b/codex-rs/Cargo.toml index 6f89e8fa..51b2b5cc 100644 --- a/codex-rs/Cargo.toml +++ b/codex-rs/Cargo.toml @@ -3,6 +3,7 @@ resolver = "2" members = [ "ansi-escape", "apply-patch", + "arg0", "cli", "common", "core", diff --git a/codex-rs/arg0/Cargo.toml b/codex-rs/arg0/Cargo.toml new file mode 100644 index 00000000..9ad18967 --- /dev/null +++ b/codex-rs/arg0/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "codex-arg0" +version = { workspace = true } +edition = "2024" + +[lib] +name = "codex_arg0" +path = "src/lib.rs" + +[lints] +workspace = true + +[dependencies] +anyhow = "1" +codex-core = { path = "../core" } +codex-linux-sandbox = { path = "../linux-sandbox" } +dotenvy = "0.15.7" +tokio = { version = "1", features = ["rt-multi-thread"] } diff --git a/codex-rs/arg0/src/lib.rs b/codex-rs/arg0/src/lib.rs new file mode 100644 index 00000000..86b98c7d --- /dev/null +++ b/codex-rs/arg0/src/lib.rs @@ -0,0 +1,68 @@ +use std::future::Future; +use std::path::Path; +use std::path::PathBuf; + +/// While we want to deploy the Codex CLI as a single executable for simplicity, +/// we also want to expose some of its functionality as distinct CLIs, so we use +/// the "arg0 trick" to determine which CLI to dispatch. This effectively allows +/// us to simulate deploying multiple executables as a single binary on Mac and +/// Linux (but not Windows). +/// +/// When the current executable is invoked through the hard-link or alias named +/// `codex-linux-sandbox` we *directly* execute +/// [`codex_linux_sandbox::run_main`] (which never returns). Otherwise we: +/// +/// 1. Use [`dotenvy::from_path`] and [`dotenvy::dotenv`] to modify the +/// environment before creating any threads. +/// 2. Construct a Tokio multi-thread runtime. +/// 3. Derive the path to the current executable (so children can re-invoke the +/// sandbox) when running on Linux. +/// 4. Execute the provided async `main_fn` inside that runtime, forwarding any +/// error. Note that `main_fn` receives `codex_linux_sandbox_exe: +/// Option`, as an argument, which is generally needed as part of +/// constructing [`codex_core::config::Config`]. +/// +/// This function should be used to wrap any `main()` function in binary crates +/// in this workspace that depends on these helper CLIs. +pub fn arg0_dispatch_or_else(main_fn: F) -> anyhow::Result<()> +where + F: FnOnce(Option) -> Fut, + Fut: Future>, +{ + // Determine if we were invoked via the special alias. + let argv0 = std::env::args().next().unwrap_or_default(); + let exe_name = Path::new(&argv0) + .file_name() + .and_then(|s| s.to_str()) + .unwrap_or(""); + + if exe_name == "codex-linux-sandbox" { + // Safety: [`run_main`] never returns. + codex_linux_sandbox::run_main(); + } + + // This modifies the environment, which is not thread-safe, so do this + // before creating any threads/the Tokio runtime. + load_dotenv(); + + // Regular invocation – create a Tokio runtime and execute the provided + // async entry-point. + let runtime = tokio::runtime::Runtime::new()?; + runtime.block_on(async move { + let codex_linux_sandbox_exe: Option = if cfg!(target_os = "linux") { + std::env::current_exe().ok() + } else { + None + }; + + main_fn(codex_linux_sandbox_exe).await + }) +} + +/// Load env vars from ~/.codex/.env and `$(pwd)/.env`. +fn load_dotenv() { + if let Ok(codex_home) = codex_core::config::find_codex_home() { + dotenvy::from_path(codex_home.join(".env")).ok(); + } + dotenvy::dotenv().ok(); +} diff --git a/codex-rs/cli/Cargo.toml b/codex-rs/cli/Cargo.toml index 94378815..ab98764b 100644 --- a/codex-rs/cli/Cargo.toml +++ b/codex-rs/cli/Cargo.toml @@ -18,12 +18,12 @@ workspace = true anyhow = "1" clap = { version = "4", features = ["derive"] } clap_complete = "4" +codex-arg0 = { path = "../arg0" } codex-chatgpt = { path = "../chatgpt" } codex-core = { path = "../core" } codex-common = { path = "../common", features = ["cli"] } codex-exec = { path = "../exec" } codex-login = { path = "../login" } -codex-linux-sandbox = { path = "../linux-sandbox" } codex-mcp-server = { path = "../mcp-server" } codex-tui = { path = "../tui" } serde_json = "1" diff --git a/codex-rs/cli/src/main.rs b/codex-rs/cli/src/main.rs index 7916a7dc..efda03bd 100644 --- a/codex-rs/cli/src/main.rs +++ b/codex-rs/cli/src/main.rs @@ -2,6 +2,7 @@ use clap::CommandFactory; use clap::Parser; use clap_complete::Shell; use clap_complete::generate; +use codex_arg0::arg0_dispatch_or_else; use codex_chatgpt::apply_command::ApplyCommand; use codex_chatgpt::apply_command::run_apply_command; use codex_cli::LandlockCommand; @@ -92,7 +93,7 @@ struct LoginCommand { } fn main() -> anyhow::Result<()> { - codex_linux_sandbox::run_with_sandbox(|codex_linux_sandbox_exe| async move { + arg0_dispatch_or_else(|codex_linux_sandbox_exe| async move { cli_main(codex_linux_sandbox_exe).await?; Ok(()) }) diff --git a/codex-rs/exec/Cargo.toml b/codex-rs/exec/Cargo.toml index ed01b78e..c9d94deb 100644 --- a/codex-rs/exec/Cargo.toml +++ b/codex-rs/exec/Cargo.toml @@ -18,13 +18,13 @@ workspace = true anyhow = "1" chrono = "0.4.40" clap = { version = "4", features = ["derive"] } +codex-arg0 = { path = "../arg0" } codex-core = { path = "../core" } codex-common = { path = "../common", features = [ "cli", "elapsed", "sandbox_summary", ] } -codex-linux-sandbox = { path = "../linux-sandbox" } owo-colors = "4.2.0" serde_json = "1" shlex = "1.3.0" diff --git a/codex-rs/exec/src/main.rs b/codex-rs/exec/src/main.rs index 3a8e1f94..03ee533e 100644 --- a/codex-rs/exec/src/main.rs +++ b/codex-rs/exec/src/main.rs @@ -10,6 +10,7 @@ //! This allows us to ship a completely separate set of functionality as part //! of the `codex-exec` binary. use clap::Parser; +use codex_arg0::arg0_dispatch_or_else; use codex_common::CliConfigOverrides; use codex_exec::Cli; use codex_exec::run_main; @@ -24,7 +25,7 @@ struct TopCli { } fn main() -> anyhow::Result<()> { - codex_linux_sandbox::run_with_sandbox(|codex_linux_sandbox_exe| async move { + arg0_dispatch_or_else(|codex_linux_sandbox_exe| async move { let top_cli = TopCli::parse(); // Merge root-level overrides into inner CLI struct so downstream logic remains unchanged. let mut inner = top_cli.inner; diff --git a/codex-rs/linux-sandbox/Cargo.toml b/codex-rs/linux-sandbox/Cargo.toml index 5c2dea60..4b173ea1 100644 --- a/codex-rs/linux-sandbox/Cargo.toml +++ b/codex-rs/linux-sandbox/Cargo.toml @@ -14,15 +14,16 @@ path = "src/lib.rs" [lints] workspace = true -[dependencies] +[target.'cfg(target_os = "linux")'.dependencies] anyhow = "1" clap = { version = "4", features = ["derive"] } codex-common = { path = "../common", features = ["cli"] } codex-core = { path = "../core" } -dotenvy = "0.15.7" -tokio = { version = "1", features = ["rt-multi-thread"] } +libc = "0.2.172" +landlock = "0.4.1" +seccompiler = "0.5.0" -[dev-dependencies] +[target.'cfg(target_os = "linux")'.dev-dependencies] tempfile = "3" tokio = { version = "1", features = [ "io-std", @@ -31,8 +32,3 @@ tokio = { version = "1", features = [ "rt-multi-thread", "signal", ] } - -[target.'cfg(target_os = "linux")'.dependencies] -libc = "0.2.172" -landlock = "0.4.1" -seccompiler = "0.5.0" diff --git a/codex-rs/linux-sandbox/src/lib.rs b/codex-rs/linux-sandbox/src/lib.rs index 96067846..80453c7f 100644 --- a/codex-rs/linux-sandbox/src/lib.rs +++ b/codex-rs/linux-sandbox/src/lib.rs @@ -4,72 +4,11 @@ mod landlock; mod linux_run_main; #[cfg(target_os = "linux")] -pub use linux_run_main::run_main; - -use std::future::Future; -use std::path::PathBuf; - -/// Helper that consolidates the common boilerplate found in several Codex -/// binaries (`codex`, `codex-exec`, `codex-tui`) around dispatching to the -/// `codex-linux-sandbox` sub-command. -/// -/// When the current executable is invoked through the hard-link or alias -/// named `codex-linux-sandbox` we *directly* execute [`run_main`](crate::run_main) -/// (which never returns). Otherwise we: -/// 1. Construct a Tokio multi-thread runtime. -/// 2. Derive the path to the current executable (so children can re-invoke -/// the sandbox) when running on Linux. -/// 3. Execute the provided async `main_fn` inside that runtime, forwarding -/// any error. -/// -/// This function eliminates duplicated code across the various `main.rs` -/// entry-points. -pub fn run_with_sandbox(main_fn: F) -> anyhow::Result<()> -where - F: FnOnce(Option) -> Fut, - Fut: Future>, -{ - use std::path::Path; - - // Determine if we were invoked via the special alias. - let argv0 = std::env::args().next().unwrap_or_default(); - let exe_name = Path::new(&argv0) - .file_name() - .and_then(|s| s.to_str()) - .unwrap_or(""); - - if exe_name == "codex-linux-sandbox" { - // Safety: [`run_main`] never returns. - crate::run_main(); - } - - // This modifies the environment, which is not thread-safe, so do this - // before creating any threads/the Tokio runtime. - load_dotenv(); - - // Regular invocation – create a Tokio runtime and execute the provided - // async entry-point. - let runtime = tokio::runtime::Runtime::new()?; - runtime.block_on(async move { - let codex_linux_sandbox_exe: Option = if cfg!(target_os = "linux") { - std::env::current_exe().ok() - } else { - None - }; - - main_fn(codex_linux_sandbox_exe).await - }) +pub fn run_main() -> ! { + linux_run_main::run_main(); } #[cfg(not(target_os = "linux"))] pub fn run_main() -> ! { panic!("codex-linux-sandbox is only supported on Linux"); } - -/// Load env vars from ~/.codex/.env and `$(pwd)/.env`. -fn load_dotenv() { - if let Ok(codex_home) = codex_core::config::find_codex_home() { - dotenvy::from_path(codex_home.join(".env")).ok(); - } - dotenvy::dotenv().ok(); -} diff --git a/codex-rs/mcp-server/Cargo.toml b/codex-rs/mcp-server/Cargo.toml index 1088b924..488ee6a6 100644 --- a/codex-rs/mcp-server/Cargo.toml +++ b/codex-rs/mcp-server/Cargo.toml @@ -16,8 +16,8 @@ workspace = true [dependencies] anyhow = "1" +codex-arg0 = { path = "../arg0" } codex-core = { path = "../core" } -codex-linux-sandbox = { path = "../linux-sandbox" } mcp-types = { path = "../mcp-types" } schemars = "0.8.22" serde = { version = "1", features = ["derive"] } diff --git a/codex-rs/mcp-server/src/main.rs b/codex-rs/mcp-server/src/main.rs index 51c46c44..60ddeeab 100644 --- a/codex-rs/mcp-server/src/main.rs +++ b/codex-rs/mcp-server/src/main.rs @@ -1,7 +1,8 @@ +use codex_arg0::arg0_dispatch_or_else; use codex_mcp_server::run_main; fn main() -> anyhow::Result<()> { - codex_linux_sandbox::run_with_sandbox(|codex_linux_sandbox_exe| async move { + arg0_dispatch_or_else(|codex_linux_sandbox_exe| async move { run_main(codex_linux_sandbox_exe).await?; Ok(()) }) diff --git a/codex-rs/tui/Cargo.toml b/codex-rs/tui/Cargo.toml index b88ac8a0..2f150921 100644 --- a/codex-rs/tui/Cargo.toml +++ b/codex-rs/tui/Cargo.toml @@ -19,6 +19,7 @@ anyhow = "1" base64 = "0.22.1" clap = { version = "4", features = ["derive"] } codex-ansi-escape = { path = "../ansi-escape" } +codex-arg0 = { path = "../arg0" } codex-core = { path = "../core" } codex-common = { path = "../common", features = [ "cli", @@ -26,7 +27,6 @@ codex-common = { path = "../common", features = [ "sandbox_summary", ] } codex-file-search = { path = "../file-search" } -codex-linux-sandbox = { path = "../linux-sandbox" } codex-login = { path = "../login" } color-eyre = "0.6.3" crossterm = { version = "0.28.1", features = ["bracketed-paste"] } diff --git a/codex-rs/tui/src/main.rs b/codex-rs/tui/src/main.rs index fdb3cdaf..480e56e8 100644 --- a/codex-rs/tui/src/main.rs +++ b/codex-rs/tui/src/main.rs @@ -1,4 +1,5 @@ use clap::Parser; +use codex_arg0::arg0_dispatch_or_else; use codex_common::CliConfigOverrides; use codex_tui::Cli; use codex_tui::run_main; @@ -13,7 +14,7 @@ struct TopCli { } fn main() -> anyhow::Result<()> { - codex_linux_sandbox::run_with_sandbox(|codex_linux_sandbox_exe| async move { + arg0_dispatch_or_else(|codex_linux_sandbox_exe| async move { let top_cli = TopCli::parse(); let mut inner = top_cli.inner; inner