Compare commits

...

1 Commits

Author SHA1 Message Date
Shaqayeq
f412b39118 python-runtime: prepare openai-codex-cli-bin package 2026-04-12 23:23:33 -07:00
8 changed files with 436 additions and 80 deletions

View File

@@ -1,9 +1,17 @@
# Codex CLI Runtime for Python SDK
Platform-specific runtime package consumed by the published `codex-app-server-sdk`.
Platform-specific runtime package consumed by the published `openai-codex` SDK.
This package is staged during release so the SDK can pin an exact Codex CLI
version without checking platform binaries into the repo.
version without checking platform binaries into the repo. The distribution name
is `openai-codex-cli-bin`, while the import module remains `codex_cli_bin`.
`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.
Expected wheel contents:
- macOS/Linux: `codex_cli_bin/bin/codex`
- Windows: `codex_cli_bin/bin/codex.exe`,
`codex_cli_bin/bin/codex-command-runner.exe`, and
`codex_cli_bin/bin/codex-windows-sandbox-setup.exe`

View File

@@ -1,15 +1,34 @@
from __future__ import annotations
import os
from hatchling.builders.hooks.plugin.interface import BuildHookInterface
PLATFORM_TAG_BY_TARGET = {
"aarch64-apple-darwin": "macosx_11_0_arm64",
"x86_64-apple-darwin": "macosx_10_12_x86_64",
"aarch64-unknown-linux-musl": "musllinux_1_2_aarch64",
"x86_64-unknown-linux-musl": "musllinux_1_2_x86_64",
"aarch64-pc-windows-msvc": "win_arm64",
"x86_64-pc-windows-msvc": "win_amd64",
}
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."
)
build_data["pure_python"] = False
build_data["infer_tag"] = True
target = os.environ.get("CODEX_PYTHON_RUNTIME_TARGET")
if target is None:
build_data["infer_tag"] = True
return
platform_tag = PLATFORM_TAG_BY_TARGET.get(target)
if platform_tag is None:
raise RuntimeError(f"Unsupported Codex Python runtime target: {target}")
build_data["tag"] = f"py3-none-{platform_tag}"

View File

@@ -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"

View File

@@ -3,12 +3,25 @@ from __future__ import annotations
import os
from pathlib import Path
PACKAGE_NAME = "codex-cli-bin"
PACKAGE_NAME = "openai-codex-cli-bin"
def bundled_bin_dir() -> Path:
return Path(__file__).resolve().parent / "bin"
def bundled_runtime_files() -> tuple[Path, ...]:
names = (
("codex.exe", "codex-command-runner.exe", "codex-windows-sandbox-setup.exe")
if os.name == "nt"
else ("codex",)
)
return tuple(bundled_bin_dir() / name for name in names)
def bundled_codex_path() -> Path:
exe = "codex.exe" if os.name == "nt" else "codex"
path = Path(__file__).resolve().parent / "bin" / exe
path = bundled_bin_dir() / exe
if not path.is_file():
raise FileNotFoundError(
f"{PACKAGE_NAME} is installed but missing its packaged codex binary at {path}"
@@ -16,4 +29,9 @@ def bundled_codex_path() -> Path:
return path
__all__ = ["PACKAGE_NAME", "bundled_codex_path"]
__all__ = [
"PACKAGE_NAME",
"bundled_bin_dir",
"bundled_codex_path",
"bundled_runtime_files",
]

View File

@@ -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"
@@ -39,17 +39,20 @@ def ensure_runtime_package_installed(
installed_version = _installed_runtime_version(python_executable)
normalized_requested = _normalized_package_version(requested_version)
if installed_version is not None and _normalized_package_version(installed_version) == normalized_requested:
if (
installed_version is not None
and _normalized_package_version(installed_version) == normalized_requested
):
return requested_version
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_bundle_dir = _extract_runtime_bundle(archive_path, temp_root)
staged_runtime_dir = _stage_runtime_package(
sdk_python_dir,
requested_version,
runtime_binary,
runtime_bundle_dir,
temp_root / "runtime-stage",
)
_install_runtime_package(python_executable, staged_runtime_dir, install_target)
@@ -61,7 +64,10 @@ def ensure_runtime_package_installed(
importlib.invalidate_caches()
installed_version = _installed_runtime_version(python_executable)
if installed_version is None or _normalized_package_version(installed_version) != normalized_requested:
if (
installed_version is None
or _normalized_package_version(installed_version) != normalized_requested
):
raise RuntimeSetupError(
f"Expected {PACKAGE_NAME} {requested_version} in {python_executable}, "
f"but found {installed_version!r} after installation."
@@ -105,7 +111,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"
)
@@ -172,7 +178,9 @@ def _download_release_archive(version: str, temp_root: Path) -> Path:
metadata = _release_metadata(version)
assets = metadata.get("assets")
if not isinstance(assets, list):
raise RuntimeSetupError(f"Release rust-v{version} returned malformed assets metadata.")
raise RuntimeSetupError(
f"Release rust-v{version} returned malformed assets metadata."
)
asset = next(
(
item
@@ -198,7 +206,10 @@ def _download_release_archive(version: str, temp_root: Path) -> Path:
headers=_github_api_headers("application/octet-stream"),
)
try:
with urllib.request.urlopen(request) as response, archive_path.open("wb") as fh:
with (
urllib.request.urlopen(request) as response,
archive_path.open("wb") as fh,
):
shutil.copyfileobj(response, fh)
return archive_path
except urllib.error.HTTPError:
@@ -236,7 +247,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_bundle(archive_path: Path, temp_root: Path) -> Path:
extract_dir = temp_root / "extracted"
extract_dir.mkdir(parents=True, exist_ok=True)
if archive_path.name.endswith(".tar.gz"):
@@ -249,38 +260,24 @@ def _extract_runtime_binary(archive_path: Path, temp_root: Path) -> Path:
with zipfile.ZipFile(archive_path) as zip_file:
zip_file.extractall(extract_dir)
else:
raise RuntimeSetupError(f"Unsupported release archive format: {archive_path.name}")
binary_name = runtime_binary_name()
archive_stem = archive_path.name.removesuffix(".tar.gz").removesuffix(".zip")
candidates = [
path
for path in extract_dir.rglob("*")
if path.is_file()
and (
path.name == binary_name
or path.name == archive_stem
or path.name.startswith("codex-")
)
]
if not candidates:
raise RuntimeSetupError(
f"Failed to find {binary_name} in extracted runtime archive {archive_path.name}."
f"Unsupported release archive format: {archive_path.name}"
)
return candidates[0]
return extract_dir
def _stage_runtime_package(
sdk_python_dir: Path,
runtime_version: str,
runtime_binary: Path,
runtime_bundle_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_bundle_dir.resolve(),
)

View File

@@ -17,6 +17,9 @@ from dataclasses import dataclass
from pathlib import Path
from typing import Any, Callable, Sequence, get_args, get_origin
SDK_PKG_NAME = "openai-codex"
RUNTIME_PKG_NAME = "openai-codex-cli-bin"
def repo_root() -> Path:
return Path(__file__).resolve().parents[3]
@@ -45,16 +48,30 @@ def schema_root_dir() -> Path:
return repo_root() / "codex-rs" / "app-server-protocol" / "schema" / "json"
def _is_windows() -> bool:
return platform.system().lower().startswith("win")
def _is_windows(system_name: str | None = None) -> bool:
return (system_name or platform.system()).lower().startswith("win")
def runtime_binary_name() -> str:
return "codex.exe" if _is_windows() else "codex"
def runtime_binary_name(system_name: str | None = None) -> str:
return "codex.exe" if _is_windows(system_name) else "codex"
def runtime_file_names(system_name: str | None = None) -> tuple[str, ...]:
if _is_windows(system_name):
return (
"codex.exe",
"codex-command-runner.exe",
"codex-windows-sandbox-setup.exe",
)
return ("codex",)
def staged_runtime_bin_dir(root: Path) -> Path:
return root / "src" / "codex_cli_bin" / "bin"
def staged_runtime_bin_path(root: Path) -> Path:
return root / "src" / "codex_cli_bin" / "bin" / runtime_binary_name()
return staged_runtime_bin_dir(root) / runtime_binary_name()
def run(cmd: list[str], cwd: Path) -> None:
@@ -110,6 +127,39 @@ def _rewrite_project_version(pyproject_text: str, version: str) -> str:
return updated
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 normalize_python_package_version(version: str) -> str:
stripped = version.strip()
if re.fullmatch(r"\d+\.\d+\.\d+(?:a\d+|b\d+|\.dev\d+)?", stripped):
return stripped
prerelease_match = re.fullmatch(
r"(\d+\.\d+\.\d+)-(alpha|beta)\.(\d+)",
stripped,
)
if prerelease_match is not None:
base, prerelease, number = prerelease_match.groups()
marker = "a" if prerelease == "alpha" else "b"
return f"{base}{marker}{number}"
raise RuntimeError(
"Unsupported Python package version. Expected x.y.z, x.y.z-alpha.n, "
f"x.y.z-beta.n, or an already-normalized PEP 440 version; got {version!r}."
)
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:
@@ -118,15 +168,46 @@ 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 = [
item
for item in raw_items
if "codex-cli-bin" not in item and RUNTIME_PKG_NAME not in item
]
raw_items.append(f'"{RUNTIME_PKG_NAME}=={runtime_version}"')
replacement = "dependencies = [\n " + ",\n ".join(raw_items) + ",\n]"
return pyproject_text[: match.start()] + replacement + pyproject_text[match.end() :]
def _rewrite_sdk_init_version(init_text: str, sdk_version: str) -> str:
updated, count = re.subn(
r'^__version__ = "[^"]+"$',
f'__version__ = "{sdk_version}"',
init_text,
count=1,
flags=re.MULTILINE,
)
if count != 1:
raise RuntimeError("Could not rewrite SDK __version__")
return updated
def _rewrite_sdk_client_version(client_text: str, sdk_version: str) -> str:
updated, count = re.subn(
r'client_version: str = "[^"]+"',
f'client_version: str = "{sdk_version}"',
client_text,
count=1,
)
if count != 1:
raise RuntimeError("Could not rewrite AppServerConfig.client_version")
return updated
def stage_python_sdk_package(
staging_dir: Path, sdk_version: str, runtime_version: str
) -> Path:
sdk_version = normalize_python_package_version(sdk_version)
runtime_version = normalize_python_package_version(runtime_version)
_copy_package_tree(sdk_root(), staging_dir)
sdk_bin_dir = staging_dir / "src" / "codex_app_server" / "bin"
if sdk_bin_dir.exists():
@@ -134,32 +215,88 @@ def stage_python_sdk_package(
pyproject_path = staging_dir / "pyproject.toml"
pyproject_text = pyproject_path.read_text()
pyproject_text = _rewrite_project_name(pyproject_text, SDK_PKG_NAME)
pyproject_text = _rewrite_project_version(pyproject_text, sdk_version)
pyproject_text = _rewrite_sdk_runtime_dependency(pyproject_text, runtime_version)
pyproject_path.write_text(pyproject_text)
init_path = staging_dir / "src" / "codex_app_server" / "__init__.py"
init_path.write_text(_rewrite_sdk_init_version(init_path.read_text(), sdk_version))
client_path = staging_dir / "src" / "codex_app_server" / "client.py"
client_path.write_text(
_rewrite_sdk_client_version(client_path.read_text(), sdk_version)
)
return staging_dir
def stage_python_runtime_package(
staging_dir: Path, runtime_version: str, binary_path: Path
staging_dir: Path, runtime_version: str, runtime_bundle_dir: Path
) -> Path:
runtime_version = normalize_python_package_version(runtime_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 = _rewrite_project_name(pyproject_path.read_text(), RUNTIME_PKG_NAME)
pyproject_text = _rewrite_project_version(pyproject_text, runtime_version)
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
)
out_bin_dir = staged_runtime_bin_dir(staging_dir)
out_bin_dir.mkdir(parents=True, exist_ok=True)
for runtime_file_name in runtime_file_names():
source = _find_runtime_bundle_file(runtime_bundle_dir, runtime_file_name)
out_path = out_bin_dir / runtime_file_name
shutil.copy2(source, out_path)
if not _is_windows():
out_path.chmod(
out_path.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH
)
return staging_dir
def _find_runtime_bundle_file(runtime_bundle_dir: Path, destination_name: str) -> Path:
if not runtime_bundle_dir.is_dir():
raise RuntimeError(f"Runtime bundle directory not found: {runtime_bundle_dir}")
exact = runtime_bundle_dir / destination_name
if exact.is_file():
return exact
patterns = {
"codex": re.compile(r"^codex-(?!responses-api-proxy)[^.]+$"),
"codex.exe": re.compile(
r"^codex-(?!command-runner|windows-sandbox-setup|responses-api-proxy).+\.exe$"
),
"codex-command-runner.exe": re.compile(r"^codex-command-runner-.+\.exe$"),
"codex-windows-sandbox-setup.exe": re.compile(
r"^codex-windows-sandbox-setup-.+\.exe$"
),
}
pattern = patterns.get(destination_name)
candidates = (
[]
if pattern is None
else sorted(
path
for path in runtime_bundle_dir.iterdir()
if path.is_file() and pattern.fullmatch(path.name)
)
)
if len(candidates) == 1:
return candidates[0]
if len(candidates) > 1:
candidate_names = ", ".join(path.name for path in candidates)
raise RuntimeError(
f"Runtime bundle has multiple candidates for {destination_name}: "
f"{candidate_names}"
)
raise RuntimeError(
f"Runtime bundle {runtime_bundle_dir} is missing required file "
f"{destination_name}"
)
def _flatten_string_enum_one_of(definition: dict[str, Any]) -> bool:
branches = definition.get("oneOf")
if not isinstance(branches, list) or not branches:
@@ -928,7 +1065,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=f"Pinned {RUNTIME_PKG_NAME} version for the staged SDK package",
)
stage_sdk_parser.add_argument(
"--sdk-version",
@@ -945,9 +1082,9 @@ def build_parser() -> argparse.ArgumentParser:
help="Output directory for the staged runtime package",
)
stage_runtime_parser.add_argument(
"runtime_binary",
"runtime_bundle_dir",
type=Path,
help="Path to the codex binary to package for this platform",
help="Directory containing the Codex runtime files to package for this platform",
)
stage_runtime_parser.add_argument(
"--runtime-version",
@@ -984,7 +1121,7 @@ def run_command(args: argparse.Namespace, ops: CliOps) -> None:
ops.stage_python_runtime_package(
args.staging_dir,
args.runtime_version,
args.runtime_binary.resolve(),
args.runtime_bundle_dir.resolve(),
)

View File

@@ -47,7 +47,8 @@ from .retry import retry_on_overload
ModelT = TypeVar("ModelT", bound=BaseModel)
ApprovalHandler = Callable[[str, JsonObject | None], JsonObject]
RUNTIME_PKG_NAME = "codex-cli-bin"
SDK_PKG_NAME = "openai-codex"
RUNTIME_PKG_NAME = "openai-codex-cli-bin"
def _params_dict(

View File

@@ -29,7 +29,22 @@ def _load_runtime_setup_module():
runtime_setup_path = ROOT / "_runtime_setup.py"
spec = importlib.util.spec_from_file_location("_runtime_setup", runtime_setup_path)
if spec is None or spec.loader is None:
raise AssertionError(f"Failed to load runtime setup module: {runtime_setup_path}")
raise AssertionError(
f"Failed to load runtime setup module: {runtime_setup_path}"
)
module = importlib.util.module_from_spec(spec)
sys.modules[spec.name] = module
spec.loader.exec_module(module)
return module
def _load_runtime_package_module(package_root: Path):
runtime_init = package_root / "src" / "codex_cli_bin" / "__init__.py"
spec = importlib.util.spec_from_file_location(
"codex_cli_bin_under_test", runtime_init
)
if spec is None or spec.loader is None:
raise AssertionError(f"Failed to load runtime package module: {runtime_init}")
module = importlib.util.module_from_spec(spec)
sys.modules[spec.name] = module
spec.loader.exec_module(module)
@@ -168,7 +183,9 @@ def test_examples_readme_matches_pinned_runtime_version() -> None:
)
def test_release_metadata_retries_without_invalid_auth(monkeypatch: pytest.MonkeyPatch) -> None:
def test_release_metadata_retries_without_invalid_auth(
monkeypatch: pytest.MonkeyPatch,
) -> None:
runtime_setup = _load_runtime_setup_module()
authorizations: list[str | None] = []
@@ -198,6 +215,14 @@ def test_runtime_package_is_wheel_only_and_builds_platform_specific_wheels() ->
)
hook_source = (ROOT.parent / "python-runtime" / "hatch_build.py").read_text()
hook_tree = ast.parse(hook_source)
platform_tag_assignment = next(
node
for node in hook_tree.body
if isinstance(node, ast.Assign)
and len(node.targets) == 1
and isinstance(node.targets[0], ast.Name)
and node.targets[0].id == "PLATFORM_TAG_BY_TARGET"
)
initialize_fn = next(
node
for node in ast.walk(hook_tree)
@@ -235,6 +260,7 @@ def test_runtime_package_is_wheel_only_and_builds_platform_specific_wheels() ->
and isinstance(node.value, ast.Constant)
}
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,23 +270,51 @@ 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}
assert ast.literal_eval(platform_tag_assignment.value) == {
"aarch64-apple-darwin": "macosx_11_0_arm64",
"x86_64-apple-darwin": "macosx_10_12_x86_64",
"aarch64-unknown-linux-musl": "musllinux_1_2_aarch64",
"x86_64-unknown-linux-musl": "musllinux_1_2_x86_64",
"aarch64-pc-windows-msvc": "win_arm64",
"x86_64-pc-windows-msvc": "win_amd64",
}
assert "CODEX_PYTHON_RUNTIME_TARGET" in hook_source
assert '"infer_tag"' in hook_source
assert '"tag"' in hook_source
def test_stage_runtime_release_copies_binary_and_sets_version(tmp_path: Path) -> None:
def test_python_release_version_normalization() -> None:
script = _load_update_script_module()
fake_binary = tmp_path / script.runtime_binary_name()
assert script.normalize_python_package_version("1.2.3") == "1.2.3"
assert script.normalize_python_package_version("1.2.3-alpha.4") == "1.2.3a4"
assert script.normalize_python_package_version("1.2.3-beta.5") == "1.2.3b5"
assert script.normalize_python_package_version("1.2.3a4") == "1.2.3a4"
assert script.normalize_python_package_version("0.0.0.dev0") == "0.0.0.dev0"
with pytest.raises(RuntimeError, match="Unsupported Python package version"):
script.normalize_python_package_version("1.2.3-rc.1")
def test_stage_runtime_release_copies_bundle_and_sets_version(tmp_path: Path) -> None:
script = _load_update_script_module()
bundle_dir = tmp_path / "bundle"
bundle_dir.mkdir()
fake_binary = bundle_dir / script.runtime_binary_name()
fake_binary.write_text("fake codex\n")
staged = script.stage_python_runtime_package(
tmp_path / "runtime-stage",
"1.2.3",
fake_binary,
"1.2.3-alpha.4",
bundle_dir,
)
assert staged == tmp_path / "runtime-stage"
assert script.staged_runtime_bin_path(staged).read_text() == "fake codex\n"
assert 'version = "1.2.3"' in (staged / "pyproject.toml").read_text()
pyproject = (staged / "pyproject.toml").read_text()
assert 'name = "openai-codex-cli-bin"' in pyproject
assert 'version = "1.2.3a4"' in pyproject
def test_stage_runtime_release_replaces_existing_staging_dir(tmp_path: Path) -> None:
@@ -270,13 +324,15 @@ def test_stage_runtime_release_replaces_existing_staging_dir(tmp_path: Path) ->
old_file.parent.mkdir(parents=True)
old_file.write_text("stale")
fake_binary = tmp_path / script.runtime_binary_name()
bundle_dir = tmp_path / "bundle"
bundle_dir.mkdir()
fake_binary = bundle_dir / script.runtime_binary_name()
fake_binary.write_text("fake codex\n")
staged = script.stage_python_runtime_package(
staging_dir,
"1.2.3",
fake_binary,
bundle_dir,
)
assert staged == staging_dir
@@ -284,13 +340,132 @@ 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_sdk_release_injects_exact_runtime_pin(tmp_path: Path) -> None:
def test_stage_runtime_release_normalizes_target_suffixed_names(
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")
bundle_dir = tmp_path / "bundle"
bundle_dir.mkdir()
(bundle_dir / "codex-x86_64-unknown-linux-musl").write_text("fake codex\n")
staged = script.stage_python_runtime_package(
tmp_path / "runtime-stage",
"1.2.3",
bundle_dir,
)
assert (staged / "src" / "codex_cli_bin" / "bin" / "codex").read_text() == (
"fake codex\n"
)
def test_stage_runtime_release_requires_complete_windows_bundle(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
script = _load_update_script_module()
monkeypatch.setattr(script.platform, "system", lambda: "Windows")
bundle_dir = tmp_path / "bundle"
bundle_dir.mkdir()
(bundle_dir / "codex-x86_64-pc-windows-msvc.exe").write_text("codex\n")
(bundle_dir / "codex-command-runner-x86_64-pc-windows-msvc.exe").write_text(
"runner\n"
)
(bundle_dir / "codex-windows-sandbox-setup-x86_64-pc-windows-msvc.exe").write_text(
"setup\n"
)
staged = script.stage_python_runtime_package(
tmp_path / "runtime-stage",
"1.2.3",
bundle_dir,
)
bin_dir = staged / "src" / "codex_cli_bin" / "bin"
assert (bin_dir / "codex.exe").read_text() == "codex\n"
assert (bin_dir / "codex-command-runner.exe").read_text() == "runner\n"
assert (bin_dir / "codex-windows-sandbox-setup.exe").read_text() == "setup\n"
def test_stage_runtime_release_fails_for_missing_required_file(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
script = _load_update_script_module()
monkeypatch.setattr(script.platform, "system", lambda: "Windows")
bundle_dir = tmp_path / "bundle"
bundle_dir.mkdir()
(bundle_dir / "codex.exe").write_text("codex\n")
with pytest.raises(RuntimeError, match="codex-command-runner.exe"):
script.stage_python_runtime_package(
tmp_path / "runtime-stage",
"1.2.3",
bundle_dir,
)
def test_runtime_package_helpers_return_packaged_paths(tmp_path: Path) -> None:
script = _load_update_script_module()
bundle_dir = tmp_path / "bundle"
bundle_dir.mkdir()
(bundle_dir / "codex").write_text("fake codex\n")
staged = script.stage_python_runtime_package(
tmp_path / "runtime-stage",
"1.2.3",
bundle_dir,
)
runtime_module = _load_runtime_package_module(staged)
assert runtime_module.PACKAGE_NAME == "openai-codex-cli-bin"
assert runtime_module.bundled_bin_dir() == staged / "src" / "codex_cli_bin" / "bin"
assert runtime_module.bundled_runtime_files() == (
staged / "src" / "codex_cli_bin" / "bin" / "codex",
)
assert runtime_module.bundled_codex_path() == (
staged / "src" / "codex_cli_bin" / "bin" / "codex"
)
def test_runtime_package_helpers_report_missing_binary(tmp_path: Path) -> None:
script = _load_update_script_module()
bundle_dir = tmp_path / "bundle"
bundle_dir.mkdir()
(bundle_dir / "codex").write_text("fake codex\n")
staged = script.stage_python_runtime_package(
tmp_path / "runtime-stage",
"1.2.3",
bundle_dir,
)
(staged / "src" / "codex_cli_bin" / "bin" / "codex").unlink()
runtime_module = _load_runtime_package_module(staged)
with pytest.raises(FileNotFoundError, match="openai-codex-cli-bin"):
runtime_module.bundled_codex_path()
def test_stage_sdk_release_injects_exact_runtime_pin_and_versions(
tmp_path: Path,
) -> None:
script = _load_update_script_module()
staged = script.stage_python_sdk_package(
tmp_path / "sdk-stage",
"0.2.1-beta.2",
"1.2.3-alpha.4",
)
pyproject = (staged / "pyproject.toml").read_text()
assert 'version = "0.2.1"' in pyproject
assert '"codex-cli-bin==1.2.3"' in pyproject
assert 'name = "openai-codex"' in pyproject
assert 'version = "0.2.1b2"' in pyproject
assert '"openai-codex-cli-bin==1.2.3a4"' in pyproject
assert (
'__version__ = "0.2.1b2"'
in (staged / "src" / "codex_app_server" / "__init__.py").read_text()
)
assert (
'client_version: str = "0.2.1b2"'
in (staged / "src" / "codex_app_server" / "client.py").read_text()
)
assert not any((staged / "src" / "codex_app_server").glob("bin/**"))
@@ -329,7 +504,7 @@ 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_bundle_dir: Path
) -> Path:
raise AssertionError("runtime staging should not run for stage-sdk")
@@ -350,14 +525,15 @@ def test_stage_sdk_runs_type_generation_before_staging(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()
fake_binary.write_text("fake codex\n")
bundle_dir = tmp_path / "bundle"
bundle_dir.mkdir()
(bundle_dir / script.runtime_binary_name()).write_text("fake codex\n")
calls: list[str] = []
args = script.parse_args(
[
"stage-runtime",
str(tmp_path / "runtime-stage"),
str(fake_binary),
str(bundle_dir),
"--runtime-version",
"1.2.3",
]
@@ -372,7 +548,7 @@ 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, _runtime_version: str, _runtime_bundle_dir: Path
) -> Path:
calls.append("stage_runtime")
return tmp_path / "runtime-stage"