R12 复测 2 钉死:剩下的 226 只点名 /home/chv/workspace——工作区由 cloud-final 的 chv-sync boot 克隆出来,晚于 multi-user.target 约 9 秒,worker 的 5 次启动预算在工作区出现前耗尽(5 台 VM 2 台 failed,手工 reset-failed + start 后立即 active,二进制无辜)。 修法三选一,选 (a)——让服务根本不在工作区出现前启动: 新单元 chv-gitea-worker.path(PathExists=/home/chv/workspace, 工作区一出现即触发启动)+ 服务加 ConditionPathExists=/home/chv/workspace(开机那次启动在命名空间 搭建之前干净跳过,不 226、不耗重启预算)。两者缺一不可: Condition 本身绝不重试,没有 .path 它就是一次跳过、服务永远不 起;只有 .path 而无 Condition,开机那次启动仍会在命名空间搭建 时 226 撞上限速。WantedBy=multi-user.target 保留(is-enabled 仍 为 enabled),runcmd 只启动 .path 单元(cloud-final 时工作区尚 未 clone,直接 start 服务会 226);.path 进 buildinfo contents 与两处 lockfile 登记,P6 预检由三件扩为四件。 不用 (b) 放宽 StartLimit:放宽只是把竞态窗口拉大,wake 时间在 负载下已翻倍(R12 复测 2),预算仍可能耗尽,仍是时序赌博。 不用 (c) 把工作区创建提前到 tmpfiles-setup 之前:/home/chv 本身 07:23:40 才出现(cloud-init 建 chv 用户),晚于 tmpfiles-setup (07:23:24);硬约束:绝不盲目补 tmpfiles 行。ExecStartPre 也不 行——命名空间搭建在它之前,救不了 226。 测试:WorkerUnitBootRaceTest 新增 8 项——Condition 在位、 StartLimit 未放宽(修法语义钉)、.path 触发配对(PathExists + WantedBy、无 Unit= 覆盖)、服务 WantedBy 保留、build.sh 启用 .path + runcmd 只起 .path、绝无工作区 tmpfiles 行、buildinfo 与 lockfile 登记;BuildInfoContentsTest 的 P6 件加 .path;P2/P4 干 跑夹具一并拷入 .path。P6/P2/P4 三模块 174 项全过。
182 lines
7.1 KiB
Python
182 lines
7.1 KiB
Python
#!/usr/bin/env python3
|
||
"""校验已构建镜像的 BUILDINFO 供应链清单(事后 lockfile 检查)。
|
||
|
||
生成端见 generate_lockfile.py(读 BUILDINFO.versions 生成
|
||
lockfile.json,版本唯一来源是 build.sh 从客机读回的
|
||
dpkg/pip freeze + chromium 目录名 + codex/repo_commit/gt
|
||
声明字段,绝不查构建宿主现场、绝不编造版本号)。
|
||
本脚本校验 BUILDINFO:versions 非空、每条有版本、contents
|
||
全覆盖、tag 与预期一致、钉死的 Chromium 修订号一致、
|
||
可选的冻结清单(--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",
|
||
"gt": "gt_version",
|
||
"chv-sync": "repo_commit",
|
||
# 对齐 Muse P2:nspawn 单元启动器 + broker + 浏览器子代理,
|
||
# 均是仓库内脚本,版本随 repo_commit(与 chv-sync 同款)。
|
||
"chv-cell": "repo_commit",
|
||
"chv-broker": "repo_commit",
|
||
"chv-browser": "repo_commit",
|
||
# 对齐 Muse P3 + P1 vsock:authd 三件套、authd/sentinel
|
||
# 叶子模块与 vsock 通知脚本,均是仓库内脚本,版本随
|
||
# repo_commit。缺了它们 check 会报 contents 未覆盖(717efc4
|
||
# 真机实测:6 项 unresolved,verify 与 check 双双失败)。
|
||
# round-11 真机修复(层 1):叶子树新增 errors.py 与 stub
|
||
# __init__.py(authd_init_stub.py),同款 repo_commit 登记。
|
||
"chv-authd": "repo_commit",
|
||
"git-credential-chv-authd": "repo_commit",
|
||
"chv-authd.service": "repo_commit",
|
||
"cloud/chv/authd.py": "repo_commit",
|
||
"cloud/chv/sentinel.py": "repo_commit",
|
||
"cloud/chv/errors.py": "repo_commit",
|
||
"cloud/chv/__init__.py": "repo_commit",
|
||
"vsock_notify": "repo_commit",
|
||
# 对齐 Muse P4:CDP broker 三件在 contents 里,登记自 P4 起
|
||
# 一直缺失;本轮补上,否则新镜像的 lockfile 仍 unresolved。
|
||
"chv-cdp-broker": "repo_commit",
|
||
"chv-browser-guard": "repo_commit",
|
||
"browser-blocklist": "repo_commit",
|
||
# 对齐 Muse P6:worker 四件(+ 路径触发器)+ 叶子模块(privsep/safety)。
|
||
"chv-gitea-worker": "repo_commit",
|
||
"chv-sync-worker-client": "repo_commit",
|
||
"chv-gitea-worker.service": "repo_commit",
|
||
"chv-gitea-worker.path": "repo_commit",
|
||
"cloud/chv/privsep.py": "repo_commit",
|
||
"cloud/chv/checkout_detect.py": "repo_commit",
|
||
"cloud/chv/safety.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() in ("chromium", "chromium-headless-shell") \
|
||
and versions.get("chromium"):
|
||
continue
|
||
missing.append(name)
|
||
if missing:
|
||
print("contents 未覆盖:%s" % sorted(missing), file=sys.stderr)
|
||
return 1
|
||
# feat-chv-closeup(M4):钉死的 Chromium 修订号须与读回
|
||
# 的缓存目录名一致;未钉(pinned_rev 为空)时不校验。
|
||
pinned = data.get("chromium_pinned_rev")
|
||
if pinned:
|
||
expected = "chromium_headless_shell-%s" % pinned
|
||
if expected not in (versions.get("chromium") or []):
|
||
print("Chromium 修订号与钉死值不符:读回 %s,钉死 %s"
|
||
% (versions.get("chromium"), expected),
|
||
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))
|