from __future__ import annotations import importlib import importlib.util import json import os import platform import shutil import subprocess import sys import tarfile import tempfile import urllib.error import urllib.request import zipfile from pathlib import Path PACKAGE_NAME = "codex-cli-bin" PINNED_RUNTIME_VERSION = "0.116.0-alpha.1" REPO_SLUG = "openai/codex" class RuntimeSetupError(RuntimeError): pass def pinned_runtime_version() -> str: return PINNED_RUNTIME_VERSION def ensure_runtime_package_installed( python_executable: str | Path, sdk_python_dir: Path, install_target: Path | None = None, ) -> str: requested_version = pinned_runtime_version() installed_version = None if install_target is None: 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: 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) staged_runtime_dir = _stage_runtime_package( sdk_python_dir, requested_version, runtime_binary, temp_root / "runtime-stage", ) _install_runtime_package(python_executable, staged_runtime_dir, install_target) if install_target is not None: return requested_version if Path(python_executable).resolve() == Path(sys.executable).resolve(): importlib.invalidate_caches() installed_version = _installed_runtime_version(python_executable) 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." ) return requested_version def platform_asset_name() -> str: system = platform.system().lower() machine = platform.machine().lower() if system == "darwin": if machine in {"arm64", "aarch64"}: return "codex-aarch64-apple-darwin.tar.gz" if machine in {"x86_64", "amd64"}: return "codex-x86_64-apple-darwin.tar.gz" elif system == "linux": if machine in {"aarch64", "arm64"}: return "codex-aarch64-unknown-linux-musl.tar.gz" if machine in {"x86_64", "amd64"}: return "codex-x86_64-unknown-linux-musl.tar.gz" elif system == "windows": if machine in {"aarch64", "arm64"}: return "codex-aarch64-pc-windows-msvc.exe.zip" if machine in {"x86_64", "amd64"}: return "codex-x86_64-pc-windows-msvc.exe.zip" raise RuntimeSetupError( f"Unsupported runtime artifact platform: system={platform.system()!r}, " f"machine={platform.machine()!r}" ) def runtime_binary_name() -> str: return "codex.exe" if platform.system().lower() == "windows" else "codex" def _installed_runtime_version(python_executable: str | Path) -> str | None: snippet = ( "import importlib.metadata, json, sys\n" "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" "except Exception:\n" " sys.exit(1)\n" ) result = subprocess.run( [str(python_executable), "-c", snippet], text=True, capture_output=True, check=False, ) if result.returncode != 0: return None return json.loads(result.stdout)["version"] def _release_metadata(version: str) -> dict[str, object]: url = f"https://api.github.com/repos/{REPO_SLUG}/releases/tags/rust-v{version}" token = _github_token() attempts = [True, False] if token is not None else [False] last_error: urllib.error.HTTPError | None = None for include_auth in attempts: headers = { "Accept": "application/vnd.github+json", "User-Agent": "codex-python-runtime-setup", } if include_auth and token is not None: headers["Authorization"] = f"Bearer {token}" request = urllib.request.Request(url, headers=headers) try: with urllib.request.urlopen(request) as response: return json.load(response) except urllib.error.HTTPError as exc: last_error = exc if include_auth and exc.code == 401: continue break assert last_error is not None raise RuntimeSetupError( f"Failed to resolve release metadata for rust-v{version} from {REPO_SLUG}: " f"{last_error.code} {last_error.reason}" ) from last_error def _download_release_archive(version: str, temp_root: Path) -> Path: asset_name = platform_asset_name() archive_path = temp_root / asset_name browser_download_url = ( f"https://github.com/{REPO_SLUG}/releases/download/rust-v{version}/{asset_name}" ) request = urllib.request.Request( browser_download_url, headers={"User-Agent": "codex-python-runtime-setup"}, ) try: with urllib.request.urlopen(request) as response, archive_path.open("wb") as fh: shutil.copyfileobj(response, fh) return archive_path except urllib.error.HTTPError: pass metadata = _release_metadata(version) assets = metadata.get("assets") if not isinstance(assets, list): raise RuntimeSetupError(f"Release rust-v{version} returned malformed assets metadata.") asset = next( ( item for item in assets if isinstance(item, dict) and item.get("name") == asset_name ), None, ) if asset is None: raise RuntimeSetupError( f"Release rust-v{version} does not contain asset {asset_name} for this platform." ) api_url = asset.get("url") if not isinstance(api_url, str): api_url = None if api_url is not None: token = _github_token() if token is not None: request = urllib.request.Request( api_url, headers=_github_api_headers("application/octet-stream"), ) try: with urllib.request.urlopen(request) as response, archive_path.open("wb") as fh: shutil.copyfileobj(response, fh) return archive_path except urllib.error.HTTPError: pass if shutil.which("gh") is None: raise RuntimeSetupError( f"Unable to download {asset_name} for rust-v{version}. " "Provide GH_TOKEN/GITHUB_TOKEN or install/authenticate GitHub CLI." ) try: subprocess.run( [ "gh", "release", "download", f"rust-v{version}", "--repo", REPO_SLUG, "--pattern", asset_name, "--dir", str(temp_root), ], check=True, text=True, capture_output=True, ) except subprocess.CalledProcessError as exc: raise RuntimeSetupError( f"gh release download failed for rust-v{version} asset {asset_name}.\n" f"STDOUT:\n{exc.stdout}\nSTDERR:\n{exc.stderr}" ) from exc return archive_path def _extract_runtime_binary(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"): with tarfile.open(archive_path, "r:gz") as tar: try: tar.extractall(extract_dir, filter="data") except TypeError: tar.extractall(extract_dir) elif archive_path.suffix == ".zip": 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}." ) return candidates[0] def _stage_runtime_package( sdk_python_dir: Path, runtime_version: str, runtime_binary: 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(), ) def _install_runtime_package( python_executable: str | Path, staged_runtime_dir: Path, install_target: Path | None, ) -> None: args = [ str(python_executable), "-m", "pip", "install", "--force-reinstall", "--no-deps", ] if install_target is not None: install_target.mkdir(parents=True, exist_ok=True) args.extend(["--target", str(install_target)]) args.append(str(staged_runtime_dir)) try: subprocess.run( args, check=True, text=True, capture_output=True, ) except subprocess.CalledProcessError as exc: raise RuntimeSetupError( f"Failed to install {PACKAGE_NAME} into {python_executable} from {staged_runtime_dir}.\n" f"STDOUT:\n{exc.stdout}\nSTDERR:\n{exc.stderr}" ) from exc def _load_update_script_module(sdk_python_dir: Path): script_path = sdk_python_dir / "scripts" / "update_sdk_artifacts.py" spec = importlib.util.spec_from_file_location("update_sdk_artifacts", script_path) if spec is None or spec.loader is None: raise RuntimeSetupError(f"Failed to load {script_path}") module = importlib.util.module_from_spec(spec) sys.modules[spec.name] = module spec.loader.exec_module(module) return module def _github_api_headers(accept: str) -> dict[str, str]: headers = { "Accept": accept, "User-Agent": "codex-python-runtime-setup", } token = _github_token() if token is not None: headers["Authorization"] = f"Bearer {token}" return headers def _github_token() -> str | None: for env_name in ("GH_TOKEN", "GITHUB_TOKEN"): token = os.environ.get(env_name) if token: return token return None def _normalized_package_version(version: str) -> str: return version.strip().replace("-alpha.", "a").replace("-beta.", "b") __all__ = [ "PACKAGE_NAME", "PINNED_RUNTIME_VERSION", "RuntimeSetupError", "ensure_runtime_package_installed", "pinned_runtime_version", "platform_asset_name", ]