chore: introduce new --native flag to Node module release process (#844)

This PR introduces an optional build flag, `--native`, that will build a
version of the Codex npm module that:

- Includes both the Node.js and native Rust versions (for Mac and Linux)
- Will run the native version if `CODEX_RUST=1` is set
- Runs the TypeScript version otherwise

Note this PR also updates the workflow URL to
https://github.com/openai/codex/actions/runs/14872557396, as that is a
build from today that includes everything up through
https://github.com/openai/codex/pull/843.

Test Plan:

In `~/code/codex/codex-cli`, I ran:

```
pnpm stage-release --native
```

The end of the output was:

```
Staged version 0.1.2505121317 for release in /var/folders/wm/f209bc1n2bd_r0jncn9s6j_00000gp/T/tmp.xd2p5ETYGN
Test Node:
    node /var/folders/wm/f209bc1n2bd_r0jncn9s6j_00000gp/T/tmp.xd2p5ETYGN/bin/codex.js --help
Test Rust:
    CODEX_RUST=1 node /var/folders/wm/f209bc1n2bd_r0jncn9s6j_00000gp/T/tmp.xd2p5ETYGN/bin/codex.js --help
Next:  cd "/var/folders/wm/f209bc1n2bd_r0jncn9s6j_00000gp/T/tmp.xd2p5ETYGN" && npm publish --tag native
```

I verified that running each of these commands ran the expected version
of Codex.

While here, I also added `bin` to the `files` list in `package.json`,
which should have been done as part of
https://github.com/openai/codex/pull/757, as that added new entries to
`bin` that were matched by `.gitignore` but should have been included in
a release.
This commit is contained in:
Michael Bolin
2025-05-12 13:38:10 -07:00
committed by GitHub
parent f3bd143867
commit 73fe1381aa
6 changed files with 277 additions and 46 deletions

View File

@@ -652,17 +652,21 @@ The **DCO check** blocks merges until every commit in the PR carries the footer
### Releasing `codex` ### 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 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) 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: 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:

View File

@@ -1,6 +1,6 @@
module.exports = { module.exports = {
root: true, root: true,
env: { browser: true, es2020: true }, env: { browser: true, node: true, es2020: true },
extends: [ extends: [
"eslint:recommended", "eslint:recommended",
"plugin:@typescript-eslint/recommended", "plugin:@typescript-eslint/recommended",

View File

@@ -1,17 +1,89 @@
#!/usr/bin/env node #!/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.
*
* 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.
*
* If the CODEX_RUST=1 is specified and there is no native binary for the
* current platform / architecture, an error is thrown.
*/
// Unified entry point for Codex CLI on all platforms import { spawnSync } from "child_process";
// Dynamically loads the compiled ESM bundle in dist/cli.js import path from "path";
import { fileURLToPath, pathToFileURL } from "url";
import path from 'path'; // Determine whether the user explicitly wants the Rust CLI.
import { fileURLToPath, pathToFileURL } from 'url'; const wantsNative =
process.env.CODEX_RUST != null
? ["1", "true", "yes"].includes(process.env.CODEX_RUST.toLowerCase())
: false;
// Try native binary if requested.
if (wantsNative) {
const { platform, arch } = process;
let targetTriple = null;
switch (platform) {
case "linux":
switch (arch) {
case "x64":
targetTriple = "x86_64-unknown-linux-musl";
break;
case "arm64":
targetTriple = "aarch64-unknown-linux-gnu";
break;
default:
break;
}
break;
case "darwin":
switch (arch) {
case "x64":
targetTriple = "x86_64-apple-darwin";
break;
case "arm64":
targetTriple = "aarch64-apple-darwin";
break;
default:
break;
}
break;
default:
break;
}
if (!targetTriple) {
throw new Error(`Unsupported platform: ${platform} (${arch})`);
}
// __dirname equivalent in ESM
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const binaryPath = path.join(__dirname, "..", "bin", `codex-${targetTriple}`);
const result = spawnSync(binaryPath, process.argv.slice(2), {
stdio: "inherit",
});
const exitCode = typeof result.status === "number" ? result.status : 1;
process.exit(exitCode);
}
// Fallback: execute the original JavaScript CLI.
// Determine this script's directory // Determine this script's directory
const __filename = fileURLToPath(import.meta.url); const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename); const __dirname = path.dirname(__filename);
// Resolve the path to the compiled CLI bundle // Resolve the path to the compiled CLI bundle
const cliPath = path.resolve(__dirname, '../dist/cli.js'); const cliPath = path.resolve(__dirname, "../dist/cli.js");
const cliUrl = pathToFileURL(cliPath).href; const cliUrl = pathToFileURL(cliPath).href;
// Load and execute the CLI // Load and execute the CLI
@@ -21,7 +93,6 @@ const cliUrl = pathToFileURL(cliPath).href;
} catch (err) { } catch (err) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.error(err); console.error(err);
// eslint-disable-next-line no-undef
process.exit(1); process.exit(1);
} }
})(); })();

