Auto merge of #145663 - Kobzol:bootstrap-test, r=jieyouxu

Enforce in bootstrap that test must have stage at least 1 (except for compiletest)

This PR cleans up a bunch of test steps and adds metadata to them. I didn't yet touch the most complicated step (`CompileTest`), I'm leaving that for another PR.

Testing anything on stage 0 is only possible for compiletest and with `build.allow-compiletest-stage0`. Testing anything else on stage 0 will either produce a nice error or crash with a stage being subtracted below zero.

r? `@jieyouxu`

try-job: dist-x86_64-linux
try-job: aarch64-gnu
try-job: arm-android
try-job: `x86_64-gnu-llvm-20*`
try-job: `x86_64-msvc-*`
try-job: aarch64-apple
try-job: test-various
This commit is contained in:
bors 2025-09-01 01:30:27 +00:00
commit 828e45ad11
13 changed files with 764 additions and 388 deletions

View file

@ -2309,10 +2309,10 @@ declare_lint! {
/// ### Example
///
/// ```rust
/// #![feature(sanitize)]
/// #![cfg_attr(not(bootstrap), feature(sanitize))]
///
/// #[inline(always)]
/// #[sanitize(address = "off")]
/// #[cfg_attr(not(bootstrap), sanitize(address = "off"))]
/// fn x() {}
///
/// fn main() {
@ -4832,13 +4832,16 @@ declare_lint! {
///
/// ### Example
///
/// ```rust,compile_fail
#[cfg_attr(not(bootstrap), doc = "```rust,compile_fail")]
#[cfg_attr(bootstrap, doc = "```rust")]
/// #![doc = in_root!()]
///
/// macro_rules! in_root { () => { "" } }
///
/// fn main() {}
/// ```
#[cfg_attr(not(bootstrap), doc = "```")]
#[cfg_attr(bootstrap, doc = "```")]
// ^ Needed to avoid tidy warning about odd number of backticks
///
/// {{produces}}
///

View file

@ -8,8 +8,8 @@ use crate::core::build_steps::compile::{
};
use crate::core::build_steps::tool;
use crate::core::build_steps::tool::{
COMPILETEST_ALLOW_FEATURES, SourceType, ToolTargetBuildMode, get_tool_target_compiler,
prepare_tool_cargo,
COMPILETEST_ALLOW_FEATURES, SourceType, TEST_FLOAT_PARSE_ALLOW_FEATURES, ToolTargetBuildMode,
get_tool_target_compiler, prepare_tool_cargo,
};
use crate::core::builder::{
self, Alias, Builder, Cargo, Kind, RunConfig, ShouldRun, Step, StepMetadata, crate_description,
@ -791,7 +791,7 @@ tool_check_step!(MiroptTestTools {
tool_check_step!(TestFloatParse {
path: "src/tools/test-float-parse",
mode: |_builder| Mode::ToolStd,
allow_features: tool::TestFloatParse::ALLOW_FEATURES
allow_features: TEST_FLOAT_PARSE_ALLOW_FEATURES
});
tool_check_step!(FeaturesStatusDump {
path: "src/tools/features-status-dump",

View file

@ -791,7 +791,11 @@ fn doc_std(
}
/// Prepare a compiler that will be able to document something for `target` at `stage`.
fn prepare_doc_compiler(builder: &Builder<'_>, target: TargetSelection, stage: u32) -> Compiler {
pub fn prepare_doc_compiler(
builder: &Builder<'_>,
target: TargetSelection,
stage: u32,
) -> Compiler {
assert!(stage > 0, "Cannot document anything in stage 0");
let build_compiler = builder.compiler(stage - 1, builder.host_target);
builder.std(build_compiler, target);
@ -1289,6 +1293,8 @@ impl Step for RustcBook {
// functional sysroot.
builder.std(self.build_compiler, self.target);
let mut cmd = builder.tool_cmd(Tool::LintDocs);
cmd.arg("--build-rustc-stage");
cmd.arg(self.build_compiler.stage.to_string());
cmd.arg("--src");
cmd.arg(builder.src.join("compiler"));
cmd.arg("--out");

File diff suppressed because it is too large Load diff

View file

@ -1539,42 +1539,7 @@ tool_rustc_extended!(Rustfmt {
add_bins_to_sysroot: ["rustfmt"]
});
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct TestFloatParse {
pub host: TargetSelection,
}
impl TestFloatParse {
pub const ALLOW_FEATURES: &'static str = "f16,cfg_target_has_reliable_f16_f128";
}
impl Step for TestFloatParse {
type Output = ToolBuildResult;
const IS_HOST: bool = true;
const DEFAULT: bool = false;
fn should_run(run: ShouldRun<'_>) -> ShouldRun<'_> {
run.path("src/tools/test-float-parse")
}
fn run(self, builder: &Builder<'_>) -> ToolBuildResult {
let bootstrap_host = builder.config.host_target;
let compiler = builder.compiler(builder.top_stage, bootstrap_host);
builder.ensure(ToolBuild {
build_compiler: compiler,
target: bootstrap_host,
tool: "test-float-parse",
mode: Mode::ToolStd,
path: "src/tools/test-float-parse",
source_type: SourceType::InTree,
extra_features: Vec::new(),
allow_features: Self::ALLOW_FEATURES,
cargo_args: Vec::new(),
artifact_kind: ToolArtifactKind::Binary,
})
}
}
pub const TEST_FLOAT_PARSE_ALLOW_FEATURES: &str = "f16,cfg_target_has_reliable_f16_f128";
impl Builder<'_> {
/// Gets a `BootstrapCommand` which is ready to run `tool` in `stage` built for

View file

@ -295,7 +295,7 @@ pub fn crate_description(crates: &[impl AsRef<str>]) -> String {
return "".into();
}
let mut descr = String::from(" {");
let mut descr = String::from("{");
descr.push_str(crates[0].as_ref());
for krate in &crates[1..] {
descr.push_str(", ");

View file

@ -2037,6 +2037,212 @@ mod snapshot {
.render_steps(), @"[check] rustc 0 <host> -> RunMakeSupport 1 <host>");
}
fn prepare_test_config(ctx: &TestCtx) -> ConfigBuilder {
ctx.config("test")
// Bootstrap only runs by default on CI, so we have to emulate that also locally.
.args(&["--ci", "true"])
// These rustdoc tests requires nodejs to be present.
// We can't easily opt out of it, so if it is present on the local PC, the test
// would have different result on CI, where nodejs might be missing.
.args(&["--skip", "rustdoc-js-std"])
.args(&["--skip", "rustdoc-js"])
.args(&["--skip", "rustdoc-gui"])
}
#[test]
fn test_all_stage_1() {
let ctx = TestCtx::new();
insta::assert_snapshot!(
prepare_test_config(&ctx)
.render_steps(), @r"
[build] rustc 0 <host> -> Tidy 1 <host>
[test] tidy <>
[build] rustdoc 0 <host>
[build] llvm <host>
[build] rustc 0 <host> -> rustc 1 <host>
[build] rustc 1 <host> -> std 1 <host>
[build] rustc 0 <host> -> Compiletest 1 <host>
[test] Ui <host>
[test] Crashes <host>
[build] rustc 0 <host> -> CoverageDump 1 <host>
[build] rustc 1 <host> -> std 1 <host>
[test] CodegenLlvm <host>
[test] CodegenUnits <host>
[test] AssemblyLlvm <host>
[test] Incremental <host>
[test] Debuginfo <host>
[test] UiFullDeps <host>
[build] rustdoc 1 <host>
[test] Rustdoc <host>
[test] CoverageRunRustdoc <host>
[test] Pretty <host>
[build] rustc 1 <host> -> std 1 <host>
[build] rustc 0 <host> -> std 0 <host>
[test] rustc 0 <host> -> CrateLibrustc 1 <host>
[build] rustc 1 <host> -> rustc 2 <host>
[test] crate-bootstrap <host> src/tools/coverage-dump
[test] crate-bootstrap <host> src/tools/jsondoclint
[test] crate-bootstrap <host> src/tools/replace-version-placeholder
[test] crate-bootstrap <host> tidyselftest
[build] rustc 0 <host> -> UnstableBookGen 1 <host>
[build] rustc 0 <host> -> Rustbook 1 <host>
[doc] unstable-book (book) <host>
[doc] book (book) <host>
[doc] book/first-edition (book) <host>
[doc] book/second-edition (book) <host>
[doc] book/2018-edition (book) <host>
[doc] rustc 0 <host> -> standalone 1 <host>
[doc] rustc 1 <host> -> std 1 <host> crates=[alloc,compiler_builtins,core,panic_abort,panic_unwind,proc_macro,rustc-std-workspace-core,std,std_detect,sysroot,test,unwind]
[build] rustc 0 <host> -> error-index 1 <host>
[doc] rustc 0 <host> -> error-index 1 <host>
[doc] nomicon (book) <host>
[doc] rustc 1 <host> -> reference (book) 2 <host>
[doc] rustdoc (book) <host>
[doc] rust-by-example (book) <host>
[build] rustc 0 <host> -> LintDocs 1 <host>
[doc] rustc (book) <host>
[doc] cargo (book) <host>
[doc] clippy (book) <host>
[doc] embedded-book (book) <host>
[doc] edition-guide (book) <host>
[doc] style-guide (book) <host>
[doc] rustc 0 <host> -> releases 1 <host>
[build] rustc 0 <host> -> Linkchecker 1 <host>
[test] link-check <host>
[test] tier-check <host>
[test] rustc 0 <host> -> rust-analyzer 1 <host>
[build] rustc 0 <host> -> RustdocTheme 1 <host>
[test] rustdoc-theme 1 <host>
[test] RustdocUi <host>
[build] rustc 0 <host> -> JsonDocCk 1 <host>
[build] rustc 0 <host> -> JsonDocLint 1 <host>
[test] RustdocJson <host>
[doc] rustc 0 <host> -> rustc 1 <host>
[build] rustc 0 <host> -> HtmlChecker 1 <host>
[test] html-check <host>
[build] rustc 0 <host> -> RunMakeSupport 1 <host>
[build] rustc 1 <host> -> cargo 2 <host>
[test] RunMake <host>
");
}
#[test]
fn test_all_stage_2() {
let ctx = TestCtx::new();
insta::assert_snapshot!(
prepare_test_config(&ctx)
.stage(2)
.render_steps(), @r"
[build] rustc 0 <host> -> Tidy 1 <host>
[test] tidy <>
[build] rustdoc 0 <host>
[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 0 <host> -> Compiletest 1 <host>
[test] Ui <host>
[test] Crashes <host>
[build] rustc 0 <host> -> CoverageDump 1 <host>
[build] rustc 2 <host> -> std 2 <host>
[test] CodegenLlvm <host>
[test] CodegenUnits <host>
[test] AssemblyLlvm <host>
[test] Incremental <host>
[test] Debuginfo <host>
[build] rustc 2 <host> -> rustc 3 <host>
[test] UiFullDeps <host>
[build] rustdoc 2 <host>
[test] Rustdoc <host>
[test] CoverageRunRustdoc <host>
[test] Pretty <host>
[build] rustc 2 <host> -> std 2 <host>
[build] rustc 1 <host> -> std 1 <host>
[build] rustdoc 1 <host>
[test] rustc 1 <host> -> CrateLibrustc 2 <host>
[test] crate-bootstrap <host> src/tools/coverage-dump
[test] crate-bootstrap <host> src/tools/jsondoclint
[test] crate-bootstrap <host> src/tools/replace-version-placeholder
[test] crate-bootstrap <host> tidyselftest
[build] rustc 0 <host> -> UnstableBookGen 1 <host>
[build] rustc 0 <host> -> Rustbook 1 <host>
[doc] unstable-book (book) <host>
[doc] book (book) <host>
[doc] book/first-edition (book) <host>
[doc] book/second-edition (book) <host>
[doc] book/2018-edition (book) <host>
[doc] rustc 1 <host> -> standalone 2 <host>
[doc] rustc 1 <host> -> std 1 <host> crates=[alloc,compiler_builtins,core,panic_abort,panic_unwind,proc_macro,rustc-std-workspace-core,std,std_detect,sysroot,test,unwind]
[build] rustc 1 <host> -> error-index 2 <host>
[doc] rustc 1 <host> -> error-index 2 <host>
[doc] nomicon (book) <host>
[doc] rustc 1 <host> -> reference (book) 2 <host>
[doc] rustdoc (book) <host>
[doc] rust-by-example (book) <host>
[build] rustc 0 <host> -> LintDocs 1 <host>
[doc] rustc (book) <host>
[doc] cargo (book) <host>
[doc] clippy (book) <host>
[doc] embedded-book (book) <host>
[doc] edition-guide (book) <host>
[doc] style-guide (book) <host>
[doc] rustc 1 <host> -> releases 2 <host>
[build] rustc 0 <host> -> Linkchecker 1 <host>
[test] link-check <host>
[test] tier-check <host>
[test] rustc 1 <host> -> rust-analyzer 2 <host>
[doc] rustc (book) <host>
[test] rustc 1 <host> -> lint-docs 2 <host>
[build] rustc 0 <host> -> RustdocTheme 1 <host>
[test] rustdoc-theme 2 <host>
[test] RustdocUi <host>
[build] rustc 0 <host> -> JsonDocCk 1 <host>
[build] rustc 0 <host> -> JsonDocLint 1 <host>
[test] RustdocJson <host>
[doc] rustc 1 <host> -> rustc 2 <host>
[build] rustc 0 <host> -> HtmlChecker 1 <host>
[test] html-check <host>
[build] rustc 0 <host> -> RunMakeSupport 1 <host>
[build] rustc 2 <host> -> cargo 3 <host>
[test] RunMake <host>
");
}
#[test]
fn test_compiler_stage_1() {
let ctx = TestCtx::new();
insta::assert_snapshot!(
ctx.config("test")
.path("compiler")
.stage(1)
.render_steps(), @r"
[build] llvm <host>
[build] rustc 0 <host> -> rustc 1 <host>
[build] rustc 0 <host> -> std 0 <host>
[build] rustdoc 0 <host>
[test] rustc 0 <host> -> CrateLibrustc 1 <host>
");
}
#[test]
fn test_compiler_stage_2() {
let ctx = TestCtx::new();
insta::assert_snapshot!(
ctx.config("test")
.path("compiler")
.stage(2)
.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 1 <host> -> std 1 <host>
[build] rustdoc 1 <host>
[test] rustc 1 <host> -> CrateLibrustc 2 <host>
");
}
#[test]
fn test_exclude() {
let ctx = TestCtx::new();
@ -2054,13 +2260,15 @@ mod snapshot {
let get_steps = |args: &[&str]| ctx.config("test").args(args).get_steps();
let rustc_metadata =
|| StepMetadata::test("CrateLibrustc", host).built_by(Compiler::new(0, host));
// Ensure our test is valid, and `test::Rustc` would be run without the exclude.
get_steps(&[]).assert_contains(StepMetadata::test("CrateLibrustc", host));
get_steps(&[]).assert_contains(rustc_metadata());
let steps = get_steps(&["--skip", "compiler/rustc_data_structures"]);
// Ensure tests for rustc are not skipped.
steps.assert_contains(StepMetadata::test("CrateLibrustc", host));
steps.assert_contains(rustc_metadata());
steps.assert_contains_fuzzy(StepMetadata::build("rustc", host));
}
@ -2077,6 +2285,7 @@ mod snapshot {
[build] rustc 1 <host> -> std 1 <host>
[build] rustdoc 1 <host>
[build] rustdoc 0 <host>
[test] rustc 0 <host> -> cargo 1 <host>
");
}
@ -2096,6 +2305,7 @@ mod snapshot {
[build] rustc 2 <host> -> std 2 <host>
[build] rustdoc 2 <host>
[build] rustdoc 1 <host>
[test] rustc 1 <host> -> cargo 2 <host>
");
}
@ -2116,6 +2326,19 @@ mod snapshot {
");
}
#[test]
fn test_tier_check() {
let ctx = TestCtx::new();
insta::assert_snapshot!(
ctx.config("test")
.path("tier-check")
.render_steps(), @r"
[build] llvm <host>
[build] rustc 0 <host> -> rustc 1 <host>
[test] tier-check <host>
");
}
#[test]
fn doc_all() {
let ctx = TestCtx::new();

View file

@ -1002,13 +1002,17 @@ impl Config {
(0, Subcommand::Install) => {
check_stage0("install");
}
(0, Subcommand::Test { .. }) if build_compiletest_allow_stage0 != Some(true) => {
eprintln!(
"ERROR: cannot test anything on stage 0. Use at least stage 1. If you want to run compiletest with an external stage0 toolchain, enable `build.compiletest-allow-stage0`."
);
exit!(1);
}
_ => {}
}
if flags_compile_time_deps && !matches!(flags_cmd, Subcommand::Check { .. }) {
eprintln!(
"WARNING: Can't use --compile-time-deps with any subcommand other than check."
);
eprintln!("ERROR: Can't use --compile-time-deps with any subcommand other than check.");
exit!(1);
}

View file

@ -1144,14 +1144,18 @@ impl Build {
};
let action = action.into().description();
let msg = |fmt| format!("{action} stage{actual_stage} {what}{fmt}");
let what = what.to_string();
let msg = |fmt| {
let space = if !what.is_empty() { " " } else { "" };
format!("{action} stage{actual_stage} {what}{space}{fmt}")
};
let msg = if let Some(target) = target.into() {
let build_stage = host_and_stage.stage;
let host = host_and_stage.host;
if host == target {
msg(format_args!(" (stage{build_stage} -> stage{actual_stage}, {target})"))
msg(format_args!("(stage{build_stage} -> stage{actual_stage}, {target})"))
} else {
msg(format_args!(" (stage{build_stage}:{host} -> stage{actual_stage}:{target})"))
msg(format_args!("(stage{build_stage}:{host} -> stage{actual_stage}:{target})"))
}
} else {
msg(format_args!(""))
@ -1159,6 +1163,25 @@ impl Build {
self.group(&msg)
}
/// Return a `Group` guard for a [`Step`] that tests `what` with the given `stage` and `target`
/// (determined by `host_and_stage`).
/// Use this instead of [`Build::msg`] when there is no clear `build_compiler` to be
/// determined.
///
/// [`Step`]: crate::core::builder::Step
#[must_use = "Groups should not be dropped until the Step finishes running"]
#[track_caller]
fn msg_test(
&self,
what: impl Display,
host_and_stage: impl Into<HostAndStage>,
) -> Option<gha::Group> {
let HostAndStage { host, stage } = host_and_stage.into();
let action = Kind::Test.description();
let msg = format!("{action} stage{stage} {what} ({host})");
self.group(&msg)
}
/// Return a `Group` guard for a [`Step`] that is only built once and isn't affected by `--stage`.
///
/// [`Step`]: crate::core::builder::Step

View file

@ -531,4 +531,9 @@ pub const CONFIG_CHANGE_HISTORY: &[ChangeInfo] = &[
severity: ChangeSeverity::Info,
summary: "It is now possible to `check/build/dist` the standard stage 0 library if you use a stage0 rustc built from in-tree sources. This is useful for quickly cross-compiling the standard library. You have to enable build.local-rebuild for this to work.",
},
ChangeInfo {
change_id: 145663,
severity: ChangeSeverity::Warning,
summary: "It is no longer possible to `x test` with stage 0, except for running compiletest and opting into `build.compiletest-allow-stage0`.",
},
];

View file

@ -44,5 +44,5 @@ RUN bash -c 'npm install -g eslint@$(cat /tmp/eslint.version)'
# NOTE: intentionally uses python2 for x.py so we can test it still works.
# validate-toolstate only runs in our CI, so it's ok for it to only support python3.
ENV SCRIPT TIDY_PRINT_DIFF=1 python2.7 ../x.py test --stage 0 \
ENV SCRIPT TIDY_PRINT_DIFF=1 python2.7 ../x.py test \
src/tools/tidy tidyselftest --extra-checks=py,cpp,js,spellcheck

View file

@ -59,6 +59,8 @@ pub struct LintExtractor<'a> {
pub rustc_target: &'a str,
/// The target linker overriding `rustc`'s default
pub rustc_linker: Option<&'a str>,
/// Stage of the compiler that builds the docs (the stage of `rustc_path`).
pub build_rustc_stage: u32,
/// Verbose output.
pub verbose: bool,
/// Validate the style and the code example.
@ -216,14 +218,7 @@ impl<'a> LintExtractor<'a> {
if let Some(text) = line.strip_prefix("/// ") {
doc_lines.push(text.to_string());
} else if let Some(text) = line.strip_prefix("#[doc = \"") {
let escaped = text.strip_suffix("\"]").unwrap();
let mut buf = String::new();
unescape_str(escaped, |_, res| match res {
Ok(c) => buf.push(c),
Err(err) => {
assert!(!err.is_fatal(), "failed to unescape string literal")
}
});
let buf = parse_doc_string(text);
doc_lines.push(buf);
} else if line == "///" {
doc_lines.push("".to_string());
@ -234,6 +229,20 @@ impl<'a> LintExtractor<'a> {
// Ignore allow of lints (useful for
// invalid_rust_codeblocks).
continue;
} else if let Some(text) =
line.strip_prefix("#[cfg_attr(not(bootstrap), doc = \"")
{
if self.build_rustc_stage >= 1 {
let buf = parse_doc_string(text);
doc_lines.push(buf);
}
} else if let Some(text) =
line.strip_prefix("#[cfg_attr(bootstrap, doc = \"")
{
if self.build_rustc_stage == 0 {
let buf = parse_doc_string(text);
doc_lines.push(buf);
}
} else {
let name = lint_name(line).map_err(|e| {
format!(
@ -580,6 +589,23 @@ impl<'a> LintExtractor<'a> {
}
}
/// Parses a doc string that follows `#[doc = "`.
fn parse_doc_string(text: &str) -> String {
let escaped = text.strip_suffix("]").unwrap_or(text);
let escaped = escaped.strip_suffix(")").unwrap_or(escaped).strip_suffix("\"");
let Some(escaped) = escaped else {
panic!("Cannot extract docstring content from {text}");
};
let mut buf = String::new();
unescape_str(escaped, |_, res| match res {
Ok(c) => buf.push(c),
Err(err) => {
assert!(!err.is_fatal(), "failed to unescape string literal")
}
});
buf
}
/// Adds `Lint`s that have been renamed.
fn add_renamed_lints(lints: &mut Vec<Lint>) {
for (level, names) in RENAMES {

View file

@ -25,6 +25,7 @@ fn doit() -> Result<(), Box<dyn Error>> {
let mut args = std::env::args().skip(1);
let mut src_path = None;
let mut out_path = None;
let mut build_rustc_stage = None;
let mut rustc_path = None;
let mut rustc_target = None;
let mut rustc_linker = None;
@ -32,6 +33,14 @@ fn doit() -> Result<(), Box<dyn Error>> {
let mut validate = false;
while let Some(arg) = args.next() {
match arg.as_str() {
"--build-rustc-stage" => {
build_rustc_stage = match args.next() {
Some(s) => {
Some(s.parse::<u32>().expect("build rustc stage has to be an integer"))
}
None => return Err("--build-rustc-stage requires a value".into()),
};
}
"--src" => {
src_path = match args.next() {
Some(s) => Some(PathBuf::from(s)),
@ -67,6 +76,9 @@ fn doit() -> Result<(), Box<dyn Error>> {
s => return Err(format!("unexpected argument `{}`", s).into()),
}
}
if build_rustc_stage.is_none() {
return Err("--build-rustc-stage must be specified to the stage of the compiler that generates the docs".into());
}
if src_path.is_none() {
return Err("--src must be specified to the directory with the compiler source".into());
}
@ -85,6 +97,7 @@ fn doit() -> Result<(), Box<dyn Error>> {
rustc_path: &rustc_path.unwrap(),
rustc_target: &rustc_target.unwrap(),
rustc_linker: rustc_linker.as_deref(),
build_rustc_stage: build_rustc_stage.unwrap(),
verbose,
validate,
};