Skip to content

Commit 1546bf0

Browse files
committed
Create mdbook-rustc preprocessor
1 parent 0ab459f commit 1546bf0

File tree

7 files changed

+352
-1
lines changed

7 files changed

+352
-1
lines changed

src/doc/rustc/book.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,9 @@ use-boolean-and = true
1010

1111
[output.html.playground]
1212
runnable = false
13+
14+
[preprocessor.rustc-gen-target-tables]
15+
command = "cargo run --manifest-path ../../../../src/tools/mdbook-rustc/Cargo.toml"
16+
17+
[build]
18+
extra-watch-dirs = ["../../tools/mdbook-rustc"]

src/tools/mdbook-rustc/Cargo.toml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
[package]
2+
name = "mdbook-rustc"
3+
version = "0.0.0"
4+
edition = "2024"
5+
workspace = "../rustbook"
6+
7+
[dependencies]
8+
mdbook-preprocessor = "0.5.1"
9+
serde = { version = "1.0.228", features = ["serde_derive"] }
10+
serde_json = "1"

src/tools/mdbook-rustc/src/lib.rs

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
use std::collections::HashMap;
2+
use std::fmt::Write;
3+
use std::path::{Path, PathBuf};
4+
use std::{env, fs};
5+
6+
use mdbook_preprocessor::book::Chapter;
7+
8+
use crate::target::{ByTier, Target};
9+
10+
pub mod target;
11+
12+
const TIER_1_HOST_MARKER: &str = "{{TIER_1_HOST_TABLE}}";
13+
const TIER_1_NOHOST_MARKER: &str = "{{TIER_1_NOHOST_TABLE}}";
14+
const TIER_2_HOST_MARKER: &str = "{{TIER_2_HOST_TABLE}}";
15+
const TIER_2_NOHOST_MARKER: &str = "{{TIER_2_NOHOST_TABLE}}";
16+
const TIER_3_MARKER: &str = "{{TIER_3_TABLE}}";
17+
18+
const EMPTY_TIER_1_NOHOST_MSG: &str =
19+
"At this time, all Tier 1 targets are [Tier 1 with Host Tools](#tier-1-with-host-tools).\n";
20+
const EMPTY_TIER_2_NOHOST_MSG: &str =
21+
"At this time, all Tier 2 targets are [Tier 2 with Host Tools](#tier-2-with-host-tools).\n";
22+
23+
pub fn process_main_chapter(
24+
chapter: &mut Chapter,
25+
rustc_targets: &[Target],
26+
target_chapters: &HashMap<String, Chapter>,
27+
) {
28+
let targets = ByTier::from(rustc_targets);
29+
let mut new_content = String::new();
30+
31+
for line in chapter.content.lines() {
32+
match line.trim() {
33+
TIER_1_HOST_MARKER => {
34+
write_host_table(&mut new_content, &targets.tier1_host, &target_chapters)
35+
}
36+
TIER_1_NOHOST_MARKER => write_nohost_table(
37+
&mut new_content,
38+
&targets.tier1_nohost,
39+
&target_chapters,
40+
EMPTY_TIER_1_NOHOST_MSG,
41+
),
42+
TIER_2_HOST_MARKER => {
43+
write_host_table(&mut new_content, &targets.tier2_host, &target_chapters)
44+
}
45+
TIER_2_NOHOST_MARKER => write_nohost_table(
46+
&mut new_content,
47+
&targets.tier2_nohost,
48+
&target_chapters,
49+
EMPTY_TIER_2_NOHOST_MSG,
50+
),
51+
TIER_3_MARKER => write_tier3_table(&mut new_content, &targets.tier3, &target_chapters),
52+
_ => {
53+
new_content.push_str(line);
54+
new_content.push_str("\n");
55+
}
56+
}
57+
}
58+
59+
debug_dump("platform-support.md", &new_content);
60+
61+
chapter.content = new_content;
62+
}
63+
64+
fn write_host_table(
65+
out: &mut String,
66+
targets: &[&Target],
67+
target_chapters: &HashMap<String, Chapter>,
68+
) {
69+
out.push_str("target | notes\n-------|-------\n");
70+
for target in targets {
71+
write_target_tuple(out, target, target_chapters);
72+
_ = writeln!(out, " | {}", target.metadata.description.as_deref().unwrap_or(""));
73+
}
74+
}
75+
76+
fn write_nohost_table(
77+
out: &mut String,
78+
targets: &[&Target],
79+
target_chapters: &HashMap<String, Chapter>,
80+
empty_msg: &str,
81+
) {
82+
if targets.is_empty() {
83+
out.push_str(empty_msg);
84+
return;
85+
}
86+
87+
out.push_str("target | std | notes\n-------|:---:|-------\n");
88+
for target in targets {
89+
write_target_tuple(out, target, target_chapters);
90+
_ = writeln!(
91+
out,
92+
" | {} | {}",
93+
std_support_symbol(target.metadata.std),
94+
target.metadata.description.as_deref().unwrap_or("")
95+
);
96+
}
97+
}
98+
99+
fn write_tier3_table(
100+
out: &mut String,
101+
targets: &[&Target],
102+
target_chapters: &HashMap<String, Chapter>,
103+
) {
104+
out.push_str("target | std | host | notes\n-------|:---:|:----:|-------\n");
105+
for target in targets {
106+
write_target_tuple(out, target, target_chapters);
107+
_ = writeln!(
108+
out,
109+
" | {} | {} | {}",
110+
std_support_symbol(target.metadata.std),
111+
host_support_symbol(target.metadata.host_tools),
112+
target.metadata.description.as_deref().unwrap_or("")
113+
);
114+
}
115+
}
116+
117+
fn write_target_tuple(
118+
out: &mut String,
119+
target: &Target,
120+
target_chapters: &HashMap<String, Chapter>,
121+
) {
122+
let doc_chapter = target.metadata.doc_chapter.as_deref().unwrap_or(&*target.tuple);
123+
124+
if target_chapters.contains_key(doc_chapter) {
125+
_ = write!(out, "[`{}`](platform-support/{}.md)", target.tuple, doc_chapter);
126+
} else {
127+
_ = write!(out, "`{}`", target.tuple);
128+
}
129+
}
130+
131+
pub fn std_support_symbol(support: Option<bool>) -> &'static str {
132+
match support {
133+
Some(true) => "✓",
134+
Some(false) => "*",
135+
None => "?",
136+
}
137+
}
138+
139+
pub fn host_support_symbol(support: Option<bool>) -> &'static str {
140+
match support {
141+
Some(true) => "✓",
142+
Some(false) => "",
143+
None => "?",
144+
}
145+
}
146+
147+
pub fn debug_dump<P: AsRef<Path>, C: AsRef<[u8]>>(path: P, contents: C) {
148+
if let Some(dir) = env::var_os("MDBOOK_RUSTC_DEBUG_DUMP_DIR")
149+
&& !dir.is_empty()
150+
{
151+
let mut dump_path = PathBuf::from(dir);
152+
dump_path.push(path.as_ref());
153+
fs::create_dir_all(dump_path.parent().unwrap()).unwrap();
154+
fs::write(dump_path, contents).unwrap();
155+
}
156+
}

