Skip to content

Commit 235dcce

Browse files
author
Luca Palmieri
committed
Add a new component, rust-json-docs, to distribute the JSON-formatted documentation for std crates in nightly toolchains.
We also add a new flag to `x doc`, `--json`, to render the JSON-formatted version alongside the HTML-formatted one.
1 parent 9da4644 commit 235dcce

File tree

6 files changed

+219
-62
lines changed

6 files changed

+219
-62
lines changed

src/bootstrap/builder.rs

+1
Original file line numberDiff line numberDiff line change
@@ -708,6 +708,7 @@ impl<'a> Builder<'a> {
708708
Kind::Dist => describe!(
709709
dist::Docs,
710710
dist::RustcDocs,
711+
dist::JsonDocs,
711712
dist::Mingw,
712713
dist::Rustc,
713714
dist::Std,

src/bootstrap/builder/tests.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -236,7 +236,7 @@ mod defaults {
236236
fn doc_default() {
237237
let mut config = configure("doc", &["A"], &["A"]);
238238
config.compiler_docs = true;
239-
config.cmd = Subcommand::Doc { paths: Vec::new(), open: false };
239+
config.cmd = Subcommand::Doc { paths: Vec::new(), open: false, json: false };
240240
let mut cache = run_build(&[], config);
241241
let a = TargetSelection::from_user("A");
242242

@@ -587,7 +587,7 @@ mod dist {
587587
fn doc_ci() {
588588
let mut config = configure(&["A"], &["A"]);
589589
config.compiler_docs = true;
590-
config.cmd = Subcommand::Doc { paths: Vec::new(), open: false };
590+
config.cmd = Subcommand::Doc { paths: Vec::new(), open: false, json: false };
591591
let build = Build::new(config);
592592
let mut builder = Builder::new(&build);
593593
builder.run_step_descriptions(&Builder::get_step_descriptions(Kind::Doc), &[]);

src/bootstrap/dist.rs

+39
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,45 @@ impl Step for Docs {
8787
}
8888
}
8989

90+
#[derive(Debug, PartialOrd, Ord, Copy, Clone, Hash, PartialEq, Eq)]
91+
pub struct JsonDocs {
92+
pub host: TargetSelection,
93+
}
94+
95+
impl Step for JsonDocs {
96+
type Output = Option<GeneratedTarball>;
97+
const DEFAULT: bool = true;
98+
99+
fn should_run(run: ShouldRun<'_>) -> ShouldRun<'_> {
100+
let default = run.builder.config.docs;
101+
run.alias("rust-json-docs").default_condition(default)
102+
}
103+
104+
fn make_run(run: RunConfig<'_>) {
105+
run.builder.ensure(JsonDocs { host: run.target });
106+
}
107+
108+
/// Builds the `rust-json-docs` installer component.
109+
fn run(self, builder: &Builder<'_>) -> Option<GeneratedTarball> {
110+
// This prevents JSON docs from being built for "dist" or "install"
111+
// on the stable/beta channels. The JSON format is not stable yet and
112+
// should not be included in stable/beta toolchains.
113+
if !builder.build.unstable_features() {
114+
return None;
115+
}
116+
117+
let host = self.host;
118+
builder.ensure(crate::doc::JsonStd { stage: builder.top_stage, target: host });
119+
120+
let dest = "share/doc/rust/json";
121+
122+
let mut tarball = Tarball::new(builder, "rust-json-docs", &host.triple);
123+
tarball.set_product_name("Rust Documentation In JSON Format");
124+
tarball.add_bulk_dir(&builder.json_doc_out(host), dest);
125+
Some(tarball.generate())
126+
}
127+
}
128+
90129
#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)]
91130
pub struct RustcDocs {
92131
pub host: TargetSelection,

src/bootstrap/doc.rs

+153-59
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
//! Everything here is basically just a shim around calling either `rustbook` or
88
//! `rustdoc`.
99
10+
use std::ffi::OsStr;
1011
use std::fs;
1112
use std::io;
1213
use std::path::{Path, PathBuf};
@@ -431,49 +432,24 @@ impl Step for Std {
431432
fn run(self, builder: &Builder<'_>) {
432433
let stage = self.stage;
433434
let target = self.target;
434-
builder.info(&format!("Documenting stage{} std ({})", stage, target));
435-
if builder.no_std(target) == Some(true) {
436-
panic!(
437-
"building std documentation for no_std target {target} is not supported\n\
438-
Set `docs = false` in the config to disable documentation."
439-
);
440-
}
441435
let out = builder.doc_out(target);
442436
t!(fs::create_dir_all(&out));
443-
let compiler = builder.compiler(stage, builder.config.build);
444-
445-
let out_dir = builder.stage_out(compiler, Mode::Std).join(target.triple).join("doc");
446-
447437
t!(fs::copy(builder.src.join("src/doc/rust.css"), out.join("rust.css")));
448438

449-
let run_cargo_rustdoc_for = |package: &str| {
450-
let mut cargo =
451-
builder.cargo(compiler, Mode::Std, SourceType::InTree, target, "rustdoc");
452-
compile::std_cargo(builder, target, compiler.stage, &mut cargo);
453-
454-
cargo
455-
.arg("-p")
456-
.arg(package)
457-
.arg("-Zskip-rustdoc-fingerprint")
458-
.arg("--")
459-
.arg("--markdown-css")
460-
.arg("rust.css")
461-
.arg("--markdown-no-toc")
462-
.arg("-Z")
463-
.arg("unstable-options")
464-
.arg("--resource-suffix")
465-
.arg(&builder.version)
466-
.arg("--index-page")
467-
.arg(&builder.src.join("src/doc/index.md"));
468-
469-
if !builder.config.docs_minification {
470-
cargo.arg("--disable-minification");
471-
}
472-
473-
builder.run(&mut cargo.into());
474-
};
439+
let index_page = builder.src.join("src/doc/index.md").into_os_string();
440+
let mut extra_args = vec![
441+
OsStr::new("--markdown-css"),
442+
OsStr::new("rust.css"),
443+
OsStr::new("--markdown-no-toc"),
444+
OsStr::new("--index-page"),
445+
&index_page,
446+
];
447+
448+
if !builder.config.docs_minification {
449+
extra_args.push(OsStr::new("--disable-minification"));
450+
}
475451

476-
let paths = builder
452+
let requested_crates = builder
477453
.paths
478454
.iter()
479455
.map(components_simplified)
@@ -491,37 +467,155 @@ impl Step for Std {
491467
})
492468
.collect::<Vec<_>>();
493469

494-
// Only build the following crates. While we could just iterate over the
495-
// folder structure, that would also build internal crates that we do
496-
// not want to show in documentation. These crates will later be visited
497-
// by the rustc step, so internal documentation will show them.
498-
//
499-
// Note that the order here is important! The crates need to be
500-
// processed starting from the leaves, otherwise rustdoc will not
501-
// create correct links between crates because rustdoc depends on the
502-
// existence of the output directories to know if it should be a local
503-
// or remote link.
504-
let krates = ["core", "alloc", "std", "proc_macro", "test"];
505-
for krate in &krates {
506-
run_cargo_rustdoc_for(krate);
507-
if paths.iter().any(|p| p == krate) {
508-
// No need to document more of the libraries if we have the one we want.
509-
break;
510-
}
511-
}
512-
builder.cp_r(&out_dir, &out);
470+
doc_std(
471+
builder,
472+
DocumentationFormat::HTML,
473+
stage,
474+
target,
475+
&out,
476+
&extra_args,
477+
&requested_crates,
478+
);
513479

514480
// Look for library/std, library/core etc in the `x.py doc` arguments and
515481
// open the corresponding rendered docs.
516-
for requested_crate in paths {
517-
if krates.iter().any(|k| *k == requested_crate.as_str()) {
482+
for requested_crate in requested_crates {
483+
if STD_PUBLIC_CRATES.iter().any(|k| *k == requested_crate.as_str()) {
518484
let index = out.join(requested_crate).join("index.html");
519485
open(builder, &index);
520486
}
521487
}
522488
}
523489
}
524490

491+
#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)]
492+
pub struct JsonStd {
493+
pub stage: u32,
494+
pub target: TargetSelection,
495+
}
496+
497+
impl Step for JsonStd {
498+
type Output = ();
499+
const DEFAULT: bool = false;
500+
501+
fn should_run(run: ShouldRun<'_>) -> ShouldRun<'_> {
502+
let default = run.builder.config.docs && run.builder.config.cmd.json();
503+
run.all_krates("test").path("library").default_condition(default)
504+
}
505+
506+
fn make_run(run: RunConfig<'_>) {
507+
run.builder.ensure(Std { stage: run.builder.top_stage, target: run.target });
508+
}
509+
510+
/// Build JSON documentation for the standard library crates.
511+
///
512+
/// This is largely just a wrapper around `cargo doc`.
513+
fn run(self, builder: &Builder<'_>) {
514+
let stage = self.stage;
515+
let target = self.target;
516+
let out = builder.json_doc_out(target);
517+
t!(fs::create_dir_all(&out));
518+
let extra_args = [OsStr::new("--output-format"), OsStr::new("json")];
519+
doc_std(builder, DocumentationFormat::JSON, stage, target, &out, &extra_args, &[])
520+
}
521+
}
522+
523+
/// Name of the crates that are visible to consumers of the standard library.
524+
/// Documentation for internal crates is handled by the rustc step, so internal crates will show
525+
/// up there.
526+
///
527+
/// Order here is important!
528+
/// Crates need to be processed starting from the leaves, otherwise rustdoc will not
529+
/// create correct links between crates because rustdoc depends on the
530+
/// existence of the output directories to know if it should be a local
531+
/// or remote link.
532+
const STD_PUBLIC_CRATES: [&str; 5] = ["core", "alloc", "std", "proc_macro", "test"];
533+
534+
#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)]
535+
enum DocumentationFormat {
536+
HTML,
537+
JSON,
538+
}
539+
540+
impl DocumentationFormat {
541+
fn as_str(&self) -> &str {
542+
match self {
543+
DocumentationFormat::HTML => "HTML",
544+
DocumentationFormat::JSON => "JSON",
545+
}
546+
}
547+
}
548+
549+
/// Build the documentation for public standard library crates.
550+
///
551+
/// `requested_crates` can be used to build only a subset of the crates. If empty, all crates will
552+
/// be built.
553+
fn doc_std(
554+
builder: &Builder<'_>,
555+
format: DocumentationFormat,
556+
stage: u32,
557+
target: TargetSelection,
558+
out: &Path,
559+
extra_args: &[&OsStr],
560+
requested_crates: &[String],
561+
) {
562+
builder.info(&format!(
563+
"Documenting stage{} std ({}) in {} format",
564+
stage,
565+
target,
566+
format.as_str()
567+
));
568+
if builder.no_std(target) == Some(true) {
569+
panic!(
570+
"building std documentation for no_std target {target} is not supported\n\
571+
Set `docs = false` in the config to disable documentation."
572+
);
573+
}
574+
let compiler = builder.compiler(stage, builder.config.build);
575+
// This is directory where the compiler will place the output of the command.
576+
// We will then copy the files from this directory into the final `out` directory, the specified
577+
// as a function parameter.
578+
let out_dir = builder.stage_out(compiler, Mode::Std).join(target.triple).join("doc");
579+
// `cargo` uses the same directory for both JSON docs and HTML docs.
580+
// This could lead to cross-contamination when copying files into the specified `out` directory.
581+
// For example:
582+
// ```bash
583+
// x doc std
584+
// x doc std --json
585+
// ```
586+
// could lead to HTML docs being copied into the JSON docs output directory.
587+
// To avoid this issue, we clean the doc folder before invoking `cargo`.
588+
if out_dir.exists() {
589+
builder.remove_dir(&out_dir);
590+
}
591+
592+
let run_cargo_rustdoc_for = |package: &str| {
593+
let mut cargo = builder.cargo(compiler, Mode::Std, SourceType::InTree, target, "rustdoc");
594+
compile::std_cargo(builder, target, compiler.stage, &mut cargo);
595+
cargo
596+
.arg("-p")
597+
.arg(package)
598+
.arg("-Zskip-rustdoc-fingerprint")
599+
.arg("--")
600+
.arg("-Z")
601+
.arg("unstable-options")
602+
.arg("--resource-suffix")
603+
.arg(&builder.version)
604+
.args(extra_args);
605+
builder.run(&mut cargo.into());
606+
};
607+
608+
for krate in STD_PUBLIC_CRATES {
609+
run_cargo_rustdoc_for(krate);
610+
if requested_crates.iter().any(|p| p == krate) {
611+
// No need to document more of the libraries if we have the one we want.
612+
break;
613+
}
614+
}
615+
616+
builder.cp_r(&out_dir, &out);
617+
}
618+
525619
#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)]
526620
pub struct Rustc {
527621
pub stage: u32,

src/bootstrap/flags.rs

+19-1
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ pub enum Subcommand {
107107
Doc {
108108
paths: Vec<PathBuf>,
109109
open: bool,
110+
json: bool,
110111
},
111112
Test {
112113
paths: Vec<PathBuf>,
@@ -325,6 +326,11 @@ To learn more about a subcommand, run `./x.py <subcommand> -h`",
325326
}
326327
Kind::Doc => {
327328
opts.optflag("", "open", "open the docs in a browser");
329+
opts.optflag(
330+
"",
331+
"json",
332+
"render the documentation in JSON format in addition to the usual HTML format",
333+
);
328334
}
329335
Kind::Clean => {
330336
opts.optflag("", "all", "clean all build artifacts");
@@ -493,6 +499,7 @@ Arguments:
493499
./x.py doc src/doc/book
494500
./x.py doc src/doc/nomicon
495501
./x.py doc src/doc/book library/std
502+
./x.py doc library/std --json
496503
./x.py doc library/std --open
497504
498505
If no arguments are passed then everything is documented:
@@ -581,7 +588,11 @@ Arguments:
581588
},
582589
},
583590
Kind::Bench => Subcommand::Bench { paths, test_args: matches.opt_strs("test-args") },
584-
Kind::Doc => Subcommand::Doc { paths, open: matches.opt_present("open") },
591+
Kind::Doc => Subcommand::Doc {
592+
paths,
593+
open: matches.opt_present("open"),
594+
json: matches.opt_present("json"),
595+
},
585596
Kind::Clean => {
586597
if !paths.is_empty() {
587598
println!("\nclean does not take a path argument\n");
@@ -787,6 +798,13 @@ impl Subcommand {
787798
_ => false,
788799
}
789800
}
801+
802+
pub fn json(&self) -> bool {
803+
match *self {
804+
Subcommand::Doc { json, .. } => json,
805+
_ => false,
806+
}
807+
}
790808
}
791809

792810
fn split(s: &[String]) -> Vec<String> {

src/bootstrap/lib.rs

+5
Original file line numberDiff line numberDiff line change
@@ -825,6 +825,11 @@ impl Build {
825825
self.out.join(&*target.triple).join("doc")
826826
}
827827

828+
/// Output directory for all JSON-formatted documentation for a target
829+
fn json_doc_out(&self, target: TargetSelection) -> PathBuf {
830+
self.out.join(&*target.triple).join("json-doc")
831+
}
832+
828833
fn test_out(&self, target: TargetSelection) -> PathBuf {
829834
self.out.join(&*target.triple).join("test")
830835
}

0 commit comments

Comments
 (0)