diff --git a/src/bootstrap/src/core/build_steps/test.rs b/src/bootstrap/src/core/build_steps/test.rs index 44bedaf878e04..242495c817f5a 100644 --- a/src/bootstrap/src/core/build_steps/test.rs +++ b/src/bootstrap/src/core/build_steps/test.rs @@ -13,6 +13,8 @@ use std::path::{Path, PathBuf}; use std::process::Command; use std::{env, fs, iter}; +use build_helper::git::get_closest_upstream_commit; + use crate::core::build_steps::compile::{ArtifactKeepMode, Std, run_cargo}; use crate::core::build_steps::doc::{DocumentationFormat, prepare_doc_compiler}; use crate::core::build_steps::gcc::{Gcc, GccTargetPair, add_cg_gcc_cargo_flags}; @@ -4622,3 +4624,90 @@ impl Step for RemoteTestClientTests { ); } } + +fn check_if_cargo_semver_checks_is_installed(builder: &Builder<'_>) -> bool { + command("cargo") + .allow_failure() + .arg("semver-checks") + .arg("--version") + // Cache the output to avoid running this command more than once (per builder). + .cached() + .run_capture_stdout(builder) + .is_success() +} + +/// Run cargo-semver-checks on the standard library and compare its API +/// versus a previous baseline, using rustdoc JSON data. +/// +/// Fails if a semver-breaking change is detected. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct StdSemverCheck { + build_compiler: Compiler, + target: TargetSelection, + /// The baseline commit that we are comparing the local stdlib API against. + commit: String, +} + +impl Step for StdSemverCheck { + type Output = (); + + fn should_run(run: ShouldRun<'_>) -> ShouldRun<'_> { + run.alias("std-semver-check") + } + + fn make_run(run: RunConfig<'_>) { + if !check_if_cargo_semver_checks_is_installed(run.builder) { + panic!("cargo-semver-checks was not found, please install it"); + } + + let baseline_commit = match get_closest_upstream_commit( + Some(&run.builder.config.src), + &run.builder.config.git_config(), + run.builder.config.ci_env, + ) { + Ok(Some(commit)) => commit, + Ok(None) => { + panic!("No baseline parent commit found for std-semver-check"); + } + Err(error) => { + panic!("Cannot get baseline parent commit for std-semver-check: {error:?}"); + } + }; + + run.builder.ensure(Self { + build_compiler: run.builder.compiler_for_std(run.builder.top_stage), + target: run.target, + commit: baseline_commit, + }); + } + + fn run(self, builder: &Builder<'_>) { + let Some(docs_dir) = builder.config.download_std_json_docs(self.target, &self.commit) + else { + return; + }; + + let directory = builder.ensure(crate::core::build_steps::doc::Std::from_build_compiler( + self.build_compiler, + self.target, + DocumentationFormat::Json, + )); + let baseline_dir = docs_dir.join("share").join("doc").join("rust").join("json"); + + for library in ["core", "alloc", "std"] { + println!("Checking semver compatibility of {library}"); + let mut cmd = command("cargo"); + cmd.arg("semver-checks") + .arg("-Z") + .arg("unstable-options") + .arg("--stability-aware") + .arg("--release-type") + .arg("minor") + .arg("--current-rustdoc") + .arg(directory.join(format!("{library}.json"))) + .arg("--baseline-rustdoc") + .arg(baseline_dir.join(format!("{library}.json"))); + cmd.run(builder); + } + } +} diff --git a/src/bootstrap/src/core/builder/mod.rs b/src/bootstrap/src/core/builder/mod.rs index 02f18b43b5ba1..84cae83c03096 100644 --- a/src/bootstrap/src/core/builder/mod.rs +++ b/src/bootstrap/src/core/builder/mod.rs @@ -962,6 +962,7 @@ impl<'a> Builder<'a> { test::RunMake, test::RunMakeCargo, test::BuildStd, + test::StdSemverCheck, test::IntrinsicTest, ), Kind::Miri => describe!(test::Crate), diff --git a/src/bootstrap/src/core/download.rs b/src/bootstrap/src/core/download.rs index d19c928e7c553..5d7b6513a5838 100644 --- a/src/bootstrap/src/core/download.rs +++ b/src/bootstrap/src/core/download.rs @@ -173,6 +173,30 @@ impl Config { ); } + pub(crate) fn download_std_json_docs( + &self, + target: TargetSelection, + commit: &str, + ) -> Option { + if self.dry_run() { + return None; + } + + self.do_if_verbose(|| println!("using downloaded std json docs from CI (commit {commit})")); + + let version = self.artifact_version_part(commit); + download_component( + DownloadContext::from(self), + &self.out, + DownloadSource::CI, + format!("rust-docs-json-{version}-{target}.tar.xz"), + "rust-docs-json-preview", + // When using DownloadSource::CI, the key is assumed to end with -llvm-assertions + &format!("{commit}-{}", self.llvm_assertions), + "ci-docs-json", + ) + } + fn download_toolchain( &self, version: &str, @@ -784,11 +808,11 @@ fn download_component<'a>( prefix: &str, key: &str, destination: &str, -) { +) -> Option { let dwn_ctx = dwn_ctx.as_ref(); if dwn_ctx.exec_ctx.dry_run() { - return; + return None; } let cache_dst = @@ -833,8 +857,7 @@ fn download_component<'a>( let sha256 = dwn_ctx.stage0_metadata.checksums_sha256.get(&url).expect(&error); if tarball.exists() { if verify(dwn_ctx.exec_ctx, &tarball, sha256) { - unpack(dwn_ctx.exec_ctx, &tarball, &bin_root, prefix); - return; + return Some(unpack(dwn_ctx.exec_ctx, &tarball, &bin_root, prefix)); } else { dwn_ctx.exec_ctx.do_if_verbose(|| { println!( @@ -847,8 +870,7 @@ fn download_component<'a>( } Some(sha256) } else if tarball.exists() { - unpack(dwn_ctx.exec_ctx, &tarball, &bin_root, prefix); - return; + return Some(unpack(dwn_ctx.exec_ctx, &tarball, &bin_root, prefix)); } else { None }; @@ -871,7 +893,7 @@ download-rustc = false panic!("failed to verify {}", tarball.display()); } - unpack(dwn_ctx.exec_ctx, &tarball, &bin_root, prefix); + Some(unpack(dwn_ctx.exec_ctx, &tarball, &bin_root, prefix)) } pub(crate) fn verify(exec_ctx: &ExecutionContext, path: &Path, expected: &str) -> bool { @@ -915,7 +937,7 @@ pub(crate) fn verify(exec_ctx: &ExecutionContext, path: &Path, expected: &str) - verified } -fn unpack(exec_ctx: &ExecutionContext, tarball: &Path, dst: &Path, pattern: &str) { +fn unpack(exec_ctx: &ExecutionContext, tarball: &Path, dst: &Path, pattern: &str) -> PathBuf { eprintln!("extracting {} to {}", tarball.display(), dst.display()); if !dst.exists() { t!(fs::create_dir_all(dst)); @@ -978,6 +1000,7 @@ fn unpack(exec_ctx: &ExecutionContext, tarball: &Path, dst: &Path, pattern: &str if dst_dir.exists() { t!(fs::remove_dir_all(&dst_dir), format!("failed to remove {}", dst_dir.display())); } + dst.to_path_buf() } fn download_file<'a>(