diff --git a/scripts/codex_package/README.md b/scripts/codex_package/README.md index c4abb58afd..c1674a8b77 100644 --- a/scripts/codex_package/README.md +++ b/scripts/codex_package/README.md @@ -10,7 +10,7 @@ The builder creates a canonical Codex package directory: . ├── codex-package.json ├── bin -│ └── codex[.exe] +│ └── [.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 diff --git a/scripts/codex_package/cargo.py b/scripts/codex_package/cargo.py index 9ee2d2f832..c265808b16 100644 --- a/scripts/codex_package/cargo.py +++ b/scripts/codex_package/cargo.py @@ -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, diff --git a/scripts/codex_package/cli.py b/scripts/codex_package/cli.py index 6b2598445f..16324cf44d 100644 --- a/scripts/codex_package/cli.py +++ b/scripts/codex_package/cli.py @@ -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: diff --git a/scripts/codex_package/layout.py b/scripts/codex_package/layout.py index faf24ef810..6d8982a631 100644 --- a/scripts/codex_package/layout.py +++ b/scripts/codex_package/layout.py @@ -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) diff --git a/scripts/codex_package/targets.py b/scripts/codex_package/targets.py index d4f85953c2..4af0d4a00d 100644 --- a/scripts/codex_package/targets.py +++ b/scripts/codex_package/targets.py @@ -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", diff --git a/scripts/codex_package/version.py b/scripts/codex_package/version.py new file mode 100644 index 0000000000..7aec8b9ccb --- /dev/null +++ b/scripts/codex_package/version.py @@ -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}")