Skip to content

Commit d017e01

Browse files
committed
Add TurtleTerm artifact verification script
1 parent 9bb300f commit d017e01

1 file changed

Lines changed: 109 additions & 0 deletions

File tree

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
#!/usr/bin/env python3
2+
"""Verify a TurtleTerm release artifact, checksum, and manifest."""
3+
4+
from __future__ import annotations
5+
6+
import argparse
7+
import hashlib
8+
import json
9+
import tarfile
10+
from pathlib import Path
11+
12+
13+
REQUIRED_BINARIES = {
14+
"wezterm",
15+
"wezterm-gui",
16+
"wezterm-mux-server",
17+
"turtle-term",
18+
"sourceos-term",
19+
}
20+
21+
REQUIRED_TOP_LEVEL_SUFFIXES = {
22+
"LICENSE.md",
23+
"README.md",
24+
"UPSTREAM_WEZTERM_README.md",
25+
}
26+
27+
28+
def sha256(path: Path) -> str:
29+
digest = hashlib.sha256()
30+
with path.open("rb") as handle:
31+
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
32+
digest.update(chunk)
33+
return digest.hexdigest()
34+
35+
36+
def read_checksum(path: Path) -> str:
37+
text = path.read_text(encoding="utf-8").strip()
38+
return text.split()[0]
39+
40+
41+
def archive_members(path: Path) -> set[str]:
42+
with tarfile.open(path, "r:gz") as tar:
43+
return set(tar.getnames())
44+
45+
46+
def suffix_present(members: set[str], suffix: str) -> bool:
47+
return any(member.endswith("/" + suffix) or member == suffix for member in members)
48+
49+
50+
def main() -> int:
51+
parser = argparse.ArgumentParser(description="verify TurtleTerm release artifact")
52+
parser.add_argument("archive")
53+
parser.add_argument("--checksum")
54+
parser.add_argument("--manifest")
55+
args = parser.parse_args()
56+
57+
archive = Path(args.archive)
58+
checksum = Path(args.checksum) if args.checksum else archive.with_suffix(archive.suffix + ".sha256")
59+
manifest_path = Path(args.manifest) if args.manifest else Path(str(archive) + ".manifest.json")
60+
61+
if not archive.exists():
62+
raise SystemExit(f"archive missing: {archive}")
63+
if not checksum.exists():
64+
raise SystemExit(f"checksum missing: {checksum}")
65+
if not manifest_path.exists():
66+
raise SystemExit(f"manifest missing: {manifest_path}")
67+
68+
archive_hash = sha256(archive)
69+
expected_hash = read_checksum(checksum)
70+
if archive_hash != expected_hash:
71+
raise SystemExit(f"checksum mismatch: {archive_hash} != {expected_hash}")
72+
73+
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
74+
if manifest.get("schema") != "sourceos.turtle-term.release.manifest.v0":
75+
raise SystemExit("unexpected manifest schema")
76+
if manifest.get("product") != "TurtleTerm":
77+
raise SystemExit("unexpected manifest product")
78+
if manifest.get("engine") != "WezTerm":
79+
raise SystemExit("unexpected manifest engine")
80+
if manifest.get("archive") != archive.name:
81+
raise SystemExit("manifest archive name does not match")
82+
if manifest.get("archive_sha256") != archive_hash:
83+
raise SystemExit("manifest archive hash does not match")
84+
85+
binaries = set(manifest.get("binaries", []))
86+
missing_manifest_bins = REQUIRED_BINARIES - binaries
87+
if missing_manifest_bins:
88+
raise SystemExit(f"manifest missing binaries: {sorted(missing_manifest_bins)}")
89+
90+
members = archive_members(archive)
91+
for binary in REQUIRED_BINARIES:
92+
if not suffix_present(members, f"bin/{binary}"):
93+
raise SystemExit(f"archive missing binary: {binary}")
94+
95+
for suffix in REQUIRED_TOP_LEVEL_SUFFIXES:
96+
if not suffix_present(members, suffix):
97+
raise SystemExit(f"archive missing {suffix}")
98+
99+
if not suffix_present(members, "etc/turtle-term/wezterm.lua"):
100+
raise SystemExit("archive missing TurtleTerm profile")
101+
if not any("share/turtle-term/sourceos/" in member for member in members):
102+
raise SystemExit("archive missing SourceOS documentation")
103+
104+
print(f"verified {archive.name}")
105+
return 0
106+
107+
108+
if __name__ == "__main__":
109+
raise SystemExit(main())

0 commit comments

Comments
 (0)