Use one requirements writer for the wheelhouse and installed venv. Do not retry failed hashes or paused downloads. Skip pure-wheel decompression, preserve runtime library notices, and keep the current working directory off the launcher import path. Document the prerelease canary package without migration or downgrade guidance.
88 lines
3.4 KiB
Python
88 lines
3.4 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:
|
|
if not any(info.filename.endswith(".so") for info in archive.infolist()):
|
|
return 0
|
|
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
|