View File

@@ -23,6 +23,7 @@
"stage-release": "./scripts/stage_release.sh" "stage-release": "./scripts/stage_release.sh"
}, },
"files": [ "files": [
"bin",
"dist" "dist"
], ],
"dependencies": { "dependencies": {

View File

@@ -1,20 +1,44 @@
#!/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: # By default the script copies the sandbox binaries that are required at
# ./scripts/install_native_deps.sh [CODEX_CLI_ROOT] # runtime. When called with the --full-native flag, 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 # Usage
# [CODEX_CLI_ROOT] Optional. If supplied, it should be the codex-cli # install_native_deps.sh [RELEASE_ROOT] [--full-native]
# folder that contains the package.json for @openai/codex.
# #
# When no argument is given we assume the script is being run directly from a # The optional RELEASE_ROOT is the path that contains package.json. Omitting
# development checkout. In that case we install the binaries into the # it installs the binaries into the repository's own bin/ folder to support
# repositorys own `bin/` directory so that the CLI can run locally. # local development.
set -euo pipefail set -euo pipefail
# ------------------
# Parse arguments
# ------------------
DEST_DIR=""
INCLUDE_RUST=0
for arg in "$@"; do
case "$arg" in
--full-native)
INCLUDE_RUST=1
;;
*)
if [[ -z "$DEST_DIR" ]]; then
DEST_DIR="$arg"
else
echo "Unexpected argument: $arg" >&2
exit 1
fi
;;
esac
done
# ---------------------------------------------------------------------------- # ----------------------------------------------------------------------------
# Determine where the binaries should be installed. # Determine where the binaries should be installed.
# ---------------------------------------------------------------------------- # ----------------------------------------------------------------------------
@@ -41,7 +65,7 @@ mkdir -p "$BIN_DIR"
# Until we start publishing stable GitHub releases, we have to grab the 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 # from the GitHub Action that created them. Update the URL below to point to the
# appropriate workflow run: # appropriate workflow run:
WORKFLOW_URL="https://github.com/openai/codex/actions/runs/14763725716" WORKFLOW_URL="https://github.com/openai/codex/actions/runs/14950726936"
WORKFLOW_ID="${WORKFLOW_URL##*/}" WORKFLOW_ID="${WORKFLOW_URL##*/}"
ARTIFACTS_DIR="$(mktemp -d)" ARTIFACTS_DIR="$(mktemp -d)"
@@ -50,12 +74,26 @@ trap 'rm -rf "$ARTIFACTS_DIR"' EXIT
# NB: The GitHub CLI `gh` must be installed and authenticated. # NB: The GitHub CLI `gh` must be installed and authenticated.
gh run download --dir "$ARTIFACTS_DIR" --repo openai/codex "$WORKFLOW_ID" gh run download --dir "$ARTIFACTS_DIR" --repo openai/codex "$WORKFLOW_ID"
# Decompress the two target architectures. # Decompress the artifacts for Linux sandboxing.
zstd -d "$ARTIFACTS_DIR/x86_64-unknown-linux-musl/codex-linux-sandbox-x86_64-unknown-linux-musl.zst" \ 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" -o "$BIN_DIR/codex-linux-sandbox-x64"
zstd -d "$ARTIFACTS_DIR/aarch64-unknown-linux-gnu/codex-linux-sandbox-aarch64-unknown-linux-gnu.zst" \ zstd -d "$ARTIFACTS_DIR/aarch64-unknown-linux-gnu/codex-linux-sandbox-aarch64-unknown-linux-gnu.zst" \
-o "$BIN_DIR/codex-linux-sandbox-arm64" -o "$BIN_DIR/codex-linux-sandbox-arm64"
echo "Installed native dependencies into $BIN_DIR" if [[ "$INCLUDE_RUST" -eq 1 ]]; then
# 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"
# ARM64 Linux
zstd -d "$ARTIFACTS_DIR/aarch64-unknown-linux-gnu/codex-aarch64-unknown-linux-gnu.zst" \
-o "$BIN_DIR/codex-aarch64-unknown-linux-gnu"
# x64 macOS
zstd -d "$ARTIFACTS_DIR/x86_64-apple-darwin/codex-x86_64-apple-darwin.zst" \
-o "$BIN_DIR/codex-x86_64-apple-darwin"
# ARM64 macOS
zstd -d "$ARTIFACTS_DIR/aarch64-apple-darwin/codex-aarch64-apple-darwin.zst" \
-o "$BIN_DIR/codex-aarch64-apple-darwin"
fi
echo "Installed native dependencies into $BIN_DIR"

