cargo_hakari/docs/
about.rs

1// Copyright (c) The cargo-guppy Contributors
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4//! About workspace-hack crates, how `cargo hakari` manages them, and how much faster they make
5//! builds.
6//!
7//! # What are workspace-hack crates?
8//!
9//! Let's say you have a Rust crate `my-crate` with two dependencies:
10//!
11//! ```toml
12//! # my-crate/Cargo.toml
13//! [dependencies]
14//! foo = "1.0"
15//! bar = "2.0"
16//! ```
17//!
18//! Let's say that `foo` and `bar` both depend on `baz`:
19//!
20//! ```toml
21//! # foo-1.0/Cargo.toml
22//! [dependencies]
23//! baz = { version = "1", features = ["a", "b"] }
24//!
25//! # bar-2.0/Cargo.toml
26//! [dependencies]
27//! baz = { version = "1", features = ["b", "c"] }
28//! ```
29//!
30//! What features is `baz` built with?
31//!
32//! One way to resolve this question might be to build `baz` twice with each requested set of
33//! features. But this is likely to cause a combinatorial explosion of crates to build, so Cargo
34//! doesn't do that. Instead, [Cargo builds `baz`
35//! once](https://doc.rust-lang.org/nightly/cargo/reference/features.html?highlight=feature#feature-unification)
36//! with the *union* of the features enabled for the package: `[a, b, c]`.
37//!
38//! ---
39//!
40//! **NOTE:** This description elides some details around unifying build and dev-dependencies: for
41//! more about this, see the documentation for guppy's
42//! [`CargoResolverVersion`](guppy::graph::cargo::CargoResolverVersion).
43//!
44//! ---
45//!
46//! Now let's say you're in a workspace, with a second crate `your-crate`:
47//!
48//! ```toml
49//! # your-crate/Cargo.toml
50//! [dependencies]
51//! baz = { version = "1", features = ["c", "d"] }
52//! ```
53//!
54//! In this situation:
55//!
56//! | if you build                                 | `baz` is built with |
57//! | -------------------------------------------- | ------------------- |
58//! | just `my-crate`                              | `a, b, c`           |
59//! | just `your-crate`                            | `c, d`              |
60//! | `my-crate` and `your-crate` at the same time | `a, b, c, d`        |
61//!
62//! Even in this simplified scenario, there are three separate ways to build `baz`. For a dependency
63//! like [`syn`](https://crates.io/crates/syn) that has [many optional
64//! features](https://github.com/dtolnay/syn#optional-features), large workspaces end up with a very
65//! large number of possible build configurations.
66//!
67//! Even worse, the feature set of a package affects everything that depends on it, so `syn` being
68//! built with a slightly different feature set than before would cause *every package that directly
69//! or transitively depends on `syn` to be rebuilt. For large workspaces, this can result a lot of
70//! wasted build time.
71//!
72//! ---
73//!
74//! To avoid this problem, many large workspaces contain a `workspace-hack` crate. The purpose of
75//! this package is to ensure that dependencies like `syn` are always built with the same feature
76//! set no matter which workspace packages are currently being built. This is done by:
77//! 1. adding dependencies like `syn` to `workspace-hack` with the full feature set required by any
78//!    package in the workspace
79//! 2. adding `workspace-hack` as a dependency of every crate in the repository.
80//!
81//! Some examples of `workspace-hack` packages:
82//!
83//! * Rust's
84//!   [`rustc-workspace-hack`](https://github.com/rust-lang/rust/blob/0bfc45aa859b94cedeffcbd949f9aaad9f3ac8d8/src/tools/rustc-workspace-hack/Cargo.toml)
85//! * Firefox's
86//!   [`mozilla-central-workspace-hack`](https://hg.mozilla.org/mozilla-central/file/cf6956a5ec8e21896736f96237b1476c9d0aaf45/build/workspace-hack/Cargo.toml)
87//! * Oxide's
88//!   [`omicron-workspace-hack`](https://github.com/oxidecomputer/omicron/blob/a8176d58352dedf6e8a90fd97de21ec854ee57d9/workspace-hack/Cargo.toml)
89//!
90//! These packages have historically been maintained by hand, on a best-effort basis.
91//!
92//! # What can hakari do?
93//!
94//! Maintaining workspace-hack packages manually can result in:
95//! * Missing crates
96//! * Missing feature lists for crates
97//! * Outdated feature lists for crates
98//!
99//! All of these can result in longer than optimal build times.
100//!
101//! `cargo hakari` can automate the maintenance of these packages, greatly reducing the amount of
102//! time and effort it takes to maintain these packages.
103//!
104//! # How does hakari work?
105//!
106//! `cargo hakari` uses [guppy]'s Cargo build simulations to determine the full set of features that
107//! can be built for each package. It then looks for dependencies that are built in more than one
108//! way. With this information:
109//!
110//! * `cargo hakari` constructs a `workspace-hack` package with the union of the feature sets for
111//!   each dependency.
112//! * `cargo hakari` can also add lines to the `Cargo.toml` files of all workspace crates, to ensure
113//!   that the `workspace-hack` package is always included.
114//!
115//! For more details about the algorithm, see the documentation for the [`hakari`] library.
116//!
117//! # How much faster do builds get?
118//!
119//! The amount to which builds get faster depends on the size of the repository. In general, the
120//! benefit grows super-linearly with the size of the workspace and the number of crates in it.
121//!
122//! On moderately large workspaces with several hundred third-party dependencies, a cumulative
123//! performance benefit of up to **1.7x** has been seen. Individual commands can be anywhere from
124//! **1.1x** to **100x** faster. `cargo check` often benefits more than `cargo build` because
125//! expensive linker invocations aren't a factor.
126//!
127//! ## Benchmarks
128//!
129//! For a moderately large workspace, here's a chart of cumulative build times across a range of
130//! `cargo build` commands, with and without hakari:
131//!
132//! ![](https://raw.githubusercontent.com/guppy-rs/hakari-on-omicron-perf/refs/heads/main/cumulative.png)
133//!
134//! The orange line ("Without Hakari") is the default experience provided by Cargo, while the blue
135//! line ("With Hakari") is the default experience with `cargo hakari` enabled. The green line
136//! ("Hakari without target-host unification") is an advanced option: see
137//! [`UnifyTargetHost`](hakari::UnifyTargetHost) for more.
138//!
139//! For more information including the raw data, see [this
140//! repository](https://github.com/guppy-rs/hakari-on-omicron-perf).
141//!
142//! # Drawbacks
143//!
144//! * The first build in a workspace might take longer because more dependencies have to be cached.
145//!   - This also applies to builds performed after `cargo clean`, or after Rust version upgrades.
146//!   - However, in some cases the first build has been observed to be faster.
147//!   - In any case, the first build is a relatively small part of overall interactive build times.
148//! * Some crates may accidentally start skipping features they really need, because the
149//!   workspace-hack turns those features on for them.
150//!   - This is not a major issue for repositories that don't release crates to `crates.io`.
151//!   - It can also be caught at publish time, or with a periodic CI job that does a build after
152//!     running `cargo hakari disable`.
153//! * Publishing crates to a registry becomes more complex: see the [publishing
154//!   section](crate::publishing) for more about this.
155//! * Downstream users that import your crate directly from your repository, rather than from the
156//!   registry, are going to import dependencies from the checked in workspace-hack. This can be
157//!   avoided by following the instructions in the [`[patch]` directive
158//!   section](crate::patch_directive).