6 个版本
0.1.5 | 2023 年 8 月 20 日 |
---|---|
0.1.4 | 2023 年 8 月 20 日 |
#993 在 Rust 模式
每月 37 次下载
6KB
关联过程宏模式
这是一个常见的模式:提供一个具有 trait 定义的字节码 foo
库,以及一个具有过程宏 derive 实现的 foo-derive
库。通常,您希望 foo
和 foo-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" }
提供了更好的编译时间,但它并没有将 foo
和 foo-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-test
和 macro-dep-test-macros
版本,所以你可以亲自检查)。