e25e51c378
背景: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 每次构建改写它且不入库,进指纹会让
门禁在"刚刚构建完"的那一刻自己变红;代价是改优化等级/宏不触发本门禁,需人工
重测——已写进边界正文。
391 lines
17 KiB
Python
391 lines
17 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""体积门禁强制路径(A-5 残余,2026-10-25)。
|
||
|
||
要消除的盲区
|
||
------------
|
||
`mem-numbers` / `mem-budget` 两根闸的**事实源都是 Keil 链接产物 `.map`**
|
||
(gitignore,只有真做过 `@build` 的机器上才有)。于是两处都是"没跑 = 通过":
|
||
|
||
· CI 侧:runner 没有 Keil,workflow 只能把这根钩子**跳过**,历史上只打一条
|
||
`::warning::`(上一轮的处置)——"CI 全绿"仍不等于"体积守住了";
|
||
· 本地侧:`ram_baseline.py --gate` 在无 `.map` 时自适应 SKIP 并返回 0,所以
|
||
"改了代码但根本没编译"这条最常见的漏测路径,本地也是绿的。
|
||
|
||
本脚本把"没跑"变成"跑不过":给体积数字配一条**可提交、CI 可复核**的同码凭证
|
||
(`scripts/size_attestation.json`),而不是要求 runner 装 Keil。凭证只能由
|
||
"新鲜 `.map` + 未超线"的测量写出,因此"代码变了但没重测"会在同码性上暴露。
|
||
|
||
三个入口(退出码统一:0=过 / 1=门禁失败 / 2=环境与解析错误)
|
||
--stamp 测量后盖章(已挂在 `ram_baseline.py --update` 里,不新增人工步骤)。
|
||
三道前置任一不过就**拒绝盖章**:1) `.map` 存在且解析得出执行区与
|
||
Grand Totals;2) `.map` 不早于指纹范围内任何源码的 mtime(防"改完代码
|
||
拿旧构建盖章");3) 受管区占比 <= `ram_baseline.THRESHOLD_PCT`(与 mem-budget
|
||
同一根线、同一个常数,不另立第二套口径)。
|
||
--local 开发者自查 / ci_local 的 L0c 步:工作树指纹必须等于凭证指纹。
|
||
--ci runner 上跑(无 Keil、无 `.map`):A. 同码性;B. 凭证里记录的受管区
|
||
占比 <=92%(数字来自一次真实链接);C. 与 `ram_baseline.tsv` 末行同源=OK,
|
||
不同源=WARN(多半只 --stamp 没 --update,不拦)。
|
||
--show 打印当前指纹与凭证状态(排障用)。
|
||
|
||
口径与边界(**正文只写这一处**,其它文档引用本节,勿另抄一份)
|
||
--------------------------------------------------------------
|
||
1. 本门禁**能**判:① 这份代码有没有对应一次真实 Keil 链接(同码性);
|
||
② 那次测量的 RAM 受管区是否 <=92%(用已提交的测量数字守红线)。
|
||
2. 本门禁**不能**判:
|
||
· CI 不重算绝对体积——runner 无 Keil/无 `.map`,绝对数字的可信度来自本地那次
|
||
`@build`;本脚本把盖戳卡在"新鲜 .map + 未超线"之后,凭证不是随手可写的。
|
||
· **编译器选项不在指纹范围内**:`MDK-ARM/STM32F407-Demo.uvprojx` 每次构建都被 UV4
|
||
改写且按约定不入库(keil-build skill),把它放进指纹会让门禁在"刚刚构建完"
|
||
的那一刻自己变红;代价是改优化等级/预处理宏不触发本门禁,这类改动必须人工
|
||
重测。scatter `.sct` 与启动文件 `.s` 不受抖动影响,**在**指纹范围内。
|
||
· 防的是**遗忘与漂移**,不防**蓄意伪造**:伪造需同时改 size_attestation.json 与
|
||
ram_baseline.tsv 两个已跟踪文件,而 .map 不在库里、评审可见。
|
||
3. 强制落点:**主干 push 为红**(唯一能保证"这份代码就是要发出去的那份"的位置);
|
||
其余事件由 workflow 加 `--warn-only` 运行——分支上的代码尚未定型,在那里拦红
|
||
只会制造噪音,并诱发多条道同时抢写 scripts/ 台账。
|
||
|
||
指纹范围(= 会影响固件体积的全部受管源码)
|
||
App/ Drivers/ Src/ Inc/ Lib/ Middlewares/ test/board/ 以及 MDK-ARM 下的 .sct/.s
|
||
列表取 `git ls-files`(与 layer-deps / id-check 同口径),逐文件 sha256(路径+内容) 再
|
||
聚合;未跟踪文件与构建产物天然不在内。
|
||
|
||
用法(仓库根):
|
||
python scripts/check_size_gate.py --stamp
|
||
python scripts/check_size_gate.py --local
|
||
python scripts/check_size_gate.py --ci [--warn-only]
|
||
python scripts/check_size_gate.py --show
|
||
|
||
设计依据:docs/任务规划/测试与架构专项.md(A-5 残余)、docs/CI/README.md。
|
||
创建日期:2026-10-25
|
||
"""
|
||
|
||
import argparse
|
||
import hashlib
|
||
import json
|
||
import os
|
||
import pathlib
|
||
import subprocess
|
||
import sys
|
||
from datetime import datetime, timezone
|
||
|
||
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent))
|
||
# 复用同一套 .map 解析与阈值常数:体积口径只许有一处定义(docs/文档规范.md)
|
||
import ram_baseline # noqa: E402
|
||
import ram_report # noqa: E402
|
||
|
||
REPO_ROOT = pathlib.Path(__file__).resolve().parent.parent
|
||
ATTESTATION = REPO_ROOT / "scripts" / "size_attestation.json"
|
||
SCHEMA = 1
|
||
|
||
SIZE_SCOPE_DIRS = ("App/", "Drivers/", "Src/", "Inc/", "Lib/", "Middlewares/",
|
||
"test/board/")
|
||
SIZE_SCOPE_ROOT = "MDK-ARM/"
|
||
SIZE_SCOPE_EXTS = {".sct", ".s"}
|
||
|
||
|
||
def in_scope(rel):
|
||
"""rel 为 git 风格 posix 路径。"""
|
||
if rel.startswith(SIZE_SCOPE_DIRS):
|
||
return True
|
||
if rel.startswith(SIZE_SCOPE_ROOT):
|
||
return os.path.splitext(rel)[1].lower() in SIZE_SCOPE_EXTS
|
||
return False
|
||
|
||
|
||
def tracked_files():
|
||
"""受管文件列表(git 不可用时退回目录遍历,同 check_layer_deps.py)。"""
|
||
try:
|
||
out = subprocess.run(["git", "ls-files", "-z"], cwd=str(REPO_ROOT),
|
||
capture_output=True, check=True)
|
||
except Exception: # noqa: BLE001 git 缺失/不在仓库内
|
||
files = []
|
||
for root, dirs, names in os.walk(REPO_ROOT):
|
||
dirs[:] = [d for d in dirs if d not in (".git", ".venv", "build")]
|
||
for n in names:
|
||
rel = os.path.relpath(os.path.join(root, n), REPO_ROOT)
|
||
files.append(rel.replace(os.sep, "/"))
|
||
return sorted(files)
|
||
return sorted(p.decode("utf-8", "replace") for p in out.stdout.split(b"\0") if p)
|
||
|
||
|
||
def scoped_files():
|
||
return [f for f in tracked_files() if in_scope(f)]
|
||
|
||
|
||
def tree_fingerprint():
|
||
"""(指纹, 文件数, 缺失数)。受管但已删除的文件以哨兵入指纹,保证增删也变指纹。"""
|
||
h = hashlib.sha256()
|
||
n = miss = 0
|
||
for rel in scoped_files():
|
||
n += 1
|
||
try:
|
||
blob = (REPO_ROOT / rel).read_bytes()
|
||
except OSError:
|
||
miss += 1
|
||
blob = b"<deleted>"
|
||
h.update(rel.encode("utf-8", "surrogateescape"))
|
||
h.update(b"\0")
|
||
h.update(hashlib.sha256(blob).digest())
|
||
return h.hexdigest(), n, miss
|
||
|
||
|
||
def newest_source_mtime():
|
||
newest = 0.0
|
||
for rel in scoped_files():
|
||
try:
|
||
newest = max(newest, (REPO_ROOT / rel).stat().st_mtime)
|
||
except OSError:
|
||
continue
|
||
return newest
|
||
|
||
|
||
def measure(map_path):
|
||
""".map -> 测量数字;解析不出返回 None(调用方按环境错误处理)。"""
|
||
info = ram_baseline.collect(map_path)
|
||
if not info["regions"]:
|
||
return None
|
||
gt = ram_report.parse_map(str(map_path))["grand_totals"]
|
||
if not gt:
|
||
return None
|
||
return {
|
||
"info": info,
|
||
"sizes": {"code": gt["code"], "ro_data": gt["ro_data"],
|
||
"rw_data": gt["rw_data"], "zi_data": gt["zi_data"]},
|
||
"regions": {name: {"size": r["size"], "max": r["max"]}
|
||
for name, r in info["regions"].items()},
|
||
"pct": {name: round(ram_baseline.region_pct(r), 2)
|
||
for name, r in info["regions"].items()},
|
||
}
|
||
|
||
|
||
def budget_offenders(m):
|
||
"""受管区超阈值条目(判据与 ram_baseline 的 --gate 完全一致)。"""
|
||
bad = []
|
||
for name in ram_baseline.GATED_REGIONS:
|
||
r = m["info"]["regions"].get(name)
|
||
if not r:
|
||
bad.append("缺执行区 %s(scatter 变更?)" % name)
|
||
continue
|
||
u = ram_baseline.region_pct(r)
|
||
if u > ram_baseline.THRESHOLD_PCT:
|
||
bad.append("%s 占用 %.1f%% > %.0f%%(%s/%s B)" % (
|
||
name, u, ram_baseline.THRESHOLD_PCT,
|
||
format(r["size"], ","), format(r["max"], ",")))
|
||
return bad
|
||
|
||
|
||
def read_attestation():
|
||
if not ATTESTATION.exists():
|
||
return None, "凭证不存在(%s)" % ATTESTATION.relative_to(REPO_ROOT)
|
||
try:
|
||
doc = json.loads(ATTESTATION.read_text(encoding="utf-8"))
|
||
except (OSError, ValueError) as exc:
|
||
return None, "凭证解析失败:%s" % exc
|
||
if doc.get("schema") != SCHEMA:
|
||
return None, "凭证 schema=%r 与本脚本 %d 不符" % (doc.get("schema"), SCHEMA)
|
||
if not doc.get("fingerprint"):
|
||
return None, "凭证缺 fingerprint 字段"
|
||
return doc, None
|
||
|
||
|
||
def write_attestation(doc):
|
||
ATTESTATION.write_text(json.dumps(doc, indent=2, sort_keys=True) + "\n",
|
||
encoding="utf-8", newline="\n")
|
||
|
||
|
||
def _git_head():
|
||
try:
|
||
out = subprocess.run(["git", "rev-parse", "HEAD"], cwd=str(REPO_ROOT),
|
||
capture_output=True, timeout=20)
|
||
return out.stdout.decode("ascii", "replace").strip() or None
|
||
except Exception: # noqa: BLE001
|
||
return None
|
||
|
||
|
||
def cmd_stamp(args):
|
||
"""用新鲜 .map 盖同码戳;三道前置任一不过即拒绝(见模块 docstring)。"""
|
||
map_path = pathlib.Path(args.map)
|
||
if not map_path.exists():
|
||
print("[FAIL] 没有 .map(%s)——先 @build 再盖戳" % map_path, file=sys.stderr)
|
||
return 1
|
||
m = measure(map_path)
|
||
if m is None:
|
||
print("[FAIL] .map 解析不出执行区/Grand Totals:%s" % map_path, file=sys.stderr)
|
||
return 2
|
||
map_mt = map_path.stat().st_mtime
|
||
src_mt = newest_source_mtime()
|
||
if src_mt > map_mt:
|
||
print("[FAIL] .map 早于最新受管源码——这份构建不含最新改动,重新 @build",
|
||
file=sys.stderr)
|
||
print(" .map mtime=%d 最新源码 mtime=%d" % (int(map_mt), int(src_mt)),
|
||
file=sys.stderr)
|
||
return 1
|
||
bad = budget_offenders(m)
|
||
if bad:
|
||
print("[FAIL] 体积已超阈值,拒绝盖戳(先削峰,勿抬阈值):", file=sys.stderr)
|
||
for b in bad:
|
||
print(" - " + b, file=sys.stderr)
|
||
return 1
|
||
fp, n, miss = tree_fingerprint()
|
||
write_attestation({
|
||
"schema": SCHEMA,
|
||
"fingerprint": fp,
|
||
"fingerprint_files": n,
|
||
"fingerprint_missing": miss,
|
||
"sizes": m["sizes"],
|
||
"regions": m["regions"],
|
||
"pct": m["pct"],
|
||
"gated_regions": list(ram_baseline.GATED_REGIONS),
|
||
"threshold_pct": ram_baseline.THRESHOLD_PCT,
|
||
"map": str(map_path.relative_to(REPO_ROOT)).replace(os.sep, "/"),
|
||
"map_mtime": int(map_mt),
|
||
"built_at": datetime.now(timezone.utc).astimezone().isoformat(
|
||
timespec="seconds"),
|
||
"git_head": _git_head(),
|
||
"written_by": "scripts/check_size_gate.py --stamp",
|
||
"scope_note": "同码凭证:指纹覆盖固件体积相关源码,口径见本脚本 docstring",
|
||
})
|
||
ir = m["regions"].get("RW_IRAM1", {}).get("size", 0)
|
||
cc = m["regions"].get("RW_CCM", {}).get("size", 0)
|
||
print("[OK] 体积同码凭证已写出:%s(指纹 %s…,%d 个文件)" % (
|
||
ATTESTATION.relative_to(REPO_ROOT), fp[:12], n))
|
||
print(" Code=%s B / RW_IRAM1=%s B / RW_CCM=%s B" % (
|
||
format(m["sizes"]["code"], ","), format(ir, ","), format(cc, ",")))
|
||
return 0
|
||
|
||
|
||
def _report(args, ci_mode, lines):
|
||
"""失败输出:CI 事件加 GHA 标注;--warn-only 降级为 0(非主干不拦)。"""
|
||
if ci_mode:
|
||
tag = "::warning::" if args.warn_only else "::error::"
|
||
print(tag + " 体积门禁未通过(A-5 强制路径)", file=sys.stderr)
|
||
for ln in lines:
|
||
print(ln, file=sys.stderr)
|
||
if args.warn_only:
|
||
print("[WARN] 已按 --warn-only 降级为告警(非主干事件不拦,见 workflow)")
|
||
return 0
|
||
return 1
|
||
|
||
|
||
def _cmd_parity(args, ci_mode):
|
||
doc, err = read_attestation()
|
||
if doc is None:
|
||
return _report(args, ci_mode, [
|
||
"体积同码凭证不可用:" + err,
|
||
" 修复:本地 @build 之后跑 python scripts/ram_baseline.py --update",
|
||
" (或单独跑 python scripts/check_size_gate.py --stamp)",
|
||
])
|
||
fp, n, miss = tree_fingerprint()
|
||
problems = []
|
||
if fp != doc["fingerprint"]:
|
||
problems += [
|
||
"本次代码(指纹 %s…,%d 文件)与体积测量(指纹 %s…,%s)不同码" % (
|
||
fp[:12], n, str(doc["fingerprint"])[:12], doc.get("built_at", "?")),
|
||
" 含义:这份源码没有被真实链接测量过(或测量完又改了代码/合入了别的改动)",
|
||
" 修复:@build 0/0 之后跑 python scripts/ram_baseline.py --update 再提交",
|
||
]
|
||
if miss:
|
||
problems.append(" (注:当前树缺 %d 个受管文件,可能 checkout 不完整)" % miss)
|
||
if ci_mode:
|
||
thr = doc.get("threshold_pct", ram_baseline.THRESHOLD_PCT)
|
||
bad = []
|
||
for name in doc.get("gated_regions") or list(ram_baseline.GATED_REGIONS):
|
||
pct = (doc.get("pct") or {}).get(name)
|
||
if pct is None:
|
||
bad.append("凭证缺 %s 占比" % name)
|
||
elif float(pct) > float(thr):
|
||
size = int(((doc.get("regions") or {}).get(name) or {}).get("size", 0))
|
||
bad.append("%s 占用 %.1f%% > %.0f%%(测量 %s B)" % (
|
||
name, float(pct), float(thr), format(size, ",")))
|
||
if bad:
|
||
problems.append("CI 按已提交测量数字复核 92%% 红线:")
|
||
problems += [" - " + b for b in bad]
|
||
_tsv_consistency(doc)
|
||
if problems:
|
||
return _report(args, ci_mode, problems)
|
||
print("[OK] 体积门禁通过(%s):同码 ✓ 受管区 <=%.0f%% ✓(测量于 %s)" % (
|
||
"CI 复核:无 Keil/无 .map" if ci_mode else "本地自查",
|
||
float(doc.get("threshold_pct", ram_baseline.THRESHOLD_PCT)),
|
||
doc.get("built_at", "?")))
|
||
print(" 边界:CI 不重算绝对数字、编译器选项(uvprojx)不在指纹内——"
|
||
"正文见 scripts/check_size_gate.py docstring")
|
||
return 0
|
||
|
||
|
||
def _tsv_consistency(doc):
|
||
"""凭证数字与趋势台账末行互证(不同源只 WARN:只 --stamp 没 --update 是合法态)。"""
|
||
tsv = REPO_ROOT / "scripts" / "ram_baseline.tsv"
|
||
if not tsv.exists():
|
||
print("[WARN] ram_baseline.tsv 不存在——凭证无法与趋势台账互证")
|
||
return
|
||
try:
|
||
rows = ram_baseline.read_tsv_rows()
|
||
except Exception as exc: # noqa: BLE001
|
||
print("[WARN] ram_baseline.tsv 解析失败(%s)——跳过互证" % exc)
|
||
return
|
||
if not rows:
|
||
return
|
||
last = rows[-1]
|
||
mine = int((doc.get("sizes") or {}).get("code", -1))
|
||
theirs = int(last.get("code", -2))
|
||
if mine == theirs:
|
||
print("[OK] 凭证与 tsv 末行同源:Code=%s B(%s)" % (format(mine, ","),
|
||
last.get("date", "?")))
|
||
else:
|
||
print("[WARN] 凭证 Code=%s 与 tsv 末行 Code=%s 不同源——多半只 --stamp 没跑 "
|
||
"--update(不拦,台账随后补)" % (format(mine, ","), format(theirs, ",")))
|
||
|
||
|
||
def cmd_show(_args):
|
||
fp, n, miss = tree_fingerprint()
|
||
print("当前受管源码指纹:%s(%d 文件,缺失 %d)" % (fp, n, miss))
|
||
doc, err = read_attestation()
|
||
if doc is None:
|
||
print("凭证:%s" % err)
|
||
return 1
|
||
print("凭证指纹:%s%s" % (doc["fingerprint"],
|
||
" <- 同码" if doc["fingerprint"] == fp else " <- 不同码"))
|
||
print("凭证测量:%s / map=%s / head=%s" % (
|
||
doc.get("built_at", "?"), doc.get("map", "?"),
|
||
str(doc.get("git_head", "?"))[:8]))
|
||
print("数字:%s" % (doc.get("sizes"),))
|
||
print("占比:%s(受管区 %s,阈值 %s%%)" % (
|
||
doc.get("pct"), doc.get("gated_regions"), doc.get("threshold_pct")))
|
||
return 0
|
||
|
||
|
||
def main(argv=None):
|
||
for stream in (sys.stdout, sys.stderr):
|
||
try:
|
||
stream.reconfigure(encoding="utf-8", errors="replace")
|
||
except Exception: # noqa: BLE001
|
||
pass
|
||
ap = argparse.ArgumentParser(
|
||
description="体积门禁强制路径(A-5 残余:同码凭证 + CI 红线复核)")
|
||
g = ap.add_mutually_exclusive_group()
|
||
g.add_argument("--stamp", action="store_true", help="用新鲜 .map 盖同码戳(@build 后)")
|
||
g.add_argument("--local", action="store_true", help="本地/ci_local 自查:工作树须与测量同码")
|
||
g.add_argument("--ci", action="store_true", help="CI 模式:同码性 + 已提交数字的 92% 红线")
|
||
g.add_argument("--show", action="store_true", help="打印当前指纹与凭证状态")
|
||
ap.add_argument("--map", default=str(ram_baseline.MAP_DEFAULT),
|
||
help=".map 路径(--stamp 用)")
|
||
ap.add_argument("--warn-only", action="store_true",
|
||
help="失败降级为告警并退出 0(workflow 用于非主干事件)")
|
||
args = ap.parse_args(argv)
|
||
|
||
if args.stamp:
|
||
return cmd_stamp(args)
|
||
if args.local:
|
||
return _cmd_parity(args, ci_mode=False)
|
||
if args.ci:
|
||
return _cmd_parity(args, ci_mode=True)
|
||
if args.show:
|
||
return cmd_show(args)
|
||
ap.print_help()
|
||
return 3
|
||
|
||
|
||
if __name__ == "__main__":
|
||
sys.exit(main())
|