Skip to content

Commit 492a9d4

Browse files
committed
add relnotes-api-list tool
1 parent 8e5b8dc commit 492a9d4

File tree

17 files changed

+1386
-3
lines changed

17 files changed

+1386
-3
lines changed

Cargo.lock

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

3188+
[[package]]
3189+
name = "relnotes-api-list"
3190+
version = "0.1.0"
3191+
dependencies = [
3192+
"anyhow",
3193+
"rustdoc-json-types",
3194+
"serde",
3195+
"serde_json",
3196+
"tempfile",
3197+
]
3198+
31883199
[[package]]
31893200
name = "remote-test-client"
31903201
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
@@ -2636,3 +2636,62 @@ impl Step for Gcc {
26362636
tarball.generate()
26372637
}
26382638
}
2639+
2640+
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
2641+
pub struct RelnotesApiList {
2642+
pub host: TargetSelection,
2643+
}
2644+
2645+
impl Step for RelnotesApiList {
2646+
type Output = ();
2647+
const DEFAULT: bool = true;
2648+
2649+
fn should_run(run: ShouldRun<'_>) -> ShouldRun<'_> {
2650+
let default = run.builder.config.docs;
2651+
run.alias("relnotes-api-list").default_condition(default)
2652+
}
2653+
2654+
fn make_run(run: RunConfig<'_>) {
2655+
run.builder.ensure(RelnotesApiList { host: run.target });
2656+
}
2657+
2658+
fn run(self, builder: &Builder<'_>) -> Self::Output {
2659+
let host = self.host;
2660+
let dest = builder.out.join("dist").join(format!("relnotes-api-list-{host}.json"));
2661+
builder.create_dir(dest.parent().unwrap());
2662+
2663+
// The HTML documentation for the standard library is needed to check all links generated by
2664+
// the tool are not broken.
2665+
builder.ensure(crate::core::build_steps::doc::Std::new(
2666+
builder.top_stage,
2667+
host,
2668+
DocumentationFormat::Html,
2669+
));
2670+
2671+
if std::env::var_os("EMILY_SKIP_DOC").is_none() {
2672+
// TODO: remove the condition
2673+
builder.ensure(
2674+
crate::core::build_steps::doc::Std::new(
2675+
builder.top_stage,
2676+
host,
2677+
DocumentationFormat::Json,
2678+
)
2679+
// Crates containing symbols exported by any std crate:
2680+
.add_extra_crate("rustc-literal-escaper")
2681+
.add_extra_crate("std_detect"),
2682+
);
2683+
}
2684+
2685+
let linkchecker = builder.tool_exe(Tool::Linkchecker);
2686+
2687+
builder.info("Generating the API list for the release notes");
2688+
builder
2689+
.tool_cmd(Tool::RelnotesApiList)
2690+
.arg(builder.json_doc_out(host))
2691+
.arg(&dest)
2692+
.env("LINKCHECKER_PATH", linkchecker)
2693+
.env("STD_HTML_DOCS", builder.doc_out(self.host))
2694+
.run(builder);
2695+
builder.info(&format!("API list for the release notes available at {}", dest.display()));
2696+
}
2697+
}

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
@@ -526,6 +526,7 @@ bootstrap_tool!(
526526
FeaturesStatusDump, "src/tools/features-status-dump", "features-status-dump";
527527
OptimizedDist, "src/tools/opt-dist", "opt-dist", submodules = &["src/tools/rustc-perf"];
528528
RunMakeSupport, "src/tools/run-make-support", "run_make_support", artifact_kind = ToolArtifactKind::Library;
529+
RelnotesApiList, "src/tools/relnotes-api-list", "relnotes-api-list";
529530
);
530531

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

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -980,6 +980,7 @@ impl<'a> Builder<'a> {
980980
tool::CoverageDump,
981981
tool::LlvmBitcodeLinker,
982982
tool::RustcPerf,
983+
tool::RelnotesApiList,
983984
),
984985
Kind::Clippy => describe!(
985986
clippy::Std,
@@ -1156,7 +1157,8 @@ impl<'a> Builder<'a> {
11561157
dist::PlainSourceTarball,
11571158
dist::BuildManifest,
11581159
dist::ReproducibleArtifacts,
1159-
dist::Gcc
1160+
dist::Gcc,
1161+
dist::RelnotesApiList,
11601162
),
11611163
Kind::Install => describe!(
11621164
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: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
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 crate::schema::{Schema, SchemaItem};
9+
use anyhow::{Error, anyhow, bail};
10+
use std::fs::File;
11+
use std::io::Write;
12+
use std::path::PathBuf;
13+
use std::process::Command;
14+
use tempfile::tempdir;
15+
16+
pub(crate) fn check_urls(schema: &Schema) -> Result<(), Error> {
17+
let mut urls = Vec::new();
18+
collect_urls(&mut urls, &schema.items);
19+
20+
let html_dir = tempdir()?;
21+
22+
let mut file = File::create(html_dir.path().join("urls.html"))?;
23+
file.write_all(render_html(&urls).as_bytes())?;
24+
25+
eprintln!("checking that all generated URLs are valid...");
26+
let result = Command::new(require_env("LINKCHECKER_PATH")?)
27+
.arg(html_dir.path())
28+
.arg("--extra-target")
29+
.arg(require_env("STD_HTML_DOCS")?)
30+
.status()?;
31+
32+
if !result.success() {
33+
bail!("some URLs are broken, the relnotes-api-list tool has a bug");
34+
}
35+
36+
dbg!(require_env("STD_HTML_DOCS")?);
37+
38+
Ok(())
39+
}
40+
41+
fn collect_urls<'a>(result: &mut Vec<&'a str>, items: &'a [SchemaItem]) {
42+
for item in items {
43+
if let Some(url) = &item.url {
44+
result.push(url);
45+
}
46+
collect_urls(result, &item.children);
47+
}
48+
}
49+
50+
fn render_html(urls: &[&str]) -> String {
51+
let mut content = "<!DOCTYPE html>\n".to_string();
52+
for url in urls {
53+
content.push_str(&format!("<a href=\"{url}\"></a>\n"));
54+
}
55+
content
56+
}
57+
58+
fn require_env(name: &str) -> Result<PathBuf, Error> {
59+
match std::env::var_os(name) {
60+
Some(value) => Ok(value.into()),
61+
None => Err(anyhow!("missing environment variable {name}")),
62+
}
63+
}

0 commit comments

Comments
 (0)