"""Build a Nuitka-compiled orisys wheel and publish it to SDK/{version}/. Pipeline: 1. Compile src/orisys → extension module (.pyd / .so) 2. Stage wheel tree (compiled module + bundled files from pyproject.toml) 3. Run bdist_wheel; on Linux, auditwheel-repair to a manylinux tag 4. Smoke-test imports in a temporary venv (required for --target pypi-linux) Targets: internal – local/SDK delivery (default). Linux still repairs with auditwheel. pypi-linux – PyPI binary wheel; requires Linux + CPython 3.10/3.11/3.12 + compliant tag. Run from repo root: python build/build_nuitka.py python build/build_nuitka.py --target pypi-linux """ from __future__ import annotations import argparse import os import shutil import subprocess import sys from pathlib import Path from nuitka_compile import compile_orisys, copy_compiled_orisys, install_nuitka from paths import NUITKA_COMPILE_DIR, NUITKA_WHEEL_DIR, REPO_ROOT, repaired_wheel_dir, repo_path from pyproject_utils import bundle_items, extras_require, get_version, install_requires from verify import verify_built_wheel from wheel_build import ( is_compliant_manylinux_wheel, publish_wheel_to_sdk, repair_linux_wheel, run_bdist_wheel, write_wheel_setup, ) from wheel_layout import copy_bundle_items, copy_orisys_blend, package_data_from_bundle VALID_TARGETS = ("internal", "pypi-linux") # ABI tags that manylinux images ship under /opt/python/cpXYY-cpXYY. PYPI_LINUX_PYTHON_VERSIONS = ((3, 10), (3, 11), (3, 12)) def _reset_dir(path: Path) -> None: """Remove and recreate a build output directory.""" if path.exists(): shutil.rmtree(path) path.mkdir(parents=True) def _validate_pypi_linux_environment() -> None: if sys.platform != "linux": raise SystemExit( "[ERROR] --target pypi-linux must run inside a Linux manylinux container " "(for example quay.io/pypa/manylinux_2_28_x86_64 via Docker Desktop)." ) if sys.version_info[:2] not in PYPI_LINUX_PYTHON_VERSIONS: supported = ", ".join(f"{major}.{minor}" for major, minor in PYPI_LINUX_PYTHON_VERSIONS) raise SystemExit( f"[ERROR] --target pypi-linux requires CPython {supported}, got " f"{sys.version_info.major}.{sys.version_info.minor}. " "Use /opt/python/cp310-cp310 (or cp311/cp312) in the manylinux_2_28 image." ) def build(*, target: str = "internal", verify: bool | None = None) -> Path: if target not in VALID_TARGETS: raise SystemExit(f"[ERROR] Unknown target {target!r}; choose from {VALID_TARGETS}") if target == "pypi-linux": _validate_pypi_linux_environment() # pypi-linux always verifies; internal opts in with --verify. if verify is None: verify = target == "pypi-linux" # --- 1. Compile --- install_nuitka() version = get_version(REPO_ROOT) _reset_dir(NUITKA_COMPILE_DIR) _reset_dir(NUITKA_WHEEL_DIR) print(f"\n=== Compiling with Nuitka (target={target}, version={version}) ===") env = os.environ.copy() src_dir = repo_path("src") env["PYTHONPATH"] = src_dir.as_posix() + os.pathsep + env.get("PYTHONPATH", "") ext_file = compile_orisys(NUITKA_COMPILE_DIR, env) # --- 2. Stage wheel contents --- print("\n=== Preparing Wheel Structure ===") pkg_dir = NUITKA_WHEEL_DIR / "wheel_root" / "orisys" pkg_dir.mkdir(parents=True) ext_suffix = copy_compiled_orisys(pkg_dir, ext_file) wheel_root = NUITKA_WHEEL_DIR / "wheel_root" blend_count = copy_orisys_blend(REPO_ROOT, wheel_root) print(f"Staged orisys_blend Python modules: {blend_count}") # Bundle rules live in pyproject.toml → [[tool.orisys.bundle.items]] bundle = bundle_items(REPO_ROOT) copied_counts = copy_bundle_items(REPO_ROOT, pkg_dir, bundle) print(f"Copied {copied_counts['configs']} JSON config files") print(f"Copied {copied_counts['examples']} example files to orisys/examples/") print(f"Copied {copied_counts['docs']} doc files to orisys/docs/") print(f"Copied {copied_counts['assets']} finger asset files to orisys/assets/") write_wheel_setup( NUITKA_WHEEL_DIR, version=version, package_data=package_data_from_bundle(ext_suffix, bundle), install_requires=install_requires(REPO_ROOT), extras_require=extras_require(REPO_ROOT), ) # --- 3. Build wheel, repair on Linux, publish to SDK/ --- wheel_path = run_bdist_wheel(NUITKA_WHEEL_DIR) if wheel_path is None: raise SystemExit("\n[ERROR] Wheel not found after bdist_wheel.") repair_dir = repaired_wheel_dir() final_candidate = repair_linux_wheel(wheel_path, repair_dir) if target == "pypi-linux" and not is_compliant_manylinux_wheel(final_candidate): raise SystemExit( f"[ERROR] Wheel is not PyPI-compliant: {final_candidate.name}. " "Expected a manylinux_2_17_x86_64 (or newer) platform tag." ) # Keep a durable copy under dist/repaired for upload tooling. dist_repaired = REPO_ROOT / "dist" / "repaired" dist_repaired.mkdir(parents=True, exist_ok=True) dist_copy = dist_repaired / final_candidate.name if dist_copy.resolve() != final_candidate.resolve(): shutil.copy2(final_candidate, dist_copy) final_wheel = publish_wheel_to_sdk(final_candidate, version) print(f"\n[SUCCESS] Wheel created and copied to: {final_wheel}") print(f"[SUCCESS] Upload candidate also at: {dist_copy}") # --- 4. Verify (isolated venv; does not touch the active environment) --- if verify: print("\n=== Verifying wheel imports in isolated venv ===") try: verify_built_wheel(final_wheel) print("[SUCCESS] Wheel smoke test passed.") except subprocess.CalledProcessError as exc: if target == "pypi-linux": raise SystemExit( f"[ERROR] Wheel verify failed (required for pypi-linux): {exc}" ) from exc print(f"\n[WARNING] Wheel verify failed: {exc}") print("The wheel was built successfully; verification runs in an isolated temp venv.") else: print("\n[INFO] Skipping wheel verify (pass --verify to enable).") return final_wheel def main(argv: list[str] | None = None) -> None: parser = argparse.ArgumentParser(description="Build a Nuitka-compiled orisys wheel.") parser.add_argument( "--target", choices=VALID_TARGETS, default="internal", help="internal: SDK delivery; pypi-linux: manylinux PyPI binary wheel", ) parser.add_argument( "--verify", action="store_true", help="Run isolated venv smoke test (always on for --target pypi-linux)", ) parser.add_argument( "--skip-verify", action="store_true", help="Skip smoke test when using --target internal", ) args = parser.parse_args(argv) verify: bool | None if args.target == "pypi-linux": verify = True elif args.verify: verify = True elif args.skip_verify: verify = False else: verify = None build(target=args.target, verify=verify) if __name__ == "__main__": main()