src/tools/mdbook-rustc/src/main.rs

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
use std::collections::HashMap;
2+
use std::path::{Path, PathBuf};
3+
use std::process::Command;
4+
use std::{env, io};
5+
6+
use mdbook_preprocessor::errors::Error;
7+
use mdbook_rustc::process_main_chapter;
8+
use mdbook_rustc::target::{Target, all_from_rustc_json};
9+
10+
fn main() -> Result<(), Error> {
11+
let mut args = std::env::args().skip(1);
12+
match args.next().as_deref() {
13+
Some("supports") => {
14+
// Supports all renderers.
15+
return Ok(());
16+
}
17+
Some(arg) => {
18+
return Err(Error::msg(format!("unknown argument: {arg}")));
19+
}
20+
None => {}
21+
}
22+
23+
let (_ctx, mut book) = mdbook_preprocessor::parse_input(io::stdin().lock())?;
24+
let rustc_targets = load_rustc_targets()?;
25+
let target_chapters = book
26+
.chapters()
27+
.filter_map(|chapter| {
28+
Some((
29+
chapter
30+
.source_path
31+
.as_ref()
32+
.map(|p| p.strip_prefix("platform-support/").ok()?.file_stem()?.to_str())
33+
.flatten()?
34+
.to_owned(),
35+
chapter.clone(),
36+
))
37+
})
38+
.collect::<HashMap<_, _>>();
39+
40+
book.for_each_chapter_mut(|chapter| {
41+
if chapter.path.as_deref() == Some("platform-support.md".as_ref()) {
42+
process_main_chapter(chapter, &rustc_targets, &target_chapters);
43+
}
44+
});
45+
46+
serde_json::to_writer(io::stdout().lock(), &book)?;
47+
Ok(())
48+
}
49+
50+
fn load_rustc_targets() -> Result<Vec<Target>, Error> {
51+
let rustc = PathBuf::from(
52+
env::var("RUSTC").map_err(|_| Error::msg("must pass RUSTC env var pointing to rustc"))?,
53+
);
54+
all_from_rustc_json(serde_json::from_str(&rustc_stdout(
55+
&rustc,
56+
&["-Z", "unstable-options", "--print", "all-target-specs-json"],
57+
))?)
58+
}
59+
60+
fn rustc_stdout(rustc: &Path, args: &[&str]) -> String {
61+
let output = Command::new(rustc).args(args).env("RUSTC_BOOTSTRAP", "1").output().unwrap();
62+
if !output.status.success() {
63+
panic!(
64+
"rustc failed: {}, {}",
65+
output.status,
66+
String::from_utf8(output.stderr).unwrap_or_default()
67+
)
68+
}
69+
String::from_utf8(output.stdout).unwrap()
70+
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
use std::borrow::Cow;
2+
3+
use mdbook_preprocessor::errors::Error;
4+
use serde::Deserialize;
5+
use serde_json::Value;
6+
7+
type StaticCow<T> = Cow<'static, T>;
8+
9+
/// This is a partial mirror of [`rustc_target::spec::Target`], containing only the relevant field(s).
10+
/// It also has the target tuple itself added as a field for convenience.
11+
#[derive(Debug, Default, Deserialize)]
12+
#[serde(default)]
13+
#[serde(rename_all = "kebab-case")]
14+
pub struct Target {
15+
pub tuple: StaticCow<str>,
16+
pub metadata: TargetMetadata,
17+
}
18+
19+
/// This is a mirror of [`rustc_target::spec::TargetMetadata`], since we can't depend on rustc_target directly.
20+
#[derive(Debug, Default, Deserialize)]
21+
#[serde(default)]
22+
#[serde(deny_unknown_fields)]
23+
pub struct TargetMetadata {
24+
/// A short description of the target including platform requirements,
25+
/// for example "64-bit Linux (kernel 3.2+, glibc 2.17+)".
26+
pub description: Option<StaticCow<str>>,
27+
/// The tier of the target. 1, 2 or 3.
28+
pub tier: Option<u64>,
29+
/// Whether the Rust project ships host tools for a target.
30+
pub host_tools: Option<bool>,
31+
/// Whether a target has the `std` library. This is usually true for targets running
32+
/// on an operating system.
33+
pub std: Option<bool>,
34+
/// The name of the documentation chapter for this target. Defaults to the target tuple.
35+
pub doc_chapter: Option<StaticCow<str>>,
36+
}
37+
38+
pub struct ByTier<'a> {
39+
pub tier1_host: Vec<&'a Target>,
40+
pub tier1_nohost: Vec<&'a Target>,
41+
pub tier2_host: Vec<&'a Target>,
42+
pub tier2_nohost: Vec<&'a Target>,
43+
pub tier3: Vec<&'a Target>,
44+
}
45+
46+
impl<'a> From<&'a [Target]> for ByTier<'a> {
47+
fn from(value: &'a [Target]) -> Self {
48+
let mut tier1_host = Vec::new();
49+
let mut tier1_nohost = Vec::new();
50+
let mut tier2_host = Vec::new();
51+
let mut tier2_nohost = Vec::new();
52+
let mut tier3 = Vec::new();
53+
54+
for target in value {
55+
let host_tools = target.metadata.host_tools.unwrap_or(false);
56+
57+
match target.metadata.tier {
58+
Some(1) if host_tools => tier1_host.push(target),
59+
Some(1) => tier1_nohost.push(target),
60+
Some(2) if host_tools => tier2_host.push(target),
61+
Some(2) => tier2_nohost.push(target),
62+
Some(3) => tier3.push(target),
63+
Some(tier) => panic!("invalid target tier for '{}': {tier:?}", target.tuple),
64+
None => {
65+
eprintln!(
66+
"WARNING: target tier not specified for '{}', defaulting to tier 3",
67+
target.tuple
68+
);
69+
tier3.push(target);
70+
}
71+
}
72+
}
73+
74+
Self { tier1_host, tier1_nohost, tier2_host, tier2_nohost, tier3 }
75+
}
76+
}
77+
78+
pub fn all_from_rustc_json(json: Value) -> Result<Vec<Target>, Error> {
79+
let Value::Object(map) = json else {
80+
return Err(Error::msg("rustc didn't output a json object"));
81+
};
82+
let mut targets: Vec<Target> = Vec::with_capacity(map.len());
83+
84+
for (tuple, mut target) in map {
85+
let target_map = target.as_object_mut().ok_or(Error::msg("target with non-object spec"))?;
86+
let old = target_map.insert("tuple".to_owned(), Value::String(tuple));
87+
assert!(old.is_none(), "rustc_target::Target does not have a 'tuple' field");
88+
targets.push(serde_json::from_value(target)?);
89+
}
90+
91+
targets.sort_by(|a, b| a.tuple.cmp(&b.tuple));
92+
Ok(targets)
93+
}

