feat-chv-to10:往 10 分方向提升(类型/lint 清零、worker 适配抽取、审计落盘、lockfile 生成脚本、前端四态与统一文案、就绪清单 §13)
This commit is contained in:
1 parent
df8c64bd64
commit
2f3ff8b3ec
22 files changed
+1400
-282
No files matched your search
@@ -0,0 +1,160 @@
|
||||
#!/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))
|
||||
Reference in new issue
Block a user