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

11 KiB

PR #2626: [apply-patch] Add binary to path

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 --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

@@ -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

@@ -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.