|
|
@@ -168,7 +168,18 @@ def assemble(out: pathlib.Path, claims: bool = True) -> tuple[str, dict]:
|
|
|
spec = importlib.util.spec_from_file_location('_portal_inject', inj)
|
|
|
mod = importlib.util.module_from_spec(spec)
|
|
|
spec.loader.exec_module(mod)
|
|
|
- rep['claims_sha'] = mod.inject(out, out)[:16]
|
|
|
+ try:
|
|
|
+ rep['claims_sha'] = mod.inject(out, out)[:16]
|
|
|
+ except FileNotFoundError as e:
|
|
|
+ # 产物被挪走时 (scripts/products_state.py --off) 契约派生物不在 —— 这不是程序坏了:
|
|
|
+ # 契约段的来源就是产物, 产物不在则内容不在。给一句人话, 别丢裸 traceback。
|
|
|
+ raise SystemExit(
|
|
|
+ f'契约结论段生成不了: 缺产物 {getattr(e, "filename", "?")}\n'
|
|
|
+ f' 原因: 门户正文的结论段由事实契约派生物渲染, 产物被挪走时它自然不在 '
|
|
|
+ f'(空状态属预期)。\n'
|
|
|
+ f' 三选一: ① scripts/products_state.py --on 还原产物后重跑;\n'
|
|
|
+ f' ② 加 --no-claims 只装外壳与内嵌件 (装出的门户会缺这一节);\n'
|
|
|
+ f' ③ 先 scripts/rebuild_from_raw.py 重新产出。')
|
|
|
else:
|
|
|
rep['claims_sha'] = '(无注入器, 跳过)'
|
|
|
b = out.read_bytes()
|
|
|
@@ -179,7 +190,16 @@ def assemble(out: pathlib.Path, claims: bool = True) -> tuple[str, dict]:
|
|
|
|
|
|
|
|
|
def do_build(claims=True) -> int:
|
|
|
- h, rep = assemble(PORTAL, claims)
|
|
|
+ # 先装到临时文件, 成功后再原子替换 —— 否则一旦中途失败 (产物不在 → 契约段生成不了),
|
|
|
+ # 就地写坏的就是正在被网关读的那个 20 MB 门户, 而且它不在 git 里, 没法 checkout 回来
|
|
|
+ # (2026-09-11 实测踩到: --no-claims 直接把 release/portal.html 换成了缺契约段的版本)。
|
|
|
+ tmp = PORTAL.with_name(PORTAL.name + '.tmp')
|
|
|
+ try:
|
|
|
+ h, rep = assemble(tmp, claims)
|
|
|
+ tmp.replace(PORTAL)
|
|
|
+ finally:
|
|
|
+ if tmp.exists():
|
|
|
+ tmp.unlink()
|
|
|
print(f'装配完成 → {P.rel(PORTAL)} sha256 {h[:16]} '
|
|
|
f'(内嵌件 {rep["templates"]} 个, 资料索引 {rep["sources"]} 处, claims {rep.get("claims_sha")})')
|
|
|
return 0
|