Skip to content

发布与 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 的硬门槛;clippyundocumented_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

判断顺序:

  1. 先找安全抽象。想省一次边界检查?先量一下——get_unchecked 带来的收益通常被 unsafe 带来的审计与维护成本吃掉。绝大多数性能问题出在算法与数据布局,不在边界检查。
  2. 标准库/生态已有安全封装就直接用slice::from_raw_parts 之外还有 split_at/chunksVec 的手工指针操作可以被 slice 方法替代。
  3. 需要 unsafe impl Send/Sync 时先问「我真的懂这个类型的内存模型吗」。这是最容易被错误承诺的地方,会导致数据竞争——安全代码里的 UB
  4. 不要为了「避免 clone」而用 unsafe。先重构借用关系,或换数据结构(Rc/Arc/Cow/arena)。
  5. 真正合理的场景:FFI 边界、实现底层数据结构(Vec/HashMap 级别)、与硬件/内存映射交互、性能关键且有 benchmark 证明的热点。

🧠 原理:安全抽象(safe abstraction)的契约是「只要外部用安全 API,就不可能触发 UB」。因此 unsafe 块内部的正确性论证必须只依赖已校验的前提。一旦你的安全 API 让用户能构造出违反内部不变量(如 lenptr 不匹配)的值,整个抽象就退化成「UB 的触发器」,这比直接暴露 unsafe 更糟——因为用户会以为自己处在安全区。



发布与分发

cargo publish 前置检查

powershell
# 1) 看看到底会打包哪些文件(防止把测试数据、密钥、巨量 fixture 发上去)
cargo package --list

# 2) 完整跑一遍打包 + 编译验证,但不真正上传
cargo publish --dry-run

# 3) 只做打包与校验(比 dry-run 更快,适合本地反复跑)
cargo package

必备的 Cargo.toml 字段:

字段作用备注
descriptioncrates.io 搜索与列表展示一句话,别写「TODO」
licenselicense-file二者必填其一,否则被拒推荐 SPDX 表达式 "MIT OR Apache-2.0"(Rust 生态惯例)
repository源码地址crates.io 会用它做链接
documentation文档地址缺省时指向 docs.rs
readmeREADME 路径默认 README.md
keywords最多 5 个,每个 ≤20 字符全小写,影响搜索
categories只能从 crates.io 分类列表 里选最多 5 个
exclude / include打包白/黑名单或用 .gitignorecargo package 会尊重它)

发布前的版本与变更记录纪律:

  1. Cargo.tomlversion(遵循 semver,见「语义化版本与 caret 语义」)。
  2. 更新 CHANGELOG.mdAdded / Changed / Fixed / Deprecated / Removed / Security 六类。
  3. 打 git tag(v0.3.0),tag 与发布版本必须一致。
  4. 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-msvcWindows 默认,需要 MSVC 工具链
x86_64-unknown-linux-gnu动态链接 glibc,需匹配目标机的 glibc 版本
x86_64-unknown-linux-musl静态链接,单文件二进制,容器部署首选
aarch64-unknown-linux-gnuARM64,需交叉链接器
wasm32-unknown-unknownWebAssembly

cross 一句话cargo install cross 后把 cargo build --target ... 换成 cross build --target ...,它用 Docker 容器提供完整的交叉工具链与目标库,能省掉手工配置链接器的绝大部分痛苦。

二进制体积优化清单

按收益从高到低:

  1. strip = true(或 cargo install cargo-binutilsstrip)—— 通常砍掉最大一块。
  2. panic = "abort" + codegen-units = 1 + lto = "fat"
  3. opt-level = "z"(体积优先)试与 "s" 对比,二者不总是一致。
  4. 删掉未用 feature:--no-default-features + 只开需要的(大依赖如 regexchrono 影响巨大)。
  5. 换掉依赖:reqwest 换成 ureqserde_json 换成更小的替代品,等等。
  6. cargo bloat --release --crates 找出「谁最占体积」,再针对性处理。
  7. 检查是否有重复的依赖大版本(cargo tree -d)。
  8. 需要压缩壳时用 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 缓存 ~/.cargotarget--locked 断言 Cargo.lockCargo.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)PythonJS/TS (npm/pnpm)Java (Maven/Gradle)Go
