#!/usr/bin/env python3 """校验已构建镜像的 BUILDINFO 供应链清单(事后 lockfile 检查)。 事前 apt/pip 精确 lockfile 无法在本轨生成:apt 版本在构建 宿主上解析、pip 版本在客机里解析,没有构建宿主就编不出 版本号——本脚本绝不编造。构建时 build.sh 已从客机读回 dpkg + pip freeze 写进 BUILDINFO.versions(事后 lockfile), 本脚本校验它:versions 非空、每条有版本、contents 全覆盖、 tag 与预期一致、可选的冻结清单(--frozen JSON,形如 {"dpkg": ["pkg version", ...]})双向比对,漂移即退出非 0。 用法: python3 check_lockfile.py out/BUILDINFO.json [--tag TAG] [--frozen frozen.json] """ import json import sys # 非包名的内容项:版本来自 BUILDINFO 的声明字段,不是 versions 列表。 DECLARED_SOURCES = { "codex": "codex_version", "codex-code-mode-host": "codex_version", "codex-resources/bwrap": "codex_version", "/app/host.py": "repo_commit", } def _names(entries, pip_style=False): """versions 条目 → 小写名集合。dpkg 行是 "name version", pip freeze 行是 "name==version"。""" names = set() for entry in entries or []: text = str(entry) if pip_style and "==" in text: names.add(text.split("==", 1)[0].lower()) else: parts = text.split(None, 1) if parts: names.add(parts[0].lower()) return names def _check_format(kind, entries): """每条须带非空版本:dpkg "name version",pip "name==version"。""" for entry in entries or []: text = str(entry) if kind == "pip" and "==" in text: if not text.split("==", 1)[1].strip(): print("版本为空的条目:%s %r" % (kind, entry), file=sys.stderr) return False else: parts = text.split() if len(parts) < 2 or not parts[1]: print("版本为空的条目:%s %r" % (kind, entry), file=sys.stderr) return False return True def main(argv): if len(argv) < 2: print("用法:check_lockfile.py BUILDINFO.json [--tag TAG] " "[--frozen frozen.json]", file=sys.stderr) return 2 path = argv[1] tag = None frozen_path = None i = 2 while i < len(argv): if argv[i] == "--tag" and i + 1 < len(argv): tag = argv[i + 1] i += 2 elif argv[i] == "--frozen" and i + 1 < len(argv): frozen_path = argv[i + 1] i += 2 else: print("未知参数:%s" % argv[i], file=sys.stderr) return 2 with open(path) as fh: data = json.load(fh) if tag is not None and data.get("tag") != tag: print("tag 不一致:%s != %s" % (data.get("tag"), tag), file=sys.stderr) return 1 versions = data.get("versions") or {} if not versions.get("dpkg") or not versions.get("pip"): print("BUILDINFO.versions.dpkg/pip 为空,供应链清单缺失", file=sys.stderr) return 1 if not _check_format("dpkg", versions.get("dpkg")): return 1 if not _check_format("pip", versions.get("pip")): return 1 # contents 全覆盖:每项须在 dpkg/pip 名单里,或有声明字段版本。 covered = _names(versions.get("dpkg")) | _names( versions.get("pip"), pip_style=True) missing = [] for name in data.get("contents") or []: if name.lower() in covered: continue source = DECLARED_SOURCES.get(name) if source and data.get(source): continue if name.lower() == "chromium" and versions.get("chromium"): continue missing.append(name) if missing: print("contents 未覆盖:%s" % sorted(missing), file=sys.stderr) return 1 if frozen_path: with open(frozen_path) as fh: frozen = json.load(fh) for kind in ("dpkg", "pip"): want = set(frozen.get(kind) or []) got = set(versions.get(kind) or []) absent = want - got if absent: print("%s 漂移:冻结清单里有、构建产物里无:%s" % (kind, sorted(absent)), file=sys.stderr) return 1 extra = got - want if extra: print("%s 漂移:构建产物里有、冻结清单里无:%s" % (kind, sorted(extra)), file=sys.stderr) return 1 print("lockfile 校验通过:tag=%s dpkg=%d pip=%d contents=%d" % (data.get("tag"), len(versions["dpkg"]), len(versions["pip"]), len(data.get("contents") or []))) return 0 if __name__ == "__main__": raise SystemExit(main(sys.argv))