mirror of
https://github.com/openai/codex.git
synced 2026-05-20 19:23:21 +00:00
## 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
154 lines
4.7 KiB
Python
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)
|