sdk: launch packaged Codex runtimes

## Why

The Python and TypeScript SDKs launch the native Codex runtime directly, so they need to understand the same package layout that the release artifacts now use. Without this, SDK runtime wheels would keep carrying only a loose `bin/codex` plus ad hoc helper binaries, the TypeScript SDK would keep resolving the legacy optional-dependency layout, and bundled `codex-path` tools such as `rg` would not be prepended when the SDK starts Codex.

## What Changed

- Stage `openai-codex-cli-bin` from a Codex package directory, including `codex-package.json`, `bin/`, `codex-resources/`, and `codex-path/`.
- Make `stage-runtime` package-dir-only; legacy loose-binary wrapping now happens in `_runtime_setup.py` before calling the staging script.
- 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 legacy layout fallback, and preserve the existing Windows path variable casing when prepending `codex-path`.
- Update the Rust release workflows to build Python runtime wheels from the generated Codex package directories instead of loose binaries.

## 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 16:54:50 -07:00
parent 896ee672cc
commit 3e77859d0a
10 changed files with 479 additions and 167 deletions

View File

@@ -65,11 +65,15 @@ def ensure_runtime_package_installed(
with tempfile.TemporaryDirectory(prefix="codex-python-runtime-") as temp_root_str:
temp_root = Path(temp_root_str)
archive_path = _download_release_archive(requested_version, temp_root)
runtime_binary = _extract_runtime_binary(archive_path, temp_root)
runtime_package_dir = _extract_runtime_package_dir(
archive_path,
temp_root,
requested_version,
)
staged_runtime_dir = _stage_runtime_package(
sdk_python_dir,
requested_version,
runtime_binary,
runtime_package_dir,
temp_root / "runtime-stage",
)
_install_runtime_package(python_executable, staged_runtime_dir, install_target)
@@ -260,7 +264,7 @@ def _download_release_archive(version: str, temp_root: Path) -> Path:
return archive_path
def _extract_runtime_binary(archive_path: Path, temp_root: Path) -> Path:
def _extract_runtime_package_dir(archive_path: Path, temp_root: Path, version: str) -> Path:
extract_dir = temp_root / "extracted"
extract_dir.mkdir(parents=True, exist_ok=True)
if archive_path.name.endswith(".tar.gz"):
@@ -275,6 +279,13 @@ def _extract_runtime_binary(archive_path: Path, temp_root: Path) -> Path:
else:
raise RuntimeSetupError(f"Unsupported release archive format: {archive_path.name}")
metadata_path = next(
(path for path in extract_dir.rglob("codex-package.json") if path.is_file()),
None,
)
if metadata_path is not None:
return metadata_path.parent
binary_name = runtime_binary_name()
archive_stem = archive_path.name.removesuffix(".tar.gz").removesuffix(".zip")
candidates = [
@@ -289,20 +300,45 @@ def _extract_runtime_binary(archive_path: Path, temp_root: Path) -> Path:
raise RuntimeSetupError(
f"Failed to find {binary_name} in extracted runtime archive {archive_path.name}."
)
return candidates[0]
return _package_legacy_runtime_binary(candidates[0], temp_root, version)
def _package_legacy_runtime_binary(runtime_binary: Path, temp_root: Path, version: str) -> Path:
package_dir = temp_root / "runtime-package"
bin_dir = package_dir / "bin"
resources_dir = package_dir / "codex-resources"
path_dir = package_dir / "codex-path"
bin_dir.mkdir(parents=True)
resources_dir.mkdir()
path_dir.mkdir()
shutil.copy2(runtime_binary, bin_dir / runtime_binary_name())
(package_dir / "codex-package.json").write_text(
json.dumps(
{
"layoutVersion": 1,
"version": version,
"entrypoint": f"bin/{runtime_binary_name()}",
"resourcesDir": "codex-resources",
"pathDir": "codex-path",
},
indent=2,
)
+ "\n"
)
return package_dir
def _stage_runtime_package(
sdk_python_dir: Path,
runtime_version: str,
runtime_binary: Path,
runtime_package_dir: Path,
staging_dir: Path,
) -> Path:
script_module = _load_update_script_module(sdk_python_dir)
return script_module.stage_python_runtime_package( # type: ignore[no-any-return]
staging_dir,
runtime_version,
runtime_binary.resolve(),
runtime_package_dir.resolve(),
)