资讯动态

Rust RFC 1977 深度解读:Cargo 公开依赖与私有依赖(public/private dependencies)机制剖析

发布时间:2026/10/16 5:49:07 来源:尧图企业网站定制
文档【免费下载链接】rfcsRFCs for changes to Rust项目地址https://gitcode.com/gh_mirrors/rf/rfcs点击查看免费下载导读本篇文章以 Rust 官方 RFC 仓库中的 text/1977-public-private-dependencies.md 为骨架系统解读 Cargo 依赖体系中「公开依赖public dependency」与「私有依赖private dependency」的区分机制它解决什么问题、编译器与 Cargo 如何协同实现、Cargo.toml与版本解析如何变化以及发布流程中的最低版本解析校验。读完本文你将理解依赖类型「泄漏」引发版本冲突的根因掌握public true的声明方式与警告处理手段并了解该设计在后续 RFC 3516 中的演进方向如exported-private-dependencieslint 与 2024 Edition 中的默认策略。一、背景与动机依赖「泄漏」引发的版本冲突自 Rust 1.0 发布以来crates 生态迅速扩张随之产生的一些依赖模式对 Cargo 现有的依赖声明体系提出了挑战。其中最普遍的问题是crateA依赖了另一个 crateB但B的部分类型通过A的公开 API 被暴露了出去。这在实际使用中会造成严重问题。典型的失败场景是用户的代码自己也依赖B而用户依赖的B与A内部依赖的B被解析成了不同版本当这两份B实例中的类型需要被放在一起使用时它们并不匹配——用户的代码将拒绝编译而编译器给出的错误信息又不够清晰让人难以定位「两个同名 crate 的不同版本」这一根源。引入公开依赖与私有依赖的显式区分正是为了解决这些痛点把「哪些依赖属于公开 API 契约的一部分」显式化从而让 semver 约束变得可核查私有依赖可以在依赖图中被自由复制duplicated不会再引发编译错误借此解除 Cargo 中一些既有限制让一部分此前被禁止编译的代码得以编译通过。从源码结构看该 RFC 的完整设计位于 text/1977-public-private-dependencies.md其在仓库中对应的追踪议题为 rust-lang/rust#44663后续的修订版 text/3516-public-private-dependencies.md 则明确声明取代supersede本 RFC。二、核心概念什么是公开依赖与私有依赖RFC 用一组 QA 精确刻画了这两个概念。Q什么是公开依赖public dependency如果某个依赖的某些类型或 trait 通过主 crate 的公开 API 被导出那么这个依赖就是公开的。最常见的出现位置是返回值与函数参数trait 实现以及许多其他场景同理。由于「公开」对普通用户来说难以自行判断RFC 提议扩展编译器基础设施来自动检测「公开依赖」这一概念帮助用户理解它从而避免在Cargo.toml中犯错。其本质含义可以浓缩为一句话如果你升级了一个公开依赖的版本那对你自己的 crate 而言就是一个破坏性变更breaking change。Q什么是私有依赖private dependency私有依赖被包含在你的 crate 内部对 crate 的使用者实际上不可见。因此私有依赖可以在依赖图中被自由复制不会引发编译错误。这一区分还使得 Cargo 中某些「有时会阻止 crate 编译」的限制可以被放宽。Q公开依赖以后能变成私有依赖吗公开依赖只是在可达子图reachable subgraph范围内是公开的一旦 crate 不再暴露某个公开依赖它就可以变成私有的。例如一个 crate 家族可能都依赖某个提供公共类型的工具 crate此时它对每个成员都是公开依赖但如果你的 crate 只是作为该工具 crate 的使用者、且没有任何类型或 trait 进入你自己的 API那么这条依赖就应标记为私有。Q公开 / 私有在哪里定义依赖默认是私有的只有通过在Cargo.toml中为依赖添加public标记才会成为公开依赖。这也意味着在本 RFC 实现之前创建的所有 crate其全部依赖都将是私有的。Q向后兼容性如何处理继续允许「泄漏」依赖这种泄漏甚至有一些正当用途但编译器或 Cargo 会在私有依赖进入公开 API 时发出警告。未来发布新 crate 时若未显式消除这些警告或未将依赖标记为公开甚至可能变得不合法。Q可以把私有依赖中的类型当作自己的类型导出吗当前阶段不允许。原因在于目前无法强制这种类型保持「与众不同」如果用户开始依赖真正实现该类型的 crate就可能无意间开始依赖这种类型与自己的兼容性。上一问中的限制同样适用例如目前仍可以覆盖这些限制。Qsemver 与依赖如何交互变更自己的依赖在既有规则下就已经要求对库本身做 semver 升级因为对外 API 契约发生了变化。而本 RFC 使得只有公开依赖才需要承担这一要求并且允许 Cargo 在将来阻止带有 semver 违规的新 crate 版本发布。三、总体设计需要改动的六个层面RFC 的详细设计Detailed design列出了一份明确的改动清单覆盖编译链路的全部关键环节层面改动内容编译器扩展以理解何时依赖被视为公开依赖Cargo.toml支持声明公开依赖先作为 nightly 上仅可通过 opt-in 启用的不稳定 Cargo 特性Cargo Index依赖的public属性需要出现在索引中供 Cargo 在版本解析时使用Cargo 版本解析拒绝「两个版本的同一 crate 对彼此公开可达」的图解析结果cargo publish警告或阻止发布带有未声明公开依赖的 cratecargo publish版本解析按最低可能版本解析依赖以校验Cargo.toml中的最小版本声明是否正确crates.io更醒目地展示公开依赖弱化私有依赖的展示下文逐一展开。四、编译器侧改动--extern-public与泄漏警告编译器的主要改动是接受一个新的参数由 Cargo 提供、内容是公开依赖的列表参数名为--extern-public。随后当编译器发现私有依赖泄漏到 crate 的公开 API时就发出警告cargo publish可能会在其 lint 阶段把这种警告升级为错误。此外将来警告可以普遍升级为硬错误。在某些场景下允许私有依赖进入公开 API 是必要的。此时可以用#[allow(external_private_dependency)]来放行。这一做法与#[doc(hidden)]等已有技巧配合尤其有用。RFC 特别指出这很可能也是 Rust 自身中libcore与libstd这种复杂依赖关系所需要的。五、Cargo.toml声明public trueCargo.toml将支持依赖上的新参数public。旧版 Cargo 遇到该键时会发出警告但继续工作。由于依赖默认就是私有的只有少数依赖需要显式打上public标记。该能力先以不稳定 Cargo 特性unstable Cargo feature的形式在 nightly 上提供作者需要在Cargo.toml中通过特性 opt-in 后Cargo 才会利用public属性改变版本解析方式该 Cargo 不稳定特性会同时打开对应的 rustc 不稳定特性以启用上文提到的编译器改动。示例依赖声明[dependencies] url { version 1.4.0, public true }六、Cargo Indexpublic属性的传递Cargo 在版本解析时使用的 Cargo Index 中将包含Cargo.toml里声明的public属性。以名为example、公开依赖urlcrate 的索引行为例JSON 已为可读性做了美化{ name:example, vers:0.1.0, deps:[ { name:url, req:^1.4.0, public:true, features:[], optional:false, default_features:true, target:null, kind:normal } ] }每个依赖条目中的public字段这里为字符串true即为本次设计新增的信息载体。七、Cargo 版本解析公开连通子图约束这是 RFC 中最具理论色彩的部分。Cargo 将专门拒绝这样的解析图同一 crate 的两个不同版本被公开依赖、并且彼此可达。这样就能把今天在编译期才出现的怪异错误提前到版本解析期拦截。其工作原理分四步节点nodes解析图由若干节点组成这些节点是「包 ID」package id即(name, source, version)三元组。同一 crate 的不同版本是不同节点同一名称的不同来源如 git 与 crates.io也是不同节点。有向边directed edges节点之间存在有向边一条有向边代表一个依赖。例如 A 依赖 B就有一条从 A 指向 B 的有向边。边的公开/私有标记有了公开/私有依赖后每条边都可以标记为 public 或 private。于是可以得到一组仅由公开依赖边连通的子图子图内公开依赖边的方向无关紧要。每个这样的子图代表一个「互相公开依赖的 crate 生态」也就是一个「公开类型池」pool of public types——你只要接触到这个子图就能接触到池子里的全部类型。约束与错误判定每个「公开连通子图」内部要求所有 crate 恰好只有一个版本。例如每个子图内只能有一个版本的 Hyper。考察解析图中从一个节点出发的所有边对如果两条边分别指向两个不同的公开连通子图而这两个子图又包含同一 crate 的两个不同版本就判定为错误。这基本意味着「你私下同时依赖 Hyper 0.3 和 Hyper 0.4」也是错误。八、cargo publish发布时的两道检查8.1 警告未声明的公开依赖当发布新 crate 版本时Cargo 会警告编译器判定为公开、但并非来自公开依赖的那些类型和 trait。当前阶段仍允许照常发布但未来某个时期起将必须显式把所有公开依赖标记为 public或显式用#[allow(external_private_dependency)]标记。8.2 最低版本解析Lowest Version Resolution一个非常常见的现实问题是人们在Cargo.toml里写下依赖的初始版本后就再也不更新下限尽管实际代码已经用上了更新版本的功能。这在大多数时候没问题因为Cargo 一般会选择满足约束的最大可用版本约束的上界至少在特定 minor 版本内相对罕见。也就是说Cargo.toml并不是版本依赖信息的准确全貌它顶多是一个下限可能存在「不可见依赖」它们不会导致解析失败但随着 API 演化会造成编译错误。公开依赖会加剧这个问题——你可能在不知不觉中依赖了某个你甚至不知道的 crate 的「新 API」。RFC 给出了一个完整的失败链条A依赖B 1.0而B公开依赖C ^1.0D 1.0无依赖初次构建A时解析为B 1.0与C 1.1因为C的 API 通过B的 re-export 对A可用所以A实际上已经依赖C 1.1尽管B只声称依赖C ^1.0特别是A中的代码可能依赖只有C 1.1才有的 API然而如果A是库它不会提交 lockfile这些信息就此丢失。现在把A改成依赖D 1.1而D 1.1依赖C 1.0重新构建A时解析图变为B 1.0, D 1.1, C 1.0但A可能突然编译失败因为它此前通过B隐式依赖了C 1.1的特性。此类问题的共同成分是某处代码使用了比Cargo.toml中声明的版本更新版本才有的 API。为了尽早暴露这一问题cargo publish将尝试在满足约束的前提下选取最小版本来解析依赖图如果 crate 在此解析图下构建失败发布就会失败。九、如何教学与推广新术语与典型泄漏场景从用户视角看RFC 初始阶段几乎是透明的但用户一定会遇到「这些新限制意味着什么」的问题。最常见的类型泄漏途径是错误处理很多 crate 会把其他库的错误包装进自己的类型。RFC 建议识别类型泄漏的常见场景并在 lint 中给出处理提示。预期需要单独讲解的案例包括通过错误泄漏类型lint 很容易识别因为包装类型会实现std::error::Error推荐做法是鼓励把内部错误再包一层。来自其他 crate 的 trait尤其是serde等常用 crate 会频繁出现可能值得把类型与 trait 分开讲解。通过 derive 泄漏类型用户在 derive 某个 trait比如serde_derive时可能根本没意识到自己产生了依赖lint 可能想单独点名这类情况。本功能被命名为public_private_dependencies配套一个 lint 标记external_private_dependency——这就是初期引入的全部新术语。RFC 同时为将来的「别名aliasing」能力埋下伏笔私有依赖可以被强制 re-export 为 crate 自己的类型。RFC 也预见到该特性会因生态中已有的模式而广受欢迎但初期可能引发负面意见。因此建议先用 cargobomb/crater 跑一轮评估新 lint 警告的实际影响面以及距离把警告升级为错误的距离。crates.io 也应更新为分开渲染公开依赖与私有依赖。十、端到端体验从作者视角看改动10.1 单一依赖 crateonedep假设库 crateonedep依赖urlcrate且url::Url类型暴露在onedep的公开 API 中。onedep的Cargo.toml[package] name onedep version 0.1.0 [dependencies] url 1.0.0onedep的src/lib.rsextern crate url; use url::Origin; use std::collections::HashMap; #[derive(Default)] pub struct OriginTracker { origin_counts: HashMapOrigin, usize, } impl OriginTracker { pub fn log_origin(mut self, origin: Origin) { let counter self.origin_counts.entry(origin).or_insert(0); *counter 1; } }当作者把 Rust/Cargo 升级到本 RFC 完全实现后的版本会注意到两处变化1.cargo build成功但有警告。警告指出私有依赖具体是url被用在了公开 API具体是pub fn log_origin中的url::Origin类型中应考虑在Cargo.toml中加上public true。理想的警告文本大致是consider changing dependency: url 1.0.0 to: url { version 1.0.0, public true } 警告还可以进一步鼓励作者升级自己 crate 的 major 版本因为添加公开依赖本身就是破坏性变更。2.cargo publish失败。打包后的构建检查会失败。原因是url::Origin的Hash实现直到urlv1.5.1 才加入作者一直定期运行cargo updateCargo.lock中是url 1.5.1但从未把Cargo.toml里的下限更新到位。由于cargo publish会尝试把依赖解析到最低可能版本它选择了url 1.0.0——该版本并未在Origin上实现Hash于是编译失败。针对此场景应有清晰的错误消息说明Cargo 已经把 crate 解析到了最低可能版本、这可能就是编译失败的原因、作者应检查Cargo.toml中各依赖的版本下限是否需要更新。该命令还应改写Cargo.lock以便运行cargo build就能复现错误供作者修复。10.2 多依赖 cratetwodeptwodep的Cargo.toml[package] name twodep version 0.1.0 [dependencies] // this is the version of onedep above using a public dep on url 1.5.1 onedep 1.0.0 url 1.0.0twodep的src/main.rsextern crate url; use url::Origin; extern crate onedep; fn main() { let mut origin_tracker onedep::OriginTracker::default(); loop { println!(Please enter a URL!); // pseudocode because Im lazy let url stdin::readline().unwrap(); let url Url::parse(url).unwrap(); origin_tracker.log_origin(url.origin()); // other stuff } println!(Here are all the origins you mentioned: {:#?}, origin_tracker); }在本 RFC 实现之前这段代码可能已经编译失败——如果 Cargo 把对url的直接依赖解析到了与onedep解析到不同的版本如果两个版本恰好解析一致则也能编译通过。升级之后的行为若此前存在编译错误现在会变成一个版本解析问题Cargo 要么自动解决要么提示用户调整版本约束或运行cargo update。若此前能编译说明旧的解析图是好的升级后不会有任何变化。twodep是二进制 crate没有公开 API因此不会收到「依赖未标记为 public」的警告。作者升级后发布到 crates.io 时由于onedep对url的公开依赖下限已是 1.5.1Cargo 能生成的合法解析图只会包含url 1.5.1或更高版本这与twodep直接依赖的url 1.0.0也兼容发布无需任何额外改动即可成功。十一、从 RFC 1977 到 RFC 3516设计与落地演进2023 年提出的 text/3516-public-private-dependencies.md 明确取代了本 RFC。这份修订版指出了一个关键事实RFC 1977 批准后围绕 resolver 的改动一直是 6 年来稳定化的主要障碍因此新方案决定解耦 resolver 改动先把 lint 与标记机制快速落地。相比 1977主要有以下演进11.1 lint 更名与 Edition 策略编译器接受的新参数改为--extern标志上的priv修饰符例如--extern priv:serde用于把依赖标记为私有。lint 更名为exported-private-dependencies对应旧称external_private_dependency当遇到私有依赖以pub形式暴露时触发。在 2024 Edition 之前的版本中该 lint 默认allow它是rust-2024-compatibilitylint 组的成员因此会被cargo fix --edition自动拾取并迁移在 2024 Edition 中该 lint 默认为deny。如需放行使用#[allow(exported_private_dependencies)]与#[doc(hidden)]配合使用。典型的合法场景如clap旧版本中的__derive_refs模块——proc-macro 只能保证clap命名空间可访问因此必须 re-export 运行时代码所需的依赖#[doc(hidden)] #[allow(exported_private_dependencies)] #[cfg(feature derive)] pub mod __derive_refs { #[doc(hidden)] pub use once_cell; }类似的还有 pub-in-private 场景对mod private内一个pub字段使用了some_dependency::SomeType的结构体可用#[allow(exported_private_dependencies)]放行或通过把可见性降到pub(crate)规避。RFC 将这种处理称为「最后手段」last ditch因为大多数情况下把 API 包装起来才是更好的选择。11.2 Cargo 侧的细化新增依赖字段public bool默认false可在workspace.dependencies中指定并被使用workspace true的依赖覆盖。构建lib时Cargo 对私有依赖一律用priv修饰符传入--extern「私有」的定义是从所有public true的依赖出发递归遍历公开依赖后剩余的部分。对bin等其他crate-typeCargo 会把全部依赖都告诉 rustc 为公开以减少「不可访问的 pub 项」带来的噪音。该特性不强制rust-version提升用旧版 Cargo 构建、依赖了设置public true包的用户在升级到新版 Cargo 时不会突然开始失败。cargo add将获得--public bool标志来控制该字段在添加依赖复用版本约束时也会检查依赖方已有的public依赖并复用其版本要求。是否把public写入 Index 被刻意留白当前并非严格必需但若将来 resolver 依赖它则可以回填。11.3 明确划出边界的问题最小版本解析RFC 1977 的内容被独立出去因为该问题无论依赖私有与否都存在应独立处理。resolver 级依赖可见性被推迟到 Future possibilities研究发现把依赖设为public true也可能是破坏性变更——如果调用方以 semver 不兼容的不同版本也依赖了它多个公开版本的包如通过重命名产生的tokio_03与tokio_1如何处理也仍需思考。草案还提出了clap { from [clap_complete, clap_mangen] }这类「调用方声明关系」的替代路线以及cargo update警告 semver 不兼容公开依赖、cargo doc跳过不可访问依赖等未来方向。11.4 新方案的已知局限修订版坦诚列出了若干局限警告由 rustc 发出、却要通过修改构建系统通常是 Cargo信息来解决可能令人困惑cargo fix需要解析非机器可应用的 lint 并翻译成Cargo.toml改动存在风险当foo 300实际公开的类型来自foo-core 1时无从澄清无法覆盖「仅当启用某 feature 时才公开」的依赖即使pub项不可访问也会发出警告已通过不对bin标记私有依赖来降低影响也无法可靠地 lint「public true未被使用」。十二、权衡、备选方案与遗留问题12.1 缺点评估RFC 认为只要实现得足够好良好的 linter 与错误消息就没有明显的缺点。12.2 备选方案最大的备选方案是「类型与 trait 别名立即成为本 RFC 的一部分」crate 可以私有地依赖某依赖然后把它 re-export 为自己的类型隐藏原始来源。这可能更容易教学也能消除用户陷入「死胡同」唯一出路是引入公开依赖的情况若类型/特性别名可用external_public_dependency即后来的exported-private-dependencies可能就无需存在。12.3 遗留问题此改动的影响面有多大最可能通过运行 cargobomb/crater 来回答。由于修改公开依赖的 pin/范围要求 semver 变更值得探索 Cargo 能否阻止用户发布违反该约束的新版本。如果在本 RFC 实现之前就推行了弃用extern crate的 RFC那么在不使用--extern时这套机制如何工作结语RFC 1977 为 Rust 依赖体系引入了「公开 / 私有」这一关键二分法把「依赖类型是否进入公开 API」从隐式的意外变成显式的契约并通过编译器警告、public true声明、Index 传递、解析图约束与发布时最低版本解析形成闭环。虽然在版本解析层面的完整落地仍有待推进但其中的 lint 与标记机制已经由 RFC 3516 细化为exported-private-dependencies并在 2024 Edition 中成为默认错误。对于任何维护库 crate 的开发者理解这一机制都有助于避免「升级依赖即破坏 API」的陷阱并让Cargo.toml真正成为准确的版本依赖下限记录。赞分享文档【免费下载链接】rfcsRFCs for changes to Rust项目地址https://gitcode.com/gh_mirrors/rf/rfcs点击查看免费下载相关推荐Rust 公共/私有依赖public/private dependencies设计全解RFC 3516 与 exported_private_dependencies 实践指南Rust 公共/私有依赖public/private dependencies设计全解RFC 3516 与 exported_private_depend文档Rust Cargo Profile 依赖级覆盖解读 RFC 2282profile-dependenciesRust Cargo Profile 依赖级覆盖解读 RFC 2282profile dependencies 本指南以 Rust RFC 仓库中的 RF文档Cargo依赖管理机制深度剖析Cargo依赖管理机制深度剖析 本文深度解析了Cargo的依赖管理机制涵盖了依赖解析算法与版本冲突解决策略、Cargo.toml与Cargo.lock文件格式开发工具包管理器CLI构建工具上一篇如何设计面向工业实践的深度学习课程体系从理论到部署的全链路教学框架下一篇Node.js私有类字段后门webshell项目中的语法特性创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