"""Assemble and build the Nuitka wheel with setuptools.""" from __future__ import annotations import re 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 sdk_dir # PyPI rejects bare tags like "manylinux_x86_64"; require a glibc ABI version. _MANYLINUX_TAG_RE = re.compile( r"(?:manylinux2014|manylinux_2_(?:17|24|27|28|31|34|35))_x86_64" ) # Source of the PyPI project description (see read_long_description). PYPI_README_RELPATH = Path("docs") / "pypi_readme.md" def _license_text(project: dict) -> str | None: """Extract free-text license from either pyproject license form.""" declared = project.get("license") if isinstance(declared, str): return declared if isinstance(declared, dict): text = declared.get("text") if isinstance(text, str): return text return None def _person_strings(entries) -> list[str]: """Render PEP 621 author/maintainer tables as ``Name `` strings.""" rendered: list[str] = [] for entry in entries or []: if not isinstance(entry, dict): continue name = (entry.get("name") or "").strip() email = (entry.get("email") or "").strip() if name and email: rendered.append(f"{name} <{email}>") elif name or email: rendered.append(name or email) return rendered def metadata_kwargs(project: dict) -> dict: """Translate a pyproject ``[project]`` table into ``setup()`` keywords. The Nuitka wheel is built by a generated ``setup.py`` outside the normal setuptools pipeline, so every PyPI-visible field has to be passed through explicitly. Omitting them is what left the early PyPI releases with no ``Requires-Python``, classifiers, project URLs or license on the project page. """ authors = _person_strings(project.get("authors")) maintainers = _person_strings(project.get("maintainers")) candidates = { "description": project.get("description"), "python_requires": project.get("requires-python"), "classifiers": list(project.get("classifiers") or []), "project_urls": dict(project.get("urls") or {}), "license": _license_text(project), "author": authors[0] if authors else None, "author_email": None, "maintainer": maintainers[0] if maintainers else None, } return {key: value for key, value in candidates.items() if value} def read_long_description(repo_root: Path) -> str | None: """Return the PyPI long description, or ``None`` when it is missing. Deliberately not the repository ``README.md``: that file documents the closed-source build/publishing layout, which should not end up on the PyPI project page. """ path = Path(repo_root) / PYPI_README_RELPATH if not path.is_file(): print( f"[WARN] {PYPI_README_RELPATH.as_posix()} not found; the wheel will be " "published without a long description." ) return None return path.read_text(encoding="utf-8") def write_wheel_setup( wheel_dir: Path, *, version: str, package_data: dict[str, list[str]], install_requires: list[str], extras_require: dict[str, list[str]], project: dict | None = None, long_description: str | None = None, ) -> Path: metadata_lines = "" if project: for key, value in sorted(metadata_kwargs(project).items()): metadata_lines += f" {key}={value!r},\n" if long_description: metadata_lines += " long_description=_LONG_DESCRIPTION,\n" metadata_lines += ' long_description_content_type="text/markdown",\n' setup_py_path = wheel_dir / "setup.py" setup_content = f""" from setuptools import setup, Distribution class BinaryDistribution(Distribution): def has_ext_modules(self): return True {'_LONG_DESCRIPTION = ' + repr(long_description) if long_description else ''} setup( name="orisys", version="{version}", {metadata_lines} packages=['orisys', 'orisys.examples', 'orisys_blend'], package_dir={{'': 'wheel_root'}}, package_data={package_data}, include_package_data=True, install_requires={install_requires!r}, extras_require={extras_require!r}, distclass=BinaryDistribution, zip_safe=False, ) """ setup_py_path.write_text(setup_content, encoding="utf-8") return setup_py_path def run_bdist_wheel(wheel_dir: Path) -> Path | None: print("\n=== Building Wheel ===") subprocess.check_call([sys.executable, "setup.py", "bdist_wheel"], cwd=wheel_dir) wheels = sorted((wheel_dir / "dist").glob("*.whl")) return wheels[0] if wheels else None def _ensure_auditwheel() -> None: try: subprocess.check_call( [sys.executable, "-m", "auditwheel", "--version"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, ) except (subprocess.CalledProcessError, FileNotFoundError): print("Installing auditwheel...") subprocess.check_call([sys.executable, "-m", "pip", "install", "auditwheel", "-q"]) def is_compliant_manylinux_wheel(wheel_path: Path) -> bool: """True if the wheel filename uses a PyPI-accepted manylinux x86_64 tag.""" return bool(_MANYLINUX_TAG_RE.search(wheel_path.name)) def repair_linux_wheel(wheel_path: Path, repair_dir: Path) -> Path: """Run auditwheel show/repair and return a PyPI-compliant manylinux wheel. Raises RuntimeError if repair fails or the result still lacks a valid tag. On non-Linux platforms the input wheel is returned unchanged. """ if sys.platform != "linux": print("Skipping auditwheel repair (not running on Linux).") return wheel_path _ensure_auditwheel() repair_dir = repair_dir.resolve() if repair_dir.exists(): shutil.rmtree(repair_dir) repair_dir.mkdir(parents=True) print("\n=== auditwheel show ===") subprocess.check_call([sys.executable, "-m", "auditwheel", "show", str(wheel_path)]) print("\n=== auditwheel repair ===") subprocess.check_call( [ sys.executable, "-m", "auditwheel", "repair", str(wheel_path), "--wheel-dir", str(repair_dir), ] ) repaired = sorted(repair_dir.glob("*.whl")) if not repaired: raise RuntimeError(f"auditwheel repair produced no wheels in {repair_dir}") compliant = [path for path in repaired if is_compliant_manylinux_wheel(path)] if not compliant: names = ", ".join(path.name for path in repaired) raise RuntimeError( "auditwheel repair did not produce a PyPI-compliant manylinux tag. " f"Got: {names}. Rebuild inside quay.io/pypa/manylinux_2_28_x86_64 " "(or manylinux2014_x86_64) instead of a generic Linux host." ) final = compliant[0] print(f"Repaired wheel: {final.name}") return final def publish_wheel_to_sdk(wheel_path: Path, version: str) -> Path: sdk_version_dir = sdk_dir(version) sdk_version_dir.mkdir(parents=True, exist_ok=True) dest_wheel = sdk_version_dir / wheel_path.name if dest_wheel.exists(): dest_wheel.unlink() shutil.copy2(str(wheel_path), str(dest_wheel)) return dest_wheel