Skip to content

Conversation

davidtwco
Copy link
Owner

@davidtwco davidtwco commented Sep 27, 2025

Supercedes #2. This should be the last round of feedback before we post the RFC publicly.

  • I've done away with the "stages" framing, including the build-std="always" and explicit dependencies as two parts of the core RFC, and moving build-std="compatible" and build-std="match-profile" into separate files as they'll eventually be follow-up RFCs:
    • 0000a-build-std-compatible.md and 0000b-build-std-match-profile.md aren't going to be included in the initial RFC upstream, but we'll link to them in read-only HackMDs in the PR description for context as to where we're going next with build-std.
  • Feel free to discuss on #project-goals/2025h1/build-std. Ask for an invite if you don't have one.

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

This RFC proposes a handful of changes to Cargo, the compiler and standard library with the goal of defining a minimal build-std that has the potential of being stabilised:

  • Explicitly declaring support for the standard library in target specs
  • Explicit and implicit dependencies on the standard library in Cargo.toml
  • Re-building the standard library when the profile or target modifiers change

Rendered

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

This RFC proposes a handful of changes to Cargo, the compiler and
standard library with the goal of defining a minimal build-std that has
the potential of being stabilised:

- Explicitly declaring support for the standard library in target specs
- Explicit and implicit dependencies on the standard library in
  `Cargo.toml`
- Re-building the standard library when the profile or target modifiers
  change

Co-authored-by: Adam Gemmell <[email protected]>
This was referenced Sep 27, 2025
Comment on lines 28 to 29
- Cargo may also want a mechanism to build the standard library for
build-std on a stable toolchain without relying on `RUSTC_BOOTSTRAP`

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO we should drop this motivation -- it doesn't seem relevant to the goal:

  • How is use of RUSTC_BOOTSTRAP to building without Cargo?
  • Clearly this is with Cargo, so also not really related to item 1?

And in general BOOTSTRAP or some other mechanism feels like an implementation detail of build-std support, not a motivation.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've reworded this motivation so it is hopefully clearer what we're trying to solve - see d3c2b1a


- While tangential to the core of build-std as a feature, projects like Rust
for Linux want to be able to build an unmodified `core` from `rust-src` in
the sysroot on a stable toolchain without Cargo

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems like this isn't really clarifying why -- maybe that's done elsewhere, but it would be useful to have that.

Are we sure this isn't actually "Unblock stabilisation of ABI-modifying compiler flags"? What is the RfL use case for building std without Cargo and without that later motivation? It seems to me that if we assume they're building exactly as we would upstream, then they don't benefit from not using the pre-built artifacts, right?

(In practice I'd guess that without Cargo in the mix, it's easier to customize the build options to match across the board etc., so maybe without Cargo ABI-modifying flags are stabilizable earlier...?)

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

> rustc will be extended with the ability to determine whether an existing
> `rlib` artefact is compatible with the flags passed to rustc
> ([?][rationale-rustc-support]).
>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd like to soften the text here so it doesn't seem like this is specifying a specific solution. (per comment at #2 (comment)).

Maybe something like:

Suggested change
>
>
> The exact mechanism is not specified in this RFC. It may be a flag such as `--compatibility-with=core` or an `--emit` or `--print` flag.
>

Or, we could take some time now and make a decision on that design. I don't think anything that involves passing a path to an rlib is going to work well with cargo.

Additionally, there will be some interaction here with actually determining if the pre-built standard library is available, since again that's not something that I would feel comfortable with cargo being able to determine on its own. I'm wondering if all these queries can be combined? Preferably in a --print flag so that cargo can include it with the other queries it sends to rustc.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 66a0f58. These queries probably could be combined together, a detail that we can definitely iterate on during implementation when we have a complete picture of all the information rustc will need to provide.

Comment on lines 187 to 189
"core and alloc", and `core` for "core"). This does not prevent users from
building more crates than the default, it is only intended to be a sensible
default for the target that is probably what the user expects.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a little concerned about the user experience here when a user explicitly sets the crates with build-std-crates, or in the explicit-dependencies step. There are a few different failure modes:

  • Build wants std for a no-std target that doesn't support std (compile errors):
    • Today (no build-std, target installed): You get error[E0463]: can't find crate for std with a helpful note that the target may not support std, and to consider using #![no_std].
    • Today with -Zbuild-std=std (intended): You're supposed to get an error with this message that explains the target doesn't support std.
    • Today with -Zbuild-std=std (actual): It was broken a while back, and now you get a large number of errors building alloc, with various cfg and resolver errors.
    • With this proposal: I think you'll get the same behavior, with a large number of confusing errors.
  • Build wants std for a no-std target that does compile successfully (due to the "unsupported" module):
    • Today (no build-std, target installed): You get error[E0463]: can't find crate for std with a helpful note that the target may not support std, and to consider using #![no_std].
    • Today with -Zbuild-std=std (intended): You're supposed to get an error with this message that explains the target doesn't support std.
    • With this proposal: The target just builds without an error or warning that this configuration is not intended. This will implicitly stabilize the target's support beyond the tier that is intended.

