ci(A-5): 体积门禁补强制路径——同码凭证 + CI 无需 Keil 复核 92% 红线
背景:A-5 残余(docs/任务规划/测试与架构专项.md)说得很直白——mem-numbers 在
CI 被跳过(runner 无 Keil/无 .map),上一轮只把"跳过"声明成一条 ::warning::,
"盲区本身未消除":看到 CI 全绿仍会以为体积也守住了。
复核时本地侧同样漏:ram_baseline.py --gate 在无 .map 时自适应 SKIP 且返回 0;
mem-budget 甚至没进 CI 的 SKIP 名单——它在 runner 上以"Passed"的面貌出现,实际
什么都没检查。两处共同点是**"没跑 = 通过"**。
做法:给体积数字配一条可提交、CI 可复核的**同码凭证**,而不是要求 runner 装 Keil。
- 新增 scripts/check_size_gate.py(口径与边界的唯一正文在其模块 docstring)
· --stamp:只在三条都成立时才盖章——① .map 存在且解析得出执行区与 Grand
Totals;② .map 不早于指纹范围内任何源码的 mtime;③ 受管区占比不超过
mem-budget 用的同一常数。凭证因此不是随手可写的。
· --local:工作树指纹必须等于凭证指纹,否则 FAIL(="改了代码但体积从没测过")
· --ci:不需要 Keil 也能判的两条——同码性 + 按凭证记录的数字复核 92% 红线
· --show:排障用。指纹 = git ls-files 范围内逐文件 sha256(路径+内容) 再聚合,
覆盖 App/ Drivers/ Src/ Inc/ Lib/ Middlewares/ test/board/ 与 MDK-ARM 的 .sct/.s
- 新增 scripts/size_attestation.json:本树的凭证(由本树一次真实 @build 盖戳)
- scripts/ram_baseline.py:--update 顺手盖戳——"每次 @build 后跑 --update"是既有
纪律,不新增人工步骤;在函数内 import 防成环;盖戳失败只告警、不改 --update
返回值(新鲜度由门禁自己 fail-closed 兜)。_util/_read_tsv 提为公开名
region_pct / read_tsv_rows 供其复用(体积口径禁第二处定义);--gate 的无 .map
文案改成明说"SKIP 不等于 PASS"并指向兜底路径。
- scripts/ci_local.py:新增 L0c 体积门禁步(fail-closed、无条件照跑、约 0.3 s)
与 --size-only 开关;docstring 里把"无 .map 时跳过两根钩子"的语义补全。
- .gitea/workflows/ci.yml:L0 作业末尾加「体积门禁」步(if: always(),无需 Keil);
主干 push 拦红、其余事件 --warn-only(分支上代码尚未定型,拦红只造噪音,还会
诱发多条道同时抢写 scripts/ 台账);SKIP 名单补 mem-budget,让它诚实显示
Skipped 而不是假 Passed;文件头的"门禁盲区声明"改写为"已关闭为强制路径 +
仍守不住的三项"。run: 块全部纯 ASCII(PS 5.1 无 BOM 按 GBK 解码的实测坑)。
- scripts/README.md:登记新脚本与凭证文件,补用法行。
验证(工作树 _wt/A5-size-gate,分支 task/A5-size-gate)
- @build 0 错 0 警:Code=213780 RO=18860 RW=1204 ZI=131852——与 ram_baseline.tsv
的 2026-10-24 行逐项相同(本改动不碰固件源码,指纹不变,零体积增量)。
- 门禁实跑全 0:check-yaml / trailing-whitespace / end-of-file-fixer /
check-added-large-files / no-mojibake / id-check / doc-links / mem-budget。
- 负向 8 例全过:改凭证指纹 → --local 与 --ci 都 rc=1;凭证占比改成 95% →
--ci rc=1,加 --warn-only 则 rc=0;凭证写成非法 JSON → rc=1;用 mtime 回拨的
.map 去 --stamp → 拒绝且不动凭证;把区 max 改小到必然超线再 --stamp → 同样拒绝。
- 正向:--stamp 之后 --local / --ci 均 rc=0,并与 tsv 末行互证同源。
- --update 的盖戳联动单独验(把 TSV_PATH 与凭证指到仓库内已 gitignore 的
ci-artifacts/ 探针文件,跑完清理,本树台账未被测试污染)。
- ci_local 全量:L0.5 33 runner / 2227 断言全绿、gitlint 10/10、L0c rc=0。L0 里
test-cppcheck 与 test-coverage 各报过一次"files were modified"——单独重跑两条
均 rc=0 且工作树无改动,判定为三条道并行跑主机单测时 %TEMP% 里按套件名派生
的 obj/exe 互相踩踏(harness 产物名全机共用一个 TEMP 目录),非本改动引入。
RAM 增量:0 B(未动固件源码,凭证数字与本树一次真实链接一致)。
gate:全部通过,未使用 --no-verify。
遗留 / 需要 lead 裁定
1. 合入后主干 push 会要求"同码测量":done 流程在 @build 0/0 之后要多跑一条
python scripts/ram_baseline.py --update,并把 scripts/size_attestation.json
(数字变了则连 scripts/ram_baseline.tsv)一并提交。这就是"强制"的含义,但
确实新增半步——若你想先只在本地强制(L0c),我把 CI 那步改成 --warn-only。
2. 文档口径同步我没做(docs/任务规划/**、docs/门禁说明/**、docs/CI/README.md
归主树):A-5 销项时建议把"CI 不守体积"改为"CI 守同码性 + 红线,绝对数字仍
由本地真实构建产生",正文引用 check_size_gate.py 的模块 docstring。
3. 编译器选项(uvprojx)刻意不进指纹:UV4 每次构建改写它且不入库,进指纹会让
门禁在"刚刚构建完"的那一刻自己变红;代价是改优化等级/宏不触发本门禁,需人工
重测——已写进边界正文。
This commit is contained in:
+35
-14
@@ -103,7 +103,8 @@ def collect(map_path):
|
||||
"subsys": subsys, "top": top}
|
||||
|
||||
|
||||
def _util(r):
|
||||
def region_pct(r):
|
||||
"""区占比(%)。**公开名**:check_size_gate.py 复用同一口径,禁第二套算法。"""
|
||||
return r["size"] / r["max"] * 100 if r["max"] else 0.0
|
||||
|
||||
|
||||
@@ -127,7 +128,7 @@ def render_budget(info):
|
||||
for r in info["region_list"]:
|
||||
gated = r["name"] in GATED_REGIONS
|
||||
label = f"{r['name']} (Flash)" if r["name"] == "ER_IROM1" else r["name"]
|
||||
u = _util(r)
|
||||
u = region_pct(r)
|
||||
mark = ("PASS" if u <= THRESHOLD_PCT else "FAIL") if gated else "参考"
|
||||
L.append(f"| {label} | {r['size']:,} B | {r['max']:,} B | "
|
||||
f"{u:.1f}% | {mark} |")
|
||||
@@ -173,8 +174,11 @@ def cmd_gate(args):
|
||||
map_path = _map_or_skip(args)
|
||||
if map_path is None:
|
||||
print("[SKIP] 无 .map(未做 Keil 构建)→ RAM 预算门禁不适用")
|
||||
print(" (CI 盲区已登记 docs/任务规划/测试与架构专项.md A-5 残余;"
|
||||
"本地改过代码请先 @build)")
|
||||
print(" ⚠ **SKIP 不等于 PASS**:本钩子这一趟什么都没检查。")
|
||||
print(" A-5 残余的强制兜底(无需 Keil,改代码后必然拦红):")
|
||||
print(" 本地 scripts/ci_local.py 的 L0c 步 / CI 的「体积门禁」步,")
|
||||
print(" 都跑 scripts/check_size_gate.py(同码凭证 + 92% 红线复核)。")
|
||||
print(" 改过代码请先 @build,再 python scripts/ram_baseline.py --update。")
|
||||
return 0
|
||||
info = collect(map_path)
|
||||
if not info["regions"]:
|
||||
@@ -187,7 +191,7 @@ def cmd_gate(args):
|
||||
if not r:
|
||||
fails.append(f"缺执行区 {name}(scatter 变更?)")
|
||||
continue
|
||||
u = _util(r)
|
||||
u = region_pct(r)
|
||||
if u > THRESHOLD_PCT:
|
||||
fails.append(f"{name} 占用 {u:.1f}% > {THRESHOLD_PCT:.0f}%"
|
||||
f"({r['size']:,}/{r['max']:,} B)")
|
||||
@@ -222,7 +226,7 @@ def cmd_gate(args):
|
||||
|
||||
# 软提醒(不拦):tsv 落后 / .map 比源码旧
|
||||
if TSV_PATH.exists():
|
||||
rows = _read_tsv()
|
||||
rows = read_tsv_rows()
|
||||
ir = info["regions"].get("RW_IRAM1")
|
||||
if rows and ir and int(rows[-1]["iram1"]) != ir["size"]:
|
||||
print("[WARN] ram_baseline.tsv 末行落后于当前 .map —— "
|
||||
@@ -235,7 +239,7 @@ def cmd_gate(args):
|
||||
print("[WARN] .map 早于最新源码——数字来自旧构建,建议先 @build 再提交")
|
||||
|
||||
ir, cc = info["regions"].get("RW_IRAM1"), info["regions"].get("RW_CCM")
|
||||
print(f"RAM 预算门禁 PASS:RW_IRAM1 {_util(ir):.1f}% / RW_CCM {_util(cc):.1f}%"
|
||||
print(f"RAM 预算门禁 PASS:RW_IRAM1 {region_pct(ir):.1f}% / RW_CCM {region_pct(cc):.1f}%"
|
||||
f"(阈值 {THRESHOLD_PCT:.0f}%)")
|
||||
return 0
|
||||
|
||||
@@ -268,7 +272,8 @@ TSV_HEADER = ("date\tcode\tro\trw\tzi\ter_irom1\ter_pct\tiram1\tiram1_pct"
|
||||
"\tccm\tccm_pct\ttop_rwzi")
|
||||
|
||||
|
||||
def _read_tsv():
|
||||
def read_tsv_rows():
|
||||
"""tsv 全部行(dict 列表)。**公开名**:check_size_gate.py 用它做凭证互证。"""
|
||||
rows = []
|
||||
lines = TSV_PATH.read_text(encoding="utf-8").splitlines()
|
||||
if len(lines) < 2:
|
||||
@@ -300,9 +305,9 @@ def cmd_update(args):
|
||||
row = [
|
||||
today, str(gt["code"]), str(gt["ro_data"]), str(gt["rw_data"]),
|
||||
str(gt["zi_data"]),
|
||||
str(reg["ER_IROM1"]["size"]), f"{_util(reg['ER_IROM1']):.1f}",
|
||||
str(reg["RW_IRAM1"]["size"]), f"{_util(reg['RW_IRAM1']):.1f}",
|
||||
str(reg["RW_CCM"]["size"]), f"{_util(reg['RW_CCM']):.1f}",
|
||||
str(reg["ER_IROM1"]["size"]), f"{region_pct(reg['ER_IROM1']):.1f}",
|
||||
str(reg["RW_IRAM1"]["size"]), f"{region_pct(reg['RW_IRAM1']):.1f}",
|
||||
str(reg["RW_CCM"]["size"]), f"{region_pct(reg['RW_CCM']):.1f}",
|
||||
_top_field(info["top"]),
|
||||
]
|
||||
lines = (TSV_PATH.read_text(encoding="utf-8").splitlines()
|
||||
@@ -317,8 +322,24 @@ def cmd_update(args):
|
||||
TSV_PATH.write_text("\n".join([lines[0]] + body) + "\n",
|
||||
encoding="utf-8", newline="\n")
|
||||
print(f"已记录 {today}:RW_IRAM1 {reg['RW_IRAM1']['size']:,} B "
|
||||
f"({_util(reg['RW_IRAM1']):.1f}%) / RW_CCM {reg['RW_CCM']['size']:,} B "
|
||||
f"({_util(reg['RW_CCM']):.1f}%) → {TSV_PATH.relative_to(REPO_ROOT)}")
|
||||
f"({region_pct(reg['RW_IRAM1']):.1f}%) / RW_CCM {reg['RW_CCM']['size']:,} B "
|
||||
f"({region_pct(reg['RW_CCM']):.1f}%) → {TSV_PATH.relative_to(REPO_ROOT)}")
|
||||
|
||||
# A-5 残余(2026-10-25):同一次「真实测量」顺带刷新**体积同码凭证**
|
||||
# scripts/size_attestation.json——这样「每次 @build 后跑 --update」这条**既有
|
||||
# 纪律**同时喂饱那条无需 Keil 也能判的强制门禁(check_size_gate --ci /
|
||||
# --local,即 ci_local 的 L0c 步与 CI 的体积门禁步),不新增人工步骤。
|
||||
# 盖戳失败只告警、不改本命令返回值:凭证新鲜度由门禁自己 fail-closed 兜,
|
||||
# 失败原因(.map 比源码旧 / 已超线)会在门禁那一侧明确报出来。
|
||||
# 为什么在函数内 import:check_size_gate 模块级 import 本模块,反过来即成环。
|
||||
try:
|
||||
from argparse import Namespace
|
||||
|
||||
import check_size_gate
|
||||
check_size_gate.cmd_stamp(Namespace(map=str(map_path)))
|
||||
except Exception as exc: # noqa: BLE001
|
||||
print(f"[WARN] 体积同码凭证未刷新:{exc}")
|
||||
print(" 单独补跑:python scripts/check_size_gate.py --stamp")
|
||||
return 0
|
||||
|
||||
|
||||
@@ -326,7 +347,7 @@ def cmd_trend(args):
|
||||
if not TSV_PATH.exists():
|
||||
print("[WARN] tsv 不存在——先跑 --update 建档")
|
||||
return 0
|
||||
rows = _read_tsv()
|
||||
rows = read_tsv_rows()
|
||||
if not rows:
|
||||
print("[WARN] tsv 为空")
|
||||
return 0
|
||||
|
||||
Reference in New Issue
Block a user