#!/usr/bin/env python3 """把「纯 Python wheel」重打包成「编译后 .so wheel」。 输入: 一个正常 wheel(含 /**/*.py + 正确的依赖元数据)和 Nuitka 编译出的 整包 .cpython-XXX-darwin.so。输出: 用单个 .so 替换掉整个 .py 包目录的 平台相关 wheel —— 解开 .whl 只能看到机器码,源码不再随包分发。 保留: dist-info(含 METADATA 的依赖声明,pip 据此拉第三方依赖)。 重写: WHEEL(标记非 purelib + 平台 tag)、RECORD(按最终文件树重算哈希)。 丢弃: / 下全部 .py 与 __pycache__(已被 .so 取代)。 用法: repack_wheel_so.py --wheel dist/lambdagent-1.3.0-py3-none-any.whl \ --so build/lambdagent.cpython-311-darwin.so \ --package lambdagent \ --tag cp311-cp311-macosx_15_0_arm64 \ --out dist-secure/ """ from __future__ import annotations import argparse import base64 import csv import hashlib import os import shutil import tempfile import zipfile from pathlib import Path def _hash(path: Path) -> tuple[str, int]: data = path.read_bytes() digest = base64.urlsafe_b64encode(hashlib.sha256(data).digest()).rstrip(b"=").decode() return f"sha256={digest}", len(data) def repack(wheel: Path, so: Path, package: str, tag: str, out_dir: Path) -> Path: out_dir.mkdir(parents=True, exist_ok=True) with tempfile.TemporaryDirectory() as td: root = Path(td) with zipfile.ZipFile(wheel) as zf: zf.extractall(root) # 1) 删除整个 .py 包目录,换成单个 .so pkg_dir = root / package if pkg_dir.is_dir(): shutil.rmtree(pkg_dir) shutil.copy2(so, root / so.name) # 2) 定位 dist-info dist_info = next(p for p in root.iterdir() if p.name.endswith(".dist-info")) # 3) 重写 WHEEL: 非 purelib + 平台 tag wheel_meta = dist_info / "WHEEL" lines = [] for ln in wheel_meta.read_text().splitlines(): if ln.startswith("Root-Is-Purelib:"): ln = "Root-Is-Purelib: false" elif ln.startswith("Tag:"): ln = f"Tag: {tag}" lines.append(ln) if not any(l.startswith("Root-Is-Purelib:") for l in lines): lines.append("Root-Is-Purelib: false") if not any(l.startswith("Tag:") for l in lines): lines.append(f"Tag: {tag}") wheel_meta.write_text("\n".join(lines) + "\n") # 4) 重算 RECORD(遍历最终文件树) record_path = dist_info / "RECORD" rows = [] for f in sorted(root.rglob("*")): if f.is_dir(): continue rel = f.relative_to(root).as_posix() if rel == f"{dist_info.name}/RECORD": rows.append([rel, "", ""]) continue h, n = _hash(f) rows.append([rel, h, str(n)]) with record_path.open("w", newline="") as fp: csv.writer(fp).writerows(rows) # 5) 重新打包,文件名换平台 tag name = wheel.name # ----.whl → 替换尾部三段为新 tag head = "-".join(name.split("-")[:2]) # dist-version out_wheel = out_dir / f"{head}-{tag}.whl" if out_wheel.exists(): out_wheel.unlink() with zipfile.ZipFile(out_wheel, "w", zipfile.ZIP_DEFLATED) as zf: # RECORD 最后写 files = [r[0] for r in rows if not r[0].endswith("/RECORD")] for rel in files: zf.write(root / rel, rel) zf.write(record_path, f"{dist_info.name}/RECORD") return out_wheel def main() -> None: ap = argparse.ArgumentParser() ap.add_argument("--wheel", required=True, type=Path) ap.add_argument("--so", required=True, type=Path) ap.add_argument("--package", required=True) ap.add_argument("--tag", required=True) ap.add_argument("--out", required=True, type=Path) a = ap.parse_args() result = repack(a.wheel, a.so, a.package, a.tag, a.out) print(f"OK -> {result}") if __name__ == "__main__": main()