MeshChatX/scripts/repack-android-pycodec2-wheels.py

154 lines
4.9 KiB
Python
Executable file

#!/usr/bin/env python3
# SPDX-License-Identifier: 0BSD
"""Bundle libcodec2.so into Chaquopy pycodec2 wheels (Android vendor dir)."""
from __future__ import annotations
import argparse
import base64
import hashlib
import re
import shutil
import sys
import tempfile
import zipfile
from pathlib import Path
def _abi_tag_from_wheel_name(name: str) -> str | None:
match = re.search(r"android_\d+_(arm64_v8a|armeabi_v7a|x86_64)", name)
return match.group(1) if match else None
def _extract_libcodec2(lib_wheel: Path, dest_dir: Path) -> Path | None:
with zipfile.ZipFile(lib_wheel) as zf:
for name in zf.namelist():
if name.endswith("chaquopy/lib/libcodec2.so"):
zf.extract(name, dest_dir)
return dest_dir / name
return None
def _find_libcodec2(vendor_dir: Path, abi_tag: str, dest_dir: Path) -> Path | None:
matches = sorted(vendor_dir.glob(f"chaquopy_libcodec2-*-android_*_{abi_tag}.whl"))
if not matches:
return None
return _extract_libcodec2(matches[-1], dest_dir)
def _urlsafe_sha256_digest(data: bytes) -> str:
return (
base64.urlsafe_b64encode(hashlib.sha256(data).digest())
.decode("ascii")
.rstrip("=")
)
def _rewrite_wheel_record(root: Path) -> None:
"""Regenerate dist-info/RECORD after wheel contents change.
Chaquopy's pip post-processor parses RECORD sizes with ``int()`` and rejects
directory placeholder lines (``path,,``). Rebuilding RECORD from file bytes
keeps repacked pycodec2 wheels installable.
"""
dist_infos = sorted(root.glob("*.dist-info"))
if not dist_infos:
return
record_path = dist_infos[0] / "RECORD"
if not record_path.is_file():
return
record_rel = record_path.relative_to(root).as_posix()
file_entries: list[tuple[str, bytes]] = []
for path in sorted(root.rglob("*")):
if not path.is_file():
continue
rel = path.relative_to(root).as_posix()
if rel == record_rel:
continue
file_entries.append((rel, path.read_bytes()))
rows = []
for rel, data in file_entries:
digest = _urlsafe_sha256_digest(data)
rows.append(f"{rel},sha256={digest},{len(data)}")
record_body = "\n".join(rows) + "\n"
record_digest = _urlsafe_sha256_digest(record_body.encode())
record_body += f"{record_rel},sha256={record_digest},{len(record_body.encode())}\n"
record_path.write_bytes(record_body.encode())
def repack_pycodec2_wheel(pycodec2_wheel: Path, libcodec2_so: Path) -> bool:
if not libcodec2_so.is_file():
print(f"Missing libcodec2.so for {pycodec2_wheel.name}", file=sys.stderr)
return False
with tempfile.TemporaryDirectory(prefix="repack-pycodec2-") as tmp_dir:
root = Path(tmp_dir)
with zipfile.ZipFile(pycodec2_wheel) as zin:
zin.extractall(root)
target = root / "pycodec2" / "libcodec2.so"
target.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(libcodec2_so, target)
_rewrite_wheel_record(root)
tmp_wheel = pycodec2_wheel.with_suffix(".repack.whl")
with zipfile.ZipFile(
tmp_wheel,
"w",
compression=zipfile.ZIP_DEFLATED,
) as zout:
for path in sorted(root.rglob("*")):
if path.is_file():
zout.write(path, path.relative_to(root).as_posix())
tmp_wheel.replace(pycodec2_wheel)
return True
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"--vendor-dir",
type=Path,
default=Path("android/vendor"),
help="Directory containing pycodec2 and chaquopy_libcodec2 wheels",
)
args = parser.parse_args()
vendor_dir = args.vendor_dir.resolve()
if not vendor_dir.is_dir():
print(f"Vendor directory not found: {vendor_dir}", file=sys.stderr)
return 1
wheels = sorted(vendor_dir.glob("pycodec2-*-android_*.whl"))
if not wheels:
print(f"No pycodec2 Android wheels under {vendor_dir}", file=sys.stderr)
return 1
ok = True
for wheel in wheels:
abi_tag = _abi_tag_from_wheel_name(wheel.name)
if not abi_tag:
print(f"Skipping unrecognized wheel name: {wheel.name}", file=sys.stderr)
ok = False
continue
with tempfile.TemporaryDirectory(prefix="libcodec2-src-") as lib_tmp:
libcodec2 = _find_libcodec2(vendor_dir, abi_tag, Path(lib_tmp))
if libcodec2 is None:
print(f"No chaquopy_libcodec2 wheel for ABI {abi_tag}", file=sys.stderr)
ok = False
continue
if not repack_pycodec2_wheel(wheel, libcodec2):
ok = False
continue
print(f"Repacked {wheel.name} with pycodec2/libcodec2.so")
return 0 if ok else 1
if __name__ == "__main__":
raise SystemExit(main())