Skip to content

Commit bda55c8

Browse files
committed
add relnotes-api-list tool
1 parent 6693b39 commit bda55c8

File tree

18 files changed

+1558
-4
lines changed

18 files changed

+1558
-4
lines changed

Cargo.lock

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3122,6 +3122,17 @@ version = "0.8.5"
31223122
source = "registry+https://github.com/rust-lang/crates.io-index"
31233123
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
31243124

3125+
[[package]]
3126+
name = "relnotes-api-list"
3127+
version = "0.1.0"
3128+
dependencies = [
3129+
"anyhow",
3130+
"rustdoc-json-types",
3131+
"serde",
3132+
"serde_json",
3133+
"tempfile",
3134+
]
3135+
31253136
[[package]]
31263137
name = "remote-test-client"
31273138
version = "0.1.0"

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ members = [
3030
"src/tools/miri/cargo-miri",
3131
"src/tools/miropt-test-tools",
3232
"src/tools/opt-dist",
33+
"src/tools/relnotes-api-list",
3334
"src/tools/remote-test-client",
3435
"src/tools/remote-test-server",
3536
"src/tools/replace-version-placeholder",

src/bootstrap/src/core/build_steps/dist.rs

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2641,3 +2641,62 @@ impl Step for Gcc {
26412641
tarball.generate()
26422642
}
26432643
}
2644+
2645+
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
2646+
pub struct RelnotesApiList {
2647+
pub host: TargetSelection,
2648+
}
2649+
2650+
impl Step for RelnotesApiList {
2651+
type Output = ();
2652+
const DEFAULT: bool = true;
2653+
2654+
fn should_run(run: ShouldRun<'_>) -> ShouldRun<'_> {
2655+
let default = run.builder.config.docs;
2656+
run.alias("relnotes-api-list").default_condition(default)
2657+
}
2658+
2659+
fn make_run(run: RunConfig<'_>) {
2660+
run.builder.ensure(RelnotesApiList { host: run.target });
2661+
}
2662+
2663+
fn run(self, builder: &Builder<'_>) -> Self::Output {
2664+
let host = self.host;
2665+
let dest = builder.out.join("dist").join(format!("relnotes-api-list-{host}.json"));
2666+
builder.create_dir(dest.parent().unwrap());
2667+
2668+
// The HTML documentation for the standard library is needed to check all links generated by
2669+
// the tool are not broken.
2670+
builder.ensure(crate::core::build_steps::doc::Std::new(
2671+
builder.top_stage,
2672+
host,
2673+
DocumentationFormat::Html,
2674+
));
2675+
2676+
if std::env::var_os("EMILY_SKIP_DOC").is_none() {
2677+
// TODO: remove the condition
2678+
builder.ensure(
2679+
crate::core::build_steps::doc::Std::new(
2680+
builder.top_stage,
2681+
host,
2682+
DocumentationFormat::Json,
2683+
)
2684+
// Crates containing symbols exported by any std crate:
2685+
.add_extra_crate("rustc-literal-escaper")
2686+
.add_extra_crate("std_detect"),
2687+
);
2688+
}
2689+
2690+
let linkchecker = builder.tool_exe(Tool::Linkchecker);
2691+
2692+
builder.info("Generating the API list for the release notes");
2693+
builder
2694+
.tool_cmd(Tool::RelnotesApiList)
2695+
.arg(builder.json_doc_out(host))
2696+
.arg(&dest)
2697+
.env("LINKCHECKER_PATH", linkchecker)
2698+
.env("STD_HTML_DOCS", builder.doc_out(self.host))
2699+
.run(builder);
2700+
builder.info(&format!("API list for the release notes available at {}", dest.display()));
2701+
}
2702+
}

src/bootstrap/src/core/build_steps/doc.rs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -560,11 +560,17 @@ pub struct Std {
560560
pub target: TargetSelection,
561561
pub format: DocumentationFormat,
562562
crates: Vec<String>,
563+
extra_crates: Vec<String>,
563564
}
564565

565566
impl Std {
566567
pub(crate) fn new(stage: u32, target: TargetSelection, format: DocumentationFormat) -> Self {
567-
Std { stage, target, format, crates: vec![] }
568+
Std { stage, target, format, crates: vec![], extra_crates: vec![] }
569+
}
570+
571+
pub(crate) fn add_extra_crate(mut self, krate: &str) -> Self {
572+
self.extra_crates.push(krate.to_string());
573+
self
568574
}
569575
}
570576

@@ -592,6 +598,7 @@ impl Step for Std {
592598
DocumentationFormat::Html
593599
},
594600
crates,
601+
extra_crates: vec![],
595602
});
596603
}
597604

