Files
codex/prs/bolinfest/PR-2626.md
2025-09-02 15:17:45 -07:00

308 lines
11 KiB
Markdown

# PR #2626: [apply-patch] Add binary to path
- URL: https://github.com/openai/codex/pull/2626
- Author: dylan-hurd-oai
- Created: 2025-08-23 19:45:37 UTC
- Updated: 2025-08-24 20:25:58 UTC
- Changes: +143/-2, Files changed: 7, Commits: 2
## Description
## Summary
Infrequently, certain models will try to use apply_patch as a real bash command. This should decrease dramatically with #2539 and #2576, but I'm opening this as a potential additional fix just in case. Dropping apply_patch as a binary on the PATH will help us reliably catch cases outlined in #2235, without going down the rabbit hole of trying to catch every invocation at the tool call level
## Testing
WIP
## Full Diff
```diff
diff --git a/.github/workflows/rust-release.yml b/.github/workflows/rust-release.yml
index 0044b864c7..19a549bcca 100644
--- a/.github/workflows/rust-release.yml
+++ b/.github/workflows/rust-release.yml
@@ -95,7 +95,7 @@ jobs:
sudo apt install -y musl-tools pkg-config
- name: Cargo build
- run: cargo build --target ${{ matrix.target }} --release --bin codex
+ run: cargo build --target ${{ matrix.target }} --release --bin codex --bin apply-patch
- name: Stage artifacts
shell: bash
@@ -105,8 +105,10 @@ jobs:
if [[ "${{ matrix.runner }}" == windows* ]]; then
cp target/${{ matrix.target }}/release/codex.exe "$dest/codex-${{ matrix.target }}.exe"
+ cp target/${{ matrix.target }}/release/apply-patch.exe "$dest/apply-patch-${{ matrix.target }}.exe"
else
cp target/${{ matrix.target }}/release/codex "$dest/codex-${{ matrix.target }}"
+ cp target/${{ matrix.target }}/release/apply-patch "$dest/apply-patch-${{ matrix.target }}"
fi
- name: Compress artifacts
diff --git a/codex-cli/bin/codex.js b/codex-cli/bin/codex.js
index b22c5180c0..81b59e48ea 100755
--- a/codex-cli/bin/codex.js
+++ b/codex-cli/bin/codex.js
@@ -2,6 +2,9 @@
// Unified entry point for the Codex CLI.
import path from "path";
+import os from "os";
+import fs from "fs";
+import { createRequire } from "module";
import { fileURLToPath } from "url";
// __dirname equivalent in ESM
@@ -56,7 +59,9 @@ if (!targetTriple) {
throw new Error(`Unsupported platform: ${platform} (${arch})`);
}
-const binaryPath = path.join(__dirname, "..", "bin", `codex-${targetTriple}`);
+const pkgRoot = path.join(__dirname, "..");
+const pkgBinDir = path.join(pkgRoot, "bin");
+const binaryPath = path.join(pkgBinDir, `codex-${targetTriple}`);
// Use an asynchronous spawn instead of spawnSync so that Node is able to
// respond to signals (e.g. Ctrl-C / SIGINT) while the native binary is
@@ -93,10 +98,35 @@ function getUpdatedPath(newDirs) {
}
const additionalDirs = [];
+// 1) Make packaged bin directory available on PATH for any helper binaries.
+additionalDirs.push(pkgBinDir);
const rgDir = await resolveRgDir();
if (rgDir) {
additionalDirs.push(rgDir);
}
+// 2) Ensure an `apply_patch` helper exists in $CODEX_HOME/<version>/ and add that directory to PATH.
+try {
+ const require = createRequire(import.meta.url);
+ // Load package.json to read the version string.
+ const { version } = require("../package.json");
+ const codexHome = process.env.CODEX_HOME || path.join(os.homedir(), ".codex");
+ const versionDir = path.join(codexHome, version);
+ fs.mkdirSync(versionDir, { recursive: true });
+ const isWindows = platform === "win32";
+ const destName = isWindows ? "apply_patch.exe" : "apply_patch";
+ const destPath = path.join(versionDir, destName);
+ const srcPath = path.join(pkgBinDir, `apply-patch-${targetTriple}`);
+ // Only copy if missing; keep it simple and fast.
+ if (!fs.existsSync(destPath)) {
+ fs.copyFileSync(srcPath, destPath);
+ if (!isWindows) {
+ fs.chmodSync(destPath, 0o755);
+ }
+ }
+ additionalDirs.push(versionDir);
+} catch {
+ // Best-effort: if anything fails, continue without the helper.
+}
const updatedPath = getUpdatedPath(additionalDirs);
const child = spawn(binaryPath, process.argv.slice(2), {
diff --git a/codex-cli/scripts/install_native_deps.sh b/codex-cli/scripts/install_native_deps.sh
index 6cf2faafc8..87da5679fd 100755
--- a/codex-cli/scripts/install_native_deps.sh
+++ b/codex-cli/scripts/install_native_deps.sh
@@ -75,17 +75,32 @@ gh run download --dir "$ARTIFACTS_DIR" --repo openai/codex "$WORKFLOW_ID"
# x64 Linux
zstd -d "$ARTIFACTS_DIR/x86_64-unknown-linux-musl/codex-x86_64-unknown-linux-musl.zst" \
-o "$BIN_DIR/codex-x86_64-unknown-linux-musl"
+if [ -f "$ARTIFACTS_DIR/x86_64-unknown-linux-musl/apply-patch-x86_64-unknown-linux-musl.zst" ]; then
+ zstd -d "$ARTIFACTS_DIR/x86_64-unknown-linux-musl/apply-patch-x86_64-unknown-linux-musl.zst" -o "$BIN_DIR/apply-patch-x86_64-unknown-linux-musl"
+fi
# ARM64 Linux
zstd -d "$ARTIFACTS_DIR/aarch64-unknown-linux-musl/codex-aarch64-unknown-linux-musl.zst" \
-o "$BIN_DIR/codex-aarch64-unknown-linux-musl"
+if [ -f "$ARTIFACTS_DIR/aarch64-unknown-linux-musl/apply-patch-aarch64-unknown-linux-musl.zst" ]; then
+ zstd -d "$ARTIFACTS_DIR/aarch64-unknown-linux-musl/apply-patch-aarch64-unknown-linux-musl.zst" -o "$BIN_DIR/apply-patch-aarch64-unknown-linux-musl"
+fi
# x64 macOS
zstd -d "$ARTIFACTS_DIR/x86_64-apple-darwin/codex-x86_64-apple-darwin.zst" \
-o "$BIN_DIR/codex-x86_64-apple-darwin"
+if [ -f "$ARTIFACTS_DIR/x86_64-apple-darwin/apply-patch-x86_64-apple-darwin.zst" ]; then
+ zstd -d "$ARTIFACTS_DIR/x86_64-apple-darwin/apply-patch-x86_64-apple-darwin.zst" -o "$BIN_DIR/apply-patch-x86_64-apple-darwin"
+fi
# ARM64 macOS
zstd -d "$ARTIFACTS_DIR/aarch64-apple-darwin/codex-aarch64-apple-darwin.zst" \
-o "$BIN_DIR/codex-aarch64-apple-darwin"
+if [ -f "$ARTIFACTS_DIR/aarch64-apple-darwin/apply-patch-aarch64-apple-darwin.zst" ]; then
+ zstd -d "$ARTIFACTS_DIR/aarch64-apple-darwin/apply-patch-aarch64-apple-darwin.zst" -o "$BIN_DIR/apply-patch-aarch64-apple-darwin"
+fi
# x64 Windows
zstd -d "$ARTIFACTS_DIR/x86_64-pc-windows-msvc/codex-x86_64-pc-windows-msvc.exe.zst" \
-o "$BIN_DIR/codex-x86_64-pc-windows-msvc.exe"
+if [ -f "$ARTIFACTS_DIR/x86_64-pc-windows-msvc/apply-patch-x86_64-pc-windows-msvc.exe.zst" ]; then
+ zstd -d "$ARTIFACTS_DIR/x86_64-pc-windows-msvc/apply-patch-x86_64-pc-windows-msvc.exe.zst" -o "$BIN_DIR/apply-patch-x86_64-pc-windows-msvc.exe"
+fi
echo "Installed native dependencies into $BIN_DIR"
diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock
index dbccbd863e..a8e2d5c416 100644
--- a/codex-rs/Cargo.lock
+++ b/codex-rs/Cargo.lock
@@ -635,6 +635,7 @@ name = "codex-apply-patch"
version = "0.0.0"
dependencies = [
"anyhow",
+ "assert_cmd",
"pretty_assertions",
"similar",
"tempfile",
diff --git a/codex-rs/apply-patch/Cargo.toml b/codex-rs/apply-patch/Cargo.toml
index 622f53ce71..33fa037c86 100644
--- a/codex-rs/apply-patch/Cargo.toml
+++ b/codex-rs/apply-patch/Cargo.toml
@@ -6,6 +6,10 @@ version = { workspace = true }
[lib]
name = "codex_apply_patch"
path = "src/lib.rs"
+
+[[bin]]
+name = "apply-patch"
+path = "src/main.rs"
[lints]
workspace = true
@@ -20,3 +24,4 @@ tree-sitter-bash = "0.25.0"
[dev-dependencies]
pretty_assertions = "1.4.1"
tempfile = "3.13.0"
+assert_cmd = "2"
diff --git a/codex-rs/apply-patch/src/main.rs b/codex-rs/apply-patch/src/main.rs
new file mode 100644
index 0000000000..ca71e97aaa
--- /dev/null
+++ b/codex-rs/apply-patch/src/main.rs
@@ -0,0 +1,40 @@
+use std::io::Write;
+use std::process::ExitCode;
+
+fn main() -> ExitCode {
+ // Expect exactly one argument: the full apply_patch payload.
+ let mut args = std::env::args_os();
+ // argv[0]
+ let _argv0 = args.next();
+
+ let patch_arg = match args.next() {
+ Some(arg) => match arg.into_string() {
+ Ok(s) => s,
+ Err(_) => {
+ eprintln!("Error: apply-patch requires a UTF-8 PATCH argument.");
+ return ExitCode::from(1);
+ }
+ },
+ None => {
+ eprintln!("Usage: apply-patch '<apply_patch_payload>'");
+ return ExitCode::from(2);
+ }
+ };
+
+ // Refuse extra args to avoid ambiguity.
+ if args.next().is_some() {
+ eprintln!("Error: apply-patch accepts exactly one argument.");
+ return ExitCode::from(2);
+ }
+
+ let mut stdout = std::io::stdout();
+ let mut stderr = std::io::stderr();
+ match codex_apply_patch::apply_patch(&patch_arg, &mut stdout, &mut stderr) {
+ Ok(()) => {
+ // Flush to ensure output ordering when used in pipelines.
+ let _ = stdout.flush();
+ ExitCode::from(0)
+ }
+ Err(_) => ExitCode::from(1),
+ }
+}
diff --git a/codex-rs/apply-patch/tests/cli.rs b/codex-rs/apply-patch/tests/cli.rs
new file mode 100644
index 0000000000..648845c99b
--- /dev/null
+++ b/codex-rs/apply-patch/tests/cli.rs
@@ -0,0 +1,48 @@
+#![allow(clippy::expect_used, clippy::unwrap_used)]
+use assert_cmd::prelude::*;
+use std::fs;
+use std::process::Command;
+use tempfile::tempdir;
+
+#[test]
+fn test_apply_patch_cli_add_and_update() -> anyhow::Result<()> {
+ let tmp = tempdir()?;
+ let file = "cli_test.txt";
+ let absolute_path = tmp.path().join(file);
+
+ // 1) Add a file
+ let add_patch = format!(
+ r#"*** Begin Patch
+*** Add File: {file}
++hello
+*** End Patch"#
+ );
+ Command::cargo_bin("apply-patch")
+ .expect("should find apply-patch binary")
+ .arg(add_patch)
+ .current_dir(tmp.path())
+ .assert()
+ .success()
+ .stdout(format!("Success. Updated the following files:\nA {file}\n"));
+ assert_eq!(fs::read_to_string(&absolute_path)?, "hello\n");
+
+ // 2) Update the file
+ let update_patch = format!(
+ r#"*** Begin Patch
+*** Update File: {file}
+@@
+-hello
++world
+*** End Patch"#
+ );
+ Command::cargo_bin("apply-patch")
+ .expect("should find apply-patch binary")
+ .arg(update_patch)
+ .current_dir(tmp.path())
+ .assert()
+ .success()
+ .stdout(format!("Success. Updated the following files:\nM {file}\n"));
+ assert_eq!(fs::read_to_string(&absolute_path)?, "world\n");
+
+ Ok(())
+}
```
## Review Comments
### codex-cli/bin/codex.js
- Created: 2025-08-24 18:06:17 UTC | Link: https://github.com/openai/codex/pull/2626#discussion_r2296755889
```diff
@@ -56,7 +59,9 @@ if (!targetTriple) {
throw new Error(`Unsupported platform: ${platform} (${arch})`);
}
-const binaryPath = path.join(__dirname, "..", "bin", `codex-${targetTriple}`);
+const pkgRoot = path.join(__dirname, "..");
```
> While this scheme would work for `codex` installed via `npm`, it would not work for someone who installs via `brew` or someone who downloads from GitHub Releases.
>
> Building this into https://github.com/openai/codex/blob/main/codex-rs/arg0/src/lib.rs would solve this problem.
### codex-rs/apply-patch/src/main.rs
- Created: 2025-08-24 18:04:27 UTC | Link: https://github.com/openai/codex/pull/2626#discussion_r2296755293
```diff
@@ -0,0 +1,40 @@
+use std::io::Write;
+use std::process::ExitCode;
+
+fn main() -> ExitCode {
+ // Expect exactly one argument: the full apply_patch payload.
```
> This is not quite right: if zero arguments are specified, it is supposed to read from stdin.