Stage publishable Python runtime wheels (#18865)

This is PR 2 of the Python SDK PyPI publishing split. [PR
1](https://github.com/openai/codex/pull/18862) refreshed the generated
SDK bindings; this PR makes the runtime package itself publishable, and
PR 3 will wire the SDK package/version pinning to this runtime package.

## Summary
- Rename the runtime distribution to `openai-codex-cli-bin` while
keeping the import package as `codex_cli_bin`.
- Make the runtime package wheel-only and build `py3-none-<platform>`
wheels instead of interpreter-specific wheels.
- Add `stage-runtime --codex-version` and `--platform-tag` so release
staging can produce the platform wheel matrix from Codex release tags.
- Add focused artifact workflow tests for version normalization,
platform tag injection, and runtime wheel metadata.

## Why Rename
There is already an unofficial PyPI package,
[`codex-bin`](https://pypi.org/project/codex-bin/), distributing OpenAI
Codex binaries. Publishing the official SDK runtime dependency as
`openai-codex-cli-bin` makes the ownership clear, avoids confusing the
SDK-pinned runtime wheel with that unowned wrapper, and keeps the import
package unchanged as `codex_cli_bin`.

## Tests
- `uv run --extra dev pytest
tests/test_artifact_workflow_and_binaries.py` -> 21 passed
- `uv run --extra dev python scripts/update_sdk_artifacts.py
stage-runtime /tmp/codex-python-pr2-rebased/runtime-stage
/tmp/codex-python-pr2-rebased/codex --codex-version
rust-v0.116.0-alpha.1 --platform-tag macosx_11_0_arm64`
- `uv run --with build --extra dev python -m build --wheel
/tmp/codex-python-pr2-rebased/runtime-stage`
- `uv run --with twine --extra dev twine check
/tmp/codex-python-pr2-rebased/runtime-stage/dist/openai_codex_cli_bin-0.116.0a1-py3-none-macosx_11_0_arm64.whl`

## Note
- Full `uv run --extra dev pytest` currently fails because regenerating
from schemas already on `main` adds new DeviceKey Python types. I left
that generated catch-up out of this runtime-only PR.
This commit is contained in:
Steve Coffey
2026-04-22 08:14:48 -07:00
committed by GitHub
parent 0ebe69a8c3
commit 0127cef5db
12 changed files with 228 additions and 54 deletions

View File

@@ -17,6 +17,8 @@ from dataclasses import dataclass
from pathlib import Path
from typing import Any, Callable, Sequence, get_args, get_origin
RUNTIME_DISTRIBUTION_NAME = "openai-codex-cli-bin"
def repo_root() -> Path:
return Path(__file__).resolve().parents[3]
@@ -76,6 +78,24 @@ def current_sdk_version() -> str:
return match.group(1)
def normalize_codex_version(version: str) -> str:
normalized = version.strip()
if normalized.startswith("rust-v"):
normalized = normalized.removeprefix("rust-v")
elif normalized.startswith("v"):
normalized = normalized.removeprefix("v")
normalized = re.sub(r"-alpha\.?([0-9]+)$", r"a\1", normalized)
normalized = re.sub(r"-beta\.?([0-9]+)$", r"b\1", normalized)
normalized = re.sub(r"-rc\.?([0-9]+)$", r"rc\1", normalized)
if not re.fullmatch(r"[0-9]+(?:\.[0-9]+)*(?:(?:a|b|rc)[0-9]+)?", normalized):
raise RuntimeError(
f"Could not normalize Codex version {version!r} to a PEP 440 version"
)
return normalized
def _copy_package_tree(src: Path, dst: Path) -> None:
if dst.exists():
if dst.is_dir():
@@ -110,6 +130,46 @@ def _rewrite_project_version(pyproject_text: str, version: str) -> str:
return updated
def _rewrite_runtime_platform_tag(pyproject_text: str, platform_tag: str) -> str:
section = "[tool.hatch.build.targets.wheel.hooks.custom]"
section_index = pyproject_text.find(section)
if section_index == -1:
raise RuntimeError("Could not find runtime wheel custom hook config")
next_section_index = pyproject_text.find("\n[", section_index + len(section))
if next_section_index == -1:
section_text = pyproject_text[section_index:]
tail = ""
else:
section_text = pyproject_text[section_index:next_section_index]
tail = pyproject_text[next_section_index:]
updated_section, count = re.subn(
r'^platform-tag = "[^"]*"$',
f'platform-tag = "{platform_tag}"',
section_text,
count=1,
flags=re.MULTILINE,
)
if count == 0:
updated_section = section_text.rstrip() + f'\nplatform-tag = "{platform_tag}"\n'
return pyproject_text[:section_index] + updated_section + tail
def _rewrite_project_name(pyproject_text: str, name: str) -> str:
updated, count = re.subn(
r'^name = "[^"]+"$',
f'name = "{name}"',
pyproject_text,
count=1,
flags=re.MULTILINE,
)
if count != 1:
raise RuntimeError("Could not rewrite project name in pyproject.toml")
return updated
def _rewrite_sdk_runtime_dependency(pyproject_text: str, runtime_version: str) -> str:
match = re.search(r"^dependencies = \[(.*?)\]$", pyproject_text, flags=re.MULTILINE)
if match is None:
@@ -119,7 +179,7 @@ def _rewrite_sdk_runtime_dependency(pyproject_text: str, runtime_version: str) -
raw_items = [item.strip() for item in match.group(1).split(",") if item.strip()]
raw_items = [item for item in raw_items if "codex-cli-bin" not in item]
raw_items.append(f'"codex-cli-bin=={runtime_version}"')
raw_items.append(f'"{RUNTIME_DISTRIBUTION_NAME}=={runtime_version}"')
replacement = "dependencies = [\n " + ",\n ".join(raw_items) + ",\n]"
return pyproject_text[: match.start()] + replacement + pyproject_text[match.end() :]
@@ -141,14 +201,21 @@ def stage_python_sdk_package(
def stage_python_runtime_package(
staging_dir: Path, runtime_version: str, binary_path: Path
staging_dir: Path,
codex_version: str,
binary_path: Path,
platform_tag: str | None = None,
) -> Path:
package_version = normalize_codex_version(codex_version)
_copy_package_tree(python_runtime_root(), staging_dir)
pyproject_path = staging_dir / "pyproject.toml"
pyproject_path.write_text(
_rewrite_project_version(pyproject_path.read_text(), runtime_version)
)
pyproject_text = pyproject_path.read_text()
pyproject_text = _rewrite_project_name(pyproject_text, RUNTIME_DISTRIBUTION_NAME)
pyproject_text = _rewrite_project_version(pyproject_text, package_version)
if platform_tag is not None:
pyproject_text = _rewrite_runtime_platform_tag(pyproject_text, platform_tag)
pyproject_path.write_text(pyproject_text)
out_bin = staged_runtime_bin_path(staging_dir)
out_bin.parent.mkdir(parents=True, exist_ok=True)
@@ -559,7 +626,7 @@ class PublicFieldSpec:
class CliOps:
generate_types: Callable[[], None]
stage_python_sdk_package: Callable[[Path, str, str], Path]
stage_python_runtime_package: Callable[[Path, str, Path], Path]
stage_python_runtime_package: Callable[[Path, str, Path, str | None], Path]
current_sdk_version: Callable[[], str]
@@ -928,7 +995,7 @@ def build_parser() -> argparse.ArgumentParser:
stage_sdk_parser.add_argument(
"--runtime-version",
required=True,
help="Pinned codex-cli-bin version for the staged SDK package",
help="Pinned openai-codex-cli-bin version for the staged SDK package",
)
stage_sdk_parser.add_argument(
"--sdk-version",
@@ -949,10 +1016,23 @@ def build_parser() -> argparse.ArgumentParser:
type=Path,
help="Path to the codex binary to package for this platform",
)
stage_runtime_parser.add_argument(
"--codex-version",
help=(
"Codex release version to write into the staged runtime package. "
"Accepts PEP 440 versions or release tags such as rust-v0.116.0-alpha.1."
),
)
stage_runtime_parser.add_argument(
"--runtime-version",
required=True,
help="Version to write into the staged runtime package",
help=argparse.SUPPRESS,
)
stage_runtime_parser.add_argument(
"--platform-tag",
help=(
"Optional wheel platform tag override, for example "
"macosx_11_0_arm64 or musllinux_1_1_x86_64."
),
)
return parser
@@ -970,6 +1050,26 @@ def default_cli_ops() -> CliOps:
)
def _resolve_runtime_version(args: argparse.Namespace) -> str:
versions = [
value
for value in (
getattr(args, "codex_version", None),
getattr(args, "runtime_version", None),
)
if value is not None
]
if not versions:
raise RuntimeError("Pass --codex-version to stage the Python runtime package")
normalized_versions = [normalize_codex_version(version) for version in versions]
if len(set(normalized_versions)) != 1:
raise RuntimeError(
"Runtime package versions must match; pass one --codex-version"
)
return normalized_versions[0]
def run_command(args: argparse.Namespace, ops: CliOps) -> None:
if args.command == "generate-types":
ops.generate_types()
@@ -981,10 +1081,12 @@ def run_command(args: argparse.Namespace, ops: CliOps) -> None:
args.runtime_version,
)
elif args.command == "stage-runtime":
runtime_version = _resolve_runtime_version(args)
ops.stage_python_runtime_package(
args.staging_dir,
args.runtime_version,
runtime_version,
args.runtime_binary.resolve(),
args.platform_tag,
)