@@ -602,7 +609,7 @@ impl Step for Std {
602609
fn run(self, builder: &Builder<'_>) {
603610
let stage = self.stage;
604611
let target = self.target;
605-
let crates = if self.crates.is_empty() {
612+
let mut crates = if self.crates.is_empty() {
606613
builder
607614
.in_tree_crates("sysroot", Some(target))
608615
.iter()
@@ -611,6 +618,7 @@ impl Step for Std {
611618
} else {
612619
self.crates
613620
};
621+
crates.extend(self.extra_crates.iter().cloned());
614622

615623
let out = match self.format {
616624
DocumentationFormat::Html => builder.doc_out(target),

src/bootstrap/src/core/build_steps/tool.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -568,6 +568,7 @@ bootstrap_tool!(
568568
FeaturesStatusDump, "src/tools/features-status-dump", "features-status-dump";
569569
OptimizedDist, "src/tools/opt-dist", "opt-dist", submodules = &["src/tools/rustc-perf"];
570570
RunMakeSupport, "src/tools/run-make-support", "run_make_support", artifact_kind = ToolArtifactKind::Library;
571+
RelnotesApiList, "src/tools/relnotes-api-list", "relnotes-api-list";
571572
);
572573

573574
/// These are the submodules that are required for rustbook to work due to

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -986,7 +986,8 @@ impl<'a> Builder<'a> {
986986
tool::LlvmBitcodeLinker,
987987
tool::RustcPerf,
988988
tool::WasmComponentLd,
989-
tool::LldWrapper
989+
tool::LldWrapper,
990+
tool::RelnotesApiList,
990991
),
991992
Kind::Clippy => describe!(
992993
clippy::Std,
@@ -1163,7 +1164,8 @@ impl<'a> Builder<'a> {
11631164
dist::PlainSourceTarball,
11641165
dist::BuildManifest,
11651166
dist::ReproducibleArtifacts,
1166-
dist::Gcc
1167+
dist::Gcc,
1168+
dist::RelnotesApiList,
11671169
),
11681170
Kind::Install => describe!(
11691171
install::Docs,
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
[package]
2+
name = "relnotes-api-list"
3+
version = "0.1.0"
4+
edition = "2024"
5+
6+
[dependencies]
7+
anyhow = "1"
8+
serde = { version = "1", features = ["derive"] }
9+
serde_json = "1"
10+
rustdoc-json-types = { path = "../../rustdoc-json-types" }
11+
tempfile = "3.20.0"

src/tools/relnotes-api-list/README.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# API list generator for the release notes
2+
3+
Rust's [release notes] include a "Stabilized APIs" section for each
4+
release, listing all APIs that became stable each release. This tool supports
5+
the creation of that section by generating a JSON file containing a concise
6+
representation of the standard library API. The [relnotes tool] will then
7+
compare the JSON files of two releases to generate the section.
8+
9+
The tool is executed by CI and produces the `relnotes-api-list-$target.json`
10+
dist artifact. You can also run the tool locally with:
11+
12+
```
13+
./x dist relnotes-api-list
14+
```
15+
16+
[release notes]: https://doc.rust-lang.org/stable/releases.html
17+
[relnotes tool]: https://github.com/rust-lang/relnotes
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
//! We generate a lot of URLs in the resulting JSON, and we need all those URLs to be correct. While
2+
//! some URLs are fairly trivial to generate, others are quite tricky (especially `impl` blocks).
3+
//!
4+
//! To ensure we always generate good URLs, we prepare a temporary HTML file containing `<a>` tags for
5+
//! every itme we collected, and we run it through linkchecker. If this fails, it means the code
6+
//! generating URLs has a bug.
7+
8+
use std::fs::File;
9+
use std::io::Write;
10+
use std::path::PathBuf;
11+
use std::process::Command;
12+
13+
use anyhow::{Error, anyhow, bail};
14+
use tempfile::tempdir;
15+
16+
use crate::schema::{Schema, SchemaItem};
17+
18+
pub(crate) fn check_urls(schema: &Schema) -> Result<(), Error> {
19+
let mut urls = Vec::new();
20+
collect_urls(&mut urls, &schema.items);
21+
22+
let html_dir = tempdir()?;
23+
24+
let mut file = File::create(html_dir.path().join("urls.html"))?;
25+
file.write_all(render_html(&urls).as_bytes())?;
26+
27+
eprintln!("checking that all generated URLs are valid...");
28+
let result = Command::new(require_env("LINKCHECKER_PATH")?)
29+
.arg(html_dir.path())
30+
.arg("--link-targets-dir")
31+
.arg(require_env("STD_HTML_DOCS")?)
32+
.status()?;
33+
34+
if !result.success() {
35+
bail!("some URLs are broken, the relnotes-api-list tool has a bug");
36+
}
37+
38+
dbg!(require_env("STD_HTML_DOCS")?);
39+
40+
Ok(())
41+
}
42+
43+
fn collect_urls<'a>(result: &mut Vec<&'a str>, items: &'a [SchemaItem]) {
44+
for item in items {
45+
if let Some(url) = &item.url {
46+
result.push(url);
47+
}
48+
collect_urls(result, &item.children);
49+
}
50+
}
51+
52+
fn render_html(urls: &[&str]) -> String {
53+
let mut content = "<!DOCTYPE html>\n".to_string();
54+
for url in urls {
55+
content.push_str(&format!("<a href=\"{url}\"></a>\n"));
56+
}
57+
content
58+
}
59+
60+
fn require_env(name: &str) -> Result<PathBuf, Error> {
61+
match std::env::var_os(name) {
62+
Some(value) => Ok(value.into()),
63+
None => Err(anyhow!("missing environment variable {name}")),
64+
}
65+
}

0 commit comments

Comments
 (0)