Cargo 的这个开发周期:1.84

2024 年 12 月 13 日 · Ed Page 代表 Cargo 团队

Cargo 的这个开发周期:1.84

这是对过去 6 周左右 Cargo 开发进展的总结,大约是 Rust 1.84 的合并窗口。

本周期的插件

Cargo 不可能满足所有人的需求,原因之一是它必须遵守兼容性保证。插件在 Cargo 生态系统中扮演着重要的角色,我们想对其进行庆祝。

我们本周期的插件是 cargo-hack,它可以轻松验证不同的功能组合是否可以协同工作,并且可以为所有支持的 Rust 版本进行构建。

感谢 epage 的建议!

请提交您对下一篇文章的建议。

实现

文档中使用简单的英语

trot 在 zulip 上联系了 Cargo 团队,希望使 Cargo 书籍 对那些以英语为第二语言的人来说更容易理解。经过一些讨论,我们决定从简化 Cargo 书籍中使用的语言开始。

KGrewal1 牵头进行了这项工作,并发布了 #14825。他们还在 #14829 中使语言更加一致。

构建脚本 API

来自 1.83 的更新

在 Cargo 团队批准拥有 build-rs 后,epageCAD97pietroalbini 合作,将 build-rs 的发布权移交给了 Rust 项目。

