about summary refs log tree commit diff
diff options
context:
space:
mode:
authorbors <bors@rust-lang.org>2025-07-07 05:04:39 +0000
committerbors <bors@rust-lang.org>2025-07-07 05:04:39 +0000
commitc720f49c46ec3e70bbc2bbb3e0dd1fe7fc0a47f2 (patch)
tree13fdaae0102dd2f0954c796bcd4cdf21be4d4a0d
parent0d11be5aabe0cd49609fff5fce57c4691a22fe55 (diff)
parent5dc77ba6586ded662d4c5447da87a70d8a77651a (diff)
downloadrust-c720f49c46ec3e70bbc2bbb3e0dd1fe7fc0a47f2.tar.gz
rust-c720f49c46ec3e70bbc2bbb3e0dd1fe7fc0a47f2.zip
Auto merge of #143048 - Kobzol:bootstrap-check-stage-1, r=jieyouxu
Enforce in bootstrap that check must have stage at least 1

This PR is another step towards https://rust-lang.zulipchat.com/#narrow/channel/326414-t-infra.2Fbootstrap/topic/Proposal.20to.20cleanup.20stages.20and.20steps.20after.20the.20redesign/with/523586917, this time dealing with `x check`.

It enforces the invariant that:
- We check std stage N with rustc stage N
- We check everything else stage N with rustc stage N - 1

It creates a single function that prepares a proper build compiler for checking something, and also adds snapshot tests for various common check steps. Some obsolete code was also removed.

The default check stage also becomes 1, for all profiles. I tested manually that `x check std` with `download-ci-rustc` still works and doesn't build rustc locally.

Closes: https://github.com/rust-lang/rust/pull/139170

r? `@ghost`
-rw-r--r--src/bootstrap/defaults/bootstrap.library.toml1
-rw-r--r--src/bootstrap/src/core/build_steps/check.rs396
-rw-r--r--src/bootstrap/src/core/build_steps/clippy.rs4
-rw-r--r--src/bootstrap/src/core/build_steps/compile.rs43
-rw-r--r--src/bootstrap/src/core/build_steps/llvm.rs9
-rw-r--r--src/bootstrap/src/core/build_steps/tool.rs2
-rw-r--r--src/bootstrap/src/core/builder/tests.rs107
-rw-r--r--src/bootstrap/src/core/config/config.rs15
-rw-r--r--src/bootstrap/src/utils/change_tracker.rs5
-rw-r--r--src/ci/docker/host-x86_64/pr-check-1/Dockerfile6
10 files changed, 304 insertions, 284 deletions
diff --git a/src/bootstrap/defaults/bootstrap.library.toml b/src/bootstrap/defaults/bootstrap.library.toml
index 6a867093b78..24ff87191d6 100644
--- a/src/bootstrap/defaults/bootstrap.library.toml
+++ b/src/bootstrap/defaults/bootstrap.library.toml
@@ -1,7 +1,6 @@
 # These defaults are meant for contributors to the standard library and documentation.
 [build]
 bench-stage = 1
-check-stage = 1
 test-stage = 1
 
 [rust]
diff --git a/src/bootstrap/src/core/build_steps/check.rs b/src/bootstrap/src/core/build_steps/check.rs
index 6c5f70b2f43..0497bae86a1 100644
--- a/src/bootstrap/src/core/build_steps/check.rs
+++ b/src/bootstrap/src/core/build_steps/check.rs
@@ -3,16 +3,19 @@
 use crate::core::build_steps::compile::{
     add_to_sysroot, run_cargo, rustc_cargo, rustc_cargo_env, std_cargo, std_crates_for_run_make,
 };
+use crate::core::build_steps::tool;
 use crate::core::build_steps::tool::{COMPILETEST_ALLOW_FEATURES, SourceType, prepare_tool_cargo};
 use crate::core::builder::{
     self, Alias, Builder, Kind, RunConfig, ShouldRun, Step, StepMetadata, crate_description,
 };
 use crate::core::config::TargetSelection;
 use crate::utils::build_stamp::{self, BuildStamp};
