feat(repack-android-wheels): implement RECORD regeneration for pycodec2 wheels to ensure installability

This commit is contained in:
Ivan 2026-05-30 14:02:56 -05:00
parent 4d7cae49a6
commit e741859c50
No known key found for this signature in database
GPG key ID: B84314E2D9332AE0
3 changed files with 66 additions and 4 deletions

View file

@ -6,6 +6,8 @@
from __future__ import annotations
import argparse
import base64
import hashlib
import re
import shutil
import sys
@ -35,6 +37,46 @@ def _find_libcodec2(vendor_dir: Path, abi_tag: str, dest_dir: Path) -> Path | No
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)
@ -48,6 +90,7 @@ def repack_pycodec2_wheel(pycodec2_wheel: Path, libcodec2_so: Path) -> bool:
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(

View file

@ -51,8 +51,15 @@ def test_repack_script_bundles_libcodec2(tmp_path):
with zipfile.ZipFile(lib_wheel, "w") as zout:
zout.writestr("chaquopy/lib/libcodec2.so", b"\x7fELF-lib")
record = (
"pycodec2/pycodec2.so,sha256=deadbeef,8\n"
"pycodec2.dist-info/METADATA,sha256=deadbeef,4\n"
"pycodec2.dist-info/RECORD,,\n"
)
with zipfile.ZipFile(py_wheel, "w") as zout:
zout.writestr("pycodec2/pycodec2.so", b"\x7fELF-mod")
zout.writestr("pycodec2.dist-info/METADATA", b"meta")
zout.writestr("pycodec2.dist-info/RECORD", record)
lib_src = tmp_path / "libcodec2.so"
lib_src.write_bytes(b"\x7fELF-lib")
@ -60,4 +67,12 @@ def test_repack_script_bundles_libcodec2(tmp_path):
with zipfile.ZipFile(py_wheel) as zin:
names = zin.namelist()
record_text = zin.read("pycodec2.dist-info/RECORD").decode()
assert "pycodec2/libcodec2.so" in names
assert ",,\n" not in record_text
for line in record_text.splitlines():
if not line.strip():
continue
size_field = line.rsplit(",", 1)[-1]
assert size_field
int(size_field)

View file

@ -313,7 +313,11 @@ async def test_discovered_interfaces_filter_works_with_ifac_network_name(temp_di
response = await handler(MagicMock())
data = json.loads(response.body)
names = [i["name"] for i in data["interfaces"]]
assert names == ["matching"]
assert data["interfaces"][0]["network_name"] == "kin.earth"
assert data["interfaces"][0]["passphrase"] == "secret"
assert len(data["interfaces"]) == 2
matching = next(i for i in data["interfaces"] if i["name"] == "matching")
non_matching = next(i for i in data["interfaces"] if i["name"] == "non-matching")
assert matching["is_allowed"] is True
assert non_matching["is_allowed"] is False
assert matching["network_name"] == "kin.earth"
assert matching["passphrase"] == "secret"