From 8956a928a13778f2fda835cb58853d9963257717 Mon Sep 17 00:00:00 2001 From: Ahmed Ibrahim Date: Fri, 8 May 2026 22:00:44 +0300 Subject: [PATCH] Support resource binaries in Python runtime staging (#21787) ## Why Some Codex runtime distributions need helper executables beside the main bundled binary. Linux sandbox fallback needs a packaged `bwrap` when no suitable system `bwrap` is available, and Windows sandbox/elevation needs helper executables discoverable beside `codex.exe`. The checked-in `openai-codex-cli-bin` template already packages everything under `codex_cli_bin/bin/**`, but the staging script only copied the main Codex binary into that directory. This PR adds the generic staging primitive needed by release workflows to build complete platform runtime wheels without baking platform-specific helper names into the package template. ## What changed - Added repeatable `stage-runtime --resource-binary` support so release workflows can copy extra executables beside the bundled Codex binary. - Kept resource selection in workflow code, where the platform target is known. - Added tests that verify resource binaries are copied into the staged runtime package, that the wheel include config covers them, and that the CLI forwards repeated `--resource-binary` values. ## Testing - `uv run ruff check scripts/update_sdk_artifacts.py tests/test_artifact_workflow_and_binaries.py` - `uv run --extra dev pytest tests/test_artifact_workflow_and_binaries.py::test_stage_runtime_release_copies_resource_binaries tests/test_artifact_workflow_and_binaries.py::test_runtime_resource_binaries_are_included_by_wheel_config tests/test_artifact_workflow_and_binaries.py::test_stage_runtime_stages_binary_without_type_generation` Full `tests/test_artifact_workflow_and_binaries.py` still has unrelated schema-normalization drift in the local checkout. --------- Co-authored-by: Codex --- sdk/python/scripts/update_sdk_artifacts.py | 30 +++++++- .../test_artifact_workflow_and_binaries.py | 76 ++++++++++++++++++- 2 files changed, 103 insertions(+), 3 deletions(-) diff --git a/sdk/python/scripts/update_sdk_artifacts.py b/sdk/python/scripts/update_sdk_artifacts.py index 0d0e739c78..be9a115914 100755 --- a/sdk/python/scripts/update_sdk_artifacts.py +++ b/sdk/python/scripts/update_sdk_artifacts.py @@ -60,6 +60,13 @@ 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: + # 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 run(cmd: list[str], cwd: Path) -> None: subprocess.run(cmd, cwd=str(cwd), check=True) @@ -211,6 +218,7 @@ def stage_python_runtime_package( codex_version: str, binary_path: 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) @@ -230,6 +238,16 @@ def stage_python_runtime_package( 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 + ) return staging_dir @@ -632,7 +650,9 @@ 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], Path] + stage_python_runtime_package: Callable[ + [Path, str, Path, str | None, Sequence[Path]], Path + ] current_sdk_version: Callable[[], str] @@ -1047,6 +1067,13 @@ 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 @@ -1101,6 +1128,7 @@ def run_command(args: argparse.Namespace, ops: CliOps) -> None: codex_version, args.runtime_binary.resolve(), args.platform_tag, + tuple(path.resolve() for path in args.resource_binary), ) diff --git a/sdk/python/tests/test_artifact_workflow_and_binaries.py b/sdk/python/tests/test_artifact_workflow_and_binaries.py index a30582517a..e9b4e6a8bb 100644 --- a/sdk/python/tests/test_artifact_workflow_and_binaries.py +++ b/sdk/python/tests/test_artifact_workflow_and_binaries.py @@ -8,6 +8,7 @@ import sys import tomllib import urllib.error from pathlib import Path +from typing import Sequence import pytest @@ -350,6 +351,62 @@ def test_stage_runtime_release_can_pin_wheel_platform_tag(tmp_path: Path) -> Non assert 'platform-tag = "musllinux_1_1_x86_64"' in pyproject +def test_stage_runtime_release_copies_resource_binaries(tmp_path: Path) -> None: + script = _load_update_script_module() + fake_binary = tmp_path / script.runtime_binary_name() + helper = tmp_path / "helper" + fallback = tmp_path / "fallback-helper" + fake_binary.write_text("fake codex\n") + helper.write_text("fake helper\n") + fallback.write_text("fake fallback\n") + + staged = script.stage_python_runtime_package( + tmp_path / "runtime-stage", + "1.2.3", + fake_binary, + resource_binaries=(helper, fallback), + ) + + assert { + path.relative_to( + staged / "src" / "codex_cli_bin" / "bin" + ).as_posix(): path.read_text() + for path in (staged / "src" / "codex_cli_bin" / "bin").iterdir() + } == { + script.runtime_binary_name(): "fake codex\n", + "fallback-helper": "fake fallback\n", + "helper": "fake helper\n", + } + + +def test_runtime_resource_binaries_are_included_by_wheel_config( + tmp_path: Path, +) -> None: + script = _load_update_script_module() + fake_binary = tmp_path / script.runtime_binary_name() + helper = tmp_path / "helper" + fake_binary.write_text("fake codex\n") + helper.write_text("fake helper\n") + + staged = script.stage_python_runtime_package( + tmp_path / "runtime-stage", + "1.2.3", + fake_binary, + resource_binaries=(helper,), + ) + + pyproject = tomllib.loads((staged / "pyproject.toml").read_text()) + assert { + "include": pyproject["tool"]["hatch"]["build"]["targets"]["wheel"]["include"], + "helper": ( + staged / "src" / "codex_cli_bin" / "bin" / "helper" + ).read_text(), + } == { + "include": ["src/codex_cli_bin/bin/**"], + "helper": "fake helper\n", + } + + def test_stage_sdk_release_injects_exact_runtime_pin(tmp_path: Path) -> None: script = _load_update_script_module() staged = script.stage_python_sdk_package( @@ -436,6 +493,7 @@ def test_stage_sdk_runs_type_generation_before_staging(tmp_path: Path) -> None: _runtime_version: str, _runtime_binary: Path, _platform_tag: str | None, + _resource_binaries: Sequence[Path], ) -> Path: raise AssertionError("runtime staging should not run for stage-sdk") @@ -476,7 +534,11 @@ def test_stage_sdk_rejects_mismatched_legacy_versions(tmp_path: Path) -> None: def test_stage_runtime_stages_binary_without_type_generation(tmp_path: Path) -> None: script = _load_update_script_module() fake_binary = tmp_path / script.runtime_binary_name() + helper = tmp_path / "helper" + fallback = tmp_path / "fallback-helper" fake_binary.write_text("fake codex\n") + helper.write_text("fake helper\n") + fallback.write_text("fake fallback\n") calls: list[str] = [] args = script.parse_args( [ @@ -487,6 +549,10 @@ def test_stage_runtime_stages_binary_without_type_generation(tmp_path: Path) -> "rust-v0.116.0-alpha.1", "--platform-tag", "musllinux_1_1_x86_64", + "--resource-binary", + str(helper), + "--resource-binary", + str(fallback), ] ) @@ -501,8 +567,12 @@ def test_stage_runtime_stages_binary_without_type_generation(tmp_path: Path) -> codex_version: str, _runtime_binary: Path, platform_tag: str | None, + resource_binaries: Sequence[Path], ) -> Path: - calls.append(f"stage_runtime:{codex_version}:{platform_tag}") + calls.append( + f"stage_runtime:{codex_version}:{platform_tag}:" + f"{','.join(path.name for path in resource_binaries)}" + ) return tmp_path / "runtime-stage" def fake_current_sdk_version() -> str: @@ -517,7 +587,9 @@ def test_stage_runtime_stages_binary_without_type_generation(tmp_path: Path) -> script.run_command(args, ops) - assert calls == ["stage_runtime:0.116.0a1:musllinux_1_1_x86_64"] + assert calls == [ + "stage_runtime:0.116.0a1:musllinux_1_1_x86_64:helper,fallback-helper" + ] def test_default_runtime_is_resolved_from_installed_runtime_package(