#!/usr/bin/env python3 # -*- coding: utf-8 -*- """分层方向门禁:Drivers/BSP/** 不得 include App/** 的头(反向依赖增量拦截)。 背景(2026-10-22 加) ---------------- AGENTS.md 与 docs/架构设计/项目架构总览.md 都写着"六层单向依赖:业务 → UI/基础服务 → 中间件 → BSP 驱动 → HAL/RTOS → 硬件",但**此前没有任何机械检查**:Drivers/BSP/ 里 可以随手 include "cfg_file.h" 而无人报警。后果是可测量的口径漂移——2026-09 审查报告 记"NETCFG 一处",2026-10-22 实测已长到 8 个文件 9 处。边界的静默腐蚀只能靠门禁止住: 先把现状钉成基线,然后**只拦新增**。 与 scripts/check_ids.py 的"增量拦截"同构: · 新出现的 (引用方, App 头) 组合 → FAIL(拦提交) · 基线里的组合已消失 → WARN,并提示收紧(基线只减不增,绝没有 --write 往里加) · 横切基础设施头走 ALLOW 永久豁免:它们是"任何层都要用的叶子能力",不是上层服务 用法 ---- python scripts/check_layer_deps.py # 门禁模式(pre-commit / CI) python scripts/check_layer_deps.py --verbose # 列出全部反向边与判定 python scripts/check_layer_deps.py --prune # 只报告可以删掉的基线条目 退出码:0 = 通过;1 = 发现新增反向依赖;2 = 用法/环境错误 """ import argparse import os import re import subprocess import sys REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) BSP_PREFIX = "Drivers/BSP/" APP_PREFIX = "App/" SRC_EXT = (".c", ".h") # 横切基础设施:任何层都可用,允许被 BSP include。 # 新增条目必须先回答"它为什么不是上层服务"——否则就是给自己开后门。 ALLOW = { "App/log/console_log.h", # 日志后端:BSP 的 dbg_log.h 本来就建在它上面 "App/util/sys_clock.h", # 时间源:日志时间戳的唯一提供者 "App/util/sha256.h", # 纯算法工具:无状态、不依赖任何上层服务 } # 存量基线(2026-10-22 清点 6 条,只减不增)。每条 = (引用方, 被引用的 App 头)。 # 修法是"注入/传参",不是把上层服务搬进 BSP。 # 2026-10-25 道B(task/UI-rec-reader)还清 UI 记录 3 条:ui_w_records 改注入式 # ui_rec_reader_t(契约 UI_数据供给设计说明.md §7.2),runlog/selftest/records.h # 不再 include sys_log.h,绑定在 App/task/ui_task.c 与 simulator/main.c。 BASELINE = { ("Drivers/BSP/NETCFG/net_cfg.c", "App/cfg/cfg_file.h"), # 配置引擎属主 = defaultTask ("Drivers/BSP/NETCFG/ftp_cfg.c", "App/cfg/cfg_file.h"), # 同上([ftp] 段注册) ("Drivers/BSP/NET/lftpd/lftpd_io.c", "App/fs/vol_lock.h"), # 卷锁在 App/fs } INC_RE = re.compile(r'^[ \t]*#[ \t]*include[ \t]*"([^"]+)"', re.M) def tracked_files(): """以 git ls-files 为准(不扫构建产物);git 不可用时退回 os.walk。""" try: out = subprocess.check_output(["git", "ls-files", "-z"], cwd=REPO) return [p.decode("utf-8") for p in out.split(b"\0") if p] except Exception: # noqa: BLE001 files = [] for root, dirs, names in os.walk(REPO): dirs[:] = [d for d in dirs if d not in (".git", ".venv", "build", ".dsh-worktrees")]; for n in names: files.append(os.path.relpath(os.path.join(root, n), REPO).replace(os.sep, "/")) return files def app_header_index(files): idx = {} for f in files: if f.startswith(APP_PREFIX) and f.endswith(".h"): idx.setdefault(os.path.basename(f), []).append(f) return idx def scan(files, idx): edges = set() for f in files: if not f.startswith(BSP_PREFIX) or not f.endswith(SRC_EXT): continue try: with open(os.path.join(REPO, f), encoding="utf-8-sig") as fh: text = fh.read() except (OSError, UnicodeDecodeError) as exc: print("[WARN] 读取失败,跳过:%s(%s)" % (f, exc)) continue for m in INC_RE.finditer(text): literal = m.group(1) owners = idx.get(os.path.basename(literal)) if not owners: continue norm = os.path.normpath(literal).replace(os.sep, "/") exact = [o for o in owners if norm == o or o.endswith("/" + norm)] for o in (exact or owners): edges.add((f, o)) return sorted(edges) def main(argv=None): ap = argparse.ArgumentParser(description="BSP 不得反向 include App 的头(增量拦截)") ap.add_argument("--verbose", action="store_true", help="列出全部反向边与判定") ap.add_argument("--prune", action="store_true", help="只报告已消失、可从基线删除的条目") args = ap.parse_args(argv) files = tracked_files() edges = scan(files, app_header_index(files)) live = set(e for e in edges if e[1] not in ALLOW) allowed = [e for e in edges if e[1] in ALLOW] new = sorted(live - BASELINE) gone = sorted(BASELINE - live) if args.verbose: print("== 反向 include 全量边(含豁免)==") for src, hdr in edges: tag = "豁免" if hdr in ALLOW else ("基线" if (src, hdr) in BASELINE else "新增") print(" [%s] %s -> %s" % (tag, src, hdr)) if args.prune: if not gone: print("[OK] 基线无需收紧(%d 条存量全部仍成立)" % len(BASELINE)) return 0 print("以下基线条目已不再成立,请从 BASELINE 删除(基线只减不增):") for src, hdr in gone: print(' ("%s", "%s"),' % (src, hdr)) return 0 print("分层方向:BSP 反向 include App 共 %d 处(豁免 %d / 基线内 %d / 新增 %d)" % (len(edges), len(allowed), len(live & BASELINE), len(new))) if gone: print("[WARN] %d 条基线已消失,建议收紧(跑 --prune 看明细)" % len(gone)) if new: print("") print("[FAIL] 新增反向依赖 %d 处——BSP 只依赖相邻下层,不许 include App 层的头:" % len(new)) for src, hdr in new: print(" %s: include \"%s\"" % (src, hdr)) print("") print(" 正确做法(三选一):") print(" ① 把上层能力注入给 BSP(函数指针/回调,参照 net_cfg_set_save_req_cb 的先例);") print(" ② 由上层把数据取好再传进来(参数/结构体),BSP 不自己去拿;") print(" ③ 若它其实是叶子工具(无状态、不依赖上层服务),可加进 ALLOW 并注明理由。") print(" 顺手修掉存量也欢迎:跑 --prune 看哪些基线可以删。") return 1 print("[OK] 无新增反向依赖(存量基线只减不增)") return 0 if __name__ == "__main__": sys.exit(main())