#!/usr/bin/env python3 """Stage a checksum-pinned, non-V8 PDFium runtime for desktop and mobile app packaging. No downloads happen at application startup or during cargo build. Release packaging calls this explicitly; developers can target target/debug. """ import argparse import hashlib import io import json import pathlib import shutil import tarfile import urllib.request RELEASE = "chromium/8044" ASSETS = { "linux-x64": ("eb142f416aed3a72fc5a02dbd5884868a16cb99dc0cf53e6bdd64afbf67b05f4", "lib/libpdfium.so"), "linux-arm64": ("e98400ef5f005f27cfba5c14f72d464e25187298f04950de46646033cf24cef0", "lib/libpdfium.so"), "mac-arm64": ("61424884d4a7f153b808deba6437848e4400834ce30aaf95d3050da44df8f420", "lib/libpdfium.dylib"), "mac-x64": ("a93d44238e05de20028446561b951d50988b849efbbe56fe40c0d376c05b45e8", "lib/libpdfium.dylib"), "android-arm64": ("37686e64fa005484d619550a78805500de4c5a6f4df7aa06d8576145bfbee98f", "lib/libpdfium.so"), "android-x64": ("e7e3072b8ad6f36ef58db999c2d91a22fa165a904d69385f13db4537c233d736", "lib/libpdfium.so"), "ios-device-arm64": ("52be735d07b6c498ed8ce68b7f0cb5a209bbe61593c7a86c556756b80abbb938", "lib/libpdfium.dylib"), "ios-simulator-arm64": ("544537fce5257146e0df501c632d490d1aa95768737770f1912c8e23b1dd238d", "lib/libpdfium.dylib"), "ios-simulator-x64": ("8d4e33e415e29894aa98481bc9a9162a42039a1672796db14c9fed9cd545c881", "lib/libpdfium.dylib"), "win-x64": ("78a17d9a5f14467631c26a3ac8741b27a0471ecc05bd6a119b523598160a0537", "bin/pdfium.dll"), "win-arm64": ("6c9ac0ddc69edd8a18d47b95098a5b843eaed5c5bbdcb9587a18c196457449f8", "bin/pdfium.dll"), } def stage(target, destination, cache=None, licenses_destination=None): checksum, library = ASSETS[target] url = f"https://github.com/bblanchon/pdfium-binaries/releases/download/{RELEASE}/pdfium-{target}.tgz" cached = cache / f"{checksum}.tgz" if cache else None if cached and cached.is_file(): data = cached.read_bytes() else: with urllib.request.urlopen(url, timeout=60) as response: data = response.read(64 * 1024 * 1024 + 1) if hashlib.sha256(data).hexdigest() != checksum: raise ValueError("PDFium archive checksum mismatch") if cached and not cached.is_file(): cache.mkdir(parents=True, exist_ok=True) # Concurrent Xcode architecture builds must never observe a partial cache. import tempfile with tempfile.NamedTemporaryFile(dir=cache, delete=False) as temp: temp.write(data) temporary = pathlib.Path(temp.name) temporary.replace(cached) destination.mkdir(parents=True, exist_ok=True) licenses_destination = licenses_destination or destination / "pdfium-licenses" with tarfile.open(fileobj=io.BytesIO(data), mode="r:gz") as archive: members = {member.name.removeprefix("./"): member for member in archive.getmembers()} for name, member in members.items(): if name != library and not name.startswith("licenses/") and name != "LICENSE": continue if not member.isfile() or ".." in pathlib.PurePosixPath(name).parts: raise ValueError("Invalid PDFium archive member") output = ( destination / pathlib.Path(library).name if name == library else licenses_destination / name ) output.parent.mkdir(parents=True, exist_ok=True) with archive.extractfile(member) as source, output.open("wb") as sink: shutil.copyfileobj(source, sink) if not (destination / pathlib.Path(library).name).is_file(): raise ValueError("PDFium runtime missing from archive") licenses_destination.mkdir(parents=True, exist_ok=True) (licenses_destination / "build.json").write_text(json.dumps({"release": RELEASE, "sha256": checksum, "url": url}, indent=2) + "\n") if __name__ == "__main__": parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("target", choices=ASSETS) parser.add_argument("destination", type=pathlib.Path) parser.add_argument("--cache", type=pathlib.Path, help="Verified archive cache for repeated packaging builds") parser.add_argument( "--licenses-destination", type=pathlib.Path, help="place PDFium notices outside the runtime-library directory", ) args = parser.parse_args() stage(args.target, args.destination, args.cache, args.licenses_destination)