#!/usr/bin/env python3 """Export exact committed offline-bootstrap tooling; never download anything.""" import argparse import hashlib import json import pathlib import re import subprocess SOURCE = "https://code.fritzlab.net/action/image-build" FILES = { "tools/private-modules/main.go": "tools/private-modules.go", "tools/private-modules/main_test.go": "tools/private-modules_test.go", } def committed(root, *args): return subprocess.check_output(["git", "-C", str(root), *args]) def export(root, target, revision, check=False): if not re.fullmatch(r"[0-9a-f]{40}", revision): raise ValueError("source revision must be a full Git commit ID") resolved = committed(root, "rev-parse", revision + "^{commit}").decode().strip() if resolved != revision: raise ValueError("source revision is not a commit") result = {} entries = [] for source, destination in FILES.items(): tree = committed(root, "ls-tree", revision, "--", source).decode() if not tree.startswith("100644 blob ") or not tree.endswith("\t" + source + "\n"): raise ValueError("source must be one regular committed file") data = committed(root, "show", revision + ":" + source) if not data or len(data) > 128 * 1024: raise ValueError("source exceeds its bounded export") result[destination] = data entries.append({"source": source, "destination": destination, "sha256": hashlib.sha256(data).hexdigest()}) result["tools/private-modules.source.json"] = (json.dumps({ "version": 1, "repository": SOURCE, "revision": revision, "files": entries, }, indent=2) + "\n").encode() result["tools/private-modules.sha256"] = "".join( hashlib.sha256(data).hexdigest() + " " + path + "\n" for path, data in sorted(result.items()) ).encode() target = target.resolve(strict=True) tools_dir = target / "tools" if tools_dir.is_symlink(): raise ValueError("target tools directory must not be a symlink") if not check: tools_dir.mkdir(exist_ok=True) for path, data in result.items(): destination = target / path if destination.is_symlink(): raise ValueError("target file must not be a symlink") if check: if destination.read_bytes() != data: raise ValueError("export differs from pinned canonical source: " + path) else: destination.write_bytes(data) def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("target", type=pathlib.Path) parser.add_argument("--revision", required=True) parser.add_argument("--check", action="store_true") args = parser.parse_args() export(pathlib.Path(__file__).resolve().parents[1], args.target, args.revision, args.check) if __name__ == "__main__": main()