What prevents Cargo from adding `[bench/bin/example-dependencies]`?

Lack of finer-grained dependencies can be particularly annoying when you want to use criterion for benchmarks, but do not want to compile it for every test run. The widely used in the ecosystem workaround with a separate benches crate is pretty unergonomic and involves additional maintenance burden.

Separate deps for binaries and examples could also be quite useful, e.g. when you want to pull clap for a nicer CLI.

6 Likes

This was touched on a little bit in Federated Registries?.

  1. There is an ecosystem cost for every new dependency table we add and we have to weigh the cost against the benefit
  2. There are many different requests for different extensions of the tables and they don't compose well together so we have to weigh one option against the others
  3. In particular, for some, just adding these tables is likely not enough and they want per-build-target dependencies (different deps for different bins). This also gets into This Development-cycle in Cargo: 1.77 | Inside Rust Blog

At least the use case you brought up to me isn't sufficient because that expresses a current local maxima of the ecosystem and I'd like to instead put my attention to helping move the ecosystem to a better place.

2 Likes

What exact cost are you talking about? The criterion use case is extremely common (just look at the number of references to the issue I linked), especially with the built-in bench harness stuck in the endless nightly limbo.

For example? Sure, for examples and bins we may want an even finer grained configuration (and it probably could be added later), but bench-dependencies looks a pretty straightforward addition to me. Especially when compared to the suggested dirty workarounds like [target.'cfg(bench)'.dev-dependencies] or separate benches crates.

IMO it may be even worth to deprecate dev-dependencies in favor of test-dependencies.

3 Likes

Third-party tooling that processes dependencies, particularly ones related to supply chain management

Does cfg(bench) work?

dev-dependencies affects

  • tests
  • doctests
  • examples
  • benches
1 Like

Does not look like a big issue to me. Without bench deps support such tools would simply ignore the section, which does not seem like a bad fallback to me.

No, it was one of suggestions to "solve" this problem in the linked issue.

Yes, I know. And my statement above was in the context of that. I probably should've added "deprecated in favor of [test/bench/examples-dependencies]".

1 Like

I think it is on them to either adapt or ignore new features. It shouldn't hold back the upstream for making a non-semver breaking change.

I would very much want bin dependencies separate from lib dependencies got bin+lib crates. Cargo moving so slowly and disliking third party PRs is a major roadblock in the Rust ecosystem. Everything just gets mired in endless discussions. But contributing to cargo does not feel useful or appreciated given what I have seen in discussions here or on the github.

4 Likes

I never said we are blocked on it but that it is a trade off to consider.

We do appreciate third party PRs and in fact merge quite a few. That said,

  • We have compatibility guarantees to uphold and have to consider the long term view when something touches on those. It isn't just "will this work for you" but "how will this affect others", "how will this be used in 5 years", "how does this align with other features in work", "what is the impact on maintainability", etc.
  • We have some processes in place that have tripped up some people when contributing but they are intended to help with long term maintainability, reviewer load, community communication, and ideally reducing contributor frustration in other ways. I've started seeing some of these policies getting traction elsewhere particularly with dealing with extra load from AI contributions

To put some of my comments in a different way, if someone where to try to move this forward, I would recommend looking at the larger landscape of potential dependencies tables and work on a clear picture of why this is the route to go along with a high level design, including

  • comparing with designs as wide as enable-features (including looking at why it was rejected and related discussion)
  • Registry index
  • cargo add / cargo remove
  • Interaction of existing and new tables

One thing that bugs me is that a single package (one Cargo.toml file) with multiple crates (AKA targets, products, bins/benches/examples) looks roughly similar to having a single workspace with multiple packages. It's like two zoom levels of the same fractal.

There's pressure to add more separation between crates of a package, as well as pressure to add more integration between packages in a workspace.

If we extrapolate these trajectories, Cargo will end up with two almost equal ways of making multi-crate workspaces, but with historical small differences between them that are going to cause confusion and debates which one is the right one.

