- Feature Name:
build-std-explicit-dependencies - Start Date: 2025-06-05
- RFC PR: rust-lang/rfcs#3875
- Tracking Issue: rust-lang/cargo#16960
Summary
Allow users to add explicit dependencies on standard library crates in the
Cargo.toml. This enables Cargo to determine which standard library crates are
required by the crate graph without build-std.crates being set and for
different crates to require different standard library crates.
This RFC is is part of the build-std project goal and a series of build-std RFCs:
- build-std context (rfcs#3873)
build-std="always"(rfcs#3874)- Explicit standard library dependencies (this RFC)
build-std="compatible"(RFC not opened yet)build-std="match-profile"(RFC not opened yet)
Motivation
This RFC builds on a large collection of prior art collated in the
build-std-context RFC. It does not directly address the
main rfcs#3873-motivation it identifies but supports later proposals.
The main motivation for this proposal is to support future extensions to build-std which allow public/private standard library dependencies or enabling features of the standard library. Allowing the standard library to behave similarly to other dependencies also reduces user friction and can improve build times.
Proposal
Users can now optionally declare explicit dependencies on the standard library
in their Cargo.toml files (?):
[package]
name = "hello_world"
version = "0.1.0"
edition = "2024"
[dependencies]
std = { builtin = true }
builtin is a new source of dependency, like registry dependencies (with the
version key and optionally the registry key), path dependencies or git
dependencies. builtin can only be set to true and cannot be combined with
any other dependency source for a given dependency
(?).
builtin can only be used with crates named core, alloc or std
(?) on stable. This set could be expanded
with new crates in future.
Use with any other crate name is gated on a perma-unstable cargo-feature
(?). If a builtin dependency on a unstable
crate name exists but is not used due to cfgs, then Cargo will still require the
Cargo feature.
Note
Explicit dependencies are passed to rustc without the
nopreludemodifier (?) (nopreluderefers to the compiler’s notion of the “extern prelude”, not the prelude in the the user-facing sense ofstd::prelude::*).When adding an explicit dependency, users may need to adjust their code (removing extraneous
extern cratestatements or root-relative paths, like::std- this will likely only be the case on the 2015 edition).
Crates without an explicit dependency on the standard library now have a
implicit dependency (?) on that target’s default set
of standard library crates (see
build-std-always). Any explicit
standard library dependency present in any dependency table applicable to the
current target will disable the implicit dependencies (e.g. an explicit
builtin or path dependency from std will disable the implicit
dependencies).
Note
Implicit dependencies are passed to rustc with the
nopreludemodifier to ensure backwards compatibility as inbuild-std=always.
When a std dependency is present an additional implicit dependency on the
test crate is added for crates that are being tested with the default test
harness. The test crate’s name, but not its interface, will be stabilised so
Cargo can refer to it.
crates.io will accept crates published which have builtin dependencies.
Standard library dependencies can be marked as optional and be enabled
conditionally by a feature in the crate:
[package]
name = "hello_world"
version = "0.1.0"
edition = "2024"
[dependencies]
std = { builtin = true, optional = true }
core = { builtin = true }
[features]
default = ["std"]
std = ["dep:std"]
If there is an optional dependency on the standard library then Cargo will
validate that there is at least one non-optional dependency on the standard
library (e.g. an optional std and non-optional core or alloc, or an
optional alloc and non-optional core). core cannot be optional. For
example, the following example will error as it could result in a build without
core (if the std feature were disabled):
[package]
name = "hello_world"
version = "0.1.0"
edition = "2024"
[dependencies]
std = { builtin = true, optional = true }
# error: must have a non-optional dependency on core
[features]
default = ["std"]
std = ["dep:std"]
However, in this example, a build for the x86-64-pc-windows-gnu target would
have an explicit dependency on alloc (and indirectly on core), while a build
for any other target would have implicit dependencies on std, alloc and
core:
[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
Dependencies with builtin = true cannot be renamed with the package key
(?). It is not possible to perform source replacement
on the builtin source using the [source] Cargo config table
(?), and nor is it possible to override
builtin dependencies with the [replace] sections or paths overrides
(?), though patching is permitted
under a perma-unstable feature flag (?).
Dependencies with builtin = true can be specified as platform-specific
dependencies:
[target.'cfg(unix)'.dependencies]
std = { builtin = true}
Implicit and explicit standard library dependencies are added to Cargo.lock
files (?).
Note
A new version of the
Cargo.lockfile will be introduced to add support for packages with abuiltinsource:[[package]] name = "std" version = "0.0.0" source = "builtin"The package version of
std,allocandcorewill be fixed at0.0.0. The optional lockfile fieldsdependenciesandchecksumwill not be present forbuiltindependencies.
A perma-unstable Cargo feature for disabling all standard library dependencies will
be added to allow the core crate to be defined.
When a crate has no builtin dependency on std (or an optional builtin
dependency on std), then Cargo will pass -Zcrate-attr=no_std to rustc (or
some equivalent; ?).
Builtin dependencies defined in workspace.dependencies are inherited by
members of the workspace in the same way as any other dependency and then the
same behaviour and constraints apply.
See the following sections for rationale/alternatives:
- Why explicitly declare dependencies on the standard library in
Cargo.toml? - Why disallow builtin dependencies to be combined with other sources?
- Why disallow builtin dependencies on other crates?
- Why unstably allow all names for
builtincrates? - Why not use
nopreludefor explicitbuiltindependencies? - Why not require builtin dependencies instead of supporting implicit ones?
- Why disallow renaming standard library dependencies?
- Why disallow source replacement on
builtinpackages? - Why add standard library dependencies to Cargo.lock?
- Why pass
-Zcrate-attr=no_stdto rustc?
See the following sections for relevant unresolved questions:
- What syntax is used to identify dependencies on the standard library in
Cargo.toml? - What is the format for builtin dependencies in
Cargo.lock?
See the following sections for future possibilities:
- Allow unstable crate names to be referenced behind cfgs without requiring nightly
- Allow
builtinsource replacement - Remove
rustc_dep_of_std
Non-builtin standard library dependencies
Cargo already supports path and git dependencies for crates named core,
alloc and std which continue to be supported and work:
[package]
name = "hello_world"
version = "0.1.0"
edition = "2024"
[dependencies]
std = { path = "../my_std" } # already supported by Cargo
A core/alloc/std dependency with a path/git source can be combined
with builtin dependencies:
[package]
name = "hello_world"
version = "0.1.0"
edition = "2024"
[dependencies]
std = { path = "../my_std" }
core = { builtin = true }
Crates with these dependency sources will remain unable to be published to crates.io.
Building on both of the above, a core/alloc/std dependency can have
path/git source at the same time as a builtin source. This allows the
crate to remain publishable while the path/git source is only used locally:
[package]
name = "hello_world"
version = "0.1.0"
edition = "2024"
[dependencies]
std = { builtin = true, path = "../my_std" } # published as `builtin` dep, `path` used locally
This mirrors the existing behaviour for path/git sources when combined with
registry sources.
Patches
Under a perma-unstable feature it is permitted to patch standard library
dependencies with path and git sources (or any other source)
(?):
[package]
name = "hello_world"
version = "0.1.0"
edition = "2024"
[dependencies]
std = { builtin = true }
[patch.builtin] # permitted on nightly
std = { path = "../libstd" }
As with dependencies, crates with path/git patches for core, alloc or
std are not accepted by crates.io.
See the following sections for rationale/alternatives:
See the following sections for relevant unresolved questions:
Features
On a stable toolchain, it is not permitted to enable or disable features of explicit standard library dependencies (?), as in the below example:
[package]
name = "hello_world"
version = "0.1.0"
edition = "2024"
[dependencies]
std = { builtin = true, features = [ "foo" ] } # not permitted
# ..or..
std = { builtin = true, default-features = false } # not permitted
See the following sections for rationale/alternatives:
See the following sections for future possibilities:
Public and private dependencies
Implicit dependencies on the standard library default to being public dependencies (?).
[package]
name = "hello_world"
version = "0.1.0"
edition = "2024"
[dependencies]
..is equivalent to the following explicit dependency on std:
[package]
name = "hello_world"
version = "0.1.0"
edition = "2024"
[dependencies]
std = { builtin = true, public = true }
Explicit dependencies on the standard library default to being private
dependencies (?). cargo add will add
public = true by default for builtin dependencies (see Cargo
subcommands).
See the following sections for relevant unresolved questions:
- Should implicit standard library dependencies default to public?
- Should explicit standard library dependencies default to private?
See the following sections for rationale/alternatives:
- Why default to public for implicit standard library dependencies?
- Why default to private for explicit standard library dependencies?
dev-dependencies and build-dependencies
Explicit dependencies on the standard library can be specified in
dev-dependencies in the same way as regular dependencies. Any explicit
builtin dependency present in dev-dependencies table will disable the
implicit 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).
Build scripts and proc macros continue to use the pre-built standard library as
in build-std=always, and so explicit dependencies on the
standard library are not supported in build-dependencies.
See the following sections for relevant unresolved questions:
Registries
Standard library dependencies will be present in the registry index such that standard library dependencies can be dependencies of other crates, but not as top-level crates in the registry (?).
A builtin_deps key is added to the index’s JSON schema
(?). builtin_deps is similar to the existing
deps key and contains a list of JSON objects, each representing a dependency
that is “builtin” to the Rust toolchain and cannot otherwise be found in the
registry. The “publish” endpoint of the Registry
Web API will similarly be updated to support builtin_deps.
Note
It is expected that the keys of these objects will be:
name
- String containing name of the
builtinpackage. Can shadow the names of other packages in the registry (except those packages in thedepskey of the current package) (?)
features:
- An array of strings containing enabled features in order to support changing the standard library features on nightly. Optional, empty by default.
optional,default_features,target,kind:
- These keys have the same definition as in the
depskeyThe keys
req,registryandpackagefromdepsare not required per the limitations on builtin dependencies.The
builtin_depskey is optional and if not present its default value will be the implicit builtin dependencies:"builtin_deps" : [ { "name": "std", "features": [], "optional": false, "default_features": true, "target": null, "kind": "normal", }, { "name": "alloc", ... # as above }, { "name": "core", ... # as above } ]When producing a registry index entry for a package Cargo will strip any
builtindependencies that match the implicit state. This allows the implicit state to change in the future if needed and prevents publishing a package with a new Cargo from raising your MSRV. Similarly, the publishedCargo.tomlwill not explicitly declare any dependencies that match the implicit state.builtindependencies that do match the implicit state could be made explicit in a future edition.
See the following sections for rationale/alternatives:
- Why add standard library crates to Cargo’s index?
- Why add a new key to Cargo’s registry index JSON schema?
- Why can
builtin_depsshadow other packages in the registry?
See the following sections for relevant unresolved questions:
Cargo subcommands
Any Cargo command which accepts a package spec with -p will now additionally
recognise core, alloc and std and none of their dependencies. Many of
Cargo’s subcommands will need modification to support build-std:
cargo add’s heuristics will include adding std, alloc or
core as builtin dependencies if these crate names are provided. cargo add
will additionally have a --builtin flag to allow for adding crates with a
builtin source explicitly:
[package]
name = "hello_world"
version = "0.1.0"
edition = "2024"
[dependencies]
std = { builtin = true } # <-- this would be added
If attempting to add a crate name outside of core, alloc or std this will
fail unless the required cargo-feature is added to allow other builtin crate
names as described in the rationale.
If attempting to add a builtin crate with features then this will fail unless
the required cargo-feature is enabled as described in Features.
Once public and private dependencies are stabilised (rust#44663), cargo add
will add public = true by default for the standard library dependencies added:
[package]
name = "hello_world"
version = "0.1.0"
edition = "2024"
[dependencies]
std = { builtin = true, public = true } # <-- this would be added
If adding std or alloc with --optional, then a non-optional core would
also be added:
[package]
name = "hello_world"
version = "0.1.0"
edition = "2024"
[dependencies]
std = { builtin = true, optional = true } # <-- this would be added
core = { builtin = true } # <-- this would also be added
cargo info will learn how to print information for the built-in
std, alloc and core dependencies:
$ cargo info std
std
rust standard library
license: Apache 2.0 + MIT
rust-version: 1.86.0
documentation: https://doc.rust-lang.org/1.86.0/std/index.html
$ cargo info alloc
alloc
rust standard library
license: Apache 2.0 + MIT
rust-version: 1.86.0
documentation: https://doc.rust-lang.org/1.86.0/alloc/index.html
$ cargo info core
core
rust standard library
license: Apache 2.0 + MIT
rust-version: 1.86.0
documentation: https://doc.rust-lang.org/1.86.0/core/index.html
cargo metadata will emit std, alloc and core
dependencies to the metadata emitted by cargo metadata (when those crates
differ from the implicit state). source would be set to builtin and the
remaining fields would be set like any other dependency. Per the cargo metadata Compatibility documentation, adding a new
source kind is not considered an incompatible change. See also unresolved
question Should cargo metadata include the standard library’s
dependencies?.
Note
cargo metadataoutput could look as follows:{ "packages": [ { /* ... */ "dependencies": [ { "name": "std", "source": "builtin", "req": "*", "kind": null, "rename": null, "optional": false, "uses_default_features": true, "features": ["compiler-builtins-mem"], "target": null, "public": true } ], /* ... */ } ] }
cargo pkgid when passed -p core would print
builtin://.#core as the source, likewise with alloc and std. This format
complies with Cargo’s spec for Package IDs. See also
unresolved question What should the exact format of the pkgid spec for
builtin dependencies be?.
cargo remove will remove core, alloc or std explicitly
from the manifest if invoked with those crate names (using the same heuristics
as those described above for cargo add):
[package]
name = "hello_world"
version = "0.1.0"
edition = "2024"
[dependencies]
std = { builtin = true } # <-- this would be removed
cargo tree will show std, alloc and core at appropriate
places in the tree of dependencies. 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.
Note
cargo treeoutput could look as follows:$ cargo tree myproject v0.1.0 (/myproject) ├── rand v0.7.3 │ ├── getrandom v0.1.14 │ │ ├── cfg-if v0.1.10 │ │ │ └── core (built-in) │ │ ├── libc v0.2.68 │ │ │ └── core (built-in) │ │ └── core (built-in) │ ├── libc v0.2.68 (*) │ │ └── core (built-in) │ ├── rand_chacha v0.2.2 │ │ ├── ppv-lite86 v0.2.6 │ │ │ └── core (built-in) │ │ ├── rand_core v0.5.1 │ │ │ ├── getrandom v0.1.14 (*) │ │ │ └── core (built-in) │ │ └── std (built-in) │ │ └── alloc (built-in) │ │ └── core (built-in) │ ├── rand_core v0.5.1 (*) │ └── std (built-in) (*) └── std (built-in) (*)
This part of the RFC has no implications for the following Cargo subcommands:
cargo benchcargo buildcargo checkcargo cleancargo clippycargo doccargo fetchcargo fixcargo fmtcargo generate-lockfilecargo helpcargo initcargo installcargo locate-projectcargo logincargo logoutcargo miricargo newcargo ownercargo packagecargo publishcargo reportcargo runcargo rustccargo rustdoccargo searchcargo testcargo uninstallcargo updatecargo vendorcargo versioncargo yank
Rationale and alternatives
This section aims to justify all of the decisions made in the proposed design from Proposal and discuss why alternatives were not chosen.
Why explicitly declare dependencies on the standard library in Cargo.toml?
If there are no explicit dependencies on standard library crates, Cargo would need to be able to determine which standard library crates to build when this is required:
-
Cargo could unconditionally build
std,allocandcore. Not only would this be unnecessary and wasteful forno_stdcrates in the embedded ecosystem, but sometimes a target may not support buildingstdat all and this would cause the build to fail. -
rustc could support a
--printvalue that would print whether the crate declares itself as#![no_std]crate, and based on this, Cargo could buildstdor onlycore. This would require asking rustc to parse crates’ sources while resolving dependencies, slowing build times. Alternatively, Cargo can already read Rust source to detect frontmatter (forcargo script) so it could additionally look for#![no_std]itself. Regardless of how it determines a crate is no-std, Cargo would also need to know whether to buildalloctoo, which checking for#![no_std]does not help with. Cargo could go further and ask rustc whether a crate (or its dependencies) usedalloc, but this seems needlessly complicated. -
Cargo could allow the user to specify which crates are required to be built, such as with the existing options to the
-Zbuild-std=flag.build-std=alwaysproposes abuild-std.cratesflag to enable explicit dependencies to be a separate part of this RFC.
Furthermore, supporting explicit dependencies on standard library crates enables
use of other Cargo features that apply to dependencies in a natural and
intuitive way. If there were not explicit standard library dependencies and
enabling features on the std crate was desirable, then a mechanism other than
the standard syntax for this would be necessary, such as a flag (e.g.
-Zbuild-std-features) or option in Cargo’s configuration. This also applies to
optional dependencies, public/private features, etc.
Users already use Cargo features to toggle #![no_std] in crates which support
building without the standard library. When dependencies on the standard library
are exposed in Cargo.toml then they can be made optional and enabled by the
existing Cargo features that crates already have.
↩ Proposal
Why disallow builtin dependencies to be combined with other sources?
If using path/git sources with builtin dependencies worked in the same way
as using path/git sources with version sources, then: crates with
path/git standard library dependencies could be pushed to crates.io.
This is not desirable as it is unclear that supporting path/git sources
which shadow standard library crates was a deliberate choice and so enabling
that pattern to be used more widely when not necessary is needlessly permissive.
In addition, when combined with a git/path source, the version constraint
also applies to package from the git/path source. If version were used
alongside builtin, then this behaviour would be a poor fit as..
-
..the
std,allocandcorecrates all currently have a version of0.0.0 -
..choosing different version requirements for different
builtincrates is confusing when a single version of these crates is provided by the toolchain
Hypothetically, choosing a different version for builtin crates could be a way
of supporting per-target/per-profile MSRVs, but this has limited utility.
↩ Proposal
Why disallow builtin dependencies on other crates?
builtin dependencies could be accepted on two other crates - dependencies of
the standard library, like compiler_builtins, or other crates in the sysroot
added manually by users, however:
-
The standard library’s dependencies are not part of the stable interface of the standard library and it is not desirable that users can observe their existence or depend on them directly
-
Other crates in the sysroot added by users are not something that can reasonably be supported by build-std and these crates should become regular dependencies
↩ Proposal
Why unstably allow all names for builtin crates?
For any crate shipped with the standard library in the sysroot, the user can
already write an extern crate declaration to use it. Most are marked unstable
either explicitly or implicitly with the use of -Zforce-unstable-if-unmarked
so this does not allow items from these crates to be used on stable.
For example, some users write benchmarks using libtest and have written
extern crate test without the #[cfg(test)] attribute to load the crate.
There may be other niche uses of unstable sysroot crates that this enables to
continue on nightly toolchains.
An allowlist of builtin crate names isn’t used here to avoid Cargo needing to
hardcode the names of many crates in the sysroot which are inherently unstable.
↩ Proposal
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, trading consistency of user experience with special-casing
in Cargo. Cargo would have to handle implicit vs explicit dependencies
differently. An explicit dependency on the standard library will behave
similarly to other dependencies in their manifest, but the behaviour will be
subtly different than with implicit builtin dependencies (where extern crate
is required).
See the rustc documentation for more details on noprelude.
↩ Proposal
Why not require builtin dependencies instead of supporting implicit ones?
Requiring explicit builtin dependencies over an edition would increase the
boilerplate required for users of Cargo and make the minimal Cargo.toml file
larger.
Supporting implicit dependencies allows the majority of the Rust ecosystem from
having to make any changes - no_std crates (or crates with a std feature)
will still benefit from adding explicit dependencies as allow them to be easily
used with no_std targets but users can still work around any legacy crates in
the graph with build-std.crates.
↩ Proposal
Why disallow renaming standard library dependencies?
Cargo allows renaming dependencies with the package
key, which allows user code to refer to dependencies by names which do not
match their package name in their respective Cargo.toml files.
However, rustc expects the standard library crates to be present with their
existing names - for example, core is always added to the
extern prelude.
Alternatively, a mechanism could be added to rustc so that it could be informed
of the user’s names for builtin crates.
↩ Proposal
Why disallow source replacement on builtin packages?
Modifying the source code of the standard library in the rust-src component is
not supported. Source replacement of the builtin source could be a way to
support this in future but this is out-of-scope for this proposal.
See Allow builtin source replacement.
↩ Proposal
Why not permit overriding dependencies with replace or paths?
Similarly to source replacement, easing modification of the standard library sources is out-of-scope for this proposal.
↩ Proposal
Why add standard library dependencies to Cargo.lock?
Cargo.lock is a direct serialisation of a resolve and that must be a two-way
non-lossy process in order to make the Cargo.lock useful without doing further
resolution to fill in missing builtin packages.
Cargo will nevertheless need to support lockfiles without builtin dependencies as Cargo does not force new lockfile versions.
↩ Proposal
Why pass -Zcrate-attr=no_std to rustc?
The introduction of explicit dependencies means that there are now two ways to
indicate whether a crate depends on the standard library - builtin dependencies
in the Cargo manifest and the #![no_std] attribute. This isn’t ideal.
By passing -Zcrate-attr=no_std to rustc (or some equivalent), users no longer
need to specify the attribute explicitly in their source code. The attribute can
still be explicitly specified this way, which is useful when rustc is used
without a build system like Cargo. Other build systems can similarly pass
-Zcrate-attr=no_std if emulating how build-std works in Cargo.
Removing or replacing #![no_std] as a mechanism is left as a follow-up to avoid
introducing a language change in this RFC - see Replace #![no_std] as the
source-of-truth for whether a crate depends on std.
↩ Proposal
Why unstably permit patching of the standard library dependencies?
Being able to patch builtin = true dependencies and replace their source with
a path dependency is required to be able to replace rustc_dep_of_std. As
crates which use these sources cannot be published to crates.io, this would not
enable a usable general-purpose mechanism for crates to modify the standard
library sources. This capability is restricted to nightly toolchains as that is
all that is required for it to be used in replacing rustc_dep_of_std.
↩ Patches
Why limit enabling standard library features to an unstable feature?
If it were possible to enable features of the standard library crates on stable then all of the standard library’s current features would immediately be held to the same stability guarantees as the rest of the standard library, which is not desirable. See Allow enabling/disabling features with build-std
↩ Features
Why default to public for implicit standard library dependencies?
There are crates building on stable which re-export from the standard library.
If implicit 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 implicit standard library dependency.
↩ Public and private dependencies
Why default to private for explicit standard library dependencies?
All other explicitly written dependencies in the Cargo manifest are
private-by-default. This RFC tries to make builtin dependencies as consistent
with other dependencies in the manifest as possible (e.g. in Why not use
noprelude for explicit builtin
dependencies?).
↩ Public and private dependencies
Why include standard library crates in Cargo’s index format?
When Cargo builds the unit graph - roughly speaking, think of this as the compiler invocations it will eventually make - it queries the dependency resolver as to which dependencies a given crate has. The dependency resolver only looks at the source of a dependency, in many cases a registry source (i.e. a dependency from crates.io). In practice, this is the entry in the Cargo index for that dependency. As this is the information that is directly available, that a crate depends on the standard library should be reflected in the index entry.
Alternatively, the Cargo manifests of dependencies would need to be parsed in order to determine whether they have standard library dependencies as the index entry would be insufficient.
Why add a new key to Cargo’s registry index JSON schema?
Cargo’s registry index schema is versioned and making a
behaviour-of-Cargo-modifying change to the existing deps keys would be a
breaking change. Each package is published under one particular version of the
schema, meaning that older versions of Cargo cannot use newer versions of
packages which are defined using a schema it does not have knowledge of.
Cargo ignores packages published under an unsupported schema version, so older versions of Cargo cannot use newer versions of packages relying on these features (though this would be true because of an incompatible Cargo manifest anyway). New schema versions are disruptive to users on older toolchains, as the resolver will act as if a package does not exist. Recent Cargo versions have improved error reporting for this circumstance.
Some new fields, including rust-version, were added to all versions of the
schema. Cargo ignores fields it does not have knowledge of, so older versions of
Cargo will simply not use rust-version and its presence does not change their
behaviour.
Existing versions of Cargo already function correctly without knowledge of crate’s standard library dependencies. A new top-level key will be ignored by older versions of Cargo, while newer versions will understand it. This is a different approach to that taken when artifact dependencies were added to the schema, as those do not have a suitable representation in older versions of Cargo.
The obvious alternative to a builtin_deps key is to modify deps entries with
a new builtin: bool field and to increment the version of the schema. However,
these entries would not be understood by older versions of Cargo which would
look in the registry to find these packages and fail to do so.
That approach could be made to work if dummy packages for core/alloc/std
were added to registries. Older versions of Cargo would pass these to rustc
via --extern and shadow the real standard library dependencies in the sysroot,
so these packages would need to contain extern crate std; pub use std::*; (and
similar for alloc/core) to try and load the pre-built libraries from the
sysroot (this is the same approach as packages like embed-rs
take today, using path dependencies for the standard library to shadow it).
Why can builtin_deps shadow other packages in the registry?
While crates.io forbids certain crate names including std, alloc and
core, third party registries may allow them without a warning. The schema
needs a way to refer to packages with the same name either in the registry or
builtin, which builtin_deps allows.
builtin_deps names are not allowed to shadow names of packages in deps as
these would conflict when passed to rustc via --extern.
Unresolved questions
The following small details are likely to be bikeshed prior to this part of the RFC’s acceptance or stabilisation and aren’t pertinent to the overall design:
What syntax is used to identify dependencies on the standard library in Cargo.toml?
What syntax should be used for the explicit standard library dependencies?
builtin = true? sysroot = true (not ideal, as “sysroot” isn’t a concept that
we typically introduce to end-users)?
↩ Proposal
What is the format for builtin dependencies in Cargo.lock?
How should builtin deps be represented in lockfiles? Is builtin = true
appropriate? Could the source field be reused with the string “builtin” or
should it stay only as a URL+scheme?
↩ Proposal
What syntax is used to patch dependencies on the standard library in Cargo.toml?
[patch.builtin] is the natural syntax given builtin is a new source, but may
be needlessly different to existing packages. This mechanism exists only for
internal purposes so the exact syntax isn’t especially important.
↩ Patches
Should implicit standard library dependencies default to public?
Implicit standard library dependencies defaulting to public is a trade-off between special-casing in Cargo and requiring that any user with a dependency on the standard library who re-exports from the standard library manually declare their dependency as public.
↩ Public and private dependencies
Should explicit standard library dependencies default to private?
Explicit standard library dependencies defaulting to private is a trade-off between consistency with other dependencies in the manifest and implicit standard library dependencies.
↩ Public and private dependencies
Should we support build-dependencies?
Allowing builtin dependencies to be used in dependencies and
dev-dependencies but not in build-dependencies is an inconsistency.
However, supporting builtin dependencies in build-dependencies would permit
no-std build scripts. It is unclear whether supporting no-std build scripts
would be desirable. Not supporting build-dependencies initially is a safe
default that can be changed later if this is deemed necessary, without breaking
any existing users.
↩ dev-dependencies and build-dependencies
Should parsed manifests be used instead of the registry index?
An alternative to changing the registry index would be checking the parsed manifests of dependencies - which are downloaded and available when the unit graph is being built (and used for other Cargo features).
Should cargo metadata include the standard library’s dependencies?
cargo metadata is used by tools like rust-analyzer to determine the entire
crate graph and would benefit from knowledge of the standard library’s
dependencies, but this leaks internal details of the standard library and is
counter to the intent behind opaque dependencies.
This is primarily a compatibility concern, though were builtin dependencies to
be included in the metadata, it is expected that it would not be too disruptive,
as:
cargo pkgidandcargo build --message-formatwill unconditionally use the newpkgidspec anyway;cargo_metadatadoes not currently parse the pkgid spec so this won’t break all users;- and this will also show up in the source which is still opaque and
cargo_metadatadoes not parse
Therefore this would only be a problem for users who are parsing the pkgid specs
using cargo-util-schemas or their own parser, which is unlikely to be
disruptive.
What should the exact format of the pkgid spec for builtin dependencies be?
The format for builtin dependencies of cargo pkgid can be changed prior to
stabilisation and does not need to match what is proposed in this RFC exactly.
Prior art
See the Background and History of the build-std context RFC.
Future possibilities
This RFC unblocks fixing rust-lang/cargo#8798, enabling no-std crates from being prevented from having std dependencies.
There are also many possible follow-ups to this part of the RFC:
Replace #![no_std] as the source-of-truth for whether a crate depends on std
Crates can currently use the crate attribute #![no_std] to indicate a lack of
dependency on std. #![no_std] serves two purposes - it stops the compiler
from adding std to the extern prelude and it prevents the user from depending
on anything from std accidentally.
rustc’s default behaviour of loading std when not explicitly provided the
crate via an --extern flag must be preserved for backwards-compatibility
with existing direct invocations of rustc.
Ideally, #![no_std] would be removed entirely and the compiler would only load
std if it were used, but this isn’t possible with the current compiler
implementation.
Therefore, an explicit flag or attribute is necessary to tell rustc whether to
load std. -Zcrate-attr=no_std could be replaced with a explicit compiler
flag --no-std (naming subject to bikeshed) that Cargo and other drivers of
rustc could use. #![no_std] could be removed over an edition alongside
addition of builtin dependencies to the Cargo manifest.
As removing #![no_std] could easily be left to a follow-up, and is a change to
a stable attribute in the surface language (which would require language team
approval), it isn’t included in this proposal to keep scope and the number of
required approvals small.
↩ Proposal
Allow unstable crate names to be referenced behind cfgs without requiring nightly
It is possible to allow builtin dependencies on unstable crate names to exist behind cfgs and for the crate to be compiled on a stable toolchain as long as the cfgs are not active. This is a trade-off - it adds a large constraint on when Cargo can validate the set of crate names, but would enable users to avoid using nightly or doing MSRV bumps.
↩ Proposal
Allow builtin source replacement
This involves allowing the user to blanket-override the standard library sources
with a [source.builtin] section of the Cargo configuration.
As rationale-source-replacement details it is unclear if users need to do this or if it’s even something the Rust project wishes to support.
↩ Proposal
Remove rustc_dep_of_std
With first-class explicit dependencies on the standard library,
rustc_dep_of_std is rendered unnecessary and explicit dependencies on the
standard library can always be present in the Cargo.toml of the standard
library’s dependencies.
The core, alloc and std dependencies can be patched in the standard
library’s workspace to point to the local copy of the crates. This avoids
crates.io dependencies needing to add support for rustc_dep_of_std before
the standard library can depend on them.
↩ Proposal
Allow enabling/disabling features with build-std
This would require the library team be comfortable with the features declared on the standard library being part of the stable interface of the standard library.
The behaviour of disabling default features has been highlighted as a potential cause of breaking changes.
Alternatively, this could be enabled alongside another proposal which would allow the standard library to define some features as stable and others as unstable.
As there are some features that Cargo will set itself when appropriate (e.g. to
enable or disable panic runtimes or
compiler-builtins/mem), Cargo may need to always
prevent some otherwise stable features from being toggled as it controls those.
↩ Features
Allow local builds of compiler-rt intrinsics
The c feature of compiler_builtins (which is also
exposed by core, alloc and std through compiler-builtins-c) causes its
build.rs file to build and link in more optimised C versions of intrinsics.
It will not be enabled by default because it is possible that the target
platform does not have a suitable C compiler available. The user being able to
enable this manually will be enabled through work on features (see
Allow enabling/disabling features with build-std). Once the
user can enable compiler-builtins/c, they will need to manually configure
CFLAGS to ensure that the C components will link with Rust code.