dafc18f5ed
启动 docs/任务规划/README.md 的 W6 UI 扩展簇(用户裁定,覆盖 2026-10-11
的"本轮不排期")。四个工作包,全部验证通过。
## U1 设计令牌层(零行为变化)
新建 Drivers/BSP/UI/ui_tokens.h(20 个宏),框架 + 7 个页面 + App/task/
ui_task.c 全部接入。屏幕几何**复用 ui_port.h 既有 UI_SCREEN_W/H**,不重复
定义;令牌头自身只 include ui_port.h,不把 LVGL 拖进全部消费者。
补收了报告没列但代码里真重复的量:UI_TEXT_BAND_H 15(7 次)、列表左右边界
与每页行数(各 4 个页面逐字复制)、推流协议几何与显示几何同源(此前硬编码
3200 且注释写着"须与 UI_FB_SIZE 一致"——典型漂移隐患)、UI_TASK_TICK_MS
(ui_port_step 与 osDelay 必须同值,原先靠注释维系)。
故意不收页面私有几何与"同形不同义"的值(device_info 的 pitch=15 与列表
UI_ROW_PITCH=14 合并即改像素)。
## U3 页面注册表与生成器(删手工分发)
ui_page_registry.{c,h} 由生成器产出,**手工 menu_content_page_for() 已删
除**(仓库内仅存 2 处历史说明注释)。密码页按代码实况**单独出口**
(ui_page_registry_password())——它非节点绑定,任何 #P 叶子进同一页。
顺带落地此前判定"U1 阶段做不了"的共享 flat 样式 ui_style.{c,h}。
生成器除产注册表外**自动把新 .c 写进 uvprojx 与 CMakeLists.txt**,且
**双向对账**:加页打印 registered、删页打印 pruned,不留死条目;并双向
校验(#page 找不到实例报错、实例未被任何 #page 引用也报错)。
## U4 门禁补强(35 个钩子)
新增 font-charset 钩子(scripts/check_font_charset.py):复用
tools/font_gen 的同一套提取与 cmap 解析规则(importlib 直载,零第二套
规则),A 段查源码在用 vs 字符集、B 段查字符集 vs 字体 cmap。
registration-check 补第 ④ 类:实测原规则对"进了两端构建但没接描述符表"
的页面**全绿**——desc 被链接器剥离、编译链接全过、菜单永远打不开的死代码。
仍照不出的两项已如实写进文档(枚举漏加归 @build、菜单树漏挂归 golden)。
## 字体子集瘦身(D 案,4 留 3 删)
ui_cn_chars.txt 122→119 字(恰删 上/电/重,零新增)。异/常/片/芯 进
KEEP_EXTRA 带理由白名单:它们曾在 9eb1bd63 被写进中文自检描述落盘
/log/selftest.log(定长 64B 记录跨固件持久),删字形会使历史故障记录
渲染空白且不可逆。保留字按**必需**处理,反向有牙——谁只跑 extract_cn
把它们静默洗掉,钩子直接 FAIL 并逐字打印理由。
--strict 口径为"未知冗余 = 0"而非"冗余 = 0"。
## 验证
@build 0 错 0 警;ER_IROM1 232,052 B(较 HEAD −228 B)、RW_IRAM1
75,232 B **零增量**、RW_CCM 87.9% 未变。golden:菜单 8 状态全像素一致 +
内容页 11 页逐字节一致,**参考图 sha256 差异 0 条**(未用 --update 刷
基线);池水位菜单基线 5368 B、开页-关页往返 5448 B 与 U1 时相同。
38 个在途修改文件逐个核对 BOM/CRLF/U+FFFD 全部与 HEAD 一致。
L0 33 钩子 + L0.5 16 runner / 1104 断言全绿;check_doc_links --all
142 篇、无法判定 0;mem-budget / mem-numbers 已回填。
## 单位成本实测(U5 排期依据)
临时页 zzprobe 绑到"遥信量"叶子 → 只跑生成器 → 带页 @build 0/0 →
注册表出现 {5u, &ui_page_zzprobe_desc} → 回滚后 6 个产出文件 zzprobe
残留计数均 0、重编回基线。
**结论:新增一页 = 3 处人工(新 .c + 新 .h + 菜单树那一行加
#page=<名>)+ 1 条生成器命令,手工编辑注册文件 0 处。**
据此修正报告原口径"8 处降到 1 处"——那个 1 只在"1 处对既有文件的修改"
意义下成立,页面自身 2 个文件本就要写,真实处数是 3。
## 文档同步与两处自我修正
§0.1 UI 行 B → B+;§6.1「内容页」「设计令牌」两行按实况回填;§6.5 U1/U3
状态更新;§6 顶部加"建议栏不可照抄"总警示(本轮四次证伪:ui_port.h 已有
同名宏、LV_COLOR_BLACK/WHITE 不存在且极性反了、27 页实为目标页数现存 7
个、生成器草图漏密码页非节点绑定)。
两处改口:① §6.5 U4 从我上一版**误写的 3/3 改回 2/3**——"golden 脚本早已
存在"不等于"已进自动门禁",两个 golden 脚本至今未挂 pre-commit,该缺口
单列为待办;② 原计划"分三笔提交"**作废**,U1 与 U3 改同一批文件、文件内
交叠,硬切会产生不可编译的中间态,故整波一笔。
386 lines
19 KiB
Python
386 lines
19 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""check_registration.py - 双端注册反向校验门禁(纯标准库)。
|
||
|
||
背景(登记簿 N-23,出处审查报告 N-06 残余):
|
||
本工程 Keil 工程与 PC 模拟器都用**显式文件列表**(无通配扫描)。新 `.c`
|
||
必须手动注册两处才会被编译:
|
||
① `MDK-ARM/STM32F407-Demo.uvprojx` 的 `<FilePath>` 条目(MCU 固件构建)
|
||
② `simulator/CMakeLists.txt` 的显式源列表(PC 模拟器构建)
|
||
历史上 `App/task/rs485_task.c`、`Drivers/BSP/NET/lftpd/lftpd_log.c` **两处都没注册**
|
||
= 写了却从不参与任何一端编译的**死代码**,误导读者以为功能已生效。
|
||
|
||
本脚本做什么(四类缺口):
|
||
扫描 `App/`、`Drivers/BSP/`、`Src/` 下全部 `.c`(排除第三方),对照两份注册表:
|
||
① 存在但未进 uvprojx -> 固件不编译(死代码 / 漏注册)
|
||
② 存在但未进 CMakeLists -> 模拟器不编译(**仅当该文件本应进模拟器时**才是缺口;
|
||
MCU-only 文件不进模拟器是**正确**的,见下方豁免)
|
||
③ 注册了但文件不存在(反向)-> 幽灵注册(改名/删除后残留的注册条目)
|
||
另对 UI 页面模块(U4 门禁补强,2026-10-15,报告 §6.5):
|
||
④ `Drivers/BSP/UI/ui_page_*.c` 定义了 `ui_page_<name>_desc`,但该符号在 UI 目录内
|
||
除自身 .c/.h 外无任何引用 -> 页面未接入
|
||
`ui_content_page.c` 的 `s_defs[]` 描述符表:两端**编译
|
||
链接全过**(desc 无人引用,链接器整段剥离),菜单永远
|
||
打不开这一页 = 又一形态的"写了却从不生效"死代码。
|
||
①②③ 对此完全照不出(2026-10-15 实测),故补本类;
|
||
即"新增 UI 页面必须过 registration-check"的机械口径。
|
||
|
||
豁免机制(关键):
|
||
模拟器只建模 **UI + 网络配置共享核心**;其余模块(MCU 驱动 / 固件任务 / 纯算法 /
|
||
CubeMX 生成层)**不进模拟器是设计事实**,模拟器另有 `_sim` 替身或根本无对应。
|
||
豁免用**显式清单 + 每行理由注释**(命名模式 `*_mcu.c` + 个别点名),
|
||
**不做整目录一刀切**(N-23 教训正是"目录级盲区")。唯一例外是 `Src/`:
|
||
CubeMX 生成的外设初始化/中断/main,按构造即 MCU 专属、不可能有模拟器替身,
|
||
故整目录豁免(任务口径明确认可"Src/ 全部")。
|
||
|
||
已知缺口过渡(存量不拦、只对新缺口 FAIL):
|
||
`KNOWN_GAPS` 登记存量缺口(N-23 存量,Lead 处置中):命中 -> 打印 `[WARN]` 不拦;
|
||
新出现的缺口 -> 打印 `[GAP]` 并 FAIL。`--strict` 关闭白名单(存量也 FAIL),供审计。
|
||
|
||
退出码:
|
||
0 = 一致(无新缺口;存量缺口已白名单 WARN)
|
||
1 = 有缺口(存在未白名单的新缺口或幽灵注册)
|
||
2 = 脚本环境错误(注册表文件缺失 / 无法读取)
|
||
|
||
用法(仓库根目录):
|
||
& '.venv\\Scripts\\python.exe' scripts/check_registration.py
|
||
& '.venv\\Scripts\\python.exe' scripts/check_registration.py --strict
|
||
|
||
设计依据:docs/任务规划/登记簿.md N-23;.dsh/skills/new-source-file
|
||
创建日期:2026-10-12
|
||
"""
|
||
|
||
import argparse
|
||
import fnmatch
|
||
import os
|
||
import re
|
||
import sys
|
||
|
||
# --- 仓库根(scripts/ 的上一级)---
|
||
REPO_ROOT = os.path.normpath(os.path.join(os.path.dirname(os.path.abspath(__file__)), ".."))
|
||
UVPROJX = os.path.join(REPO_ROOT, "MDK-ARM", "STM32F407-Demo.uvprojx")
|
||
CMAKELISTS = os.path.join(REPO_ROOT, "simulator", "CMakeLists.txt")
|
||
SIM_DIR = os.path.join(REPO_ROOT, "simulator")
|
||
|
||
# --- 扫描范围与第三方排除 ---
|
||
SCAN_ROOTS = ["App", "Drivers/BSP", "Src"]
|
||
# 第三方目录(即便落在扫描根下也跳过)
|
||
THIRD_PARTY = [
|
||
"Drivers/STM32F4xx_HAL_Driver",
|
||
"Drivers/CMSIS",
|
||
"Middlewares",
|
||
"Lib",
|
||
]
|
||
|
||
# --- 模拟器豁免清单(category ②:不进模拟器是**正确**的)---
|
||
# 每项 = (fnmatch 模式(对仓库相对 posix 路径匹配), 理由)。
|
||
# 原则:命名模式 + 个别点名,不整目录一刀切;唯一目录级豁免 Src/(CubeMX 生成,MCU 专属)。
|
||
SIM_EXEMPT = [
|
||
# CubeMX 生成的外设初始化 / 中断向量 / main / freertos:按构造即 MCU 专属,
|
||
# 模拟器无对应替身(任务口径明确认可"Src/ 全部")。唯一目录级豁免。
|
||
("Src/*", "CubeMX 生成的外设初始化/中断/main,MCU 专属,模拟器无替身"),
|
||
|
||
# MCU 平台后端:命名约定 *_mcu.c,模拟器另有同名 *_sim.c 替身(在 simulator/sim/)。
|
||
("*_mcu.c", "MCU 平台后端,模拟器另有 *_sim.c 替身"),
|
||
|
||
# --- App/:固件任务 / MCU 依赖 / 纯算法(模拟器无消费路径)---
|
||
("App/algo/adc_health.c", "L1 有效性校验纯算法,模拟器无消费路径"),
|
||
("App/algo/adc_input.c", "依赖 HAL/TPAFE 取数,MCU 专属"),
|
||
("App/algo/phasor.c", "L2 相量纯算法,模拟器无消费路径"),
|
||
("App/algo/prot_elm.c", "L3 保护元件纯算法,模拟器无消费路径"),
|
||
("App/algo/prot_set.c", "L5 保护定值纯算法,模拟器无消费路径"),
|
||
("App/algo/freq.c", "L2 派生测频纯算法(输入=phasor 使用区),模拟器无消费路径"),
|
||
("App/cfg/prot_set_store.c", "定值持久化 MCU 胶水(依赖 prot_set 纯算法),模拟器无消费路径"),
|
||
("App/log/soe_rec.c", "SOE 记录编解码纯逻辑(B9),L0.5 主机单测覆盖,模拟器无消费路径"),
|
||
("App/log/soe_store.c", "SOE 落盘 MCU 胶水(队列+FatFs 单写者,B9),模拟器无消费路径"),
|
||
("App/algo/sample_ring.c", "L2 环形缓冲纯算法,模拟器无消费路径"),
|
||
("App/app/app_main.c", "固件启动/就绪屏障,MCU 专属"),
|
||
("App/fs/vol_lock.c", "FreeRTOS 互斥量卷锁,MCU 专属"),
|
||
("App/log/console_log.c", "MCU 控制台输出,模拟器无消费路径"),
|
||
("App/sync/time_sync.c", "对时任务(net+RTOS),MCU 专属"),
|
||
("App/task/adc_task.c", "固件 FreeRTOS 任务,模拟器不跑固件任务"),
|
||
("App/task/default_task.c", "固件 FreeRTOS 任务,模拟器不跑固件任务"),
|
||
("App/task/iec104_serv.c", "固件 FreeRTOS 任务(104 上行,B4),模拟器不跑固件任务"),
|
||
("App/task/protect_task.c", "固件 FreeRTOS 任务(保护判据节拍,B5),模拟器不跑固件任务"),
|
||
("App/task/net_task.c", "固件 FreeRTOS 任务,模拟器不跑固件任务"),
|
||
("App/task/rs485_task.c", "固件 RS-485 任务,MCU 专属(另:uvprojx 缺口见 KNOWN_GAPS)"),
|
||
("App/task/ui_task.c", "固件 UI 任务,模拟器另有 main.c 驱动 UI"),
|
||
("App/protocol/iec104/iec104_asdu.c", "104 ASDU 编解码纯逻辑,L0.5 主机单测覆盖,模拟器无消费路径"),
|
||
("App/protocol/iec104/iec104_link.c", "104 链路状态机纯逻辑(I/O 注入),L0.5 覆盖,模拟器无消费路径"),
|
||
("App/protocol/iec104/iec104_pts.c", "104 点表/数据源适配(phasor/prot 依赖),模拟器无消费路径"),
|
||
("App/util/sha256.c", "纯工具(FTP 鉴权在 MCU 端),模拟器无消费路径"),
|
||
("App/util/sys_clock.c", "RTC/SD2506 依赖墙钟,MCU 专属"),
|
||
("App/util/task_prof.c", "任务性能探针,MCU 专属"),
|
||
("App/util/task_watchdog.c", "看门狗判定(RTOS 依赖),MCU 专属"),
|
||
|
||
# --- Drivers/BSP/:MCU 芯片/网络驱动与 FTP 服务(模拟器无对应)---
|
||
("Drivers/BSP/CH395F/ch395f.c", "MCU SPI 网卡驱动"),
|
||
("Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.c", "MCU SPI NAND 驱动"),
|
||
("Drivers/BSP/GD5F2GQ5UE/nand_ftl.c", "dhara FTL 适配,MCU 存储栈"),
|
||
("Drivers/BSP/NET/lftpd/lftpd.c", "MCU FTP 服务器"),
|
||
("Drivers/BSP/NET/lftpd/lftpd_auth.c", "MCU FTP 鉴权"),
|
||
("Drivers/BSP/NET/lftpd/lftpd_inet.c", "MCU FTP 网络层"),
|
||
("Drivers/BSP/NET/lftpd/lftpd_io.c", "MCU FTP 文件 IO"),
|
||
("Drivers/BSP/NET/lftpd/lftpd_string.c", "MCU FTP 字符串工具"),
|
||
("Drivers/BSP/NET/net_select.c", "MCU socket 选择层"),
|
||
("Drivers/BSP/NET/net_socket.c", "MCU socket 后端,模拟器另有 net_socket_sim.c"),
|
||
("Drivers/BSP/NET/net_listener.c", "MCU 监听器生命周期(依赖 ch395f_* SPI 驱动与 net_socket.c 静态状态),模拟器无对应(Winsock 后端无孤儿态/重监听概念)"),
|
||
("Drivers/BSP/NET/net_events.c", "MCU 芯片事件处理 + 100ms 周期对账(依赖 ch395f_* SPI 驱动与驱动状态缓存),模拟器无对应(Winsock 后端无\"事件未收到/静默回收\"概念,无失同步可收敛)"),
|
||
("Drivers/BSP/NET/net_pending.c", "MCU 挂起 recv 模型(依赖 ch395f_* SPI 读出与 FreeRTOS 通知),模拟器另有 net_socket_sim.c 按外部语义模拟"),
|
||
("Drivers/BSP/RS485/rs485.c", "MCU RS-485 驱动"),
|
||
("Drivers/BSP/SD2506/sd2506.c", "MCU RTC 驱动"),
|
||
("Drivers/BSP/TPAFE5160/tpafe5160.c", "MCU 计量芯片驱动"),
|
||
("Drivers/BSP/UI/ui_port.c", "MCU UI 端口层(显示/触摸硬件),模拟器另有 sim/sim_port.c"),
|
||
]
|
||
|
||
# --- 已知缺口白名单(存量,N-23,Lead 已处置)---
|
||
# 命中 -> WARN 不拦;新缺口 -> GAP FAIL。仅针对"存在但未注册"方向。
|
||
KNOWN_GAPS = {
|
||
"App/task/rs485_task.c": "E-59 门禁刻意未接线(B2 消费者就绪时随任务立项注册)",
|
||
}
|
||
|
||
|
||
def to_repo_rel(path):
|
||
"""把绝对/相对路径归一为仓库相对 posix 路径。"""
|
||
p = os.path.normpath(path)
|
||
rel = os.path.relpath(p, REPO_ROOT)
|
||
return rel.replace("\\", "/")
|
||
|
||
|
||
def is_scan_scope(repo_rel):
|
||
"""是否在扫描范围内(App/、Drivers/BSP/、Src/ 下的 .c,排除第三方)。"""
|
||
if not repo_rel.endswith(".c"):
|
||
return False
|
||
for tp in THIRD_PARTY:
|
||
if repo_rel == tp or repo_rel.startswith(tp + "/"):
|
||
return False
|
||
for root in SCAN_ROOTS:
|
||
if repo_rel.startswith(root + "/"):
|
||
return True
|
||
return False
|
||
|
||
|
||
def scan_sources():
|
||
"""扫描扫描根下全部 .c,返回仓库相对 posix 路径集合。"""
|
||
found = set()
|
||
for root in SCAN_ROOTS:
|
||
abs_root = os.path.join(REPO_ROOT, root.replace("/", os.sep))
|
||
if not os.path.isdir(abs_root):
|
||
continue
|
||
for dirpath, _dirnames, filenames in os.walk(abs_root):
|
||
for fn in filenames:
|
||
if fn.endswith(".c"):
|
||
abs_p = os.path.join(dirpath, fn)
|
||
rel = to_repo_rel(abs_p)
|
||
if is_scan_scope(rel):
|
||
found.add(rel)
|
||
return found
|
||
|
||
|
||
def parse_uvprojx():
|
||
"""解析 uvprojx 的 <FilePath>,返回仓库相对 posix 路径集合(含 .c/.s)。"""
|
||
with open(UVPROJX, "r", encoding="utf-8", errors="replace") as f:
|
||
text = f.read()
|
||
mdk_dir = os.path.dirname(UVPROJX)
|
||
entries = set()
|
||
for m in re.finditer(r"<FilePath>(.*?)</FilePath>", text):
|
||
raw = m.group(1).strip()
|
||
if not raw:
|
||
continue
|
||
# uvprojx 路径相对 MDK-ARM/ 目录,反斜杠或正斜杠混用
|
||
joined = os.path.join(mdk_dir, raw.replace("\\", os.sep))
|
||
entries.add(to_repo_rel(joined))
|
||
return entries
|
||
|
||
|
||
def _expand_var(s, varmap, depth=0):
|
||
"""递归展开 ${VAR}。"""
|
||
if depth > 12:
|
||
return s
|
||
|
||
def repl(m):
|
||
name = m.group(1)
|
||
val = varmap.get(name, m.group(0))
|
||
return _expand_var(val, varmap, depth + 1)
|
||
|
||
return re.sub(r"\$\{(\w+)\}", repl, s)
|
||
|
||
|
||
def parse_cmake():
|
||
"""解析 CMakeLists 的 *_SOURCES 列表(先展开变量),返回仓库相对 posix 路径集合。"""
|
||
with open(CMAKELISTS, "r", encoding="utf-8", errors="replace") as f:
|
||
text = f.read()
|
||
|
||
# 1) 单值 set(NAME value):目录变量(不含 *_SOURCES)
|
||
varmap = {"CMAKE_CURRENT_SOURCE_DIR": SIM_DIR}
|
||
for m in re.finditer(r'set\s*\(\s*([A-Za-z_]\w*)\s+("?)([^"\r\n]*?)\2\s*\)', text):
|
||
name = m.group(1)
|
||
val = m.group(3).strip()
|
||
if name.endswith("_SOURCES"):
|
||
continue
|
||
varmap[name] = val
|
||
|
||
# 2) 多值 set(NAME ... ) 中名字以 _SOURCES 结尾的块
|
||
sources = set()
|
||
for m in re.finditer(r"set\s*\(\s*([A-Za-z_]\w*_SOURCES)\b(.*?)\)", text, re.DOTALL):
|
||
body = m.group(2)
|
||
# 逐行剥注释
|
||
lines = []
|
||
for line in body.splitlines():
|
||
idx = line.find("#")
|
||
if idx >= 0:
|
||
line = line[:idx]
|
||
lines.append(line)
|
||
body = " ".join(lines)
|
||
for tok in body.split():
|
||
expanded = _expand_var(tok, varmap)
|
||
if not expanded:
|
||
continue
|
||
# 展开后若仍是绝对/带盘符路径直接用,否则相对 simulator/ 目录
|
||
if os.path.isabs(expanded) or re.match(r"^[A-Za-z]:[\\/]", expanded):
|
||
cand = expanded
|
||
else:
|
||
cand = os.path.join(SIM_DIR, expanded)
|
||
sources.add(to_repo_rel(cand))
|
||
return sources
|
||
|
||
|
||
# --- UI 页面模块 → 描述符表接线(④,U4 门禁补强)---
|
||
UI_DIR_REL = "Drivers/BSP/UI"
|
||
|
||
|
||
def check_ui_page_wiring():
|
||
"""每个定义 `ui_page_<name>_desc` 的 `ui_page_*.c`,其 desc 必须被 UI 目录内其他文件引用。
|
||
|
||
UI 页面的"注册"有**两层**:构建注册(uvprojx + CMakeLists,①② 类)与框架注册
|
||
(`ui_content_page.h` 枚举 + `ui_content_page.c` 的 `s_defs[]` 表)。漏了后者的页面
|
||
两端照常编译链接(desc 无引用 → 链接器剥离),①②③ 全绿但菜单打不开这一页。
|
||
判定:页面模块 = `ui_page_*.c` 中定义 `<stem>_desc =` 的文件(不定义 desc 的
|
||
`ui_page_*.c` 不是页面模块——未来 U3 生成的 `ui_page_registry.c` 承载表引用,
|
||
本规则依旧成立且不会把它误判为页面);引用面 = `Drivers/BSP/UI/` 下全部 .c/.h,
|
||
排除该页自身 .c/.h(自身头文件里的 extern 声明不算接线)。
|
||
"""
|
||
ui_dir = os.path.join(REPO_ROOT, UI_DIR_REL.replace("/", os.sep))
|
||
if not os.path.isdir(ui_dir):
|
||
return [], 0
|
||
|
||
# 1) 页面模块清单:ui_page_*.c 中定义 `<stem>_desc =` 的
|
||
pages = {} # sym -> repo-rel .c
|
||
for fn in sorted(os.listdir(ui_dir)):
|
||
if not (fn.startswith("ui_page_") and fn.endswith(".c")):
|
||
continue
|
||
try:
|
||
text = open(os.path.join(ui_dir, fn), encoding="utf-8-sig", errors="replace").read()
|
||
except OSError:
|
||
continue
|
||
sym = fn[:-2] + "_desc"
|
||
if re.search(r"\b%s\b\s*=" % re.escape(sym), text):
|
||
pages[sym] = "%s/%s" % (UI_DIR_REL, fn)
|
||
if not pages:
|
||
return [], 0
|
||
|
||
# 2) 引用面扫描:UI 目录全部 .c/.h,排除各页自身 .c/.h
|
||
referenced = {sym: False for sym in pages}
|
||
for dirpath, _dirnames, filenames in os.walk(ui_dir):
|
||
for fname in filenames:
|
||
if not fname.endswith((".c", ".h")):
|
||
continue
|
||
rel_p = to_repo_rel(os.path.join(dirpath, fname))
|
||
try:
|
||
text = open(os.path.join(dirpath, fname), encoding="utf-8-sig", errors="replace").read()
|
||
except OSError:
|
||
continue
|
||
for sym, own_c in pages.items():
|
||
if rel_p in (own_c, own_c[:-2] + ".h"):
|
||
continue
|
||
if re.search(r"\b%s\b" % re.escape(sym), text):
|
||
referenced[sym] = True
|
||
|
||
gaps = []
|
||
for sym, rel_c in sorted(pages.items()):
|
||
if not referenced[sym]:
|
||
gaps.append("[GAP] %s | 页面未接入描述符表(%s 在 %s/ 内除自身外无引用 -> "
|
||
"编译得过但菜单打不开,死代码;新增 UI 页面必须过本项)"
|
||
% (rel_c, sym, UI_DIR_REL))
|
||
return gaps, len(pages)
|
||
|
||
|
||
def main():
|
||
ap = argparse.ArgumentParser(description="双端注册反向校验(uvprojx + CMakeLists)")
|
||
ap.add_argument("--strict", action="store_true",
|
||
help="关闭 KNOWN_GAPS 白名单:存量缺口也按 FAIL 处理(供审计)")
|
||
args = ap.parse_args()
|
||
|
||
# 环境校验
|
||
for path, label in ((UVPROJX, "uvprojx"), (CMAKELISTS, "CMakeLists.txt")):
|
||
if not os.path.isfile(path):
|
||
print("[ENV-ERROR] 注册表缺失: %s (%s)" % (label, path))
|
||
return 2
|
||
|
||
try:
|
||
scanned = scan_sources()
|
||
uv = parse_uvprojx()
|
||
cmake = parse_cmake()
|
||
except OSError as e:
|
||
print("[ENV-ERROR] 读取失败: %s" % e)
|
||
return 2
|
||
|
||
uv_c = {p for p in uv if is_scan_scope(p)}
|
||
cmake_c = {p for p in cmake if is_scan_scope(p)}
|
||
|
||
gaps = [] # 新缺口 -> FAIL
|
||
warns = [] # 存量白名单 -> WARN
|
||
|
||
# ① 存在但未进 uvprojx(无豁免:扫描范围内每个 .c 都应编入固件)
|
||
for f in sorted(scanned):
|
||
if f not in uv:
|
||
if (not args.strict) and f in KNOWN_GAPS:
|
||
warns.append("[WARN] %s | 缺 uvprojx(存量:%s)" % (f, KNOWN_GAPS[f]))
|
||
else:
|
||
gaps.append("[GAP] %s | 缺 uvprojx" % f)
|
||
|
||
# ② 存在但未进 CMakeLists(MCU-only 豁免)
|
||
for f in sorted(scanned):
|
||
if f in cmake_c:
|
||
continue
|
||
if any(fnmatch.fnmatch(f, pat) for pat, _ in SIM_EXEMPT):
|
||
continue
|
||
if (not args.strict) and f in KNOWN_GAPS:
|
||
warns.append("[WARN] %s | 缺 CMakeLists(存量:%s)" % (f, KNOWN_GAPS[f]))
|
||
else:
|
||
gaps.append("[GAP] %s | 缺 CMakeLists" % f)
|
||
|
||
# ③ 幽灵注册:注册了但文件不存在(反向)
|
||
for f in sorted(uv_c):
|
||
if not os.path.isfile(os.path.join(REPO_ROOT, f.replace("/", os.sep))):
|
||
gaps.append("[GAP] %s | 幽灵注册(uvprojx)" % f)
|
||
for f in sorted(cmake):
|
||
if not os.path.isfile(os.path.join(REPO_ROOT, f.replace("/", os.sep))):
|
||
gaps.append("[GAP] %s | 幽灵注册(CMakeLists)" % f)
|
||
|
||
# ④ UI 页面模块未接入描述符表(框架注册,U4;①②③ 照不出的盲区)
|
||
ui_gaps, ui_pages = check_ui_page_wiring()
|
||
gaps.extend(ui_gaps)
|
||
|
||
# 输出
|
||
print("== 双端注册反向校验 ==")
|
||
print("扫描 .c: %d | uvprojx(扫描范围): %d | CMakeLists(扫描范围): %d | UI 页面模块: %d"
|
||
% (len(scanned), len(uv_c), len(cmake_c), ui_pages))
|
||
for w in warns:
|
||
print(w)
|
||
for g in gaps:
|
||
print(g)
|
||
|
||
if gaps:
|
||
print("结果: FAIL(%d 个新缺口/幽灵注册/页面未接线,%d 个存量 WARN)" % (len(gaps), len(warns)))
|
||
return 1
|
||
if warns:
|
||
print("结果: PASS(无新缺口;%d 个存量缺口已白名单 WARN,N-23 Lead 处置中)" % len(warns))
|
||
return 0
|
||
print("结果: PASS(双端注册一致)")
|
||
return 0
|
||
|
||
|
||
if __name__ == "__main__":
|
||
sys.exit(main())
|