src/tools/rustbook/Cargo.lock

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -786,6 +786,15 @@ dependencies = [
786786
"serde_json",
787787
]
788788

789+
[[package]]
790+
name = "mdbook-rustc"
791+
version = "0.0.0"
792+
dependencies = [
793+
"mdbook-preprocessor",
794+
"serde",
795+
"serde_json",
796+
]
797+
789798
[[package]]
790799
name = "mdbook-spec"
791800
version = "0.0.0"
@@ -1161,6 +1170,7 @@ dependencies = [
11611170
"clap",
11621171
"mdbook-driver",
11631172
"mdbook-i18n-helpers",
1173+
"mdbook-rustc",
11641174
"mdbook-spec",
11651175
"mdbook-trpl",
11661176
"tracing-subscriber",

src/tools/rustbook/Cargo.toml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
[workspace]
2-
members = ["../error_index_generator"]
2+
members = [
3+
# tidy-alphabetical-start
4+
"../error_index_generator",
5+
"../mdbook-rustc"
6+
# tidy-alphabetical-end
7+
]
38

49
[package]
510
name = "rustbook"
@@ -11,6 +16,7 @@ edition = "2021"
1116
clap = { version = "4.0.32", features = ["cargo"] }
1217
mdbook-driver = { version = "0.5.2", features = ["search"] }
1318
mdbook-i18n-helpers = "0.4.0"
19+
mdbook-rustc = { path = "../mdbook-rustc" }
1420
mdbook-spec = { path = "../../doc/reference/tools/mdbook-spec" }
1521
mdbook-trpl = { path = "../../doc/book/packages/mdbook-trpl" }
1622
tracing-subscriber = { version = "0.3.20", features = ["env-filter"] }

0 commit comments

Comments
 (0)