It's a common pattern to provide a foo
crate with trait definitions, and foo-derive
crate with a
proc-macro derive implementation. Typically, you want foo
and foo-derive
to be versioned in
lockstep, because derive crates like to use #[doc(hidden)]
non-semver-guarded API. Usually, this
is solved by a derive
feature, which makes foo
depend on foo-derive
with =x.y.z
constraint.
This, however, is problematic for compile times! It means that compilation of foo-derive
is
sequenced before compilation of foo
. As foo-derive
is a derive macro, it needs to parse the Rust
language. Rust is not a small language, so parsing it is fundamentally hard, and requires loads of
code to do correctly. So it takes some time to compile foo-derive
. What's worse, while normally
Cargo pipelines compilation such that .rmeta
files are all that's needed to unblock compilation of
dependent crates, for proc macros Cargo really needs to link the whole .so!
The bottom line, while
toml
foo = { version = "x.y.z", features = ["derive"] }
is easy to explain and works correctly, it could significantly reduce the amount of parallelism available during builds.
On the other hand, while
toml
foo = { version = "x.y.z" }
foo-derive = { version = "x.y.z" }
provides better compilation time, it doesn't constrain foo
and foo-derive
to be the same
version.
The pattern in this crate shows how to add that constraint! We can use the following declaration of
dependencies in foo
's Cargo.toml:
```toml [package] name = "foo" version = "1.2.3"
[dependencies] foo-derive = { version = "=1.2.3", optional = true }
[target.'cfg(all(tobe, not(tobe)))'.dependencies] # <- the trick foo-derive = { version = "=1.2.3" }
[features] derive = ["dep:foo-derive"] ```
The trick is a target specific dependency with "impossible" cfg. This cfg is never true, so foo
never actually depends on foo-derive
(unless the derive
feature flag is enabled). Non the less,
be