recorded decision · public · no signup

accepted2026-06-22rust-lang/rfcs

error: must have a non-optional dependency on core

What was chosen

  • Standard library dependencies will be present in the registry index to allow them as dependencies of other crates.adr
  • The `builtin = true` key is a new dependency source, usable only for `core`, `alloc`, or `std` on stable.adr
  • Explicit dependencies are passed to rustc without the `noprelude` modifier for consistent user experience.adr
  • Crates without explicit standard library dependencies retain an implicit dependency on the target's default set.adr
  • Explicit standard library dependencies default to private, consistent with other manifest dependencies.adr
  • Users can now optionally declare explicit dependencies on standard library crates in their `Cargo.toml` files.adr
  • Standard library dependencies can be marked `optional` and enabled conditionally by a feature in the crate.adr
  • Implicit standard library dependencies default to public to avoid breaking existing crates re-exporting from `std`.adr

Constraints

  • If an optional standard library dependency exists, a non-optional `core` dependency must also be present.adr

Consequences

  • Any explicit standard library dependency disables the implicit dependencies for that target.adr

The recorded why

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

  1. build-std context (rfcs#3873)
  2. build-std="always" (rfcs#3874)
  3. Explicit standard library dependencies (this RFC)
    • Proposal
    • [Rationale and alternatives][rationale-and-alternatives]
    • [Unresolved questions][unresolved-questions]
    • [Future possibilities][future-possibilities]
  4. build-std="compatible" (RFC not opened yet)
  5. 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 ([?][rationale-why-explicit-deps]):

[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 ([?][rationale-builtin-other-sources]).

builtin can only be used with crates named core, alloc or std ([?][rationale-no-builtin-other-crates]) 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 ([?][rationale-unstable-builtin-crates]). 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 noprelude modifier ([?][rationale-explicit-noprelude]) (noprelude refers to the compiler's notion of the "extern prelude", not the prelude in the the user-facing sense of std::prelude::*).

When adding an explicit dependency, users may need to adjust their code (removing extraneous extern crate statements 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 ([?][rationale-no-migration]) 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 noprelude modifier to ensure backwards compatibility as in build-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 ([?][rationale-package-key]). It is not possible to perform source replacement on the builtin source using the [source] Cargo config table ([?][rationale-source-replacement]), and nor is it possible to override builtin dependencies with the [replace] sections or paths overrides ([?][rationale-overriding-builtins]), though patching is permitted under a perma-unstable feature flag ([?][rationale-patching]).

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 ([?][rationale-cargo-lock]).

[!NOTE]

A new version of the Cargo.lock file will be introduced to add support for packages with a builtin source:

[[package]]
name = "std"
version = "0.0.0"
source = "builtin"

The package version of std, alloc and core will be fixed at 0.0.0. The optional lockfile fields dependencies and checksum will not be present for builtin dependencies.

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; [?][rationale-no_std]).

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?][rationale-why-explicit-deps]
  • [Why disallow builtin dependencies to be combined with other sources?][rationale-builtin-other-sources]
  • [Why disallow builtin dependencies on other crates?][rationale-no-builtin-other-crates]
  • [Why unstably allow all names for builtin crates?][rationale-unstable-builtin-crates]
  • [Why not use noprelude for explicit builtin dependencies?][rationale-explicit-noprelude]
  • [Why not require builtin dependencies instead of supporting implicit ones?][rationale-no-migration]
  • [Why disallow renaming standard library dependencies?][rationale-package-key]
  • [Why disallow source replacement on builtin packages?][rationale-source-replacement]
  • [Why add standard library dependencies to Cargo.lock?][rationale-cargo-lock]
  • [Why pass -Zcrate-attr=no_std to rustc?][rationale-no_std]

See the following sections for relevant unresolved questions:

  • [What syntax is used to identify dependencies on the standard library in Cargo.toml?][unresolved-dep-syntax]
  • [What is the format for builtin dependencies in Cargo.lock?][unresolved-lockfile]

See the following sections for future possibilities:

  • [Allow unstable crate names to be referenced behind cfgs without requiring nightly][future-cfg-unstable-crate-name]
  • [Allow builtin source replacement][future-source-replacement]
  • [Remove rustc_dep_of_std][future-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) ([?][rationale-patching]):

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

  • [Why unstably permit patching of standard library dependencies?][rationale-patching]

See the following sections for relevant unresolved questions:

  • [What syntax is used to patch dependencies on the standard library in Cargo.toml?][unresolved-patch-syntax]

Features

On a stable toolchain, it is not permitted to enable or disable features of explicit standard library dependencies ([?][rationale-features]), 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:

  • [Why limit enabling standard library features to an unstable feature?][rationale-features]

See the following sections for future possibilities:

  • [Allow enabling/disabling features with build-std][future-features]

Public and private dependencies

Implicit dependencies on the standard library default to being public dependencies ([?][rationale-default-implicit-public]).

[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 ([?][rationale-default-implicit-public]). 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?][unresolved-std-default-implicit-public]
  • [Should explicit standard library dependencies default to private?][unresolved-std-default-explicit-private]

See the following sections for rationale/alternatives:

  • [Why default to public for implicit standard library dependencies?][rationale-default-implicit-public]
  • [Why default to private for explicit standard library dependencies?][rationale-default-explicit-private]

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:

  • [Should we support build-dependencies?][unresolved-build-deps]

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 ([?][rationale-cargo-index]).

A builtin_deps key is added to the [index's JSON schema][cargo-json-schema] ([?][rationale-cargo-builtindeps]). 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 builtin package. Can shadow the names of other packages in the registry (except those packages in the deps key of the current package) ([?][rationale-cargo-index-shadowing])
  • 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 deps key

The keys req, registry and package from deps are not required per the limitations on builtin dependencies.

The builtin_deps key 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 builtin dependencies 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 published Cargo.toml will not explicitly declare any dependencies that match the implicit state. builtin dependencies 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?][rationale-cargo-index]
  • [Why add a new key to Cargo's registry index JSON schema?][rationale-cargo-builtindeps]
  • [Why can builtin_deps shadow other packages in the registry?][rationale-cargo-index-shadowing]

See the following sections for relevant unresolved questions:

  • [Should parsed manifests be used instead of the registry index?][unresolved-registry-changes]

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][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][rationale-unstable-builtin-crates].

If attempting to add a builtin crate with features then this will fail unless the required cargo-feature is enabled as described in [Features][featur

Excerpt — the full document is at the cited source: text/3875-build-std-explicit-dependencies.md