diff --git a/sdk/python-runtime/README.md b/sdk/python-runtime/README.md index 0e7b26d5f..22c59ef15 100644 --- a/sdk/python-runtime/README.md +++ b/sdk/python-runtime/README.md @@ -5,5 +5,5 @@ Platform-specific runtime package consumed by the published `codex-app-server-sd This package is staged during release so the SDK can pin an exact Codex CLI version without checking platform binaries into the repo. -`codex-cli-bin` is intentionally wheel-only. Do not build or publish an sdist -for this package. +`openai-codex-cli-bin` is intentionally wheel-only. Do not build or publish an +sdist for this package. diff --git a/sdk/python-runtime/hatch_build.py b/sdk/python-runtime/hatch_build.py index 319e6973f..d49e3e106 100644 --- a/sdk/python-runtime/hatch_build.py +++ b/sdk/python-runtime/hatch_build.py @@ -1,15 +1,30 @@ from __future__ import annotations +import os + from hatchling.builders.hooks.plugin.interface import BuildHookInterface +def _platform_tag() -> str: + from packaging.tags import sys_tags + + return next(iter(sys_tags())).platform + + class RuntimeBuildHook(BuildHookInterface): def initialize(self, version: str, build_data: dict[str, object]) -> None: del version if self.target_name == "sdist": raise RuntimeError( - "codex-cli-bin is wheel-only; build and publish platform wheels only." + "openai-codex-cli-bin is wheel-only; build and publish platform wheels only." ) + platform_tag = self.config.get("platform-tag") or os.environ.get( + "CODEX_CLI_BIN_PLATFORM_TAG" + ) + if not isinstance(platform_tag, str) or not platform_tag: + platform_tag = _platform_tag() + build_data["pure_python"] = False - build_data["infer_tag"] = True + build_data["infer_tag"] = False + build_data["tag"] = f"py3-none-{platform_tag}" diff --git a/sdk/python-runtime/pyproject.toml b/sdk/python-runtime/pyproject.toml index 761dccbb9..281cb7d1a 100644 --- a/sdk/python-runtime/pyproject.toml +++ b/sdk/python-runtime/pyproject.toml @@ -3,7 +3,7 @@ requires = ["hatchling>=1.24.0"] build-backend = "hatchling.build" [project] -name = "codex-cli-bin" +name = "openai-codex-cli-bin" version = "0.0.0-dev" description = "Pinned Codex CLI runtime for the Python SDK" readme = "README.md" diff --git a/sdk/python-runtime/src/codex_cli_bin/__init__.py b/sdk/python-runtime/src/codex_cli_bin/__init__.py index 8059d3921..dbd9a6f66 100644 --- a/sdk/python-runtime/src/codex_cli_bin/__init__.py +++ b/sdk/python-runtime/src/codex_cli_bin/__init__.py @@ -3,7 +3,7 @@ from __future__ import annotations import os from pathlib import Path -PACKAGE_NAME = "codex-cli-bin" +PACKAGE_NAME = "openai-codex-cli-bin" def bundled_codex_path() -> Path: diff --git a/sdk/python/README.md b/sdk/python/README.md index 97068afe3..1331ebfe2 100644 --- a/sdk/python/README.md +++ b/sdk/python/README.md @@ -11,7 +11,7 @@ cd sdk/python python -m pip install -e . ``` -Published SDK builds pin an exact `codex-cli-bin` runtime dependency. For local +Published SDK builds pin an exact `openai-codex-cli-bin` runtime dependency. For local repo development, either pass `AppServerConfig(codex_bin=...)` to point at a local build explicitly, or use the repo examples/notebook bootstrap which installs the pinned runtime package automatically. @@ -53,7 +53,7 @@ python examples/01_quickstart_constructor/async.py The repo no longer checks `codex` binaries into `sdk/python`. -Published SDK builds are pinned to an exact `codex-cli-bin` package version, +Published SDK builds are pinned to an exact `openai-codex-cli-bin` package version, and that runtime package carries the platform-specific binary for the target wheel. @@ -73,7 +73,7 @@ python scripts/update_sdk_artifacts.py \ --runtime-version 1.2.3 python scripts/update_sdk_artifacts.py \ stage-runtime \ - /tmp/codex-python-release/codex-cli-bin \ + /tmp/codex-python-release/openai-codex-cli-bin \ /path/to/codex \ --runtime-version 1.2.3 ``` @@ -81,14 +81,14 @@ python scripts/update_sdk_artifacts.py \ This supports the CI release flow: - run `generate-types` before packaging -- stage `codex-app-server-sdk` once with an exact `codex-cli-bin==...` dependency -- stage `codex-cli-bin` on each supported platform runner with the same pinned runtime version -- build and publish `codex-cli-bin` as platform wheels only; do not publish an sdist +- stage `codex-app-server-sdk` once with an exact `openai-codex-cli-bin==...` dependency +- stage `openai-codex-cli-bin` on each supported platform runner with the same pinned runtime version +- build and publish `openai-codex-cli-bin` as platform wheels only; do not publish an sdist ## Compatibility and versioning - Package: `codex-app-server-sdk` -- Runtime package: `codex-cli-bin` +- Runtime package: `openai-codex-cli-bin` - Current SDK version in this repo: `0.2.0` - Python: `>=3.10` - Target protocol: Codex `app-server` JSON-RPC v2 diff --git a/sdk/python/_runtime_setup.py b/sdk/python/_runtime_setup.py index 5eb3999f4..6c4cf457a 100644 --- a/sdk/python/_runtime_setup.py +++ b/sdk/python/_runtime_setup.py @@ -15,7 +15,7 @@ import urllib.request import zipfile from pathlib import Path -PACKAGE_NAME = "codex-cli-bin" +PACKAGE_NAME = "openai-codex-cli-bin" PINNED_RUNTIME_VERSION = "0.116.0-alpha.1" REPO_SLUG = "openai/codex" @@ -105,7 +105,7 @@ def _installed_runtime_version(python_executable: str | Path) -> str | None: "try:\n" " from codex_cli_bin import bundled_codex_path\n" " bundled_codex_path()\n" - " print(json.dumps({'version': importlib.metadata.version('codex-cli-bin')}))\n" + f" print(json.dumps({{'version': importlib.metadata.version({PACKAGE_NAME!r})}}))\n" "except Exception:\n" " sys.exit(1)\n" ) diff --git a/sdk/python/docs/faq.md b/sdk/python/docs/faq.md index b2c9cf3b1..af688a3a1 100644 --- a/sdk/python/docs/faq.md +++ b/sdk/python/docs/faq.md @@ -54,13 +54,13 @@ This avoids duplicate ways to do the same operation and keeps behavior explicit. Common causes: -- published runtime package (`codex-cli-bin`) is not installed +- published runtime package (`openai-codex-cli-bin`) is not installed - local `codex_bin` override points to a missing file - local auth/session is missing - incompatible/old app-server Maintainers stage releases by building the SDK once and the runtime once per -platform with the same pinned runtime version. Publish `codex-cli-bin` as +platform with the same pinned runtime version. Publish `openai-codex-cli-bin` as platform wheels only; do not publish an sdist: ```bash @@ -72,7 +72,7 @@ python scripts/update_sdk_artifacts.py \ --runtime-version 1.2.3 python scripts/update_sdk_artifacts.py \ stage-runtime \ - /tmp/codex-python-release/codex-cli-bin \ + /tmp/codex-python-release/openai-codex-cli-bin \ /path/to/codex \ --runtime-version 1.2.3 ``` diff --git a/sdk/python/docs/getting-started.md b/sdk/python/docs/getting-started.md index 76034d72e..70a193a3d 100644 --- a/sdk/python/docs/getting-started.md +++ b/sdk/python/docs/getting-started.md @@ -16,7 +16,7 @@ python -m pip install -e . Requirements: - Python `>=3.10` -- installed `codex-cli-bin` runtime package, or an explicit `codex_bin` override +- installed `openai-codex-cli-bin` runtime package, or an explicit `codex_bin` override - local Codex auth/session configured ## 2) Run your first turn (sync) diff --git a/sdk/python/examples/README.md b/sdk/python/examples/README.md index 5edf2badb..ffdc86162 100644 --- a/sdk/python/examples/README.md +++ b/sdk/python/examples/README.md @@ -23,11 +23,11 @@ python -m pip install -e . When running examples from this repo checkout, the SDK source uses the local tree and does not bundle a runtime binary. The helper in `examples/_bootstrap.py` -uses the installed `codex-cli-bin` runtime package. +uses the installed `openai-codex-cli-bin` runtime package. -If the pinned `codex-cli-bin` runtime is not already installed, the bootstrap +If the pinned `openai-codex-cli-bin` runtime is not already installed, the bootstrap will download the matching GitHub release artifact, stage a temporary local -`codex-cli-bin` package, install it into your active interpreter, and clean up +`openai-codex-cli-bin` package, install it into your active interpreter, and clean up the temporary files afterward. Current pinned runtime version: `0.116.0-alpha.1` @@ -43,7 +43,7 @@ python examples//async.py The examples bootstrap local imports from `sdk/python/src` automatically, so no SDK wheel install is required. You only need the Python dependencies for your -active interpreter and an installed `codex-cli-bin` runtime package (either +active interpreter and an installed `openai-codex-cli-bin` runtime package (either already present or automatically provisioned by the bootstrap). ## Recommended first run diff --git a/sdk/python/scripts/update_sdk_artifacts.py b/sdk/python/scripts/update_sdk_artifacts.py index 6685fd099..42c1ec091 100755 --- a/sdk/python/scripts/update_sdk_artifacts.py +++ b/sdk/python/scripts/update_sdk_artifacts.py @@ -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, ) diff --git a/sdk/python/src/codex_app_server/client.py b/sdk/python/src/codex_app_server/client.py index 146d5186e..db7cf77cd 100644 --- a/sdk/python/src/codex_app_server/client.py +++ b/sdk/python/src/codex_app_server/client.py @@ -47,7 +47,7 @@ from .retry import retry_on_overload ModelT = TypeVar("ModelT", bound=BaseModel) ApprovalHandler = Callable[[str, JsonObject | None], JsonObject] -RUNTIME_PKG_NAME = "codex-cli-bin" +RUNTIME_PKG_NAME = "openai-codex-cli-bin" def _params_dict( diff --git a/sdk/python/tests/test_artifact_workflow_and_binaries.py b/sdk/python/tests/test_artifact_workflow_and_binaries.py index b19dc745a..03252154e 100644 --- a/sdk/python/tests/test_artifact_workflow_and_binaries.py +++ b/sdk/python/tests/test_artifact_workflow_and_binaries.py @@ -168,6 +168,19 @@ def test_examples_readme_matches_pinned_runtime_version() -> None: ) +def test_runtime_distribution_name_is_consistent() -> None: + script = _load_update_script_module() + runtime_setup = _load_runtime_setup_module() + from codex_app_server import client as client_module + + assert script.RUNTIME_DISTRIBUTION_NAME == "openai-codex-cli-bin" + assert runtime_setup.PACKAGE_NAME == "openai-codex-cli-bin" + assert client_module.RUNTIME_PKG_NAME == "openai-codex-cli-bin" + assert "importlib.metadata.version('codex-cli-bin')" not in ( + ROOT / "_runtime_setup.py" + ).read_text() + + def test_release_metadata_retries_without_invalid_auth(monkeypatch: pytest.MonkeyPatch) -> None: runtime_setup = _load_runtime_setup_module() authorizations: list[str | None] = [] @@ -222,19 +235,24 @@ def test_runtime_package_is_wheel_only_and_builds_platform_specific_wheels() -> ), None, ) - build_data_assignments = { - node.targets[0].slice.value: node.value.value - for node in initialize_fn.body - if isinstance(node, ast.Assign) - and len(node.targets) == 1 - and isinstance(node.targets[0], ast.Subscript) - and isinstance(node.targets[0].value, ast.Name) - and node.targets[0].value.id == "build_data" - and isinstance(node.targets[0].slice, ast.Constant) - and isinstance(node.targets[0].slice.value, str) - and isinstance(node.value, ast.Constant) - } + build_data_assignments = {} + for node in initialize_fn.body: + if ( + not isinstance(node, ast.Assign) + or len(node.targets) != 1 + or not isinstance(node.targets[0], ast.Subscript) + or not isinstance(node.targets[0].value, ast.Name) + or node.targets[0].value.id != "build_data" + or not isinstance(node.targets[0].slice, ast.Constant) + or not isinstance(node.targets[0].slice.value, str) + ): + continue + if isinstance(node.value, ast.Constant): + build_data_assignments[node.targets[0].slice.value] = node.value.value + elif isinstance(node.value, ast.JoinedStr): + build_data_assignments[node.targets[0].slice.value] = "joined-string" + assert pyproject["project"]["name"] == "openai-codex-cli-bin" assert pyproject["tool"]["hatch"]["build"]["targets"]["wheel"] == { "packages": ["src/codex_cli_bin"], "include": ["src/codex_cli_bin/bin/**"], @@ -244,7 +262,11 @@ def test_runtime_package_is_wheel_only_and_builds_platform_specific_wheels() -> "hooks": {"custom": {}}, } assert sdist_guard is not None - assert build_data_assignments == {"pure_python": False, "infer_tag": True} + assert build_data_assignments == { + "pure_python": False, + "infer_tag": False, + "tag": "joined-string", + } def test_stage_runtime_release_copies_binary_and_sets_version(tmp_path: Path) -> None: @@ -260,9 +282,19 @@ def test_stage_runtime_release_copies_binary_and_sets_version(tmp_path: Path) -> assert staged == tmp_path / "runtime-stage" assert script.staged_runtime_bin_path(staged).read_text() == "fake codex\n" + assert 'name = "openai-codex-cli-bin"' in (staged / "pyproject.toml").read_text() assert 'version = "1.2.3"' in (staged / "pyproject.toml").read_text() +def test_normalize_codex_version_accepts_release_tags_and_pep440_versions() -> None: + script = _load_update_script_module() + + assert script.normalize_codex_version("rust-v0.116.0-alpha.1") == "0.116.0a1" + assert script.normalize_codex_version("v0.116.0-beta.2") == "0.116.0b2" + assert script.normalize_codex_version("0.116.0rc3") == "0.116.0rc3" + assert script.normalize_codex_version("0.116.0") == "0.116.0" + + def test_stage_runtime_release_replaces_existing_staging_dir(tmp_path: Path) -> None: script = _load_update_script_module() staging_dir = tmp_path / "runtime-stage" @@ -284,13 +316,30 @@ def test_stage_runtime_release_replaces_existing_staging_dir(tmp_path: Path) -> assert script.staged_runtime_bin_path(staged).read_text() == "fake codex\n" +def test_stage_runtime_release_can_pin_wheel_platform_tag(tmp_path: Path) -> None: + script = _load_update_script_module() + fake_binary = tmp_path / script.runtime_binary_name() + fake_binary.write_text("fake codex\n") + + staged = script.stage_python_runtime_package( + tmp_path / "runtime-stage", + "0.116.0a1", + fake_binary, + platform_tag="musllinux_1_1_x86_64", + ) + + pyproject = (staged / "pyproject.toml").read_text() + assert 'platform-tag = "musllinux_1_1_x86_64"' in pyproject + + def test_stage_sdk_release_injects_exact_runtime_pin(tmp_path: Path) -> None: script = _load_update_script_module() staged = script.stage_python_sdk_package(tmp_path / "sdk-stage", "0.2.1", "1.2.3") pyproject = (staged / "pyproject.toml").read_text() assert 'version = "0.2.1"' in pyproject - assert '"codex-cli-bin==1.2.3"' in pyproject + assert '"openai-codex-cli-bin==1.2.3"' in pyproject + assert '"codex-cli-bin==1.2.3"' not in pyproject assert not any((staged / "src" / "codex_app_server").glob("bin/**")) @@ -329,7 +378,10 @@ def test_stage_sdk_runs_type_generation_before_staging(tmp_path: Path) -> None: return tmp_path / "sdk-stage" def fake_stage_runtime_package( - _staging_dir: Path, _runtime_version: str, _runtime_binary: Path + _staging_dir: Path, + _runtime_version: str, + _runtime_binary: Path, + _platform_tag: str | None, ) -> Path: raise AssertionError("runtime staging should not run for stage-sdk") @@ -358,8 +410,10 @@ def test_stage_runtime_stages_binary_without_type_generation(tmp_path: Path) -> "stage-runtime", str(tmp_path / "runtime-stage"), str(fake_binary), - "--runtime-version", - "1.2.3", + "--codex-version", + "rust-v0.116.0-alpha.1", + "--platform-tag", + "musllinux_1_1_x86_64", ] ) @@ -372,9 +426,12 @@ def test_stage_runtime_stages_binary_without_type_generation(tmp_path: Path) -> raise AssertionError("sdk staging should not run for stage-runtime") def fake_stage_runtime_package( - _staging_dir: Path, _runtime_version: str, _runtime_binary: Path + _staging_dir: Path, + codex_version: str, + _runtime_binary: Path, + platform_tag: str | None, ) -> Path: - calls.append("stage_runtime") + calls.append(f"stage_runtime:{codex_version}:{platform_tag}") return tmp_path / "runtime-stage" def fake_current_sdk_version() -> str: @@ -389,7 +446,7 @@ def test_stage_runtime_stages_binary_without_type_generation(tmp_path: Path) -> script.run_command(args, ops) - assert calls == ["stage_runtime"] + assert calls == ["stage_runtime:0.116.0a1:musllinux_1_1_x86_64"] def test_default_runtime_is_resolved_from_installed_runtime_package(