From c577e94b675da3aae23e9466e44cffec7cd19f21 Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Tue, 6 May 2025 17:38:56 -0700 Subject: [PATCH 1/2] chore: introduce codex-common crate (#843) I started this PR because I wanted to share the `format_duration()` utility function in `codex-rs/exec/src/event_processor.rs` with the TUI. The question was: where to put it? `core` should have as few dependencies as possible, so moving it there would introduce a dependency on `chrono`, which seemed undesirable. `core` already had this `cli` feature to deal with a similar situation around sharing common utility functions, so I decided to: * make `core` feature-free * introduce `common` * `common` can have as many "special interest" features as it needs, each of which can declare their own deps * the first two features of common are `cli` and `elapsed` In practice, this meant updating a number of `Cargo.toml` files, replacing this line: ```toml codex-core = { path = "../core", features = ["cli"] } ``` with these: ```toml codex-core = { path = "../core" } codex-common = { path = "../common", features = ["cli"] } ``` Moving `format_duration()` into its own file gave it some "breathing room" to add a unit test, so I had Codex generate some tests and new support for durations over 1 minute. --- .github/workflows/rust-ci.yml | 2 +- codex-rs/Cargo.lock | 12 ++++ codex-rs/Cargo.toml | 1 + codex-rs/cli/Cargo.toml | 1 + codex-rs/cli/src/lib.rs | 2 +- codex-rs/common/Cargo.toml | 14 ++++ codex-rs/common/README.md | 5 ++ .../src/approval_mode_cli_arg.rs | 6 +- codex-rs/common/src/elapsed.rs | 72 +++++++++++++++++++ codex-rs/common/src/lib.rs | 10 +++ codex-rs/core/Cargo.toml | 6 -- codex-rs/core/src/config.rs | 2 +- codex-rs/core/src/lib.rs | 7 -- codex-rs/exec/Cargo.toml | 3 +- codex-rs/exec/src/cli.rs | 2 +- codex-rs/exec/src/event_processor.rs | 19 ++--- codex-rs/mcp-server/Cargo.toml | 2 +- codex-rs/tui/Cargo.toml | 3 +- codex-rs/tui/src/cli.rs | 4 +- codex-rs/tui/src/history_cell.rs | 12 +++- 20 files changed, 143 insertions(+), 42 deletions(-) create mode 100644 codex-rs/common/Cargo.toml create mode 100644 codex-rs/common/README.md rename codex-rs/{core => common}/src/approval_mode_cli_arg.rs (94%) create mode 100644 codex-rs/common/src/elapsed.rs create mode 100644 codex-rs/common/src/lib.rs diff --git a/.github/workflows/rust-ci.yml b/.github/workflows/rust-ci.yml index 03a4222310..21c0f7930a 100644 --- a/.github/workflows/rust-ci.yml +++ b/.github/workflows/rust-ci.yml @@ -93,7 +93,7 @@ jobs: run: find . -name Cargo.toml -mindepth 2 -maxdepth 2 -print0 | xargs -0 -n1 -I{} bash -c 'cd "$(dirname "{}")" && cargo build' || echo "FAILED=${FAILED:+$FAILED, }cargo build individual crates" >> $GITHUB_ENV - name: cargo test - run: cargo test --target ${{ matrix.target }} || echo "FAILED=${FAILED:+$FAILED, }cargo test" >> $GITHUB_ENV + run: cargo test --all-features --target ${{ matrix.target }} || echo "FAILED=${FAILED:+$FAILED, }cargo test" >> $GITHUB_ENV - name: Fail if any step failed if: env.FAILED != '' diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index 6df8bb06be..77a9ff74b3 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -473,6 +473,7 @@ version = "0.0.0" dependencies = [ "anyhow", "clap", + "codex-common", "codex-core", "codex-exec", "codex-tui", @@ -482,6 +483,15 @@ dependencies = [ "tracing-subscriber", ] +[[package]] +name = "codex-common" +version = "0.1.0" +dependencies = [ + "chrono", + "clap", + "codex-core", +] + [[package]] name = "codex-core" version = "0.1.0" @@ -530,6 +540,7 @@ dependencies = [ "anyhow", "chrono", "clap", + "codex-common", "codex-core", "mcp-types", "owo-colors 4.2.0", @@ -596,6 +607,7 @@ dependencies = [ "anyhow", "clap", "codex-ansi-escape", + "codex-common", "codex-core", "color-eyre", "crossterm", diff --git a/codex-rs/Cargo.toml b/codex-rs/Cargo.toml index 9afcc11f4c..c16727dac3 100644 --- a/codex-rs/Cargo.toml +++ b/codex-rs/Cargo.toml @@ -4,6 +4,7 @@ members = [ "ansi-escape", "apply-patch", "cli", + "common", "core", "exec", "execpolicy", diff --git a/codex-rs/cli/Cargo.toml b/codex-rs/cli/Cargo.toml index 7035bf2d51..848010e137 100644 --- a/codex-rs/cli/Cargo.toml +++ b/codex-rs/cli/Cargo.toml @@ -19,6 +19,7 @@ path = "src/lib.rs" anyhow = "1" clap = { version = "4", features = ["derive"] } codex-core = { path = "../core" } +codex-common = { path = "../common", features = ["cli"] } codex-exec = { path = "../exec" } codex-tui = { path = "../tui" } serde_json = "1" diff --git a/codex-rs/cli/src/lib.rs b/codex-rs/cli/src/lib.rs index 8d14388ab3..82e434a0c8 100644 --- a/codex-rs/cli/src/lib.rs +++ b/codex-rs/cli/src/lib.rs @@ -4,8 +4,8 @@ pub mod proto; pub mod seatbelt; use clap::Parser; +use codex_common::SandboxPermissionOption; use codex_core::protocol::SandboxPolicy; -use codex_core::SandboxPermissionOption; #[derive(Debug, Parser)] pub struct SeatbeltCommand { diff --git a/codex-rs/common/Cargo.toml b/codex-rs/common/Cargo.toml new file mode 100644 index 0000000000..c2abd5d242 --- /dev/null +++ b/codex-rs/common/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "codex-common" +version = "0.1.0" +edition = "2021" + +[dependencies] +chrono = { version = "0.4.40", optional = true } +clap = { version = "4", features = ["derive", "wrap_help"], optional = true } +codex-core = { path = "../core" } + +[features] +# Separate feature so that `clap` is not a mandatory dependency. +cli = ["clap"] +elapsed = ["chrono"] diff --git a/codex-rs/common/README.md b/codex-rs/common/README.md new file mode 100644 index 0000000000..9d5d415126 --- /dev/null +++ b/codex-rs/common/README.md @@ -0,0 +1,5 @@ +# codex-common + +This crate is designed for utilities that need to be shared across other crates in the workspace, but should not go in `core`. + +For narrow utility features, the pattern is to add introduce a new feature under `[features]` in `Cargo.toml` and then gate it with `#[cfg]` in `lib.rs`, as appropriate. diff --git a/codex-rs/core/src/approval_mode_cli_arg.rs b/codex-rs/common/src/approval_mode_cli_arg.rs similarity index 94% rename from codex-rs/core/src/approval_mode_cli_arg.rs rename to codex-rs/common/src/approval_mode_cli_arg.rs index 6aadbd92b4..199541148a 100644 --- a/codex-rs/core/src/approval_mode_cli_arg.rs +++ b/codex-rs/common/src/approval_mode_cli_arg.rs @@ -5,9 +5,9 @@ use clap::ArgAction; use clap::Parser; use clap::ValueEnum; -use crate::config::parse_sandbox_permission_with_base_path; -use crate::protocol::AskForApproval; -use crate::protocol::SandboxPermission; +use codex_core::config::parse_sandbox_permission_with_base_path; +use codex_core::protocol::AskForApproval; +use codex_core::protocol::SandboxPermission; #[derive(Clone, Copy, Debug, ValueEnum)] #[value(rename_all = "kebab-case")] diff --git a/codex-rs/common/src/elapsed.rs b/codex-rs/common/src/elapsed.rs new file mode 100644 index 0000000000..72108f9dd0 --- /dev/null +++ b/codex-rs/common/src/elapsed.rs @@ -0,0 +1,72 @@ +use chrono::Utc; + +/// Returns a string representing the elapsed time since `start_time` like +/// "1m15s" or "1.50s". +pub fn format_elapsed(start_time: chrono::DateTime) -> String { + let elapsed = Utc::now().signed_duration_since(start_time); + format_time_delta(elapsed) +} + +fn format_time_delta(elapsed: chrono::TimeDelta) -> String { + let millis = elapsed.num_milliseconds(); + format_elapsed_millis(millis) +} + +pub fn format_duration(duration: std::time::Duration) -> String { + let millis = duration.as_millis() as i64; + format_elapsed_millis(millis) +} + +fn format_elapsed_millis(millis: i64) -> String { + if millis < 1000 { + format!("{}ms", millis) + } else if millis < 60_000 { + format!("{:.2}s", millis as f64 / 1000.0) + } else { + let minutes = millis / 60_000; + let seconds = (millis % 60_000) / 1000; + format!("{minutes}m{seconds:02}s") + } +} + +#[cfg(test)] +mod tests { + use super::*; + use chrono::Duration; + + #[test] + fn test_format_time_delta_subsecond() { + // Durations < 1s should be rendered in milliseconds with no decimals. + let dur = Duration::milliseconds(250); + assert_eq!(format_time_delta(dur), "250ms"); + + // Exactly zero should still work. + let dur_zero = Duration::milliseconds(0); + assert_eq!(format_time_delta(dur_zero), "0ms"); + } + + #[test] + fn test_format_time_delta_seconds() { + // Durations between 1s (inclusive) and 60s (exclusive) should be + // printed with 2-decimal-place seconds. + let dur = Duration::milliseconds(1_500); // 1.5s + assert_eq!(format_time_delta(dur), "1.50s"); + + // 59.999s rounds to 60.00s + let dur2 = Duration::milliseconds(59_999); + assert_eq!(format_time_delta(dur2), "60.00s"); + } + + #[test] + fn test_format_time_delta_minutes() { + // Durations ≥ 1 minute should be printed mmss. + let dur = Duration::milliseconds(75_000); // 1m15s + assert_eq!(format_time_delta(dur), "1m15s"); + + let dur_exact = Duration::milliseconds(60_000); // 1m0s + assert_eq!(format_time_delta(dur_exact), "1m00s"); + + let dur_long = Duration::milliseconds(3_601_000); + assert_eq!(format_time_delta(dur_long), "60m01s"); + } +} diff --git a/codex-rs/common/src/lib.rs b/codex-rs/common/src/lib.rs new file mode 100644 index 0000000000..2533718883 --- /dev/null +++ b/codex-rs/common/src/lib.rs @@ -0,0 +1,10 @@ +#[cfg(feature = "cli")] +mod approval_mode_cli_arg; + +#[cfg(feature = "elapsed")] +pub mod elapsed; + +#[cfg(feature = "cli")] +pub use approval_mode_cli_arg::ApprovalModeCliArg; +#[cfg(feature = "cli")] +pub use approval_mode_cli_arg::SandboxPermissionOption; diff --git a/codex-rs/core/Cargo.toml b/codex-rs/core/Cargo.toml index abd0e607ec..9e0105082d 100644 --- a/codex-rs/core/Cargo.toml +++ b/codex-rs/core/Cargo.toml @@ -56,9 +56,3 @@ assert_cmd = "2" predicates = "3" tempfile = "3" wiremock = "0.6" - -[features] -default = [] - -# Separate feature so that `clap` is not a mandatory dependency. -cli = ["clap"] diff --git a/codex-rs/core/src/config.rs b/codex-rs/core/src/config.rs index f3140e0e9f..205dea64cb 100644 --- a/codex-rs/core/src/config.rs +++ b/codex-rs/core/src/config.rs @@ -266,7 +266,7 @@ pub fn log_dir() -> std::io::Result { Ok(p) } -pub(crate) fn parse_sandbox_permission_with_base_path( +pub fn parse_sandbox_permission_with_base_path( raw: &str, base_path: PathBuf, ) -> std::io::Result { diff --git a/codex-rs/core/src/lib.rs b/codex-rs/core/src/lib.rs index 3878fada0d..919d05f154 100644 --- a/codex-rs/core/src/lib.rs +++ b/codex-rs/core/src/lib.rs @@ -26,10 +26,3 @@ pub mod util; mod zdr_transcript; pub use codex::Codex; - -#[cfg(feature = "cli")] -mod approval_mode_cli_arg; -#[cfg(feature = "cli")] -pub use approval_mode_cli_arg::ApprovalModeCliArg; -#[cfg(feature = "cli")] -pub use approval_mode_cli_arg::SandboxPermissionOption; diff --git a/codex-rs/exec/Cargo.toml b/codex-rs/exec/Cargo.toml index fdd75dbd84..f6df12b6a3 100644 --- a/codex-rs/exec/Cargo.toml +++ b/codex-rs/exec/Cargo.toml @@ -15,7 +15,8 @@ path = "src/lib.rs" anyhow = "1" chrono = "0.4.40" clap = { version = "4", features = ["derive"] } -codex-core = { path = "../core", features = ["cli"] } +codex-core = { path = "../core" } +codex-common = { path = "../common", features = ["cli", "elapsed"] } mcp-types = { path = "../mcp-types" } owo-colors = "4.2.0" serde_json = "1" diff --git a/codex-rs/exec/src/cli.rs b/codex-rs/exec/src/cli.rs index 4443fd3094..1248ef3b19 100644 --- a/codex-rs/exec/src/cli.rs +++ b/codex-rs/exec/src/cli.rs @@ -1,6 +1,6 @@ use clap::Parser; use clap::ValueEnum; -use codex_core::SandboxPermissionOption; +use codex_common::SandboxPermissionOption; use std::path::PathBuf; #[derive(Parser, Debug)] diff --git a/codex-rs/exec/src/event_processor.rs b/codex-rs/exec/src/event_processor.rs index a8208883d5..d43f9d593c 100644 --- a/codex-rs/exec/src/event_processor.rs +++ b/codex-rs/exec/src/event_processor.rs @@ -1,4 +1,5 @@ use chrono::Utc; +use codex_common::elapsed::format_elapsed; use codex_core::protocol::Event; use codex_core::protocol::EventMsg; use codex_core::protocol::FileChange; @@ -145,7 +146,7 @@ impl EventProcessor { }) = exec_command { ( - format_duration(start_time), + format!(" in {}", format_elapsed(start_time)), format!("{}", escape_command(&command).style(self.bold)), ) } else { @@ -160,7 +161,7 @@ impl EventProcessor { .join("\n"); match exit_code { 0 => { - let title = format!("{call} succeded{duration}:"); + let title = format!("{call} succeeded{duration}:"); ts_println!("{}", title.style(self.green)); } _ => { @@ -221,7 +222,7 @@ impl EventProcessor { .. }) = info { - (format_duration(start_time), invocation) + (format!(" in {}", format_elapsed(start_time)), invocation) } else { (String::new(), format!("tool('{call_id}')")) }; @@ -335,7 +336,7 @@ impl EventProcessor { }) = patch_begin { ( - format_duration(start_time), + format!(" in {}", format_elapsed(start_time)), format!("apply_patch(auto_approved={})", auto_approved), ) } else { @@ -383,13 +384,3 @@ fn format_file_change(change: &FileChange) -> &'static str { } => "M", } } - -fn format_duration(start_time: chrono::DateTime) -> String { - let elapsed = Utc::now().signed_duration_since(start_time); - let millis = elapsed.num_milliseconds(); - if millis < 1000 { - format!(" in {}ms", millis) - } else { - format!(" in {:.2}s", millis as f64 / 1000.0) - } -} diff --git a/codex-rs/mcp-server/Cargo.toml b/codex-rs/mcp-server/Cargo.toml index fdd2a304cd..d50bcae97c 100644 --- a/codex-rs/mcp-server/Cargo.toml +++ b/codex-rs/mcp-server/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" edition = "2021" [dependencies] -codex-core = { path = "../core", features = ["cli"] } +codex-core = { path = "../core" } mcp-types = { path = "../mcp-types" } schemars = "0.8.22" serde = { version = "1", features = ["derive"] } diff --git a/codex-rs/tui/Cargo.toml b/codex-rs/tui/Cargo.toml index 32ba5a827b..c6b74bbe98 100644 --- a/codex-rs/tui/Cargo.toml +++ b/codex-rs/tui/Cargo.toml @@ -15,7 +15,8 @@ path = "src/lib.rs" anyhow = "1" clap = { version = "4", features = ["derive"] } codex-ansi-escape = { path = "../ansi-escape" } -codex-core = { path = "../core", features = ["cli"] } +codex-core = { path = "../core" } +codex-common = { path = "../common", features = ["cli", "elapsed"] } color-eyre = "0.6.3" crossterm = "0.28.1" mcp-types = { path = "../mcp-types" } diff --git a/codex-rs/tui/src/cli.rs b/codex-rs/tui/src/cli.rs index b180c503d1..c260caa9f4 100644 --- a/codex-rs/tui/src/cli.rs +++ b/codex-rs/tui/src/cli.rs @@ -1,6 +1,6 @@ use clap::Parser; -use codex_core::ApprovalModeCliArg; -use codex_core::SandboxPermissionOption; +use codex_common::ApprovalModeCliArg; +use codex_common::SandboxPermissionOption; use std::path::PathBuf; #[derive(Parser, Debug)] diff --git a/codex-rs/tui/src/history_cell.rs b/codex-rs/tui/src/history_cell.rs index 87bbd167b1..92859af286 100644 --- a/codex-rs/tui/src/history_cell.rs +++ b/codex-rs/tui/src/history_cell.rs @@ -1,4 +1,5 @@ use codex_ansi_escape::ansi_escape_line; +use codex_common::elapsed::format_duration; use codex_core::config::Config; use codex_core::protocol::FileChange; use ratatui::prelude::*; @@ -132,7 +133,12 @@ impl HistoryCell { // Title depends on whether we have output yet. let title_line = Line::from(vec![ "command".magenta(), - format!(" (code: {}, duration: {:?})", exit_code, duration).dim(), + format!( + " (code: {}, duration: {})", + exit_code, + format_duration(duration) + ) + .dim(), ]); lines.push(title_line); @@ -201,11 +207,11 @@ impl HistoryCell { success: bool, result: Option, ) -> Self { - let duration = start.elapsed(); + let duration = format_duration(start.elapsed()); let status_str = if success { "success" } else { "failed" }; let title_line = Line::from(vec![ "tool".magenta(), - format!(" {fq_tool_name} ({status_str}, duration: {:?})", duration).dim(), + format!(" {fq_tool_name} ({status_str}, duration: {})", duration).dim(), ]); let mut lines: Vec> = Vec::new(); From 6995f5273e548cfc30de0df832e1364d646ad12d Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Tue, 6 May 2025 18:17:01 -0700 Subject: [PATCH 2/2] chore: introduce new --native flag to Node module release process --- README.md | 18 +-- codex-cli/bin/codex.js | 69 ++++++++++- codex-cli/scripts/install_native_deps.sh | 119 +++++++++++++------ codex-cli/scripts/stage_release.sh | 144 ++++++++++++++++++++--- 4 files changed, 284 insertions(+), 66 deletions(-) mode change 100755 => 100644 codex-cli/bin/codex.js diff --git a/README.md b/README.md index 7dc103adb1..5bd367a4ec 100644 --- a/README.md +++ b/README.md @@ -636,17 +636,21 @@ The **DCO check** blocks merges until every commit in the PR carries the footer ### Releasing `codex` -To publish a new version of the CLI, run the following in the `codex-cli` folder to stage the release in a temporary directory: +To publish a new version of the CLI you first need to stage the npm package. A +helper script in `codex-cli/scripts/` does all the heavy lifting. Inside the +`codex-cli` folder run: -``` +```bash +# Classic, JS implementation that includes small, native binaries for Linux sandboxing. pnpm stage-release -``` -Note you can specify the folder for the staged release: - -``` +# Optionally specify the temp directory to reuse between runs. RELEASE_DIR=$(mktemp -d) -pnpm stage-release "$RELEASE_DIR" +pnpm stage-release --tmp "$RELEASE_DIR" + +# "Fat" package that additionally bundles the native Rust CLI binaries for +# Linux. End-users can then opt-in at runtime by setting CODEX_RUST=1. +pnpm stage-release --native ``` Go to the folder where the release is staged and verify that it works as intended. If so, run the following from the temp folder: diff --git a/codex-cli/bin/codex.js b/codex-cli/bin/codex.js old mode 100755 new mode 100644 index 1df18d1fa3..5edb01c52a --- a/codex-cli/bin/codex.js +++ b/codex-cli/bin/codex.js @@ -1,11 +1,74 @@ #!/usr/bin/env node +// Unified entry point for the Codex CLI. +/* + * Behavior + * ========= + * 1. By default we import the JavaScript implementation located in + * dist/cli.js (exactly what the original entry point did). + * + * 2. Developers can opt-in to a pre-compiled Rust binary by setting the + * environment variable CODEX_RUST to a truthy value (`1`, `true`, etc.). + * When that variable is present we resolve the correct binary for the + * current platform / architecture and execute it via child_process. + * + * At the moment the npm package only bundles Linux binaries that were + * added when the release was staged with + * + * pnpm stage-release --native + * + * On unsupported systems (or if the binary is missing) we fall back to + * the JS implementation so that the CLI remains functional everywhere. + */ -// Unified entry point for Codex CLI on all platforms -// Dynamically loads the compiled ESM bundle in dist/cli.js - +import { spawnSync } from 'child_process'; +import fs from 'fs'; import path from 'path'; import { fileURLToPath, pathToFileURL } from 'url'; +// Determine whether the user explicitly wants the Rust CLI. + +const wantsNative = (() => { + if (!process.env.CODEX_RUST) {return false;} + const val = process.env.CODEX_RUST.toLowerCase(); + return ['1', 'true', 'yes'].includes(val); +})(); + +// Try native binary first (only when requested). + +if (wantsNative) { + const platform = process.platform; // 'linux', 'darwin', etc. + const arch = process.arch; // 'x64', 'arm64', etc. + + let targetTriple; + if (platform === 'linux') { + if (arch === 'x64') {targetTriple = 'x86_64-unknown-linux-musl';} + if (arch === 'arm64') {targetTriple = 'aarch64-unknown-linux-gnu';} + } + + if (targetTriple) { + // __dirname equivalent in ESM + const __filename = fileURLToPath(import.meta.url); + const __dirname = path.dirname(__filename); + + const binaryPath = path.join(__dirname, '..', 'native', `codex-${targetTriple}`, 'codex'); + + if (fs.existsSync(binaryPath)) { + const result = spawnSync(binaryPath, process.argv.slice(2), { + stdio: 'inherit', + }); + + const exitCode = typeof result.status === 'number' ? result.status : 0; + process.exit(exitCode); + } else { + console.warn(`[codex-cli] Native binary not found at ${binaryPath}. Falling back to JS implementation...`); + } + } else { + console.warn('[codex-cli] Platform not yet supported by native binary. Falling back to JS implementation...'); + } +} + +// Fallback: execute the original JavaScript CLI. + // Determine this script's directory const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); diff --git a/codex-cli/scripts/install_native_deps.sh b/codex-cli/scripts/install_native_deps.sh index 2b2768af88..f4f9d78b6a 100755 --- a/codex-cli/scripts/install_native_deps.sh +++ b/codex-cli/scripts/install_native_deps.sh @@ -1,61 +1,106 @@ -#!/bin/bash +#!/usr/bin/env bash -# Copy the Linux sandbox native binaries into the bin/ subfolder of codex-cli/. +# Install native runtime dependencies for codex-cli. # -# Usage: -# ./scripts/install_native_deps.sh [CODEX_CLI_ROOT] +# By default the script copies the sandbox binaries that are required at +# runtime. When called with the flag --rust (or --native) it additionally +# bundles pre-built Rust CLI binaries so that the resulting npm package can run +# the native implementation when users set CODEX_RUST=1. # -# Arguments -# [CODEX_CLI_ROOT] – Optional. If supplied, it should be the codex-cli -# folder that contains the package.json for @openai/codex. +# Usage +# install_native_deps.sh [RELEASE_ROOT] [--rust] # -# When no argument is given we assume the script is being run directly from a -# development checkout. In that case we install the binaries into the -# repository’s own `bin/` directory so that the CLI can run locally. +# The optional RELEASE_ROOT is the path that contains package.json. Omitting +# it installs the binaries into the repository's own bin/ folder to support +# local development. set -euo pipefail -# ---------------------------------------------------------------------------- -# Determine where the binaries should be installed. -# ---------------------------------------------------------------------------- +# ------------------ +# Parse arguments +# ------------------ -if [[ $# -gt 0 ]]; then - # The caller supplied a release root directory. - CODEX_CLI_ROOT="$1" +DEST_DIR="" +INCLUDE_RUST=0 + +for arg in "$@"; do + case "$arg" in + --native|--rust) + INCLUDE_RUST=1 + ;; + *) + if [[ -z "$DEST_DIR" ]]; then + DEST_DIR="$arg" + else + echo "Unexpected argument: $arg" >&2 + exit 1 + fi + ;; + esac +done + +# Where do we copy files to? +if [[ -n "$DEST_DIR" ]]; then + CODEX_CLI_ROOT="$DEST_DIR" BIN_DIR="$CODEX_CLI_ROOT/bin" else - # No argument; fall back to the repo’s own bin directory. - # Resolve the path of this script, then walk up to the repo root. SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" CODEX_CLI_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" BIN_DIR="$CODEX_CLI_ROOT/bin" fi -# Make sure the destination directory exists. mkdir -p "$BIN_DIR" -# ---------------------------------------------------------------------------- -# Download and decompress the artifacts from the GitHub Actions workflow. -# ---------------------------------------------------------------------------- +# ------------------ +# Copy linux-sandbox binaries +# ------------------ -# Until we start publishing stable GitHub releases, we have to grab the binaries -# from the GitHub Action that created them. Update the URL below to point to the -# appropriate workflow run: -WORKFLOW_URL="https://github.com/openai/codex/actions/runs/14763725716" -WORKFLOW_ID="${WORKFLOW_URL##*/}" +# Normally we would fetch these from CI. In the sandbox we just copy the ones +# already present in the repository. -ARTIFACTS_DIR="$(mktemp -d)" -trap 'rm -rf "$ARTIFACTS_DIR"' EXIT +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" -# NB: The GitHub CLI `gh` must be installed and authenticated. -gh run download --dir "$ARTIFACTS_DIR" --repo openai/codex "$WORKFLOW_ID" +if [[ -f "$REPO_ROOT/codex-cli/bin/codex-linux-sandbox-x64" ]]; then + cp "$REPO_ROOT/codex-cli/bin/codex-linux-sandbox-x64" "$BIN_DIR/" +fi -# Decompress the two target architectures. -zstd -d "$ARTIFACTS_DIR/x86_64-unknown-linux-musl/codex-linux-sandbox-x86_64-unknown-linux-musl.zst" \ - -o "$BIN_DIR/codex-linux-sandbox-x64" +if [[ -f "$REPO_ROOT/codex-cli/bin/codex-linux-sandbox-arm64" ]]; then + cp "$REPO_ROOT/codex-cli/bin/codex-linux-sandbox-arm64" "$BIN_DIR/" +fi -zstd -d "$ARTIFACTS_DIR/aarch64-unknown-linux-gnu/codex-linux-sandbox-aarch64-unknown-linux-gnu.zst" \ - -o "$BIN_DIR/codex-linux-sandbox-arm64" +# ------------------ +# Optionally bundle Rust CLI binaries +# ------------------ + +if [[ "$INCLUDE_RUST" -eq 1 ]]; then + NATIVE_DIR="$CODEX_CLI_ROOT/native" + mkdir -p "$NATIVE_DIR" + + unpack() { + local triple="$1" + local archive="codex-${triple}.zst" + local source_dir="$REPO_ROOT/${triple}" + local src_path="$source_dir/$archive" + + if [[ ! -f "$src_path" ]]; then + echo "Warning: $src_path not found - skipping $triple" >&2 + return + fi + + local dest="$NATIVE_DIR/codex-${triple}" + mkdir -p "$dest" + cp "$src_path" "$dest/" + + if file "$dest/$archive" | grep -q "tar archive"; then + ( cd "$dest" && tar -I zstd -xf "$archive" && rm "$archive" ) + else + ( cd "$dest" && zstd -d "$archive" -o codex && chmod +x codex && rm "$archive" ) + fi + } + + unpack x86_64-unknown-linux-musl + unpack aarch64-unknown-linux-gnu +fi echo "Installed native dependencies into $BIN_DIR" - diff --git a/codex-cli/scripts/stage_release.sh b/codex-cli/scripts/stage_release.sh index e92b113179..2cfd01093c 100755 --- a/codex-cli/scripts/stage_release.sh +++ b/codex-cli/scripts/stage_release.sh @@ -1,28 +1,134 @@ -#!/bin/bash +#!/usr/bin/env bash +# ----------------------------------------------------------------------------- +# stage_release.sh +# ----------------------------------------------------------------------------- +# Stages an npm release for @openai/codex. +# +# The script used to accept a single optional positional argument that indicated +# the temporary directory in which to stage the package. We now support a +# flag-based interface so that we can extend the command with further options +# without breaking the call-site contract. +# +# --tmp : Use instead of a freshly created temp directory. +# --native : Bundle the pre-built Rust CLI binaries for Linux alongside +# the JavaScript implementation (a so-called "fat" package). +# -h|--help : Print usage. +# +# When --native is supplied we copy the linux-sandbox binaries (as before) and +# additionally fetch / unpack the two Rust targets that we currently support: +# - x86_64-unknown-linux-musl +# - aarch64-unknown-linux-gnu +# +# NOTE: This script is intended to be run from the repository root via +# `pnpm --filter codex-cli stage-release ...` or inside codex-cli with the +# helper script entry in package.json (`pnpm stage-release ...`). +# ----------------------------------------------------------------------------- set -euo pipefail -# Change to the codex-cli directory. -cd "$(dirname "${BASH_SOURCE[0]}")/.." +# Helper - usage / flag parsing -# First argument is where to stage the release. Creates a temporary directory -# if not provided. -RELEASE_DIR="${1:-$(mktemp -d)}" -[ -n "${1-}" ] && shift +usage() { + cat <&2 + usage 1 + ;; + *) + echo "Unexpected extra argument: $1" >&2 + usage 1 + ;; + esac + shift +done + +# Fallback when the caller did not specify a directory. +# If no directory was specified create a fresh temporary one. +if [[ -z "$TMPDIR" ]]; then + TMPDIR="$(mktemp -d)" +fi + +# Ensure the directory exists, then resolve to an absolute path. +mkdir -p "$TMPDIR" +TMPDIR="$(cd "$TMPDIR" && pwd)" + +# Main build logic + +echo "Staging release in $TMPDIR" + +# The script lives in codex-cli/scripts/ - change into codex-cli root so that +# relative paths keep working. +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +CODEX_CLI_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" + +pushd "$CODEX_CLI_ROOT" >/dev/null + +# 1. Build the JS artifacts --------------------------------------------------- -# Compile the JavaScript. pnpm install pnpm build -mkdir "$RELEASE_DIR/bin" -cp -r bin/codex.js "$RELEASE_DIR/bin/codex.js" -cp -r dist "$RELEASE_DIR/dist" -cp -r src "$RELEASE_DIR/src" # important if we want sourcemaps to continue to work -cp ../README.md "$RELEASE_DIR" -# TODO: Derive version from Git tag. -VERSION=$(printf '0.1.%d' "$(date +%y%m%d%H%M)") -jq --arg version "$VERSION" '.version = $version' package.json > "$RELEASE_DIR/package.json" -# Copy the native dependencies. -./scripts/install_native_deps.sh "$RELEASE_DIR" +# Paths inside the staged package +mkdir -p "$TMPDIR/bin" -echo "Staged version $VERSION for release in $RELEASE_DIR" +cp -r bin/codex.js "$TMPDIR/bin/codex.js" +cp -r dist "$TMPDIR/dist" +cp -r src "$TMPDIR/src" # keep source for TS sourcemaps +cp ../README.md "$TMPDIR" || true # README is one level up - ignore if missing + +# Derive a timestamp-based version (keep same scheme as before) +VERSION="$(printf '0.1.%d' "$(date +%y%m%d%H%M)")" + +# Modify package.json - bump version and optionally add the native directory to +# the files array so that the binaries are published to npm. + +jq --arg version "$VERSION" \ + '.version = $version' \ + package.json > "$TMPDIR/package.json" + +# 2. Native runtime deps (sandbox plus optional Rust binaries) + +if [[ "$INCLUDE_NATIVE" -eq 1 ]]; then + ./scripts/install_native_deps.sh "$TMPDIR" --rust +else + ./scripts/install_native_deps.sh "$TMPDIR" +fi + +popd >/dev/null + +echo "Staged version $VERSION for release in $TMPDIR" + +# Print final hint for convenience +echo "Next: cd \"$TMPDIR\" && npm publish"