repack_wheel_so.py 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112
  1. #!/usr/bin/env python3
  2. """把「纯 Python wheel」重打包成「编译后 .so wheel」。
  3. 输入: 一个正常 wheel(含 <pkg>/**/*.py + 正确的依赖元数据)和 Nuitka 编译出的
  4. 整包 <pkg>.cpython-XXX-darwin.so。输出: 用单个 .so 替换掉整个 .py 包目录的
  5. 平台相关 wheel —— 解开 .whl 只能看到机器码,源码不再随包分发。
  6. 保留: dist-info(含 METADATA 的依赖声明,pip 据此拉第三方依赖)。
  7. 重写: WHEEL(标记非 purelib + 平台 tag)、RECORD(按最终文件树重算哈希)。
  8. 丢弃: <pkg>/ 下全部 .py 与 __pycache__(已被 .so 取代)。
  9. 用法:
  10. repack_wheel_so.py --wheel dist/lambdagent-1.3.0-py3-none-any.whl \
  11. --so build/lambdagent.cpython-311-darwin.so \
  12. --package lambdagent \
  13. --tag cp311-cp311-macosx_15_0_arm64 \
  14. --out dist-secure/
  15. """
  16. from __future__ import annotations
  17. import argparse
  18. import base64
  19. import csv
  20. import hashlib
  21. import os
  22. import shutil
  23. import tempfile
  24. import zipfile
  25. from pathlib import Path
  26. def _hash(path: Path) -> tuple[str, int]:
  27. data = path.read_bytes()
  28. digest = base64.urlsafe_b64encode(hashlib.sha256(data).digest()).rstrip(b"=").decode()
  29. return f"sha256={digest}", len(data)
  30. def repack(wheel: Path, so: Path, package: str, tag: str, out_dir: Path) -> Path:
  31. out_dir.mkdir(parents=True, exist_ok=True)
  32. with tempfile.TemporaryDirectory() as td:
  33. root = Path(td)
  34. with zipfile.ZipFile(wheel) as zf:
  35. zf.extractall(root)
  36. # 1) 删除整个 .py 包目录,换成单个 .so
  37. pkg_dir = root / package
  38. if pkg_dir.is_dir():
  39. shutil.rmtree(pkg_dir)
  40. shutil.copy2(so, root / so.name)
  41. # 2) 定位 dist-info
  42. dist_info = next(p for p in root.iterdir() if p.name.endswith(".dist-info"))
  43. # 3) 重写 WHEEL: 非 purelib + 平台 tag
  44. wheel_meta = dist_info / "WHEEL"
  45. lines = []
  46. for ln in wheel_meta.read_text().splitlines():
  47. if ln.startswith("Root-Is-Purelib:"):
  48. ln = "Root-Is-Purelib: false"
  49. elif ln.startswith("Tag:"):
  50. ln = f"Tag: {tag}"
  51. lines.append(ln)
  52. if not any(l.startswith("Root-Is-Purelib:") for l in lines):
  53. lines.append("Root-Is-Purelib: false")
  54. if not any(l.startswith("Tag:") for l in lines):
  55. lines.append(f"Tag: {tag}")
  56. wheel_meta.write_text("\n".join(lines) + "\n")
  57. # 4) 重算 RECORD(遍历最终文件树)
  58. record_path = dist_info / "RECORD"
  59. rows = []
  60. for f in sorted(root.rglob("*")):
  61. if f.is_dir():
  62. continue
  63. rel = f.relative_to(root).as_posix()
  64. if rel == f"{dist_info.name}/RECORD":
  65. rows.append([rel, "", ""])
  66. continue
  67. h, n = _hash(f)
  68. rows.append([rel, h, str(n)])
  69. with record_path.open("w", newline="") as fp:
  70. csv.writer(fp).writerows(rows)
  71. # 5) 重新打包,文件名换平台 tag
  72. name = wheel.name
  73. # <dist>-<ver>-<pytag>-<abitag>-<plat>.whl → 替换尾部三段为新 tag
  74. head = "-".join(name.split("-")[:2]) # dist-version
  75. out_wheel = out_dir / f"{head}-{tag}.whl"
  76. if out_wheel.exists():
  77. out_wheel.unlink()
  78. with zipfile.ZipFile(out_wheel, "w", zipfile.ZIP_DEFLATED) as zf:
  79. # RECORD 最后写
  80. files = [r[0] for r in rows if not r[0].endswith("/RECORD")]
  81. for rel in files:
  82. zf.write(root / rel, rel)
  83. zf.write(record_path, f"{dist_info.name}/RECORD")
  84. return out_wheel
  85. def main() -> None:
  86. ap = argparse.ArgumentParser()
  87. ap.add_argument("--wheel", required=True, type=Path)
  88. ap.add_argument("--so", required=True, type=Path)
  89. ap.add_argument("--package", required=True)
  90. ap.add_argument("--tag", required=True)
  91. ap.add_argument("--out", required=True, type=Path)
  92. a = ap.parse_args()
  93. result = repack(a.wheel, a.so, a.package, a.tag, a.out)
  94. print(f"OK -> {result}")
  95. if __name__ == "__main__":
  96. main()