Skip to content
/ rust Public
forked from rust-lang/rust

Commit 2b3a462

Browse files
authored
Rollup merge of rust-lang#159671 - Kobzol:semver-checks, r=jieyouxu
Add semver check test command for checking API compatibility of stdlib This PR adds a new test to bootstrap (not executed by default at the moment) that checks the API compatibility of the standard library using the https://github.com/obi1kenobi/cargo-semver-checks tool. The command can be executed using `x test std-semver-check`. (Note: I realized that `x dist rust-docs-json` executed twice in a row invalidates Cargo cache for some reason, that is a separate issue though). The test is not yet executed on CI, I would do that in a separate PR (maybe pending a t-libs FCP or something). The test checks that c-s-c is installed, and then uses git to lookup a parent baseline commit (in a future extension, we should make the baseline commit be configurable through `config.toml`). Then it downloads the corresponding `rust-docs-json` component of that commit from CI and extracts it. It generates the same JSON docs component from local sources, and then runs c-s-c on those two JSON files to compare their API. Output when API stability is broken: ``` Checking semver compatibility of core Checking <unknown> v1.99.0-nightly (d527bc9 2026-07-20) -> v1.99.0-dev (assume minor change) Checked [ 1.282s] 196 checks: 196 pass, 57 skip Summary no semver update required Finished [ 2.368s] <unknown> Checking semver compatibility of alloc Checking <unknown> v1.99.0-nightly (d527bc9 2026-07-20) -> v1.99.0-dev (assume minor change) Checked [ 0.030s] 196 checks: 196 pass, 57 skip Summary no semver update required Finished [ 0.127s] <unknown> Checking semver compatibility of std Checking <unknown> v1.99.0-nightly (d527bc9 2026-07-20) -> v1.99.0-dev (assume minor change) Checked [ 0.055s] 196 checks: 195 pass, 1 fail, 0 warn, 57 skip --- failure function_missing: pub fn removed or renamed --- Description: A publicly-visible function cannot be imported by its prior path. A `pub use` may have been removed, or the function itself may have been renamed or removed entirely. ref: https://doc.rust-lang.org/cargo/reference/semver.html#item-remove impl: https://github.com/obi1kenobi/cargo-semver-checks/tree/v0.49.0/src/lints/function_missing.ron Failed in: function std::process::id, previously in file library/std/src/process.rs:2656 Summary semver requires new major version: 1 major and 0 minor checks failed Finished [ 0.243s] <unknown> ``` CC @obi1kenobi @Amanieu r? @jieyouxu
2 parents ce69831 + 73ba963 commit 2b3a462

5 files changed

Lines changed: 129 additions & 8 deletions

File tree

src/bootstrap/src/core/build_steps/test.rs

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ use std::path::{Path, PathBuf};
1313
use std::process::Command;
1414
use std::{env, fs, iter};
1515

