Cargo:Cargo.lock 固定了依赖,构建为什么仍然会漂移
Cargo 项目最危险的误解,是把“Rust 能编译”当成“依赖契约已经稳定”。一个 crate 在开发者机器上可能复用了旧 target,CI 在 Linux 上才触发原生链接;一个 workspace 成员打开 serde feature,另一个成员的同名依赖会得到 feature 并集;一个 build.rs 看似只是构建辅助,却能读取环境、运行外部工具并生成代码。
Cargo 同时是包管理器、依赖解析器、构建入口和 workspace 协调器。架构师要治理的不是几条命令,而是 manifest 意图、lock 结果、toolchain、平台 target、外部 registry、可执行构建扩展和最终产物之间的证据链。
先确认漂移来自 Rust,还是来自 Rust 之外
准备一台非生产开发机和一个可删除目录。Windows MSVC 目标通常还需要 Visual Studio C++ Build Tools;涉及 OpenSSL、数据库驱动或其他 native dependency 时,还要准备对应平台的编译器、链接器、头文件和系统库。Cargo.lock 无法固定这些外部输入,所以开始实验前必须把它们与 Rust 依赖分开记录。
先从 Rust Release Announcements确认稳定通道的最新发布,再用命令记录本机真正参与构建的版本、宿主平台和活动工具链:
rustup show active-toolchain
rustc --version --verbose
cargo --version --verboseCargo 的 manifest、resolver、workspace 或 registry 行为有疑问时,以随当前 stable 发布的 Cargo Reference 为入口,不从教程中的固定版本号反推当前能力。
用 rustup 安装并识别工具链
Rust 官方安装页推荐多数开发者使用 rustup。Linux、macOS 或 WSL 可使用官方安装脚本;Windows 从官方页面下载 rustup-init.exe,并按目标 ABI 安装所需构建工具。
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh企业环境不应在 CI 中每次盲目执行远程脚本。可以镜像经审批的 rustup 安装器和 toolchain,校验摘要后安装,并记录分发来源。安装完成后验证:
rustup --version
rustup show active-toolchain
rustc --version --verbose
cargo --version --verbose
rustup target list --installedWindows 还应检查命令来源:
Get-Command rustup,rustc,cargo | Format-Table Name,Sourcerustup 安装的代理命令通常位于用户目录下的 .cargo/bin。PATH 中若混有系统包管理器安装的 Rust,cargo 与 rustc 可能来自不同工具链。必须用上述命令记录事实,不能只看 IDE 状态栏。
用项目文件固定通道
项目可提交 rust-toolchain.toml:
[toolchain]
channel = "1.97.0"
profile = "minimal"
components = ["rustfmt", "clippy"]
targets = ["x86_64-unknown-linux-gnu"]精确版本提高重建稳定性,但也意味着团队要主动追踪安全和 bugfix 发布。若用 stable 通道,开发者更新时间不同会产生漂移。架构选择应明确:精确版本配升级机器人和窗口,或通道版本配每次 CI 记录真实 toolchain;不能两者都不做。
package、crate、target 与 workspace
Cargo package 是由一个 Cargo.toml 描述的分发与构建单元。crate 是一次 Rust 编译单元;一个 package 可以包含 library crate、多个 binary、example、test 和 benchmark target。workspace 则让多个 package 共享 lockfile、输出目录和部分配置。
platform/
├─ Cargo.toml # workspace manifest
├─ Cargo.lock # 整个 workspace 的解析结果
├─ rust-toolchain.toml
├─ crates/api/
│ ├─ Cargo.toml # package api
│ └─ src/main.rs # binary crate target
├─ crates/domain/
│ ├─ Cargo.toml # package domain
│ └─ src/lib.rs # library crate target
└─ target/ # 派生构建输出与增量缓存这几个对象不能混用:package 版本进入依赖解析,crate 决定编译边界,target 决定构建产物,workspace 决定多 package 的共享上下文,target/ 只是可删除重建的输出。
创建最小项目并验证
cargo new cargo-lab --bin
cd cargo-lab
cargo fmt --check
cargo check
cargo test
cargo build
cargo run将 src/main.rs 改为:
fn main() {
println!("cargo-ok");
}预期 cargo check 完成类型检查,cargo test 成功,cargo build 在 target/debug 生成产物,cargo run 输出 cargo-ok。check 不执行最终代码生成和链接,不能代替完整 build;完整 build 又不能代替测试。
查看 Cargo 实际识别的 package、target 和 feature:
cargo metadata --format-version 1 --no-deps
cargo tree
cargo tree -e featurescargo metadata 是结构化项目模型入口,适合 IDE 和治理工具使用。对其输出分享前要脱敏路径、私有 registry 和 Git URL。
Cargo.toml 表达意图,Cargo.lock 固定解析结果
一个 library package 的 manifest 可以是:
[package]
name = "order-domain"
version = "0.1.0"
edition = "2024" # Rust Edition 版本标识
rust-version = "1.85"
[dependencies]
serde = { version = "1", features = ["derive"] }
[dev-dependencies]
serde_json = "1"version = "1" 是 SemVer 需求,不是精确版本。resolver 结合所有约束选择具体版本,并写入 Cargo.lock。后续构建会尽量复用锁定结果;manifest 变化或显式 cargo update 可能改写 lockfile。
应用、服务和 workspace 通常应提交 Cargo.lock,因为它们需要固定实际部署的依赖图。library 是否提交 lockfile 要区分仓库验证和下游解析:库的消费者不会使用库仓库中的 lockfile 来解析自己的图,但提交 lockfile仍可稳定该库自身的 CI 和工具依赖。不要用一句“库不提交 lock”替代团队决策。
CI 的只读恢复入口:
cargo build --locked
cargo test --locked --workspace --all-targets--locked 在 lockfile 缺失或需要更新时失败。它不会阻止网络,也不会保证系统库、编译器和链接器一致。
resolver 与 workspace
虚拟 workspace 没有 [package],应显式声明 resolver:
[workspace]
members = ["crates/*"]
resolver = "3"
[workspace.package]
edition = "2024" # Rust Edition 版本标识
rust-version = "1.85"
[workspace.dependencies]
serde = { version = "1", features = ["derive"] }成员继承共享依赖:
[package]
name = "order-api"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
[dependencies]
serde.workspace = trueDependency Resolution 会列出当前 resolver 版本、edition 默认值和最低工具链要求。升级前先用 rustc --version --verbose 记录批准的最低工具链,再检查项目 edition 与 resolver 的组合;旧项目不能只改一个数字就完成迁移,应在最低支持 toolchain、stable 通道和目标平台组合上比较锁文件与测试。
workspace 的共享 target/ 能显著加速构建,也会让本地缓存掩盖成员边界。发布或验证单个 crate 时使用明确范围:
cargo check -p order-domain
cargo test --workspace --all-targets
cargo build --workspace --release默认成员、members 通配符和 exclude 都应审查。新增目录未进入 workspace 时,根目录全量测试可能根本没有覆盖它。
Features 是可加能力,不是互斥模式
Cargo feature 默认是 additive:多个依赖路径对同一 package 启用的 feature 会形成并集。若 A 要求 codec/x,B 要求 codec/y,最终可能同时编译 x 和 y。设计 feature 时应允许组合,避免 feature-a 与 feature-b 互斥;互斥往往在 workspace 或下游集成时才爆炸。
[features]
default = ["json"]
json = ["dep:serde_json"]
metrics = []
[dependencies]
serde_json = { version = "1", optional = true }验证不能只跑默认集合:
cargo test --workspace --all-targets
cargo test --workspace --all-targets --all-features
cargo test -p order-domain --no-default-features
cargo tree -e features -i serde--no-default-features 的作用域容易被误判,在 workspace 中应配合 -p 或明确 package feature。cargo tree -e features 可以反查谁启用了 feature。feature 名属于公共 API:删除默认 feature 或改变组合行为可能是破坏性变更。
依赖来源:registry、Git 与 path
Cargo 支持 registry、Git 和本地 path 依赖:
[dependencies]
serde = "1"
internal-sdk = { version = "2", registry = "company" }
patched-lib = { git = "https://code.example.invalid/team/lib.git", rev = "0123456789abcdef" }
local-lib = { path = "../local-lib" }registry 依赖由索引元数据与 crate 内容支持;Git 依赖应固定 rev,branch 会漂移,tag 仍可能被服务端移动;path 依赖适合 workspace 内部成员或临时开发,但发布前必须验证外部消费者能够解析。
Git URL、锁文件和诊断日志可能暴露仓库结构。不要在 URL 中嵌入 token。需要临时修补 registry crate 时使用 [patch] 并设置退出条件,不要滥用 source replacement。
Registry、index 与 sparse
默认 crates.io 使用 sparse index,可以减少传统 Git index 的体量和更新时间。自定义 registry 在 .cargo/config.toml 声明:
[registries.company]
index = "sparse+https://registry.example.invalid/index/"
credential-provider = "cargo:token"占位域名必须替换为团队入口。sparse URL 通常需要末尾 /,实际格式由 registry 服务端能力决定。配置 registry 名和 index 可以入库,凭证不可以。
source replacement 的目标是用内容完全相同的镜像或 vendor 目录替换来源:
[source.crates-io]
replace-with = "vendored-sources"
[source.vendored-sources]
directory = "vendor"Cargo 官方明确假设替代源包含与原源完全相同的代码。它不适合私有扩展 registry,也不适合修改某个依赖;私有包用 registry 配置,修改依赖用 [patch]。
凭证、代理与 CA
Cargo 官方推荐使用 credential provider 处理认证。cargo:token 可以读取凭证存储,但生产团队更适合接入操作系统密钥链、企业 provider 或短期令牌流程。不要提交 $CARGO_HOME/credentials.toml,也不要把 token 写进 Cargo.toml、.cargo/config.toml 或 index URL。
诊断配置来源:
cargo --version --verbose
cargo metadata --format-version 1 --no-deps
env | grep '^CARGO_\|^HTTP_PROXY\|^HTTPS_PROXY\|^NO_PROXY'对环境变量输出必须脱敏。代理、CA 和 TLS 可通过 Cargo [http] 配置或对应环境变量接入,例如 CARGO_HTTP_PROXY_CAINFO;团队应安装受控 CA 并保留证书轮换流程。关闭证书检查不是长期修复。
凭证至少满足只读下载与发布分离、最小 registry 作用域、短期有效、可撤销、日志不回显。CI 任务无发布职责时,不应获得 publish token。
缓存、target 与可重建性
Cargo home 保存 registry index、crate 下载和 Git checkout;项目 target/ 保存编译输出、增量缓存、build script 产物和最终二进制。查看真实位置:
echo "${CARGO_HOME:-$HOME/.cargo}"
cargo metadata --format-version 1 --no-deps
du -sh target "${CARGO_HOME:-$HOME/.cargo}" 2>/dev/null共享 target 加速 workspace,但缓存键必须包含 toolchain、target triple、profile、feature 集、影响代码生成的环境变量和系统依赖。错误复用会产生链接异常,或更糟的是错误命中旧产物。
先定向清理:
cargo clean -p <package-name>
cargo clean --release完全 cargo clean 会删除当前 target 目录,不会清理 registry 与 Git 缓存。删除用户级 Cargo home 影响所有项目,应在确认可恢复后进行。排障优先用新的 CARGO_HOME 和 CARGO_TARGET_DIR 复现,而不是破坏共享缓存。
离线、vendor、locked 与 frozen
四个概念必须分开:
--locked 禁止 lockfile 变化,仍可联网下载缺失内容。--offline 禁止联网,但只能从本地已有索引和缓存解析,结果可能受本地缓存状态影响。--frozen 等价于同时使用 --locked 和 --offline。
vendor 把 registry 与 Git 依赖复制到本地目录,并通过 source replacement 使用。
联网准备阶段:
cargo fetch --locked --target x86_64-unknown-linux-gnu
cargo vendor --locked vendor > .cargo/config.toml随后在隔离网络和空缓存中验证:
cargo test --frozen --workspace --all-targets
cargo build --frozen --workspace --release不要把 cargo build --offline 在开发者旧缓存上成功当作离线验收。跨平台离线还要预取每个 target 的依赖,并准备 native toolchain 和系统库。vendor 内容应按只读来源管理,修改上游代码用 [patch] 或 path 依赖,而不是直接改 vendor。
Build scripts 与过程宏是代码执行边界
build.rs 会在 package 编译前被编译并执行,可探测系统库、编译 C 代码、生成 Rust 文件并向 Cargo输出 cargo:: 指令。过程宏在编译阶段作为宿主平台程序运行。两者都不是“静态依赖元数据”,而是供应链代码执行入口。
审查依赖时定位它们:
cargo tree -e normal,build
cargo build -vv
cargo metadata --format-version 1-vv 可能输出环境、路径和编译命令,日志上传前必须脱敏。build script 应把输出写到 OUT_DIR,用 rerun-if-changed/rerun-if-env-changed 收窄重跑条件;交叉编译时要区分 host 和 target,不能用宿主 cfg! 推断目标平台。
团队准入需要记录新增 build dependency、proc-macro、外部命令、网络访问、生成文件和许可证。高敏构建可在无网络、只读源码、最小环境变量和受限文件系统中执行。
MSRV 与 resolver 的版本约束
package.rust-version 声明该 package 支持的最低 Rust 版本(MSRV):
[package]
rust-version = "1.85"它能让 Cargo 在过旧 toolchain 上给出明确错误,也会影响 resolver 对不兼容依赖版本的处理。声明值不是自动证明;每次新增语法、标准库 API、依赖或 build script 都可能提高真实 MSRV。
CI 至少有两个维度:批准的最低 toolchain 验证兼容性,当前 stable 验证前向状态。library 还应验证 feature 和平台矩阵。不要在失败时长期使用 --ignore-rust-version,那只是绕过声明,不会让代码真正兼容。
跨平台与交叉编译
安装目标标准库:
rustup target add x86_64-unknown-linux-gnu
rustup target add aarch64-unknown-linux-gnu
rustc --print target-list
cargo build --target aarch64-unknown-linux-gnurustup target add 通常只提供 Rust 标准库,不会自动提供 C 编译器、链接器、sysroot 或目标系统库。.cargo/config.toml 可以配置 linker:
[target.aarch64-unknown-linux-gnu]
linker = "aarch64-linux-gnu-gcc"交叉编译失败要分层:Rust target 是否已安装,依赖是否支持该 target,build script 是否误用 host,native library 是否存在,linker/sysroot 是否正确,运行时 libc/ABI 是否兼容。仅仅“编译过”还需在目标环境运行 smoke test。
平台条件依赖写入 target table,但 resolver 可能按多平台考虑依赖图。lockfile 相同不代表所有平台都能构建;CI 应覆盖真实支持矩阵,而不是用一个 lockfile 得出跨平台结论。
本机与 CI 共享同一权威入口
一个稳健入口可按以下顺序执行:
rustup show active-toolchain
rustc --version --verbose
cargo --version --verbose
cargo fmt --check
cargo check --locked --workspace --all-targets
cargo test --locked --workspace --all-targets
cargo build --locked --workspace --release再根据项目增加 cargo clippy、MSRV、feature、target 和离线任务。CI 开始前检查 Cargo.lock 已提交,结束后执行 git diff --exit-code -- Cargo.toml Cargo.lock rust-toolchain.toml,防止工具悄悄修改契约。
缓存只加速上述入口,不改变命令。构建证据保留 toolchain、target triple、profile、feature 集、lockfile 摘要、测试退出码和产物摘要。不要把完整 cargo metadata、环境变量或 -vv 日志直接公开。
常见失败:现象、判断与修复
the lock file needs to be updated but --locked was passed
manifest 与 lockfile 不一致,或 lockfile 缺失。开发分支执行普通 cargo check 生成变化,审查 Cargo.lock,再用 --locked 验证。不要在 CI 去掉 --locked 让依赖图漂移。
feature 在 workspace 中意外启用
用以下命令反查来源:
cargo tree -e features -i <crate-name>
cargo test -p <package> --no-default-features若 feature 互斥,应重构为可组合能力或拆分 crate;靠调用顺序关闭 feature 不符合 Cargo 的 additive 模型。
私有 registry 返回 401/403
先确认 registry 名、index URL 与 package 声明一致,再检查 credential provider 是否可在非交互环境取得目标 registry 的凭证。401 常是缺失或过期,403 常是身份存在但无权限,也可能由服务端策略统一伪装。禁止把 token 加到 URL 后重试并提交日志。
failed to get ... as a dependency 或 index 超时
检查代理、DNS、CA、registry 协议和 sparse URL 末尾斜线。使用新的 CARGO_HOME 复现可区分缓存命中与真实网络恢复。不要通过永久关闭证书校验解决。
Linker 或 native library 失败
记录 rustc --version --verbose、host/target triple、linker、系统包和 cargo build -vv 的脱敏输出。cargo check 成功而 build 失败,通常说明问题在代码生成、build script、native 编译或链接阶段。
--offline 在一台机器成功、另一台失败
比较 Cargo home 中已有 index、crate 和 Git checkout;检查是否为全部 target 执行过 cargo fetch。真正的修复是构建 vendor 或可审计离线包,并在空缓存隔离网络中验证。
MSRV 构建失败
确认失败来自当前 crate、依赖、feature、build dependency 还是过程宏。若真实最低版本提高,更新 rust-version、版本策略和发布说明;若要保持 MSRV,则定向降级依赖并锁定,随后在最低与 stable 双轨验证。
升级、迁移与回滚
升级前记录:
rustup show active-toolchain
cargo tree > cargo-tree.before.txt
cargo test --locked --workspace --all-targets定向升级依赖:
cargo update -p <crate-name> --precise <version>
cargo tree -d
cargo test --locked --workspace --all-targetstoolchain、edition、resolver 和依赖升级最好拆成独立变更。resolver 迁移要观察 lockfile、feature 集、MSRV 与平台差异;edition 迁移还要按官方迁移工具和编译诊断处理,不能因为源代码“看起来没变”就合并。
回滚必须恢复 Cargo.toml、Cargo.lock、rust-toolchain.toml 与 .cargo/config.toml 的一致提交,并在空 target 上重建。若产物依赖系统库或镜像,还要恢复对应构建环境;只回滚 lockfile 不一定恢复 ABI。
Feature 并集会放大未测试组合
团队应列出对外支持的 feature 组合,默认、无默认、全量和关键组合进入 CI。若组合数量爆炸,说明 crate 边界或 feature 设计需要调整,而不是继续增加条件编译。
build.rs 把构建变成受信任代码执行
依赖升级评审要查看新增 build script、过程宏和 native tool。高敏构建采用无网络、只读源码、最小 secret、受限 runner;构建阶段不应获得发布凭证。
Lockfile 固定的是 Rust 依赖,不是整个世界
系统库、链接器、环境变量、Git 子模块、target、CPU 特性和容器镜像仍会漂移。可重建声明必须列出这些输入,产物摘要只能在输入等价时比较。
Registry 镜像与 source replacement 不能混为一谈
镜像替代要求内容等价;私有扩展包属于独立 registry;临时修改属于 [patch]。混用会导致依赖身份不清、凭证路由错误和审计失真。
缓存可能跨 feature、target 和 toolchain 污染
远端或共享缓存键应包含所有编译输入,写权限按分支和信任级别隔离。受外部贡献代码影响的缓存不能直接喂给受信发布构建。
MSRV 是持续成本,不是一次声明
每次依赖升级和 feature 变化都可能抬升 MSRV。团队要定义支持窗口、验证任务和退出策略;没有持续 CI 的 MSRV 只是愿望。
诊断证据可能携带敏感信息
cargo metadata、cargo build -vv、环境变量、Git URL 和 registry 配置可能暴露路径、内部 crate、代理和 token。上传工单或公开日志前进行字段级脱敏,而不是只删除一行密码。
团队治理基线
每个 Rust 仓库需要明确 toolchain owner、workspace owner、依赖审批人和 registry owner。基线至少包含 toolchain 固定方式、MSRV、resolver、lockfile 策略、允许的 registry/Git 来源、credential provider、build script 审查、平台矩阵、缓存权限和升级窗口。
代码评审应关注新增依赖来源、默认 feature、build dependency、proc-macro、[patch]、Git rev、rust-version 和 lockfile 变化。CI 必须在干净环境使用同一入口,并定期进行空缓存与离线重建演练。
rustup、rustc、Cargo 的来源、实际版本和项目 toolchain 策略已记录。package、crate、target、workspace 和 target/ 输出职责没有混淆。Cargo.toml 表达意图、Cargo.lock 固定解析结果的边界已明确。
workspace 显式声明 resolver,成员、默认成员和继承配置经过审查。默认、无默认、全量及关键 feature 组合进入测试矩阵。registry、Git、path、[patch] 和 source replacement 各自用途清楚。
私有源凭证由 credential provider 或短期 secret 注入,未进入 URL 和仓库。代理与企业 CA 正确接入,没有关闭 TLS 校验。--locked、--offline、--frozen 和 vendor 的语义没有混用。
build script、过程宏、外部命令和 native dependency 已纳入供应链审查。MSRV 在最低 toolchain 持续验证,stable 作为前向验证。每个支持 target 有编译器、链接器、系统库和目标环境 smoke test。
CI 缓存按 toolchain、target、profile、feature 和信任边界隔离。升级和回滚会一致恢复 manifest、lock、toolchain 与构建环境。metadata、详细构建日志、私有 crate 名和 registry 信息对外分享前会脱敏。
构建事实对不上时,按断点回到一手入口
工具链来源、stable 通道或安装方式不确定时,从 Install Rust 和 Rust Release Announcements核对,再用 rustup show active-toolchain、rustc --version --verbose 与 cargo --version --verbose 记录现场。
workspace 成员、共享 lock 或 resolver 行为不符合预期时,分别查 Workspaces 和 Dependency Resolution,并结合 cargo metadata、cargo tree 观察真实图。feature 组合异常时查 Features,随后用 cargo tree -e features -i <crate-name> 反查启用路径。
私有源认证失败时查 Registry Authentication;镜像、vendor 与 [patch] 职责混乱时查 Source Replacement。构建阶段出现不明外部命令、环境读取或交叉编译偏差时,沿 Build Scripts 检查 host/target 和 OUT_DIR;离线恢复仍访问网络时,用 cargo vendor 对照 --locked、--offline 与 --frozen 的实际语义。
