mirror of
https://github.com/openai/codex.git
synced 2026-04-28 10:21:06 +03:00
308 lines
11 KiB
Markdown
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. |