发布与 CI
发布流程、跨平台 CI,以及 unsafe/FFI 的原则边界。
unsafe 与 FFI 入门(只讲原则)
⚠️ 注意:本节只建立心智模型与纪律,不展开具体 FFI 实现。要写真实的 FFI 代码,请先通读 The Rustonomicon(入口见附录 D)。
unsafe 的五个超能力
unsafe 不关闭借用检查器,也不改变类型系统,它只解锁五件事:
| # | 能力 | 说明 |
|---|---|---|
| 1 | 解引用裸指针 | *const T / *mut T 的读写 |
| 2 | 调用 unsafe fn / extern 函数 | 包括 FFI 与 unsafe 标注的 API |
| 3 | 访问/修改可变静态变量 | static mut(2024 起对其取引用也是 unsafe) |
| 4 | 实现 unsafe trait | 承诺编译器无法验证的不变量 |
| 5 | 访问 union 字段 | 读 union 字段必须 unsafe |
unsafe fn / unsafe block / unsafe trait / unsafe impl 的区别
| 语法 | 含义 | 「谁」承担举证责任 |
|---|---|---|
unsafe fn f() | 调用它需要 unsafe;函数体内部不再自动是 unsafe 上下文(2024) | 调用者必须满足文档 # Safety 中的前置条件 |
unsafe { ... } | 一个块:块内允许不安全操作 | 写这个块的人,必须在块前用注释说明为什么此时满足所有前提 |
unsafe trait T | 实现该 trait 本身不安全(因为实现者要维护编译器依赖的不变量) | 实现者 |
unsafe impl T for X | 承诺「X 满足 T 的不变量」 | 写这行的作者(典型:unsafe impl Send for X) |
rust
/// 一个用 unsafe 实现的安全抽象示例:把裸指针包成带长度的切片。
pub struct RawSlice {
ptr: *const u8,
len: usize,
}
impl RawSlice {
/// 从裸指针构造。
///
/// # Safety
///
/// `ptr` 必须指向至少 `len` 个已初始化的、在 `'static` 期间有效的字节。
pub unsafe fn from_raw(ptr: *const u8, len: usize) -> Self {
Self { ptr, len }
}
/// 安全方法:安全抽象的意义就是把 unsafe 关在内部,对外只给安全 API。
#[must_use]
pub fn as_slice(&self) -> &[u8] {
// SAFETY: 构造函数已要求 ptr 至少 len 字节有效且活着,
// 且本结构体不改动该内存,故此处一定是合法的共享切片。
unsafe { std::slice::from_raw_parts(self.ptr, self.len) }
}
}
fn main() {
let data = [1u8, 2, 3];
// SAFETY: data 是栈上数组,存活于 main 期间,长度正确。
let raw = unsafe { RawSlice::from_raw(data.as_ptr(), data.len()) };
assert_eq!(raw.as_slice(), &[1, 2, 3]);
println!("ok");
}// SAFETY: 注释规范
每一条不安全操作都必须紧跟(或紧前)一条 // SAFETY: 注释,说明为什么此刻所有前提都成立。这不是可选项,而是团队 review 的硬门槛;clippy 的 undocumented_unsafe_blocks 可以强制它。
rust
// clippy.toml 里可以要求:
// # 在 [lints.clippy] 中启用
// undocumented_unsafe_blocks = "deny"repr(C) 与 FFI 边界
Rust 的默认 repr(Rust) 不保证字段顺序、不保证布局(编译器可以重排以减小体积、可以插入填充)。跨越 FFI 边界的类型必须显式声明布局:
rust
/// 与 C 的结构体布局兼容:字段顺序固定、按 C 规则对齐。
#[repr(C)]
#[derive(Debug)] // repr(C) 只管布局,Debug 这类能力仍要单独 derive
pub struct Point {
/// 横坐标。
pub x: f64,
/// 纵坐标。
pub y: f64,
}
// extern "C":C ABI、C 调用约定,是跨语言边界的事实标准。
// 2024 edition 起,extern 块本身也必须标 unsafe。
unsafe extern "C" {
/// 由 C 侧提供的函数(仅示意,真实项目需链接对应库)。
fn abs(input: i32) -> i32;
}
fn main() {
let p = Point { x: 0.0, y: 0.0 };
println!("{p:?}");
// 调用外部函数需要 unsafe:编译器无法检查 C 侧的契约
// let n = unsafe { abs(-3) }; // 未链接真实库,故此处注释掉
}其他要点:#[repr(C)] 用于结构体/枚举;#[repr(transparent)] 用于 newtype(保证与内部类型完全相同的 ABI,是 FFI 包装的常用手段);#[repr(u8)] 等给枚举指定整数表示;FFI 边界的函数不应 panic(unwind 穿过 extern "C" 是未定义行为或直接 abort),所以要 catch_unwind 或改成返回错误码。
工具一句话:bindgen 把 C 头文件生成 Rust 绑定(C → Rust);cbindgen 把 Rust 代码生成 C 头文件(Rust → C);pyo3 写 Python 扩展模块;napi-rs 写 Node.js 原生插件。
什么时候不该用 unsafe
判断顺序:
- 先找安全抽象。想省一次边界检查?先量一下——
get_unchecked带来的收益通常被unsafe带来的审计与维护成本吃掉。绝大多数性能问题出在算法与数据布局,不在边界检查。 - 标准库/生态已有安全封装就直接用:
slice::from_raw_parts之外还有split_at/chunks;Vec的手工指针操作可以被slice方法替代。 - 需要
unsafe impl Send/Sync时先问「我真的懂这个类型的内存模型吗」。这是最容易被错误承诺的地方,会导致数据竞争——安全代码里的 UB。 - 不要为了「避免
clone」而用unsafe。先重构借用关系,或换数据结构(Rc/Arc/Cow/arena)。 - 真正合理的场景:FFI 边界、实现底层数据结构(
Vec/HashMap级别)、与硬件/内存映射交互、性能关键且有 benchmark 证明的热点。
🧠 原理:安全抽象(safe abstraction)的契约是「只要外部用安全 API,就不可能触发 UB」。因此
unsafe块内部的正确性论证必须只依赖已校验的前提。一旦你的安全 API 让用户能构造出违反内部不变量(如len与ptr不匹配)的值,整个抽象就退化成「UB 的触发器」,这比直接暴露unsafe更糟——因为用户会以为自己处在安全区。
发布与分发
cargo publish 前置检查
powershell
# 1) 看看到底会打包哪些文件(防止把测试数据、密钥、巨量 fixture 发上去)
cargo package --list
# 2) 完整跑一遍打包 + 编译验证,但不真正上传
cargo publish --dry-run
# 3) 只做打包与校验(比 dry-run 更快,适合本地反复跑)
cargo package必备的 Cargo.toml 字段:
| 字段 | 作用 | 备注 |
|---|---|---|
description | crates.io 搜索与列表展示 | 一句话,别写「TODO」 |
license 或 license-file | 二者必填其一,否则被拒 | 推荐 SPDX 表达式 "MIT OR Apache-2.0"(Rust 生态惯例) |
repository | 源码地址 | crates.io 会用它做链接 |
documentation | 文档地址 | 缺省时指向 docs.rs |
readme | README 路径 | 默认 README.md |
keywords | 最多 5 个,每个 ≤20 字符 | 全小写,影响搜索 |
categories | 只能从 crates.io 分类列表 里选 | 最多 5 个 |
exclude / include | 打包白/黑名单 | 或用 .gitignore(cargo package 会尊重它) |
发布前的版本与变更记录纪律:
- 改
Cargo.toml的version(遵循 semver,见「语义化版本与 caret 语义」)。 - 更新
CHANGELOG.md:Added/Changed/Fixed/Deprecated/Removed/Security六类。 - 打 git tag(
v0.3.0),tag 与发布版本必须一致。 cargo publish是不可撤销的:版本号一旦占用就不能删除,只能cargo yank(撤回,已下载的用户不受影响)。
cargo install 与本地安装
powershell
cargo install --path . # 从当前目录安装到 ~/.cargo/bin
cargo install --path . --force # 覆盖已安装的同名二进制
cargo install ripgrep --locked # 从 crates.io 安装,--locked 用发布者的 lock(更可复现)
cargo install --list # 列出已安装💡 对照:
cargo install类似pipx install/npm i -g,但它是从源码编译的(因此需要 Rust 工具链),适合命令行工具,不用于库依赖。
release profile 调优
toml
[profile.release]
opt-level = 3 # 默认;追求体积可试 "s" 或 "z"
lto = "thin" # 跨 crate 内联;"fat" 更激进但编译慢很多
codegen-units = 1 # 单编译单元 → 更多内联与优化机会,编译变慢
panic = "abort" # 去掉 unwind 表,体积/性能双赢,但失去 catch_unwind
strip = true # 剥离符号与调试信息(1.59+ 稳定)
debug = false # 与 strip 二选一即可;想保留行号排障可设 1
incremental = false # release 下增量编译通常无益
# 「要体积」的专用 profile:cargo build --profile release-small
[profile.release-small]
inherits = "release"
opt-level = "z"
lto = "fat"
codegen-units = 1
panic = "abort"
strip = true⚠️ 陷阱:
lto = true等价于"fat",会显著拉长编译时间;CI 上若只做测试,用cargo test(dev profile)就够,别把 release 调优塞进每次 PR。另外panic = "abort"会让#[should_panic]测试无法工作——所以它只应放在 release profile,绝不能放 dev/test。
交叉编译
powershell
rustup target add x86_64-unknown-linux-musl # 加目标平台的标准库
cargo build --release --target x86_64-unknown-linux-musl| 目标三元组 | 特点 |
|---|---|
x86_64-pc-windows-msvc | Windows 默认,需要 MSVC 工具链 |
x86_64-unknown-linux-gnu | 动态链接 glibc,需匹配目标机的 glibc 版本 |
x86_64-unknown-linux-musl | 静态链接,单文件二进制,容器部署首选 |
aarch64-unknown-linux-gnu | ARM64,需交叉链接器 |
wasm32-unknown-unknown | WebAssembly |
cross 一句话:cargo install cross 后把 cargo build --target ... 换成 cross build --target ...,它用 Docker 容器提供完整的交叉工具链与目标库,能省掉手工配置链接器的绝大部分痛苦。
二进制体积优化清单
按收益从高到低:
strip = true(或cargo install cargo-binutils后strip)—— 通常砍掉最大一块。panic = "abort"+codegen-units = 1+lto = "fat"。opt-level = "z"(体积优先)试与"s"对比,二者不总是一致。- 删掉未用 feature:
--no-default-features+ 只开需要的(大依赖如regex、chrono影响巨大)。 - 换掉依赖:
reqwest换成ureq,serde_json换成更小的替代品,等等。 - 用
cargo bloat --release --crates找出「谁最占体积」,再针对性处理。 - 检查是否有重复的依赖大版本(
cargo tree -d)。 - 需要压缩壳时用
upx(注意某些平台的安全软件/企业策略会拦截)。
CI/CD 与跨平台
GitHub Actions 最小可用 workflow
yaml
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [main]
pull_request:
env:
CARGO_TERM_COLOR: always
RUSTFLAGS: "" # 保持为空,避免影响依赖的编译警告策略
jobs:
check:
name: fmt + clippy
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt, clippy
- uses: Swatinem/rust-cache@v2
- run: cargo fmt --all --check
- run: cargo clippy --all-targets --all-features -- -D warnings
test:
name: test (${{ matrix.os }})
strategy:
fail-fast: false # 一个平台失败不要掩盖其他平台的结果
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- run: cargo test --all-features --locked
- run: cargo test --no-default-features # 验证 feature 的最小组合
docs:
name: docs
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- run: cargo doc --no-deps --all-features
env:
RUSTDOCFLAGS: -D warnings # 文档里的坏链接/坏代码块即失败要点解释:matrix 覆盖三平台(跨平台问题只在真机上暴露,尤其是路径、换行、缺 native 依赖);rust-cache 缓存 ~/.cargo 与 target;--locked 断言 Cargo.lock 与 Cargo.toml 一致(防止 CI 静默升级依赖);--no-default-features 那一行是 feature 加法纪律的自动化。
测试与覆盖率工具
| 工具 | 一句话 |
|---|---|
cargo-nextest | 更快的测试运行器,进程隔离、失败重试、更好的输出;cargo install cargo-nextest && cargo nextest run |
cargo-llvm-cov | 基于 LLVM 的覆盖率,支持 --html 与 CI 上传统计;cargo llvm-cov --html |
tarpaulin | 另一个覆盖率方案,Linux 上常用;精度不如 llvm-cov |
cargo-hack | 枚举 feature / 版本组合,验证全集可编译 |
cargo-mutants | 变异测试:自动改代码看测试是否能发现 |
cargo-fuzz | 基于 libFuzzer 的模糊测试 |
cargo-semver-checks | 对比两个版本的公开 API,自动判定改动是否违反 semver |
🚀 进阶:
cargo-semver-checks值得进 CI——它是「lib 作者忘记自己做了破坏性变更」这一经典事故的自动化防线。
Windows/Linux 路径与换行差异
路径:一律用 PathBuf/Path,用 .join() 拼,不要用字符串拼接、不要硬编码 / 或 \。
rust
use std::path::{Path, PathBuf};
fn main() {
// 好:跨平台
let dir = Path::new("data");
let f: PathBuf = dir.join("nested").join("file.txt");
println!("{}", f.display()); // display() 是给人看的;转发给别人用 to_string_lossy
// 反例(不要写):
// let bad = format!("{}/{}", "data", "file.txt"); // Windows 下也能跑,但语义不清晰
// let worse = "data\\file.txt"; // Linux 下是字面文件名
// 平台专属逻辑用 cfg 隔离
#[cfg(windows)]
println!("Windows 路径分隔符是反斜杠");
#[cfg(unix)]
println!("Unix 路径分隔符是正斜杠");
// 文件名的类型安全:OsString 而非 String(Windows 文件名可以不是合法 UTF-8)
if let Some(name) = f.file_name() {
println!("文件名 {}", name.to_string_lossy());
}
}换行(\r\n vs \n):
rust
use std::io::BufRead;
fn read_lines(path: &std::path::Path) -> std::io::Result<Vec<String>> {
let f = std::fs::File::open(path)?;
// lines() 会吃掉 \n 和 \r\n —— 这是跨平台读文本的首选
let mut out = Vec::new();
for line in std::io::BufReader::new(f).lines() {
// 注意:lines() 仍然会剥掉行尾的 \r(它按 \r\n 或 \n 分割),
// 但如果文件里有「孤立 \r」,就需要自己 trim
out.push(line?);
}
Ok(out)
}
fn main() -> std::io::Result<()> {
// 二进制模式不受换行影响;文本模式在 Windows 上会被转换
// 快照测试 / 生成的代码里写死 "\n",否则 CI 在 Windows 上会 diff 失败
let text = "a\nb\n";
std::fs::write("tmp_demo.txt", text)?;
let back = std::fs::read_to_string("tmp_demo.txt")?;
assert_eq!(back, text); // read_to_string 不做换行转换
let _ = std::fs::remove_file("tmp_demo.txt");
Ok(())
}⚠️ 陷阱:GUI 与代码生成最容易踩换行坑。若测试断言里硬编码了
\n,而某处经过「文本模式」写文件(Windows 会写成\r\n),CI 在 Windows 上就会失败。对策:断言前统一.replace("\r\n", "\n");在仓库加.gitattributes强制* text=auto eol=lf;快照测试工具(如insta)里统一规范换行。
其他跨平台注意点:cfg(windows)/cfg(unix) 分支的行为也要一致(例如 Unix 有可执行权限位、Windows 有 .exe 后缀);std::os::windows::... 与 std::os::unix::... 里的平台扩展 trait 要 use 才能用;时区数据库在 Windows 上可能缺失(chrono 会退化)。
Git hooks(可选)
pre-commit:跑cargo fmt --check+cargo clippy -- -D warnings,拦住格式问题。commit-msg:校验 Conventional Commits 格式,便于自动生成 CHANGELOG。- 工具:
cargo-husky(开发依赖即可自动装 hook,但会写进用户.git/hooks,团队需约定)、lefthook/pre-commit(语言无关,配置进仓库,推荐)。 - 原则:hook 里只放秒级检查。跑完整测试的 hook 会让人习惯性
--no-verify,反而降低门槛。
与其他语言的工程化对照
| 主题 | Rust(Cargo) | Python | JS/TS (npm/pnpm) | Java (Maven/Gradle) | Go |
|---|---|---|---|---|---|
| 依赖清单 | Cargo.toml | pyproject.toml / requirements.txt | package.json | pom.xml / build.gradle | go.mod |
| 锁定文件 | Cargo.lock(应用提交,库可不提交) | poetry.lock / uv.lock | package-lock.json / pnpm-lock.yaml | gradle.lockfile(需开启) | go.sum |
| 添加依赖 | cargo add serde | poetry add requests | npm i lodash | 手写 POM / gradle add | go get x/y |
| 安装/构建 | cargo build(自动下依赖) | poetry install | npm install | mvn package | go build ./... |
| 运行脚本 | cargo run -- args | python -m pkg | npm run dev | mvn exec:java | go run . |
| 测试 | cargo test(含 doctest) | pytest | jest / vitest | JUnit | go test ./... |
| 前端格式化 | cargo fmt(官方唯一标准) | black / ruff format | prettier | google-java-format | gofmt(官方唯一标准) |
| 静态检查 | cargo clippy(官方,规则分组可调) | ruff / pylint / mypy | eslint / tsc | SpotBugs / Checkstyle | go vet + staticcheck |
| 文档内代码测试 | doctest 默认执行(cargo test) | doctest(pytest --doctest-modules,需显式开) | 无原生支持(需 tsdoc + 自建) | 无(Javadoc 不执行代码) | Example 函数靠约定,不自动执行 |
| 条件编译 | #[cfg] + feature(编译期裁剪,零运行时开销) | 无(靠运行时判断或 extras) | 无(打包器 tree-shaking) | 无(profile / 多模块) | build tag(//go:build linux) |
| 可选依赖 | [features] + dep:(加法语义) | extras(pip install pkg[dev]) | optionalDependencies / peerDependencies | Maven optional / profile | 无(靠 build tag 或子模块) |
| 依赖仲裁 | 同一 major 内自动统一,跨 major 可并存 | 全局一份(冲突需人工解) | 树形 node_modules,可多版本并存 | 最近优先,强制单版本 | MVS,取最高版本 |
| 多包仓库 | Cargo workspace(原生,共享 lock 与 target) | 无原生(uv workspace / Pants / Bazel) | npm workspaces / pnpm workspace | Maven multi-module / Gradle composite | go.work |
| 发布到中央仓库 | cargo publish(不可撤销,只能 yank) | twine upload / poetry publish | npm publish | mvn deploy(常配 Nexus) | 打 tag 即发布(模块代理拉取) |
| 基准测试 | criterion(生态标配) | pytest-benchmark | benchmark.js / vitest bench | JMH | go test -bench |
| 安全审计 | cargo audit / cargo deny | pip-audit / safety | npm audit | OWASP dependency-check | govulncheck |
| 覆盖率 | cargo-llvm-cov / tarpaulin | coverage.py | c8 / istanbul | JaCoCo | go test -cover |
一句话总结差异:Rust 的工程化能力几乎全部内建在 Cargo 里(构建、测试、文档、格式化、lint、发布、多包),因此「工具链选择困难」比 Python/JS 小得多;代价是 Cargo 的约定更强——比如 src/lib.rs、tests/、benches/、examples/ 这些目录名不能随意改。
最佳实践清单(Checklist)
可直接放进 CONTRIBUTING.md 或 PR 模板:
API 设计
代码风格
工程组织
常见坑与编译错误
坑 1:file for module 'net' found at both ...
text
error: file for module `net` found at both "src\net.rs" and "src\net\mod.rs"原因:两种模块文件风格混用。 修法:删掉其中一个,全项目统一风格(推荐 net.rs + net/)。
坑 2:unresolved import / use of undeclared crate or module
text
error[E0432]: unresolved import `crate::utils`原因:mod utils; 忘了声明(Rust 不会自动发现文件),或路径没写 crate::(2018 后 use 不允许隐式相对)。 修法:在 lib.rs 或父模块里补 mod utils;;use crate::utils::... 写全前缀。
坑 3:feature X does not exist(引入 dep: 之后)
text
error: failed to select a version for `mylib`.
... required by ...
the package `mylib` does not have the feature `serde`原因:你在 [features] 里写了 json = ["dep:serde"],于是 Cargo 不再自动生成名为 serde 的隐式 feature;下游仍写 features = ["serde"]。 修法:下游改用 features = ["json"],或上游额外提供 serde = ["dep:serde"] 作为兼容别名(记得写进 CHANGELOG)。
坑 4:feature 统一导致「我明明关了,却被打开了」
text
# 现象:cargo tree -e features 显示 mylib 的 "full" 被启用,但你自己的 Cargo.toml 没写原因:依赖图里另一个 crate 启用了 mylib/full;Cargo 对同一版本做 feature 统一(additive),最终集合是并集。 修法:不要依赖「我的 feature 没开」来做行为分支;feature 必须是加法的。想严格隔离,只能靠不同的 crate/进程,而不是 feature。
坑 5:multiple packages link to native library 'foo'
text
error: multiple packages link to native library `openssl`, but ...原因:两个不同版本的 -sys crate 都声明了 links = "openssl"。Cargo 只允许一个 links 持有者。 修法:让两个依赖对齐到同一个 openssl-sys 版本(cargo tree -i openssl-sys 找出责任人,cargo update -p 或升级上游)。
坑 6:dev-dependencies 泄漏到 release
text
error: no matching package named `criterion` found # 在 cargo build --release 时原因:把只用于测试的 crate 写进了 [dependencies],或者反过来——在 src/ 非 #[cfg(test)] 的代码里 use 了 dev-dependency(dev-dependency 对 lib/bin 的正常构建不可见)。 修法:测试专用 → [dev-dependencies];src 里用到的 → [dependencies]。别把 criterion、tempfile 放进 [dependencies](会白白增加下游的依赖树)。
坑 7:include_str! 路径错误
text
error: couldn't read src/../data/tpl.txt: No such file or directory (os error 2)原因:include_str! 的相对路径基准是包含该宏调用的源文件所在目录,不是 crate 根,也不是 CARGO_MANIFEST_DIR。 修法:按源文件位置写路径;需要稳定基准时用 concat!(env!("CARGO_MANIFEST_DIR"), "/data/tpl.txt")。
坑 8:Windows 上 target 目录路径过长
text
error: failed to write `...\target\debug\build\...\out\...`
文件名或扩展名太长。 (os error 206)原因:Windows 的 260 字符路径限制,加上 Rust 依赖目录层级深、名字长。 修法(任一):在 C:\ 根附近放项目(如 C:\dev\proj);设置 CARGO_TARGET_DIR 到短路径:$env:CARGO_TARGET_DIR = "C:\t";启用 Windows 长路径支持(组策略/注册表 LongPathsEnabled=1);项目根 Cargo.toml 里配 [build] target-dir = "C:/t"(写在 .cargo/config.toml)。
坑 9:cargo publish 因缺 license 被拒
text
error: api errors (status 200 OK): missing `license` or `license-file` in ...原因:crates.io 强制要求许可证信息。 修法:license = "MIT OR Apache-2.0"(并在仓库放 LICENSE-MIT 与 LICENSE-APACHE),或 license-file = "LICENSE"。注意两者不能同时写。
坑 10:build.rs 误用
text
error: failed to run custom build command for `mylib`常见错误做法:在 build.rs 里做业务代码生成之外的副作用(写用户目录、联网、跑测试)、忘了 println!("cargo:rerun-if-changed=build.rs") 导致改动不重编、或者用 build.rs 干「本来可以用 cfg! 判断」的事。
正确姿势:
rust
// build.rs —— 只在必须「编译期探测环境/生成代码」时使用
fn main() {
// 告诉 Cargo:这些文件变了才重跑本脚本(否则默认行为是「包内任何文件变了都重跑」)
println!("cargo:rerun-if-changed=build.rs");
println!("cargo:rerun-if-changed=proto/schema.fbs");
// 把探测结果作为编译期变量传给源码:env!("MY_FLAG")
println!("cargo:rustc-env=MY_FLAG=on");
// 传递链接参数(FFI 场景)
// println!("cargo:rustc-link-lib=static=foo");
}判断标准:能用 #[cfg]、option_env!、运行时探测解决的,就不要写 build.rs。build.rs 会让 crate 无法被完全静态分析、拖慢编译、并让「同一份源码在不同机器上编译出不同产物」成为可能。
坑 11:循环依赖
text
error: cyclic package dependency: package `a` depends on itself原因:a → b → a。Cargo 直接拒绝(不像 Python 能靠延迟 import 绕过)。 修法:把b 需要的类型抽到第三个 crate core,让 a 与 b 都依赖它;或把 b 中依赖 a 的部分用 trait 反转(a 定义 trait,b 实现)。注意:unit test 的 #[cfg(test)] mod tests 里 use crate::... 不算循环依赖,这是常见误判。
速查表
| 我想…… | 命令 / 写法 |
|---|---|
| 建 workspace | 根 Cargo.toml 写 [workspace] members = ["crates/*"],成员加 [lints] workspace = true |
| 建 bin+lib 包 | cargo new --lib myapp 后手写 src/main.rs;main 里 use myapp::... |
| 加依赖 / 可选依赖 | cargo add serde --features derive / cargo add serde --optional |
| 定义 feature | [features] json = ["dep:serde"] |
| 关掉默认 feature 构建 | cargo build --no-default-features --features json |
| 查重复依赖 | cargo tree -d |
| 查谁依赖了它 | cargo tree -i serde |
| 只升级一个包 | cargo update -p serde --precise 1.0.219 |
| 安全审计 | cargo audit / cargo deny check |
| 离线构建 | cargo vendor vendor/ + .cargo/config.toml 的 [source] 替换 |
| 文档 | cargo doc --open、cargo doc --no-deps |
| 跑 doctest | cargo test --doc |
| 格式化 / 检查 | cargo fmt / cargo fmt --check |
| lint | cargo clippy --all-targets --all-features -- -D warnings |
| 集中配 lint | Cargo.toml 的 [lints.rust] / [lints.clippy],成员 [lints] workspace = true |
| 基准测试 | cargo bench(criterion,harness = false) |
| 覆盖率 | cargo llvm-cov --html |
| 打包预检 | cargo package --list、cargo publish --dry-run |
| 本地安装 | cargo install --path . |
| 交叉编译 | rustup target add x86_64-unknown-linux-musl + cargo build --target ... |
| 看二进制体积构成 | cargo bloat --release --crates |
| 日志级别 | $env:RUST_LOG = "info,mylib=debug" |
| 退出码 | fn main() -> ExitCode,ExitCode::from(2);main -> Result 失败即 1 |
| 环境变量必填校验 | std::env::var("K").map_err(|_| format!("缺少 {K}"))? |
| 全局懒初始化 | static X: LazyLock<T> = LazyLock::new(...)(1.80+,此前用 once_cell) |
| 标记忽略返回值是 bug | #[must_use = "理由"] |
| 放行一个 lint | #[expect(clippy::xxx, reason = "理由")] |