build: package prebuilt Codex entrypoints (#23586)

## Why

The package builder should describe the binaries it is actually
packaging, not require callers to restate release metadata out of band.
A caller-provided `--version` flag can drift from the workspace version,
but running the target entrypoint to discover its version breaks
cross-target packages when the produced binary cannot execute on the
build host.

This PR keeps package metadata tied to the repository source of truth by
reading `[workspace.package].version` from `codex-rs/Cargo.toml`. It
also prepares the package layout for `codex-app-server` packages: the
same package structure can now represent either the CLI entrypoint or
the app-server entrypoint while keeping shared sidecars such as `rg`,
`bwrap`, and Windows sandbox helpers in the existing package
directories.

## What changed

- Removes the `--version` CLI flag from
`scripts/build_codex_package.py`.
- Adds Cargo.toml version discovery for `codex-package.json.version` via
`codex-rs/Cargo.toml`.
- Adds `--entrypoint-bin` so callers can package a prebuilt entrypoint
instead of rebuilding it with Cargo.
- Makes `--variant` an explicit choice between `codex` and
`codex-app-server`, and uses it to select the cargo binary and packaged
`bin/` entrypoint name.
- Updates `scripts/codex_package/README.md` to document variants,
prebuilt entrypoints, and Cargo.toml version detection.

## Verification

- Compiled `scripts/build_codex_package.py` and
`scripts/codex_package/*.py` with `PYTHONDONTWRITEBYTECODE=1`.
- Ran `scripts/build_codex_package.py --help` and verified `--version`
is gone while `--variant` and `--entrypoint-bin` are present.
- Verified the package builder reads version `0.0.0` from
`codex-rs/Cargo.toml`.
- Built a fake cross-target `codex-app-server` package using a
non-executable `--entrypoint-bin`; verified metadata records version
`0.0.0`, variant `codex-app-server`, and `bin/codex-app-server` as the
entrypoint.
This commit is contained in:
Michael Bolin
2026-05-19 22:10:03 -07:00
committed by GitHub
parent dc255b0d8a
commit 343a74076f
6 changed files with 147 additions and 43 deletions

View File

@@ -10,7 +10,7 @@ The builder creates a canonical Codex package directory:
.
├── codex-package.json
├── bin
│ └── codex[.exe]
│ └── <entrypoint>[.exe]
├── codex-resources
│ ├── bwrap # Linux only
│ ├── codex-command-runner.exe # Windows only
@@ -28,18 +28,24 @@ artifacts; pass a GNU Linux target explicitly for native glibc local builds. If
`--package-dir` is omitted, the builder creates a new temporary directory and
prints its path after the package is built.
The `--variant` flag selects the package entrypoint. Supported variants are
`codex` and `codex-app-server`. The `version` field in `codex-package.json` is
read from `[workspace.package].version` in `codex-rs/Cargo.toml`.
## Source-built artifacts
Artifacts built from this repository are always built by the package builder in
one grouped `cargo build` command per package:
one grouped `cargo build` command per package when they are needed:
- all targets: `codex`
- all targets: the selected entrypoint, unless `--entrypoint-bin` is provided
- Linux targets: `bwrap`
- Windows targets: `codex-command-runner` and `codex-windows-sandbox-setup`
The default cargo profile is `dev-small` because local iteration should favor
fast, small builds. Release jobs should pass `--cargo-profile release` and an
explicit target.
explicit target. Release jobs that already built and signed/notarized the
entrypoint should pass `--entrypoint-bin` so the package contains that exact
binary instead of rebuilding it.
`rg` is not built from this repository, so the builder fetches it from the
DotSlash manifest at `codex-cli/bin/rg`. Downloaded archives are cached under

View File

@@ -6,6 +6,7 @@ from dataclasses import dataclass
from pathlib import Path
from .targets import REPO_ROOT
from .targets import PackageVariant
from .targets import TargetSpec
@@ -14,7 +15,7 @@ CODEX_RS_ROOT = REPO_ROOT / "codex-rs"
@dataclass(frozen=True)
class SourceBuildOutputs:
codex_bin: Path
entrypoint_bin: Path
bwrap_bin: Path | None
codex_command_runner_bin: Path | None
codex_windows_sandbox_setup_bin: Path | None
@@ -22,28 +23,39 @@ class SourceBuildOutputs:
def build_source_binaries(
spec: TargetSpec,
variant: PackageVariant,
*,
cargo: str,
profile: str,
entrypoint_bin: Path | None,
) -> SourceBuildOutputs:
binaries = source_binaries_for_target(spec)
cmd = [
cargo,
"build",
"--target",
spec.target,
"--profile",
profile,
]
for binary in binaries:
cmd.extend(["--bin", binary])
binaries = source_binaries_for_target(
spec,
variant,
build_entrypoint=entrypoint_bin is None,
)
if binaries:
cmd = [
cargo,
"build",
"--target",
spec.target,
"--profile",
profile,
]
for binary in binaries:
cmd.extend(["--bin", binary])
print("+", " ".join(cmd))
subprocess.run(cmd, cwd=CODEX_RS_ROOT, check=True)
print("+", " ".join(cmd))
subprocess.run(cmd, cwd=CODEX_RS_ROOT, check=True)
output_dir = cargo_profile_output_dir(spec, profile)
outputs = SourceBuildOutputs(
codex_bin=output_dir / spec.codex_name,
entrypoint_bin=(
entrypoint_bin.resolve()
if entrypoint_bin is not None
else output_dir / variant.entrypoint_name(spec)
),
bwrap_bin=output_dir / "bwrap" if spec.is_linux else None,
codex_command_runner_bin=(
output_dir / "codex-command-runner.exe" if spec.is_windows else None
@@ -56,8 +68,15 @@ def build_source_binaries(
return outputs
def source_binaries_for_target(spec: TargetSpec) -> list[str]:
binaries = ["codex"]
def source_binaries_for_target(
spec: TargetSpec,
variant: PackageVariant,
*,
build_entrypoint: bool,
) -> list[str]:
binaries = []
if build_entrypoint:
binaries.append(variant.cargo_bin)
if spec.is_linux:
binaries.append("bwrap")
if spec.is_windows:
@@ -97,7 +116,7 @@ def cargo_profile_dirname(profile: str) -> str:
def validate_source_outputs(outputs: SourceBuildOutputs) -> None:
for path in [
outputs.codex_bin,
outputs.entrypoint_bin,
outputs.bwrap_bin,
outputs.codex_command_runner_bin,
outputs.codex_windows_sandbox_setup_bin,

View File

@@ -10,9 +10,12 @@ from .layout import build_package_dir
from .layout import prepare_package_dir
from .layout import validate_package_dir
from .ripgrep import resolve_rg_bin
from .targets import PACKAGE_VARIANTS
from .targets import TARGET_SPECS
from .targets import PackageInputs
from .targets import default_target
from .targets import resolve_input_path
from .version import read_workspace_version
def parse_args() -> argparse.Namespace:
@@ -29,15 +32,11 @@ def parse_args() -> argparse.Namespace:
"for this host platform."
),
)
parser.add_argument(
"--version",
default="0.0.0-dev",
help="Codex version to record in codex-package.json.",
)
parser.add_argument(
"--variant",
choices=sorted(PACKAGE_VARIANTS),
default="codex",
help="Package variant to record in codex-package.json.",
help="Package variant to build.",
)
parser.add_argument(
"--package-dir",
@@ -74,6 +73,14 @@ def parse_args() -> argparse.Namespace:
"release packages."
),
)
parser.add_argument(
"--entrypoint-bin",
type=Path,
help=(
"Optional prebuilt entrypoint executable for the selected package "
"variant. If omitted, the entrypoint is built with Cargo."
),
)
parser.add_argument(
"--rg-bin",
type=Path,
@@ -88,6 +95,7 @@ def parse_args() -> argparse.Namespace:
def main() -> int:
args = parse_args()
spec = TARGET_SPECS[getattr(args, "target", None) or default_target()]
variant = PACKAGE_VARIANTS[args.variant]
package_dir_arg = getattr(args, "package_dir", None)
package_dir = (
package_dir_arg.resolve()
@@ -97,19 +105,30 @@ def main() -> int:
source_outputs = build_source_binaries(
spec,
variant,
cargo=args.cargo,
profile=args.cargo_profile,
entrypoint_bin=(
resolve_input_path(
args.entrypoint_bin,
"prebuilt entrypoint executable",
"--entrypoint-bin",
)
if args.entrypoint_bin is not None
else None
),
)
version = read_workspace_version()
inputs = PackageInputs(
codex_bin=source_outputs.codex_bin,
entrypoint_bin=source_outputs.entrypoint_bin,
rg_bin=resolve_rg_bin(spec, args.rg_bin),
bwrap_bin=source_outputs.bwrap_bin,
codex_command_runner_bin=source_outputs.codex_command_runner_bin,
codex_windows_sandbox_setup_bin=source_outputs.codex_windows_sandbox_setup_bin,
)
prepare_package_dir(package_dir, force=args.force)
build_package_dir(package_dir, args.version, args.variant, spec, inputs)
validate_package_dir(package_dir, spec)
build_package_dir(package_dir, version, variant, spec, inputs)
validate_package_dir(package_dir, variant, spec)
archive_output = args.archive_output
if archive_output is not None:

View File

@@ -6,6 +6,7 @@ import stat
from pathlib import Path
from .targets import PackageInputs
from .targets import PackageVariant
from .targets import TargetSpec
@@ -30,7 +31,7 @@ def prepare_package_dir(package_dir: Path, *, force: bool) -> None:
def build_package_dir(
package_dir: Path,
version: str,
variant: str,
variant: PackageVariant,
spec: TargetSpec,
inputs: PackageInputs,
) -> None:
@@ -41,7 +42,12 @@ def build_package_dir(
resources_dir.mkdir()
path_dir.mkdir()
copy_executable(inputs.codex_bin, bin_dir / spec.codex_name, is_windows=spec.is_windows)
entrypoint_name = variant.entrypoint_name(spec)
copy_executable(
inputs.entrypoint_bin,
bin_dir / entrypoint_name,
is_windows=spec.is_windows,
)
copy_executable(inputs.rg_bin, path_dir / spec.rg_name, is_windows=spec.is_windows)
if inputs.bwrap_bin is not None:
@@ -65,15 +71,19 @@ def build_package_dir(
"layoutVersion": LAYOUT_VERSION,
"version": version,
"target": spec.target,
"variant": variant,
"entrypoint": f"bin/{spec.codex_name}",
"variant": variant.name,
"entrypoint": f"bin/{entrypoint_name}",
"resourcesDir": "codex-resources",
"pathDir": "codex-path",
}
write_json(package_dir / "codex-package.json", metadata)
def validate_package_dir(package_dir: Path, spec: TargetSpec) -> None:
def validate_package_dir(
package_dir: Path,
variant: PackageVariant,
spec: TargetSpec,
) -> None:
required_dirs = [
Path("bin"),
Path("codex-resources"),
@@ -94,7 +104,8 @@ def validate_package_dir(package_dir: Path, spec: TargetSpec) -> None:
expected_metadata = {
"layoutVersion": LAYOUT_VERSION,
"target": spec.target,
"entrypoint": f"bin/{spec.codex_name}",
"variant": variant.name,
"entrypoint": f"bin/{variant.entrypoint_name(spec)}",
"resourcesDir": "codex-resources",
"pathDir": "codex-path",
}
@@ -106,7 +117,7 @@ def validate_package_dir(package_dir: Path, spec: TargetSpec) -> None:
)
required_files = [
Path("bin") / spec.codex_name,
Path("bin") / variant.entrypoint_name(spec),
Path("codex-path") / spec.rg_name,
]
executable_files = list(required_files)

View File

@@ -21,24 +21,44 @@ class TargetSpec:
def exe_suffix(self) -> str:
return ".exe" if self.is_windows else ""
@property
def codex_name(self) -> str:
return f"codex{self.exe_suffix}"
@property
def rg_name(self) -> str:
return f"rg{self.exe_suffix}"
@dataclass(frozen=True)
class PackageVariant:
name: str
cargo_bin: str
executable_stem: str
def entrypoint_name(self, spec: TargetSpec) -> str:
return f"{self.executable_stem}{spec.exe_suffix}"
@dataclass(frozen=True)
class PackageInputs:
codex_bin: Path
entrypoint_bin: Path
rg_bin: Path
bwrap_bin: Path | None
codex_command_runner_bin: Path | None
codex_windows_sandbox_setup_bin: Path | None
PACKAGE_VARIANTS: dict[str, PackageVariant] = {
"codex": PackageVariant(
name="codex",
cargo_bin="codex",
executable_stem="codex",
),
"codex-app-server": PackageVariant(
name="codex-app-server",
cargo_bin="codex-app-server",
executable_stem="codex-app-server",
),
}
TARGET_SPECS: dict[str, TargetSpec] = {
"x86_64-unknown-linux-gnu": TargetSpec(
target="x86_64-unknown-linux-gnu",

View File

@@ -0,0 +1,29 @@
"""Version discovery for Codex packages."""
import re
from .targets import REPO_ROOT
WORKSPACE_VERSION_PATTERN = re.compile(r'^version\s*=\s*"([^"]+)"')
def read_workspace_version() -> str:
cargo_toml = REPO_ROOT / "codex-rs" / "Cargo.toml"
in_workspace_package = False
with open(cargo_toml, encoding="utf-8") as fh:
for line in fh:
stripped = line.strip()
if stripped == "[workspace.package]":
in_workspace_package = True
continue
if in_workspace_package and stripped.startswith("["):
break
if in_workspace_package:
match = WORKSPACE_VERSION_PATTERN.match(stripped)
if match is not None:
return match.group(1)
raise RuntimeError(f"Could not find [workspace.package].version in {cargo_toml}")