215 lines
7.4 KiB
Python
215 lines
7.4 KiB
Python
"""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/run.py wheel
|
||
python build/run.py wheel --target pypi-linux
|
||
python build/wheel/build.py --target pypi-linux
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import argparse
|
||
import os
|
||
import shutil
|
||
import subprocess
|
||
import sys
|
||
from pathlib import Path
|
||
|
||
_BUILD_ROOT = Path(__file__).resolve().parents[1]
|
||
if str(_BUILD_ROOT) not in sys.path:
|
||
sys.path.insert(0, str(_BUILD_ROOT))
|
||
|
||
from common.paths import NUITKA_COMPILE_DIR, NUITKA_WHEEL_DIR, REPO_ROOT, repaired_wheel_dir, repo_path
|
||
from common.pyproject_utils import (
|
||
bundle_items,
|
||
extras_require,
|
||
get_version,
|
||
install_requires,
|
||
project_metadata,
|
||
)
|
||
from common.verify.wheel import verify_built_wheel
|
||
from wheel.compile import compile_orisys, copy_compiled_orisys, install_nuitka
|
||
from wheel.layout import (
|
||
copy_bundle_items,
|
||
copy_orisys_blend,
|
||
package_data_from_bundle,
|
||
)
|
||
from wheel.package import (
|
||
is_compliant_manylinux_wheel,
|
||
publish_wheel_to_sdk,
|
||
read_long_description,
|
||
repair_linux_wheel,
|
||
run_bdist_wheel,
|
||
write_wheel_setup,
|
||
)
|
||
|
||
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)
|
||
for item in bundle:
|
||
destination = f"orisys/{item['target']}/"
|
||
print(
|
||
f"Copied {copied_counts.get(item['name'], 0)} file(s) "
|
||
f"for bundle item {item['name']!r} to {destination}"
|
||
)
|
||
|
||
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),
|
||
project=project_metadata(REPO_ROOT),
|
||
long_description=read_long_description(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()
|