Would it be possible to avoid these two failure modules with better behavior?

For both of these, I would expect a clear error message that the target doesn't support the given crate (alloc or std), without spitting out pages of other errors.

For the second one, we may need a nightly-only escape hatch. We've had a few targets that have incrementally migrated to supporting std. During that transition period, std is in various states of functionality, but the target itself has not yet stabilized or committed to supporting std. I'm not sure on whether or not that is actually necessary.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm definitely open to making a change here - the very first early drafts had a much stricter check, and it sounds like we want something closer to that.

It'll require a bit of thought as to how to frame this because it involves a bunch of things - whether a crate should build for a given target depends on if we want to consider that crate "stable" for that target, if that depends on the target tier, or just completeness, as well as whether we do want some targets to default to only core but permit building alloc/std, etc.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We discussed this in the sync call, I think I've wrote what we discussed in
9719228

Comment on lines +105 to +110
> Cargo will resolves the dependencies of opaque dependencies, such as the
> standard library, separately in their own workspaces. The root of such a
> resolve will be the crates specified in `build-std-crates` or, if
> [*Standard library dependencies*][deps] is implemented, the unified set of
> packages that any crate in the dependency has a direct dependency on. A
> dependency on the relevant roots are added to all crates in the main resolve.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unsure what section this should be called out but we should specify that patch and replaces can't be used with these dependencies?

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added some wording for this in cd627f5

Comment on lines 494 to 516
As opaque dependencies, any Cargo command which accepts a package spec with `-p`
will only additionally recognise `core`, `alloc` and `std` and none of their
dependencies. Many of Cargo's subcommands will need modification to support
build-std:
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should -p support be left to "Explicit dependencies"? As is, this phase doesn't require any special integration of build-std into how we treat dependencies until we get to this part. Not saying it can't be done at this phase but feels like it should be left to the implementation while required for "Explicit dependencies".

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense to me, changed in 47e90e8

Comment on lines 1070 to 1218
## What should the `build-std` configuration in `.cargo/config` be named?
[unresolved-config-name]: #what-should-the-build-std-configuration-in-cargoconfig-be-named

What should this configuration option be named? `build-std`?
`rebuild-standard-library`?

[*Proposal*][proposal]

## What should the "always" and "never" values of `build-std` be named?
[unresolved-config-values]: #what-should-the-always-and-never-values-of-build-std-be-named

What is the most intuitive name for the values of the `build-std` setting?
`always`? `manual`? `unconditional`?

[*Proposal*][proposal]
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unsure if this had come up before but combining build-std with always, auto, never naming has the problem in how it reads: build-std = "always" makes it sound like we'll always rebuild the standard library, rather than that we'll not use the pre-built versions and cache the build of it as needed

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree and I'm happy to change the name, maybe missing - as in, "build the standard library when it is missing"?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That sounds like auto.

More so I commented on this not to figure it out now but that we should likely call this out in this section to help with the conversation.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a mention of why "always" isn't a great name to this unresolved question in c25a29a

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Saying this is a problem with always isn't quite correct; it is a problem with build-std combined with always. I intentionally highlighted both sections and called this out as a combination of them.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed phrasing for this in b15595e

Comment on lines 41 to 43
([?][rationale-unstable-builtin-crates]). If a builtin dependency on a unstable
crate name exists but is not used due to cfgs, then the crate could still be
compiled with a stable toolchain.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All dependency tables are always resolved. Its later phases that we trim out cfg related dependencies. This is putting a large design constraint on where it is that we deal with validating the set of crate names.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed this in 503a6d9 to flip the behaviour here to avoid that constraint on Cargo

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it could be worth calling this out as a trade off between how things are validated and forcing the use of nightly / MSRV bumps

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a future possibility for this in d890a6a

Comment on lines 52 to 55
Crates without an explicit dependency on the standard library now have a
implicit dependency ([?][rationale-no-migration]) on `std`, `alloc` and `core`.
Any explicit `builtin` dependency present in the manifest will disable the
implicit dependencies.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This sounds like it means "without an explicit dependency from any dependency table" but later you have an example of a target.*.dependencies where the inferring is per-table.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I expect my mental model of how Cargo works internally here is incorrect. My intention was that any explicit builtin dependency in any dependency table would remove the implicit dependencies, rather than each table having its own implicit dependencies. Clarified that in f436c76 but can change if that's just now how Cargo works.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure how a mental model can be wrong here as we're defining it.

