Add Phase 0 build verification gate (T-004)

This commit is contained in:
ila
2026-07-16 15:51:34 +08:00
parent 6f920eb457
commit 45c242ecec
10 changed files with 458 additions and 29 deletions
+179
View File
@@ -0,0 +1,179 @@
#!/usr/bin/env python3
"""Validate toolchain pins and Go 1.20 compatibility of core/win7 modules."""
import json
import os
import pathlib
import re
import subprocess
import sys
MAX_LEGACY_GO_VERSION = (1, 20)
def version_tuple(value):
match = re.fullmatch(r"(\d+)\.(\d+)(?:\.(\d+))?", value)
if not match:
raise ValueError("invalid Go version {!r}".format(value))
return tuple(int(part or 0) for part in match.groups())
def decode_json_stream(text):
decoder = json.JSONDecoder()
index = 0
while index < len(text):
while index < len(text) and text[index].isspace():
index += 1
if index >= len(text):
return
value, index = decoder.raw_decode(text, index)
yield value
def read_text(path):
return path.read_text(encoding="utf-8")
def go_directive(path):
match = re.search(r"(?m)^go\s+(\d+\.\d+(?:\.\d+)?)\s*$", read_text(path))
if not match:
raise ValueError("{} has no go directive".format(path))
return match.group(1)
def required_version(path, module_path):
pattern = r"(?m)^\s*{}\s+(v[^\s]+)".format(re.escape(module_path))
match = re.search(pattern, read_text(path))
if not match:
raise ValueError(
"{} does not require {}".format(path, module_path)
)
return match.group(1)
def run_go(repo_root, cwd, arguments, go_work):
environment = os.environ.copy()
environment["GOWORK"] = str(go_work) if go_work else "off"
result = subprocess.run(
["go"] + arguments,
cwd=str(cwd),
env=environment,
check=False,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
universal_newlines=True,
)
if result.returncode != 0:
print(result.stderr, file=sys.stderr, end="")
raise SystemExit(result.returncode)
return result.stdout
def validate_pins(repo_root):
expected_go_directives = {
repo_root / "go.work": "1.25.0",
repo_root / "core" / "go.mod": "1.20",
repo_root / "app-modern" / "go.mod": "1.25.0",
repo_root / "app-win7" / "go.mod": "1.20",
repo_root / "app-win7" / "go.work": "1.20",
}
violations = []
for path, expected in expected_go_directives.items():
actual = go_directive(path)
if actual != expected:
violations.append(
"{} go directive is {}, want {}".format(
path.relative_to(repo_root), actual, expected
)
)
gio_pins = {
repo_root / "app-modern" / "go.mod": "v0.10.1",
repo_root / "app-win7" / "go.mod": "v0.6.0",
}
for path, expected in gio_pins.items():
actual = required_version(path, "gioui.org")
if actual != expected:
violations.append(
"{} pins gioui.org {}, want {}".format(
path.relative_to(repo_root), actual, expected
)
)
return violations
def validate_go20_toolchain(repo_root):
output = run_go(repo_root, repo_root, ["version"], None).strip()
if "go1.20.14" not in output:
return ["compatibility scan uses {!r}, want go1.20.14".format(output)]
return []
def validate_module_versions(repo_root):
module_sets = (
("core", repo_root / "core", None),
(
"win7",
repo_root / "app-win7",
repo_root / "app-win7" / "go.work",
),
)
violations = []
checked = 0
for label, cwd, go_work in module_sets:
output = run_go(
repo_root,
cwd,
["list", "-m", "-json", "all"],
go_work,
)
for module in decode_json_stream(output):
checked += 1
go_version = module.get("GoVersion")
if not go_version:
continue
if version_tuple(go_version)[:2] > MAX_LEGACY_GO_VERSION:
violations.append(
"{} module {} declares go {}, exceeds 1.20".format(
label, module["Path"], go_version
)
)
return violations, checked
def self_check():
if version_tuple("1.20")[:2] > MAX_LEGACY_GO_VERSION:
raise AssertionError("Go 1.20 should be accepted")
if version_tuple("1.20.14")[:2] > MAX_LEGACY_GO_VERSION:
raise AssertionError("Go 1.20.14 should be accepted")
if not version_tuple("1.21")[:2] > MAX_LEGACY_GO_VERSION:
raise AssertionError("Go 1.21 should be rejected")
def main():
self_check()
repo_root = pathlib.Path(__file__).resolve().parents[1]
violations = validate_pins(repo_root)
violations.extend(validate_go20_toolchain(repo_root))
module_violations, checked = validate_module_versions(repo_root)
violations.extend(module_violations)
if violations:
for violation in violations:
print("ERROR: " + violation, file=sys.stderr)
raise SystemExit(1)
print(
"Go version check passed: pins valid; {} module records are Go 1.20-compatible.".format(
checked
)
)
if __name__ == "__main__":
main()