Expand description

About workspace-hack crates, how cargo hakari manages them, and how much faster they make builds.

What are workspace-hack crates?

Let’s say you have a Rust crate my-crate with two dependencies:

foo = "1.0"
bar = "2.0"

Let’s say that foo and bar both depend on baz:

baz = { version = "1", features = ["a", "b"] }

baz = { version = "1", features = ["b", "c"] }

What features is baz built with?

One way to resolve this question might be to build baz twice with each requested set of features. But this is likely to cause a combinatorial explosion of crates to build, so Cargo doesn’t do that. Instead, Cargo builds baz once with the union of the features enabled for the package: [a, b, c].

NOTE: This description elides some details around unifying build and dev-dependencies: for more about this, see the documentation for guppy’s CargoResolverVersion.

Now let’s say you’re in a workspace, with a second crate your-crate:

baz = { version = "1", features = ["c", "d"] }

In this situation:

if you buildbaz is built with
just my-cratea, b, c
just your-cratec, d
my-crate and your-crate at the same timea, b, c, d

Even in this simplified scenario, there are three separate ways to build baz. For a dependency like syn that has many optional features, large workspaces end up with a very large number of possible build configurations.

Even worse, the feature set of a package affects everything that depends on it, so syn being built with a slightly different feature set than before would cause *every package that directly or transitively depends on syn to be rebuilt. For large workspaces, this can result a lot of wasted build time.

To avoid this problem, many large workspaces contain a workspace-hack crate. The purpose of this package is to ensure that dependencies like syn are always built with the same feature set no matter which workspace packages are currently being built. This is done by:

  1. adding dependencies like syn to workspace-hack with the full feature set required by any package in the workspace
  2. adding workspace-hack as a dependency of every crate in the repository.

Some examples of workspace-hack packages:

These packages have historically been maintained by hand, on a best-effort basis.

What can hakari do?

Maintaining workspace-hack packages manually can result in:

  • Missing crates
  • Missing feature lists for crates
  • Outdated feature lists for crates

All of these can result in longer than optimal build times.

cargo hakari can automate the maintenance of these packages, greatly reducing the amount of time and effort it takes to maintain these packages.

How does hakari work?

cargo hakari uses guppy’s Cargo build simulations to determine the full set of features that can be built for each package. It then looks for

For more details about the algorithm, see the documentation for the hakari library.

How much faster do builds get?

The amount to which builds get faster depends on the size of the repository. In general, the benefit grows super-linearly with the size of the workspace and the number of crates in it.

On moderately large workspaces with several hundred third-party dependencies, a cumulative performance benefit of 20-25% has been seen. Individual commands can be anywhere from 10% to 95+% faster. cargo check often benefits more than cargo build because expensive linker invocations aren’t a factor.

Performance metrics

All measurements were taken on the following system:

  • Processor: AMD Ryzen 9 3900X processor (12 cores, 24 threads)
  • Memory: 64GB
  • Operating system: Pop!_OS 21.04, running Linux kernel 5.13
  • Filesystem: btrfs

On the Diem repository, at revision 6fa1c8c0, with the following cargo build commands in sequence:

CommandBefore (s)After (s)ChangeNotes
-p move-lang35.5653.0649.21%First command has to build more dependencies
-p move-lang --all-targets46.6425.45-45.44%
-p move-vm-types10.560.29-97.24%This didn’t have to build anything
-p network19.1614.10-26.42%
-p network --all-features21.5918.20-15.70%
-p storage-interface7.042.97-57.83%
-p storage-interface --all-features12.781.15-91.03%
-p diem-node102.3284.65-17.27%This command built a large C++ dependency
-p backup-cli52.4733.26-36.61%Linked several binaries

With the following cargo check commands in sequence:

CommandBefore (s)After (s)ChangeNotes
-p move-lang16.0436.55127.83%First command has to build more dependencies
-p move-lang --all-targets26.7313.22-50.56%
-p move-vm-types9.410.29-96.91%This didn’t have to build anything
-p network12.419.43-24.01%
-p network --all-features15.1211.54-23.69%
-p storage-interface5.331.65-68.98%
-p storage-interface --all-features8.221.02-87.59%
-p diem-node56.6051.29-9.38%This command built two large C++ dependencies
-p backup-cli13.575.51-59.40%

On the much smaller cargo-guppy repository, at revision 65e8c8d7, with the following cargo build commands in sequence:

CommandBefore (s)After (s)ChangeNotes
-p guppy11.7713.4814.53%First command has to build more dependencies
-p guppy --all-features9.839.72-1.12%
-p hakari6.033.75-37.94%
-p hakari --all-features10.7810.28-4.68%
-p determinator4.603.90-15.22%
-p cargo-hakari17.727.22-59.26%


  • The first build in a workspace will take longer because more dependencies have to be cached.
    • This also applies to builds performed after cargo clean, or after Rust version upgrades.
    • However, this usually pays off over time.
  • Some crates may accidentally start skipping features they really need, because the workspace-hack turns those features on for them.
    • This is not a major issue for repositories that don’t release crates to crates.io.
    • It can also be caught at publish time, or with a periodic CI job that does a build after running cargo hakari disable.
  • Downstream users that import your crate directly from your repository, rather than from the registry, are going to import dependencies from the checked in workspace-hack.
  • Publishing crates to a registry becomes more complex: see the publishing module for more about this.