pnpm 对 Cargo[patch]/[replace]源覆盖的支持:为 Rust 工作区生成并保留 Cargo.lock
【免费下载链接】pnpmFast, disk space efficient package manager项目地址: https://gitcode.com/gh_mirrors/pn/pnpm
pnpm 的 Cargo 依赖支持(crate:依赖、cargo.enabled工作区)此前无法正确处理带有路径(path)或 Git 来源的[patch]、[replace]源覆盖的 Rust 工作区。本次功能更新(见 变更记录)让pnpm install可以为这类工作区正确生成Cargo.lock,并在添加、删除、更新 crate 时保留覆盖配置;同时,Cargo lockfile 解析会提前阻断由传递依赖声明的、pnpm 不支持的 Git 传输助手(transport helper),避免解析过程执行不可信的可执行文件。读完本文,你将掌握 pnpm 在何种条件下接管 Cargo 解析、源覆盖如何被检测与校验、以及这一特性背后的实现与测试证据。
背景:pnpm 如何参与 Cargo 依赖解析
pnpm 在启用cargo.enabled: true的 workspace(见 pnpm-workspace.yaml 配置)中,可以用crate:extra@1这类说明符为 Cargo 清单添加 Rust crate 依赖(见 add.rs)。当缺少Cargo.lock时,pnpm 需要负责解析依赖并生成锁文件。其解析策略集中在 lockfile.rs 的read_or_resolve_lockfile:
- 若锁文件已存在且策略为复用(
UseExisting),直接读取; - 若配置了
--frozen-lockfile而锁文件缺失,直接报错,绝不自行生成; - 否则调用
cargo metadata并检测工作区是否含有Git 依赖或source overrides([patch]/[replace]); - 若命中,则放弃 pnpm 自研的稀疏索引解析,转交真实的
cargo generate-lockfile来完成解析; - 否则优先走 pnpr 服务端解析(
resolve_via_pnpr),最后回退到 pnpm 内置的 cargo-resolver。
也就是说,source overrides 与 git 依赖是触发「委托 Cargo 本体解析」的两类关键信号,这正是本次变更的核心逻辑。
理解 Cargo 的[patch]与[replace]源覆盖
Rust 生态中有两种在清单层面替换依赖来源的机制:
[patch.crates-io](推荐):把某个来自 crates.io 的 crate 覆盖为本地路径或 Git 仓库,例如:[package] name = "app" version = "0.1.0" [dependencies] demo = "0" [patch.crates-io] demo = { git = "https://example.com/demo", rev = "abc123" } # 或 demo = { path = "dep" }[replace](遗留机制):以精确的"名称:版本"键指定被替换的 crate:[replace] "demo:0.0.0" = { path = "dep" } # 或 "demo:1.0.0" = { git = "https://example.com/demo" }
两者都要求解析器具备与常规 registry 依赖不同的来源处理能力,因此此前 pnpm 无法为这类工作区生成锁文件。
检测逻辑:has_source_overrides如何识别覆盖
在 resolution.rs 的has_source_overrides中,pnpm 读取工作区根目录的Cargo.toml并将其解析为 TOML 表:
- 遍历
[patch]下的每一个子表(如[patch.crates-io]); - 检查顶层
[replace]表; - 只要任一覆盖表非空,即判定工作区存在 source overrides(无论覆盖来源是 path 还是 git)。
这一判定与解析解耦:判定只需要清单文本,不触发网络请求;只有判定命中后,后续cargo generate-lockfile才会真正联网解析。
覆盖来源的 Git 传输校验
对于每个声明了git = "..."的覆盖项,has_override_sources会构造git+<url>形式的SourceId并调用git::validate_transport(见 git.rs):仅当 URL scheme 属于SUPPORTED_GIT_PROTOCOLS(来自 pnpm-git-fetcher crate)时才放行,否则报错:
Cargo source <url> asks for the <scheme> transport, which pnpm does not fetch a git dependency over这保证了在 Cargo 运行之前,ext://、ssh://等 pnpm 不支持的传输协议就会被拦截,而不是等到解析中途失败或触发意外执行。
委托解析:resolve_with_cargo的隔离与安全
当检测到 source overrides 或 git 依赖时,resolve_with_cargo会:
- 强制默认 registry:若配置了自定义
cargo.indexUrl(非 crates.io),直接拒绝解析并提示「Resolving Cargo git dependencies or source overrides requires the default cargo.indexUrl. Use an existing Cargo.lock with a custom Cargo registry.」——自定义 registry 场景只能配合既有锁文件使用; - 使用工具链 sysroot 中的
cargo二进制(而非 PATH 上任意 cargo),以--manifest-path指向工作区Cargo.toml执行cargo generate-lockfile(见 resolution_command); - 通过
GIT_ALLOW_PROTOCOL环境变量把 Git 传输白名单(来自pnpm_git_fetcher::read_allowed_git_protocols)传递给 Cargo,使子进程的 git 抓取同样受控; - 把
.cargo/config/.cargo/config.toml中的unstable.bindeps与resolver.incompatible-rust-versions两项经校验后以--config key=value显式传入(见 resolution_settings),其余配置(含[env]、git-fetch-with-cli、凭据提供器)一律不进入解析进程,防止 checkout 目录中不可信的可执行助手被执行; - 支持
--offline透传。
解析完成后,pnpm 读取工作区新生成的Cargo.lock作为结果。生成流程结束后,覆盖配置仍原样保留在Cargo.toml中。
覆盖在 add / remove / update 场景下的保留
变更记录承诺「Adding, removing, and updating crates also preserve these overrides」。在 cargo_git_install.rs 集成测试 中可验证该行为:
adding_a_registry_crate_preserves_git_dependencies_and_source_overrides(第 213 行):对 git 依赖、git patch、path patch 三种工作区执行pnpm add crate:extra@1 --lockfile-only后,新锁文件既包含新增的extra(含正确 checksum),又保留原有的demo(以及sibling)git 依赖,且.cargo/config.toml不被改写;lockfile_generation_preserves_legacy_path_replacements(第 357 行):针对遗留[replace]语法,解析后锁文件同时保留被替换包(带 source)与替换包(无 source、带replace标记)的独立身份,cargo check --locked --offline可成功通过;failed_resolution_restores_manifest_lockfile_and_sources(第 280 行):当解析失败(如新增的 crate 在 registry 中 404)时,Cargo.toml、Cargo.lock、.cargo/config.toml三者全部回滚到原状,保证失败不污染工作区。
此外,pnpm remove与pnpm update复用同一套cargo generate-lockfile管线,因此覆盖信息天然随Cargo.toml保留,解析结果只反映清单当前的覆盖声明。
阻止传递依赖声明的 Git 传输助手
变更记录的后半句针对的是安全边界:传递依赖(transitive dependencies)通过Cargo.toml声明的 Git 传输协议必须受控。实现上做了两道防线:
- 解析入口校验:
has_git_dependencies(resolution.rs 第 7-13 行)提取元数据中所有 git 依赖来源并对每个来源执行validate_transport; - 执行期环境收紧:解析子进程工作目录设在工具链 sysroot,避免读取 checkout 目录中的配置;同时仅放行
GIT_ALLOW_PROTOCOL白名单中的协议。
对应测试path_patched_dependencies_cannot_execute_git_transport_helpers(第 424 行):一个 path 覆盖的本地依赖里声明helper = { git = "pnpm-test://invalid.example/repository" },并在 PATH 上放置伪造的git-remote-pnpm-test助手;执行pnpm install --lockfile-only后,安装失败且助手从未被执行,报错包含transport 'pnpm-test' not allowed。
锁文件复用场景下的 Git 包落地
若锁文件已存在(--frozen-lockfile),pnpm 不再解析,而是直接解析锁文件中的 git 包并落地到 store:git 包以git-submodules-v1-<commit>作为 slot 标识(见 git.rs 的store_slot),通过生成[source."git+<url>..."]替换块与.cargo-checksum.json(package 字段为 null)的方式,让 Cargo 以目录源形式直接使用被钉住的 commit,无需再次抓取(见 git.rs 模块注释)。frozen_install_vendors_versionless_git_members_and_path_dependencies(第 100 行)验证了冻结安装下版本号缺失的 git 成员与 path 依赖都能被正确落地且配置不被篡改。
单元测试佐证:覆盖检测的判定矩阵
resolution/tests.rs 的root_source_overrides_are_detected_and_git_transports_are_validated给出了精确的判定矩阵:
| Cargo.toml 片段 | has_source_overrides结果 |
|---|---|
[workspace] | false |
空[patch]/ 空[patch.crates-io]/ 空[replace] | false |
[patch.crates-io]+ 无 git 的其它注册表 | false |
[patch.crates-io] demo = { path = "dep" } | true |
[patch."https://example.test/index"] demo = { git = ..., rev = ... } | true |
[replace] "demo:1.0.0" = { path = "dep" } | true |
覆盖使用git = "ext://..." | 报错(transport 校验失败) |
空表判定为 false 是刻意设计:无实际覆盖项时无需委托 Cargo,可继续走 pnpm 自身更快的解析路径。
使用前提与限制小结
- 需要可用的 Cargo 工具链(
cargo/rustc可从rustc --print sysroot定位); - Git 覆盖要求配置
cargo.indexUrl为默认的 crates.io;自定义 registry 场景请使用既有Cargo.lock; - 传递依赖声明的传输协议必须落在 pnpm 支持的白名单内(如
https、file、git等),ext://等自定义传输会被拒绝; --frozen-lockfile下锁文件缺失时不会生成,而是直接报错「forbids generating」;- 解析失败时工作区文件(
Cargo.toml、Cargo.lock、.cargo/config.toml)会自动回滚。
综上,本次更新让 pnpm 在 Rust 工作区中真正补齐了「覆盖来源」场景的锁文件生成、保留与安全校验能力,使其与常规 npm 依赖安装一样具备可重复、可冻结、可回滚的安装语义。
【免费下载链接】pnpmFast, disk space efficient package manager项目地址: https://gitcode.com/gh_mirrors/pn/pnpm
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考