75 lines
2.8 KiB
Python
75 lines
2.8 KiB
Python
#!/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()
|