Files
codex/scripts/codex_package/layout.py
Michael Bolin 7f4d7ae3a4 build: add Codex package builder (#23513)
## Why

Codex CLI packaging is currently split across npm staging, standalone
installers, and release bundle creation, which makes it hard to define
and validate a single valid package directory. This adds the first
standalone package builder so later release paths can converge on the
same canonical layout.

## What changed

- Added `scripts/build_codex_package.py` as the stable executable
wrapper around `scripts/codex_package`.
- Added modules for CLI parsing, target metadata, grouped cargo builds,
package layout validation, and archive writing.
- The builder creates a package directory with `codex-package.json`,
`bin/`, `codex-resources/`, and `codex-path`, and can serialize it as
`.tar.gz`, `.tar.zst`, or `.zip`.
- Source-built artifacts are built by one grouped `cargo build`: `codex`
for all targets, `bwrap` for Linux, and the Windows sandbox helpers for
Windows. `rg` remains an input because it is vendored from upstream
rather than built from this repo.
- Added `scripts/codex_package/README.md` to document the package
layout, source-built artifacts, and cargo profile behavior.

## Verification

- Ran wrapper/module syntax compilation.
- Ran `scripts/build_codex_package.py --help` from `/private/tmp`.
- Ran fake-cargo package/archive builds for macOS, Linux, and Windows
target layouts, including an assertion that generated tar archives
contain no duplicate member names.


---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/openai/codex/pull/23513).
* #23526
* __->__ #23513
2026-05-19 19:54:03 +00:00

154 lines
4.7 KiB
Python

"""Canonical Codex package directory layout."""
import json
import shutil
import stat
from pathlib import Path
from .targets import PackageInputs
from .targets import TargetSpec
LAYOUT_VERSION = 1
def prepare_package_dir(package_dir: Path, *, force: bool) -> None:
if package_dir.exists():
if not package_dir.is_dir():
raise RuntimeError(f"Package output exists and is not a directory: {package_dir}")
if any(package_dir.iterdir()):
if not force:
raise RuntimeError(
f"Package output directory is not empty: {package_dir}. "
"Pass --force to replace it."
)
shutil.rmtree(package_dir)
package_dir.mkdir(parents=True, exist_ok=True)
def build_package_dir(
package_dir: Path,
version: str,
variant: str,
spec: TargetSpec,
inputs: PackageInputs,
) -> None:
bin_dir = package_dir / "bin"
resources_dir = package_dir / "codex-resources"
path_dir = package_dir / "codex-path"
bin_dir.mkdir()
resources_dir.mkdir()
path_dir.mkdir()
copy_executable(inputs.codex_bin, bin_dir / spec.codex_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:
copy_executable(inputs.bwrap_bin, resources_dir / "bwrap", is_windows=False)
if inputs.codex_command_runner_bin is not None:
copy_executable(
inputs.codex_command_runner_bin,
resources_dir / "codex-command-runner.exe",
is_windows=True,
)
if inputs.codex_windows_sandbox_setup_bin is not None:
copy_executable(
inputs.codex_windows_sandbox_setup_bin,
resources_dir / "codex-windows-sandbox-setup.exe",
is_windows=True,
)
metadata = {
"layoutVersion": LAYOUT_VERSION,
"version": version,
"target": spec.target,
"variant": variant,
"entrypoint": f"bin/{spec.codex_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:
required_dirs = [
Path("bin"),
Path("codex-resources"),
Path("codex-path"),
]
for relative_dir in required_dirs:
path = package_dir / relative_dir
if not path.is_dir():
raise RuntimeError(f"Missing package directory: {relative_dir}")
metadata_path = package_dir / "codex-package.json"
if not metadata_path.is_file():
raise RuntimeError("Missing package metadata: codex-package.json")
with open(metadata_path, encoding="utf-8") as fh:
metadata = json.load(fh)
expected_metadata = {
"layoutVersion": LAYOUT_VERSION,
"target": spec.target,
"entrypoint": f"bin/{spec.codex_name}",
"resourcesDir": "codex-resources",
"pathDir": "codex-path",
}
for key, expected in expected_metadata.items():
actual = metadata.get(key)
if actual != expected:
raise RuntimeError(
f"Invalid package metadata field {key!r}: expected {expected!r}, got {actual!r}"
)
required_files = [
Path("bin") / spec.codex_name,
Path("codex-path") / spec.rg_name,
]
executable_files = list(required_files)
if spec.is_linux:
required_files.append(Path("codex-resources") / "bwrap")
executable_files.append(Path("codex-resources") / "bwrap")
if spec.is_windows:
required_files.extend(
[
Path("codex-resources") / "codex-command-runner.exe",
Path("codex-resources") / "codex-windows-sandbox-setup.exe",
]
)
for relative_file in required_files:
path = package_dir / relative_file
if not path.is_file():
raise RuntimeError(f"Missing package file: {relative_file}")
if not spec.is_windows:
for relative_file in executable_files:
path = package_dir / relative_file
if not is_executable(path):
raise RuntimeError(f"Package file is not executable: {relative_file}")
def copy_executable(src: Path, dest: Path, *, is_windows: bool) -> None:
dest.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(src, dest)
if not is_windows:
mode = dest.stat().st_mode
dest.chmod(mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
def write_json(path: Path, value: object) -> None:
with open(path, "w", encoding="utf-8") as out:
json.dump(value, out, indent=2)
out.write("\n")
def is_executable(path: Path) -> bool:
return bool(path.stat().st_mode & stat.S_IXUSR)