View File

@@ -1,28 +1,145 @@
#!/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 <dir> : Use <dir> 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 set -euo pipefail
# Change to the codex-cli directory. # Helper - usage / flag parsing
cd "$(dirname "${BASH_SOURCE[0]}")/.."
# First argument is where to stage the release. Creates a temporary directory usage() {
# if not provided. cat <<EOF
RELEASE_DIR="${1:-$(mktemp -d)}" Usage: $(basename "$0") [--tmp DIR] [--native]
[ -n "${1-}" ] && shift
Options
--tmp DIR Use DIR to stage the release (defaults to a fresh mktemp dir)
--native Bundle Rust binaries for Linux (fat package)
-h, --help Show this help
Legacy positional argument: the first non-flag argument is still interpreted
as the temporary directory (for backwards compatibility) but is deprecated.
EOF
exit "${1:-0}"
}
TMPDIR=""
INCLUDE_NATIVE=0
# Manual flag parser - Bash getopts does not handle GNU long options well.
while [[ $# -gt 0 ]]; do
case "$1" in
--tmp)
shift || { echo "--tmp requires an argument"; usage 1; }
TMPDIR="$1"
;;
--tmp=*)
TMPDIR="${1#*=}"
;;
--native)
INCLUDE_NATIVE=1
;;
-h|--help)
usage 0
;;
--*)
echo "Unknown option: $1" >&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 install
pnpm build 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. # Paths inside the staged package
./scripts/install_native_deps.sh "$RELEASE_DIR" 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" --full-native
else
./scripts/install_native_deps.sh "$TMPDIR"
fi
popd >/dev/null
echo "Staged version $VERSION for release in $TMPDIR"
echo "Test Node:"
echo " node ${TMPDIR}/bin/codex.js --help"
if [[ "$INCLUDE_NATIVE" -eq 1 ]]; then
echo "Test Rust:"
echo " CODEX_RUST=1 node ${TMPDIR}/bin/codex.js --help"
fi
# Print final hint for convenience
if [[ "$INCLUDE_NATIVE" -eq 1 ]]; then
echo "Next: cd \"$TMPDIR\" && npm publish --tag native"
else
echo "Next: cd \"$TMPDIR\" && npm publish"
fi