"""Command-line interface for building Codex package directories.""" import argparse import re import tempfile from pathlib import Path from .archive import write_archive from .cargo import build_source_binaries 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 .symbols import strip_binary 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 # Release pipelines run this builder with system Python, so avoid new dependencies. SEMVER_PATTERN = re.compile( r"(?P0|[1-9][0-9]*)\." r"(?P0|[1-9][0-9]*)\." r"(?P0|[1-9][0-9]*)" r"(?:-(?P[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?" r"(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?" ) def parse_package_version(value: str) -> str: match = SEMVER_PATTERN.fullmatch(value) if match is not None: components = (match.group(name) for name in ("major", "minor", "patch")) prerelease = match.group("prerelease") or "" if all(int(component) <= 2**64 - 1 for component in components) and all( not (part.isdigit() and len(part) > 1 and part.startswith("0")) for part in prerelease.split(".") ): return value raise argparse.ArgumentTypeError(f"invalid semantic version: {value!r}") def parse_args() -> argparse.Namespace: return create_parser().parse_args() def create_parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser( description="Build a canonical Codex package directory and optional archive.", formatter_class=argparse.ArgumentDefaultsHelpFormatter, ) parser.add_argument( "--target", default=argparse.SUPPRESS, choices=sorted(TARGET_SPECS), help=( "Rust target triple for the package. Defaults to the release target " "for this host platform." ), ) parser.add_argument( "--variant", choices=sorted(PACKAGE_VARIANTS), default="codex", help="Package variant to build.", ) parser.add_argument( "--package-version", type=parse_package_version, default=read_workspace_version(), help="Semantic version to record in codex-package.json.", ) parser.add_argument( "--package-dir", type=Path, default=argparse.SUPPRESS, help=( "Output directory to create as the package root. Defaults to a new temporary directory." ), ) parser.add_argument( "--archive-output", type=Path, action="append", default=[], help=( "Optional archive output path. May be repeated. Supported suffixes: " ".tar.gz, .tgz, .tar.zst, .zip." ), ) parser.add_argument( "--force", action="store_true", help="Replace an existing package directory or archive output.", ) parser.add_argument( "--cargo", default="cargo", help="Cargo executable to use for source-built package artifacts.", ) parser.add_argument( "--cargo-profile", default="dev-small", help=( "Cargo profile for source-built package artifacts. Use release for release packages." ), ) parser.add_argument( "--strip", choices=("auto", "all", "none"), default="auto", help=( "Strip Unix first-party package binaries: auto strips source-built " "release-profile inputs, all also strips other profiles and prebuilt " "inputs, none preserves symbols. " "Original inputs and third-party resources are never modified." ), ) parser.add_argument( "--strip-tool", help="Target-compatible strip executable override, for example llvm-strip.", ) 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( "--code-mode-host-bin", type=Path, help=( "Optional prebuilt codex-code-mode-host executable. If omitted, " "the host is built with Cargo." ), ) parser.add_argument( "--bwrap-bin", type=Path, help=( "Optional prebuilt Linux bwrap executable. If omitted for Linux " "targets, bwrap is built with Cargo." ), ) parser.add_argument( "--codex-command-runner-bin", type=Path, help=( "Optional prebuilt Windows codex-command-runner.exe executable. " "If omitted for Windows targets, codex-command-runner is built " "with Cargo." ), ) parser.add_argument( "--codex-windows-sandbox-setup-bin", type=Path, help=( "Optional prebuilt Windows codex-windows-sandbox-setup.exe " "executable. If omitted for Windows targets, " "codex-windows-sandbox-setup is built with Cargo." ), ) parser.add_argument( "--rg-bin", type=Path, help=( "Optional local ripgrep executable override instead of fetching from " "scripts/codex_package/rg." ), ) return parser def main() -> int: args = parse_args() package_dir = assemble_package(args) archive_package(package_dir, args) return 0 def assemble_package(args: argparse.Namespace) -> Path: """Build the directory before optional downstream additions and archiving.""" 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() if package_dir_arg is not None else Path(tempfile.mkdtemp(prefix="codex-package-")).resolve() ) source_outputs = build_source_binaries( spec, variant, cargo=args.cargo, profile=args.cargo_profile, entrypoint_bin=resolve_optional_input_path( args.entrypoint_bin, "prebuilt entrypoint executable", "--entrypoint-bin", ), code_mode_host_bin=resolve_optional_input_path( args.code_mode_host_bin, "prebuilt code-mode host executable", "--code-mode-host-bin", ), bwrap_bin=resolve_optional_input_path( args.bwrap_bin, "prebuilt Linux bwrap executable", "--bwrap-bin", ), codex_command_runner_bin=resolve_optional_input_path( args.codex_command_runner_bin, "prebuilt Windows codex-command-runner.exe executable", "--codex-command-runner-bin", ), codex_windows_sandbox_setup_bin=resolve_optional_input_path( args.codex_windows_sandbox_setup_bin, "prebuilt Windows codex-windows-sandbox-setup.exe executable", "--codex-windows-sandbox-setup-bin", ), ) inputs = PackageInputs( entrypoint_bin=source_outputs.entrypoint_bin, code_mode_host_bin=source_outputs.code_mode_host_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.package_version, variant, spec, inputs) for filename, prebuilt in ( (variant.entrypoint_name(spec), args.entrypoint_bin), (f"codex-code-mode-host{spec.exe_suffix}", args.code_mode_host_bin), ): if args.strip == "all" or ( args.strip == "auto" and args.cargo_profile == "release" and prebuilt is None ): strip_binary(package_dir / "bin" / filename, spec, tool=args.strip_tool) validate_package_dir(package_dir, variant, spec) return package_dir def archive_package(package_dir: Path, args: argparse.Namespace) -> None: for archive_output in args.archive_output: archive_path = archive_output.resolve() write_archive(package_dir, archive_path, force=args.force) print(f"Built Codex package archive at {archive_path}") print(f"Built Codex package directory at {package_dir}") def resolve_optional_input_path( explicit_path: Path | None, description: str, flag_name: str, ) -> Path | None: if explicit_path is None: return None return resolve_input_path(explicit_path, description, flag_name)