- Feature Name:
build-std-context - Start Date: 2025-06-05
- RFC PR: rust-lang/rfcs#3873
- Rust Issue: N/A
Summary
While Rust’s pre-built standard library has proven itself sufficient for the majority of use cases, there are a handful of use cases that are not well supported:
- Rebuilding the standard library to match the user’s profile
- Rebuilding the standard library with ABI-modifying flags
- Building the standard library for tier three targets
Proposals to solve these problems come broadly under the umbrella of “build-std” and date back over 10 years ago, though no complete solution has yet reached consensus.
This RFC does not propose any changes directly, only document the background, history and motivations for build-std. It is part of a series of build-std RFCs and later RFCs will reference this one. This RFC is part of the build-std project goal.
- build-std context (this RFC)
build-std="always"(rfcs#3874)- Explicit standard library dependencies (rfcs#3875)
build-std="compatible"(RFC not opened yet)build-std="match-profile"(RFC not opened yet)
This RFC is co-authored by David Wood and Adam Gemmell. To improve the readability of this RFC, it does not follow the standard RFC template, while still aiming to capture all of the salient details that the template encourages.
There is also a literature review appendix in a HackMD which contains a summary of all literature found during the process of writing this RFC.
Scope
build-std has a long and storied history of previous discussions and proposals which cover a large area and many use-cases. Any individual future RFC will not be able to support many use cases that those waiting for build-std hope that it will. This is also an explicit and deliberate choice for the build-std project goal’s proposals.
This RFC will focus on summarising these previous discussions and proposals in order to enable an MVP of build-std to be accepted and stabilised. This will lay the foundation for future proposals to lift restrictions and enable build-std to support more use cases, without those proposals having to survey the ten-plus years of issues, pull requests and discussion that this RFC has.
Acknowledgements
This RFC would not have been possible without the advice, feedback and support of Josh Triplett, Eric Huss, Wesley Wiser and Tomas Sedovic throughout this entire effort.
Thanks to mati865 for advising on some of the specifics related to special object files, petrochenkov for his expertise on rustc’s dependency loading and name resolution; fee1-dead for their early and thorough reviews; Ed Page for writing about opaque dependencies and his invaluable Cargo expertise; Jacob Bramley for his feedback on early drafts; as well as Amanieu D’Antras, Tobias Bieniek, Adam Harvey, James Munns, Jonathan Pallant, Jieyou Xu, Jakub Beránek, Weihang Lo, and Mark Rousskov for providing feedback from their areas of expertise on later drafts.
Terminology
The following terminology is used throughout the RFC:
- “the standard library” is used to refer to multiple of the crates that
constitute the standard library such as
core,alloc,std,test,proc_macroor their dependencies. - “std” is used to refer only to the
stdcrate, not the entirety of the standard library
Throughout the build-std project goal’s later RFCs, parentheses with “?” links (?) will be present which link to the relevant “Rationale and alternatives” section to justify a decision or provide alternatives to it.
Additionally, “note alerts” will be used in the Proposal sections to separate implementation considerations from the core proposal. Implementation details should be considered non-normative. These details could change during implementation and are present solely to demonstrate that the implementation feasibility has been considered and to provide an example of how implementation could proceed.
Note
This is an example of a “note alert” that will be used to separate implementation detail from the proposal proper.
Background
This section aims to introduce any relevant details about the standard library and compiler that are assumed knowledge by referenced sources and later RFCs.
See Implementation summary for a summary of the current unstable build-std feature in Cargo.
Standard library
Since the first stable release of Rust, the standard library has been distributed as a pre-built artifact via rustup, which has a variety of advantages and/or rationale:
- It saves Rust users from having to rebuild the standard library whenever they start a project or do a clean build
- The standard library has and has had dependencies which require a more
complicated build environment than typical Rust projects
- e.g. requiring a working C toolchain to build
compiler_builtins’cfeature
- e.g. requiring a working C toolchain to build
- To varying degrees at different times in its development, the standard library’s implementation has been tied to the compiler implementation and has had to change in lockstep
Not all targets have a pre-built standard library distributed via rustup, though it is a minimum requirement for certain platform support tiers. According to rustc’s platform support docs, for tier three targets:
Tier 3 targets are those which the Rust codebase has support for, but which the Rust project does not build or test automatically, so they may or may not work. Official builds are not available.
..and tier two targets:
The Rust project builds official binary releases of the standard library (or, in some cases, only the core library) for each tier 2 target, and automated builds ensure that each tier 2 target can be used as build target after each change.
..and finally, tier one targets:
The Rust project builds official binary releases for each tier 1 target, and automated testing ensures that each tier 1 target builds and passes tests after each change.
As an innate property of the target, not all targets can support the std crate.
This is independent of its tier, where as stated in the
Target Tier Policy lower-tier targets may not have a
complete implementation for all APIs in the crates they can support.
All of the standard library crates leverage permanently unstable features provided by the compiler that will never be stabilised and therefore require nightly to build.
The configuration for the pre-built standard library build is spread across bootstrap, the standard library workspace, individual standard library crate manifests and the target specification. The pre-built standard library is installed into the sysroot.
At the beginning of compilation, unless the crate has the #![no_std]
attribute, the compiler will load the libstd.rlib file from the sysroot as a
dependency of the current crate and add an implicit extern crate std for it.
This is the mechanism by which every crate has an implicit dependency on the
standard library.
The standard library sources are distributed in the rust-src component by
rustup and placed in the sysroot under lib/rustlib/src/. The sources consist
of the library/ workspace plus src/llvm-project/libunwind, which was
required in the past to build the unwind crate on some targets.
Cargo supports explicitly declaring a dependency on crates with the same names
as standard library crates with a path source
(e.g. core = { path = "../my_core" }), which rustc will load instead of crates
in the sysroot. Crates with these dependencies are not accepted by crates.io,
but there are crates on GitHub that use this pattern, such as
embed-rs/stm32f7-discovery, which are used as git
dependencies of other crates on GitHub.
Dependencies
Behind the facade, the standard library is split into multiple crates, some of which are in different repositories and included as submodules or using JOSH.
As well as local crates, the standard library depends on crates from crates.io.
It needs to be able to point these crates’ dependencies on the standard library
at the sources of core, alloc and std in the current rust-lang/rust
checkout.
This is achieved through use of the rustc-dep-of-std feature. Crates used in
the dependency graph of std declare a rustc-dep-of-std feature and when
enabled, add new dependencies on rustc-std-workspace-{core,alloc,std}.
rustc-std-workspace-{core,alloc,std} are empty crates published on crates.io.
As part of the workspace for the standard library,
rustc-std-workspace-{core,alloc,std} are patched with a path source to the
directory for the corresponding crate.
Historically, there have necessarily been C dependencies of the standard library,
increasing the complexity of the build environment required. While these have
largely been removed over time - for example, libbacktrace previously depended
on backtrace-sys but now uses gimli (rust#46439), a pure-rust
implementation. There are still some C dependencies:
libunwindwill either link to the LLVMlibunwindor the system’slibunwind/libgcc_s. LLVM’slibunwindis shipped as part of the rustup component for the standard library and will be linked against when-Clink-self-containedis used- This only applies to Linux and Fuchsia targets
compiler_builtinshas an optionalcfeature that will use optimised routines fromcompiler-rtwhen enabled. It is enabled for the pre-built standard librarycompiler_builtinshas an optionalmemfeature that provides symbols for common memory routines (e.g.memcpy)- It is enabled automatically on some
no_stdplatforms as whenstdis builtlibcprovides these routines. - Users can rely on weak linkage to override these symbols, but in scenarios where weak linkage is not supported or where the symbols are to be overridden from a shared library, then users must directly turn the feature off.
- It is enabled automatically on some
- To use sanitizers, the sanitizer runtimes from LLVM’s compiler-rt need to
be linked against. Building of these is enabled in
bootstrap.toml(build.sanitizers) and they are included in the rustup components shipped by the project.
Dependencies of the standard library may use unstable or internal compiler and language features only when they are a dependency of the standard library.
Features
There are a handful of features defined in the standard library crates’
Cargo.tomls. These features are not strictly additive (llvm-libunwind and
system-llvm-libunwind are mutually exclusive). There is currently no stable
existing mechanism for users to enable or disable these features. The default
set of features is determined by logic in bootstrap
and the rust.std-features key in bootstrap.toml.
The enabled features are often different depending on the target.
It is also common for user crates to depend on the standard library (via
#![no_std]) conditional on Cargo features being enabled or disabled (e.g. a
std feature or if --test is used).
Target support
The std crate’s build.rs checks for supported values of the
CARGO_CFG_TARGET_* environment variables. These variables are akin to the
conditional compilation configuration options,
and often correspond to parts of the target triple (for example,
CARGO_CFG_TARGET_OS corresponds to the “os” part of a target triple - “linux”
in “aarch64-unknown-linux-gnu”). This filtering is strict enough to distinguish
between built-in targets but loose enough to match similar custom targets. There
is no equivalent mechanism on the alloc or core crates.
When encountering an unknown or unsupported operating system then the
restricted_std cfg is set. restricted_std marks the entire standard library
as unstable, requiring feature(restricted_std) to be enabled on any crate that
depends on it. The only way for users to enable the restricted_std feature on
behalf of dependencies is the uncommon -Zcrate-attr=features(restricted_std)
rustc flag and users commonly report that they are not aware how to do this.
Cargo and rustc support custom targets, defined in JSON files according to an
unstable schema defined in the compiler. On nightly, users can dump the
target-spec-json for an existing target using --print target-spec-json. This
JSON can be saved in a file, tweaked and used as the argument to --target. It
is unintentional but custom target specifications can be used with --target
even on stable toolchains (rust#71009 proposes destabilising this behaviour).
However, as custom targets do not have a pre-built standard library and so must
use -Zbuild-std, their use is relegated to nightly toolchains in practice.
Custom targets may have restricted_std set depending on their cfg
configuration options.
Prelude
rustc has the concept of the “extern prelude” which is the set of crates that
can be referred to without an explicit extern crate statement. Originally this
was populated by users writing extern crate $crate in their code for each
direct dependency. Since the 2018 edition, crates passed via --extern are
added to the extern prelude. core is always added to the extern prelude. For
crates without the #![no_std] attribute, std is added to the extern prelude.
core or std’s prelude module (depending on the presence of #![no_std]) is
imported by rustc injecting a use $crate::prelude::rust_20XX::* statement.
extern crate can still be used and will search for the dependency in locations
where direct dependencies can be found, such as -L crate= paths or in the
sysroot. -L dependency= paths will not be searched, as these directories only
contain indirect dependencies (i.e. dependencies of direct dependencies).
Although only std or core are added to the extern prelude automatically,
users can still write extern crate alloc or extern crate test to load them
from the sysroot.
--extern has a noprelude modifier which will allow the user to use
--extern to specify the location at which a crate can be found without adding
it to the extern prelude. This could allow a path for crates like alloc or
test to be provided without affecting the observable behaviour of the
language.
Panic strategies
Rust has the concept of a panic handler, which is a crate that is responsible
for performing a panic. There are various panic handler crates on crates.io,
such as panic-abort (which is different from the panic_abort panic
runtime!), panic-halt, panic-itm, and panic-semihosting. Panic handler
crates define a function annotated with #[panic_handler]. There can only be
one #[panic_handler] in the crate graph.
core uses the panic handler to implement panics inserted by code generation
(e.g. arithmetic overflow or out-of-bounds access) and the core::panic! macro
immediately delegates to the panic handler crate.
std defines a panic handler. std’s panic handler function and its
std::panic! macro print panic information to stderr and delegate to a
panic runtime to decide what to do next, determined by the panic strategy.
There are two panic runtime crates in the standard library - panic_unwind
(which gracefully unwinds the stack using libunwind and performs cleanup) and
panic_abort (which terminates the program shortly after being called). Each
target supported by rustc specifies a default panic strategy - either “unwind”
or “abort” - though these are only relevant if std’s panic handler is used
(i.e. the target isn’t a no_std target or being used with a no_std crate).
Rust’s -Cpanic flag allows the user to choose the panic strategy, with the
target’s default as a fallback. If -Cpanic=unwind is provided then this
doesn’t guarantee that the unwind strategy is used, as the target may not
support it.
Both crates are compiled and shipped with the pre-built standard library for
targets which support std. Some targets have a pre-built standard library with
only the core and alloc crates, such as the x86_64-unknown-none target.
While x86_64-unknown-none defaults to the abort panic strategy, as this
target does not support the standard library, this default isn’t actually
relevant.
The std crate has a panic_unwind feature that enables an optional dependency
on the panic_unwind crate.
core also provides support for the (unstable) -Cpanic=immediate_abort
strategy by modifying the core::panic! macro to immediately call the abort
intrinsic without calling the panic handler, which can dramatically reduce code
size. std also adds an immediate abort to its panic! macro.
Cargo
Cargo’s building of the dependency graph is largely driven by the registry
index, except for crates from git or path sources.
Cargo registries, like crates.io, are centralised sources for crates. A registry’s index is the interface between Cargo and the registry that Cargo queries to know which versions are available for any given crate, what its dependencies are, etc.
Cargo can query registries using a Git protocol which caches the registry on disk, or using a sparse protocol which exposes the index over HTTP and allows Cargo to avoid having a local copy of the whole index, which has become quite large for crates.io.
crates.io’s registry index is exposed as both a HTTP API and a Git repository - rust-lang/crates.io-index - both are updated automatically by crates.io when crates are published, yanked, etc. The HTTP API is mostly used.
Each crate in the registry index has a JSON file, following
a defined schema which is jointly maintained by the Cargo
and crates.io teams. Crates may refer to those in other registries, but all
non-path/git crates in the dependency graph must exist in a registry. As the
registry index drives the building of Cargo’s dependency graph, all
non-path/git crates that end up in the dependency graph must be present in a
registry.
When a package is published, Cargo posts a JSON blob to the registry which is
not an index entry but has sufficient information to generate one. crates.io does
not use Cargo’s JSON blob, instead re-generating it from the Cargo.toml (this
avoids the index and Cargo.toml from going out-of-sync due to bugs or
malicious publishes). As a consequence, changes to the index format must be
duplicated in Cargo and crates.io. Behind the scenes, data from the Cargo.toml
extracted by crates.io is written to a database, which is where the index entry
and frontend are generated from.
Dependency information of crates in the registry are rendered in the crates.io frontend.
Registries can have different policies for what crates are accepted. For
example, crates.io does not permit publishing packages named std or core but
other registries might.
Public/private dependencies
Public and private dependencies are an unstable feature which enables declaring which dependencies form part of a library’s public interface, so as to make it easier to avoid breaking semver compatibility.
With the public-dependency feature enabled, dependencies are marked as
“private” by default which can be overridden with a public = true declaration.
Private dependencies are passed to rustc with a priv modifier to the
--extern flag. Dependencies without this modifier are treated as public by
rustc for backwards compatibility reasons. rust emits the
exported-private-dependencies lint if an item from a private dependency is
re-exported.
Target modifiers
rfcs#3716 introduced the concept of target modifiers to rustc. Flags marked as target modifiers must match across the entire crate graph or the compilation will fail.
For example, flags are made target modifiers when they change the ABI of generated code and could result in unsound ABI mismatches if two crates are linked together with different values of the flag set.
History
The following summary of the prior art is necessarily less detailed than the source material, which is exhaustively surveyed in Appendix: Exhaustive literature review.
rfcs#1133 (2015)
build-std was first proposed in a 2015 RFC (rfcs#1133) by
Ericson2314, aiming to improve support for targets that do not have a
pre-built standard library; to enable building the standard library with
different profiles; and to simplify rustbuild (now bootstrap). It also was
written with the goal of supporting the user in providing a custom
implementation of the standard library and supporting different implementations
of the language that provide their own standard libraries.
This RFC proposed that the standard library be made an explicit dependency in
Cargo.toml and be rebuilt automatically when required. An implicit dependency
on the standard library would be added automatically unless an explicit
dependency is written. This RFC was written prior to a stable #![no_std]
attribute and so does not address the circumstance where an implicit dependency
would make a #![no_std] crate fail to compile on a target that does not
support the standard library.
There were objectives of and possibilities enabled by the RFC that were not shared with the project teams at the time, such as the standard library being a regular crate on crates.io and the concept of the sysroot being retired. Despite this, the RFC appeared to be close to acceptance before being blocked by Cargo having a mechanism to have unstable features and then closed in favour of cargo#4959.
xargo and cargo#4959 (2016)
While the discussions around rfcs#1133 were ongoing, xargo was released in
2016. Xargo is a Cargo wrapper that builds a sysroot with a customised standard
library and then uses that with regular Cargo operations (i.e. xargo build
performs the same operation as cargo build but with a customised standard
library). Configuration for the customised standard library was configured in
the Xargo.toml, supporting configuring codegen flags, profile settings, Cargo
features and multi-stage builds. It required nightly to build the standard
library as it did not use RUSTC_BOOTSTRAP. Xargo had inherent limitations due
to being a Cargo wrapper, leading to suggestions that its functionality be
integrated into Cargo.
cargo#4959 is a proposal inspired by xargo, suggesting that a [sysroot]
section be added to .cargo/config which would enable similar configuration to
that of Xargo.toml. If this configuration is set, Cargo would build and use a
sysroot with a customised standard library according to the configuration
specified and the release profile. This sysroot would be rebuilt whenever
relevant configuration changes (e.g. profiles). cargo#4959 received varied
feedback: the proposed syntax was not sufficiently user-friendly; it did not
enable the user to customise the standard library implementation; and that
exposing bootstrap stages was brittle and user-unfriendly. cargo#4959 wasn’t
updated after submission so ultimately stalled and remains open.
rfcs#1133 and cargo#4959 took very different approaches to build-std, with cargo#4959 proposing a simpler approach that exposed the necessary low-level machinery to users and rfcs#1133 attempting to take a more first-class and user-friendly approach that has many tricky design implications.
rfcs#2663 (2019)
In 2019, rfcs#2663: std Aware Cargo was opened as the most
recent RFC attempting to advance build-std. rfcs#2663 shared many of the
motivations of rfcs#1133: building the standard library for tier three and
custom targets; customising the standard library with different Cargo features;
and applying different codegen flags to the standard library. It did not concern
itself with build-std’s potential use in rustbuild or with abolishing the
sysroot.
rfcs#2663 was primarily concerned with what functionality should be available
to the user and what the user experience ought to be. It proposed that core,
alloc and std be automatically built when the target did not have a
pre-built standard library available through rustup. It would be automatically
rebuilt on any target when the profile configuration was modified such that it
no longer matched the pre-built standard library. If using nightly, the user
could enable Cargo features and modify the source of the standard library.
Standard library dependencies were implicit by default, as today, but would be
written explicitly when enabling Cargo features. It also aimed to stabilise the
target-spec-json format and allow “stable” Cargo features to be enabled on
stable toolchains, and as such proposed the concept of stable and unstable Cargo
features be introduced.
There was a lot of feedback on rfcs#2663 which largely stemmed from it being very high-level, containing many large unresolved questions and details left for the implementers to work out. For example, it proposed that there be a concept of stable and unstable Cargo features but did not elaborate any further, leaving that as an implementation detail. Nevertheless, the proposal was valuable in more clearly elucidating a potential user experience that build-std could aim for, and the feedback provided was incorporated into the wg-cargo-std-aware effort, described below.
wg-cargo-std-aware (2019-)
rfcs#2663 demonstrated that there was demand for a mechanism for being able to (re-)build the standard library, and the feedback showed that this was a thorny problem with lots of complexity, so in 2019, the wg-cargo-std-aware repository was created to organise related work and explore the issues involved in build-std.
wg-cargo-std-aware led to the current unstable implementation of -Zbuild-std
in Cargo, which is described in detail in the Implementation summary
section below.
Issues in the wg-cargo-std-aware repository can be roughly partitioned into seven categories:
-
Exploring the motivations and use cases for the standard library
There are a handful of motivations catalogued in the wg-cargo-std-aware repository, corresponding to those raised in the earlier RFCs and proposals:
- Building with custom profile settings (wg-cargo-std-aware#2)
- Building for unsupported targets (wg-cargo-std-aware#3)
- Building with different Cargo features (wg-cargo-std-aware#4)
- Replacing the source of the standard library (wg-cargo-std-aware#7)
- Using build-std in bootstrap/rustbuild (wg-cargo-std-aware#19)
- Improving the user experience for
no_stdbinary projects (wg-cargo-std-aware#36)
These are all either fairly self-explanatory, described in the summary of the previous RFCs/proposals above, or in the Motivation section of this RFC.
-
Support for build-std in Cargo’s subcommands
Cargo has various subcommands where the desired behaviour when used with build-std needs some thought and consideration. A handful of issues were created to track this, most receiving little to no discussion:
cargo metadata,cargo clean,cargo pkgid, and the-pflag.cargo fetchhad fairly intuitive interactions with build-std - thatcargo fetchshould also fetch any dependencies of the standard library - which was implemented in cargo#10129.The
--build-planflag does not support build-std and its issue did not receive much discussion, but the future of this flag in its entirety seems to be uncertain.cargo vendordid receive lots of discussion. Vendoring the standard library is desirable (for the same reasons as any vendoring), but would lock the user to a specific version of the toolchain when using a vendored standard library. However, if therust-srccomponent contained already-vendored dependencies, thencargo vendorwould not need to support build-std and users would see the same advantages.Vendored standard library dependencies were implemented using a hacky approach (necessarily, prior to the standard library having its own workspace), but this was later reverted due to bugs. No attempt has been made to reimplement vendoring since the standard library has had its own workspace.
-
Dependencies of the standard library
There are a handful of dependencies of the standard library that may pose challenges for build-std by dint of needing a working C toolchain or special-casing.
libbacktracepreviously required a C compiler to buildbacktrace-sys, but now usesgimliinternally.compiler_builtinshas acfeature that uses C versions of some intrinsics that are more optimised. This is used by the pre-built standard library, and if not used by build-std, could be a point of divergence.compiler-builtins/ccan have a significant impact on code quality and build size. It also has amemfeature which provides symbols (memcpy, etc) for platforms withoutstdthat don’t have these same symbols provided bylibc.compiler_builtinsis also built with a large number of compilation units to force each function into a different unit, avoiding unintentionally bringing in a symbol that conflicts with one in the system’slibgcc.‘unwind’ links to the system’s version of libunwind. Enabling the
llvm-libunwindfeature,-Clink-self-containedor-Ctarget-feature=+crt-staticwill statically link to the pre-builtlibunwinddistributed in the standard library component for the target, if present.Sanitizers, when enabled, require a sanitizer runtime to be present. These are currently built by bootstrap and part of LLVM.
-
Design considerations
There are many design considerations discussed in the wg-cargo-std-aware repository:
wg-cargo-std-aware#5 explored how/if dependencies on the standard library should be declared. The issue claims that users should have to opt-in to build-std, support alternative standard library implementations, and that Cargo needs to be able to pass
--externto rustc for all dependencies.It is an open question how to handle multiple dependencies each declaring a dependency on the standard library. A preference towards unifying standard library dependencies was expressed (these would have no concept of a version, so just union all features).
There was no consensus on how to find a balance between explicitly depending on the standard library versus implicitly, or on whether the pre-built-ness of a dependency should be surfaced to the user.
wg-cargo-std-aware#6 argues that target-spec-json would be de-facto stable if it can be used by build-std on stable. While
--target=custom.jsoncan be used on stable today, it effectively requires build-std and so a nightly toolchain. As build-std enables custom targets to be used on stable, this would effectively be a greater commitment to the current stability of custom targets than currently exists and would warrant an explicit decision.wg-cargo-std-aware#8 highlighted that a more-portable standard library would be beneficial for build-std (i.e. a
stdthat could build on any target), but that making the standard library more portable isn’t necessarily in-scope for build-std.wg-cargo-std-aware#11 investigated how build-std could get the standard library sources. rustup can download
rust-src, but there was a preference expressed that rustup not be required. Cargo could have reasonable default probing locations that could be used by distros and would include where rustup putsrust-src.wg-cargo-std-aware#12 concluded that the
Cargo.lockof the standard library would need to be respected so that the project can guarantee that the standard library works with the project’s current testing.wg-cargo-std-aware#13 explored how to determine the default set of cfg values for the standard library. This is currently computed by bootstrap. This could be duplicated in Cargo in the short-term, made visible to build-std through some configuration, or require the user to explicitly declare them.
wg-cargo-std-aware#14 looks into additional rustc flags and environment variables passed by bootstrap to the compiler. A comparison of the compilation flags from bootstrap and build-std was posted in a comment. No solutions were suggested, other than that it may need a similar mechanism as wg-cargo-std-aware#13.
wg-cargo-std-aware#29 tries to determine how to support different panic strategies. Should Cargo use the profile to decide what to use? How does it know which panic strategy crate to use? It is argued that Cargo ought to work transparently - if the user sets the panic strategy differently then a rebuild is triggered.
wg-cargo-std-aware#30 identifies that some targets have special handling in bootstrap which will need to be duplicated in build-std. Targets could be allowlisted or denylisted to avoid having to address this initially.
wg-cargo-std-aware#38 argues that a forced lock of the standard library is desirable, to which there was no disagreement. This was more relevant when build-std did not use the on-disk
Cargo.lock.wg-cargo-std-aware#39 explores the interaction between build-std and public/private dependencies (rfcs#3516). Should the standard library always be public? There were no solutions presented, only that if defined in
Cargo.toml, the standard library will likely inherit the default from that.wg-cargo-std-aware#43 investigates the options for the UX of build-std.
-Zbuild-stdflag is not a good experience as it needs added to every invocation and has few extension points. Using build-std should be an unstable feature at first. It was argued that build-std should be transparent and happen automatically when Cargo determines it is necessary. There are concerns that this could trigger too often and that it should only happen automatically for ABI-modifying flags.wg-cargo-std-aware#46 observes that some targets link against special object flags (e.g.
crt1.oon musl) and that build-std will need to handle these without hardcoding target-specific logic. There were no conclusions, but-Clink-self-containedmight be able to help.wg-cargo-std-aware#47 discusses how to handle targets that typically ship with a different linker (e.g.
rust-lldorgcc).rust-lldis now shipped by default reducing the potential impact of this, though it is discovered via the sysroot, and so will need to be found via another mechanism if disabled.wg-cargo-std-aware#50 argues that the impact on build probes ought to be considered and was later closed as t-cargo do not want to support build probes.
wg-cargo-std-aware#51 plans for removal of
rustc-dep-of-std, identifying that if explicit dependencies on the standard library are adopted, that the need for this feature could be made redundant.wg-cargo-std-aware#68 notices that
profiler_builtinsneeds to be compiled aftercore(i.e.corecan’t be compiled with profiling). The error message has been improved for this but there was otherwise no commentary. This has changed since the issue was filed, asprofiler_builtinsis now a#![no_core]crate.wg-cargo-std-aware#85 considers that there has to be a deliberate testing strategy in place between the rust-lang/rust and rust-lang/cargo repositories to ensure there is no breakage.
rust-toolstatecould be used but is not very good. Alternatively, Cargo could become a JOSH subtree of rust-lang/rust.wg-cargo-std-aware#86 proposes that the initial set of targets supported by build-std be limited at first to further reduce scope and limit exposure to the trickier issues.
wg-cargo-std-aware#88 reports that
cargo doc -Zbuild-stddoesn’t generate links to the standard library. Cargo doesn’t think the standard library comes from crates.io, and bootstrap isn’t involved to pass-Zcrate-attr="doc(html_root_url=..)"like in the pre-built standard library.wg-cargo-std-aware#90 asks how
restricted_stdshould apply to custom targets.restricted_stdis triggered based on thetarget_osvalue, which means it will apply for some custom targets but not others. build-std needs to determine what guarantees are desirable/expected. Current implementation wants slightly-modified-from-default target specs to be accepted and completely new target specs to hitrestricted_std.wg-cargo-std-aware#92 suggests that some targets could be made “unstable” and as such only support build-std on nightly. This forces users of those targets to use nightly where they will receive more frequent fixes for their target. It would also permit more experimentation with build-std while enabling stabilisation for mainstream targets.
-
Implementation considerations These won’t be discussed in this summary, see the implementation summary or the relevant section of the literature review for more detail
-
Bugs in the compiler or standard library These aren’t especially relevant to this summary, see the relevant section of the literature review for more detail
-
Cargo feature requests narrowly applied to build-std These aren’t especially relevant to this summary, see the relevant section of the literature review for more detail
Since around 2020, activity in the wg-cargo-std-aware repository largely trailed off and there have not been any significant developments related to build-std since.
Implementation summary
An exhaustive review of implementation-related issues, pull requests and discussions can be found in the relevant section of the literature review.
There has been an unstable and experimental implementation of build-std in Cargo since August 2019 (wg-cargo-std-aware#10/cargo#7216).
cargo#7216 added the -Zbuild-std flag to Cargo. -Zbuild-std
re-builds the standard library crates which rustc then uses instead of the
pre-built standard library from the sysroot.
Originally, -Zbuild-std always build std by default. Since the addition of
the std field to target metadata in rust#122305, Cargo only builds std by
default if metadata.std is true.
test is also built if std is being built and tests are being run with the
default harness.
Optionally, users can provide the list of crates to be built, though this was intended as an escape hatch to work around bugs - the arguments to the flag are unstable since the names of crates comprising the standard library are not stable.
Cargo has a hardcoded list of what dependencies need to be added for a given
user-requested crate (i.e. std implies building core, alloc,
compiler_builtins, etc.). It is common for users to manually specify the
panic_abort crate.
Originally, -Zbuild-std required that --target be provided
(wg-cargo-std-aware#25) to force Cargo to use different sysroots for the host
and target , but this restriction was later resolved (cargo#14317).
A second flag, -Zbuild-std-features, was added in
cargo#8490 and allows overriding the default Cargo features of the standard
library. Like the arguments to -Zbuild-std, the values accepted by this flag
are inherently unstable as the library team has not committed to any of the
standard library’s Cargo features being stable. Features are enabled on the
sysroot crate and propagate down through the crate graph of the standard
library (e.g. compiler-builtins-mem is a feature in sysroot, std, alloc,
and core until compiler_builtins).
build-std gets the source of the standard library from the rust-src rustup
component. This does not happen automatically and the user must ensure the
component has been downloaded themselves. Only the standard library crates from
the rust-lang/rust repository are included in the rust-src dependency (i.e.
none of the crates.io dependencies).
When -Zbuild-std has been passed, Cargo creates a second workspace for the
standard library based on the Cargo.{toml,lock} from the rust-src component.
Originally this was an in-memory workspace, prior to the standard library having
a separate workspace from the compiler which could be used independently
(rust#128534/cargo#14358). This workspace is then resolved separately and
the resolve is combined with the user’s resolve to produce a dependency graph of
things to build with the user’s crates depending on the standard library’s
crates. Some additional work is done to deduplicate crates across the graph and
then this crate graph is used to drive work (usually rustc invocations) as
usual. This approach allows for build-time parallelism and sharing of crates
between the two separate resolves but does involve build-std-specific logic in
and around unit generation and is very unlike the rest of Cargo
(wg-cargo-std-aware#64).
Resolving the standard library separately from the user’s crate helps guarantee
that the exact dependency versions of the pre-built standard library are used,
which is a key constraint (wg-cargo-std-aware#12). Locking the standard
library could also help (wg-cargo-std-aware#38). A consequence of this is that
each of the Cargo subcommands (e.g. cargo metadata) need to have special
support for build-std implemented, but this might be desirable.
The standard library crates are considered non-local packages and so are not compiled with incremental compilation or dep-info fingerprint tracking and any warnings will be silenced.
build-std provides newly-built standard library dependencies to rustc using
--extern noprelude:$crate. noprelude was added in rust#67074 to support
build-std and ensure that loading from the sysroot and using --extern were
equivalent (wg-cargo-std-aware#40). Prior to the addition of noprelude,
build-std briefly created new sysroots and used those instead of --extern
(cargo#7421). rustc can still try to load a crate from the sysroot if the user
uses it which is currently a common source of confusing “duplicate lang item”
errors (as the user ends up with build-std core and sysroot core
conflicting).
Host dependencies like build scripts and proc_macro crates use the
existing pre-built standard library from the sysroot, so Cargo does not
pass --extern to those.
Modifications to the standard library are not supported. While build-std
has no mechanism to detect or prevent modifications to the rust-src content,
rebuilds aren’t triggered automatically on modifications. The user cannot
override dependencies in the standard library workspace with [patch] sections
of their Cargo.toml.
To simplify build-std in Cargo, build-std wants to be able to always build
std, which is accomplished through use of the
unsupported module in std’s platform abstraction layer,
and restricted_std. std checks for unsupported targets in its
build.rs and applies the restricted_std cfg which marks the
standard library as unstable for unsupported targets.
Users can enable the restricted_std feature in their crates. This mechanism
has been noted as confusing (wg-cargo-std-aware#87) and has the issue that the
user cannot opt into the feature on behalf of dependencies
(wg-cargo-std-aware#69).
The initial implementation does not include support for build-std in many of
Cargo’s subcommands including metadata, clean, vendor, pkgid and the
-p options for various commands. Support for cargo fetch was implemented in
cargo#10129.
no_std Usability
There are also issues related to the usability of no_std crates:
-
Discoverability of
no_stdcrates is difficult with a mix of categories (no-std) and keywords (nostd/no_std) that are not used consistently byno_stdcrates (crates.io#7306). -
no_stdcrates can accidentally and easily depend on crates that usestdwhich can result in build failures in some targets (cargo#8798).
Related work
There are a variety of ongoing efforts, ideas, RFCs or draft notes describing features that are related or would be beneficial for build-std:
- Opaque dependencies, epage, May 2025
- Introduces the concept of an opaque dependency that has its own
Cargo.lock,RUSTFLAGSandprofile - Opaque dependencies could enable a variety of build-time performance
improvements:
- Caching - differences in dependency versions can cause unique instances of every dependent crate
- Pre-built binaries - can leverage a pre-built artifact for a given opaque
dependency
- e.g. the standard library’s distributed
rlibs
- e.g. the standard library’s distributed
- MIR-only/cross-crate lazy compilation - Small dependencies could be built lazily and larger dependencies built once
- Optimising dependencies - dependencies could always be optimised when they are unlikely to be needed during debugging
- Introduces the concept of an opaque dependency that has its own
Motivation
Important
This section lists all of the motivations that have been associated with build-std in its various iterations, but not all of these use cases will be addressed by this project goal.
The motivations that will not be addressed are nevertheless mentioned here so that reviewers have a more complete context for what has and hasn’t been desired of build-std over time.
While the pre-built standard library has been sufficient for the majority of Rust users, there are a variety of use-cases which require the ability to rebuild the standard library.
-
Building the standard library without relying on unstable escape hatches
-
While tangential to the core of build-std as a feature, projects like Rust for Linux want to be able to build crates from the standard library using a stable toolchain without relying on escape hatches like
RUSTC_BOOTSTRAPthat the Rust project does not encourage use of-
It is relatively straightforward to support this, hence its inclusion
-
Cargo’s implementation of build-std should be able to re-use whichever mechanism is designed to address this
-
-
-
Building standard library crates that are not shipped for a target
- Targets which have limited
stdsupport may wish to use the subsets of the standard library which could work but are not shipped by the project (e.g.stdonx86_64-unknown-none)
- Targets which have limited
-
Using the standard library with tier three targets
-
There is no stable mechanism for using the standard library on a tier three target that does not ship a pre-built std
-
While it is common for these targets to not support the
stdcrate, they should be able to usecore -
These users are forced to use nightly and the unstable
-Zbuild-stdfeature or third-party tools like cargo-xbuild (formerly xargo)
-
-
Unblock stabilisation of ABI-modifying compiler flags
-
Any compiler flags which change the ABI cannot currently be stabilised as they would immediately mismatch with the pre-built standard library
- Without an ability to rebuild the standard library using these flags, it is impossible to use them effectively and safely if stabilised
-
ABI-modifying flags are designated as target modifiers (rfcs#3716/rust#136966) and require that the same value for the flag is passed to all compilation units
-
Flags which need to be set across the entire crate graph to uphold some property (i.e. enhanced security) are also target modifiers
-
For example: sanitizers, control flow integrity,
-Zfixed-x18, etc
-
-
-
Re-building the standard library with different codegen flags or profile (wg-cargo-std-aware#2)
-
Embedded users need to optimise aggressively for size, due to the limited space available on their target platforms, which can be achieved in Cargo by setting
opt-level = s/zandpanic = "abort"in their profile. However, these settings will not apply to the pre-built standard library -
Similarly, when deploying to known environments, use of
target-cpuortarget-featurecan improve the performance of code generation or allow the use of newer hardware features than the target’s baseline provides. As above, these configurations will not apply to the pre-built standard library -
While the pre-built standard library is built to support debugging without compromising size and performance by setting
debuginfo=1, this isn’t ideal, and building the standard library with the dev profile would provide a better experience when debugging
-
The following use cases are not currently planned as part of this project goal, but could be supported with follow-up RFCs (and any RFCs proposed as part of this goal will attempt to ensure they remain viable as future possibilities):
-
Using the standard library with custom targets
-
There is no stable mechanism for using the standard library for a custom target (using target-spec-json)
-
Like tier three targets, these targets often only support
coreand are forced to use nightly today
-
-
Enabling Cargo features for the standard library (wg-cargo-std-aware#4)
-
There are opportunities to expose Cargo features from the standard library that would be useful for certain subsets of the Rust users.
- For example, embedded users may want to enable
optimize_for_sizeor disablebacktraceto reduce binary size
- For example, embedded users may want to enable
-
-
Progress towards using miri on a stable toolchain
- One of the limitations of miri is that it requires building the standard library with specific compiler flags that would not be appropriate for the pre-built standard library, this is part of miri’s dependency on nightly to build its own sysroot using rustc-build-sysroot
Some use cases are unlikely to be supported by the project unless a new and compelling use-case is presented, and so this project goal may make decisions which make these motivations harder to solve in future:
-
Modifying the source code of the standard library (wg-cargo-std-aware#7)
-
Some platforms require a heavily modified standard library that would not be suitable for upstreaming, such as Apache’s SGX SDK which replaces some standard library and ecosystem crates with forks or custom crates for a custom
x86_64-unknown-linux-sgxtarget -
Similarly, some tier three targets may wish to patch standard library dependencies to add or improve support for the target
-
If a stable mechanism were provided to make such changes to the standard library, then this would constrain future standard library development. These changes are better attempted by maintaining a fork of the standard library.
-
-
Retire the concept of the sysroot
-
Earlier proposals for build-std were motivated in-part by the desire to see the concept of the sysroot retired.
- This is challenging while maintaining backwards-compatibility, especially for users who do not use Cargo and assume rustc can find the standard library in the sysroot. Removing the sysroot has no advantages to the end-user of Rust in itself.
-
Rationale and alternatives
These rationales and alternatives apply to the build-std proposal as a whole:
Why have rationale sections?
A separate rationale section makes for easier reading by letting the proposal sections of the RFCs flow better without interruptions or tangents.
Why not do nothing?
Support for rebuilding the standard library is a long-standing feature request
from subsets of the Rust community and blocks the work of some project teams
(e.g. sanitisers and branch protection in the compiler team, amongst others).
Inaction forces these users to remain on nightly and depend on the unstable
-Zbuild-std flag indefinitely. RFCs and discussion dating back to the first
stable release of the language demonstrate the longevity of build-std as a
need.
Shouldn’t build-std be part of rustup?
build-std is effectively creating a new sysroot with a customised standard
library. rustup as Rust’s toolchain manager has existing machinery to create and
maintain sysroots, and if it could invoke Cargo to build the standard library
then it could create a new toolchain from a build from a rust-src component.
rustup would be invoking tools from the next layer of abstraction (Cargo) in the
same way that Cargo invokes tools from the layer of abstraction after it
(rustc).
A brief prototype of this idea was created and a short design document was drafted before concluding that it would not be possible. With Cargo’s artifact dependencies it may be desirable to build with a different standard library and if rustup was creating different toolchains per-customised standard library then Cargo would need to have knowledge of these to switch between them, which isn’t possible (and something of a layering violation). It is also unclear how Cargo would find and use the uncustomized host sysroot for build scripts and procedural macros. In addition rustup’s knowledge of sysroots and toolchains is limited to the archives it unpacks - it becoming a part of the build system is not trivial, especially considering it uses a different versioning system to Cargo, Rust and the standard library.