#!/usr/bin/env python3 """在构建宿主上生成 CHV 客机镜像的供应链 lockfile(to10 轨)。 事后校验见 check_lockfile.py(校验 BUILDINFO.versions); 本脚本是事前/事中生成端:在构建宿主上跑,读 BUILDINFO 的 tag 与 contents(仅名字清单,不含版本号),逐项从本 机 dpkg / pip 查真实版本,写出 lockfile.json: {"tag": ..., "image_sha256": ... | null, "generated_at": UTC ISO 时间, "versions": {"dpkg": ["name version", ...], "pip": ["name version", ...]}, "unresolved": ["查不到版本的名", ...]} 绝不编造版本号:查不到的进 unresolved,不猜、不填占位 版本。--verify 模式校验一份 lockfile:tag 与 BUILDINFO 一致、dpkg/pip 非空、每条都有非空版本、unresolved 为空; 任一不满足即退出非 0(unresolved 非空 = 清单不完整, 不是“生成成功”)。 用法(均在构建宿主上跑): python3 generate_lockfile.py BUILDINFO.json --output lockfile.json python3 generate_lockfile.py --verify lockfile.json \ --buildinfo BUILDINFO.json 本轨无构建宿主:脚本已落地且可跑(见回执里的 /tmp 合成 BUILDINFO 实测),真实清单待构建宿主产出,绝不把合成 数据写成生产清单。 """ import json import subprocess import sys from datetime import datetime, timezone def _dpkg_version(name): """dpkg 查版本,查不到回 None(不抛)。""" try: out = subprocess.run( ["dpkg-query", "-W", "-f=${Version}", name], capture_output=True, text=True, timeout=30) except Exception: return None if out.returncode != 0: return None version = out.stdout.strip() return version or None def _pip_version(name): """pip 查版本,查不到回 None(不抛)。""" try: out = subprocess.run( [sys.executable, "-m", "pip", "show", name], capture_output=True, text=True, timeout=30) except Exception: return None if out.returncode != 0: return None for line in out.stdout.splitlines(): if line.startswith("Version:"): version = line.split(":", 1)[1].strip() return version or None return None def generate(buildinfo_path, output_path): with open(buildinfo_path) as fh: buildinfo = json.load(fh) names = buildinfo.get("contents") or [] if not isinstance(names, list): print("BUILDINFO.contents 不是列表", file=sys.stderr) return 2 dpkg, pip, unresolved = [], [], [] for name in names: version = _dpkg_version(name) if version is not None: dpkg.append("%s %s" % (name, version)) continue version = _pip_version(name) if version is not None: pip.append("%s %s" % (name, version)) continue unresolved.append(name) lockfile = { "tag": buildinfo.get("tag"), "image_sha256": buildinfo.get("image_sha256") or buildinfo.get("sha256"), "generated_at": datetime.now(timezone.utc).isoformat(), "versions": {"dpkg": sorted(dpkg), "pip": sorted(pip)}, "unresolved": sorted(unresolved), } with open(output_path, "w") as fh: json.dump(lockfile, fh, indent=2, sort_keys=True) fh.write("\n") if unresolved: print("警告:%d 项查不到版本,已列入 unresolved:%s" % (len(unresolved), ", ".join(sorted(unresolved))), file=sys.stderr) return 0 def verify(lockfile_path, buildinfo_path): with open(lockfile_path) as fh: lockfile = json.load(fh) if buildinfo_path: with open(buildinfo_path) as fh: buildinfo = json.load(fh) if lockfile.get("tag") != buildinfo.get("tag"): print("tag 不一致:lockfile %s != BUILDINFO %s" % (lockfile.get("tag"), buildinfo.get("tag")), file=sys.stderr) return 1 versions = lockfile.get("versions") or {} for kind in ("dpkg", "pip"): entries = versions.get(kind) if not entries: print("versions.%s 为空,供应链清单缺失" % kind, file=sys.stderr) return 1 for entry in entries: parts = str(entry).split() if len(parts) < 2 or not parts[1]: print("版本为空的条目:%s %r" % (kind, entry), file=sys.stderr) return 1 unresolved = lockfile.get("unresolved") or [] if unresolved: print("清单不完整,unresolved 非空:%s" % sorted(unresolved), file=sys.stderr) return 1 print("lockfile 校验通过:tag=%s dpkg=%d pip=%d" % (lockfile.get("tag"), len(versions["dpkg"]), len(versions["pip"]))) return 0 def main(argv): if "--verify" in argv: lockfile_path = argv[argv.index("--verify") + 1] buildinfo_path = None if "--buildinfo" in argv: buildinfo_path = argv[argv.index("--buildinfo") + 1] return verify(lockfile_path, buildinfo_path) if len(argv) < 2: print("用法:generate_lockfile.py BUILDINFO.json " "--output lockfile.json | --verify lockfile.json " "[--buildinfo BUILDINFO.json]", file=sys.stderr) return 2 output_path = None if "--output" in argv: output_path = argv[argv.index("--output") + 1] if not output_path: print("缺少 --output", file=sys.stderr) return 2 return generate(argv[1], output_path) if __name__ == "__main__": raise SystemExit(main(sys.argv))