|
| 1 | +#!/usr/bin/env python3 |
| 2 | +"""Package a pre-built PyO3 cdylib plus the Python source into a PEP 427 wheel. |
| 3 | +
|
| 4 | +Nix builds the `tui-py` cdylib through cargo-unit and calls this to assemble the |
| 5 | +`ix-tui` wheel, so there is no maturin / PEP 517 backend in the loop. The |
| 6 | +extension is abi3 (`pyo3/abi3-py311`), hence the `cp311-abi3` tag: one wheel |
| 7 | +loads on CPython 3.11+. |
| 8 | +""" |
| 9 | + |
| 10 | +from __future__ import annotations |
| 11 | + |
| 12 | +import argparse |
| 13 | +import base64 |
| 14 | +import hashlib |
| 15 | +import pathlib |
| 16 | +import zipfile |
| 17 | + |
| 18 | +# Import package vs. PyPI distribution: `import tui`, but the wheel and |
| 19 | +# dist-info carry the distribution name `ix-tui` (normalized to `ix_tui`). |
| 20 | +PKG = "tui" |
| 21 | +DIST = "ix_tui" |
| 22 | +DIST_NAME = "ix-tui" |
| 23 | +SO_NAME = "_tui.abi3.so" |
| 24 | +# Files copied verbatim from the Python source tree into the wheel. |
| 25 | +SOURCE_FILES = ["__init__.py", "_tui.pyi", "py.typed"] |
| 26 | + |
| 27 | + |
| 28 | +def sha256_b64(data: bytes) -> str: |
| 29 | + digest = hashlib.sha256(data).digest() |
| 30 | + return base64.urlsafe_b64encode(digest).rstrip(b"=").decode("ascii") |
| 31 | + |
| 32 | + |
| 33 | +def record_line(name: str, data: bytes) -> str: |
| 34 | + return f"{name},sha256={sha256_b64(data)},{len(data)}" |
| 35 | + |
| 36 | + |
| 37 | +def build_wheel( |
| 38 | + *, |
| 39 | + cdylib: pathlib.Path, |
| 40 | + python_src: pathlib.Path, |
| 41 | + version: str, |
| 42 | + platform_tag: str, |
| 43 | + out: pathlib.Path, |
| 44 | +) -> pathlib.Path: |
| 45 | + tag = f"cp311-abi3-{platform_tag}" |
| 46 | + dist_info = f"{DIST}-{version}.dist-info" |
| 47 | + wheel_path = out / f"{DIST}-{version}-{tag}.whl" |
| 48 | + |
| 49 | + files: dict[str, bytes] = {} |
| 50 | + for name in SOURCE_FILES: |
| 51 | + files[f"{PKG}/{name}"] = (python_src / PKG / name).read_bytes() |
| 52 | + files[f"{PKG}/{SO_NAME}"] = cdylib.read_bytes() |
| 53 | + |
| 54 | + files[f"{dist_info}/METADATA"] = ( |
| 55 | + "Metadata-Version: 2.4\n" |
| 56 | + f"Name: {DIST_NAME}\n" |
| 57 | + f"Version: {version}\n" |
| 58 | + "Summary: Python bindings for the tui PTY-backed terminal manager. " |
| 59 | + "Imported as `tui`.\n" |
| 60 | + "Author: indexable\n" |
| 61 | + "Requires-Python: >=3.11\n" |
| 62 | + "Requires-Dist: numpy>=1.26\n" |
| 63 | + ).encode() |
| 64 | + files[f"{dist_info}/WHEEL"] = ( |
| 65 | + "Wheel-Version: 1.0\n" |
| 66 | + "Generator: mkwheel\n" |
| 67 | + "Root-Is-Purelib: false\n" |
| 68 | + f"Tag: {tag}\n" |
| 69 | + ).encode() |
| 70 | + |
| 71 | + records = [record_line(name, data) for name, data in files.items()] |
| 72 | + records.append(f"{dist_info}/RECORD,,") |
| 73 | + files[f"{dist_info}/RECORD"] = "\n".join(records).encode() + b"\n" |
| 74 | + |
| 75 | + out.mkdir(parents=True, exist_ok=True) |
| 76 | + # Deterministic order so the wheel hash is stable across builds. |
| 77 | + with zipfile.ZipFile(wheel_path, "w", zipfile.ZIP_DEFLATED) as zf: |
| 78 | + for name in sorted(files): |
| 79 | + zf.writestr(name, files[name]) |
| 80 | + |
| 81 | + return wheel_path |
| 82 | + |
| 83 | + |
| 84 | +def main() -> None: |
| 85 | + p = argparse.ArgumentParser() |
| 86 | + p.add_argument("--cdylib", type=pathlib.Path, required=True) |
| 87 | + p.add_argument("--python-src", type=pathlib.Path, required=True) |
| 88 | + p.add_argument("--version", default="0.1.0") |
| 89 | + p.add_argument("--platform-tag", default="manylinux_2_34_x86_64") |
| 90 | + p.add_argument("--out", type=pathlib.Path, required=True) |
| 91 | + args = p.parse_args() |
| 92 | + |
| 93 | + print( |
| 94 | + build_wheel( |
| 95 | + cdylib=args.cdylib, |
| 96 | + python_src=args.python_src, |
| 97 | + version=args.version, |
| 98 | + platform_tag=args.platform_tag, |
| 99 | + out=args.out, |
| 100 | + ) |
| 101 | + ) |
| 102 | + |
| 103 | + |
| 104 | +if __name__ == "__main__": |
| 105 | + main() |
0 commit comments