And I feel like that commit didn't clarify my confusion.

Take the example I was referencing

[package]
name = "hello_world"
version = "0.1.0"
edition = "2024"

[dependencies]
# implicit deps on `core`, `alloc` and `std` unless target='x86_64-pc-windows-gnu'

[target.x86_64-pc-windows-gnu.dependencies]
alloc.builtin = true

According to the section its in, this works. According to the other text and your edits of it, this will fail because there is no core dependency. We can either model that as "there is no core dependency in dependencies so it errors" or "when we build for non x86_64-pc-windows-gnu targets, there is no core dependency and we error". The latter one is likely easier to work with overall but it delays errors.

Copy link
Owner Author

@davidtwco davidtwco Oct 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure how a mental model can be wrong here as we're defining it.

I just meant that I'm not especially familiar with how Cargo normally deals with multiple dependency tables - as in, does validation happen on [target.x86_64-pc-windows-gnu.dependencies] in isolation or once all the dependencies from all the relevant tables are combined. Given however Cargo does this that today, is it more natural to think of each dependency table as having their own set of implicit and explicit dependencies or as that being global across all the dependency tables?

Take the example I was referencing

[package]
name = "hello_world"
version = "0.1.0"
edition = "2024"

[dependencies]
# implicit deps on `core`, `alloc` and `std` unless target='x86_64-pc-windows-gnu'

[target.x86_64-pc-windows-gnu.dependencies]
alloc.builtin = true

According to the section its in, this works. According to the other text and your edits of it, this will fail because there is no core dependency. We can either model that as "there is no core dependency in dependencies so it errors" or "when we build for non x86_64-pc-windows-gnu targets, there is no core dependency and we error". The latter one is likely easier to work with overall but it delays errors.

My intention with this example was:

  • On not-x86_64-pc-windows-gnu, there are no explicit dependencies, so you have implicit dependencies. As you have implicit dependencies, you have a dependency on core, so there's no error.
  • On x86_64-pc-windows-gnu, there is a explicit alloc dependency, so you don't have implicit dependencies. As you have a explicit dependency on alloc, you have a dependency on core, so there's no error.

I think the "no core, need to error" case only really comes up with optional dependencies, not dependencies in other tables. I'll have a re-read of this section of the RFC to see if what I've written is ambiguous.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How cargo views it depends on where you are at in the build process. When we parse, they are separate tables. For resolving of dependencies, we merge all of the tables together. When we resolve features, ... I'm not sure how we do it all. Then we eventually get to execution.

The later in the process we consider implicit vs explicit packages, the more hacks we have to put in to handle all of this. However, if we put it in too early, it either requires an MSRV bump just for existing (not allowed) or we have to hack up the Index generation and Cargo.toml rewriting.

Comment on lines 311 to 316
Implicit and explicit dependencies on the standard library are supported for
`dev-dependencies` in the same way as regular `dependencies`. `dev-dependencies`
continues to be a superset of `dependencies`, including with `builtin`
dependencies. It is possible for `dev-dependencies` to have additional `builtin`
dependencies that the `dependencies` section does not have (e.g. requiring `std`
when the regular dependencies only require `core`).
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So does inferring happen within dev-dependencies or is it special cased to not have any inferring logic and "just do whatever depdendencies does"?

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed this in #4 (comment)

Comment on lines 506 to 510
[`cargo tree`][cargo-tree] will show `std`, `alloc` and `core` at appropriate
places in the tree of dependencies. `alloc` will always be shown as a dependency
of `std`, and `core` a dependency of `alloc`. As opaque dependencies, none of
the other dependencies of `std`, `alloc` or `core` will be shown. Neither `std`,
`alloc` or `core` will have a version number.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to specify that the std/alloc/core relationship will be expressed?

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably not, removed in 0897312

Comment on lines +779 to +787
## Why default to public for standard library dependencies?
[rationale-default-public]: #why-default-to-public-for-standard-library-dependencies

There are crates building on stable which re-export from the standard library.
If the standard library dependencies were not public then these crates would
start to trigger the `exported_private_dependencies` lint when upgrading to a
version of Cargo with a standard library dependency.

↩ [*Public and private dependencies*][public-and-private-dependencies]
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a big fan of this special casing and this is inconsistent within this RFC, like with "Why not use noprelude for explicit builtin dependencies?"

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added a unresolved question about this special-casing and noting the inconsistency in 74bc861 - I think making the dependencies public by default is probably the right choice to avoid what I assume are quite common std re-exports triggering lints but it's important to acknowledge that is a trade-off.

