Преглед на файлове

portal_build: 装配改为临时文件 + 原子替换, 失败不动 20MB 门户

自己踩的坑: 为空状态做验证时跑了一次 --no-claims, 它**就地改写**了 release/portal.html
(20,226,052 → 20,181,245 B, 契约结论段没了)。这文件是产物、不在 git 里, 没有 checkout 可回退 ——
一旦被就地写坏, 只能靠 products_state --on 还原产物再重装。

两处修:

1. do_build 先装到 portal.html.tmp, 成功才 replace 覆盖; 失败时 finally 清掉临时件,
   正在被网关读的门户一动不动。实测: 空状态下跑默认装配 → 退出 1 且 portal.html
   sha256 装配前后同为 9b6aabeb6ca18d15, 无 .tmp 残留。
2. 契约段生成不了时(产物被挪走, 属空状态的预期)给人话 + 三条出路(--on 还原 / --no-claims /
   rebuild_from_raw), 而不是把注入器的 FileNotFoundError 裸抛出来。

另: 顺带确认现场已修回 —— products_state --on → 重装 → sha256 9b6aabeb6ca18d15 / 20,226,052 B,
与出厂逐字节相同, 再 --off 回到空状态待人工检查。
zhouyang.xie преди 1 месец
родител
ревизия
ffa25ee220
променени са 1 файла, в които са добавени 22 реда и са изтрити 2 реда
  1. 22 2
      scripts/portal_build.py

+ 22 - 2
scripts/portal_build.py

@@ -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