16+
use build_helper::git::get_closest_upstream_commit;
17+
1618
use crate::core::build_steps::compile::{ArtifactKeepMode, Std, run_cargo};
1719
use crate::core::build_steps::doc::{DocumentationFormat, prepare_doc_compiler};
1820
use crate::core::build_steps::gcc::{Gcc, GccTargetPair, add_cg_gcc_cargo_flags};
@@ -4613,3 +4615,90 @@ impl CommandLineStep for RemoteTestClientTests {
46134615
);
46144616
}
46154617
}
4618+
4619+
fn check_if_cargo_semver_checks_is_installed(builder: &Builder<'_>) -> bool {
4620+
command("cargo")
4621+
.allow_failure()
4622+
.arg("semver-checks")
4623+
.arg("--version")
4624+
// Cache the output to avoid running this command more than once (per builder).
4625+
.cached()
4626+
.run_capture_stdout(builder)
4627+
.is_success()
4628+
}
4629+
4630+
/// Run cargo-semver-checks on the standard library and compare its API
4631+
/// versus a previous baseline, using rustdoc JSON data.
4632+
///
4633+
/// Fails if a semver-breaking change is detected.
4634+
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
4635+
pub struct StdSemverCheck {
4636+
build_compiler: Compiler,
4637+
target: TargetSelection,
4638+
/// The baseline commit that we are comparing the local stdlib API against.
4639+
commit: String,
4640+
}
4641+
4642+
impl CommandLineStep for StdSemverCheck {
4643+
type Output = ();
4644+
4645+
fn should_run(run: ShouldRun<'_>) -> ShouldRun<'_> {
4646+
run.alias("std-semver-check")
4647+
}
4648+
4649+
fn make_run(run: RunConfig<'_>) {
4650+
if !check_if_cargo_semver_checks_is_installed(run.builder) {
4651+
panic!("cargo-semver-checks was not found, please install it");
4652+
}
4653+
4654+
let baseline_commit = match get_closest_upstream_commit(
4655+
Some(&run.builder.config.src),
4656+
&run.builder.config.git_config(),
4657+
run.builder.config.ci_env,
4658+
) {
4659+
Ok(Some(commit)) => commit,
4660+
Ok(None) => {
4661+
panic!("No baseline parent commit found for std-semver-check");
4662+
}
4663+
Err(error) => {
4664+
panic!("Cannot get baseline parent commit for std-semver-check: {error:?}");
4665+
}
4666+
};
4667+
4668+
run.builder.ensure(Self {
4669+
build_compiler: run.builder.compiler_for_std(run.builder.top_stage),
4670+
target: run.target,
4671+
commit: baseline_commit,
4672+
});
4673+
}
4674+
4675+
fn run(self, builder: &Builder<'_>) {
4676+
let Some(docs_dir) = builder.config.download_std_json_docs(self.target, &self.commit)
4677+
else {
4678+
return;
4679+
};
4680+
4681+
let directory = builder.ensure(crate::core::build_steps::doc::Std::from_build_compiler(
4682+
self.build_compiler,
4683+
self.target,
4684+
DocumentationFormat::Json,
4685+
));
4686+
let baseline_dir = docs_dir.join("share").join("doc").join("rust").join("json");
4687+
4688+
for library in ["core", "alloc", "std"] {
4689+
println!("Checking semver compatibility of {library}");
4690+
let mut cmd = command("cargo");
4691+
cmd.arg("semver-checks")
4692+
.arg("-Z")
4693+
.arg("unstable-options")
4694+
.arg("--stability-aware")
4695+
.arg("--release-type")
4696+
.arg("minor")
4697+
.arg("--current-rustdoc")
4698+
.arg(directory.join(format!("{library}.json")))
4699+
.arg("--baseline-rustdoc")
4700+
.arg(baseline_dir.join(format!("{library}.json")));
4701+
cmd.run(builder);
4702+
}
4703+
}
4704+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
source: src/bootstrap/src/core/builder/cli_paths/tests.rs
3+
expression: test std-semver-check
4+
---
5+
[Test] test::StdSemverCheck
6+
targets: [aarch64-unknown-linux-gnu]
7+
- Set({test::std-semver-check})

src/bootstrap/src/core/builder/cli_paths/tests.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,7 @@ declare_tests!(
170170
(x_test_librustdoc_rustdoc_html, "test librustdoc rustdoc-html"),
171171
(x_test_rustdoc, "test rustdoc"),
172172
(x_test_rustdoc_html, "test rustdoc-html"),
173+
(x_test_semver_check, "test std-semver-check"),
173174
(x_test_skip_coverage, "test --skip=coverage"),
174175
(x_test_skip_coverage_map, "test --skip=coverage-map"),
175176
(x_test_skip_coverage_run, "test --skip=coverage-run"),

src/bootstrap/src/core/builder/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -974,6 +974,7 @@ impl<'a> Builder<'a> {
974974
test::RunMake,
975975
test::RunMakeCargo,
976976
test::BuildStd,
977+
test::StdSemverCheck,
977978
test::IntrinsicTest,
978979
),
979980
Kind::Miri => describe!(test::Crate),

src/bootstrap/src/core/download.rs

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,30 @@ impl Config {
173173
);
174174
}
175175

176+
pub(crate) fn download_std_json_docs(
177+
&self,
178+
target: TargetSelection,
179+
commit: &str,
180+
) -> Option<PathBuf> {
181+
if self.dry_run() {
182+
return None;
183+
}
184+
185+
self.do_if_verbose(|| println!("using downloaded std json docs from CI (commit {commit})"));
186+
187+
let version = self.artifact_version_part(commit);
188+
download_component(
189+
DownloadContext::from(self),
190+
&self.out,
191+
DownloadSource::CI,
192+
format!("rust-docs-json-{version}-{target}.tar.xz"),
193+
"rust-docs-json-preview",
194+
// When using DownloadSource::CI, the key is assumed to end with -llvm-assertions
195+
&format!("{commit}-{}", self.llvm_assertions),
196+
"ci-docs-json",
197+
)
198+
}
199+
176200
fn download_toolchain(
177201
&self,
178202
version: &str,
@@ -785,11 +809,11 @@ fn download_component<'a>(
785809
prefix: &str,
786810
key: &str,
787811
destination: &str,
788-
) {
812+
) -> Option<PathBuf> {
789813
let dwn_ctx = dwn_ctx.as_ref();
790814

791815
if dwn_ctx.exec_ctx.dry_run() {
792-
return;
816+
return None;
793817
}
794818

795819
let cache_dst =
@@ -834,8 +858,7 @@ fn download_component<'a>(
834858
let sha256 = dwn_ctx.stage0_metadata.checksums_sha256.get(&url).expect(&error);
835859
if tarball.exists() {
836860
if verify(dwn_ctx.exec_ctx, &tarball, sha256) {
837-
unpack(dwn_ctx.exec_ctx, &tarball, &bin_root, prefix);
838-
return;
861+
return Some(unpack(dwn_ctx.exec_ctx, &tarball, &bin_root, prefix));
839862
} else {
840863
dwn_ctx.exec_ctx.do_if_verbose(|| {
841864
println!(
@@ -848,8 +871,7 @@ fn download_component<'a>(
848871
}
849872
Some(sha256)
850873
} else if tarball.exists() {
851-
unpack(dwn_ctx.exec_ctx, &tarball, &bin_root, prefix);
852-
return;
874+
return Some(unpack(dwn_ctx.exec_ctx, &tarball, &bin_root, prefix));
853875
} else {
854876
None
855877
};
@@ -872,7 +894,7 @@ download-rustc = false
872894
panic!("failed to verify {}", tarball.display());
873895
}
874896

875-
unpack(dwn_ctx.exec_ctx, &tarball, &bin_root, prefix);
897+
Some(unpack(dwn_ctx.exec_ctx, &tarball, &bin_root, prefix))
876898
}
877899

878900
pub(crate) fn verify(exec_ctx: &ExecutionContext, path: &Path, expected: &str) -> bool {
@@ -916,7 +938,7 @@ pub(crate) fn verify(exec_ctx: &ExecutionContext, path: &Path, expected: &str) -
916938
verified
917939
}
918940

919-
fn unpack(exec_ctx: &ExecutionContext, tarball: &Path, dst: &Path, pattern: &str) {
941+
fn unpack(exec_ctx: &ExecutionContext, tarball: &Path, dst: &Path, pattern: &str) -> PathBuf {
920942
eprintln!("extracting {} to {}", tarball.display(), dst.display());
921943
if !dst.exists() {
922944
t!(fs::create_dir_all(dst));
@@ -979,6 +1001,7 @@ fn unpack(exec_ctx: &ExecutionContext, tarball: &Path, dst: &Path, pattern: &str
9791001
if dst_dir.exists() {
9801002
t!(fs::remove_dir_all(&dst_dir), format!("failed to remove {}", dst_dir.display()));
9811003
}
1004+
dst.to_path_buf()
9821005
}
9831006

9841007
fn download_file<'a>(

0 commit comments

Comments
 (0)