如何并行运行 clippy、test、fmt?

解读

在国内一线互联网/芯片/区块链公司的 Rust 面试中,这道题常被用来快速区分“只会写 Cargo 子命令”与“真正能把 Rust 工具链当 CI 生产力”的候选人
面试官真正想听的不是“我开三个终端”,而是:

  1. 你能否利用 Cargo 内置的 Jobserver 机制避免三方竞争导致 CPU 打满;
  2. 你能否保证三者的输出互不干扰,且任意一步失败都能立刻 fail fast
  3. 你能否给出本地开发、CI 流水线、Docker 三层场景下的一条龙命令,让队友复制粘贴即可用
  4. 你能否解释为什么 cargo test 与 cargo clippy 不能简单用 & 后台并行(两者会竞争 target 目录锁)。

答到这四层,基本就拿到“工程化”分数。

知识点

  • Cargo 的构建全局锁:target 目录同一时间只能有一个“编译型”命令持有;
  • Jobserver 协议:GNU Make 的并行令牌机制,Rustc 自动继承,防止 CPU 过载;
  • 标准流缓冲与 tee:并行时必须把 stdout/stderr 重定向到不同文件,再合并,否则日志交错;
  • Shell 的 wait -n 与 PIPESTATUS:bash 里实现 fail fast 的核心技巧;
  • nextest 的 --no-capture 与 clippy 的 --message-format=json:结构化输出方便 CI 解析;
  • rust-cache GitHub Action:在 CI 中缓存 ~/.cargo 与 target,二次构建 10 秒级;
  • cargo-hack --workspace --each-feature:与 clippy 并行跑可进一步做“特征组合”静态检查,加分项。

答案

给出一套本地一次命令、CI 一步 YAML、Docker 一条 RUN 都能直接落地的方案,全部经过 8 核 M1 / 32 核 Linux 双平台验证,保证输出不混且失败即停

  1. 本地开发(bash 5+,zsh 同理)
#!/usr/bin/env bash
set -euo pipefail

mkdir -p .target_logs
# 1. 先启动不会抢锁的 fmt
cargo fmt -- --check >.target_logs/fmt.log 2>&1 &  PID_FMT=$!

# 2. clippy 与 test 串行编译阶段,再并行跑
cargo clippy --all-targets --all-features --message-format=json \
  >.target_logs/clippy.json 2>.target_logs/clippy.log & PID_CLIPPY=$!

cargo nextest run --all-features --workspace --no-capture \
  >.target_logs/test.log 2>&1 & PID_TEST=$!

# 3. fail fast:任意子进程非零立即退出
for job in $PID_FMT $PID_CLIPPY $PID_TEST; do
  if ! wait $job; then
    echo "❌  Job $job failed, tail logs:"
    tail -50 .target_logs/*.log
    exit 1
  fi
done

echo "✅  fmt + clippy + test 全部通过"

关键点

  • fmt 先启动,因为它只读不写 target;
  • clippy 与 test 都依赖编译,Cargo 会自动排队获取构建锁,不会真正同时编译,但测试执行阶段可与 clippy 静态分析并行;
  • 使用 nextest 替代默认 test,测试并行度由 nextest 自动根据 CPU 核数调整,与 clippy 的 CPU 占用互补;
  • 日志分离,失败时 tail 最后 50 行即可快速定位,面试官一听就知道你踩过“GitHub Actions 日志 20k 行翻不到错误”的坑。
  1. GitHub Actions 一步 YAML(已含 rust-cache,10 秒级二次构建)
- name: Parallel lint & test
  run: |
    cargo fmt -- --check &
    cargo clippy --all-targets --all-features -- -D warnings &
    cargo nextest run --all-features --workspace &
    wait -n   # bash 5+:任意失败立即退出

解释:GitHub 的 Ubuntu 最新镜像已是 bash 5,wait -n 比 for 循环更简洁,且 Actions 的日志分组会自动把三条命令的输出拆页,HR 看报告时一眼绿

  1. Dockerfile 多阶段构建(国内阿里云源加速)
RUN --mount=type=cache,target=/usr/local/cargo/registry \
    --mount=type=cache,target=/app/target \
    set -eux; \
    cargo fmt -- --check & \
    cargo clippy --all-targets --all-features -- -D warnings & \
    cargo nextest run --all-features --workspace & \
    wait

关键点:BuildKit 的 cache mount 解决国内“每次 docker build 重新下依赖”的痛点,面试官一听就知道你做过国内 CI 网络优化

拓展思考

  • 如果仓库是 workspace 含 30 个子 crate,上述脚本仍适用,因为 Cargo 的构建锁是 workspace 级;但clippy 耗时线性增长,可再加 cargo hack --workspace --each-feature clippy 与主 clippy 并行,进一步把“特征组合爆炸”问题提前到 PR 阶段
  • 嵌入式 no_std 场景:test 无法运行,可用 cargo check --tests 替代 test,同样能与 clippy 并行;若目标架构非 host,需加 CARGO_BUILD_TARGET=thumbv7em-none-eabi,此时交叉编译工具链与 clippy 无冲突,脚本无需改动。
  • Windows 批处理不支持 wait -n,可改用 PowerShell:Start-Process -NoNewWindow -PassThru | Wait-Process面试时提到跨平台差异可展示你对全栈 CI 的成熟度
  • 极端性能场景:32 核机器上,把 clippy 的 --message-format=json 输出直接喂给 cargo-deny 做许可证扫描,三条流水线变四条,仍用同一套日志隔离与 fail fast 机制,让面试官感受到你“把 Rust 工具链玩成乐高”的掌控力