然后,CAD97 对 build-rs 进行了初步审查和更新,epage 将其合并到 cargo 中 (#14786)。然后,epage 在 #14817 中进行了更新 build-rs 的工作。

zulip 上,Turbo87build-rs(以及 Cargo 维护的更多 crate)在 Cargo 代码库中并与 Cargo 发布过程相关联表示担忧。这意味着从错误修复被合并到发布之间有 6-12 周的延迟,需要访问不稳定功能的项目必须使用 git 依赖项,MSRV 具有传染性,这给 Cargo 团队带来了定期提升它的压力,并且问题混杂在一起。另一方面,Cargo 的支持、文档和 API 能够同步开发。如果我们能够改进 Cargo 代码库中的发布流程(例如 #14538),那就太好了,但要使其与 3 个并行发布渠道(稳定版、测试版、每夜版)保持同步,包括为每个发布渠道的任意数量的 crate 版本留出修补空间,这使得这项工作变得很困难。

用校验和替换 mtime

来自 1.83 的更新

由于不稳定地支持使用 校验和而不是 mtime 来确定构建何时是脏的,Cargo 团队讨论了稳定化的路径。

当前设计中的一个漏洞是 Cargo 不会对构建脚本的输入进行校验和计算。如果 Cargo 代表用户进行校验和计算,那么它可能会看到与构建脚本不同的文件版本。但是,要求构建脚本对文件进行校验和计算并将其报告给 Cargo 会给构建脚本增加显著的复杂性,包括与 Cargo 协调使用哪种哈希算法。这还需要构建脚本引入哈希器依赖项,从而增加其构建时间。但是,目前尚不清楚是否必须对构建脚本进行校验和计算。此外,如果我们能够制定计划来减少对构建脚本的需求,我们将减少问题的范围。

另一个担忧是性能。校验和计算的开销在没有任何更改的构建中最明显,否则编译时间将占主导地位。我们正在 #14722 中进一步跟踪此问题。

还有一个问题是这将是什么样子。我们是完全切换到校验和还是让人们选择?至少,我们需要给人们一个临时的逃生舱口,因为我们在将 Cargo 过渡到校验和的过程中,以防他们以某种方式依赖于 mtime 行为。Cargo 是否需要永久的配置字段尚不清楚。

我们在 zulip 上联系了一些构建系统的所有者,进行了一次测试征集。到目前为止,我们只收到了 Rust 项目本身的消息,其中这使得构建时间测试更加困难,因为触摸文件以强制重建比尝试仔细地对文件进行重复编辑要容易得多。

Rustflags 和缓存

Cargo 的 构建指纹必须满足以下几个需求,包括

  • 检测何时必须丢弃现有构建缓存并重新构建,称为指纹
  • 隔离构建缓存,以便在不相关的命令(例如,交替使用 cargo checkcargo test 而不重新构建依赖项)之间保留它,称为 -Cextra-filename
  • 使符号名称唯一,这样您就不会在 ABI 不兼容的上下文中有意使用类型,称为 -Cmetadata

RUSTFLAGS 是一种绕过 Cargo 抽象并直接控制 rustc 行为的方法。Cargo 将 RUSTFLAGS 包括在指纹哈希中,但不包括在 -Cextra-filename 哈希中,导致它们更改时进行完全重建。当用户的 RUSTFLAGS 与其编辑器运行的 cargo 不同时,这尤其成问题。例如,一些用户报告说他们在编辑器中设置了 --cfg test,以便在 rust-analyzer 中启用所有 #[cfg(test)]

先前尝试在 #6503 中隔离 RUSTFLAGS 的缓存。但是,Cargo 对 -Cextra-filename-Cmetadata 使用相同的哈希,因此通过隔离缓存,符号名称也变得唯一。理论上,这是一件好事,因为 RUSTFLAGS 会影响 ABI。但是,并非所有 RUSTFLAGS 都会影响 ABI。

--remap-path-prefix 为例,它应该通过剥离特定于特定构建的信息来使二进制文件的构建更具可重复性。通过将其包含在 -Cmetadata 中,二进制文件会发生更改 (#6914)。为此添加了一个特殊情况 #6966

我们遇到的另一个案例是 PGO。对于 PGO,您使用 -Cprofile-generate 创建一个构建,然后针对基准测试运行它。然后,您使用 -Cprofile-use 将其反馈到构建中,以改进编译器执行的优化。此时,我们在 #7417 中还原了 #6503

#8716 中,ehuss 建议 Cargo 分别跟踪 -Cextra-filename-Cmetadata,并且仅将 RUSTFLAGS 包括在 -Cextra-filename 中。

经过一些重构 (#14826) 和测试改进 ( #14848, #14846, #14859 ) epageweihanglo 后,epage 发布了 #14830。但是,weihanglo 发现 --remap-path-prefix 仍然存在问题:即使使用 profile.dev.split-debuginfo="packed",二进制文件也是不同的,因为二进制文件包含 DW_AT_GNU_dwo_name,它指向包含 -Cextra-filename 的每个 rlib 的调试文件。

在解决 --remap-path-prefix 的问题之前,#14830 的合并被阻止。

快照测试

来自 1.82 的更新

epage 完成了从 Cargo 的自定义断言移出的工作。

在删除自定义断言的核心时,我们依赖于 dead_code 警告,因为我们删除了不再使用的断言。但是,我们错过了一个删除的断言,epage 在 #14759 中将其删除。

#14781#14785 见证了我们迁移了最后一部分 "无序行" 断言测试。 #14785 花了一些时间研究以找出最佳迁移方式。Cargo 的自定义断言删减的值较少,并允许测试作者通过在预期结果中使用原始值来忽略值删减。snapbox 在流程的早期应用删减,要求始终使用它们。这使得 Cargo 在切换到 snapbox 时会丢失测试覆盖率,因为我们无法验证 cargo 的那么多输出。然而,在与测试作者协商后,他们并不打算覆盖那些被删减的值,暂时规避了这个问题。

这仍然留下了 "contains"、"does not contain" 和 "contains x but not y" 断言。与其试图设计如何将这些断言融入 snapbox,epage 在 #14790 中切换到 snapbox 的删减后,保留了它们。

此时,epage 在 #14793 中记录了此次工作的经验教训,我们现在认为此次迁移已完成,关闭了 #14039

JSON schema 文件

#12883 中,我们收到了对 .cargo/config.toml 文件 JSON schema 支持的请求。我们已经需要在源代码和文档之间复制 schema,我们不想通过手动维护的 JSON schema 表示形式来复制它。幸运的是,有 schemars 可以从 serde 类型生成 JSON schema。

为了尝试 JSON schema 生成,dacianpascu06#14683 中为 Cargo.toml 添加了 JSON schema 生成的支持,请参阅 manifest.schema.json

.cargo/config.toml 生成 JSON schema 需要更多调查。Cargo.toml 具有一个单一的顶层定义,其中包含 schema 中的特定扩展点。.cargo/config.toml 没有单一的顶层定义,而是根据表或字段定义 schema。这是因为配置分层操作在被查找的特定路径上。schema 的类型分散在整个 Cargo 代码库中,需要工作将它们收集在一起,创建一个仅用于 JSON schema 生成的顶层定义。

设计讨论

改进内置配置文件

与基准测试密切相关的是性能分析,但 bench 配置文件不包含性能分析的相关调试信息,需要用户在每个仓库(或在其主目录中)调整其配置文件。CraftSpider#14032 中建议我们更新 bench 配置文件以使其更容易。然而,基准测试还需要与 release 构建保持一致,以确保您的数字与用户将看到的数字匹配。我们决定应使 bench 配置文件与 release 匹配,尽管我们认识到有改进用户性能分析工作流程的空间。

foxtran#11298 中重新启动了关于更改 release 默认值以提高运行时性能的讨论。

潜在的更改包括

  • 启用 LTO,无论是 thin 还是 fat
  • 减少 codegen-units
  • 增加 opt-level

虽然 release 构建不专注于快速编译时间,但在编译时间和运行时性能之间进行权衡仍然存在收益递减点。虽然 release 通常面向生产构建,但在某些情况下,dev 对于开发来说太慢了。weihanglo#14719 中运行了 Cargo 的 LTO 和 codegen-units 的数据。从这些数据来看,似乎 thin LTO 很容易获胜。

一种选择是创建 release-fastrelease-heavy。添加新的配置文件可能会是一个重大更改,我们需要谨慎地进行。我们已经存在 release 的可发现性问题,并且它有一个专用的标志 (--release)。如果没有某种内置集成,这些策略似乎最好留给用户。

无论使用哪个配置文件,LTO 的一个问题是存在可能阻止其成为安全默认值的误编译(例如 #115344)。

另一方面是 dev 配置文件。此配置文件有两个作用

  • 快速迭代时间
  • 通过调试器运行代码

事实证明,这两者可能是相互冲突的。通过调试器运行时,您通常希望二进制文件的行为类似于源代码,而优化可能会妨碍调试。但是,优化可以减少正在处理的 IR 数量,从而加快代码生成速度。它们还可以加快 proc macros、构建脚本和测试运行的速度。也许我们甚至可以设计一个专注于以牺牲调试器体验为代价来提高编译时间的优化级别。类似地,在二进制文件中携带的调试信息量也会影响您的构建时间。

查看 Rust 2023 调查结果,提高编译时间和调试体验势均力敌。问题是他们指的是哪种调试体验?通话中的人大多使用 "printf" 式调试,并能从提高编译时间中获益。即使我们对人们进行了调查,发现这代表了 Rust 社区(davidlattimoreFediverse 上对社区的子集进行了调查),这在多大程度上是调试器体验质量带来的幸存者偏差?如果调试器体验得到改善,即使是现有社区成员的行为也会发生多大的变化?

然而,这可能不是非此即彼。我们可以将 dev 配置文件拆分为单独的迭代时间和调试器配置文件,以便以低摩擦的方式访问非默认工作流程。仍然会有摩擦。如果迭代时间是默认值,并且有足够多的人通过他们的 IDE 使用调试器,并且这些 IDE 是预先配置的,那么与 IDE 供应商合作更改其默认值将减少很多摩擦。这可能需要很长的过渡期。

我们可以将两个工作流程中的一个拆分到一个全新的配置文件中,但这会遇到与 release-fastrelease-heavy 相同的问题。

解决潜在破坏性问题的一个想法是将内置配置文件移动到 cargo:: 命名空间并使其不可变。我们将保留的配置文件切换为默认情况下仅继承一个命名空间的配置文件。关于这是否会是一个重大更改,还存在一些未解决的问题,需要进行更多分析。

Cargo 不使用保留的新配置文件名称,而是使用保留的 debug 名称怎么样?debug 已经是一个保留的配置文件名称,在几个面向用户的位置,dev 配置文件被称为 debug (--debug, target/debug)。我们可以使 dev (--dev) 专注于迭代时间,而使 debug (--debug) 专注于调试器。target/debug 存在问题,因为将用户更改为 target/dev 可能会造成太大干扰。

完成一个计划并弄清楚它是否具有破坏性需要工作。如果可以继续进行,则可能需要很长的过渡时间和多个项目的支持。

此更改是否值得?joshtriplettInternals 上对仅使用 CARGO_PROFILE_DEV_DEBUG=line-tables-only 对编译时间的影响进行了一项调查,并在 zulip 上进行了后续讨论。

提高 dev 迭代时间的另一个角度是更容易加快热路径中依赖项的速度。Cargo 允许您为不同的依赖项设置不同的优化级别,并且某些项目鼓励这样做,例如 sqlx

[profile.dev.package.sqlx-macros]
opt-level = 3

如果软件包可以提供一个 软件包覆盖,以供它们用作依赖项时使用,那会怎么样?

依赖项指定的配置文件覆盖的另一个潜在用例是仅用于 mir 的 rlib。Cargo 为依赖项树中的每个 rlib 执行代码生成,并依赖链接器删除任何未使用的内容。仅用于 mir 的 rlib 会将所有代码生成推迟到最后,从而允许执行较少的代码生成,从而可能加快构建速度。这有可能取代大量用例的 [features] 的需求。一个问题是,如果测试二进制文件之间存在大量共享 mir,那将导致冗余代码生成,从而减慢构建速度。尝试使用此方法的一种方法是通过配置文件在每个软件包的基础上启用仅用于 mir 的 rlib。通过依赖项指定的配置文件覆盖,像 windows-sys 这样的大型软件包可以选择成为仅用于 mir 的 rlib。

依赖项指定的配置文件覆盖将是一个隐藏的交互,需要仔细考虑。

更改测试时避免构建生产代码

milianwzulip 上发布了关于他们的库以及在更改单元测试时所有依赖项都会重建的问题。

当一个库中的 #[test] 发生更改时,该文件的时间戳会发生更改,并且 Cargo 会重新构建该文件。一种避免这种情况的方法是将测试移动到专用文件中。rust 仓库使用一个工具来强制执行这种做法。epagerust-clippy#13589 中提出了一个 clippy lint 来解决这个问题。

当一个库发生更改时,Cargo 总是会重新构建依赖项。之前,Osiewiczzulip 上提出,让 rustc 哈希一个 crate 的 API,以便 Cargo 仅在 API 哈希值发生更改时才重新构建依赖项。这个问题正在 #14604 中被跟踪。

其他

  • 每日报告,由 Eh2406 提供,关于 PugGrub 版本解决算法的 Rust 实现进展情况
  • 基于 epage#14750 中的工作,linyihai 修复了 #14497 中带有无关细节的诊断信息。
  • Rustin170506#14749 中更新了 cargo script 加载配置文件的方式。
  • epage#14792 中更新了 cargo script 的 frontmatter 解析,并在 #14857#14864 中更新了清单编辑命令以支持 cargo script。
  • arlosi#14388 中完成了 CARGO_BUILD_WARNINGS=deny 的工作 (从 1.81 版本更新而来)。

没有进展的重点领域

这些是 Cargo 团队成员感兴趣的领域,但在本次开发周期中没有可报告的进展。

准备开发

需要设计和/或实验

规划

您如何提供帮助

如果您有改进 cargo 的想法,我们建议您首先查看 我们的积压列表,然后在 Internals 上探索这个想法。

如果有一个您希望解决但此处未讨论的特定问题,您可以采取一些步骤来帮助推动它,包括

  • 总结现有的对话(例如:更好地支持 docker 层缓存Cargo.lock 策略的更改MSRV 感知的解析器
  • 记录其他生态系统中的现有技术,以便我们可以在其他人已经完成的工作的基础上进行构建,并在合理的情况下让用户感到熟悉
  • 记录 Cargo 中相关的问题和解决方案,以便我们了解我们是否正在解决正确的抽象层
  • 基于以上帖子,提出一个解决方案,该解决方案考虑到上述信息和 cargo 的兼容性要求 (示例)

我们可以在 zulip 上帮助指导人们解决 S-accepted 问题,您可以在 贡献者办公时间 与我们实时交谈。如果您想帮助解决这里提到的一些较大项目,并且刚开始,修复一些问题将帮助您熟悉流程和期望,使事情进展更顺利。如果您想在没有导师的情况下解决一些问题,那么您需要自己完成的事情的期望会更高。