Workspace with precise and incompatible dependencies

I'm running into an issue where I have a cargo workspace that needs to configure dependency versions based on a feature flag.

When configured for next_13, I need the following dependencies:

swc_core = { version = "0.43.13", features = ["plugin_transform", "__ecma"]}

When configured for next_12, I need the following dependencies:

swc_common = { version = "0.28.10", features = ["concurrent"] }
swc_core = { version = "0.22.0", features = [
] }
swc_plugin_macro = {version="=0.9.8"}

Critically, I need swc_plugin_macro to be precisely 0.9.8 when the next_12 feature flag is set, because an incompatibility with swc_core@0.22.0. The rub is that swc_core@44 requires swc_plugin_macro@^0.9.9

Somehow, I need to be able to specify / exclude that precise package dependency. But no matter what I try, I get

failed to select a version for swc_plugin_macro
... required by package swc_core v0.43.13
... which satisfies dependency swc_core_next_13 = "^0.43.13"

Before needing to support multiple versions of the same package, I had the swc libraries declared in the in the virtual manifest. I've tried the following to no avail:

  • renaming packages in the virtual manifest, declaring them as optional dependencies in crates
  • creating a next_12 and next_13 crate, that import the dependencies required for each, removing the dependencies from the virtual manifest, and depending on them directly from other crates
  • duplicating an entire lib that specifies next_12 dependencies instead of the dependencies declared in the virtual manifest

Surely it must be possible to have two versions of the same lib in the same workspace, with a precise dependency only when one of the versions is required. Can anyone help?


In general, you can't trick Cargo into resolving an unresolvable dependency tree like this. Workspaces use a shared lock file which "flattens" the dependency tree into a shape that fits every package in the workspace. What you are asking is to split the dependency trees, and that's fundamentally incompatible with workspaces.

This part of your comment does not make sense to me. Going on semantic versioning rules, 0.9.8 to 0.9.11 are all compatible versions. If this is not true, this is a failure of the crate maintainers. Breaking changes need to bump the highest non-zero version component for this exact reason. E.g. the breaking change should have happened in 0.10.0, not 0.9.9 (or whatever).

From what I gather, you have two packages; one depends on swc_core = "0.43" and the other depends on swc_core = "0.22" and swc_plugin_macro = "=0.9.8". The second package has the plugin macro pinned "for reasons". Is this correct?

The only thing you can do in this situation is remove one of the packages from the workspace with:

members = [ "package_for_next_13" ]
exclude = [ "package_for_next_12" ]

If you do this, you will get multiple lock files (one for the workspace and one for the excluded package). You can inspect these lock files independently to see why there is a conflict.

$ cargo tree -i swc_plugin_macro
swc_plugin_macro v0.9.11 (proc-macro)
โ””โ”€โ”€ swc_core v0.43.41
    โ””โ”€โ”€ package_for_next_13 v0.1.0 (/Users/parasyte/Desktop/urlo-93387/package_for_next_13)
$ cd package_for_next_12
$ cargo tree -i swc_plugin_macro
swc_plugin_macro v0.9.8 (proc-macro)
โ”œโ”€โ”€ package_for_next_12 v0.1.0 (/Users/parasyte/Desktop/urlo-93387/package_for_next_12)
โ””โ”€โ”€ swc_core v0.22.17
    โ””โ”€โ”€ package_for_next_12 v0.1.0 (/Users/parasyte/Desktop/urlo-93387/package_for_next_12)

You lose some of the benefits of workspaces like this, but it's the only workaround for the "flattening" done for the shared lock file. Worst case, if you are attempting to build a third crate that depends on both next_12 and next_13 packages, you will end up with the same exact problem and it's just not possible to solve that at all.

See also Allow to share versions of dependencies between unrelated Cargo packages ยท Issue #5332 ยท rust-lang/cargo ยท GitHub


Hey thanks!

This part of your comment does not make sense to me. Going on semantic versioning rules, 0.9.8 to 0.9.11 are all compatible versions. If this is not true, this is a failure of the crate maintainers.

Yes, I agree. SWC is a bit chaotic, especially in the earlier versions. It has stabilized a bit since then. Ultimately, I'm not going to complain about them not respecting semver, it is what it is.

Here's the thing: the apis and signatures for SWC remain stable between versions. I don't necessarily need a package for next_12, and a package for next_13, as much as I need a way to "swap out" dependencies for the actual packages in the workspace.

If this were a single dependency (swc_core@0.22.0 vs swc_core@0.43.13), I would be able to manage this with a third lib and a "shim". Some like this: Allow specifying dependencies per feature ยท Issue #5954 ยท rust-lang/cargo ยท GitHub.

Essentially I'm looking for something like

cargo build -p my_plugin --features next_12 # this builds with swc_core@0.22.0 and swc_plugin_macro =0.9.8
cargo build -p my_plugin --features next_13 # this build with swc_core@0.43.13 and swc_plugin_macro@0.9.9

However, because of the way the SWC package ecosystem is split up and the way cargo flattens deps, apparently I cannot have a "dependant dependency" -- something like "if using swc_core@0.22.0, use swc_plugin_macro =0.9.8, else use swc_plugin_macro@0.9.9

The only other things that I can think of, unless someone from the community knows something that solves all my problems is to

  1. Somehow abuse Specifying Dependencies - The Cargo Book
  2. Swap out the virtual manifest

Neither of these solutions are particularly appealing to me though.

This falls under the worst-case scenario that I described. You cannot make Cargo use swc_plugin_macro versions 0.9.8 and 0.9.9 simultaneously in a workspace, much less in a single package.

Cargo "knows" these versions are compatible, so it will pick the version that is the best fit (0.9.9) but the exact match =0.9.8 forbids that and the crate maintainers require >=0.9.9 for swc_core@0.43.13. It's mutually assured destruction.

Your best bet is forking swc_core@0.43.13 and swc_plugin_macro@0.9.9 to fix the semantic versioning issues. But that will break other dependencies that want these same crates, so you have to fork those, too!

Aside: Given the frequency of updates on these swc crates and the incredible number of crates they publish, I have several other concerns about this beyond them not following a simple set of SemVer rules.

Thanks for your help. I decided to solve this in a rather un-ideal way, but nonetheless one that works for me:

A ci job swaps out the cargo file with the dependencies needed, builds and publishes. I have true e2e tests that validate the plugin post compile, so while development is against latest, e2e tests can be run against all supported versions.

As far as I can tell this is the only solution, barring forking and getting into a nightmare of patched vendored copies that propagate into the myriad of crates that SWC maintains.