diff --git a/scripts/codex_package/README.md b/scripts/codex_package/README.md index cf38cfa38f..461b80e343 100644 --- a/scripts/codex_package/README.md +++ b/scripts/codex_package/README.md @@ -31,8 +31,9 @@ artifacts; pass a GNU Linux target explicitly for native glibc local builds. If 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`. +`codex` and `codex-app-server`. The `--package-version` flag sets the version in +`codex-package.json`; it defaults to `[workspace.package].version` in +`codex-rs/Cargo.toml`. ## Source-built artifacts diff --git a/scripts/codex_package/cli.py b/scripts/codex_package/cli.py index 595158d2dc..fa569e9ae7 100644 --- a/scripts/codex_package/cli.py +++ b/scripts/codex_package/cli.py @@ -1,6 +1,7 @@ """Command-line interface for building Codex package directories.""" import argparse +import re import tempfile from pathlib import Path @@ -19,6 +20,30 @@ from .zsh import resolve_zsh_bin 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: parser = argparse.ArgumentParser( description="Build a canonical Codex package directory and optional archive.", @@ -39,6 +64,12 @@ def parse_args() -> argparse.Namespace: 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, @@ -183,7 +214,6 @@ def main() -> int: "--codex-windows-sandbox-setup-bin", ), ) - version = read_workspace_version() inputs = PackageInputs( entrypoint_bin=source_outputs.entrypoint_bin, code_mode_host_bin=source_outputs.code_mode_host_bin, @@ -194,7 +224,7 @@ def main() -> int: 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, version, variant, spec, inputs) + build_package_dir(package_dir, args.package_version, variant, spec, inputs) validate_package_dir( package_dir, variant, spec, include_zsh=inputs.zsh_bin is not None ) diff --git a/scripts/codex_package/test_cli.py b/scripts/codex_package/test_cli.py new file mode 100644 index 0000000000..3b48467317 --- /dev/null +++ b/scripts/codex_package/test_cli.py @@ -0,0 +1,48 @@ +#!/usr/bin/env python3 + +import argparse +from pathlib import Path +import sys +import unittest + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) + +from codex_package.cli import parse_package_version + + +class PackageVersionTest(unittest.TestCase): + def test_accepts_release_prerelease_and_build_versions(self) -> None: + for version in ( + "0.0.0", + "1.2.3", + "0.0.0-internal.deadbeef", + "1.2.3-alpha.1+build.01", + "18446744073709551615.0.0", + ): + with self.subTest(version=version): + self.assertEqual(parse_package_version(version), version) + + def test_rejects_versions_the_runtime_cannot_parse(self) -> None: + for version in ( + "", + "1", + "1.2", + "1.2.3.4", + "v1.2.3", + "01.2.3", + "1.02.3", + "1.2.03", + "1.2.3-", + "1.2.3-alpha..1", + "1.2.3-01", + "1.2.3+", + "1.2.3+build..1", + "18446744073709551616.0.0", + ): + with self.subTest(version=version): + with self.assertRaises(argparse.ArgumentTypeError): + parse_package_version(version) + + +if __name__ == "__main__": + unittest.main()