RTL 提交前 lint 钩子模板:pre-commit 调 Verilator 在提交时拦截问题代码
更新时间:2026-11-04。模板实战页,属于芯片设计入门系列。组合逻辑缺个 else、信号忘声明就被引入版本库,等 CI 或评审发现时,问题代码已经躺在历史里了。本文给出一份可以直接抄走的 RTL 提交前 lint 钩子模板:
git commit时自动对暂存的 Verilog 跑一遍 Verilator,有告警当场拦下,修完再提。同系列的仿真自动化模板见 仿真回归控制Shell脚本模板,lint 规则本身的含义对照 Verilator lint 报错矩阵实验。
本文要回答的问题
- 一份最小可用的 RTL pre-commit 钩子要做哪几件事?
- 为什么钩子检查的是暂存区快照,而不是工作区里的文件?
- 本机没装 Verilator,怎么用 docker 镜像兜底?
- 钩子装上不生效、在 Windows 和 CI 上行为不同,分别是什么原因?
- 想加 iverilog 编译检查、想豁免某条公认告警,改动落在哪?
一、使用场景与提交拦截流
适用场景很朴素:用 git 管理 RTL 的个人练习工程或小组项目,希望「latch 推断、隐式线网、宽度不匹配这类问题在敲下 commit 的瞬间就被挡住」,而不是等到 RTL lint 25 项自查清单 统一过堂时再翻旧账。
什么时候不必用它:一次性的 throwaway 实验目录、没有 git 管理的单文件验证,直接手敲 verilator --lint-only 更轻便;公司级流程则通常把 lint 放在服务端 CI 强制,客户端钩子管不了别人的 clone,两者是互补关系而非替代。
一次提交经过的五个环节如下,钩子占中间两格。
这张卡要抓住一个要点:钩子检查的输入是「暂存快照」,出口是「退出码」。git 只认退出码——0 放行提交,任何非 0 值都中止提交,所以钩子内部的全部工作就是把 lint 结果翻译成这一个数字。
钩子内部取文件的方式值得单独看一眼,它决定了检查对象是不是你真正要提交的那份代码。
读图注意紫色那一步:快照从 index 里 git show 出来,与工作区此刻的内容无关。这是钩子正确性的关键,第三节展开。
二、完整钩子脚本
下面就是模板本体,保存为工程内 .git/hooks/pre-commit 即可使用。脚本本机有 verilator 就用本机的,没有就回退 docker 镜像 geek-eda:0.4-flow,两种路径对上层完全透明。
#!/usr/bin/env bash
# pre-commit:对暂存区里的 Verilog/SystemVerilog 文件跑 verilator lint
set -u
cd "$(git rev-parse --show-toplevel)" || exit 2
files=()
while IFS= read -r f; do
[ -n "$f" ] && files+=("$f")
done < <(git diff --cached --name-only --diff-filter=ACMR -- '*.v' '*.sv')
[ ${#files[@]} -eq 0 ] && exit 0
# 优先用本机 verilator,否则回退到 geek-eda docker 镜像
if command -v verilator >/dev/null 2>&1; then
run_lint() { verilator --lint-only -Wall "$1"; }
else
run_lint() {
docker run --rm -v "$PWD":/work -w /work geek-eda:0.4-flow \
verilator --lint-only -Wall "$1"
}
fi
# lint 的是暂存区快照(git show :file),不是工作区当前内容
tmp=$(mktemp -d .git/hooks/.lint-tmp.XXXXXX)
trap 'rm -rf "$tmp"' EXIT
fail=0
for f in "${files[@]}"; do
mkdir -p "$tmp/$(dirname "$f")"
git show ":$f" > "$tmp/$f"
echo "[pre-commit] verilator lint: $f"
if run_lint "$tmp/$f"; then
echo "[pre-commit] PASS: $f"
else
echo "[pre-commit] FAIL: $f(lint 未通过,提交已拦截;git commit --no-verify 可临时跳过)"
fail=1
fi
done
exit $fail三、逐段讲解
定位仓库根与候选文件。 git rev-parse --show-toplevel 让钩子无论在哪个子目录触发都先回到仓库根,docker 的 -v "$PWD":/work 才有稳定挂载点。文件清单来自 git diff --cached --name-only --diff-filter=ACMR:--cached 把范围限制在暂存区,ACMR 表示只取新增、复制、修改、重命名的文件,删除的 RTL 不必再 lint。pathspec '*.v' '*.sv' 让提交文档、脚本时钩子直接 exit 0 放行,不产生任何打扰。
本机优先、docker 兜底。 command -v verilator 探测本机工具链,有就走本机(毫秒级);没有则定义同名函数走容器。用 docker 时把仓库根挂成容器的 /work,容器内 verilator 看到的路径与相对结构和本机一致,同一份脚本在两种环境下行为相同。
只 lint 暂存快照。 这是最容易写错的一段。直接 lint 工作区文件有个经典漏洞:git add 之后你又顺手改了两行但忘记再次 add,此时工作区是新版、暂存区是旧版,钩子检查工作区等于检查了一个不会被提交的东西。模板用 git show ":$f" 把暂存版本导出到临时文件再检查,保证「被检查的就是将入库的」。临时目录建在 .git/hooks/ 下而不是系统临时目录,是因为它必须位于 docker 挂载点之内,容器才看得到;每次用 mktemp -d 生成唯一名并在退出时清掉,则避开了共享文件系统上「删除再建同名目录」可能出现的短暂不可见。
退出码与告警等级。 Verilator 5.020 实测:--lint-only -Wall 遇到 LATCH 这类告警时退出码就是 1(输出结尾的 Exiting due to 1 warning(s)),无需额外参数即可拦住提交。只想做提示不拦截可以加 -Wno-fatal 把告警降级;反过来,想把某条告警在任何情况下都当错误,用 -Werror-规则名,例如 -Werror-WIDTH。
四、真实跑一遍
在 /tmp 下新建临时 git 仓库,放入一个组合逻辑缺少 else 分支的 latch_demo.v(会推断出隐式锁存,详见 Verilator LATCH 告警实例),装好钩子后连续两次提交。
实录里有三个关键证据。第一,第一次提交时 verilator 报 %Warning-LATCH 并以非零退出结束,钩子打印 FAIL,commit rc=1,仓库里没有任何提交。第二,给 always @(*) 补上 else q = 1'b0; 后重新 add 再提交,钩子里同一文件这次 PASS,提交正常生成根提交。第三,输出中的检查路径是 .git/hooks/.lint-tmp.* 下的临时快照,印证了第三节「只检查暂存内容」的取法。
代码获取:钩子脚本、正反两版 latch_demo.v 与一键演示脚本在公开仓 experiments/tpl-git-precommit-rtl-lint,克隆后执行 bash run_demo.sh 即可在 /tmp 下复现拦截与放行全过程。
五、使用注意事项
安装与执行位。 钩子文件必须命名为 .git/hooks/pre-commit 且带可执行位,复制后执行 chmod +x .git/hooks/pre-commit——少了执行位是钩子「装了没反应」的头号原因。.git/hooks/ 不随仓库分发,团队共享时把脚本放进被版本管理的目录(如 scripts/hooks/pre-commit),每人执行一次 git config core.hooksPath scripts/hooks 指向它即可。git 操作上的其他常见坑见 芯片工程的 Git 使用陷阱。
Windows 差异。 Git for Windows 自带 bash 执行 shell 钩子,脚本本身可直接用,但 Windows 上没有 Unix 执行位概念,确保文件名为 pre-commit(无扩展名)即可被识别;走 docker 兜底需要 Docker Desktop 正在运行,且仓库位于共享挂载范围内。团队成员工具不统一时,最省心的办法是让所有人都走容器分支,版本与告警口径天然一致。
CI 差异。 客户端钩子可以被 git commit --no-verify 跳过,也管不到 Web 端合并,所以 CI 里必须把同一条 verilator --lint-only -Wall(或你们的 Makefile lint 目标,见 Makefile 与 Shell 的 EDA 管理分工)再跑一遍作为强制门。钩子负责即时反馈,CI 负责不可绕过,两层都要。
速度与逃生门。 钩子只检查本次暂存的文件,十个文件的提交也就十次秒级 lint;走容器时每次 docker run 有固定启动开销,文件多可以把文件列表一次性传给 verilator 而不是逐个起容器。--no-verify 是留给「临时合并点、CI 随后兜底」的逃生门,不应成为日常习惯。
六、修改示范
加 iverilog 编译检查。 lint 查风格与可综合性线索,编译则查语法和例化完整性,两道门可以叠加。镜像里自带 Icarus Verilog 12.0,在 for 循环里 lint 之后追加一段:
run_compile() {
docker run --rm -v "$PWD":/work -w /work geek-eda:0.4-flow \
iverilog -g2012 -o /dev/null "$1"
}
run_compile "$tmp/$f" || { echo "[pre-commit] COMPILE FAIL: $f"; fail=1; }iverilog 只做语法编译不跑仿真,纯 RTL 文件没有 testbench 也能通过,适合放在提交前;需要真正跑激励的检查属于回归脚本的职责,不在钩子里做。
加 waiver 豁免文件。 对团队评审确认可接受的告警,不要全局 -Wno-LATCH 一刀关掉,用 waiver 文件精确豁免。先用 --waiver-output waivers.vlt 让 Verilator 生成豁免建议,把其中对应行的注释去掉后随仓库管理:
`verilator_config
lint_off -rule LATCH -file "*latch_demo.v" -match "Latch inferred for signal *".vlt 是 Verilator 的配置文件,作为普通源文件参数追加到命令行末尾即可生效:verilator --lint-only -Wall waivers.vlt "$1"。实测对本文反例加上这份豁免后退出码由 1 变 0。豁免必须带 -file 与 -match 限定范围,避免新文件里的同类告警被悄悄放过。
实验环境
- 机器/系统:Mac mini / macOS(Darwin 25)
- 工具版本:Git 2.42.1;Verilator 5.020 与 Icarus Verilog 12.0 均来自 docker 镜像
geek-eda:0.4-flow(verilator --version输出Verilator 5.020 2024-01-01 rev (Debian 5.020-1)) - 日期:2026-11-04
- 完整命令:
bash run_demo.sh(脚本在/tmp下建临时仓库、装钩子、连续两次提交);verilator --lint-only -Wall waivers.vlt latch_demo.v(waiver 验证)
这一篇在知识体系里的位置
本篇是「把质量门槛前移到敲回车那一刻」的最小工程件:规则含义对照 Verilator lint 报错矩阵实验 与 RTL lint 25 项自查清单,提交后的批量自动化接 仿真回归控制Shell脚本模板,工程组织上的分工见 Makefile 与 Shell 的 EDA 管理分工。
总结
RTL 提交前 lint 钩子模板的核心是四点:用 git diff --cached 圈定暂存文件、用 git show :file 导出暂存快照作为检查输入、把 verilator --lint-only -Wall 的退出码直接作为钩子退出码、本机无工具时用 docker 镜像透明兜底。Verilator 5.020 下 -Wall 告警本身即返回非零,问题提交被拦在入库之前;经临时仓库真实验证,缺 else 的 latch 代码首次提交被拦截、补全后第二次提交成功。安装记得加执行位,团队共享用 core.hooksPath,CI 再跑同一条命令补上不可绕过的一层。