Files
orisys-sdk/build/wheel/build.py
T
ykcheungab bb15b750fb feat: 添加 ARM64 manylinux wheel 构建选项
复用现有 Docker 和 Nuitka 流程,隔离架构镜像并支持 aarch64 标签。已通过 47 项测试、代码及文档门禁,并完成 Python 3.10 ARM64 wheel 构建、隔离导入与内容审计。
2026-10-14 16:08:54 +08:00

220 lines
7.4 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,
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 "
"(manylinux_2_28_x86_64 or manylinux_2_28_aarch64 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 supported manylinux x86_64 or aarch64 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" or args.verify:
verify = True
elif args.skip_verify:
verify = False
else:
verify = None
build(target=args.target, verify=verify)
if __name__ == "__main__":
main()