依赖清单Cargo.tomlpyproject.toml / requirements.txtpackage.jsonpom.xml / build.gradlego.mod
锁定文件Cargo.lock(应用提交,库可不提交)poetry.lock / uv.lockpackage-lock.json / pnpm-lock.yamlgradle.lockfile(需开启)go.sum
添加依赖cargo add serdepoetry add requestsnpm i lodash手写 POM / gradle addgo get x/y
安装/构建cargo build(自动下依赖)poetry installnpm installmvn packagego build ./...
运行脚本cargo run -- argspython -m pkgnpm run devmvn exec:javago run .
测试cargo test(含 doctest)pytestjest / vitestJUnitgo test ./...
前端格式化cargo fmt(官方唯一标准)black / ruff formatprettiergoogle-java-formatgofmt(官方唯一标准)
静态检查cargo clippy(官方,规则分组可调)ruff / pylint / mypyeslint / tscSpotBugs / Checkstylego vet + staticcheck
文档内代码测试doctest 默认执行cargo testdoctest(pytest --doctest-modules,需显式开)无原生支持(需 tsdoc + 自建)无(Javadoc 不执行代码)Example 函数靠约定,不自动执行
条件编译#[cfg] + feature(编译期裁剪,零运行时开销)无(靠运行时判断或 extras)无(打包器 tree-shaking)无(profile / 多模块)build tag(//go:build linux
可选依赖[features] + dep:加法语义)extraspip install pkg[dev]optionalDependencies / peerDependenciesMaven optional / profile无(靠 build tag 或子模块)
依赖仲裁同一 major 内自动统一,跨 major 可并存全局一份(冲突需人工解)树形 node_modules,可多版本并存最近优先,强制单版本MVS,取最高版本
多包仓库Cargo workspace(原生,共享 lock 与 target)无原生(uv workspace / Pants / Bazel)npm workspaces / pnpm workspaceMaven multi-module / Gradle compositego.work
发布到中央仓库cargo publish(不可撤销,只能 yank)twine upload / poetry publishnpm publishmvn deploy(常配 Nexus)打 tag 即发布(模块代理拉取)
基准测试criterion(生态标配)pytest-benchmarkbenchmark.js / vitest benchJMHgo test -bench
安全审计cargo audit / cargo denypip-audit / safetynpm auditOWASP dependency-checkgovulncheck
覆盖率cargo-llvm-cov / tarpaulincoverage.pyc8 / istanbulJaCoCogo test -cover

一句话总结差异:Rust 的工程化能力几乎全部内建在 Cargo 里(构建、测试、文档、格式化、lint、发布、多包),因此「工具链选择困难」比 Python/JS 小得多;代价是 Cargo 的约定更强——比如 src/lib.rstests/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。

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]。别把 criteriontempfile 放进 [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-MITLICENSE-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.rsbuild.rs 会让 crate 无法被完全静态分析、拖慢编译、并让「同一份源码在不同机器上编译出不同产物」成为可能。

坑 11:循环依赖

text
error: cyclic package dependency: package `a` depends on itself

原因aba。Cargo 直接拒绝(不像 Python 能靠延迟 import 绕过)。 修法:把b 需要的类型抽到第三个 crate core,让 ab 都依赖它;或把 b 中依赖 a 的部分用 trait 反转(a 定义 trait,b 实现)。注意:unit test 的 #[cfg(test)] mod testsuse crate::... 不算循环依赖,这是常见误判。



速查表

我想……命令 / 写法
建 workspaceCargo.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 --opencargo doc --no-deps
跑 doctestcargo test --doc
格式化 / 检查cargo fmt / cargo fmt --check
lintcargo clippy --all-targets --all-features -- -D warnings
集中配 lintCargo.toml[lints.rust] / [lints.clippy],成员 [lints] workspace = true
基准测试cargo benchcriterionharness = false
覆盖率cargo llvm-cov --html
打包预检cargo package --listcargo 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() -> ExitCodeExitCode::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 = "理由")]


内容以 rustc 1.98.1 · Rust 2024 edition 为基准