-use crate::{Mode, Subcommand};
+use crate::{Compiler, Mode, Subcommand};
 
 #[derive(Debug, Clone, PartialEq, Eq, Hash)]
 pub struct Std {
+    /// Compiler that will check this std.
+    pub build_compiler: Compiler,
     pub target: TargetSelection,
     /// Whether to build only a subset of crates.
     ///
@@ -20,16 +23,13 @@ pub struct Std {
     ///
     /// [`compile::Rustc`]: crate::core::build_steps::compile::Rustc
     crates: Vec<String>,
-    /// Never use this from outside calls. It is intended for internal use only within `check::Std::make_run`
-    /// and `check::Std::run`.
-    custom_stage: Option<u32>,
 }
 
 impl Std {
     const CRATE_OR_DEPS: &[&str] = &["sysroot", "coretests", "alloctests"];
 
-    pub fn new(target: TargetSelection) -> Self {
-        Self { target, crates: vec![], custom_stage: None }
+    pub fn new(build_compiler: Compiler, target: TargetSelection) -> Self {
+        Self { build_compiler, target, crates: vec![] }
     }
 }
 
@@ -48,14 +48,11 @@ impl Step for Std {
 
     fn make_run(run: RunConfig<'_>) {
         let crates = std_crates_for_run_make(&run);
-
-        let stage = if run.builder.config.is_explicit_stage() || run.builder.top_stage >= 1 {
-            run.builder.top_stage
-        } else {
-            1
-        };
-
-        run.builder.ensure(Std { target: run.target, crates, custom_stage: Some(stage) });
+        run.builder.ensure(Std {
+            build_compiler: prepare_compiler_for_check(run.builder, run.target, Mode::Std),
+            target: run.target,
+            crates,
+        });
     }
 
     fn run(self, builder: &Builder<'_>) {
@@ -66,40 +63,20 @@ impl Step for Std {
             return;
         }
 
-        let stage = self.custom_stage.unwrap_or(builder.top_stage);
-
+        let build_compiler = self.build_compiler;
+        let stage = build_compiler.stage;
         let target = self.target;
-        let compiler = builder.compiler(stage, builder.config.host_target);
-
-        if stage == 0 {
-            let mut is_explicitly_called =
-                builder.paths.iter().any(|p| p.starts_with("library") || p.starts_with("std"));
-
-            if !is_explicitly_called {
-                for c in Std::CRATE_OR_DEPS {
-                    is_explicitly_called = builder.paths.iter().any(|p| p.starts_with(c));
-                }
-            }
-
-            if is_explicitly_called {
-                eprintln!("WARNING: stage 0 std is precompiled and does nothing during `x check`.");
-            }
-
-            // Reuse the stage0 libstd
-            builder.std(compiler, target);
-            return;
-        }
 
         let mut cargo = builder::Cargo::new(
             builder,
-            compiler,
+            build_compiler,
             Mode::Std,
             SourceType::InTree,
             target,
             Kind::Check,
         );
 
-        std_cargo(builder, target, compiler.stage, &mut cargo);
+        std_cargo(builder, target, stage, &mut cargo);
         if matches!(builder.config.cmd, Subcommand::Fix) {
             // By default, cargo tries to fix all targets. Tell it not to fix tests until we've added `test` to the sysroot.
             cargo.arg("--lib");
@@ -115,16 +92,9 @@ impl Step for Std {
             Some(stage),
         );
 
-        let stamp = build_stamp::libstd_stamp(builder, compiler, target).with_prefix("check");
+        let stamp = build_stamp::libstd_stamp(builder, build_compiler, target).with_prefix("check");
         run_cargo(builder, cargo, builder.config.free_args.clone(), &stamp, vec![], true, false);
 
-        // We skip populating the sysroot in non-zero stage because that'll lead
-        // to rlib/rmeta conflicts if std gets built during this session.
-        if compiler.stage == 0 {
-            let libdir = builder.sysroot_target_libdir(compiler, target);
-            let hostdir = builder.sysroot_target_libdir(compiler, compiler.host);
-            add_to_sysroot(builder, &libdir, &hostdir, &stamp);
-        }
         drop(_guard);
 
         // don't check test dependencies if we haven't built libtest
@@ -140,21 +110,14 @@ impl Step for Std {
         // Currently only the "libtest" tree of crates does this.
         let mut cargo = builder::Cargo::new(
             builder,
-            compiler,
+            build_compiler,
             Mode::Std,
             SourceType::InTree,
             target,
             Kind::Check,
         );
 
-        // If we're not in stage 0, tests and examples will fail to compile
-        // from `core` definitions being loaded from two different `libcore`
-        // .rmeta and .rlib files.
-        if compiler.stage == 0 {
-            cargo.arg("--all-targets");
-        }
-
-        std_cargo(builder, target, compiler.stage, &mut cargo);
+        std_cargo(builder, target, build_compiler.stage, &mut cargo);
 
         // Explicitly pass -p for all dependencies krates -- this will force cargo
         // to also check the tests/benches/examples for these crates, rather
@@ -163,18 +126,23 @@ impl Step for Std {
             cargo.arg("-p").arg(krate);
         }
 
-        let stamp = build_stamp::libstd_stamp(builder, compiler, target).with_prefix("check-test");
+        let stamp =
+            build_stamp::libstd_stamp(builder, build_compiler, target).with_prefix("check-test");
         let _guard = builder.msg_check("library test/bench/example targets", target, Some(stage));
         run_cargo(builder, cargo, builder.config.free_args.clone(), &stamp, vec![], true, false);
     }
 
     fn metadata(&self) -> Option<StepMetadata> {
-        Some(StepMetadata::check("std", self.target))
+        Some(StepMetadata::check("std", self.target).built_by(self.build_compiler))
     }
 }
 
+/// Checks rustc using `build_compiler` and copies the built
+/// .rmeta files into the sysroot of `build_compiler`.
 #[derive(Debug, Clone, PartialEq, Eq, Hash)]
 pub struct Rustc {
+    /// Compiler that will check this rustc.
+    pub build_compiler: Compiler,
     pub target: TargetSelection,
     /// Whether to build only a subset of crates.
     ///
@@ -185,13 +153,13 @@ pub struct Rustc {
 }
 
 impl Rustc {
-    pub fn new(target: TargetSelection, builder: &Builder<'_>) -> Self {
+    pub fn new(builder: &Builder<'_>, build_compiler: Compiler, target: TargetSelection) -> Self {
         let crates = builder
             .in_tree_crates("rustc-main", Some(target))
             .into_iter()
             .map(|krate| krate.name.to_string())
             .collect();
-        Self { target, crates }
+        Self { build_compiler, target, crates }
     }
 }
 
@@ -206,40 +174,43 @@ impl Step for Rustc {
 
     fn make_run(run: RunConfig<'_>) {
         let crates = run.make_run_crates(Alias::Compiler);
-        run.builder.ensure(Rustc { target: run.target, crates });
+        run.builder.ensure(Rustc {
+            target: run.target,
+            build_compiler: prepare_compiler_for_check(run.builder, run.target, Mode::Rustc),
+            crates,
+        });
     }
 
-    /// Builds the compiler.
+    /// Check the compiler.
     ///
-    /// This will build the compiler for a particular stage of the build using
+    /// This will check the compiler for a particular stage of the build using
     /// the `compiler` targeting the `target` architecture. The artifacts
     /// created will also be linked into the sysroot directory.
+    ///
+    /// If we check a stage 2 compiler, we will have to first build a stage 1 compiler to check it.
     fn run(self, builder: &Builder<'_>) {
-        let compiler = builder.compiler(builder.top_stage, builder.config.host_target);
+        let build_compiler = self.build_compiler;
         let target = self.target;
 
-        if compiler.stage != 0 {
-            // If we're not in stage 0, then we won't have a std from the beta
-            // compiler around. That means we need to make sure there's one in
-            // the sysroot for the compiler to find. Otherwise, we're going to
-            // fail when building crates that need to generate code (e.g., build
-            // scripts and their dependencies).
-            builder.std(compiler, compiler.host);
-            builder.std(compiler, target);
-        } else {
-            builder.ensure(Std::new(target));
-        }
+        // Build host std for compiling build scripts
+        builder.std(build_compiler, build_compiler.host);
+
+        // Build target std so that the checked rustc can link to it during the check
+        // FIXME: maybe we can a way to only do a check of std here?
+        // But for that we would have to copy the stdlib rmetas to the sysroot of the build
+        // compiler, which conflicts with std rlibs, if we also build std.
+        builder.std(build_compiler, target);
 
         let mut cargo = builder::Cargo::new(
             builder,
-            compiler,
+            build_compiler,
             Mode::Rustc,
             SourceType::InTree,
             target,
             Kind::Check,
         );
 
-        rustc_cargo(builder, &mut cargo, target, &compiler, &self.crates);
+        rustc_cargo(builder, &mut cargo, target, &build_compiler, &self.crates);
 
         // Explicitly pass -p for all compiler crates -- this will force cargo
         // to also check the tests/benches/examples for these crates, rather
@@ -254,22 +225,75 @@ impl Step for Rustc {
             None,
         );
 
-        let stamp = build_stamp::librustc_stamp(builder, compiler, target).with_prefix("check");
+        let stamp =
+            build_stamp::librustc_stamp(builder, build_compiler, target).with_prefix("check");
 
         run_cargo(builder, cargo, builder.config.free_args.clone(), &stamp, vec![], true, false);
 
-        let libdir = builder.sysroot_target_libdir(compiler, target);
-        let hostdir = builder.sysroot_target_libdir(compiler, compiler.host);
+        let libdir = builder.sysroot_target_libdir(build_compiler, target);
+        let hostdir = builder.sysroot_target_libdir(build_compiler, build_compiler.host);
         add_to_sysroot(builder, &libdir, &hostdir, &stamp);
     }
 
     fn metadata(&self) -> Option<StepMetadata> {
-        Some(StepMetadata::check("rustc", self.target))
+        Some(StepMetadata::check("rustc", self.target).built_by(self.build_compiler))
+    }
+}
+
+/// Prepares a compiler that will check something with the given `mode`.
+fn prepare_compiler_for_check(
+    builder: &Builder<'_>,
+    target: TargetSelection,
+    mode: Mode,
+) -> Compiler {
+    let host = builder.host_target;
+    match mode {
+        Mode::ToolBootstrap => builder.compiler(0, host),
+        Mode::ToolStd => {
+            // These tools require the local standard library to be checked
+            let build_compiler = builder.compiler(builder.top_stage, host);
+
+            // We need to build the host stdlib to check the tool itself.
+            // We need to build the target stdlib so that the tool can link to it.
+            builder.std(build_compiler, host);
+            // We could only check this library in theory, but `check::Std` doesn't copy rmetas
+            // into `build_compiler`'s sysroot to avoid clashes with `.rlibs`, so we build it
+            // instead.
+            builder.std(build_compiler, target);
+            build_compiler
+        }
+        Mode::ToolRustc | Mode::Codegen => {
+            // FIXME: this is a hack, see description of Mode::Rustc below
+            let stage = if host == target { builder.top_stage - 1 } else { builder.top_stage };
+            // When checking tool stage N, we check it with compiler stage N-1
+            let build_compiler = builder.compiler(stage, host);
+            builder.ensure(Rustc::new(builder, build_compiler, target));
+            build_compiler
+        }
+        Mode::Rustc => {
+            // This is a horrible hack, because we actually change the compiler stage numbering
+            // here. If you do `x check --stage 1 --host FOO`, we build stage 1 host rustc,
+            // and use that to check stage 1 FOO rustc (which actually makes that stage 2 FOO
+            // rustc).
+            //
+            // FIXME: remove this and either fix cross-compilation check on stage 2 (which has a
+            // myriad of other problems) or disable cross-checking on stage 1.
+            let stage = if host == target { builder.top_stage - 1 } else { builder.top_stage };
+            builder.compiler(stage, host)
+        }
+        Mode::Std => {
+            // When checking std stage N, we want to do it with the stage N compiler
+            // Note: we don't need to build the host stdlib here, because when compiling std, the
+            // stage 0 stdlib is used to compile build scripts and proc macros.
+            builder.compiler(builder.top_stage, host)
+        }
     }
 }
 
+/// Checks a single codegen backend.
 #[derive(Debug, Clone, PartialEq, Eq, Hash)]
 pub struct CodegenBackend {
+    pub build_compiler: Compiler,
     pub target: TargetSelection,
     pub backend: &'static str,
 }
@@ -284,8 +308,10 @@ impl Step for CodegenBackend {
     }
 
     fn make_run(run: RunConfig<'_>) {
+        // FIXME: only check the backend(s) that were actually selected in run.paths
+        let build_compiler = prepare_compiler_for_check(run.builder, run.target, Mode::Codegen);
         for &backend in &["cranelift", "gcc"] {
-            run.builder.ensure(CodegenBackend { target: run.target, backend });
+            run.builder.ensure(CodegenBackend { build_compiler, target: run.target, backend });
         }
     }
 
@@ -296,15 +322,13 @@ impl Step for CodegenBackend {
             return;
         }
 
-        let compiler = builder.compiler(builder.top_stage, builder.config.host_target);
+        let build_compiler = self.build_compiler;
         let target = self.target;
         let backend = self.backend;
 
-        builder.ensure(Rustc::new(target, builder));
-
         let mut cargo = builder::Cargo::new(
             builder,
-            compiler,
+            build_compiler,
             Mode::Codegen,
             SourceType::InTree,
             target,
@@ -314,23 +338,25 @@ impl Step for CodegenBackend {
         cargo
             .arg("--manifest-path")
             .arg(builder.src.join(format!("compiler/rustc_codegen_{backend}/Cargo.toml")));
-        rustc_cargo_env(builder, &mut cargo, target, compiler.stage);
+        rustc_cargo_env(builder, &mut cargo, target, build_compiler.stage);
 
-        let _guard = builder.msg_check(backend, target, None);
+        let _guard = builder.msg_check(format!("rustc_codegen_{backend}"), target, None);
 
-        let stamp = build_stamp::codegen_backend_stamp(builder, compiler, target, backend)
+        let stamp = build_stamp::codegen_backend_stamp(builder, build_compiler, target, backend)
             .with_prefix("check");
 
         run_cargo(builder, cargo, builder.config.free_args.clone(), &stamp, vec![], true, false);
     }
 
     fn metadata(&self) -> Option<StepMetadata> {
-        Some(StepMetadata::check(self.backend, self.target))
+        Some(StepMetadata::check(self.backend, self.target).built_by(self.build_compiler))
     }
 }
 
+/// Checks Rust analyzer that links to .rmetas from a checked rustc.
 #[derive(Debug, Clone, PartialEq, Eq, Hash)]
 pub struct RustAnalyzer {
+    pub build_compiler: Compiler,
     pub target: TargetSelection,
 }
 
@@ -351,18 +377,17 @@ impl Step for RustAnalyzer {
     }
 
     fn make_run(run: RunConfig<'_>) {
-        run.builder.ensure(RustAnalyzer { target: run.target });
+        let build_compiler = prepare_compiler_for_check(run.builder, run.target, Mode::ToolRustc);
+        run.builder.ensure(RustAnalyzer { build_compiler, target: run.target });
     }
 
     fn run(self, builder: &Builder<'_>) {
-        let compiler = builder.compiler(builder.top_stage, builder.config.host_target);
+        let build_compiler = self.build_compiler;
         let target = self.target;
 
-        builder.ensure(Rustc::new(target, builder));
-
         let mut cargo = prepare_tool_cargo(
             builder,
-            compiler,
+            build_compiler,
             Mode::ToolRustc,
             target,
             builder.kind,
@@ -379,7 +404,7 @@ impl Step for RustAnalyzer {
 
         // Cargo's output path in a given stage, compiled by a particular
         // compiler for the specified target.
-        let stamp = BuildStamp::new(&builder.cargo_out(compiler, Mode::ToolRustc, target))
+        let stamp = BuildStamp::new(&builder.cargo_out(build_compiler, Mode::ToolRustc, target))
             .with_prefix("rust-analyzer-check");
 
         let _guard = builder.msg_check("rust-analyzer artifacts", target, None);
@@ -387,7 +412,7 @@ impl Step for RustAnalyzer {
     }
 
     fn metadata(&self) -> Option<StepMetadata> {
-        Some(StepMetadata::check("rust-analyzer", self.target))
+        Some(StepMetadata::check("rust-analyzer", self.target).built_by(self.build_compiler))
     }
 }
 
@@ -417,19 +442,11 @@ impl Step for Compiletest {
         } else {
             Mode::ToolStd
         };
-
-        let compiler = builder.compiler(
-            if mode == Mode::ToolBootstrap { 0 } else { builder.top_stage },
-            builder.config.host_target,
-        );
-
-        if mode != Mode::ToolBootstrap {
-            builder.ensure(Rustc::new(self.target, builder));
-        }
+        let build_compiler = prepare_compiler_for_check(builder, self.target, mode);
 
         let mut cargo = prepare_tool_cargo(
             builder,
-            compiler,
+            build_compiler,
             mode,
             self.target,
             builder.kind,
@@ -442,7 +459,7 @@ impl Step for Compiletest {
 
         cargo.arg("--all-targets");
 
-        let stamp = BuildStamp::new(&builder.cargo_out(compiler, mode, self.target))
+        let stamp = BuildStamp::new(&builder.cargo_out(build_compiler, mode, self.target))
             .with_prefix("compiletest-check");
 
         let _guard = builder.msg_check("compiletest artifacts", self.target, None);
@@ -460,12 +477,15 @@ macro_rules! tool_check_step {
             // The part of this path after the final '/' is also used as a display name.
             path: $path:literal
             $(, alt_path: $alt_path:literal )*
+            , mode: $mode:path
+            $(, allow_features: $allow_features:expr )?
             $(, default: $default:literal )?
             $( , )?
         }
     ) => {
         #[derive(Debug, Clone, PartialEq, Eq, Hash)]
         pub struct $name {
+            pub build_compiler: Compiler,
             pub target: TargetSelection,
         }
 
@@ -480,16 +500,30 @@ macro_rules! tool_check_step {
             }
 
             fn make_run(run: RunConfig<'_>) {
-                run.builder.ensure($name { target: run.target });
+                let target = run.target;
+                let build_compiler = prepare_compiler_for_check(run.builder, target, $mode);
+
+                // It doesn't make sense to cross-check bootstrap tools
+                if $mode == Mode::ToolBootstrap && target != run.builder.host_target {
+                    println!("WARNING: not checking bootstrap tool {} for target {target} as it is a bootstrap (host-only) tool", stringify!($path));
+                    return;
+                };
+
+                run.builder.ensure($name { target, build_compiler });
             }
 
             fn run(self, builder: &Builder<'_>) {
-                let Self { target } = self;
-                run_tool_check_step(builder, target, stringify!($name), $path);
+                let Self { target, build_compiler } = self;
+                let allow_features = {
+                    let mut _value = "";
+                    $( _value = $allow_features; )?
+                    _value
+                };
+                run_tool_check_step(builder, build_compiler, target, $path, $mode, allow_features);
             }
 
             fn metadata(&self) -> Option<StepMetadata> {
-                Some(StepMetadata::check(stringify!($name), self.target))
+                Some(StepMetadata::check(stringify!($name), self.target).built_by(self.build_compiler))
             }
         }
     }
@@ -498,19 +532,18 @@ macro_rules! tool_check_step {
 /// Used by the implementation of `Step::run` in `tool_check_step!`.
 fn run_tool_check_step(
     builder: &Builder<'_>,
+    build_compiler: Compiler,
     target: TargetSelection,
-    step_type_name: &str,
     path: &str,
+    mode: Mode,
+    allow_features: &str,
 ) {
     let display_name = path.rsplit('/').next().unwrap();
-    let compiler = builder.compiler(builder.top_stage, builder.config.host_target);
-
-    builder.ensure(Rustc::new(target, builder));
 
     let mut cargo = prepare_tool_cargo(
         builder,
-        compiler,
-        Mode::ToolRustc,
+        build_compiler,
+        mode,
         target,
         builder.kind,
         path,
@@ -521,98 +554,65 @@ fn run_tool_check_step(
         SourceType::InTree,
         &[],
     );
+    cargo.allow_features(allow_features);
 
+    // FIXME: check bootstrap doesn't currently work with --all-targets
     cargo.arg("--all-targets");
 
-    let stamp = BuildStamp::new(&builder.cargo_out(compiler, Mode::ToolRustc, target))
-        .with_prefix(&format!("{}-check", step_type_name.to_lowercase()));
+    let stamp = BuildStamp::new(&builder.cargo_out(build_compiler, mode, target))
+        .with_prefix(&format!("{display_name}-check"));
+
+    let stage = match mode {
+        // Mode::ToolRustc is included here because of how msg_sysroot_tool prints stages
+        Mode::Std | Mode::ToolRustc => build_compiler.stage,
+        _ => build_compiler.stage + 1,
+    };
 
-    let _guard = builder.msg_check(format!("{display_name} artifacts"), target, None);
+    let _guard =
+        builder.msg_tool(builder.kind, mode, display_name, stage, &build_compiler.host, &target);
     run_cargo(builder, cargo, builder.config.free_args.clone(), &stamp, vec![], true, false);
 }
 
-tool_check_step!(Rustdoc { path: "src/tools/rustdoc", alt_path: "src/librustdoc" });
+tool_check_step!(Rustdoc {
+    path: "src/tools/rustdoc",
+    alt_path: "src/librustdoc",
+    mode: Mode::ToolRustc
+});
 // Clippy, miri and Rustfmt are hybrids. They are external tools, but use a git subtree instead
 // of a submodule. Since the SourceType only drives the deny-warnings
 // behavior, treat it as in-tree so that any new warnings in clippy will be
 // rejected.
-tool_check_step!(Clippy { path: "src/tools/clippy" });
-tool_check_step!(Miri { path: "src/tools/miri" });
-tool_check_step!(CargoMiri { path: "src/tools/miri/cargo-miri" });
-tool_check_step!(Rustfmt { path: "src/tools/rustfmt" });
-tool_check_step!(MiroptTestTools { path: "src/tools/miropt-test-tools" });
-tool_check_step!(TestFloatParse { path: "src/tools/test-float-parse" });
-tool_check_step!(FeaturesStatusDump { path: "src/tools/features-status-dump" });
-
-tool_check_step!(Bootstrap { path: "src/bootstrap", default: false });
+tool_check_step!(Clippy { path: "src/tools/clippy", mode: Mode::ToolRustc });
+tool_check_step!(Miri { path: "src/tools/miri", mode: Mode::ToolRustc });
+tool_check_step!(CargoMiri { path: "src/tools/miri/cargo-miri", mode: Mode::ToolRustc });
+tool_check_step!(Rustfmt { path: "src/tools/rustfmt", mode: Mode::ToolRustc });
+tool_check_step!(MiroptTestTools {
+    path: "src/tools/miropt-test-tools",
+    mode: Mode::ToolBootstrap
+});
+// We want to test the local std
+tool_check_step!(TestFloatParse {
+    path: "src/tools/test-float-parse",
+    mode: Mode::ToolStd,
+    allow_features: tool::TestFloatParse::ALLOW_FEATURES
+});
+tool_check_step!(FeaturesStatusDump {
+    path: "src/tools/features-status-dump",
+    mode: Mode::ToolBootstrap
+});
+
+tool_check_step!(Bootstrap { path: "src/bootstrap", mode: Mode::ToolBootstrap, default: false });
 
 // `run-make-support` will be built as part of suitable run-make compiletest test steps, but support
 // check to make it easier to work on.
-tool_check_step!(RunMakeSupport { path: "src/tools/run-make-support", default: false });
-
-/// Check step for the `coverage-dump` bootstrap tool. The coverage-dump tool
-/// is used internally by coverage tests.
-///
-/// FIXME(Zalathar): This is temporarily separate from the other tool check
-/// steps so that it can use the stage 0 compiler instead of `top_stage`,
-/// without introducing conflicts with the stage 0 redesign (#119899).
-///
-/// After the stage 0 redesign lands, we can look into using the stage 0
-/// compiler to check all bootstrap tools (#139170).
-#[derive(Debug, Clone, PartialEq, Eq, Hash)]
-pub(crate) struct CoverageDump;
-
-impl CoverageDump {
-    const PATH: &str = "src/tools/coverage-dump";
-}
-
-impl Step for CoverageDump {
-    type Output = ();
-
-    /// Most contributors won't care about coverage-dump, so don't make their
-    /// check builds slower unless they opt in and check it explicitly.
-    const DEFAULT: bool = false;
-    const ONLY_HOSTS: bool = true;
-
-    fn should_run(run: ShouldRun<'_>) -> ShouldRun<'_> {
-        run.path(Self::PATH)
-    }
-
-    fn make_run(run: RunConfig<'_>) {
-        run.builder.ensure(Self {});
-    }
-
-    fn run(self, builder: &Builder<'_>) -> Self::Output {
-        // Make sure we haven't forgotten any fields, if there are any.
-        let Self {} = self;
-        let display_name = "coverage-dump";
-        let host = builder.config.host_target;
-        let target = host;
-        let mode = Mode::ToolBootstrap;
-
-        let compiler = builder.compiler(0, host);
-        let cargo = prepare_tool_cargo(
-            builder,
-            compiler,
-            mode,
-            target,
-            builder.kind,
-            Self::PATH,
-            SourceType::InTree,
-            &[],
-        );
-
-        let stamp = BuildStamp::new(&builder.cargo_out(compiler, mode, target))
-            .with_prefix(&format!("{display_name}-check"));
-
-        let _guard = builder.msg_tool(
-            builder.kind,
-            mode,
-            display_name,
-            compiler.stage,
-            &compiler.host,
-            &target,
-        );
-        run_cargo(builder, cargo, builder.config.free_args.clone(), &stamp, vec![], true, false);
-    }
-}
+tool_check_step!(RunMakeSupport {
+    path: "src/tools/run-make-support",
+    mode: Mode::ToolBootstrap,
+    default: false
+});
+
+tool_check_step!(CoverageDump {
+    path: "src/tools/coverage-dump",
+    mode: Mode::ToolBootstrap,
+    default: false
+});
diff --git a/src/bootstrap/src/core/build_steps/clippy.rs b/src/bootstrap/src/core/build_steps/clippy.rs
index 1e44b5b67a4..a0371eb7155 100644
--- a/src/bootstrap/src/core/build_steps/clippy.rs
+++ b/src/bootstrap/src/core/build_steps/clippy.rs
@@ -215,7 +215,7 @@ impl Step for Rustc {
                 builder.std(compiler, compiler.host);
                 builder.std(compiler, target);
             } else {
-                builder.ensure(check::Std::new(target));
+                builder.ensure(check::Std::new(compiler, target));
             }
         }
 
@@ -287,7 +287,7 @@ macro_rules! lint_any {
                 let target = self.target;
 
                 if !builder.download_rustc() {
-                    builder.ensure(check::Rustc::new(target, builder));
+                    builder.ensure(check::Rustc::new(builder, compiler, target));
                 };
 
                 let cargo = prepare_tool_cargo(
diff --git a/src/bootstrap/src/core/build_steps/compile.rs b/src/bootstrap/src/core/build_steps/compile.rs
index 431c242608b..3e2bdc2d6b5 100644
--- a/src/bootstrap/src/core/build_steps/compile.rs
+++ b/src/bootstrap/src/core/build_steps/compile.rs
@@ -1741,17 +1741,19 @@ fn copy_codegen_backends_to_sysroot(
         }
 
         let stamp = build_stamp::codegen_backend_stamp(builder, compiler, target, backend);
-        let dylib = t!(fs::read_to_string(stamp.path()));
-        let file = Path::new(&dylib);
-        let filename = file.file_name().unwrap().to_str().unwrap();
-        // change `librustc_codegen_cranelift-xxxxxx.so` to
-        // `librustc_codegen_cranelift-release.so`
-        let target_filename = {
-            let dash = filename.find('-').unwrap();
-            let dot = filename.find('.').unwrap();
-            format!("{}-{}{}", &filename[..dash], builder.rust_release(), &filename[dot..])
-        };
-        builder.copy_link(file, &dst.join(target_filename), FileType::NativeLibrary);
+        if stamp.path().exists() {
+            let dylib = t!(fs::read_to_string(stamp.path()));
+            let file = Path::new(&dylib);
+            let filename = file.file_name().unwrap().to_str().unwrap();
+            // change `librustc_codegen_cranelift-xxxxxx.so` to
+            // `librustc_codegen_cranelift-release.so`
+            let target_filename = {
+                let dash = filename.find('-').unwrap();
+                let dot = filename.find('.').unwrap();
+                format!("{}-{}{}", &filename[..dash], builder.rust_release(), &filename[dot..])
+            };
+            builder.copy_link(file, &dst.join(target_filename), FileType::NativeLibrary);
+        }
     }
 }
 
@@ -2162,6 +2164,25 @@ impl Step for Assemble {
                 continue; // Already built as part of rustc
             }
 
+            // FIXME: this is a horrible hack used to make `x check` work when other codegen
+            // backends are enabled.
+            // `x check` will check stage 1 rustc, which copies its rmetas to the stage0 sysroot.
+            // Then it checks codegen backends, which correctly use these rmetas.
+            // Then it needs to check std, but for that it needs to build stage 1 rustc.
+            // This copies the build rmetas into the stage0 sysroot, effectively poisoning it,
+            // because we then have both check and build rmetas in the same sysroot.
+            // That would be fine on its own. However, when another codegen backend is enabled,
+            // then building stage 1 rustc implies also building stage 1 codegen backend (even if
+            // it isn't used for anything). And since that tries to use the poisoned
+            // rmetas, it fails to build.
+            // We don't actually need to build rustc-private codegen backends for checking std,
+            // so instead we skip that.
+            // Note: this would be also an issue for other rustc-private tools, but that is "solved"
+            // by check::Std being last in the list of checked things (see
+            // `Builder::get_step_descriptions`).
+            if builder.kind == Kind::Check && builder.top_stage == 1 {
+                continue;
+            }
             builder.ensure(CodegenBackend {
                 compiler: build_compiler,
                 target: target_compiler.host,
diff --git a/src/bootstrap/src/core/build_steps/llvm.rs b/src/bootstrap/src/core/build_steps/llvm.rs
index de67a5f77e6..b2056f5cf37 100644
--- a/src/bootstrap/src/core/build_steps/llvm.rs
+++ b/src/bootstrap/src/core/build_steps/llvm.rs
@@ -263,6 +263,15 @@ impl Step for Llvm {
     }
 
     /// Compile LLVM for `target`.
+    #[cfg_attr(
+        feature = "tracing",
+        instrument(
+            level = "debug",
+            name = "Llvm::run",
+            skip_all,
+            fields(target = ?self.target),
+        ),
+    )]
     fn run(self, builder: &Builder<'_>) -> LlvmResult {
         let target = self.target;
         let target_native = if self.target.starts_with("riscv") {
diff --git a/src/bootstrap/src/core/build_steps/tool.rs b/src/bootstrap/src/core/build_steps/tool.rs
index ad3f8d89767..b05b34b9b22 100644
--- a/src/bootstrap/src/core/build_steps/tool.rs
+++ b/src/bootstrap/src/core/build_steps/tool.rs
@@ -77,7 +77,7 @@ impl Builder<'_> {
                 *target,
             ),
             // doesn't depend on compiler, same as host compiler
-            _ => self.msg(Kind::Build, build_stage, format_args!("tool {tool}"), *host, *target),
+            _ => self.msg(kind, build_stage, format_args!("tool {tool}"), *host, *target),
         }
     }
 }
diff --git a/src/bootstrap/src/core/builder/tests.rs b/src/bootstrap/src/core/builder/tests.rs
index 1c5267cb75e..1d5690a8197 100644
--- a/src/bootstrap/src/core/builder/tests.rs
+++ b/src/bootstrap/src/core/builder/tests.rs
@@ -1240,24 +1240,23 @@ mod snapshot {
             ctx.config("check")
                 .path("compiler")
                 .render_steps(), @r"
-        [check] std <host>
         [build] llvm <host>
-        [check] rustc <host>
-        [check] cranelift <host>
-        [check] gcc <host>
+        [check] rustc 0 <host> -> rustc 1 <host>
+        [check] rustc 0 <host> -> cranelift 1 <host>
+        [check] rustc 0 <host> -> gcc 1 <host>
         ");
 
         insta::assert_snapshot!(
             ctx.config("check")
                 .path("rustc")
                 .render_steps(), @r"
-        [check] std <host>
         [build] llvm <host>
-        [check] rustc <host>
+        [check] rustc 0 <host> -> rustc 1 <host>
         ");
     }
 
     #[test]
+    #[should_panic]
     fn check_compiler_stage_0() {
         let ctx = TestCtx::new();
         ctx.config("check").path("compiler").stage(0).run();
@@ -1272,11 +1271,9 @@ mod snapshot {
                 .stage(1)
                 .render_steps(), @r"
         [build] llvm <host>
-        [build] rustc 0 <host> -> rustc 1 <host>
-        [build] rustc 1 <host> -> std 1 <host>
-        [check] rustc <host>
-        [check] cranelift <host>
-        [check] gcc <host>
+        [check] rustc 0 <host> -> rustc 1 <host>
+        [check] rustc 0 <host> -> cranelift 1 <host>
+        [check] rustc 0 <host> -> gcc 1 <host>
         ");
     }
 
@@ -1291,11 +1288,9 @@ mod snapshot {
         [build] llvm <host>
         [build] rustc 0 <host> -> rustc 1 <host>
         [build] rustc 1 <host> -> std 1 <host>
-        [build] rustc 1 <host> -> rustc 2 <host>
-        [build] rustc 2 <host> -> std 2 <host>
-        [check] rustc <host>
-        [check] cranelift <host>
-        [check] gcc <host>
+        [check] rustc 1 <host> -> rustc 2 <host>
+        [check] rustc 1 <host> -> cranelift 2 <host>
+        [check] rustc 1 <host> -> gcc 2 <host>
         ");
     }
 
@@ -1304,30 +1299,24 @@ mod snapshot {
         let ctx = TestCtx::new();
         insta::assert_snapshot!(
             ctx.config("check")
-                .stage(2)
                 .targets(&[TEST_TRIPLE_1])
                 .hosts(&[TEST_TRIPLE_1])
                 .render_steps(), @r"
         [build] llvm <host>
         [build] rustc 0 <host> -> rustc 1 <host>
         [build] rustc 1 <host> -> std 1 <host>
-        [build] rustc 1 <host> -> rustc 2 <host>
-        [build] rustc 2 <host> -> std 2 <host>
         [build] rustc 1 <host> -> std 1 <target1>
-        [build] rustc 2 <host> -> std 2 <target1>
-        [check] rustc <target1>
-        [check] Rustdoc <target1>
-        [check] cranelift <target1>
-        [check] gcc <target1>
-        [check] Clippy <target1>
-        [check] Miri <target1>
-        [check] CargoMiri <target1>
-        [check] MiroptTestTools <target1>
-        [check] Rustfmt <target1>
-        [check] rust-analyzer <target1>
-        [check] TestFloatParse <target1>
-        [check] FeaturesStatusDump <target1>
-        [check] std <target1>
+        [check] rustc 1 <host> -> rustc 2 <target1>
+        [check] rustc 1 <host> -> Rustdoc 2 <target1>
+        [check] rustc 1 <host> -> cranelift 2 <target1>
+        [check] rustc 1 <host> -> gcc 2 <target1>
+        [check] rustc 1 <host> -> Clippy 2 <target1>
+        [check] rustc 1 <host> -> Miri 2 <target1>
+        [check] rustc 1 <host> -> CargoMiri 2 <target1>
+        [check] rustc 1 <host> -> Rustfmt 2 <target1>
+        [check] rustc 1 <host> -> rust-analyzer 2 <target1>
+        [check] rustc 1 <host> -> TestFloatParse 2 <target1>
+        [check] rustc 1 <host> -> std 1 <target1>
         ");
     }
 
@@ -1340,11 +1329,12 @@ mod snapshot {
                 .render_steps(), @r"
         [build] llvm <host>
         [build] rustc 0 <host> -> rustc 1 <host>
-        [check] std <host>
+        [check] rustc 1 <host> -> std 1 <host>
         ");
     }
 
     #[test]
+    #[should_panic]
     fn check_library_stage_0() {
         let ctx = TestCtx::new();
         ctx.config("check").path("library").stage(0).run();
@@ -1360,7 +1350,7 @@ mod snapshot {
                 .render_steps(), @r"
         [build] llvm <host>
         [build] rustc 0 <host> -> rustc 1 <host>
-        [check] std <host>
+        [check] rustc 1 <host> -> std 1 <host>
         ");
     }
 
@@ -1376,7 +1366,7 @@ mod snapshot {
         [build] rustc 0 <host> -> rustc 1 <host>
         [build] rustc 1 <host> -> std 1 <host>
         [build] rustc 1 <host> -> rustc 2 <host>
-        [check] std <host>
+        [check] rustc 2 <host> -> std 2 <host>
         ");
     }
 
@@ -1390,8 +1380,8 @@ mod snapshot {
                 .render_steps(), @r"
         [build] llvm <host>
         [build] rustc 0 <host> -> rustc 1 <host>
-        [check] std <target1>
-        [check] std <target2>
+        [check] rustc 1 <host> -> std 1 <target1>
+        [check] rustc 1 <host> -> std 1 <target2>
         ");
     }
 
@@ -1402,14 +1392,14 @@ mod snapshot {
             ctx.config("check")
                 .path("miri")
                 .render_steps(), @r"
-        [check] std <host>
         [build] llvm <host>
-        [check] rustc <host>
-        [check] Miri <host>
+        [check] rustc 0 <host> -> rustc 1 <host>
+        [check] rustc 0 <host> -> Miri 1 <host>
         ");
     }
 
     #[test]
+    #[should_panic]
     fn check_miri_stage_0() {
         let ctx = TestCtx::new();
         ctx.config("check").path("miri").stage(0).run();
@@ -1424,10 +1414,8 @@ mod snapshot {
                 .stage(1)
                 .render_steps(), @r"
         [build] llvm <host>
-        [build] rustc 0 <host> -> rustc 1 <host>
-        [build] rustc 1 <host> -> std 1 <host>
-        [check] rustc <host>
-        [check] Miri <host>
+        [check] rustc 0 <host> -> rustc 1 <host>
+        [check] rustc 0 <host> -> Miri 1 <host>
         ");
     }
 
@@ -1442,10 +1430,8 @@ mod snapshot {
         [build] llvm <host>
         [build] rustc 0 <host> -> rustc 1 <host>
         [build] rustc 1 <host> -> std 1 <host>
-        [build] rustc 1 <host> -> rustc 2 <host>
-        [build] rustc 2 <host> -> std 2 <host>
-        [check] rustc <host>
-        [check] Miri <host>
+        [check] rustc 1 <host> -> rustc 2 <host>
+        [check] rustc 1 <host> -> Miri 2 <host>
         ");
     }
 
@@ -1466,9 +1452,9 @@ mod snapshot {
                 .path("compiletest")
                 .args(&["--set", "build.compiletest-use-stage0-libtest=false"])
                 .render_steps(), @r"
-        [check] std <host>
         [build] llvm <host>
-        [check] rustc <host>
+        [build] rustc 0 <host> -> rustc 1 <host>
+        [build] rustc 1 <host> -> std 1 <host>
         [check] compiletest <host>
         ");
     }
@@ -1480,11 +1466,10 @@ mod snapshot {
             ctx.config("check")
                 .path("rustc_codegen_cranelift")
                 .render_steps(), @r"
-        [check] std <host>
         [build] llvm <host>
-        [check] rustc <host>
-        [check] cranelift <host>
-        [check] gcc <host>
+        [check] rustc 0 <host> -> rustc 1 <host>
+        [check] rustc 0 <host> -> cranelift 1 <host>
+        [check] rustc 0 <host> -> gcc 1 <host>
         ");
     }
 
@@ -1495,10 +1480,9 @@ mod snapshot {
             ctx.config("check")
                 .path("rust-analyzer")
                 .render_steps(), @r"
-        [check] std <host>
         [build] llvm <host>
-        [check] rustc <host>
-        [check] rust-analyzer <host>
+        [check] rustc 0 <host> -> rustc 1 <host>
+        [check] rustc 0 <host> -> rust-analyzer 1 <host>
         ");
     }
 
@@ -1508,12 +1492,7 @@ mod snapshot {
         insta::assert_snapshot!(
             ctx.config("check")
                 .path("run-make-support")
-                .render_steps(), @r"
-        [check] std <host>
-        [build] llvm <host>
-        [check] rustc <host>
-        [check] RunMakeSupport <host>
-        ");
+                .render_steps(), @"[check] rustc 0 <host> -> RunMakeSupport 1 <host>");
     }
 
     #[test]
diff --git a/src/bootstrap/src/core/config/config.rs b/src/bootstrap/src/core/config/config.rs
index 49b3fec4c35..59693dc3e4c 100644
--- a/src/bootstrap/src/core/config/config.rs
+++ b/src/bootstrap/src/core/config/config.rs
@@ -1025,7 +1025,7 @@ impl Config {
             || bench_stage.is_some();
 
         config.stage = match config.cmd {
-            Subcommand::Check { .. } => flags_stage.or(check_stage).unwrap_or(0),
+            Subcommand::Check { .. } => flags_stage.or(check_stage).unwrap_or(1),
             Subcommand::Clippy { .. } | Subcommand::Fix => flags_stage.or(check_stage).unwrap_or(1),
             // `download-rustc` only has a speed-up for stage2 builds. Default to stage2 unless explicitly overridden.
             Subcommand::Doc { .. } => {
@@ -1052,9 +1052,16 @@ impl Config {
         };
 
         // Now check that the selected stage makes sense, and if not, print a warning and end
-        if let (0, Subcommand::Build) = (config.stage, &config.cmd) {
-            eprintln!("WARNING: cannot build anything on stage 0. Use at least stage 1.");
-            exit!(1);
+        match (config.stage, &config.cmd) {
+            (0, Subcommand::Build) => {
+                eprintln!("WARNING: cannot build anything on stage 0. Use at least stage 1.");
+                exit!(1);
+            }
+            (0, Subcommand::Check { .. }) => {
+                eprintln!("WARNING: cannot check anything on stage 0. Use at least stage 1.");
+                exit!(1);
+            }
+            _ => {}
         }
 
         // CI should always run stage 2 builds, unless it specifically states otherwise
diff --git a/src/bootstrap/src/utils/change_tracker.rs b/src/bootstrap/src/utils/change_tracker.rs
index f5a958618f9..aa968ea3d98 100644
--- a/src/bootstrap/src/utils/change_tracker.rs
+++ b/src/bootstrap/src/utils/change_tracker.rs
@@ -451,4 +451,9 @@ pub const CONFIG_CHANGE_HISTORY: &[ChangeInfo] = &[
         severity: ChangeSeverity::Warning,
         summary: "The `spellcheck:fix` tidy extra check argument has been removed, use `--bless` instead",
     },
+    ChangeInfo {
+        change_id: 143048,
+        severity: ChangeSeverity::Warning,
+        summary: "The default check stage has been changed to 1. It is no longer possible to `x check` with stage 0. All check commands have to be on stage 1+. Bootstrap tools can now also only be checked for the host target.",
+    },
 ];
diff --git a/src/ci/docker/host-x86_64/pr-check-1/Dockerfile b/src/ci/docker/host-x86_64/pr-check-1/Dockerfile
index 7a8056b70dc..8bbcc18e2be 100644
--- a/src/ci/docker/host-x86_64/pr-check-1/Dockerfile
+++ b/src/ci/docker/host-x86_64/pr-check-1/Dockerfile
@@ -45,11 +45,11 @@ COPY host-x86_64/pr-check-1/validate-toolstate.sh /scripts/
 # We also skip the x86_64-unknown-linux-gnu target as it is well-tested by other jobs.
 ENV SCRIPT \
   /scripts/check-default-config-profiles.sh && \
-  python3 ../x.py build --stage 1 src/tools/build-manifest && \
+  python3 ../x.py build src/tools/build-manifest && \
   python3 ../x.py test --stage 0 src/tools/compiletest && \
   python3 ../x.py check compiletest --set build.compiletest-use-stage0-libtest=true && \
-  python3 ../x.py check --stage 1 --target=i686-pc-windows-gnu --host=i686-pc-windows-gnu && \
-  python3 ../x.py check --stage 1 --set build.optimized-compiler-builtins=false core alloc std --target=aarch64-unknown-linux-gnu,i686-pc-windows-msvc,i686-unknown-linux-gnu,x86_64-apple-darwin,x86_64-pc-windows-gnu,x86_64-pc-windows-msvc && \
+  python3 ../x.py check --target=i686-pc-windows-gnu --host=i686-pc-windows-gnu && \
+  python3 ../x.py check --set build.optimized-compiler-builtins=false core alloc std --target=aarch64-unknown-linux-gnu,i686-pc-windows-msvc,i686-unknown-linux-gnu,x86_64-apple-darwin,x86_64-pc-windows-gnu,x86_64-pc-windows-msvc && \
   /scripts/validate-toolstate.sh && \
   reuse --include-submodules lint && \
   python3 ../x.py test collect-license-metadata && \