swayfx-mosaic/tooling/probes/step20c-corec-v2/assemble.py

87 lines
4.8 KiB
Python

#!/usr/bin/env python3
"""Assemble NEW private source trees; no build, install, runtime or Git mutation."""
import argparse, hashlib, io, json, os, pathlib, stat, subprocess, tarfile
HERE = pathlib.Path(__file__).resolve().parent
def run(argv, **kw):
return subprocess.check_output(argv, stderr=subprocess.PIPE, **kw)
def manifest(root):
result = {}
for p in sorted(root.rglob("*")):
if p.is_symlink():
raise ValueError(f"Unexpected symlink: {p}")
if p.is_file():
result[str(p.relative_to(root))] = {"sha256": hashlib.sha256(p.read_bytes()).hexdigest(), "mode": oct(p.stat().st_mode & 0o777)}
return result
def verify(root, provenance):
results = {}
for name, entry in provenance["components"].items():
actual = manifest(root / name)
if actual != entry["files"]:
raise ValueError(f"Source bytes/modes/file list mismatch: {name}")
results[name] = {"files": len(actual), "manifest_sha256": hashlib.sha256(json.dumps(actual, sort_keys=True).encode()).hexdigest()}
return results
def main():
ap = argparse.ArgumentParser(description=__doc__)
ap.add_argument("--project-git", type=pathlib.Path, required=True)
ap.add_argument("--scene-git", type=pathlib.Path, help="optional read-only verified bare cache; otherwise fetch exact public pin")
ap.add_argument("--destination", type=pathlib.Path, required=True)
args = ap.parse_args()
dest = args.destination.absolute()
if not dest.is_relative_to(pathlib.Path("/var/tmp")) or dest == pathlib.Path("/var/tmp"):
raise ValueError("Explicit destination strictly below /var/tmp required")
if dest.exists() or dest.is_symlink() or dest.resolve() != dest:
raise ValueError("Destination must be absent, without symlink ancestors")
if not dest.parent.is_dir():
raise ValueError("Existing destination parent required")
# All generated writes, including Git/cache/Python temp files, remain here.
os.umask(0o077)
dest.mkdir(mode=0o700)
if dest.stat().st_uid != os.getuid() or stat.S_IMODE(dest.stat().st_mode) != 0o700:
raise ValueError("Destination owner/mode mismatch")
for key, sub in [("TMPDIR", "scratch"), ("TMP", "scratch"), ("TEMP", "scratch"), ("XDG_CACHE_HOME", "cache"), ("CCACHE_DIR", "cache/ccache"), ("PYTHONPYCACHEPREFIX", "pycache"), ("XDG_DATA_HOME", "data")]:
path = dest / sub
path.mkdir(parents=True, exist_ok=True, mode=0o700)
os.environ[key] = str(path)
for key in ("SWAYSOCK", "WAYLAND_DISPLAY", "DISPLAY", "XDG_RUNTIME_DIR"):
os.environ.pop(key, None)
p = json.loads((HERE / "provenance.json").read_text())
scene_git = args.scene_git
if scene_git is None:
scene_git = dest / "scene.git"
run(["git", "init", "--bare", str(scene_git)])
run(["git", "--git-dir=" + str(scene_git), "fetch", "--depth=1", p["scene_remote"], p["components"]["scene"]["pin"]])
receipts = []
for name, entry in p["components"].items():
git = ["git", "--git-dir=" + str(scene_git)] if name == "scene" else ["git", "-C", str(args.project_git)]
pin = entry["pin"]
if run(git + ["rev-parse", pin + "^{commit}"]).decode().strip() != pin:
raise ValueError("Commit mismatch")
if run(git + ["rev-parse", pin + "^{tree}"]).decode().strip() != entry["parent_tree"]:
raise ValueError("Parent tree mismatch")
patch = HERE / entry["patch"]
if hashlib.sha256(patch.read_bytes()).hexdigest() != entry["patch_sha256"]:
raise ValueError("Patch checksum mismatch")
root = dest / name
root.mkdir(mode=0o700)
archive = run(git + ["archive", "--format=tar", pin])
with tarfile.open(fileobj=io.BytesIO(archive)) as tf:
tf.extractall(root, filter="data")
# Disable discovery of any enclosing project repository.
env = dict(os.environ, GIT_CEILING_DIRECTORIES=str(dest))
run(["git", "apply", "--check", str(patch)], cwd=root, env=env)
run(["git", "apply", str(patch)], cwd=root, env=env)
# git apply replaces changed regular files using process umask. Restore
# their pinned regular-file mode explicitly inside this private tree.
for relative in entry["changed_files"]:
(root / relative).chmod(int(entry["files"][relative]["mode"], 8))
receipts.append({"component": name, "parent_pin": pin, "parent_tree": entry["parent_tree"], "archive_sha256": hashlib.sha256(archive).hexdigest(), "patch_sha256": entry["patch_sha256"], "forward_apply_rc": 0})
result = {"identity": p["identity"], "status": "EXACT_FOR_NEW_TREE", "components": verify(dest, p), "receipts": receipts}
(dest / "assembly-receipt.json").write_text(json.dumps(result, indent=2) + "\n")
print(json.dumps(result, indent=2))
if __name__ == "__main__":
main()