#!/usr/bin/env python3 """生成 CHV 客机镜像的 BUILDINFO.json(build.sh 调用)。 从 build.sh 抽出(B-02),使其可独立单元测试。输入全部是 已落盘的构建产物与读回文件,不编造版本号:versions 来自 客机读回的 dpkg-query / pip freeze(Playwright=1 时另有 ms-playwright 缓存目录名作 chromium 标识);artifacts 的 sha256/size 是对产物文件实测的;image_sha256 即 disk.raw 的 sha256,供 generate_lockfile.py 沿用。 用法: python3 buildinfo.py OUT_DIR TAG UBUNTU CODEX_VERSION \\ REPO_COMMIT UBUNTU_IMAGE_SHA256 PLAYWRIGHT(0|1) [CHROMIUM_HEADLESS_SHELL_REV] CHROMIUM_HEADLESS_SHELL_REV(可选,纯数字):钉死的 ms-playwright 缓存修订号(如 1248)。PLAYWRIGHT=1 且 给了该值时,versions.chromium 里须恰好出现 chromium_headless_shell-,否则退出 1、构建失败; 未给时只记录、不强制,BUILDINFO.chromium_pinned_rev 记 null,构建输出由 build.sh 明说“未钉死”。 """ import hashlib import json import os import re import sys import time # 轻量口径(2026-11-06 用户定):Ubuntu Minimal 底座,只装 # codex + Python + SSH + cloud-init + 薄 agent + git + gt + # 最小中文字体;已裁剪 pandoc/texlive/python-docx 等文档工具 # 与 qemu-guest-agent。chv-sync 是仓库内脚本,版本随 repo_commit。 BASE_CONTENTS = [ "git", "openssh-server", "openssh-client", "cloud-init", "python3", "python3-pip", "curl", "ca-certificates", "xz-utils", "fonts-wqy-microhei", "fontconfig", "codex", "codex-code-mode-host", "codex-resources/bwrap", "/app/host.py", "gt", "chv-sync", "chv-memory", ] # 执行器默认内核 cmdline(config.py),构建产物须与它一起记录(B-12), # 否则换宿主/换执行器配置时无法复现。 KERNEL_CMDLINE = "console=hvc0 root=/dev/vda1 rw" # 浏览器装在系统路径(B-22),不是某用户的 ~/.cache:构建时 # PLAYWRIGHT_BROWSERS_PATH 指这里,agent 运行时也须设同一变量。 PLAYWRIGHT_BROWSERS_PATH = "/usr/local/share/ms-playwright" def _read_lines(path): try: with open(path) as fh: return [line.strip() for line in fh if line.strip()] except OSError: return [] def _sha256_size(path): digest = hashlib.sha256() size = 0 with open(path, "rb") as fh: for chunk in iter(lambda: fh.read(1 << 20), b""): digest.update(chunk) size += len(chunk) return digest.hexdigest(), size def build(outdir, tag, ubuntu, codex_version, repo_commit, ubuntu_image_sha256, playwright, chromium_pinned_rev=None): disk_path = os.path.join(outdir, "disk.raw") vmlinux_path = os.path.join(outdir, "vmlinux") disk_sha, disk_size = _sha256_size(disk_path) vmlinux_sha, vmlinux_size = _sha256_size(vmlinux_path) contents = list(BASE_CONTENTS) versions = { "dpkg": _read_lines( os.path.join(outdir, "guest-versions-dpkg.txt")), "pip": _read_lines( os.path.join(outdir, "guest-versions-pip.txt")), "gt": _read_lines( os.path.join(outdir, "guest-versions-gt.txt")), } gt_version = "" for line in versions["gt"]: m = re.search(r"(\d+\.\d+\.\d+)", line) if m: gt_version = m.group(1) break if playwright: contents += ["playwright", "chromium-headless-shell"] versions["chromium"] = _read_lines( os.path.join(outdir, "guest-versions-chromium.txt")) info = { "tag": tag, "ubuntu": ubuntu, "codex_version": codex_version, "repo_commit": repo_commit, "gt_version": gt_version, "built_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()), "contents": contents, "versions": versions, "ubuntu_image_sha256": ubuntu_image_sha256, "ubuntu_image_sha256_set": True, "kernel_cmdline": KERNEL_CMDLINE, "playwright": bool(playwright), "playwright_browsers_path": PLAYWRIGHT_BROWSERS_PATH if playwright else "", # feat-chv-closeup(M4):钉死的 Chromium 修订号(未设 # 记 null,绝不编一个默认值出来假装已钉死)。 "chromium_pinned_rev": chromium_pinned_rev or None, "image_sha256": disk_sha, "artifacts": { "disk.raw": {"sha256": disk_sha, "size": disk_size}, "vmlinux": {"sha256": vmlinux_sha, "size": vmlinux_size}, }, } path = os.path.join(outdir, "BUILDINFO.json") with open(path, "w") as fh: json.dump(info, fh, indent=2) fh.write("\n") return info def main(argv): if len(argv) not in (8, 9): print("用法:buildinfo.py OUT TAG UBUNTU CODEX_VERSION " "REPO_COMMIT UBUNTU_IMAGE_SHA256 PLAYWRIGHT " "[CHROMIUM_HEADLESS_SHELL_REV]", file=sys.stderr) return 2 _, outdir, tag, ubuntu, codex_version, repo_commit, \ ubuntu_image_sha256, playwright = argv[:8] chromium_pinned_rev = argv[8] if len(argv) == 9 else None if chromium_pinned_rev and not re.fullmatch(r"[0-9]+", chromium_pinned_rev): print("CHROMIUM_HEADLESS_SHELL_REV 须为纯数字:%s" % chromium_pinned_rev, file=sys.stderr) return 2 info = build(outdir, tag, ubuntu, codex_version, repo_commit, ubuntu_image_sha256, playwright == "1", chromium_pinned_rev) if not info["versions"]["dpkg"]: print("BUILDINFO versions.dpkg 为空", file=sys.stderr) return 1 if not info["versions"]["pip"]: print("BUILDINFO versions.pip 为空", file=sys.stderr) return 1 if not info["gt_version"]: print("BUILDINFO gt_version 为空(gt 未装好)", file=sys.stderr) return 1 if playwright == "1" and not info["versions"].get("chromium"): print("BUILDINFO versions.chromium 为空(浏览器未装好或" "读回路径不对)", file=sys.stderr) return 1 if playwright == "1" and chromium_pinned_rev: # feat-chv-closeup(M4):钉死校验——读回的缓存目录名 # 须恰好是该修订号,playwright 升级导致目录名变了就 # 在这里失败,不静默换浏览器版本。 expected = "chromium_headless_shell-%s" % chromium_pinned_rev if expected not in (info["versions"].get("chromium") or []): print("Chromium 修订号与钉死值不符:读回 %s,钉死 %s" % (info["versions"].get("chromium"), expected), file=sys.stderr) return 1 if info["tag"] != tag: print("BUILDINFO tag 不一致", file=sys.stderr) return 1 return 0 if __name__ == "__main__": raise SystemExit(main(sys.argv))