6 个版本

0.1.5 2023 年 8 月 20 日
0.1.4 2023 年 8 月 20 日

#993Rust 模式

每月 37 次下载

MIT/Apache

6KB

关联过程宏模式

这是一个常见的模式:提供一个具有 trait 定义的字节码 foo 库,以及一个具有过程宏 derive 实现的 foo-derive 库。通常,您希望 foofoo-derive 保持版本同步,因为 derive 库喜欢使用非 semver 保护的 API。通常,这通过一个 derive 功能来解决,这使得 foo 依赖于 foo-derive 并使用 =x.y.z 约束。

然而,这会给编译时间带来问题!这意味着 foo-derive 的编译要在 foo 的编译之前进行。由于 foo-derive 是一个 derive 宏,它需要解析 Rust 语言。Rust 不是一个简单的语言,解析它本质上很困难,需要大量的代码才能正确完成。因此,编译 foo-derive 需要一些时间。更糟糕的是,虽然通常 Cargo 会将编译管道化,使得 .rmeta 文件成为解除依赖库编译阻塞的唯一所需文件,但对于过程宏,Cargo 实际上需要链接整个 .so!

总之,虽然

foo = { version = "x.y.z", features = ["derive"] }

解释简单且工作正确,但它可能会显著减少构建过程中的并行性。

另一方面,虽然

foo = { version = "x.y.z" }
foo-derive = { version = "x.y.z" }

提供了更好的编译时间,但它并没有将 foofoo-derive 限制在相同的版本上。

这个库中的模式展示了如何添加这个约束!我们可以在 foo 的 Cargo.toml 中使用以下依赖声明

[package]
name = "foo"
version = "1.2.3"

[dependencies]
foo-derive = { version = "=1.2.3", optional = true }

[target.'cfg(any())'.dependencies] # <- the trick
foo-derive = { version = "=1.2.3" }

[features]
derive = ["dep:foo-derive"]

技巧是使用带有“不可能”的特定目标依赖项的 any() cfg。此配置从不为真,因此 foo 实际上从不依赖于 foo-derive(除非启用了 derive 功能标志)。尽管如此,这个特定于平台的依赖项迫使 Cargo 将 foo-derive 包含到 lockfile 中,因此“同一 crate 的 semver 兼容版本不超过两个”的约束条件生效。

例如,如果用户尝试执行

[dependencies]
foo = "=1.2.3"
foo-derive = "=1.2.2"

他们的构建将(正确地)失败。

关键的是,因为配置从不为真,所以 foo crate 并不真正依赖于 foo-derive,因此它可以独立编译。同样,尽管每个 lockfile 都有一个 foo-derive,但除非它在 crate 图中的其他地方需要,否则实际上并不会下载(这种情况与在仅限 Linux 的 crate 的 lockfile 中具有特定于 Windows 的依赖项类似)。

请注意,在这里使用特定目标的依赖项而不是功能非常重要。使用功能时,Cargo 可以查看正在编译的根 crate 的所有功能的集合,推断出可以激活依赖项的 确切 功能集,并从 Cargo.lock 中删除任何肯定不需要的内容。

使用特定目标的依赖项(target.'cfg()'.dependencies 语法),Cargo 必须假设每个 cfg 可能 为真,因此它必须保守地将所有内容包含到 lockfile 中。

重要澄清

那么,这实际上可行吗?我不知道!我不认为有人尝试过这种大规模的破解,所以可能某处会发生严重的问题。不过,如果不尝试,我们也不会知道(:-)而且它在这个小实验中似乎可行(crates.io 上发布了一些 macro-dep-testmacro-dep-test-macros 版本,所以你可以亲自检查)。

依赖项