Comment on lines 686 to 693
## Why not use `noprelude` for explicit `builtin` dependencies?
[rationale-explicit-noprelude]: #why-not-use-noprelude-for-explicit-builtin-dependencies

Explicit builtin dependencies without the `noprelude` modifier behave more
consistently with other dependencies specified in the Cargo manifest.

This is a trade-off, as the behaviour will be subtly different than with
implicit builtin dependencies (where `extern crate` is required).
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To be clear, this is a consistency of UX at the cost of special casing in Cargo to handle implicit and explicit dependencies separately.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've elaborated on this being the trade-off in be70a36

Comment on lines +211 to +225
5. [build-std=always](./0000-build-std/4-build-std-always.md)

- Proposes adding a `build-std = "always|never"` option to the Cargo
configuration which will unconditionally re-build the standard library
crates listed in a new `build-std-crates` option

- [Proposal](./0000-build-std/4-build-std-always.md#proposal)

- [Rationale and alternatives](./0000-build-std/4-build-std-always.md#rationale-and-alternatives)

- [Unresolved questions](./0000-build-std/4-build-std-always.md#unresolved-questions)

- [Future possibilities](./0000-build-std/4-build-std-always.md#future-possibilities)

6. [Explicit dependencies](./0000-build-std/5-standard-library-dependencies.md)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This RFC includes two separate but related proposals

  • build-std=always
  • Explicit dependencies

I am not seeing any rationale for why these are being conjoined into one RFC. Large RFCs are like large PRs. An area of focus draws everyone's attention, neglecting other topics. Once that area is "resolved enough", people move onto the next one. And so on until everyone is burnt out and we accept or reject it without reviewing important details. This will also make the conversation harder because of how much more there will be. This also pulls teams onto FCPs that aren't relevant.

Examples of splitting related RFCs

I would need to see a strong case made for why these should be kept in the same RFC as having them conjoined will make things harder for all parties involved and, at least for myself, will have less trust in the process.

I could even see splitting build-std=always up further to be team focused, layered where at the bottom is infra and lib, then compiler, then cargo. I've not thought through the implications of doing such a split and have no opinion at the moment on what should be done.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've kept both together as they address the same motivations, and share the history, prior art, etc sections. We've wrote all of these together to make sure that the proposals synergise and that everything included is presented in the context of the larger feature/vision it enables.

I'm open to splitting it into a handful of RFCs if we can work out how to partition the content up in a way that makes sense. My plan so far has been that by doing lots of iterations with smaller groups and representatives from each team that we should have addressed a majority of the major feedback we're likely to get and the RFC shouldn't be too surprising to any of the affected teams.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The examples I gave are in a similar boat in terms of synergy and being written together. As for any shared content, I'm unsure how best to handle that.

My plan so far has been that by doing lots of iterations with smaller groups and representatives from each team that we should have addressed a majority of the major feedback we're likely to get and the RFC shouldn't be too surprising to any of the affected teams.

My response to some of what was discussed was effectively "let's document this and work it out during the RFC with all parties present". Getting feedback in the small is important for faster iteration but it does not replace having a team-wide, project-wide, and community-wide conversation.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As for any shared content, I'm unsure how best to handle that.

I think the easiest way is probably to pick a part of the proposal to make first - probably explicit dependencies? - and then include all the context/history/etc with that, and then just reference that RFC's context/history/prior art sections from everything that follows.

My response to some of what was discussed was effectively "let's document this and work it out during the RFC with all parties present". Getting feedback in the small is important for faster iteration but it does not replace having a team-wide, project-wide, and community-wide conversation.

👍

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the easiest way is probably to pick a part of the proposal to make first - probably explicit dependencies? - and then include all the context/history/etc with that, and then just reference that RFC's context/history/prior art sections from everything that follows.

I'd post both at once. I feel like "explicit dependencies" could even stand on its own separate from build-std.

Alternatively, we create another RFC that is laying down the build-std project constraints. Not quite a eRFC but not quite a behavior-specifying RFC, making it so there are (at least) three proposals in flight.

Copy link
Owner Author

@davidtwco davidtwco Oct 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alternatively, we create another RFC that is laying down the build-std project constraints. Not quite a eRFC but not quite a behavior-specifying RFC, making it so there are (at least) three proposals in flight.

I really like that as an approach. Most of the sections that would make up that RFC are basically already ready and could be posted much sooner than the rest of the RFC, then we can just post everything else in whatever chunks make most sense.

@davidtwco davidtwco force-pushed the build-std-release-candidate branch from 7394a1b to 9719228 Compare October 3, 2025 13:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants