Cargo 的本次开发周期:1.82

2024 年 10 月 1 日 · Ed Page 代表 Cargo 团队

Cargo 的本次开发周期:1.82

这是 Rust 1.82 合并窗口期间 Cargo 开发的总结。

本周期的插件

Cargo 不可能满足所有人的需求,原因之一是它必须维护兼容性保证。插件在 Cargo 生态系统中扮演着重要的角色,我们希望对它们表示赞赏。

本周期的插件是 cargo-show-asm,它允许你查看你的 Rust 代码编译后的结果,包括汇编、WASM、LLVM-IR 或 MIR。

感谢 epage 的建议!

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

实现

cargo info

来自 1.81 的更新

FCP 已关闭,cargo info 已合并到 Cargo 中 (#14141)!在讨论过程中,发现了一个 bug,导致我们暂时取消了所有者的报告 (#14418)。

rendering of cargo-info's verbose output using SVG

感谢 Rustin170506 将其推动到稳定!

Shell 补全

今年夏天,我们有 shannmu 作为 Google Summer of Code 的一部分为 Cargo 做贡献,目标是减少 shell 补全的脆弱性,同时通过将实现移到 Rust 中使其更强大 (#6645)。到目前为止,这项工作主要集中在扩展 clap_complete 中对 Rust 原生补全的不稳定支持,以提供一个与 Cargo 手写补全相媲美的基础。请参阅 clap_complete::env 以了解此功能的用户界面,以及 clap#3166 以了解开发进度。

最近,shannmu 一直在私有分支中尝试将其应用于 Cargo,并开始将此集成到主线 Cargo 中的第一步,见 #14493

MSRV 感知的 Cargo

来自 1.80 的更新

在没有“完美”解决方案的情况下,我们继续调整了配置,从我们的占位符

[resolver]
something-like-precedence = "something-like-rust-version"

[resolver]
incompatible-rust-version = "fallback"

(#14296)

有了这些,并且没有意识到任何其他障碍,我们发布了新一轮的 征求测试。我们很感激我们收到的反馈!

fallback 策略中一个特别令人困惑的方面是,Cargo 可能会选择一个依赖项,该依赖项要么

  • 对于你的 package.rust-version 来说太新,因为没有与 MSRV 兼容的版本可用,因此 Cargo fallback 到了一个与 MSRV 不兼容的版本
  • 如果你的版本要求过于宽松,并且你的包没有设置 package.rust-version,但工作区中的其他内容设置了,那么它会比你的包需要的版本旧

在对 Cargo 的锁定输出进行了一些调整后 ( #14401 #14440. #14445. #14457. #14459. #14461. #14471. ),Cargo 现在向用户报告

  • 当它选择的依赖项版本与传递依赖于它的工作区成员的 MSRV 不兼容时
  • 当有一个新版本的包可用,并且与传递依赖于它的工作区成员的 MSRV 兼容时

rendering of cargo update

测试反馈不仅仅是 fallback 行为令人困惑,而且它在具有多个 MSRV 的工作区中无法提供 MSRV 感知的解析器的优势 (#14414)。理想的状态是,在解析依赖项时,Cargo 只考虑传递依赖于它的工作区成员的 MSRV。问题在于,Cargo 在解析完成之前不知道依赖项的每一条路径。Cargo 在第一次遇到依赖项时会选择一个版本,然后只有当所选版本被它的另一条路径拒绝时才会尝试另一个版本。如果我们 deny 具有不兼容 MSRV 的依赖项版本,那么通过足够的工作,我们可以达到理想的状态。问题是这将需要多少工作,并且有时包会故意选择与 MSRV 不兼容的依赖项(例如,具有不同 MSRV 的功能,不关心 dev-dependencies 的 MSRV 等)。我们的 fallback 策略要求 Cargo 在遇到依赖项版本时选择一个“足够好”的版本,因为它的另一条路径不能拒绝它并导致选择新版本。目前,“足够好”的版本是工作区中所有 package.rust-version 的最低版本。对于工作区中任何具有较高 MSRV 的包,这使得用户面临以下情况:要么故意保留其依赖项版本以弥补 Cargo 的缺点,要么他们会失去 MSRV 感知的解析器。

到目前为止,我们为此提出的唯一解决方案是加倍努力采用此策略:跟踪所有依赖项的所有工作区成员 MSRV,根据它们兼容的 MSRV 数量来优先处理包。只有当版本要求过高时,Cargo 才应该选择与 MSRV 不兼容的版本。这使得维护更容易,但代价是有时会选择比必要版本旧的版本。

对于此提出的解决方案,我们最担心的是,一个维护工作区的新手或中级用户被要求设置或降低其某个包的 MSRV,他们照做了,然后不知道依赖项被保留(如果用户之前没有在任何包上设置 MSRV,那么无论此提议的更改如何,他们都将处于这种情况)。让用户知道他们可以手动应用更新是之前那些锁定消息更改的驱动原因。问题在于这些消息是否足够好,以及是否可以在其分配的空间内做得更好。

cargo publish --workspace

来自 1.81 的更新

经过一些准备工作 ( #14340, #14408, #14488, #14496, ),jneem 发布了 #14433 以添加对一次发布多个包的支持。

作为不稳定功能成本的一个例子,#14433 最初被 open-namespaces 中缺少的功能所阻碍。一个 open-namespaces 测试使用 cargo publish 验证了某个案例,但是如果没有 Package ID Spec 支持,就无法再这样做,因为 cargo publish 现在需要它才能用于每个包。这在 #14467 中得到了解决。

cargo::error 构建脚本指令

之前在 #10159 中,提出了 cargo::error=<msg> 构建脚本指令。Cargo 团队开会讨论了细节,并表示可以继续进行,torhovland#14435 中完成了。

提议的语义是,任何通过 cargo::error 发送的消息都将显示为错误,并导致构建脚本出错,从而有效地提供内置的错误恢复方式。这与 cargo::warning 形成对比,后者仅针对本地包显示。

与原始请求者争论的重点是,发出 cargo:: 指令是否应导致构建脚本出错。最初的意图是使用它来清理来自 pkg_config 的输出,但是调用者需要决定某项内容是否为错误。

cargo update --precise <prerelease>

来自 1.80 的更新

除了一个 bug 修复 (#14412) 之外,linyihai#14305 中致力于详细阐述预发布版本的版本要求匹配规则。查看该 PR 以了解提议的匹配规则的详细描述。

快照测试

来自 1.81 的更新

感谢以下人员,我们完成了对一些遗留测试模块的大规模迁移

对于单个测试,阻碍迁移的一个因素是如何处理 JSON 比较。

Cargo 的编程 API 通常使用 jsonlines,但对于阅读和编辑内容的人来说,效果并不理想。切换到 snapbox 增加了一个额外的复杂性,因为它通过比较预期和实际的测试结果来突出显示失败,而差异比较最适合行导向的内容。

{"executable":"[ROOT]/foo/target/debug/007bar[EXE]","features":[],"filenames":"{...}","fresh":false,"manifest_path":"[ROOT]/foo/Cargo.toml","package_id":"path+[ROOTURL]/foo#0.0.1","profile":"{...}","reason":"compiler-artifact","target":"{...}"}
{"reason":"build-finished","success":true}

旧的 cargo-test-support API 通过始终将 jsonlines 元素渲染为格式化的 JSON,并在每个元素之间添加闪烁的行来解决这个问题,并且断言只打印第一个不同的字段。

{
    "reason": "compiler-artifact",
    "package_id": "path+file:///[..]/foo#0.0.1",
    "manifest_path": "[CWD]/Cargo.toml",
    "target": "{...}",
    "profile": "{...}",
    "features": [],
    "filenames": "{...}",
    "executable": "[ROOT]/foo/target/debug/007bar[EXE]",
    "fresh": false
}

{"reason":"build-finished", "success":true}

该格式不适用于 snapbox,因为它的目的是处理内存或文件系统快照,并且快照应该可以作为真实数据使用。隐式地以与声明格式相反的格式存储数据会与 snapbox 的模型背道而驰。我们在 snapbox#348 中通过允许显式声明解决了这个问题,允许测试作者声明预期数据的格式以及它将与什么进行比较。

这允许我们替换

        .with_stdout_data(str![[r#"
{"executable":"[ROOT]/foo/target/debug/007bar[EXE]","features":[],"filenames":"{...}","fresh":false,"manifest_path":"[ROOT]/foo/Cargo.toml","package_id":"path+[ROOTURL]/foo#0.0.1","profile":"{...}","reason":"compiler-artifact","target":"{...}"}
{"reason":"build-finished","success":true}

"#]].json_lines())

使用

        .with_stdout_data(
            str![[r#"
[
  {
    "executable": "[ROOT]/foo/target/debug/007bar[EXE]",
    "features": [],
    "filenames": "{...}",
    "fresh": false,
    "manifest_path": "[ROOT]/foo/Cargo.toml",
    "package_id": "path+[ROOTURL]/foo#0.0.1",
    "profile": "{...}",
    "reason": "compiler-artifact",
    "target": "{...}"
  },
  {
    "reason": "build-finished",
    "success": true
  }
]
"#]]
            .is_json()
            .against_jsonlines(),
        )

这既易于阅读,又能提供更深入的差异比较。这为转换我们大多数使用 jsonlines 的测试扫清了障碍,epage 在 #14293 中完成了这项工作。

一些其他使用 jsonlines 的测试受限于 snapbox 中对已编辑内容的相当原始的处理。Snapbox 允许在预期结果中放置通配符,以便缩小对测试作者关心内容的关注范围,并删除运行或机器相关的内容。这个问题在 snapbox#358 中得到了修复,更多的测试在 #14402 中被迁移。

通过正确的标志组合,由于混入了人工输出,`cargo run` 可能会产生无效的 jsonlines。cargo-test-support 通过忽略任何不以 { 开头的行来解决这个问题。最后,我们发现只有一个测试依赖于此行为,并且我们发现可以用不同的方式测试相同的功能,因此 epage 在 #14297 中这样做了。

设计讨论

time

在 1.80 中,有一个类型推断回归影响了 time 包 (rust#127343)。更多上下文,请参阅

特别是,Cargo 团队讨论了 #14452,这将隐式地修补现有版本的 time 以使其与较新的 Rust 工具链一起工作。引用我们在该 PR 上的回复:

我们理解 time 问题导致了很多意想不到的动荡,并且像您一样,我们也有兴趣找到改进现在和未来情况的方法。为此,我们 @rust-lang/cargo 的大部分会议都在讨论这个话题。我们考虑了通过各种手段修补特定版本范围的 time 包的可能性。我们担心匆忙推出这样的功能,以及它现在和未来可能带来的意想不到的后果

  • Cargo 越过了修补用户源代码而不是构建完全原始源代码的义务论界限。

  • 用户对这种修补感到惊讶(例如,因为他们预计会失败,并且正在测试失败)。

  • 必须在未来的功能(如缓存和验证)中考虑这种修补。

鉴于此,我们更希望不为 time 包的这个问题采取短期修复措施。但是,我们正在考虑未来可能有帮助的解决方案。我们已经开启了 #14458 来探索这些想法。特别是,我们希望继续进行已经开始的更渐进式的工作,例如

  • 撤回原因,这目前是一个项目目标,并且有可能最终逐渐发展为 可变的数据库,该数据库可能支持注册表补丁。

  • 补丁实验。由于对解析器交互的担忧,这项工作已经停止。解析器交互是该更改背后的主要组成部分,但像 time 这样的情况表明,也许不支持 Cargo.toml 更改的解决方案会很有用。

  • 在 rustc 和 cargo 之间提供一个接口,以便双方都可以更好地支持报告与这种情况相关的诊断。(向 rustc 添加新的 --orchestrator-id 标志 compiler-team#635 与此相关。)

通过 rust-lang/rust#129343,正在向 rustc 添加缓解措施。我们承认,这可能对许多人来说还不够,因为它只提供诊断而不是修复。

包括 libs-api、lang、compiler 和 release 在内的其他团队也在努力改进,以更好地处理或防止这种情况发生。

构建探针

一些包在 build.rs 内部调用 rustc 来编译代码,以查看它是否按预期工作。包可以使用它来判断当前工具链是否支持某个功能,以及当该功能不稳定时是否仍然按预期工作。

Zulip 上,mrkajetanp 正在寻找关于修复几个问题的输入,其中 build.rs 不知道如何像 Cargo 那样调用 rustc ( #11244, #7501 )。作为一个团队,我们讨论了 Cargo 内部构建探针的状态。

对于稳定功能,cfg(accessible)cfg(version) 将提供更好的体验。cfg(version) 受阻于 cfg(accessible),而 cfg(accessible) 的范围已缩小以避免设计问题,只需要有人在稳定化的道路上支持它。在更多的基础包将 MSRV 提升到能够使用这些新功能之前,还需要一段时间。我们希望 MSRV 感知的解析器 可以改善 MSRV 较低的人的体验,基础包的维护者将愿意提升他们的 MSRV 以利用这些新功能。如果为旧的 MSRV 提供错误修复是一个问题,他们可以在提升 MSRV 时提升他们的小版本,从而在他们的版本范围中留下一个漏洞来修补旧的 MSRV。这是一个额外的支持负担,但对于大多数基础包来说,他们不太可能需要使用此逃生舱。

这仍然留下了一些 nightly 功能。Nightly 功能旨在选择加入,并且依赖项在未经他们同意的情况下将人们选择加入 nightly 功能似乎是不合适的。当构建探针中的潜在错误意外地启用不兼容的 nightly 功能时,这尤其有问题,这会破坏人们的工作。我们鼓励这些包改为将 nightly 功能放在功能标志或 --cfg 之后。将来,也许 全局功能 可以改善这种情况的体验。

由于在本次讨论后我们认为没有足够的构建探针用例,因此我们关闭了这两个问题。

检测未使用的依赖项

Rustc 的 unused_crate_dependencies lint 的好处有限,因为 Cargo 在包级别跟踪依赖项,而 Rustc 在构建目标级别警告它们。如果你有一个仅在某些构建目标中使用的依赖项,则其他目标会警告它未被使用 ( rust#95513, rust#57274 )。作为一个团队,我们讨论了 Cargo 如何与 Rustc 协作以使此 lint 更有用。

Rustc 能够在 json 中“静默”发出 lint (compiler-team#674),以便 Cargo 可以聚合结果并向用户报告 lint。如果你构建 cargo check --bin foo,则 Cargo 仍然没有足够的信息,因为所有构建目标都需要运行。为了处理这个问题,我们计划 Cargo 仅在构建所有相关的构建目标时才发出 lint。build.rs 始终被构建,因此 Cargo 可以始终为 build-dependencies 发出。如果所有 binlib 构建目标都被构建,则 Cargo 可以为普通 dependencies 发出。这只剩下 dev-dependencies,因为目前没有任何方法可以一次构建 benchtestexample 和 doc 测试,因为 --all-targets 排除 doc 测试。

一旦我们弄清楚 --all-targets 和 doc 测试,我们就可以将 cargo-udeps 上游到 Cargo

--all-targets 和 doc 测试

因此,作为一个团队,我们还讨论了 #6424: “Cargo check 不检查 doctest”,这类似于 #6669: cargo test --all-targets 不运行 doc 测试 。我们之所以陷入这种情况,是因为 Cargo 在不同的模式下运行,而 --all-targets 激活了一种模式(编译),而 --doc 在不同的模式下运行(文档)。但是,用户不太可能从模式的角度考虑,将它们都视为“构建目标”可能是一个更容易理解的选择。

那么问题是我们是否可以在此时更改 --all-targets 的含义。我们发现很多人都在使用 --all-targets,假设它包括 doctest,并且当他们发现并非如此并且有许多损坏的 doctest 时感到惊讶。我们可以从中推断出有更多的人不知道,而更改含义会破坏他们。虽然这会提高他们代码的质量,但迫使他们在我们的时间表上处理它,而不是在他们的时间表上,可能会令人不快。为了不阻塞命名讨论,我们总是可以有一个不稳定的名称,如 --all-targets-and-doctests

为了推进这一点,我们可能需要首先将 cargo test 中的 --doc 标志添加到 cargo checkcargo buildcargo clippycargo fix。当我们努力将 --doc 添加到这些命令时,我们可以添加 --all-targets-and-doctests 以将 --doc--all-targets 组合在一起。

target-dirartifact-dir

RFC #3371 使 target-dir 配置字段和 --target-dir 标志成为模板,以便用户可以轻松地将所有 target-dir 移出其源代码目录,同时保持它们之间的分离。 Cargo 还有一个长期存在的未稳定功能,可以使用 --artifact-dir(以前是 --out-dir)来指定写入构建工件的位置 (#6790)。

kornelski#14125 中分享了一个反提案:不是将最终构建工件从 target-dir 移到 artifact-dir,而是将所有中间构建工件从 target-dir 移到一个仅供内部使用的位置,该位置经过重新组织以实现跨工作区的共享。中间构建工件的重新组织和共享不必作为此过程的一部分处理,并且正在 #5931 中跟踪。

作为一个团队,我们讨论了这两个方案

  • 方案 1 (RFC #3371): target-dir 用于中间工件,artifact-dir 用于最终工件
    1. 使 target-dir 可移动 (RFC #3371)
    2. 稳定 --artifact-dir (#6790)
    3. 允许在 artifact-dir 中使用模板,并使其负责写入可运行的二进制文件等
    4. 将 target-dir 的默认值更改为指向中心基本路径,例如 cargo script,使 artifact-dir 写入 target/ 内的路径
  • 方案 2 (#14125): target-dir 用于最终工件,私有的/未指定的目录用于中间工件
    1. 在中心基本路径中定义一个新的中间工件目录,类似于 cargo script 的做法
    2. 逐步将中间工件从 target/ 迁移到这个新目录
    3. 拒绝 --artifact-dir (#6790),声明 --target-dir 可以解决该需求

#14125 的方案确实简化了问题,但失去 --artifact-dir 的一个好处:用户能够指定一个可预测的路径,而不是必须跟踪内部的位置。用户也可能希望定义中间构建工件的位置,即使 Cargo 有一个单独的 CARGO_CACHE_HOME (#1734),因为构建工件可能比其他缓存大得多,或者可能希望针对其他属性(如文件 IO 速度)进行优化。

我们提出了自己的结合了这两种方案的反提案。从高层次来看,它是:

  • 将中间工件移到 build-dir,它仅受配置控制,并且像 RFC #3371 一样使用模板
  • 将最终工件移到 artifact-dir,它受配置和 CLI 控制,并且也使用模板。
  • 一段时间后,逐步淘汰 --target-dir(将其隐藏在帮助输出中)

有关更多详细信息,请参见我们的帖子

cargo update --save-Zminimal-versions

随着 cargo update --breaking 取得进展(上次在 1.81 中讨论过),我们再次讨论了 cargo update --save 的想法 (#10498),该想法在我们之前的讨论中被推迟了

我们在这方面没有取得太大进展(主要是回顾过去的观点),直到我们转移到稳定 -Zminimal-versions 的话题 (#5657)。

-Zminimal-versions 主要用于验证您自己的版本要求的下限。由于难以处理不验证下限的传递依赖项,因此稳定化一直处于僵局。但是,稳定化往往意味着“认可”,并且通过社区压力可能会导致这方面发生积极变化,就像我们以前的不提交 Cargo.lock 的政策一样,对维护 SemVer 施加了社区压力。

如果每个人都使用 cargo update --save 来使用最新版本,那么这将不再是一个问题。或者本质上是“向前拖动清单,而不是向后拖动 lockfile”。一个 MSRV 感知的解析器将有助于防止清单和 lockfile 被拖得太远。具有选择加入的工作流程(如 cargo update --save)需要手动干预以确保它们被遵循,包括仍然需要进行测试以确保版本要求被向前拖动(cargo update --save --locked 将不起作用,因为这将导致新的更新),并且需要应用于任何可以更改 Cargo.lock 的命令,包括 cargo check。用户可能会受益于其他人遵循此工作流程,因为一些具有不良传递依赖项的流行依赖项可能会被其他引入它的依赖项修复,但这将是一个零散的解决方案。相反,如果我们将社区迁移到一个工作流程,其中默认情况下将 Cargo.toml 版本要求下限设置为 Cargo.lock 中的版本,那么版本要求将“正常工作”。但是,库允许依赖项的最低版本是可取的 (#14372)。这使应用程序作者可以灵活地保留版本以解决错误或控制依赖项更新的节奏,从而减少需要审核的依赖项版本。

我们正在 Internals 上继续讨论。

其他

  • cargo install --dry-run 的工作正在 #14280 中进行
  • Ifropc#14326 中添加了对 --lockfile-path 的初步支持(跟踪问题: #14421
  • dpaoliello 添加了对 path-bases 的初步支持 (#14360) 以及 cargo add 支持 (#14427) (跟踪问题:#14355
  • arlosi#14388 中开始了另一种将警告转换为错误的方法尝试(问题:#8424
  • 每日报告Eh2406 发布,关于 PubGrub 版本求解算法的 Rust 实现的进展

没有进展的关注领域

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

准备开发

需要设计和/或实验

规划

如何提供帮助

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

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

  • 总结现有对话(示例:更好地支持 docker 层缓存Cargo.lock 策略的更改MSRV 感知的解析器
  • 记录其他生态系统的现有技术,以便我们可以借鉴他人所做的工作,并制作出用户熟悉的东西(在有意义的地方)
  • 记录 Cargo 中的相关问题和解决方案,以便我们了解我们是否在正确的抽象层上进行解决
  • 基于这些帖子,提出一个解决方案,该解决方案考虑了上述信息和 cargo 的兼容性要求 (示例)

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