"cargo build -p app1 -p app2" behaves differently than "cargo build -p app1 && cargo build -p app2"

Hi fellow rustaceans :wave:

While using cargo, I noticed something that seemed strange to me: After building a workspace using cargo build --workspace, there were some executables where cargo run still needed to build something before executing them.

As far as I see, this is connected to different executables in the workspace using different features for depending crates, e.g.

app1/Cargo.toml:

[dependencies]
proc-macro2 = { version = "1.0.46", features = ["span-locations"] }

app2/Cargo.toml:

[dependencies]
proc-macro2 = "1.0.46"

So for this example workspace, I can do this:

simon@machina ~/repos/cargo-workspace-build-mwe (git)-[main] % cargo build -p app1 -p app2
   Compiling proc-macro2 v1.0.46
   Compiling unicode-ident v1.0.4
   Compiling app2 v0.1.0 (/Users/simon/repos/cargo-workspace-build-mwe/app2)
   Compiling app1 v0.1.0 (/Users/simon/repos/cargo-workspace-build-mwe/app1)
    Finished dev [unoptimized + debuginfo] target(s) in 2.83s

Notice that proc-macro2 is only built one time. Furthermore, I can now find target/debug/app1 as well as target/debug/app2 on disk.

simon@machina ~/repos/cargo-workspace-build-mwe (git)-[main] % cargo build -p app1        
    Finished dev [unoptimized + debuginfo] target(s) in 0.08s

As expected, nothing to do for app1.

simon@machina ~/repos/cargo-workspace-build-mwe (git)-[main] % cargo build -p app2
   Compiling proc-macro2 v1.0.46
   Compiling app2 v0.1.0 (/Users/simon/repos/cargo-workspace-build-mwe/app2)
    Finished dev [unoptimized + debuginfo] target(s) in 2.11s

For app2 however, the app as well as its dependency is being rebuilt. That is at least not what I expected :sweat_smile:

Can you tell me whether this is intended behavior or a bug?

Both crates probably enable different featurws for proc-macro2. If you specify both at the same time, features will be unified such that proc-macro2 will have all features used by app1 and app2 enabled, but when only compiling one of them, only the features used by the respective crate will be enabled.

Yes, they definitely do :slight_smile:

Confirmed :+1:t2:

Still, the question remains: Is this intended behavior? To me (as a newbie to Rust), it is very much surprising getting different binaries depending on whether I build that binary in parallel with some other target. As such, this looks like a bug to me. On the other hand, maybe there is a good reason why cargo should behave the way it does and I'm just not seeing it?

This is how cargo works — it selects what targets to build, and then makes a plan how to build them together with unified features and deduplicated dependencies.

This is a bit confusing, so just to clarify based on testing that repo:

  • cargo build --workspace builds everything. You can run both binaries immediately afterwards manually without any issues with ./target/debug/app1 and ./target/debug/app2
  • cargo build --workspace && cargo run -p app1 && cargo run -p app2 appears to cause proc-macro2 to be rebuilt the first time app2 is run, but if you subsequently switch back and forth between running app1 and app2 it doesn't try to rebuild it again until I do a cargo clean. Reversing the order so app2 is run first after cargo build --workspace still causes the rebuild in app2 and not app1.

That initial rebuild of proc-macro2 definitely seems inconsistent to me. Cargo seems to have everything it needs after cargo build --workspace since both binaries are present in target/debug, so why the rebuild the first time you cargo run -p app2?

I understand why in isolation cargo would choose to build the minimal version of proc-macro2 if you just built app1. Then cargo run -p app2 requiring rebuilding makes perfect sense. However it already seems to have built the dependency such that app1 AND app2 can link it in cargo build --workspace, so I don't see an obvious reason why it should need to rebuild proc-macro2 again.

Thanks for the replies! :slight_smile:

The interesting question is: Should features be unified between executables? This is where cargo's approach seems inconsistent to me:

  • If I build two executables using a single Cargo invocation (i.e, cargo build -p app1 -p app2), the executables will get unified features.
  • If I build the same to executables (which are still part of the same workspace) using separate Cargo invocations (i.e, cargo build -p app1 && cargo build -p app2), features are not unified.

As a Cargo user, it surprises me that the single-invocation and the double-invocation approach produce different binaries for the same targets. Currently, I would still argue that the binaries should be the same. Do you (dis)agree, @kornel?


IMO, it is a good thing to avoid including features into an executable that does not need them. So I think, it is perfectly reasonable in this case for Cargo to build two variants of proc-macro2 for the two executables. My expectation would rather be that cargo build -p app1 -p app2 (or cargo build --workspace if you will) builds these two variants.

I'm actually not entirely sure that it's building two separate variants when you build the workspace, but I'm not very familiar with how cargo organizes files in target. There are two proc-macro2 folders in build after building the workspace, and 4 after subsequently building app2.

No matter what that behavior is though it seems wrong that you have both completed executables after building the workspace, but running app2 the first time via cargo run causes a rebuild. I definitely agree with you there.

Yup, sure seems that it is currently building one version for -p app1 -p app2 and another for -p app2 :+1:t2: I probably did not express that right in the previous message: Building two variants for -p app1 -p app2 would be my expectation of what Cargo should do, not my assumption of what it currently actually does :sweat_smile:.


I'll take that as enough confirmation of this being a valid concern to open a GitHub issue on the Cargo project :slight_smile: