sdk: launch packaged Codex runtimes (#23786)

## Why

The Python and TypeScript SDKs launch the native Codex runtime directly,
so they need to consume the same package artifact shape that release
jobs now produce. The runtime wheel should be built from the canonical
Codex package archive rather than reconstructing a parallel layout from
loose binaries.

## What Changed

- Stage `openai-codex-cli-bin` by extracting
`codex-package-<target>.tar.gz` into `src/codex_cli_bin` and validating
the expected package layout.
- Update release workflows to pass the generated package archive into
`stage-runtime` instead of the temporary package directory.
- Update Python runtime setup to download `codex-package-*.tar.gz`
release assets directly.
- Expose Python runtime helpers for the bundled package directory and
`codex-path`, and prepend that path when `openai_codex` launches the
installed runtime without duplicating Windows `Path`/`PATH` keys.
- Teach the TypeScript SDK to resolve package-layout optional
dependencies while keeping the existing npm fallback layout, and
preserve the existing Windows path variable casing when prepending
`codex-path`.

## Test Plan

- `python3 -m py_compile sdk/python/scripts/update_sdk_artifacts.py
sdk/python/_runtime_setup.py sdk/python/src/openai_codex/client.py
sdk/python-runtime/src/codex_cli_bin/__init__.py`
- `uv run --frozen --project sdk/python --extra dev ruff check
sdk/python/scripts/update_sdk_artifacts.py sdk/python/_runtime_setup.py
sdk/python/src/openai_codex/client.py
sdk/python/tests/test_artifact_workflow_and_binaries.py
sdk/python-runtime/src/codex_cli_bin/__init__.py`
- `uv run --frozen --project sdk/python --extra dev pytest
sdk/python/tests/test_artifact_workflow_and_binaries.py`
- `pnpm eslint src/exec.ts tests/exec.test.ts`
- `pnpm test --runInBand tests/exec.test.ts`
This commit is contained in:
Michael Bolin
2026-05-20 18:01:22 -07:00
committed by GitHub
parent 63a72e6b78
commit 0b4f86095c
10 changed files with 425 additions and 213 deletions

View File

@@ -1,5 +1,4 @@
#!/usr/bin/env python3
from __future__ import annotations
import argparse
import importlib
@@ -8,9 +7,9 @@ import json
import platform
import re
import shutil
import stat
import subprocess
import sys
import tarfile
import tempfile
import types
import typing
@@ -20,6 +19,8 @@ from typing import Any, Callable, Sequence, get_args, get_origin
SDK_DISTRIBUTION_NAME = "openai-codex"
RUNTIME_DISTRIBUTION_NAME = "openai-codex-cli-bin"
RUNTIME_PACKAGE_ROOT = Path("src") / "codex_cli_bin"
CODEX_PACKAGE_METADATA = "codex-package.json"
def repo_root() -> Path:
@@ -52,16 +53,8 @@ def runtime_binary_name() -> str:
return "codex.exe" if _is_windows() else "codex"
def staged_runtime_bin_path(root: Path) -> Path:
return root / "src" / "codex_cli_bin" / "bin" / runtime_binary_name()
def staged_runtime_resource_path(root: Path, resource: Path) -> Path:
"""Stage runtime helper binaries beside the main bundled Codex binary."""
# Runtime wheels include the whole bin/ directory, so helper executables
# should be staged beside the main Codex binary instead of changing the
# package template for each platform.
return root / "src" / "codex_cli_bin" / "bin" / resource.name
def staged_runtime_package_root(root: Path) -> Path:
return root / RUNTIME_PACKAGE_ROOT
def run(cmd: list[str], cwd: Path) -> None:
@@ -259,9 +252,8 @@ def stage_python_sdk_package(staging_dir: Path, codex_version: str) -> Path:
def stage_python_runtime_package(
staging_dir: Path,
codex_version: str,
binary_path: Path,
package_archive: Path,
platform_tag: str | None = None,
resource_binaries: Sequence[Path] = (),
) -> Path:
package_version = normalize_codex_version(codex_version)
_copy_package_tree(python_runtime_root(), staging_dir)
@@ -274,24 +266,39 @@ def stage_python_runtime_package(
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)
shutil.copy2(binary_path, out_bin)
if not _is_windows():
out_bin.chmod(out_bin.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
for resource_binary in resource_binaries:
# Some release targets need helper executables beside the main binary
# (for example Linux bwrap or Windows sandbox helpers). Keep this
# generic so release workflows own the platform-specific list.
out_resource = staged_runtime_resource_path(staging_dir, resource_binary)
shutil.copy2(resource_binary, out_resource)
if not _is_windows():
out_resource.chmod(
out_resource.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH
)
_extract_codex_package_archive(package_archive, staged_runtime_package_root(staging_dir))
return staging_dir
def _extract_codex_package_archive(package_archive: Path, runtime_package_root: Path) -> None:
if not package_archive.name.endswith(".tar.gz"):
raise RuntimeError(f"Expected a .tar.gz Codex package archive: {package_archive}")
runtime_package_root.mkdir(parents=True, exist_ok=True)
with tarfile.open(package_archive, "r:gz") as archive:
try:
archive.extractall(runtime_package_root, filter="data")
except TypeError:
archive.extractall(runtime_package_root)
_validate_codex_package_layout(runtime_package_root, package_archive)
def _validate_codex_package_layout(package_dir: Path, package_archive: Path) -> None:
missing_entries = []
if not (package_dir / CODEX_PACKAGE_METADATA).is_file():
missing_entries.append(CODEX_PACKAGE_METADATA)
for entry in ("bin", "codex-resources", "codex-path"):
if not (package_dir / entry).is_dir():
missing_entries.append(entry)
package_binary = package_dir / "bin" / runtime_binary_name()
if not package_binary.is_file():
missing_entries.append(str(Path("bin") / runtime_binary_name()))
if missing_entries:
missing = ", ".join(missing_entries)
raise RuntimeError(f"Missing Codex package layout entries in {package_archive}: {missing}")
def _flatten_string_enum_one_of(definition: dict[str, Any]) -> bool:
branches = definition.get("oneOf")
if not isinstance(branches, list) or not branches:
@@ -752,7 +759,7 @@ class PublicFieldSpec:
class CliOps:
generate_types: Callable[[], None]
stage_python_sdk_package: Callable[[Path, str], Path]
stage_python_runtime_package: Callable[[Path, str, Path, str | None, Sequence[Path]], Path]
stage_python_runtime_package: Callable[[Path, str, Path, str | None], Path]
current_sdk_version: Callable[[], str]
@@ -1218,9 +1225,9 @@ def build_parser() -> argparse.ArgumentParser:
help="Output directory for the staged runtime package",
)
stage_runtime_parser.add_argument(
"runtime_binary",
"package_archive",
type=Path,
help="Path to the codex binary to package for this platform",
help="Path to a Codex package .tar.gz archive for this platform.",
)
stage_runtime_parser.add_argument(
"--codex-version",
@@ -1240,13 +1247,6 @@ def build_parser() -> argparse.ArgumentParser:
"macosx_11_0_arm64 or musllinux_1_1_x86_64."
),
)
stage_runtime_parser.add_argument(
"--resource-binary",
action="append",
default=[],
type=Path,
help="Additional executable to package beside the codex runtime binary.",
)
return parser
@@ -1297,9 +1297,8 @@ def run_command(args: argparse.Namespace, ops: CliOps) -> None:
ops.stage_python_runtime_package(
args.staging_dir,
codex_version,
args.runtime_binary.resolve(),
args.package_archive.resolve(),
args.platform_tag,
tuple(path.resolve() for path in args.resource_binary),
)