So I'm wondering whether these two can actually be made the same thing, in a direct way. What if workspaces could be freely nested? What if lib+bin packages were retroactively explained to be workspaces containing a lib-only package and a bin-only package, merely defined using "syntax sugar" of [lib] and [[bin]] in one file instead of separate Cargo.toml files for each.

Imagine that you could start with a lib+bin Cargo.toml package, but once you needed to have separate dependencies for the bin, you would make a full cli/Cargo.toml for the binary with its unique dependencies and features, but some yet-to-be-invented inclusion/inlining mechanism would let you merge it back with its lib crate to keep it behaving just like before, like single lib+bin package.

7 Likes

My nested Cargo packages RFC was supposed to move in this direction, by making it possible to publish packages within packages. The PR comments mention some of the complications of making this actually possible (some of which would also apply to adding bin-dependencies, like distinguishing them in the package index to be able to compute the transitive closure of library dependencies).

I think that as long as we don't have such nested packages, the thing that distinguishes “many packages” from “many crates” is that packages are the unit of versioning. That is, code within a package knows that all other code in that package is of exactly the same version; it cannot know that about code in a different package (without using fragile "=1.2.3" dependency version requirements). But that doesn’t say very much about whether a target/crate within a package should be essentially different, in some way, from a nested package.

1 Like

I simply described how it appears to me as an outside observer who decided against contributing to cargo for the reasons I described. I don't have the mental energy for months of arguing to make a change. I would rather just contribute to something else then. You select for a specific type of contributor with your process (and so does any process of course). I think that the cargo process is particularly uninviting.

2 Likes

Yes, there is a push to make packages more like workspaces. My earlier posts link to an earlier summary.

Concerns I have with pushing too much into a package is the complexity it might entail and the transition path from where we are at. Overall, I personally lean towards keeping packages simple and improving the workspace experience.

There have been multiple design discussions around nested workspaces. There are other use cases, like rust-lang/rust's workspace pulling in the rust-lang/cargo workspace. Originally rust-lang/cargo had no workspace but inside of rust-lang/rust, Cargo was a part of that workspace but the Cargo team wanted access to workspace features. There were some challenging aspects of this (1) optional parent workspaces, (2) worskace inheritance, and (3) do we want local control of things like profiles or top-level control? This is only a rough sketch of the conversation as it has been a while and I wasn't the main person driving these conversations.

If we shift to crates are packages and packages are workspaces, we would also need to figure out the impact on package and target selection.

1 Like

The versioning unit could be preserved by making version.workspace = true implied/required for the package-is-a-workspace case.

Alternatively, there's the RFC for packages being namespaces on crates.io. This could be used to make packagename/lib an installable item with potentially separate versioning from packagename/bin[1].

Of course every change will need all the tools to follow. OTOH being able to normalize workspaces and packages with multiple targets into a single tree with single-target crates is a very nice simplification for tooling (speaking from my experience of parsing these in multiple tools).


  1. syntax/reserved keyword used for the naming scheme to be bikeshedded ↩︎

In the sense I mean it, that’s not true — and it's a trap for library designers, currently. Suppose you publish two packages with identical versions declared:

[package]
name = "foo-derive"
version = "1.2.0" # obtained from workspace inheritance
[package]
name = "foo"
version = "1.2.0" # obtained from workspace inheritance

[dependencies]
foo-derive = "1.2.0" # obtained from workspace inheritance

Then suppose a release 1.3.0 is made. Then, in a dependent workspace, it is possible for versions to resolve such that foo-derive 1.3.0 and foo 1.2.0 are in use.[1] This means that the library author must ensure that foo-derive is compatible with older versions of foo — which may be especially tricky for doc(hidden) pseudo-private macro helper items which aren’t necessarily designed with API compatibility in mind.

This demonstrates that workspace inheritance does not create a unit of versioning — that is, foo-derive cannot be assured of what version of foo it interacts with, and vice versa.

This problem can be addressed for 1:1 relationships of “internal use only” packages by using = version requirements, but in more complex situations, the only robust solutions are to use a single package or to bump major versions whenever such an internal API change occurs.


  1. For example, if the user executes cargo update, followed by cargo update foo --precise 1.2.0 to try to revert. ↩︎