文档体系重组与 D-7 一致性门禁落地,清理芯片手册孤儿图片
- D-6 目录规范化:docs 根目录散落文档归入 UI设计/子系统设计/架构设计/审查记录/ 环境与部署/网络 等子目录,去掉目录名空格,重命名 嵌入式C语言代码规范_V1.0.md - D-7 三档一致性门禁:新增 scripts/check_doc_links.py(链接存在性)、 scripts/gen_task_table.py(FreeRTOS 任务表数值)、scripts/check_mem_numbers.py (内存/Flash 体积),均支持 --check/--write/--diff,pre-commit 钩子同步注册 - D-5 文档索引:新建 docs/README.md,新建 docs/文档规范.md 约束目录命名与链接写法 - D-1 芯片手册瘦身:删除 1069 张无引用孤儿图片(77.06 MB), docs/芯片手册 120 MB → 41 MB,已引用图片零断链 - 同步 C 源码注释中的文档路径引用(D-6 重组后),构建 0 错误 0 警告
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
C 语言代码规范检查(《嵌入式C语言代码规范(V1.0)》可机械化校验的规则)
|
||||
C 语言代码规范检查(《嵌入式C语言代码规范_V1.0》可机械化校验的规则)
|
||||
由 pre-commit 调用:python scripts/check_coding_standard.py <file...>,发现违规退出码 1。
|
||||
|
||||
检查项(括号内为规范条款号):
|
||||
|
||||
@@ -0,0 +1,249 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
文档路径一致性 + 命名规范检查(D-3/D-6/D-7)。
|
||||
由 pre-commit 调用:python scripts/check_doc_links.py <file...>,发现问题退出码 1。
|
||||
|
||||
检查三类:
|
||||
A. Markdown 链接 [文本](相对路径) —— 相对当前文档所在目录解析
|
||||
B. 反引号路径引用 `docs/xxx/yyy.md` —— 相对仓库根解析(本工程约定:文档间引用
|
||||
一律用「仓库根相对路径」)
|
||||
C. 文件名规范 目录名 / 文件名不得含空格与路径敏感字符(见 docs/文档规范.md §1)
|
||||
|
||||
豁免(不算失效,不报错):
|
||||
* 以 `[计划]` 前缀标记的路径(尚未创建的约定产物,见《项目审查与优化建议报告》D-3 建议)
|
||||
* 含 `[示例]` 标记的行——引用报错样例 / 日志片段等含路径的引文
|
||||
* 引用附近(±40 字符)出现「未创建/尚未创建/已删除/待排期」等状态词
|
||||
* 含通配符 `*`、尖括号 `<模块>` 等占位符
|
||||
* 模板变量样式(NN_ / XX_ / 界面名 / 模块名 / 日期 / 示例 / ${...} / %s 等)
|
||||
* 行内代码 span(`...`)里的链接写法——那是「怎么写」的示例,不是真实引用
|
||||
* 外部 URL(http/https/mailto)与纯锚点(#...)
|
||||
* 第三方目录下的文档(由 pre-commit exclude 控制,脚本内亦再兜底)
|
||||
* `docs/芯片手册/` 为外部资料:链接形态与文件名均不受控(整体豁免)
|
||||
|
||||
用法:
|
||||
python scripts/check_doc_links.py docs/a.md docs/b.md ... # 检查指定文档
|
||||
python scripts/check_doc_links.py --all # 检查全库文档
|
||||
退出码:0 通过;1 有问题。
|
||||
"""
|
||||
import re
|
||||
import sys
|
||||
import pathlib
|
||||
import urllib.parse
|
||||
|
||||
REPO_ROOT = pathlib.Path(__file__).resolve().parent.parent
|
||||
|
||||
# 不参与检查的路径片段(第三方/生成物/外部资料)
|
||||
SKIP_PARTS = ("node_modules", ".venv", ".git", "build", "obj", "__pycache__",
|
||||
"Lib/lvgl", "simulator/SDL2", ".opencode/node_modules",
|
||||
".workbuddy-ai", # 本地记忆/会话数据,非项目文档
|
||||
"docs/芯片手册") # 外部数据手册转录,非项目自有文档,链接形态不受控
|
||||
|
||||
# Markdown 链接:[text](target)。目标须以「非空白、非右括号」构成,
|
||||
# 且排除纯数字(字体尺寸等畸形写法)。
|
||||
MD_LINK_RE = re.compile(r"\[([^\]]*)\]\(([^)\s]+)(?:\s+\"[^\"]*\")?\)")
|
||||
# 行内代码 span:`...`(含 ``` 围栏行整行)。先剥离再扫链接——
|
||||
# 反引号里的内容按文档约定是「字面量/示例」,不应被当作真实引用。
|
||||
INLINE_CODE_RE = re.compile(r"`[^`\n]*`")
|
||||
# 反引号路径:`docs/xxx/yyy.md`(仓库根相对);终止字符含中英文标点,防同行粘连
|
||||
BACKTICK_PATH_RE = re.compile(r"`((?:docs|scripts|tools|test|simulator)/[^`\n]*?\.(?:md|c|h|py|json|txt|bat|svg|ini|tsv|yml|yaml))`")
|
||||
# 行内状态词:出现这些说明该引用已失效/尚未创建,属正常叙述。
|
||||
# ⚠️ 必须与引用「就近」匹配(见 has_status_hint),不能整行匹配——
|
||||
# 否则文件名本身含「不存在」「删除」等字样时会误豁免同行的所有引用。
|
||||
STATUS_HINTS = ("计划", "未创建", "尚未创建", "已删除", "删除", "不存在", "缺失",
|
||||
"断链", "待建", "待创建")
|
||||
# 显式「这是举例」标记:整行豁免。用于引用报错样例、日志片段、期望输出等
|
||||
# 含路径的引文(如「典型报错:`docs/a.md:1 [markdown 链接] x.svg`」)。
|
||||
EXAMPLE_MARKER = "[示例]"
|
||||
# 状态词与引用之间的最大字符距离(就近判定窗口)
|
||||
STATUS_PROXIMITY = 40
|
||||
# 占位符 token
|
||||
PLACEHOLDER_TOKENS = ("NN_", "XX_", "界面名", "模块名", "日期", "示例", "{{", "}}",
|
||||
"${", "%s", "%d", "<", ">")
|
||||
|
||||
|
||||
def is_placeholder(text: str) -> bool:
|
||||
if "*" in text or "<" in text or ">" in text or "{" in text or "}" in text:
|
||||
return True
|
||||
if "xxx" in text or "yyy" in text:
|
||||
return True
|
||||
return any(tok in text for tok in PLACEHOLDER_TOKENS)
|
||||
|
||||
|
||||
def is_valid_link_target(target: str) -> bool:
|
||||
"""过滤畸形写法:纯数字(字体尺寸等)、空目标、非路径形态。"""
|
||||
if not target:
|
||||
return False
|
||||
if target.isdigit():
|
||||
return False
|
||||
# 必须含「/」或「.」才像路径(避免把普通文本/锚点当路径)
|
||||
if "." not in target and "/" not in target:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def has_status_hint(text: str, pos: int = -1, span: int = 0) -> bool:
|
||||
"""是否靠近某状态词。
|
||||
|
||||
pos<0(默认)→ 退化为整行匹配,兼容无位置信息的调用。
|
||||
pos>=0 → 只在引用的 span 两侧 STATUS_PROXIMITY 字符内找状态词,
|
||||
避免「文件名里含状态词」豁免掉同行的其它真实引用。
|
||||
"""
|
||||
if pos < 0:
|
||||
return any(h in text for h in STATUS_HINTS)
|
||||
lo = max(0, pos - STATUS_PROXIMITY)
|
||||
hi = min(len(text), pos + max(span, 0) + STATUS_PROXIMITY)
|
||||
window = text[lo:hi]
|
||||
return any(h in window for h in STATUS_HINTS)
|
||||
|
||||
|
||||
def strip_inline_code(line: str) -> str:
|
||||
"""剥离行内代码 span(`...`),但保持字符数不变(同长空白替换),
|
||||
以便后续报错时的列号/上下文仍可对应原文。
|
||||
目的:文档常把「链接写法示例」放在反引号里(如 ),
|
||||
这类字面量不是真实引用,不应参与存在性校验。
|
||||
注意:反引号包裹的仓库根相对路径由 check_backtick_paths 单独处理,
|
||||
故这里用逐段替换、只挖掉成对反引号内部内容。
|
||||
"""
|
||||
return INLINE_CODE_RE.sub(lambda m: " " * len(m.group(0)), line)
|
||||
|
||||
|
||||
def check_md_links(path: pathlib.Path, lines, errors):
|
||||
"""A 类:markdown 链接 —— 相对当前文档目录解析。
|
||||
扫描前剥离行内代码 span:反引号里的链接写法是「示例」,非真实引用。
|
||||
"""
|
||||
for lineno, line in enumerate(lines, 1):
|
||||
scan = strip_inline_code(line)
|
||||
for m in MD_LINK_RE.finditer(scan):
|
||||
target = m.group(2).strip()
|
||||
if not target or target.startswith(("http://", "https://", "mailto:", "#")):
|
||||
continue
|
||||
if not is_valid_link_target(target):
|
||||
continue
|
||||
# 去掉锚点后判断
|
||||
raw_path = urllib.parse.unquote(target.split("#")[0])
|
||||
if not raw_path:
|
||||
continue
|
||||
if is_placeholder(target) or "[计划]" in line or EXAMPLE_MARKER in line:
|
||||
continue
|
||||
if has_status_hint(line, m.start(), len(m.group(0))):
|
||||
continue
|
||||
resolved = (path.parent / raw_path).resolve()
|
||||
if not resolved.exists():
|
||||
errors.append((path, lineno, target, "markdown 链接"))
|
||||
|
||||
|
||||
def check_backtick_paths(path: pathlib.Path, lines, errors):
|
||||
"""B 类:反引号内的仓库根相对路径。
|
||||
与 A 类相反:这里反引号**就是**引用容器(本工程约定 `docs/xxx.md` 表引用),
|
||||
故不剥离代码 span,直接扫原文。
|
||||
"""
|
||||
for lineno, line in enumerate(lines, 1):
|
||||
for m in BACKTICK_PATH_RE.finditer(line):
|
||||
target = m.group(1).strip()
|
||||
if is_placeholder(target) or "[计划]" in line or EXAMPLE_MARKER in line:
|
||||
continue
|
||||
if has_status_hint(line, m.start(), len(m.group(0))):
|
||||
continue
|
||||
if not (REPO_ROOT / target).exists():
|
||||
errors.append((path, lineno, target, "路径引用"))
|
||||
|
||||
|
||||
def _rel(p: pathlib.Path) -> str:
|
||||
"""仓库相对路径(正斜杠),用于与 SKIP_PARTS 前缀比较。"""
|
||||
try:
|
||||
return p.resolve().relative_to(REPO_ROOT).as_posix()
|
||||
except ValueError:
|
||||
return p.as_posix()
|
||||
|
||||
|
||||
def is_skipped(p: pathlib.Path) -> bool:
|
||||
rel = _rel(p)
|
||||
parts = rel.split("/")
|
||||
for s in SKIP_PARTS:
|
||||
if "/" in s:
|
||||
if rel == s or rel.startswith(s + "/"):
|
||||
return True
|
||||
elif s in parts:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
# --- 文件名规范(docs/文档规范.md §1) -----------------------------------
|
||||
# 路径敏感 / 全角字符:禁止出现在文档目录名与文件名中
|
||||
BAD_NAME_CHARS = set("()【】〔〕<>:|*?\"\\&")
|
||||
# 外部资料目录:沿用上游原名,不受命名规范约束
|
||||
NAME_EXEMPT_DIRS = ("docs/芯片手册",)
|
||||
|
||||
|
||||
def check_name(path: pathlib.Path, errors):
|
||||
"""校验文档「目录名 + 文件名」不含空格与路径敏感字符(规范 §1)。"""
|
||||
rel = _rel(path)
|
||||
for exempt in NAME_EXEMPT_DIRS:
|
||||
if rel == exempt or rel.startswith(exempt + "/"):
|
||||
return
|
||||
parts = rel.split("/")[1:] # 去掉仓库根
|
||||
for part in parts:
|
||||
stem = part[:-3] if part.endswith(".md") else part
|
||||
if " " in stem:
|
||||
errors.append((path, 0, f"名称含空格: {stem}", "文件名规范"))
|
||||
bad = sorted(set(stem) & BAD_NAME_CHARS)
|
||||
if bad:
|
||||
errors.append((path, 0,
|
||||
f"名称含禁用字符 {' '.join(bad)}: {stem}", "文件名规范"))
|
||||
|
||||
|
||||
def iter_docs(argv):
|
||||
if "--all" in argv:
|
||||
for p in REPO_ROOT.rglob("*.md"):
|
||||
if not is_skipped(p):
|
||||
yield p
|
||||
return
|
||||
for a in argv:
|
||||
if a.startswith("--"):
|
||||
continue
|
||||
p = pathlib.Path(a)
|
||||
if not p.is_absolute():
|
||||
p = pathlib.Path.cwd() / p
|
||||
if p.is_file() and p.suffix.lower() == ".md":
|
||||
yield p.resolve()
|
||||
|
||||
|
||||
def main(argv):
|
||||
errors = []
|
||||
n_files = 0
|
||||
for doc in iter_docs(argv[1:] if len(argv) > 1 else []):
|
||||
if is_skipped(doc):
|
||||
continue
|
||||
try:
|
||||
text = doc.read_text(encoding="utf-8")
|
||||
except (UnicodeDecodeError, OSError):
|
||||
continue
|
||||
n_files += 1
|
||||
lines = text.splitlines()
|
||||
check_md_links(doc, lines, errors)
|
||||
check_backtick_paths(doc, lines, errors)
|
||||
check_name(doc, errors)
|
||||
|
||||
if errors:
|
||||
print(f"发现的失效文档引用 / 命名问题({len(errors)} 处):")
|
||||
for path, lineno, target, kind in errors:
|
||||
try:
|
||||
rel = path.relative_to(REPO_ROOT)
|
||||
except ValueError:
|
||||
rel = path
|
||||
loc = f"{rel}:{lineno}" if lineno else f"{rel}"
|
||||
print(f" {loc} [{kind}] {target}")
|
||||
print(f"\n检查 {n_files} 个文档,{len(errors)} 处问题。")
|
||||
print("提示:引用计划中的文件请用 `[计划] <路径>` 前缀标记;"
|
||||
"已删除的文件请写明「已删除」状态词;"
|
||||
"引用报错样例/日志片段请在该行加 `[示例]` 标记;"
|
||||
"命名规范见 docs/文档规范.md §1。")
|
||||
return 1
|
||||
|
||||
print(f"文档路径检查通过({n_files} 个文档)。")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main(sys.argv))
|
||||
@@ -0,0 +1,420 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
内存/Flash 数字一致性门禁(D-7 第三档:体积)。
|
||||
|
||||
问题
|
||||
----
|
||||
文档(尤其 `docs/内存管理/*.md`)大量引用 `RW_IRAM1 82,048 B / 62.6%`、
|
||||
`合计 ROM 249,432 B / 23.8%` 这类**具体数字**。这些数字来自 `.map`,每次重建都可能变化,
|
||||
而手抄必然滞后——实测 2026-09-26 复核:文档写 `82,048 B / 62.6%`,实际 `.map` 已是
|
||||
`87,040 B / 66.4%`;Flash 写 `249,432 / 23.8%`,实际 `261,932 / 25.0%`。
|
||||
读者照文档推算内存余量会得到错误结论。
|
||||
|
||||
做法
|
||||
----
|
||||
与 `gen_task_table.py` 同构:**`.map` 是唯一事实源**,文档里放**受管生成区**:
|
||||
|
||||
<!-- MEM_SNAPSHOT:BEGIN generated by scripts/check_mem_numbers.py -->
|
||||
| 区域 | 用量 | 容量 | 占比 |
|
||||
|---|---|---|---|
|
||||
| RW_IRAM1 | 87,040 B (85.00 KB) | 131,072 B (128 KB) | 66.4% |
|
||||
...
|
||||
<!-- MEM_SNAPSHOT:END -->
|
||||
|
||||
脚本解析 `.map`,重新渲染该区并校验一致;`--write` 自动同步。
|
||||
**行文中的散句数字**(如"现 RW_IRAM1 62.6%")不自动改写——它们由作者从受管区抄写,
|
||||
`--check` 会额外做**散句数字扫描**:发现文中出现与受管区不符的"旧值"即告警(见 §扫描)。
|
||||
这样既避免全文自动改写的高风险,又能抓出漏改的散句。
|
||||
|
||||
用法
|
||||
----
|
||||
python scripts/check_mem_numbers.py # 检查(默认)
|
||||
python scripts/check_mem_numbers.py --write # 同步受管区
|
||||
python scripts/check_mem_numbers.py --diff # 只打印期望快照
|
||||
python scripts/check_mem_numbers.py --scan # 额外扫描散句数字不一致
|
||||
python scripts/check_mem_numbers.py --map <path> # 指定 .map(默认 MDK-ARM/.../*.map)
|
||||
|
||||
退出码:0 通过;1 不一致 / 找不到事实源 / 解析失败。
|
||||
"""
|
||||
import argparse
|
||||
import pathlib
|
||||
import re
|
||||
import sys
|
||||
|
||||
REPO_ROOT = pathlib.Path(__file__).resolve().parent.parent
|
||||
MAP_DEFAULT = REPO_ROOT / "MDK-ARM" / "STM32F407-Demo" / "STM32F407-Demo.map"
|
||||
|
||||
# 受管快照区锚点
|
||||
BEGIN = "<!-- MEM_SNAPSHOT:BEGIN generated by scripts/check_mem_numbers.py -->"
|
||||
END = "<!-- MEM_SNAPSHOT:END -->"
|
||||
|
||||
# 需要保持同步的文档
|
||||
TARGETS = [
|
||||
REPO_ROOT / "docs" / "内存管理" / "RAM使用分析与优化方案.md",
|
||||
]
|
||||
|
||||
# 散句扫描范围(只扫这些文档里的"裸数字",避免误伤历史快照 data/*.md)
|
||||
SCAN_TARGETS = [
|
||||
REPO_ROOT / "docs" / "内存管理" / "RAM使用分析与优化方案.md",
|
||||
REPO_ROOT / "docs" / "内存管理" / "内存内容分配策略.md",
|
||||
REPO_ROOT / "docs" / "内存管理" / "存储栈RAM优化调研.md",
|
||||
]
|
||||
|
||||
|
||||
# --- .map 解析(与 scripts/ram_report.py 同口径,保持数据一致)---------------
|
||||
|
||||
def parse_map(path: pathlib.Path):
|
||||
"""解析 Keil .map,返回 {'regions': [...], 'grand_totals': {...}}。
|
||||
|
||||
与 ram_report.py 使用**相同**的正则与口径,避免两处数字打架。
|
||||
"""
|
||||
text = path.read_text(encoding="utf-8", errors="replace")
|
||||
regions, grand = [], None
|
||||
for ln in text.splitlines():
|
||||
if "Execution Region" in ln and "Exec base:" in ln:
|
||||
m = re.search(r"Execution Region (\w+) \((.*?)\)", ln)
|
||||
if not m:
|
||||
continue
|
||||
params = {}
|
||||
for part in m.group(2).split(","):
|
||||
part = part.strip()
|
||||
if ":" in part:
|
||||
k, v = part.split(":", 1)
|
||||
params[k.strip()] = v.strip()
|
||||
regions.append({
|
||||
"name": m.group(1),
|
||||
"size": int(params.get("Size", "0"), 16),
|
||||
"max": int(params.get("Max", "0"), 16),
|
||||
})
|
||||
if "Grand Totals" in ln:
|
||||
nums = []
|
||||
for p in ln.split():
|
||||
try:
|
||||
nums.append(int(p))
|
||||
except ValueError:
|
||||
if "Grand" in p:
|
||||
break
|
||||
if len(nums) >= 6:
|
||||
grand = {"code": nums[0], "ro_data": nums[2],
|
||||
"rw_data": nums[3], "zi_data": nums[4]}
|
||||
return {"regions": regions, "grand_totals": grand}
|
||||
|
||||
|
||||
def compute_facts(data):
|
||||
"""从解析结果算出受管快照要展示的指标(字节 → 展示口径)。"""
|
||||
facts = {}
|
||||
for r in data["regions"]:
|
||||
util = (r["size"] / r["max"] * 100) if r["max"] else 0.0
|
||||
facts[r["name"]] = {
|
||||
"size": r["size"], "max": r["max"], "util": util,
|
||||
}
|
||||
gt = data["grand_totals"]
|
||||
if gt:
|
||||
rom = gt["code"] + gt["ro_data"] + gt["rw_data"]
|
||||
rw_total = gt["rw_data"] + gt["zi_data"]
|
||||
facts["_flash"] = {"rom": rom, "code": gt["code"], "ro": gt["ro_data"],
|
||||
"flash_max": 1048576}
|
||||
facts["_rw_total"] = rw_total
|
||||
return facts
|
||||
|
||||
|
||||
def fmt_kb(n, digits=2):
|
||||
return f"{n/1024:.{digits}f} KB"
|
||||
|
||||
|
||||
def render_snapshot(facts):
|
||||
"""渲染受管快照 Markdown。顺序/口径固定,保证多次运行结果稳定。"""
|
||||
order = ["ER_IROM1", "RW_IRAM1", "RW_CCM"]
|
||||
lines = [
|
||||
"| 区域 | 用量 | 容量 | 占比 |",
|
||||
"|---|---|---|---|",
|
||||
]
|
||||
for name in order:
|
||||
f = facts.get(name)
|
||||
if not f:
|
||||
continue
|
||||
lines.append(
|
||||
f"| {name} | {f['size']:,} B ({fmt_kb(f['size'])}) | "
|
||||
f"{f['max']:,} B ({fmt_kb(f['max'], 0)}) | {f['util']:.1f}% |"
|
||||
)
|
||||
fl = facts.get("_flash")
|
||||
if fl:
|
||||
util = fl["rom"] / fl["flash_max"] * 100
|
||||
lines.append(
|
||||
f"| **Flash (ROM 合计)** | **{fl['rom']:,} B ({fmt_kb(fl['rom'], 1)})** | "
|
||||
f"{fl['flash_max']:,} B (1024 KB) | **{util:.1f}%** |"
|
||||
)
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def snapshot_metrics(facts):
|
||||
"""返回用于散句扫描的「期望值集合」:(数字, 允许的等价写法列表)。"""
|
||||
out = []
|
||||
def add(n):
|
||||
out.append(n)
|
||||
for name in ("ER_IROM1", "RW_IRAM1", "RW_CCM"):
|
||||
f = facts.get(name)
|
||||
if f:
|
||||
add(f["size"]); add(f["max"])
|
||||
add(round(f["util"], 1))
|
||||
fl = facts.get("_flash")
|
||||
if fl:
|
||||
add(fl["rom"]); add(fl["code"]); add(fl["ro"])
|
||||
add(round(fl["rom"] / fl["flash_max"] * 100, 1))
|
||||
if "_rw_total" in facts:
|
||||
add(facts["_rw_total"])
|
||||
return out
|
||||
|
||||
|
||||
# --- 文档读写(保真换行 / BOM,同 gen_task_table.py)------------------------
|
||||
|
||||
def read_doc(path: pathlib.Path):
|
||||
raw = path.read_bytes()
|
||||
bom = raw.startswith(b"\xef\xbb\xbf")
|
||||
body = raw[3:] if bom else raw
|
||||
crlf = body.count(b"\r\n")
|
||||
lf = body.count(b"\n") - crlf
|
||||
newline = "\r\n" if crlf > lf else "\n"
|
||||
text = body.decode("utf-8", errors="replace")
|
||||
return text.replace("\r\n", "\n").replace("\r", "\n"), newline, bom
|
||||
|
||||
|
||||
def write_doc(path: pathlib.Path, text: str, newline: str, bom: bool):
|
||||
out = text if newline == "\n" else text.replace("\n", newline)
|
||||
data = out.encode("utf-8")
|
||||
path.write_bytes((b"\xef\xbb\xbf" if bom else b"") + data)
|
||||
|
||||
|
||||
def replace_managed(text: str, snapshot: str):
|
||||
if BEGIN not in text or END not in text:
|
||||
return text, False
|
||||
s = text.index(BEGIN) + len(BEGIN)
|
||||
e = text.index(END)
|
||||
new = "\n" + snapshot + "\n"
|
||||
if text[s:e] == new:
|
||||
return text, False
|
||||
return text[:s] + new + text[e:], True
|
||||
|
||||
|
||||
def extract_managed(text: str):
|
||||
if BEGIN not in text or END not in text:
|
||||
return None
|
||||
s = text.index(BEGIN) + len(BEGIN)
|
||||
e = text.index(END)
|
||||
return text[s:e].strip()
|
||||
|
||||
|
||||
# --- 散句扫描 ---------------------------------------------------------------
|
||||
#
|
||||
# 朴素做法(扫所有 4~7 位数字)会产生大量误报——任务栈 `4096`、堆配置 `49,152`、
|
||||
# 峰值水位 `1184` 都是合法数字(实测一次报 45 处,全是噪音,门禁会被无视)。
|
||||
#
|
||||
# 正确判据:**「上一个快照里是区域用量、但当前已不是」的数字**才是陈旧数字。
|
||||
# 例如历史 `RW_IRAM1 = 82,048 B`、当前 `87,040 B` → 正文若仍写 `82,048` 即为旧值。
|
||||
# 故扫描只针对:从 `data/ram_report_*.md` 历史快照提取的**区域用量/占比**值。
|
||||
|
||||
SNAPSHOT_DIR = REPO_ROOT / "docs" / "内存管理" / "data"
|
||||
# 历史快照里 "| RW_IRAM1 | 0x... | 82,048 B (...)" 这类行
|
||||
SNAP_REGION_RE = re.compile(
|
||||
r"\|\s*(ER_IROM1|RW_IRAM1|RW_CCM)\s*\|\s*0x[0-9A-Fa-f]+\s*\|\s*([\d,]+)\s*B"
|
||||
r"\s*\([^)]*\)\s*\|\s*([\d,]+)\s*B[^|]*\|\s*([\d.]+)%"
|
||||
)
|
||||
|
||||
# 行内出现这些词说明是在讲**别的**数字(栈配置/峰值/池大小),不是区域用量。
|
||||
# ⚠️ 只在**紧邻数字**(前后 12 字符内)时才算命中——否则会误豁免整句
|
||||
# (实测:「堆迁 CCM 后…(现 RW_IRAM1 62.6%)」因含「堆」被整句跳过,漏掉现状数字)。
|
||||
NON_REGION_HINTS = ("栈", "STACK", "stack", "堆", "heap", "pool", "池",
|
||||
"峰值", "水位", "high water", "min_ever", "free=",
|
||||
"LV_MEM_SIZE", "config", "任务", "TCB", "缓冲", "buffer")
|
||||
HINT_WINDOW = 12
|
||||
|
||||
|
||||
def _is_non_region_metric(sent: str, start: int, end: int) -> bool:
|
||||
"""数字 [start,end) 是否处于「栈/池/峰值」语境(紧邻 ≤12 字符内有提示词)。"""
|
||||
lo = max(0, start - HINT_WINDOW)
|
||||
hi = min(len(sent), end + HINT_WINDOW)
|
||||
window = sent[lo:hi]
|
||||
return any(h in window for h in NON_REGION_HINTS)
|
||||
|
||||
|
||||
def collect_stale_numbers(current_facts):
|
||||
"""从历史快照提取「曾经是区域用量、现已变化」的数字 → 陈旧值集合。
|
||||
|
||||
只收**当前值不含**的历史区域用量/占比;这样扫正文时命中的就是真正的陈旧数字。
|
||||
"""
|
||||
cur = set()
|
||||
for name in ("ER_IROM1", "RW_IRAM1", "RW_CCM"):
|
||||
f = current_facts.get(name)
|
||||
if f:
|
||||
cur.add(f["size"])
|
||||
cur.add(f["max"])
|
||||
cur.add(round(f["util"], 1))
|
||||
fl = current_facts.get("_flash")
|
||||
if fl:
|
||||
cur.add(fl["rom"])
|
||||
cur.add(fl["code"])
|
||||
cur.add(fl["ro"])
|
||||
cur.add(round(fl["rom"] / fl["flash_max"] * 100, 1))
|
||||
|
||||
stale = {} # 数字 -> 来源文件名
|
||||
if not SNAPSHOT_DIR.is_dir():
|
||||
return stale
|
||||
for snap in sorted(SNAPSHOT_DIR.glob("ram_report_*.md")):
|
||||
text = snap.read_text(encoding="utf-8", errors="replace")
|
||||
for m in SNAP_REGION_RE.finditer(text):
|
||||
size = int(m.group(2).replace(",", ""))
|
||||
util = float(m.group(4))
|
||||
for v in (size, util):
|
||||
if v not in cur:
|
||||
stale.setdefault(v, snap.name)
|
||||
# 旧快照的 Grand Totals 行(Total ROM)
|
||||
for m in re.finditer(r"\*\*Total ROM[^|]*\|\s*\*\*([\d,]+)\s*B", text):
|
||||
v = int(m.group(1).replace(",", ""))
|
||||
if v not in cur:
|
||||
stale.setdefault(v, snap.name)
|
||||
return stale
|
||||
|
||||
|
||||
def scan_prose(text: str, stale, errors, path: pathlib.Path):
|
||||
"""正文里出现「历史快照的区域用量、但当前已变化」的数字 → 报为陈旧引用。
|
||||
|
||||
只报这一类(实测 0 误报),不报任务栈/池配置等其它数字。
|
||||
|
||||
⚠️ **按句子而非按行判定**:同一行常混有「当时 X」与「现 Y」两种时态,
|
||||
按行判会把整行豁免掉、漏掉后半句的现状数字(实测踩中)。
|
||||
"""
|
||||
if not stale:
|
||||
return
|
||||
in_managed = False
|
||||
# 句读切分:中文句号/分号/感叹号/问号 + Markdown 分隔(`|`、`;`)
|
||||
SENT_SPLIT = re.compile(r"(?<=[。;;!!??])|(?<=\|)")
|
||||
for lineno, line in enumerate(text.splitlines(), 1):
|
||||
if BEGIN in line:
|
||||
in_managed = True
|
||||
continue
|
||||
if END in line:
|
||||
in_managed = False
|
||||
continue
|
||||
if in_managed:
|
||||
continue
|
||||
for sent in SENT_SPLIT.split(line):
|
||||
if not sent or not sent.strip():
|
||||
continue
|
||||
# 明确标注「历史/曾/旧/快照」或引用 data/ 的**句子** → 允许保留旧数字
|
||||
if any(h in sent for h in ("历史", "旧", "曾", "此前", "快照", "data/",
|
||||
"修复前", "优化前", "迁入前", "实施时",
|
||||
"实施前", "当时", "原值", "改动前", "改造前")):
|
||||
continue
|
||||
# 讲栈/池/峰值的**数字**(提示词紧邻该数字)语义不同,跳过;
|
||||
# 注意不能按整句跳过——同一句可能既讲堆又报区域现状。
|
||||
for m in re.finditer(r"\b(\d{1,3}(?:,\d{3})+|\d{4,7}|\d{1,3}\.\d)%?", sent):
|
||||
if _is_non_region_metric(sent, m.start(), m.end()):
|
||||
continue
|
||||
raw = m.group(1)
|
||||
try:
|
||||
v = float(raw.replace(",", ""))
|
||||
except ValueError:
|
||||
continue
|
||||
key = int(v) if v.is_integer() else v
|
||||
if key in stale:
|
||||
errors.append((path, lineno,
|
||||
f"陈旧区域数字 {raw}(历史快照 {stale[key]} 的值,"
|
||||
f"当前已变化)", "陈旧数字"))
|
||||
|
||||
|
||||
def main(argv):
|
||||
ap = argparse.ArgumentParser(description="内存/Flash 数字一致性门禁(D-7 体积档)")
|
||||
g = ap.add_mutually_exclusive_group()
|
||||
g.add_argument("--check", action="store_true", help="检查模式(默认)")
|
||||
g.add_argument("--write", action="store_true", help="同步受管快照区")
|
||||
g.add_argument("--diff", action="store_true", help="只打印期望快照")
|
||||
ap.add_argument("--scan", action="store_true", help="额外扫描散句数字")
|
||||
ap.add_argument("--map", default=str(MAP_DEFAULT), help=".map 文件路径")
|
||||
args = ap.parse_args(argv[1:])
|
||||
|
||||
map_path = pathlib.Path(args.map)
|
||||
if not map_path.exists():
|
||||
print(f"[FAIL] 找不到 .map 事实源:{map_path}", file=sys.stderr)
|
||||
print(" 先跑一次 @build(或 MDK-ARM/build.bat)生成 .map。", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
data = parse_map(map_path)
|
||||
if not data["regions"]:
|
||||
print(f"[FAIL] 未能从 {map_path.name} 解析出执行区,请检查文件格式。",
|
||||
file=sys.stderr)
|
||||
return 1
|
||||
facts = compute_facts(data)
|
||||
snapshot = render_snapshot(facts)
|
||||
stale = collect_stale_numbers(facts)
|
||||
|
||||
if args.diff:
|
||||
print(snapshot)
|
||||
return 0
|
||||
|
||||
changed, missing, problems = [], [], []
|
||||
errors = []
|
||||
|
||||
for t in TARGETS:
|
||||
if not t.exists():
|
||||
problems.append(f"{t.relative_to(REPO_ROOT)} 不存在")
|
||||
continue
|
||||
text, newline, bom = read_doc(t)
|
||||
rel = t.relative_to(REPO_ROOT).as_posix()
|
||||
if args.write:
|
||||
new_text, did = replace_managed(text, snapshot)
|
||||
if did:
|
||||
write_doc(t, new_text, newline, bom)
|
||||
changed.append(rel)
|
||||
else:
|
||||
got = extract_managed(text)
|
||||
if got is None:
|
||||
missing.append(rel)
|
||||
elif got.replace("\r\n", "\n") != snapshot.strip():
|
||||
changed.append(rel)
|
||||
if args.scan:
|
||||
scan_prose(text, stale, errors, t)
|
||||
|
||||
if args.write:
|
||||
if missing:
|
||||
print("[WARN] 以下文档缺受管锚点,未写入:" + ", ".join(missing))
|
||||
print(f" 请在快照表上下各加一行:\n {BEGIN}\n {END}")
|
||||
if changed:
|
||||
print("已更新内存快照:" + ", ".join(changed))
|
||||
if not changed and not missing:
|
||||
print("内存快照已是最新,无需改动。")
|
||||
return 0
|
||||
|
||||
if problems:
|
||||
for p in problems:
|
||||
print(f"[FAIL] {p}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
if args.scan:
|
||||
for path, lineno, msg, kind in errors:
|
||||
rel = path.relative_to(REPO_ROOT)
|
||||
print(f" {rel}:{lineno} [{kind}] {msg}")
|
||||
|
||||
if missing or changed or errors:
|
||||
if missing:
|
||||
print("[FAIL] 以下文档缺少受管锚点:")
|
||||
for n in missing:
|
||||
print(f" - {n}")
|
||||
if changed:
|
||||
print("[FAIL] 以下文档的内存快照与 .map 不一致:")
|
||||
for n in changed:
|
||||
print(f" - {n}")
|
||||
print(f"\n事实源:{map_path.relative_to(REPO_ROOT)}")
|
||||
print("\n期望快照:")
|
||||
print(snapshot)
|
||||
print("\n修复:运行 `python scripts/check_mem_numbers.py --write`")
|
||||
if errors:
|
||||
print("(散句数字告警需人工复核,--write 不会自动改写正文)")
|
||||
return 1
|
||||
|
||||
print(f"内存数字一致(事实源 {map_path.name})。")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main(sys.argv))
|
||||
@@ -0,0 +1,289 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
FreeRTOS 任务表一致性工具(D-2 / D-7)。
|
||||
|
||||
背景
|
||||
----
|
||||
`README.md` 与 `AGENTS.md` 各有一张 FreeRTOS 任务表(任务名 / 栈大小 / 优先级)。
|
||||
历史上这两张表全靠手写,`Src/freertos.c` 改栈后表格忘记同步——2026-09 就出现过
|
||||
两张表都比源码落后一大截(写 2048/4096/4096/4096/16384/2048,实际 1024...512)。
|
||||
本脚本以 `Src/freertos.c` 为**唯一事实源**,生成 / 校验这两张表。
|
||||
|
||||
事实源
|
||||
------
|
||||
解析 `Src/freertos.c` 里形如:
|
||||
|
||||
const osThreadAttr_t netTask_attributes = {
|
||||
.name = "netTask",
|
||||
.stack_size = 1024 * 4, /* 注释 */
|
||||
.priority = (osPriority_t) osPriorityNormal,
|
||||
};
|
||||
|
||||
的块,抽出 name / stack_words / priority。`stack_size` 一律按 `N * 4`
|
||||
(CMSIS-RTOS2 的 stack_size 单位是字节,工程约定写成 `words * 4`)换算为 words。
|
||||
|
||||
用法
|
||||
----
|
||||
python scripts/gen_task_table.py # 检查模式(默认):不一致即退出码 1
|
||||
python scripts/gen_task_table.py --check # 同上
|
||||
python scripts/gen_task_table.py --write # 就地重写 README/AGENTS 中的任务表
|
||||
python scripts/gen_task_table.py --diff # 只打印期望表格,不写文件
|
||||
|
||||
pre-commit 用法:`python scripts/gen_task_table.py --check`,
|
||||
任一被跟踪文档里的任务表与 `Src/freertos.c` 不一致即拦截。
|
||||
|
||||
退出码:0 通过;1 不一致 / 解析失败。
|
||||
"""
|
||||
import argparse
|
||||
import pathlib
|
||||
import re
|
||||
import sys
|
||||
REPO_ROOT = pathlib.Path(__file__).resolve().parent.parent
|
||||
FREERTOS_C = REPO_ROOT / "Src" / "freertos.c"
|
||||
|
||||
# 需要保持同步的文档及其任务表锚点
|
||||
# anchor_begin / anchor_end 之间即为「受管表格区」,脚本只替换这一段,
|
||||
# 表格外的说明文字(含各文档自己的职责描述/口径注记)原样保留。
|
||||
TARGETS = [
|
||||
{
|
||||
"path": REPO_ROOT / "README.md",
|
||||
"begin": "<!-- TASK_TABLE:BEGIN generated by scripts/gen_task_table.py -->",
|
||||
"end": "<!-- TASK_TABLE:END -->",
|
||||
"header": "| 任务 | 栈大小 | 优先级 | 职责 |\n"
|
||||
"|------|--------|--------|------|",
|
||||
},
|
||||
{
|
||||
"path": REPO_ROOT / "AGENTS.md",
|
||||
"begin": "<!-- TASK_TABLE:BEGIN generated by scripts/gen_task_table.py -->",
|
||||
"end": "<!-- TASK_TABLE:END -->",
|
||||
"header": "| Task | 栈 (words) | 优先级 |\n|---|---|---|",
|
||||
},
|
||||
]
|
||||
|
||||
# 文档中的展示顺序(与 README/AGENTS 既有一致;不在表内的任务按源码顺序追加)。
|
||||
# 不用源码顺序:freertos.c 里 timeSyncTask 定义在最前,但文档按「主任务→辅助任务」排列。
|
||||
DISPLAY_ORDER = ["defaultTask", "netTask", "ftpTask", "testTask", "uiTask",
|
||||
"timeSyncTask"]
|
||||
|
||||
# 各任务在文档里的「职责」描述(README 需要,AGENTS 不需要)。
|
||||
# 只影响展示文案,不参与一致性判定;未知任务回落为空。
|
||||
ROLE = {
|
||||
"defaultTask": "FTL + FatFS 挂载、配置引擎属主、syslog 唯一写者",
|
||||
"netTask": "网络初始化 + CH395F 轮询",
|
||||
"ftpTask": "FTP 服务(等待网络+存储就绪)",
|
||||
"testTask": "硬件测试入口",
|
||||
"uiTask": "LVGL UI 界面",
|
||||
"timeSyncTask": "墙钟时间同步",
|
||||
}
|
||||
|
||||
|
||||
def sort_tasks(tasks):
|
||||
"""按 DISPLAY_ORDER 排序;未列出的任务保持源码相对顺序排在最后。"""
|
||||
idx = {n: i for i, n in enumerate(DISPLAY_ORDER)}
|
||||
return sorted(tasks, key=lambda t: (idx.get(t[0], len(idx)),))
|
||||
|
||||
# 任务块:const osThreadAttr_t <id>_attributes = { ... };
|
||||
BLOCK_RE = re.compile(
|
||||
r"const\s+osThreadAttr_t\s+(?P<id>\w+)_attributes\s*=\s*\{(?P<body>.*?)\};",
|
||||
re.S,
|
||||
)
|
||||
STACK_RE = re.compile(r"\.stack_size\s*=\s*(?P<expr>[^,]+),")
|
||||
PRIO_RE = re.compile(r"\.priority\s*=\s*\(osPriority_t\)\s*(?P<prio>\w+)")
|
||||
|
||||
# 优先级枚举 → 文档里使用的短名(去掉 osPriority 前缀)
|
||||
PRIO_SHORT = lambda p: p[len("osPriority"):] if p.startswith("osPriority") else p
|
||||
|
||||
|
||||
def parse_tasks(text: str):
|
||||
"""从 freertos.c 文本解析任务列表,按出现顺序返回。
|
||||
|
||||
stack_size 允许以下写法(工程约定):
|
||||
1024 * 4 -> 1024 words
|
||||
(1024 * 4) -> 1024 words
|
||||
4096 -> 1024 words(按 bytes/4 折算,并给出提示)
|
||||
返回 [(name, words, prio, raw_expr), ...]
|
||||
"""
|
||||
tasks = []
|
||||
for m in BLOCK_RE.finditer(text):
|
||||
body = m.group("body")
|
||||
sm = STACK_RE.search(body)
|
||||
pm = PRIO_RE.search(body)
|
||||
if not (sm and pm):
|
||||
continue
|
||||
expr = sm.group("expr").strip()
|
||||
name_lit = re.search(r'\.name\s*=\s*"([^"]+)"', body)
|
||||
name = name_lit.group(1) if name_lit else m.group("id")
|
||||
|
||||
# 去掉注释后求值:`1024 * 4` -> 4096 (bytes)
|
||||
clean = re.sub(r"/\*.*?\*/", "", expr, flags=re.S).strip()
|
||||
clean = clean.strip("()").strip()
|
||||
try:
|
||||
num = eval(clean, {"__builtins__": {}}, {}) # noqa: S307 (受控字面量)
|
||||
except Exception:
|
||||
print(f"[WARN] 无法解析 {name} 的 stack_size 表达式:{expr!r}",
|
||||
file=sys.stderr)
|
||||
continue
|
||||
|
||||
# 工程约定 = words*4,故 words = 字节/4
|
||||
words = num // 4 if num % 4 == 0 else num
|
||||
tasks.append((name, words, PRIO_SHORT(pm.group("prio")), expr))
|
||||
return tasks
|
||||
|
||||
|
||||
def render_table(tasks, header: str, with_role: bool) -> str:
|
||||
lines = [header]
|
||||
for name, words, prio, _expr in tasks:
|
||||
if with_role:
|
||||
lines.append(f"| {name} | {words} words | {prio} | {ROLE.get(name, '')} |")
|
||||
else:
|
||||
lines.append(f"| {name} | {words} | {prio} |")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def replace_managed(text: str, target: dict, table: str):
|
||||
"""替换 <!-- TASK_TABLE:BEGIN --> ... <!-- TASK_TABLE:END --> 之间的内容。
|
||||
|
||||
返回 (新文本, 是否变化)。若锚点不存在则原样返回(由调用方决定是否补锚点)。
|
||||
"""
|
||||
b, e = target["begin"], target["end"]
|
||||
if b not in text or e not in text:
|
||||
return text, False
|
||||
start = text.index(b) + len(b)
|
||||
end = text.index(e)
|
||||
old = text[start:end]
|
||||
new = "\n" + table + "\n"
|
||||
if old == new:
|
||||
return text, False
|
||||
return text[:start] + new + text[end:], True
|
||||
|
||||
|
||||
def extract_managed(text: str, target: dict):
|
||||
b, e = target["begin"], target["end"]
|
||||
if b not in text or e not in text:
|
||||
return None
|
||||
start = text.index(b) + len(b)
|
||||
end = text.index(e)
|
||||
return text[start:end].strip()
|
||||
|
||||
|
||||
def read_doc(path: pathlib.Path):
|
||||
"""读文档,返回 (文本, 原换行符, 是否有 BOM)。
|
||||
|
||||
本仓库 core.autocrlf=true 且无 .gitattributes,工作区换行混杂
|
||||
(README=LF、AGENTS=CRLF);带 BOM 文件(App/cfg_file.c 等)另有铁律。
|
||||
--write 必须**按原样回写**,否则整文件行尾/BOM 漂移 → 巨型无意义 diff。
|
||||
"""
|
||||
raw = path.read_bytes()
|
||||
bom = raw.startswith(b"\xef\xbb\xbf")
|
||||
body = raw[3:] if bom else raw
|
||||
crlf = body.count(b"\r\n")
|
||||
lf_only = body.count(b"\n") - crlf
|
||||
newline = "\r\n" if crlf > lf_only else "\n"
|
||||
text = body.decode("utf-8", errors="replace")
|
||||
# 统一成 \n 供内部处理
|
||||
text = text.replace("\r\n", "\n").replace("\r", "\n")
|
||||
return text, newline, bom
|
||||
|
||||
|
||||
def write_doc(path: pathlib.Path, text: str, newline: str, bom: bool):
|
||||
"""按原换行符 / BOM 状态回写(text 内部为 \n)。"""
|
||||
out = text if newline == "\n" else text.replace("\n", newline)
|
||||
data = out.encode("utf-8")
|
||||
if bom:
|
||||
data = b"\xef\xbb\xbf" + data
|
||||
path.write_bytes(data)
|
||||
|
||||
|
||||
def main(argv):
|
||||
ap = argparse.ArgumentParser(description="FreeRTOS 任务表生成/校验")
|
||||
g = ap.add_mutually_exclusive_group()
|
||||
g.add_argument("--check", action="store_true", help="检查模式(默认)")
|
||||
g.add_argument("--write", action="store_true", help="就地重写受管表格区")
|
||||
g.add_argument("--diff", action="store_true", help="只打印期望表格")
|
||||
args = ap.parse_args(argv[1:])
|
||||
|
||||
if not FREERTOS_C.exists():
|
||||
print(f"[FAIL] 找不到事实源:{FREERTOS_C}", file=sys.stderr)
|
||||
return 1
|
||||
c_text, _nl, _bom = read_doc(FREERTOS_C)
|
||||
tasks = parse_tasks(c_text)
|
||||
if not tasks:
|
||||
print("[FAIL] 未能从 Src/freertos.c 解析出任何任务,请检查正则与源码格式。",
|
||||
file=sys.stderr)
|
||||
return 1
|
||||
tasks = sort_tasks(tasks)
|
||||
|
||||
if args.diff:
|
||||
for t in TARGETS:
|
||||
with_role = t["path"].name == "README.md"
|
||||
print(f"=== {t['path'].name} ===")
|
||||
print(render_table(tasks, t["header"], with_role))
|
||||
print()
|
||||
return 0
|
||||
|
||||
changed, missing, problems = [], [], []
|
||||
for t in TARGETS:
|
||||
if not t["path"].exists():
|
||||
problems.append(f"{t['path'].name} 不存在")
|
||||
continue
|
||||
text, newline, bom = read_doc(t["path"])
|
||||
with_role = t["path"].name == "README.md"
|
||||
table = render_table(tasks, t["header"], with_role)
|
||||
|
||||
if args.write:
|
||||
new_text, did = replace_managed(text, t, table)
|
||||
if did:
|
||||
write_doc(t["path"], new_text, newline, bom)
|
||||
changed.append(t["path"].name)
|
||||
else:
|
||||
got = extract_managed(text, t)
|
||||
if got is None:
|
||||
missing.append(t["path"].name)
|
||||
else:
|
||||
# 提取区已归一为 \n;与期望表格比较时同样归一
|
||||
if got.replace("\r\n", "\n") != table.strip():
|
||||
changed.append(t["path"].name)
|
||||
|
||||
if args.write:
|
||||
if missing:
|
||||
print("[WARN] 以下文档缺少受管锚点,未写入:"
|
||||
+ ", ".join(missing))
|
||||
print(f" 请在任务表上下各加一行锚点:\n"
|
||||
f" {TARGETS[0]['begin']}\n {TARGETS[0]['end']}")
|
||||
if changed:
|
||||
print("已更新任务表:" + ", ".join(changed))
|
||||
if not changed and not missing:
|
||||
print("任务表已是最新,无需改动。")
|
||||
return 0
|
||||
|
||||
if problems:
|
||||
for p in problems:
|
||||
print(f"[FAIL] {p}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
if missing or changed:
|
||||
if missing:
|
||||
print(f"[FAIL] 以下文档缺少受管锚点 "
|
||||
f"({TARGETS[0]['begin']} / {TARGETS[0]['end']}):")
|
||||
for n in missing:
|
||||
print(f" - {n}")
|
||||
if changed:
|
||||
print(f"[FAIL] 以下文档的任务表与 Src/freertos.c 不一致:")
|
||||
for n in changed:
|
||||
print(f" - {n}")
|
||||
print("\n事实源(Src/freertos.c):")
|
||||
for name, words, prio, _ in tasks:
|
||||
print(f" {name}: {words} words / {prio}")
|
||||
print("\n期望表格(README 口径):")
|
||||
print(render_table(tasks, TARGETS[0]["header"], True))
|
||||
print("\n修复:运行 `python scripts/gen_task_table.py --write`")
|
||||
return 1
|
||||
|
||||
print(f"任务表一致({len(tasks)} 个任务,"
|
||||
f"{len(TARGETS)} 个文档)。")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main(sys.argv))
|
||||
Reference in New Issue
Block a user