| """Prepare receipt-verified native libraries without changing their input trees.""" |
|
|
| from collections.abc import Callable |
| from dataclasses import dataclass |
| import hashlib |
| import json |
| import os |
| from pathlib import Path |
| import re |
| import shutil |
|
|
| PLUGINS = ( |
| "app", |
| "audioconvert", |
| "audioresample", |
| "coreelements", |
| "opus", |
| "rtp", |
| "rtpmanager", |
| ) |
|
|
|
|
| def required_library_paths(target: str) -> tuple[str, ...]: |
| """Libraries needed by the bindings even when no selected plugin imports them.""" |
| if target.endswith("-apple-darwin"): |
| return ("lib/libgio-2.0.0.dylib",) |
| if target.endswith("-unknown-linux-gnu"): |
| return ("lib/libgio-2.0.so.0",) |
| if target.endswith("-pc-windows-msvc"): |
| return ("bin/gio-2.0-0.dll",) |
| raise ValueError("unsupported native runtime target") |
|
|
|
|
| @dataclass(frozen=True) |
| class Binary: |
| identity: str |
| imports: tuple[str, ...] |
| rpaths: tuple[str, ...] |
|
|
|
|
| @dataclass(frozen=True) |
| class RuntimeFormat: |
| """Platform loader policy; inspect reads metadata, finalize_copy transforms or checks a verified copy.""" |
|
|
| plugins: tuple[Path, ...] |
| system_imports: frozenset[str] |
| inspect: Callable |
| finalize_copy: Callable |
| library_dir: str = "lib" |
| plugin_dir: str = "plugins" |
| required_libraries: tuple[str, ...] = () |
|
|
|
|
| def digest(path): |
| with path.open("rb") as source: |
| return hashlib.file_digest(source, "sha256").hexdigest() |
|
|
|
|
| def prepare(prefix, receipts, target, output, format): |
| prefix, receipts = prefix.resolve(strict=True), receipts.resolve(strict=True) |
| output = output.absolute() |
| if ( |
| output.exists() |
| or output.is_symlink() |
| or any( |
| parent.exists() and parent.samefile(source) |
| for parent in output.resolve().parents |
| for source in (prefix, receipts) |
| ) |
| ): |
| raise ValueError("output must be fresh and outside the inputs") |
| ci = json.loads((receipts / "ci.json").read_text()) |
| source_hash = digest(Path(__file__).with_name("sources.json")) |
| if ( |
| ci.get("target") != target |
| or ci.get("build_complete") is not True |
| or ci.get("inspection_complete") is not True |
| or ci.get("manifest_sha256") != source_hash |
| or not re.fullmatch(r"[0-9a-f]{40}", ci.get("commit", "")) |
| ): |
| raise ValueError( |
| "native build receipt does not match the pinned source inputs and target" |
| ) |
| inventory_path = receipts / "inspection/binaries.json" |
| inventory = json.loads(inventory_path.read_text()) |
| if not 1 <= len(inventory) <= 128: |
| raise ValueError("unexpected native inventory size") |
| binaries, identities, destinations = {}, {}, {} |
| for record in inventory: |
| spelling = ( |
| record["path"].replace("\\", "/") if os.name == "nt" else record["path"] |
| ) |
| relative = Path(spelling) |
| if relative.anchor or ".." in relative.parts or relative.as_posix() != spelling: |
| raise ValueError("native inventory path must be canonical and relative") |
| path = prefix / relative |
| if ( |
| path.is_symlink() |
| or not path.is_file() |
| or not path.resolve().is_relative_to(prefix) |
| ): |
| raise ValueError( |
| "native inventory entries must be regular files inside the prefix" |
| ) |
| if record["target"] != target or digest(path) != record["sha256"]: |
| raise ValueError(f"native input target/digest mismatch: {relative}") |
| metadata = format.inspect(path, target) |
| name = Path(metadata.identity).name |
| if not re.fullmatch(r"[A-Za-z0-9_+.-]+", name): |
| raise ValueError("invalid native library identity") |
| if relative in format.plugins and name != relative.name: |
| raise ValueError("explicit plugin identity must preserve its filename") |
| if ( |
| metadata.identity in identities |
| or metadata.identity in format.system_imports |
| or relative in binaries |
| ): |
| raise ValueError("duplicate or system native library identity") |
| destination = ( |
| Path( |
| format.plugin_dir if relative in format.plugins else format.library_dir |
| ) |
| / name |
| ) |
| if any( |
| destination.as_posix().casefold() == p.as_posix().casefold() |
| for p in destinations.values() |
| ): |
| raise ValueError("colliding runtime library filenames") |
| binaries[relative] = (record, metadata) |
| identities[metadata.identity] = relative |
| destinations[relative] = destination |
| libraries = {path.name: relative for relative, path in destinations.items()} |
| if any(name not in libraries for name in format.required_libraries): |
| raise ValueError("missing required native library") |
| pending = [ |
| *format.plugins, |
| *(libraries[name] for name in format.required_libraries), |
| ] |
| selected = set() |
| while pending: |
| relative = pending.pop() |
| if relative in selected: |
| continue |
| if relative not in binaries: |
| raise ValueError(f"missing explicit plugin: {relative}") |
| selected.add(relative) |
| for dependency in binaries[relative][1].imports: |
| if dependency in format.system_imports: |
| continue |
| if dependency not in identities: |
| raise ValueError(f"undeclared native dependency: {dependency}") |
| pending.append(identities[dependency]) |
| output.mkdir() |
| try: |
| records = [] |
| dependency_paths = { |
| identity: output / destinations[path] |
| for identity, path in identities.items() |
| } |
| for relative in sorted(selected): |
| record, metadata = binaries[relative] |
| destination = output / destinations[relative] |
| destination.parent.mkdir(parents=True, exist_ok=True) |
| shutil.copy2(prefix / relative, destination) |
| if ( |
| digest(destination) != record["sha256"] |
| or format.inspect(destination, target) != metadata |
| ): |
| raise ValueError("copied native input changed after verification") |
| expected = format.finalize_copy(destination, metadata, dependency_paths) |
| if format.inspect(destination, target) != expected: |
| raise ValueError( |
| "finalized loader commands did not match the private runtime layout" |
| ) |
| records.append( |
| { |
| "path": destinations[relative].as_posix(), |
| "sourcePath": relative.as_posix(), |
| "sourceSha256": record["sha256"], |
| "sha256": digest(destination), |
| "imports": list(expected.imports), |
| } |
| ) |
| manifest = { |
| "schemaVersion": 1, |
| "developmentOnly": True, |
| "target": target, |
| "sourceCommit": ci["commit"], |
| "sourceManifestSha256": source_hash, |
| "inventorySha256": digest(inventory_path), |
| "libraries": records, |
| "plugins": [destinations[path].as_posix() for path in format.plugins], |
| } |
| (output / "runtime.json").write_text( |
| json.dumps(manifest, indent=2) + "\n", encoding="utf-8" |
| ) |
| except BaseException: |
| shutil.rmtree(output) |
| raise |
|
|