# Breaking down crate dependencies?

**URL:** <https://internals.rust-lang.org/t/breaking-down-crate-dependencies/23566>\
**Category:** cargo\
**Created:** [September 25, 2025, 6:00pm UTC](https://internals.rust-lang.org/t/breaking-down-crate-dependencies/23566 "2025-09-25T18:00:37Z")\
**Posts on this page:** 16\
**Page:** 1

<div class="post-metadata">

**Author:** ![matthieum](https://sea2.discourse-cdn.com/flex002/user_avatar/internals.rust-lang.org/matthieum/32/4023_2.png) [@matthieum](https://internals.rust-lang.org/u/matthieum)\
**Post date:** [September 25, 2025, 6:00pm UTC](https://internals.rust-lang.org/t/breaking-down-crate-dependencies/23566/1 "2025-09-25T18:00:37Z")

</div>

At the moment, dependencies in `Cargo.toml` are _coarse-grained_.

In _theory_ a crate can contain:

- One library.
- Multiple binaries.
- Multiple example binaries.
- One unit-test binaries.
- Multiple doc-test binaries.
- Multiple integration test binaries.
- Multiple benchmark binaries.
- One build-script binary.

And it's quite useful to have a single crate contain all those, as it means that one's fingertips, one gets to run `cargo check`, `cargo clippy --all-targets`, `cargo test`, `cargo bench`, etc...

**Unfortunately, dependency-wise, it's a complete nightmare.**

There's only 3 categories of dependencies in cargo:

- `[dependencies]` means that if one binary includes heavy-weight dependencies, like, say `tokio`, then any crate wishing to use the _library_ will also depend on (and bundle) `tokio` now. Tough luck.
- `[dev-dependencies]` means that if there's a single benchmark depending on `criterion`, then all unit-test, doc-test, and integration test binaries will also depend (and bundle) `criterion`.
- `[build-dependencies]` is fairly nicely insulated at least, though with multiple build scripts being worked on... a single build script depending on a heavyweight dependency will drag down all other build scripts.

The current work-around is to break down the crates. Binaries must be their own crates. Examples and integration tests are better off in their own crates. But then convenience is lost. Doubly so as running cargo in a folder which is not a crate does NOT limit cargo to running only on the crates contained in this folder, but instead lead to cargo attempting to run on the entire workspace.

Is there any work towards making the dependencies more _fine-grained_?

Ideally, one would be capable of specifying the dependencies for each compilation target/artifact independently, something like:

```toml
[dependencies.lib]
[dependencies.bin.my-binary-1]
[dev-dependencies.test]
[dev-dependencies.doctest]
[dev-dependencies.examples.my-example-1]
[dev-dependencies.tests.my-integration-test-1]
[build-dependencies.my-script-1]

```

Which would be backward compatible with using a plain unqualified `[dependencies]` or `[dev-dependencies]` or `[build-dependencies]` as today.

_Note: in the context of multiple build scripts, it would notably allow starting to run fast-to-compile build scripts while the heavyweight dependencies of other build scripts are still being compiled._

It's not that the state of things today is unworkable. It's just... _sad_. One has to choose between convenience/ergonomics and compile-time performance/binary size1.

1 _Especially so as apparently, per Kobolz discovery, debug information are not tree-shaked, so that including a heavyweight dependency in the binary results in said binary containing the DI of this dependency even if not its code._

---

<div class="post-metadata">

**Author:** ![epage](https://sea2.discourse-cdn.com/flex002/user_avatar/internals.rust-lang.org/epage/32/3171_2.png) [@epage](https://internals.rust-lang.org/u/epage)\
**Post date:** [September 25, 2025, 8:49pm UTC](https://internals.rust-lang.org/t/breaking-down-crate-dependencies/23566/2 "2025-09-25T20:49:38Z")

</div>

Per-build-target features was discussed in [This Development-cycle in Cargo: 1.77 | Inside Rust Blog](https://blog.rust-lang.org/inside-rust/2024/02/13/this-development-cycle-in-cargo-1-77/#when-to-use-packages-or-workspaces) which can be used similarly to per-build-target dependencies and talks about the trade offs of overloading treating the primary unit of work as a build target in a package vs a package in a workspace.

> Which would be backward compatible with using a plain unqualified `[dependencies]` or `[dev-dependencies]` or `[build-dependencies]` as today.

That data model would not work because `dependencies.<name>` is your dependencies.

In TOML, the following are the same

```toml
[dependencies.regex]

[dependencies}
regex = {}

```

---

<div class="post-metadata">

**Author:** ![kornel](https://sea2.discourse-cdn.com/flex002/user_avatar/internals.rust-lang.org/kornel/32/2711_2.png) [@kornel](https://internals.rust-lang.org/u/kornel)\
**Post date:** [September 26, 2025, 1:41am UTC](https://internals.rust-lang.org/t/breaking-down-crate-dependencies/23566/3 "2025-09-26T01:41:27Z")

</div>

This creates even more overlap between crates with multiple artifacts vs workspaces with multiple crates.

It feels like both of these concepts are converging (more subdivision inside crates, more integration across crates in a workspace). Eventually crates and workspaces will become the same thing.

---

<div class="post-metadata">

**Author:** ![matthieum](https://sea2.discourse-cdn.com/flex002/user_avatar/internals.rust-lang.org/matthieum/32/4023_2.png) [@matthieum](https://internals.rust-lang.org/u/matthieum)\
**Post date:** [September 26, 2025, 4:44pm UTC](https://internals.rust-lang.org/t/breaking-down-crate-dependencies/23566/4 "2025-09-26T16:44:24Z")

</div>

> [@kornel](#):
>
> This creates even more overlap between crates with multiple artifacts vs workspaces with multiple crates.
> 
> It feels like both of these concepts are converging (more subdivision inside crates, more integration across crates in a workspace). Eventually crates and workspaces will become the same thing.

I can see the overlap, and yet at the same time...

### Logical Unit

There's a strong argument for bundling library code with its benchmark, examples & tests.

Logically speaking, this is a single logical unit, and the benchmarks, examples, & tests are "satellites" of the library.

This single logical unit happens to be called a crate.

### Work Unit

Similarly, after making changes on a library, it's quite natural to _first_ ensure that the changes on this library are sound (in isolation) by running the tests, and potentially the benchmarks. By adjusting the examples and ensuring they still compile & work.

Only then, after the library has been proven to work according to the updated expectations, is it time to integrate the changes into the downstream dependencies within the workspace and beyond.

It's therefore natural to run a workflow equivalent to `clear && cargo check && cargo clippy --all-targets && cargo test`, and optionally `cargo run --example` and `cargo bench` as appropriate.

Due to past unfortunate decisions, `cargo` can only work at crate or workspace level, and not any intermediary (sub-folder) level, so crate it is by necessity.

### Practice

The main codebase I work on is a single workspace of _250_ crates, and growing. Given that I have personally written most of the code in the past 3 years, I expect larger companies have much larger workspaces.

This is 250 crates using crates as a logical unit: without artificially splitting out test crates, bench crates, binary crates, or example crates to reduce false dependencies. We are fortunate that the compilation times are not atrocious -- most single binary can be compiled within 15s from scratch on my own machine -- and therefore we have not sought to break the ergonomics to gain more performance.

But I have been tempted to, countless times, and I have sighed, countless times.

### Is a crate that similar to a workspace?

I don't feel so, in my work.

They do share many things -- dependencies, perhaps one day features, etc... -- but they have very distinct roles regardless.

> [@kornel](#):
>
> Eventually crates and workspaces will become the same thing.

I would also reiterate that at the scale I mentioned, the fact that `cargo` can only compile either 1 crate or 250 crates (and their 500 binaries when testing) is actually _just_ impractical.

The ability to run `cargo` on _subsets_ of the workspace is _really_ missing. I cannot recall having this issue with Bazel, for example.

Every time I run `cargo` in a subfolder and it starts re-compiling the entire workspace, I curse, reminded of how unintuitive and unergonomic this decision is at scale. Hindsight...

If it was possible to run sub-folders, then I could in good conscience split up the benchmarks, the binaries, the tests, the examples, etc... in separate crates in sibling folders. I _could_.

It would _still_ be slightly unergonomic, death by boilerplate:

1. Paperwork: I would need to create yet another Cargo.toml, and a `src` subfolder.
2. Names: I would need to _name_ those. Sigh.
3. Unit tests: separating them requires, unfortunately, exposing APIs which shouldn't be. And it's not possible to conditionally define the visibility by `cfg`, I'd need a macro, or a forwarding function. Not great.
4. Doc tests: they cannot be separated from the items they document, so they would remain behind...

Then again, unit-tests and doc-tests are likely to have relatively few dependencies, and if they're the only ones sharing `[dev-dependencies]` maybe it's not too bad.

> [@epage](#):
>
> Per-build-target features was discussed in [This Development-cycle in Cargo: 1.77 | Inside Rust Blog](https://blog.rust-lang.org/inside-rust/2024/02/13/this-development-cycle-in-cargo-1-77/#when-to-use-packages-or-workspaces) which can be used similarly to per-build-target dependencies and talks about the trade offs of overloading treating the primary unit of work as a build target in a package vs a package in a workspace.

> [From the link] To that end, a thought experiment was proposed: what if we only supported one built output per package? Where would be the pain points?

I guess it really depends what is consider a build output:

- First of all: benchmarks, examples, doc tests, integration tests, and unit-tests are satellites, and not really "packages" of their own.
- With regard to libraries vs binaries, I could see it, though it also has a cost.

One great pattern -- especially in Rust -- is a Sans IO with a "pure" functional core wrapped into an "impure" (IO) shell. Today, the easy way to do so is to:

1. Define a library for the functional core.
2. Define a binary which provides the shell.

Splitting this explicitly in a lib & bin allows ensuring that I/O doesn't accidentally sneak in in the library, as well as allows "black box" benchmarking & integration testing the entire core.

It's possible to break that down into two crates, certainly. I in fact just did it yesterday. Then I lost 15 minutes failing to understand why my binary wasn't behaving as I expected, only to realize I had executed `cargo build` on the lib crate, not the bin crate, and thus the binary of course didn't reflect the changes I had made.

It's one of those ergonomic costs that come from artificially splitting a logical unit in two just to escape the dread of spurious dependencies.

> [@epage](#):
>
> That data model would not work because `dependencies.<name>` is your dependencies.

Sure, I was just spitballing to illustrate the point. _Some_ syntax can surely be worked out.

---

<div class="post-metadata">

**Author:** ![epage](https://sea2.discourse-cdn.com/flex002/user_avatar/internals.rust-lang.org/epage/32/3171_2.png) [@epage](https://internals.rust-lang.org/u/epage)\
**Post date:** [September 26, 2025, 5:05pm UTC](https://internals.rust-lang.org/t/breaking-down-crate-dependencies/23566/5 "2025-09-26T17:05:22Z")

</div>

> [@matthieum](#):
>
> I guess it really depends what is consider a build output:

In that post, I was referring to public outputs (binaries and libs) and not internal ones (build scripts, tests, benches, or examples)..

In your reply, you talked frequently of "benchmarks, examples, & tests'. For myself, I see less justification to re-design `Cargo.toml` enough to allow splitting out dependencies for these.

If a library exists just to isolate side effects from the binary, then I'm also not seeing the benefit to splitting up the dependencies. If the library is also being used in other packages, then it can make sense. However, at that point, your requirements have shifted. If you are using a registry, then the versioning requirements between the binary and library become different.

The linked post links out to [RFC #3374](https://github.com/rust-lang/rfcs/pull/3374) which caused problems with verifying the appropriate features are activated in light of feature unification ([https://github.com/rust-lang/rfcs/pull/3374#discussion\_r1235768792](https://github.com/rust-lang/rfcs/pull/3374#discussion_r1235768792)). I had considered calling out this problem specifically but per-target dependencies doesn't touch on package features. However, it does touch on dependency features. This is less likely to be an issue without the associated package feature enabled but there are still risks there.

Another consideration unique to this request is there is a concern over the ecosystem cost to adapting to new dependency tables. This doesn't mean we can't have new ones but that the benefits need to justify the costs.

ps I just remembered [Allow dependencies that only apply to specific cargo targets (bin, example, etc.) · Issue #1982 · rust-lang/cargo · GitHub](https://github.com/rust-lang/cargo/issues/1982) exists

---

<div class="post-metadata">

**Author:** ![matthieum](https://sea2.discourse-cdn.com/flex002/user_avatar/internals.rust-lang.org/matthieum/32/4023_2.png) [@matthieum](https://internals.rust-lang.org/u/matthieum)\
**Post date:** [September 27, 2025, 11:05am UTC](https://internals.rust-lang.org/t/breaking-down-crate-dependencies/23566/6 "2025-09-27T11:05:46Z")

</div>

> [@epage](#):
>
> If a library exists just to isolate side effects from the binary, then I'm also not seeing the benefit to splitting up the dependencies. If the library is also being used in other packages, then it can make sense. However, at that point, your requirements have shifted. If you are using a registry, then the versioning requirements between the binary and library become different.

There's more than one usecase 🙂

* * *

With regard to a library/binary split for Sans IO, separating dependencies has two benefits:

- Functional: prevents accidentally using IO libraries in the Sans IO core.
- Compile-time: avoids dragging in pointless dependencies (tokio, the banana/gorilla/jungle) in integration tests which are mocking the IO, and thus not using tokio at all.

* * *

Another usecase I find myself having is with generated protocol libraries. The library itself just provides model/encoders/decoders for the protocol, and that's all that clients of the libraries really need, however it's also useful for developers to provide "minimal" binaries which can be used to decode a blob from the command line, or to connect to a stream of messages and process them on the fly -- filtering, printing, saving to file, etc...

At the moment, I (ab)use examples for these binaries. Not because they're good examples of library usage, but simply because it means I get separate dependencies that do not get dragged into the downstream dependencies. In exchange, those dependencies bloat the tests, because unfortunately examples & tests share the `[dev-dependencies]` section.

This is another example of "satellite" binary, which is not strictly intended to be a "build output" in the grand sense of the term. It's never intended to be used in production, it's just a little debugging utility.

> [@epage](#):
>
> Another consideration unique to this request is there is a concern over the ecosystem cost to adapting to new dependency tables. This doesn't mean we can't have new ones but that the benefits need to justify the costs.

I think it's very important to ensure backward compatibility here.

That is, I think the _existing_ sections should be _extended_ but not _replaced_. In fact, the ability to specify _common_ dependencies only once is quite useful even after splitting. For example, when doing a binary/library split for Sans IO, chances are that the binary will need to use quite a few of the crates the library depends on (directly).

Thus I would see specialized sections as _adding_ dependencies to the existing sections for certain build targets. Just like `[dev-dependencies]` adds to `[dependencies]` for "dev" build targets today:

```toml
[dependencies]

# All build targets depend on a, b, and c.

a = "0.1"
b = "0.2"
c = "0.3"

[bin-dependencies]

# Binaries in this crate depend on d in addition to a, b, and c.

d = "0.4"

[dev-dependencies]

# Dev binaries in this crate depend on x in addition to a, b, and c.
# They do not depend on x by default.

x = "0.5"

[examples-dev-dependencies]

# Example binaries in this crate depend on y in addition to a, b, c, and x.

y = "0.6"

[benches-dev-dependencies]

# Benchmark binaries in this crate depend on criterion in addition to a, b, c, and x.

criterion = "0.7"

[unit-tests-dev-dependencies]
[doc-tests-dev-dependencies]
[integration-tests-dev-dependencies]

```

In the absence of `[dev-dependencies-unit-tests]`, unit tests are compiled with just `[dev-dependencies]` and `[dependencies]`; just like today.

> [@epage](#):
>
> ps I just remembered [Allow dependencies that only apply to specific cargo targets (bin, example, etc.) · Issue #1982 · rust-lang/cargo · GitHub](https://github.com/rust-lang/cargo/issues/1982) exists

Ah! I hesitated in calling this post "Pre-RFC" as I knew it must have come up before, and it was probably just my Google-fu being weak.

---

<div class="post-metadata">

**Author:** ![kornel](https://sea2.discourse-cdn.com/flex002/user_avatar/internals.rust-lang.org/kornel/32/2711_2.png) [@kornel](https://internals.rust-lang.org/u/kornel)\
**Post date:** [September 27, 2025, 12:43pm UTC](https://internals.rust-lang.org/t/breaking-down-crate-dependencies/23566/7 "2025-09-27T12:43:41Z")

</div>

> [@matthieum](#):
>
> `cargo` can only compile either 1 crate or 250 crates (and their 500 binaries when testing) is actually _just_ impractical.

Is this a UI issue? `cargo` technically can compile an arbitrary subset if you specify multiple `-p` flags. There's also `default-members` workspace setting.

Dependencies are cached in workspaces. In such large workspace, I presume you would have a lot of common dependencies shared between crates, and their tests and examples. Would you actually save much build time if you specified deps very granularly? Your test helpers and bench frameworks could be compiled once and reused everywhere (unless they get different feature flag, which is made worse by more fragmented dependency declarations).

---

<div class="post-metadata">

**Author:** ![matthieum](https://sea2.discourse-cdn.com/flex002/user_avatar/internals.rust-lang.org/matthieum/32/4023_2.png) [@matthieum](https://internals.rust-lang.org/u/matthieum)\
**Post date:** [September 28, 2025, 10:32am UTC](https://internals.rust-lang.org/t/breaking-down-crate-dependencies/23566/8 "2025-09-28T10:32:40Z")

</div>

> [@kornel](#):
>
> Dependencies are cached in workspaces. In such large workspace, I presume you would have a lot of common dependencies shared between crates, and their tests and examples. Would you actually save much build time if you specified deps very granularly? Your test helpers and bench frameworks could be compiled once and reused everywhere (unless they get different feature flag, which is made worse by more fragmented dependency declarations).

All 3rd-party dependencies are specified at the workspace level, with base features, and "inherited" with `dep = { workspace = true }`. A few dependencies such as tokio get extra features in specific crates (`net`, in particular), but that is the exception, rather than the norm.

So, yes, in theory most dependencies should be compiled once (or perhaps twice, for tokio). In practice, compiling multiple packages one after another tends to lead to some dependencies getting recompiling. I have not investigated any further, but suspect that some of the 3rd-party dependencies may enable feature that I do not, and depending on whether the package includes them or not...

This helps with compile times, but bloat still means extra link time (for nothing), especially with DI, and extra space consumed in the `target` folder, which is already regularly pushing the 100GB.

> [@kornel](#):
>
> > [@matthieum](#):
> >
> > `cargo` can only compile either 1 crate or 250 crates (and their 500 binaries when testing) is actually _just_ impractical.
> 
> Is this a UI issue? `cargo` technically can compile an arbitrary subset if you specify multiple `-p` flags. There's also `default-members` workspace setting.

Very much.

The `default-members` is of no help. The workspace is really only used to harmonize the 3rd-party dependency versions being used, and the members being compiled very much depend on the task at hand, and vary completely from one time to the next.

The `-p` flag is in theory possible, but specifying the package names one by one is very inconvenient. Sometimes, I'll use a little shell help: `for d in *; do $(cd $d && cargo build --release); done`.

Double-checking the docs, they do mention that `-p` supports wildcards, though there's no example with them, so it's not clear _where_ they're supported. I'll make a note to experiment with that, it may help.

---

<div class="post-metadata">

**Author:** ![kornel](https://sea2.discourse-cdn.com/flex002/user_avatar/internals.rust-lang.org/kornel/32/2711_2.png) [@kornel](https://internals.rust-lang.org/u/kornel)\
**Post date:** [September 28, 2025, 1:13pm UTC](https://internals.rust-lang.org/t/breaking-down-crate-dependencies/23566/9 "2025-09-28T13:13:02Z")

</div>

Specifying dependencies at workspace level helps, but it isn't sufficient, because features of indirect dependencies can be enabled or disabled depending on the set of packages being built together (e.g. you only use `tokio` without `net`, but build with or without another crate that uses `reqwest` which enables `tokio/net`).

```bash
for d in *; do $(cd $d && cargo build --release); done

```

This is the worst-case scenario for extra rebuilds, because feature flags of transitive dependencies will be adjusted for each build (OTOH you get potentially leaner executables due to each using minimum features).

You don't need wildcards in `-p`, you can specify it any number of times:

```bash
cargo build -p crate1 -p crate2 -p crate3 -p crate4 

```

and this will unify feature flags across the crates, so their dependencies will be built once.

[Cargo supports aliases](https://doc.rust-lang.org/cargo/reference/config.html#alias), so if you're frequently building or testing a certain subset, you could have an alias with all the `-p` flags needed.

If you have heavy deps that are only needed for examples or rarely used binaries, you could hide these behind feature flag, and use [`required-features`](https://doc.rust-lang.org/cargo/reference/cargo-targets.html#the-required-features-field) when defining each `[[example]]` and `[[bin]]`, although IMHO it's [disappointing that it can only break builds](https://github.com/rust-lang/cargo/issues/4663), instead of configuring them to work automatically.

---

<div class="post-metadata">

**Author:** ![Nemo157](https://sea2.discourse-cdn.com/flex002/user_avatar/internals.rust-lang.org/nemo157/32/11585_2.png) [@Nemo157](https://internals.rust-lang.org/u/Nemo157)\
**Post date:** [September 28, 2025, 3:23pm UTC](https://internals.rust-lang.org/t/breaking-down-crate-dependencies/23566/10 "2025-09-28T15:23:21Z")

</div>

You could also use aliases to get the “build this subdirectory” behavior (though with a lot of manual management): within each subdirectory define a `buildset = ["build", "-pfoo-bar", "-pfoo-baz"]` alias for that directory’s packages, then wherever you run `cargo buildset` from will use the nearest alias.

---

<div class="post-metadata">

**Author:** ![Vorpal](https://avatars.discourse-cdn.com/v4/letter/v/aca169/32.png) [@Vorpal](https://internals.rust-lang.org/u/Vorpal)\
**Post date:** [September 28, 2025, 8:12pm UTC](https://internals.rust-lang.org/t/breaking-down-crate-dependencies/23566/11 "2025-09-28T20:12:05Z")

</div>

> [@kornel](#):
>
> This is the worst-case scenario for extra rebuilds, because feature flags of transitive dependencies will be adjusted for each build (OTOH you get potentially leaner executables due to each using minimum features).

A way to deal with this is the cargo workspace hack technique: [cargo\_hakari::about - Rust](https://docs.rs/cargo-hakari/latest/cargo_hakari/about/index.html) explains the idea, and using cargo-hakari is the most reasonable way I have found for doing this.

---

<div class="post-metadata">

**Author:** ![matthieum](https://sea2.discourse-cdn.com/flex002/user_avatar/internals.rust-lang.org/matthieum/32/4023_2.png) [@matthieum](https://internals.rust-lang.org/u/matthieum)\
**Post date:** [September 29, 2025, 4:09pm UTC](https://internals.rust-lang.org/t/breaking-down-crate-dependencies/23566/12 "2025-09-29T16:09:17Z")

</div>

> [@kornel](#):
>
> You don't need wildcards in `-p`, you can specify it any number of times:
> 
> `cargo build -p crate1 -p crate2 -p crate3 -p crate4 `

I know.

The problem is that it's very unergonomic.

> [@kornel](#):
>
> [Cargo supports aliases](https://doc.rust-lang.org/cargo/reference/config.html#alias), so if you're frequently building or testing a certain subset, you could have an alias with all the `-p` flags needed.

I usually build subsets, but which subsets tend to vary depending on the task at hand...

> [@kornel](#):
>
> ```rust
> for d in *; do $(cd $d && cargo build --release); done
> 
> ```
> 
> This is the worst-case scenario for extra rebuilds, because feature flags of transitive dependencies will be adjusted for each build (OTOH you get potentially leaner executables due to each using minimum features).

TIL feature unification will unify features across binaries, thereby bloating binaries by default.

It's probably nice for Debug builds, but a bit unexpected for Release builds. I'll have to double-check how performance sensitive binaries are build.

---

<div class="post-metadata">

**Author:** ![kpreid](https://sea2.discourse-cdn.com/flex002/user_avatar/internals.rust-lang.org/kpreid/32/8484_2.png) [@kpreid](https://internals.rust-lang.org/u/kpreid)\
**Post date:** [September 29, 2025, 5:01pm UTC](https://internals.rust-lang.org/t/breaking-down-crate-dependencies/23566/13 "2025-09-29T17:01:19Z")

</div>

Most extraneous features should not have any effect on the binary because they only add items to the library, and as long as those items go unused, they have no effect on the contents of the binary.

The typical exception to this principle would be when enabling the feature requires the library having the feature to make changes to _existing_ items to support the feature — this should be kept uncommon.

---

<div class="post-metadata">

**Author:** ![matthieum](https://sea2.discourse-cdn.com/flex002/user_avatar/internals.rust-lang.org/matthieum/32/4023_2.png) [@matthieum](https://internals.rust-lang.org/u/matthieum)\
**Post date:** [September 29, 2025, 5:15pm UTC](https://internals.rust-lang.org/t/breaking-down-crate-dependencies/23566/14 "2025-09-29T17:15:27Z")

</div>

> [@kpreid](#):
>
> Most extraneous features should not have any effect on the binary because they only add items to the library, and as long as those items go unused, they have no effect on the contents of the binary.

Actually, even _addition_ is in itself a potential optimization barrier.

Specifically, the issue mostly manifests when transitioning from 1 to 2 possibility:

- A single implementation of a trait means that the dynamically dispatched calls can be optimized to the only implementation, this isn't the case any longer when there are 2 implementations.
- A single way to define a return value means that the value can be optimized to its only known value, this isn't the case any longer when there are 2 ways.

Pretty niche, admittedly, but still 🙂

---

<div class="post-metadata">

**Author:** ![bjorn3](https://sea2.discourse-cdn.com/flex002/user_avatar/internals.rust-lang.org/bjorn3/32/2736_2.png) [@bjorn3](https://internals.rust-lang.org/u/bjorn3)\
**Post date:** [September 30, 2025, 8:55am UTC](https://internals.rust-lang.org/t/breaking-down-crate-dependencies/23566/15 "2025-09-30T08:55:23Z")

</div>

> [@matthieum](#):
>
> A single implementation of a trait means that the dynamically dispatched calls can be optimized to the only implementation, this isn't the case any longer when there are 2 implementations.

That optimization is only possible to do with LTO and with LTO each binary gets optimized separately and the optimization would almost certainly run after throwing away all unused vtables.

> [@matthieum](#):
>
> A single way to define a return value means that the value can be optimized to its only known value, this isn't the case any longer when there are 2 ways.

This probably also needs LTO for the compiler to know.

---

<div class="post-metadata">

**Author:** ![DragonDev1906](https://avatars.discourse-cdn.com/v4/letter/d/e68b1a/32.png) [@DragonDev1906](https://internals.rust-lang.org/u/DragonDev1906)\
**Post date:** [September 30, 2025, 10:49am UTC](https://internals.rust-lang.org/t/breaking-down-crate-dependencies/23566/16 "2025-09-30T10:49:51Z")

</div>

> [@matthieum](#):
>
> Double-checking the docs, they do mention that `-p` supports wildcards, though there's no example with them, so it's not clear _where_ they're supported. I'll make a note to experiment with that, it may help.

TIL that wildcards are supported. Here is an example I've just tried: `cargo check -p '*-enclave'`.

(slightly off-topic): Now I just wish there'd be a cli flag to disable/choose feature unification between packages \[1\] (which would also make this behavior more explicit, while reusing the -p syntax): \[2\]

```rust
// Same as individual cargo build calls, same as current behavior with
// only one -p and no wildcards, could also be called '--no-feature-unification'.
cargo build --release -p '*-enclave' --feature-unification=''
// Same feature unification as --workspace but don't build everything
// Unless I'm mistaken that is what hakari is effectively doing.
cargo build --release -p '*-enclave' --feature-unification='*'
// Consider all packages ending with '-enclave' for feature unification,
// but only compile those ending with '-whatever-enclave',
// as if only packages ending with '-enclave' existed in the workspace.
cargo build --release -p '*-whatever-enclave' --feature-unification='*-enclave'

// Not quite sure what should happen in this case. It probably should
// behave like '--no-feature-unification' except for the (in-workspace)
// dependencies ending with '-types':
// If 'a-enclave' needs feature 'some-types:X' it will not be enabled for
// 'b-enclave' (because we didn't tell it to include 'a-enclave' for feature
// unification. But if 'other-types' needs feature 'tokio:net', both 'a-enclave'
// and 'b-enclave' will have 'tokio:net'.
cargo build --release -p '*-enclave' --feature-unification='*-types'

// These are/would be the same (I think)
cargo build --release --worksapce
cargo build --release -p '*'
cargo build --release --worksapce --feature-unification='*'

// For backwards compatibility these would need to be equivalent
// (should work for all wildcards and '--workspace').
cargo build --release -p '*-enclave'
cargo build --release -p '*-enclave' --feature-unification='*-enclave'

```

This is effectively telling cargo (and whoever sees/uses the command) which crates he has to be aware of if he wants reproducible builds. Another way would be to specify which individual dependencies should have all their features be unified like with '--workspace', but I think this is more intuitive and requires fewer entries (thanks to the wildcards). Especially due to its similarity with `-p`.

This way you can also decide when exactly you want it (perhaps with a way to specify a default behavior in the future). Splitting the "what to build" from the "which features should be unified" when needed.

As long as you don't change the `feature-unification` list each dependency is only compiled once (unless you have multiple major versions of course).

> [@kornel](#):
>
> OTOH you get potentially leaner executables due to each using minimum features

> [@matthieum](#):
>
> thereby bloating binaries by default.

Another situation where feature unification is unwanted is when the crates are intended for different compilation targets (e.g. embedded) or if there is a security boundary between them and you want reproducible builds that are not affected by other workspace members.

* * *

1. Argument name is bikesheddable of course. 

2. Shoud also work with wildcards like `something-*`, `something-*-whatever` and `*-foo-*`.
