c936441c83
本轮把"自研代码落在门禁之外"的口子收掉,同时落地 LVGL 特性裁剪与 L3 保护定值/元件框架。 门禁(.pre-commit-config.yaml,26 个钩子) - 新增 scripts/check_lv_conf_sync.py + 钩子 lv-conf-sync:比对两份 lv_conf.h 的**生效值**(未定义则取 lv_conf_internal.h 兜底默认), 抓 MISMATCH / LVGL 不认的宏名 / 同文件重复定义 - 顶层 exclude 删掉 simulator/:其下 21 个 tracked 文件全是本项目自研 代码,生成物(build/、SDL2/、tests/ref/、shot*)已在 .gitignore; doc-links 的 exclude 同步删除 - lv-conf-sync 的 files: 覆盖两端配置 → 只改 simulator/lv_conf.h 也触发 (pre-commit 4.x 已移除 always: true,实测只告警并跳过,无兜底开关) - 新增 prot-set-unit-test / prot-elm-unit-test 两个主机侧单测钩子 - S11(2.3.5) 判定细化:禁的是"用路径找文件",尖括号系统头带子目录 (如 <sys/stat.h>)不再误报;全仓库复核仅 2 处项目代码判定变化 模拟器代码整改(纳入钩子后暴露,全部修代码而非豁免) - cppcheck 4 处:sim_syslog.c 判空分支内解引用 p_epoch(真实 bug)、 2 处 struct tm 未初始化、1 处指针可加 const - coding-standard 61 处:44 处函数左大括号并到右括号同行、8 处补函数 注释、3 处补文件头注释块、2 处 __ 前缀保护宏改 XXX_H、2 处 sprintf 改 snprintf(写入字节数与原来完全一致) LVGL 特性裁剪(F-1/F-2/F-3) - 两端 lv_conf.h 对齐同一特性子集:删 26 个 LVGL 9.3 不存在的选项名、 3 处重复定义,10 项靠默认值隐式生效的改为显式固化 - 实测对镜像体积中性;价值在防漂移,且模拟器 golden 回归跑的正是固件 实际编译的配置 L3 软件侧 - App/algo/prot_set.* Q10 定值模型(范围校验 / 整表 all-or-nothing / 校验和封存-自检-闭锁)+ prot_elm.* 通用"滞回+定时限"元件框架 7 实例 - test/host/prot_set、test/host/prot_elm 主机侧单测;uvprojx 挂载两处 文档同步 - pre-commit使用说明:§2.8 S11 口径、§2.15 lv-conf-sync 详解、新增 §5.5 复盘(教训:exclude 只写真正不该进钩子的路径) - AGENTS.md、UI_设计架构 §1.4 第 10 条、RAM 分析、审查报告刷新与销项 验证 - @build BUILD_OK 0/0(Code=194276 RO=15872 RW=876 ZI=139892) - 模拟器重建:菜单 8 状态像素逐字节一致(池 5368 B)、11 内容页黄金参考 全过(往返 5448 B)、remo sink 0/25600 位差异、--logtest/--cfgtest PASS - 全仓库 cppcheck / coding-standard / no-mojibake --all-files 全过 - check_doc_links --all 129/0;check_mem_numbers --check 内存数字一致 - 端到端:往 simulator/lv_conf.h 注入一行漂移 → 钩子 FAIL(exit 1)
279 lines
11 KiB
Python
279 lines
11 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""两份 lv_conf.h 特性子集一致性门禁(2026-10-11 加,随 F-1/F-2/F-3 落地)。
|
||
|
||
背景:`Lib/lv_conf.h`(MCU)与 `simulator/lv_conf.h`(PC)是**两份独立文件**,
|
||
此前长期漂移——模拟器早已 I1-only,MCU 端却让 11 个 `LV_DRAW_SW_SUPPORT_*`、
|
||
5 个未用控件类、`LV_USE_THEME_SIMPLE`、`LV_USE_FLEX/GRID` 全靠
|
||
`lv_conf_internal.h` 的默认值隐式生效,把用不到的代码整份链进固件
|
||
= 实测 **64,576 B 死代码**(旧镜像的 23.5%)。更糟的是 golden 回归跑的是
|
||
模拟器那份配置,**验证不到固件实际编译的特性子集**。
|
||
|
||
判定口径(关键):比对的是**生效值**,不是"文件里写了什么"——
|
||
文件里没定义的宏,取 `Lib/lvgl/src/lv_conf_internal.h` 的默认值。
|
||
所以"MCU 没写、模拟器写了 0"会被算成 MCU=1 → 直接判 FAIL,这正是原事故形态。
|
||
|
||
四类问题:
|
||
MISMATCH 生效值不同 → FAIL(特性子集不一致,golden 回归无效)
|
||
INVALID 写了 LVGL 不认的宏名 → FAIL(如 9.3 没有 LV_USE_THEME_BASIC,写了不生效)
|
||
DUPLICATE 同一文件里定义两次 → FAIL(后者生效,极易误读)
|
||
IMPLICIT 一边靠默认值生效 → 警告(不 FAIL;若默认值与另一边不同则已升级为 MISMATCH)
|
||
|
||
用法:
|
||
python scripts/check_lv_conf_sync.py # 检查(默认)
|
||
python scripts/check_lv_conf_sync.py --list # 打印全量对照表
|
||
python scripts/check_lv_conf_sync.py --mcu A --sim B --internal C
|
||
退出码:0 = 一致;1 = 有不一致/无效/重复;2 = 环境错误(文件缺失)
|
||
"""
|
||
import argparse
|
||
import io
|
||
import os
|
||
import re
|
||
import sys
|
||
|
||
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||
DEF_MCU = os.path.join(REPO, "Lib", "lv_conf.h")
|
||
DEF_SIM = os.path.join(REPO, "simulator", "lv_conf.h")
|
||
DEF_INTERNAL = os.path.join(REPO, "Lib", "lvgl", "src", "lv_conf_internal.h")
|
||
DEF_LVGL_SRC = os.path.join(REPO, "Lib", "lvgl", "src")
|
||
|
||
RE_DEFINE = re.compile(r"^[ \t]*#[ \t]*define[ \t]+(LV_[A-Za-z0-9_]+)[ \t]*(.*)$")
|
||
RE_IFNDEF = re.compile(r"^[ \t]*#[ \t]*ifndef[ \t]+(LV_[A-Za-z0-9_]+)[ \t]*$")
|
||
RE_ANYIF = re.compile(r"^[ \t]*#[ \t]*if(def|ndef)?\b")
|
||
RE_ENDIF = re.compile(r"^[ \t]*#[ \t]*endif\b")
|
||
RE_NUM = re.compile(r"^(?:0[xX][0-9a-fA-F]+|[-+]?\d+)[uUlL]*$")
|
||
|
||
# 两份文件自己的 include 保护宏,不参与比对
|
||
IGNORE_EXACT = {"LV_CONF_H"}
|
||
|
||
# 只属于某一端的宏(前缀匹配):模拟器驱动 / 演示 / 头文件替换 / OS 抽象细节。
|
||
# 这些不参与"特性子集"一致性判定。
|
||
IGNORE_PREFIX = (
|
||
"LV_USE_SDL", "LV_SDL_", # SDL2 驱动(仅模拟器)
|
||
"LV_USE_LINUX_", "LV_USE_WINDOWS", "LV_USE_QNX", "LV_USE_UEFI",
|
||
"LV_USE_WAYLAND", "LV_USE_GLFW", "LV_USE_NUTTX", "LV_USE_OPENGLES",
|
||
"LV_USE_EVDEV", "LV_USE_LIBINPUT", "LV_USE_GENERIC_MIPI",
|
||
"LV_USE_FT81X", "LV_USE_LOVYAN_GFX",
|
||
"LV_USE_DEMO_", # 演示工程
|
||
"LV_STDINT_INCLUDE", "LV_STDDEF_INCLUDE", "LV_STDBOOL_INCLUDE",
|
||
"LV_INTTYPES_INCLUDE", "LV_LIMITS_INCLUDE", "LV_STDARG_INCLUDE",
|
||
"LV_OS_PRIORITY", "LV_OS_TASK_NAME", # LV_USE_OS=0 时惰性
|
||
"LV_FONT_CUSTOM_INCLUDE", "LV_FONT_USE_CUSTOM_INCLUDE",
|
||
)
|
||
|
||
# 允许两端不同(已核对理由):仍会打印,但不判 FAIL。
|
||
ALLOW_DIFF = {
|
||
"LV_USE_SNAPSHOT": "模拟器 --screenshot 需要 lv_snapshot;固件不用(省 Flash)",
|
||
"LV_OBJ_STYLE_CACHE": "模拟器开样式缓存便于调试;固件靠默认",
|
||
"LV_USE_OBJ_NAME": "模拟器调试辅助(对象命名);固件不需要",
|
||
"LV_USE_OBJ_ID": "模拟器调试辅助;固件不需要",
|
||
"LV_OBJ_ID_AUTO_ASSIGN": "随 LV_USE_OBJ_ID",
|
||
"LV_USE_OBJ_ID_BUILTIN": "随 LV_USE_OBJ_ID",
|
||
"LV_USE_ASSERT_NULL": "固件关断言省体积;模拟器保留有助定位",
|
||
"LV_USE_ASSERT_MALLOC": "同上",
|
||
"LV_USE_ASSERT_USER_CODE": "同上",
|
||
"LV_USE_ASSERT_OBJ": "同上",
|
||
"LV_USE_ASSERT_STYLE": "同上",
|
||
"LV_USE_ASSERT_LAYOUT": "同上",
|
||
}
|
||
|
||
|
||
def strip_comment(text):
|
||
"""去掉行内 /* */ 与 // 注释(不跨行)。"""
|
||
text = re.sub(r"/\*.*?\*/", "", text)
|
||
text = re.sub(r"//[^\"]*$", "", text)
|
||
return text.strip()
|
||
|
||
|
||
def read_lines(path):
|
||
"""按 UTF-8 读(MCU 那份带 BOM,用 utf-8-sig 兼容)。"""
|
||
with io.open(path, "r", encoding="utf-8-sig", errors="replace") as f:
|
||
return f.read().split("\n")
|
||
|
||
|
||
def parse_defines(path):
|
||
"""返回 (name -> 首个定义值, name -> 定义次数, 出现顺序)。"""
|
||
vals, counts, order = {}, {}, []
|
||
for line in read_lines(path):
|
||
m = RE_DEFINE.match(line)
|
||
if not m:
|
||
continue
|
||
name, raw = m.group(1), strip_comment(m.group(2))
|
||
counts[name] = counts.get(name, 0) + 1
|
||
if name not in vals:
|
||
vals[name] = raw
|
||
order.append(name)
|
||
return vals, counts, order
|
||
|
||
|
||
def parse_defaults(path):
|
||
"""从 lv_conf_internal.h 抽**兜底**默认值。
|
||
|
||
该文件每个选项的形状是:
|
||
#ifndef LV_X
|
||
#ifdef CONFIG_LV_X
|
||
#define LV_X CONFIG_LV_X <- ESP-IDF 兼容分支,不是默认值
|
||
#else
|
||
#define LV_X 0 <- 真正的兜底默认值
|
||
#endif
|
||
#endif
|
||
所以必须扫完整块、取**非 CONFIG_ 的字面量**(多个取最后一个)。
|
||
"""
|
||
lines = read_lines(path)
|
||
defaults = {}
|
||
i, n = 0, len(lines)
|
||
while i < n:
|
||
m = RE_IFNDEF.match(lines[i])
|
||
if not m:
|
||
i += 1
|
||
continue
|
||
name = m.group(1)
|
||
block, depth, j = [], 0, i + 1
|
||
while j < n:
|
||
line = lines[j]
|
||
if RE_ANYIF.match(line):
|
||
depth += 1
|
||
elif RE_ENDIF.match(line):
|
||
if depth == 0:
|
||
break
|
||
depth -= 1
|
||
block.append(line)
|
||
j += 1
|
||
cands = []
|
||
for line in block:
|
||
d = RE_DEFINE.match(line)
|
||
if d and d.group(1) == name:
|
||
cands.append(strip_comment(d.group(2)) or "1")
|
||
if cands:
|
||
literal = [c for c in cands if not c.startswith("CONFIG_")]
|
||
defaults[name] = literal[-1] if literal else cands[-1]
|
||
# 注意:不能跳到 j+1——lv_conf_internal.h 自身有 `#ifndef LV_CONF_INTERNAL_H`
|
||
# 大保护块,跳过去会把整份文件吞掉(实测 defaults 变空)。逐行前进,
|
||
# 让嵌套的 `#ifndef` 也能被扫到。
|
||
i += 1
|
||
return defaults
|
||
|
||
|
||
def build_symbols(src_dir):
|
||
"""扫 LVGL 头文件里的数值宏(LV_OS_NONE / LV_STDLIB_BUILTIN / LV_TXT_ENC_UTF8 等)。"""
|
||
symbols = {}
|
||
for root, _dirs, files in os.walk(src_dir):
|
||
for fn in files:
|
||
if not fn.endswith(".h"):
|
||
continue
|
||
try:
|
||
lines = read_lines(os.path.join(root, fn))
|
||
except OSError:
|
||
continue
|
||
for line in lines:
|
||
m = RE_DEFINE.match(line)
|
||
if not m:
|
||
continue
|
||
raw = strip_comment(m.group(2))
|
||
if RE_NUM.match(raw):
|
||
symbols.setdefault(m.group(1), raw)
|
||
return symbols
|
||
|
||
|
||
def normalize(value, symbols, depth=0):
|
||
"""归一化取值:去括号/整数后缀/空白,符号名解析成数值,便于跨文件比对。"""
|
||
v = (value or "").strip()
|
||
while v.startswith("(") and v.endswith(")"):
|
||
inner = v[1:-1].strip()
|
||
if not inner:
|
||
break
|
||
v = inner
|
||
v = re.sub(r"\s+", " ", v)
|
||
if RE_NUM.match(v):
|
||
v = re.sub(r"[uUlL]+$", "", v)
|
||
return str(int(v, 0))
|
||
if re.fullmatch(r"[A-Za-z_][A-Za-z0-9_]*", v):
|
||
if v in symbols and depth < 4:
|
||
return normalize(symbols[v], symbols, depth + 1)
|
||
return v
|
||
return re.sub(r"(\d)[uUlL]+\b", r"\1", v)
|
||
|
||
|
||
def main(argv=None):
|
||
ap = argparse.ArgumentParser(description="两份 lv_conf.h 特性子集一致性门禁")
|
||
ap.add_argument("--mcu", default=DEF_MCU)
|
||
ap.add_argument("--sim", default=DEF_SIM)
|
||
ap.add_argument("--internal", default=DEF_INTERNAL)
|
||
ap.add_argument("--lvgl-src", default=DEF_LVGL_SRC)
|
||
ap.add_argument("--list", action="store_true", help="打印全量对照表")
|
||
a = ap.parse_args(argv)
|
||
|
||
for p in (a.mcu, a.sim, a.internal):
|
||
if not os.path.isfile(p):
|
||
print("[ERROR] 文件不存在: %s" % p)
|
||
return 2
|
||
|
||
mcu, mcu_cnt, _ = parse_defines(a.mcu)
|
||
sim, sim_cnt, _ = parse_defines(a.sim)
|
||
defaults = parse_defaults(a.internal)
|
||
symbols = build_symbols(a.lvgl_src)
|
||
|
||
names = sorted(set(mcu) | set(sim))
|
||
checked = [n for n in names if n not in IGNORE_EXACT and not n.startswith(IGNORE_PREFIX)]
|
||
|
||
bad = []
|
||
warn = []
|
||
rows = []
|
||
for n in checked:
|
||
in_mcu, in_sim = n in mcu, n in sim
|
||
vm = mcu.get(n, defaults.get(n))
|
||
vs = sim.get(n, defaults.get(n))
|
||
if vm is None or vs is None:
|
||
bad.append(("UNKNOWN", n, "取不到生效值(MCU=%s / SIM=%s)" % (vm, vs)))
|
||
continue
|
||
nm, ns = normalize(vm, symbols), normalize(vs, symbols)
|
||
rows.append((n, "显式" if in_mcu else "默认", nm, "显式" if in_sim else "默认", ns))
|
||
if nm != ns:
|
||
if n in ALLOW_DIFF:
|
||
print("[allow] %-40s MCU=%s SIM=%s (%s)" % (n, nm, ns, ALLOW_DIFF[n]))
|
||
else:
|
||
bad.append(("MISMATCH", n, "MCU=%s(%s) vs SIM=%s(%s)"
|
||
% (nm, "显式" if in_mcu else "默认", ns, "显式" if in_sim else "默认")))
|
||
if in_mcu != in_sim:
|
||
side = "MCU" if not in_mcu else "SIM"
|
||
warn.append(("IMPLICIT", n, "%s 端未显式定义,靠 lv_conf_internal 默认 = %s"
|
||
% (side, ns if not in_mcu else nm)))
|
||
|
||
known = set(defaults) | set(symbols)
|
||
for label, vals in (("MCU", mcu), ("SIM", sim)):
|
||
for n in sorted(vals):
|
||
if n in known or n in IGNORE_EXACT or n.startswith(IGNORE_PREFIX):
|
||
continue
|
||
bad.append(("INVALID", n, "%s 端定义了 LVGL 不认的宏(写了不生效)" % label))
|
||
for label, cnt in (("MCU", mcu_cnt), ("SIM", sim_cnt)):
|
||
for n in sorted(cnt):
|
||
if cnt[n] > 1:
|
||
bad.append(("DUPLICATE", n, "%s 端定义 %d 次(生效值 = 最后一次)" % (label, cnt[n])))
|
||
|
||
if a.list:
|
||
print("%-42s %-8s %-24s %-8s %s" % ("宏", "MCU 来源", "MCU 生效值", "SIM 来源", "SIM 生效值"))
|
||
for r in rows:
|
||
print("%-42s %-8s %-24s %-8s %s" % r)
|
||
print("对照 %d 项(忽略端专属宏 %d 项)" % (len(rows), len(names) - len(checked)))
|
||
|
||
for kind, n, why in warn:
|
||
print("[warn] %-40s %s" % (n, why))
|
||
if warn:
|
||
print("-> IMPLICIT 不判失败,但默认值随 LVGL 版本变;建议显式固化")
|
||
|
||
if not bad:
|
||
print("PASS: 两份 lv_conf.h 特性子集一致(生效值比对 %d 项,警告 %d 项)"
|
||
% (len(rows), len(warn)))
|
||
return 0
|
||
|
||
print("FAIL: %d 项不一致" % len(bad))
|
||
for kind, n, why in bad:
|
||
print(" [%s] %-38s %s" % (kind, n, why))
|
||
print("-> 两端必须跑同一套特性子集,否则模拟器 golden 回归验证不到固件实际编译的配置")
|
||
print(" 规则与理由见 docs/UI设计/UI_设计架构.md §1.4 第 10 条")
|
||
return 1
|
||
|
||
|
||
if __name__ == "__main__":
|
||
sys.exit(main())
|