Skip to content

Commit ffd991b

Browse files
committed
feat: use the arg0 trick with apply_patch
1 parent 4157788 commit ffd991b

File tree

10 files changed

+248
-1
lines changed

10 files changed

+248
-1
lines changed

codex-rs/Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

codex-rs/apply-patch/Cargo.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ version = { workspace = true }
77
name = "codex_apply_patch"
88
path = "src/lib.rs"
99

10+
[[bin]]
11+
name = "apply-patch"
12+
path = "src/main.rs"
13+
1014
[lints]
1115
workspace = true
1216

@@ -18,5 +22,6 @@ tree-sitter = "0.25.8"
1822
tree-sitter-bash = "0.25.0"
1923

2024
[dev-dependencies]
25+
assert_cmd = "2"
2126
pretty_assertions = "1.4.1"
2227
tempfile = "3.13.0"

codex-rs/apply-patch/src/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
mod parser;
22
mod seek_sequence;
3+
mod standalone_executable;
34

45
use std::collections::HashMap;
56
use std::path::Path;
@@ -19,6 +20,8 @@ use tree_sitter::LanguageError;
1920
use tree_sitter::Parser;
2021
use tree_sitter_bash::LANGUAGE as BASH;
2122

23+
pub use standalone_executable::main;
24+
2225
/// Detailed instructions for gpt-4.1 on how to use the `apply_patch` tool.
2326
pub const APPLY_PATCH_TOOL_INSTRUCTIONS: &str = include_str!("../apply_patch_tool_instructions.md");
2427

