Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

  • 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:

  1. Rebuilding the standard library to match the user’s profile
  2. Rebuilding the standard library with ABI-modifying flags
  3. 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.

  1. build-std context (this RFC)
  2. build-std="always" (rfcs#3874)
  3. Explicit standard library dependencies (rfcs#3875)
  4. build-std="compatible" (RFC not opened yet)
  5. 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_macro or their dependencies.
  • “std” is used to refer only to the std crate, 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_builtinsc feature
  • 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:

  • libunwind will either link to the LLVM libunwind or the system’s libunwind/libgcc_s. LLVM’s libunwind is shipped as part of the rustup component for the standard library and will be linked against when -Clink-self-contained is used
    • This only applies to Linux and Fuchsia targets
  • compiler_builtins has an optional c feature that will use optimised routines from compiler-rt when enabled. It is enabled for the pre-built standard library
  • compiler_builtins has an optional mem feature that provides symbols for common memory routines (e.g. memcpy)
    • It is enabled automatically on some no_std platforms as when std is built libc provides 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.
  • 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:

  1. 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:

    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.

  2. 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 -p flag.

    cargo fetch had fairly intuitive interactions with build-std - that cargo fetch should also fetch any dependencies of the standard library - which was implemented in cargo#10129.

    The --build-plan flag 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 vendor did 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 the rust-src component contained already-vendored dependencies, then cargo vendor would 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.

  3. 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.

    libbacktrace previously required a C compiler to build backtrace-sys, but now uses gimli internally.

    compiler_builtins has a c feature 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/c can have a significant impact on code quality and build size. It also has a mem feature which provides symbols (memcpy, etc) for platforms without std that don’t have these same symbols provided by libc. compiler_builtins is 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’s libgcc.

    ‘unwind’ links to the system’s version of libunwind. Enabling the llvm-libunwind feature, -Clink-self-contained or -Ctarget-feature=+crt-static will statically link to the pre-built libunwind distributed 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.

  4. 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 --extern to 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.json can 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 std that 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 puts rust-src.

    wg-cargo-std-aware#12 concluded that the Cargo.lock of 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-std flag 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.o on musl) and that build-std will need to handle these without hardcoding target-specific logic. There were no conclusions, but -Clink-self-contained might be able to help.

    wg-cargo-std-aware#47 discusses how to handle targets that typically ship with a different linker (e.g. rust-lld or gcc). rust-lld is 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_builtins needs to be compiled after core (i.e. core can’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, as profiler_builtins is 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-toolstate could 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-std doesn’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_std should apply to custom targets. restricted_std is triggered based on the target_os value, 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 hit restricted_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.

  5. 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

  6. 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

  7. 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_std crates is difficult with a mix of categories (no-std) and keywords (nostd/no_std) that are not used consistently by no_std crates (crates.io#7306).

  • no_std crates can accidentally and easily depend on crates that use std which can result in build failures in some targets (cargo#8798).

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, RUSTFLAGS and profile
    • 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
      • 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

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.

  1. 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_BOOTSTRAP that 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

  2. Building standard library crates that are not shipped for a target

    • Targets which have limited std support may wish to use the subsets of the standard library which could work but are not shipped by the project (e.g. std on x86_64-unknown-none)
  3. 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 std crate, they should be able to use core

    • These users are forced to use nightly and the unstable -Zbuild-std feature or third-party tools like cargo-xbuild (formerly xargo)

  4. 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

  5. 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/z and panic = "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-cpu or target-feature can 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):

  1. 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 core and are forced to use nightly today

  2. 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_size or disable backtrace to reduce binary size
  3. 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:

  1. 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-sgx target

    • 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.

  2. 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.

Terminology

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.