Files
orisys-sdk/build/wheel/build.py
T
2026-08-26 19:22:37 +08:00

204 lines
7.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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
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,
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)
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['sensors']} sensor bundle files to orisys/sensors/")
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()