codex-rs/apply-patch/src/main.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
pub fn main() -> ! {
2+
codex_apply_patch::main()
3+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
use std::io::Read;
2+
use std::io::Write;
3+
4+
pub fn main() -> ! {
5+
let exit_code = run_main();
6+
std::process::exit(exit_code);
7+
}
8+
9+
/// We would prefer to return `std::process::ExitCode`, but its `exit_process()`
10+
/// is still a nightly API and we want main() to return !.
11+
pub fn run_main() -> i32 {
12+
// Expect either one argument (the full apply_patch payload) or read it from stdin.
13+
let mut args = std::env::args_os();
14+
let _argv0 = args.next();
15+
16+
let patch_arg = match args.next() {
17+
Some(arg) => match arg.into_string() {
18+
Ok(s) => s,
19+
Err(_) => {
20+
eprintln!("Error: apply_patch requires a UTF-8 PATCH argument.");
21+
return 1;
22+
}
23+
},
24+
None => {
25+
// No argument provided; attempt to read the patch from stdin.
26+
let mut buf = String::new();
27+
match std::io::stdin().read_to_string(&mut buf) {
28+
Ok(_) => {
29+
if buf.is_empty() {
30+
eprintln!("Usage: apply_patch 'PATCH'\n echo 'PATCH' | apply-patch");
31+
return 2;
32+
}
33+
buf
34+
}
35+
Err(err) => {
36+
eprintln!("Error: Failed to read PATCH from stdin.\n{err}");
37+
return 1;
38+
}
39+
}
40+
}
41+
};
42+
43+
// Refuse extra args to avoid ambiguity.
44+
if args.next().is_some() {
45+
eprintln!("Error: apply_patch accepts exactly one argument.");
46+
return 2;
47+
}
48+
49+
let mut stdout = std::io::stdout();
50+
let mut stderr = std::io::stderr();
51+
match crate::apply_patch(&patch_arg, &mut stdout, &mut stderr) {
52+
Ok(()) => {
53+
// Flush to ensure output ordering when used in pipelines.
54+
let _ = stdout.flush();
55+
0
56+
}
57+
Err(_) => 1,
58+
}
59+
}

codex-rs/apply-patch/tests/all.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
// Single integration test binary that aggregates all test modules.
2+
// The submodules live in `tests/suite/`.
3+
mod suite;
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
#![allow(clippy::expect_used, clippy::unwrap_used)]
2+
use assert_cmd::prelude::*;
3+
use std::fs;
4+
use std::process::Command;
5+
use tempfile::tempdir;
6+
7+
#[test]
8+
fn test_apply_patch_cli_add_and_update() -> anyhow::Result<()> {
9+
let tmp = tempdir()?;
10+
let file = "cli_test.txt";
11+
let absolute_path = tmp.path().join(file);
12+
13+
// 1) Add a file
14+
let add_patch = format!(
15+
r#"*** Begin Patch
16+
*** Add File: {file}
17+
+hello
18+
*** End Patch"#
19+
);
20+
Command::cargo_bin("apply-patch")
21+
.expect("should find apply-patch binary")
22+
.arg(add_patch)
23+
.current_dir(tmp.path())
24+
.assert()
25+
.success()
26+
.stdout(format!("Success. Updated the following files:\nA {file}\n"));
27+
assert_eq!(fs::read_to_string(&absolute_path)?, "hello\n");
28+
29+
// 2) Update the file
30+
let update_patch = format!(
31+
r#"*** Begin Patch
32+
*** Update File: {file}
33+
@@
34+
-hello
35+
+world
36+
*** End Patch"#
37+
);
38+
Command::cargo_bin("apply-patch")
39+
.expect("should find apply-patch binary")
40+
.arg(update_patch)
41+
.current_dir(tmp.path())
42+
.assert()
43+
.success()
44+
.stdout(format!("Success. Updated the following files:\nM {file}\n"));
45+
assert_eq!(fs::read_to_string(&absolute_path)?, "world\n");
46+
47+
Ok(())
48+
}
49+
50+
#[test]
51+
fn test_apply_patch_cli_stdin_add_and_update() -> anyhow::Result<()> {
52+
let tmp = tempdir()?;
53+
let file = "cli_test_stdin.txt";
54+
let absolute_path = tmp.path().join(file);
55+
56+
// 1) Add a file via stdin
57+
let add_patch = format!(
58+
r#"*** Begin Patch
59+
*** Add File: {file}
60+
+hello
61+
*** End Patch"#
62+
);
63+
let mut cmd =
64+
assert_cmd::Command::cargo_bin("apply-patch").expect("should find apply-patch binary");
65+
cmd.current_dir(tmp.path());
66+
cmd.write_stdin(add_patch)
67+
.assert()
68+
.success()
69+
.stdout(format!("Success. Updated the following files:\nA {file}\n"));
70+
assert_eq!(fs::read_to_string(&absolute_path)?, "hello\n");
71+
72+
// 2) Update the file via stdin
73+
let update_patch = format!(
74+
r#"*** Begin Patch
75+
*** Update File: {file}
76+
@@
77+
-hello
78+
+world
79+
*** End Patch"#
80+
);
81+
let mut cmd =
82+
assert_cmd::Command::cargo_bin("apply-patch").expect("should find apply-patch binary");
83+
cmd.current_dir(tmp.path());
84+
cmd.write_stdin(update_patch)
85+
.assert()
86+
.success()
87+
.stdout(format!("Success. Updated the following files:\nM {file}\n"));
88+
assert_eq!(fs::read_to_string(&absolute_path)?, "world\n");
89+
90+
Ok(())
91+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
mod cli;

codex-rs/arg0/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,5 @@ codex-apply-patch = { path = "../apply-patch" }
1616
codex-core = { path = "../core" }
1717
codex-linux-sandbox = { path = "../linux-sandbox" }
1818
dotenvy = "0.15.7"
19+
tempfile = "3"
1920
tokio = { version = "1", features = ["rt-multi-thread"] }

codex-rs/arg0/src/lib.rs

Lines changed: 80 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,13 @@ use std::path::Path;
33
use std::path::PathBuf;
44

55
use codex_core::CODEX_APPLY_PATCH_ARG1;
6+
#[cfg(unix)]
7+
use std::os::unix::fs::symlink;
8+
use tempfile::TempDir;
9+
10+
const LINUX_SANDBOX_ARG0: &str = "codex-linux-sandbox";
11+
const APPLY_PATCH_ARG0: &str = "apply_patch";
12+
const MISSPELLED_APPLY_PATCH_ARG0: &str = "applypatch";
613

714
/// While we want to deploy the Codex CLI as a single executable for simplicity,
815
/// we also want to expose some of its functionality as distinct CLIs, so we use
@@ -39,9 +46,11 @@ where
3946
.and_then(|s| s.to_str())
4047
.unwrap_or("");
4148

42-
if exe_name == "codex-linux-sandbox" {
49+
if exe_name == LINUX_SANDBOX_ARG0 {
4350
// Safety: [`run_main`] never returns.
4451
codex_linux_sandbox::run_main();
52+
} else if exe_name == APPLY_PATCH_ARG0 || exe_name == MISSPELLED_APPLY_PATCH_ARG0 {
53+
codex_apply_patch::main();
4554
}
4655

4756
let argv1 = args.next().unwrap_or_default();
@@ -68,6 +77,19 @@ where
6877
// before creating any threads/the Tokio runtime.
6978
load_dotenv();
7079

80+
// Retain the TempDir so it exists for the lifetime of the invocation of
81+
// this executable. Admittedly, we could invoke `keep()` on it, but it
82+
// would be nice to avoid leaving temporary directories behind, if possible.
83+
let _path_entry = match prepend_path_entry_for_apply_patch() {
84+
Ok(path_entry) => path_entry,
85+
Err(err) => {
86+
// While it is possible that Codex could likely proceed successfully
87+
// even if updating the PATH fails, let's be strict.
88+
eprintln!("could not update PATH: {err}");
89+
std::process::exit(1);
90+
}
91+
};
92+
7193
// Regular invocation – create a Tokio runtime and execute the provided
7294
// async entry-point.
7395
let runtime = tokio::runtime::Runtime::new()?;
@@ -113,3 +135,60 @@ where
113135
}
114136
}
115137
}
138+
139+
/// Creates a temporary directory with either:
140+
///
141+
/// - UNIX: `apply_patch` symlink to the current executable
142+
/// - WINDOWS: `apply_patch.bat` batch script to invoke the current executable
143+
/// with the "secret" --codex-run-as-apply-patch flag.
144+
///
145+
/// This temporary directory is prepended to the PATH environment variable so
146+
/// that `apply_patch` can be on the PATH without requiring the user to
147+
/// install a separate `apply_patch` executable, simplifying the deployment of
148+
/// Codex CLI.
149+
///
150+
/// IMPORTANT: This function modifies the PATH environment variable, so it MUST
151+
/// be called before multiple threads are spawned.
152+
fn prepend_path_entry_for_apply_patch() -> std::io::Result<TempDir> {
153+
let temp_dir = TempDir::new()?;
154+
let path = temp_dir.path();
155+
156+
for filename in &[APPLY_PATCH_ARG0, MISSPELLED_APPLY_PATCH_ARG0] {
157+
let exe = std::env::current_exe()?;
158+
159+
#[cfg(unix)]
160+
{
161+
let link = path.join(filename);
162+
symlink(&exe, &link)?;
163+
}
164+
165+
#[cfg(windows)]
166+
{
167+
let batch_script = path.join(format!("{filename}.bat"));
168+
std::fs::write(
169+
&batch_script,
170+
format!(
171+
r#"@echo off
172+
"{}" {CODEX_APPLY_PATCH_ARG1} %*
173+
"#,
174+
exe.display()
175+
),
176+
)?;
177+
}
178+
}
179+
180+
let updated_path_env_var = match std::env::var("PATH") {
181+
Ok(existing_path) => {
182+
format!("{}:{}", path.display(), existing_path)
183+
}
184+
Err(_) => {
185+
format!("{}", path.display())
186+
}
187+
};
188+
189+
unsafe {
190+
std::env::set_var("PATH", updated_path_env_var);
191+
}
192+
193+
Ok(temp_dir)
194+
}

0 commit comments

Comments
 (0)