6个版本
0.1.5 | 2023年8月20日 |
---|---|
0.1.4 | 2023年8月20日 |
#66 in #resolution
每月25次下载
用于 macro-dep-test
2KB
关联过程宏模式
提供一个包含特定义义的 foo
crate 和包含过程宏 derive 实现的 foo-derive
crate 是一种常见的模式。通常,您希望 foo
和 foo-derive
保持版本一致,因为 derive crate 倾向于使用非 semver 保护的非 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
约束为相同的版本。
这个crate中的模式展示了如何添加这个约束!我们可以在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。这个cfg从不为真,所以foo
实际上从不依赖于foo-derive
(除非启用了derive
功能标志)。尽管如此,这个平台特定的依赖项强制Cargo将foo-derive包含到锁文件中,从而触发“一个crate没有两个semver兼容版本”的约束。
例如,如果用户尝试
[dependencies]
foo = "=1.2.3"
foo-derive = "=1.2.2"
他们的构建将(正确地)失败。
关键的是,因为cfg从不为真,foo
crate实际上不依赖于foo-derive
,所以它可以独立编译。同样,虽然每个锁文件都有一个foo-derive
,但它实际上只有在crate图的其他地方需要时才会下载(这种情况类似于在仅支持linux的crate的锁文件中拥有特定于windows的依赖项)。
注意,这里使用的是特定目标的依赖项,而不是功能。使用功能时,Cargo可以查看正在编译的根crate的所有功能的集合,推断出可以激活的依赖项的精确功能集,并从Cargo.lock中删除任何肯定不需要的内容。
使用特定目标的依赖项(target.'cfg()'.dependencies
语法),Cargo必须假设每个cfg
可能为真,因此它必须保守地将所有内容包含到锁文件中。
重要说明
那么,这实际上可行吗?我不知道!我不认为有人尝试过这种规模的破解,所以可能某处会有严重的问题。但是没有尝试我们就不会知道(:-))。它似乎在这个小实验中有效(crates.io上发布了许多macro-dep-test
和macro-dep-test-macros
版本,您可以亲自检查)