cargo_hakari/docs/about.rs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158
// Copyright (c) The cargo-guppy Contributors
// SPDX-License-Identifier: MIT OR Apache-2.0
//! 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:
//!
//! ```toml
//! # my-crate/Cargo.toml
//! [dependencies]
//! foo = "1.0"
//! bar = "2.0"
//! ```
//!
//! Let's say that `foo` and `bar` both depend on `baz`:
//!
//! ```toml
//! # foo-1.0/Cargo.toml
//! [dependencies]
//! baz = { version = "1", features = ["a", "b"] }
//!
//! # bar-2.0/Cargo.toml
//! [dependencies]
//! 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](https://doc.rust-lang.org/nightly/cargo/reference/features.html?highlight=feature#feature-unification)
//! 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`](guppy::graph::cargo::CargoResolverVersion).
//!
//! ---
//!
//! Now let's say you're in a workspace, with a second crate `your-crate`:
//!
//! ```toml
//! # your-crate/Cargo.toml
//! [dependencies]
//! baz = { version = "1", features = ["c", "d"] }
//! ```
//!
//! In this situation:
//!
//! | if you build | `baz` is built with |
//! | -------------------------------------------- | ------------------- |
//! | just `my-crate` | `a, b, c` |
//! | just `your-crate` | `c, d` |
//! | `my-crate` and `your-crate` at the same time | `a, b, c, d` |
//!
//! Even in this simplified scenario, there are three separate ways to build `baz`. For a dependency
//! like [`syn`](https://crates.io/crates/syn) that has [many optional
//! features](https://github.com/dtolnay/syn#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:
//!
//! * Rust's
//! [`rustc-workspace-hack`](https://github.com/rust-lang/rust/blob/0bfc45aa859b94cedeffcbd949f9aaad9f3ac8d8/src/tools/rustc-workspace-hack/Cargo.toml)
//! * Firefox's
//! [`mozilla-central-workspace-hack`](https://hg.mozilla.org/mozilla-central/file/cf6956a5ec8e21896736f96237b1476c9d0aaf45/build/workspace-hack/Cargo.toml)
//! * Oxide's
//! [`omicron-workspace-hack`](https://github.com/oxidecomputer/omicron/blob/a8176d58352dedf6e8a90fd97de21ec854ee57d9/workspace-hack/Cargo.toml)
//!
//! 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 dependencies that are built in more than one
//! way. With this information:
//!
//! * `cargo hakari` constructs a `workspace-hack` package with the union of the feature sets for
//! each dependency.
//! * `cargo hakari` can also add lines to the `Cargo.toml` files of all workspace crates, to ensure
//! that the `workspace-hack` package is always included.
//!
//! 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 up to **1.7x** has been seen. Individual commands can be anywhere from
//! **1.1x** to **100x** faster. `cargo check` often benefits more than `cargo build` because
//! expensive linker invocations aren't a factor.
//!
//! ## Benchmarks
//!
//! For a moderately large workspace, here's a chart of cumulative build times across a range of
//! `cargo build` commands, with and without hakari:
//!
//! ![](https://raw.githubusercontent.com/guppy-rs/hakari-on-omicron-perf/refs/heads/main/cumulative.png)
//!
//! The orange line ("Without Hakari") is the default experience provided by Cargo, while the blue
//! line ("With Hakari") is the default experience with `cargo hakari` enabled. The green line
//! ("Hakari without target-host unification") is an advanced option: see
//! [`UnifyTargetHost`](hakari::UnifyTargetHost) for more.
//!
//! For more information including the raw data, see [this
//! repository](https://github.com/guppy-rs/hakari-on-omicron-perf).
//!
//! # Drawbacks
//!
//! * The first build in a workspace might take longer because more dependencies have to be cached.
//! - This also applies to builds performed after `cargo clean`, or after Rust version upgrades.
//! - However, in some cases the first build has been observed to be faster.
//! - In any case, the first build is a relatively small part of overall interactive build times.
//! * 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`.
//! * Publishing crates to a registry becomes more complex: see the [publishing
//! section](crate::publishing) for more about this.
//! * 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. This can be
//! avoided by following the instructions in the [`[patch]` directive
//! section](crate::patch_directive).