#!/usr/bin/env python3 # -*- coding: utf-8 -*- """两份 lv_conf.h 特性子集一致性门禁(2026-10-11 加)。 背景:`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())