Files
hermes-agent/scripts/termux/python_linkage.py
ethernet 7606b014f5 fix(termux): complete the sealed CLI payload and verify installed startup
Stage npm, ffmpeg and its bionic runtime libraries, and static ARM ripgrep. Bind caches to actual build inputs. Generate entrypoints from the project manifest and verify real CLI/TUI startup and media conversion offline. Keep versions unchanged. Windows service work remains out of scope.
2026-09-06 14:00:42 -04:00

86 lines
3.3 KiB
Python

"""Link Python extensions explicitly for bionic's local symbol lookup."""
from __future__ import annotations
import csv
import io
import os
from pathlib import Path
import shutil
import subprocess
import tempfile
import zipfile
try:
from .retag_wheel import record_hash
except ImportError:
from retag_wheel import record_hash
def link_extension(extension: Path, library: Path) -> bool:
reader = shutil.which("llvm-readelf") or shutil.which("readelf")
if reader is None:
raise RuntimeError("a native ELF reader is required for Python linkage verification")
symbols = subprocess.check_output(
[reader, "--dyn-syms", "--wide", str(extension)], text=True,
)
if not any(
" UND " in line and line.split()[-1].startswith(("Py", "_Py"))
for line in symbols.splitlines()
):
return False
needed = subprocess.check_output(
["patchelf", "--print-needed", str(extension)], text=True,
).splitlines()
soname = subprocess.check_output(
["patchelf", "--print-soname", str(library)], text=True,
).strip()
if not soname:
raise RuntimeError(f"libpython has no SONAME: {library}")
foreign = [name for name in needed if name.startswith("libpython") and name != soname]
if foreign:
raise RuntimeError(f"{extension.name} links a different interpreter: {foreign}")
if soname in needed:
return False
subprocess.run(["patchelf", "--add-needed", soname, str(extension)], check=True)
return True
def repair_wheel(wheel: Path, library: Path, *, repair=link_extension) -> int:
"""Repair native members without extracting untrusted archive paths."""
with tempfile.TemporaryDirectory(prefix="hermes-wheel-link-") as tmp:
native = Path(tmp) / "extension.so"
with zipfile.ZipFile(wheel) as archive:
members = [(info, archive.read(info)) for info in archive.infolist() if not info.is_dir()]
records = [info.filename for info, _ in members if info.filename.endswith(".dist-info/RECORD")]
if len(records) != 1:
raise RuntimeError(f"expected one wheel RECORD: {wheel.name}")
changed = 0
updated = []
for info, data in members:
if info.filename.endswith(".so"):
native.write_bytes(data)
if repair(native, library):
data = native.read_bytes()
changed += 1
updated.append((info, data))
if not changed:
return 0
rows = [
(info.filename, record_hash(data), str(len(data)))
for info, data in updated if info.filename != records[0]
]
rows.append((records[0], "", ""))
record = io.StringIO()
csv.writer(record, lineterminator="\n").writerows(rows)
fd, staged = tempfile.mkstemp(prefix=".linked-", suffix=".whl", dir=wheel.parent)
os.close(fd)
try:
with zipfile.ZipFile(staged, "w", zipfile.ZIP_DEFLATED) as archive:
for info, data in updated:
archive.writestr(info, record.getvalue().encode() if info.filename == records[0] else data)
os.chmod(staged, wheel.stat().st_mode & 0o777)
os.replace(staged, wheel)
finally:
Path(staged).unlink(missing_ok=True)
return changed