# 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// 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 ''"); + 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.