import argparse import hashlib import sys import tomllib import urllib.request from pathlib import Path PROJECT_ROOT = Path(__file__).resolve().parent LOCK_PATH = PROJECT_ROOT / "vendor.lock.toml" VENDOR_DIR = PROJECT_ROOT / "vendor" CHUNK_SIZE = 1024 * 1024 def load_lock(path=LOCK_PATH): """Load and validate the vendor lock file.""" with Path(path).open("rb") as stream: lock = tomllib.load(stream) if lock.get("schema_version") != 1: raise RuntimeError("Unsupported vendor.lock.toml schema") return lock def file_sha256(path): """Return the SHA-256 digest for a file.""" digest = hashlib.sha256() with Path(path).open("rb") as stream: while chunk := stream.read(CHUNK_SIZE): digest.update(chunk) return digest.hexdigest() def download_artifact(artifact, destination_dir=VENDOR_DIR): """Download one locked artifact and verify its content.""" destination_dir = Path(destination_dir) destination = destination_dir / artifact["filename"] expected = artifact["sha256"] if destination.is_file() and file_sha256(destination) == expected: print(f"Ready: {destination.name}") return destination destination_dir.mkdir(parents=True, exist_ok=True) temporary = destination.with_name(f"{destination.name}.part") temporary.unlink(missing_ok=True) print(f"Downloading: {destination.name}") request = urllib.request.Request( artifact["url"], headers={"User-Agent": "SnapForm vendor preparation"}, ) try: with urllib.request.urlopen(request) as response, temporary.open("wb") as output: while chunk := response.read(CHUNK_SIZE): output.write(chunk) actual = file_sha256(temporary) if actual != expected: raise RuntimeError( f"SHA-256 mismatch for {destination.name}: " f"expected {expected}, got {actual}" ) temporary.replace(destination) except Exception: temporary.unlink(missing_ok=True) raise return destination def prepare(platforms=None, lock_path=LOCK_PATH, destination_dir=VENDOR_DIR): """Prepare common third-party archives for selected platforms.""" lock = load_lock(lock_path) available = lock["platforms"] selected = list(available) if platforms is None else platforms unknown = sorted(set(selected) - set(available)) if unknown: raise ValueError(f"Unsupported platform: {', '.join(unknown)}") artifacts = list(lock["artifacts"].values()) artifacts.extend(available[platform] for platform in selected) return [ download_artifact(artifact, destination_dir) for artifact in artifacts ] def parse_args(argv=None): """Parse command-line options.""" parser = argparse.ArgumentParser( description="Download and verify third-party files for SnapForm.", ) parser.add_argument( "--platform", action="append", dest="platforms", help="Prepare one platform; repeat as needed. Default: all platforms.", ) return parser.parse_args(argv) def main(argv=None): """Run vendor preparation from the command line.""" args = parse_args(argv) try: paths = prepare(args.platforms) except (OSError, RuntimeError, ValueError) as error: print(f"Preparation failed: {error}", file=sys.stderr) return 1 print(f"Prepared {len(paths)} files") return 0 if __name__ == "__main__": raise SystemExit(main())