mirror of
https://github.com/Noratrieb/target-tier-docs-experiment.git
synced 2026-01-14 08:30:12 +01:00
sync
This commit is contained in:
parent
a6884454c0
commit
720b23bd8e
7 changed files with 249 additions and 295 deletions
64
Cargo.lock
generated
64
Cargo.lock
generated
|
|
@ -2,37 +2,6 @@
|
||||||
# It is not intended for manual editing.
|
# It is not intended for manual editing.
|
||||||
version = 3
|
version = 3
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "crossbeam-deque"
|
|
||||||
version = "0.8.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d"
|
|
||||||
dependencies = [
|
|
||||||
"crossbeam-epoch",
|
|
||||||
"crossbeam-utils",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "crossbeam-epoch"
|
|
||||||
version = "0.9.18"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
|
|
||||||
dependencies = [
|
|
||||||
"crossbeam-utils",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "crossbeam-utils"
|
|
||||||
version = "0.8.19"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "either"
|
|
||||||
version = "1.9.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "equivalent"
|
name = "equivalent"
|
||||||
version = "1.0.1"
|
version = "1.0.1"
|
||||||
|
|
@ -107,26 +76,6 @@ dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "rayon"
|
|
||||||
version = "1.8.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "fa7237101a77a10773db45d62004a272517633fbcc3df19d96455ede1122e051"
|
|
||||||
dependencies = [
|
|
||||||
"either",
|
|
||||||
"rayon-core",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "rayon-core"
|
|
||||||
version = "1.12.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2"
|
|
||||||
dependencies = [
|
|
||||||
"crossbeam-deque",
|
|
||||||
"crossbeam-utils",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ryu"
|
name = "ryu"
|
||||||
version = "1.0.16"
|
version = "1.0.16"
|
||||||
|
|
@ -153,6 +102,17 @@ dependencies = [
|
||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_json"
|
||||||
|
version = "1.0.114"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c5f09b1bd632ef549eaa9f60a1f8de742bdbc698e6cee2095fc84dde5f549ae0"
|
||||||
|
dependencies = [
|
||||||
|
"itoa",
|
||||||
|
"ryu",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_yaml"
|
name = "serde_yaml"
|
||||||
version = "0.9.31"
|
version = "0.9.31"
|
||||||
|
|
@ -183,8 +143,8 @@ version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"eyre",
|
"eyre",
|
||||||
"glob-match",
|
"glob-match",
|
||||||
"rayon",
|
|
||||||
"serde",
|
"serde",
|
||||||
|
"serde_json",
|
||||||
"serde_yaml",
|
"serde_yaml",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,12 +2,13 @@
|
||||||
name = "target-docs"
|
name = "target-docs"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
license = "MIT OR Apache-2.0"
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
eyre = "0.6.12"
|
eyre = "0.6.12"
|
||||||
glob-match = "0.2.1"
|
glob-match = "0.2.1"
|
||||||
rayon = "1.8.1"
|
serde = { version = "1.0.185", features = ["derive"] }
|
||||||
serde = { version = "1.0.196", features = ["derive"] }
|
serde_json = "1.0.114"
|
||||||
serde_yaml = "0.9.31"
|
serde_yaml = "0.9.31"
|
||||||
|
|
|
||||||
69
README.md
69
README.md
|
|
@ -1,26 +1,57 @@
|
||||||
# target tier docs experiment
|
# target-docs
|
||||||
|
|
||||||
Experiment with automatically generating target tier docs.
|
This tool generates target documentation for all targets in the rustc book.
|
||||||
|
|
||||||
**View the deployment on <https://nilstrieb.github.io/target-tier-docs-experiment/>**
|
To achieve this, it uses a list of input markdown files provided in `src/doc/rustc/target_infos`. These files follow a strict format.
|
||||||
|
Every file covers a glob pattern of targets according to its file name.
|
||||||
|
|
||||||
## Problems
|
For every rustc target, we iterate through all the target infos and find matching globs.
|
||||||
|
When a glob matches, it extracts the h2 markdown sections and saves them for the target.
|
||||||
|
|
||||||
Currenly, the [target tier docs](https://doc.rust-lang.org/rustc/platform-support.html) are hard to navigate.
|
In the end, a page is generated for every target using these sections.
|
||||||
If you want to find information about a specific target, you first need to do some glob-search yourself and then also hope
|
Sections that are not provided are stubbed out. Currently, the sections are
|
||||||
that the target actually exists. This is super annoying (`:(`). Additionally, some targets are completely missing and there
|
|
||||||
is no reason to believe that the documentation won't suddenly start being out of date.
|
|
||||||
Pages are also inconsistent about which sections exist and which ones don't.
|
|
||||||
|
|
||||||
## Solution
|
- Overview
|
||||||
|
- Requirements
|
||||||
|
- Testing
|
||||||
|
- Building the target
|
||||||
|
- Cross compilation
|
||||||
|
- Building Rust programs
|
||||||
|
|
||||||
Enter: adding yet another preprocessing step.
|
In addition to the markdown sections, we also have extra data about the targets.
|
||||||
|
This is achieved through YAML frontmatter.
|
||||||
|
|
||||||
By adding yet another preprocessing step, we can solve all these problems.
|
The frontmatter follows the following format:
|
||||||
- Have a *dedicated* page for *every single* target including information about maintainers etc.
|
|
||||||
This makes it super easy to find things when there are problems.
|
```yaml
|
||||||
- Ensure that no target is completely undocumented, at least having a stub page pointing out the undocumentedness
|
tier: "1"
|
||||||
- Error when there is documentation that is not needed anymore, for example a removed target
|
maintainers: ["@someone"]
|
||||||
- Still keep the nice and easy-to-organize glob structure in the source
|
metadata:
|
||||||
- Use a unified structure for all the pages
|
- target: "i686-pc-windows-gnu"
|
||||||
- This also allows us to put more dynamic values into the docs. For example, I put `--print cfg` there, isn't that pretty!?
|
notes: "32-bit MinGW (Windows 7+)"
|
||||||
|
std: true
|
||||||
|
host: true
|
||||||
|
footnotes:
|
||||||
|
- name: "x86_32-floats-return-ABI"
|
||||||
|
content: |
|
||||||
|
Due to limitations of the C ABI, floating-point support on `i686` targets is non-compliant:
|
||||||
|
floating-point return values are passed via an x87 register, so NaN payload bits can be lost.
|
||||||
|
See [issue #114479][https://github.com/rust-lang/rust/issues/114479].
|
||||||
|
- name: "windows-support"
|
||||||
|
content: "Only Windows 10 currently undergoes automated testing. Earlier versions of Windows rely on testing and support from the community."
|
||||||
|
```
|
||||||
|
|
||||||
|
The top level keys are:
|
||||||
|
|
||||||
|
- `tier` (optional): `1`, `2` or `3`
|
||||||
|
- `maintainers` (optional): list of strings
|
||||||
|
|
||||||
|
There is also `metadata`, which is specific to every single target and not just a target "group" (the glob).
|
||||||
|
|
||||||
|
`metadata` has the following properties:
|
||||||
|
|
||||||
|
- `target`: the target name
|
||||||
|
- `notes`: a string containing a short description of the target for the table
|
||||||
|
- `std`: `true`, `false`, `unknown`, whether the target has `std`
|
||||||
|
- `host`: `true`, `false`, `unknown`, whether the target has host tools
|
||||||
|
- `footnotes` (optional): a list of footnotes, where every footnote has a `name` and `content`. These are used in the table.
|
||||||
|
|
|
||||||
137
src/main.rs
137
src/main.rs
|
|
@ -2,33 +2,27 @@ mod parse;
|
||||||
mod render;
|
mod render;
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
io,
|
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
process::Command,
|
process::Command,
|
||||||
};
|
};
|
||||||
|
|
||||||
use eyre::{bail, Context, OptionExt, Result};
|
use eyre::{bail, Context, OptionExt, Result};
|
||||||
use parse::{Footnote, ParsedTargetInfoFile, Tier, TriStateBool};
|
use parse::{Footnote, ParsedTargetInfoFile, Tier, TriStateBool};
|
||||||
use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
|
use serde::Deserialize;
|
||||||
|
|
||||||
/// Information about a target obtained from `target_info.toml``.
|
/// Information about a target obtained from the target_info markdown file.
|
||||||
struct TargetDocs {
|
struct TargetDocs {
|
||||||
name: String,
|
name: String,
|
||||||
maintainers: Vec<String>,
|
maintainers: Vec<String>,
|
||||||
sections: Vec<(String, String)>,
|
sections: Vec<(String, String)>,
|
||||||
tier: Option<Tier>,
|
footnotes: Vec<String>,
|
||||||
// TODO: Make this mandatory.
|
|
||||||
metadata: Option<TargetMetadata>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Metadata for the table
|
|
||||||
struct TargetMetadata {
|
|
||||||
notes: String,
|
|
||||||
std: TriStateBool,
|
|
||||||
host: TriStateBool,
|
|
||||||
footnotes: Vec<Footnote>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// All the sections that we want every doc page to have.
|
||||||
|
/// It may make sense to relax this into two kinds of sections, "required" sections
|
||||||
|
/// and "optional" sections, where required sections will get stubbed out when not found
|
||||||
|
/// while optional sections will just not exist when not found.
|
||||||
|
// IMPORTANT: This is also documented in the README, keep it in sync.
|
||||||
const SECTIONS: &[&str] = &[
|
const SECTIONS: &[&str] = &[
|
||||||
"Overview",
|
"Overview",
|
||||||
"Requirements",
|
"Requirements",
|
||||||
|
|
@ -38,51 +32,34 @@ const SECTIONS: &[&str] = &[
|
||||||
"Building Rust programs",
|
"Building Rust programs",
|
||||||
];
|
];
|
||||||
|
|
||||||
fn is_in_rust_lang_rust() -> bool {
|
|
||||||
std::env::var("RUST_LANG_RUST") == Ok("1".to_owned())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn main() -> Result<()> {
|
fn main() -> Result<()> {
|
||||||
let args = std::env::args().collect::<Vec<_>>();
|
let args = std::env::args().collect::<Vec<_>>();
|
||||||
let input_dir = args
|
let input_dir = args
|
||||||
.get(1)
|
.get(1)
|
||||||
.ok_or_eyre("first argument must be path to directory containing source md files")?;
|
.ok_or_eyre("first argument must be path to target_infos directory containing target source md files (src/doc/rustc/target_infos/)")?;
|
||||||
let output_src = args
|
let output_src = args
|
||||||
.get(2)
|
.get(2)
|
||||||
.ok_or_eyre("second argument must be path to `src` output directory")?;
|
.ok_or_eyre("second argument must be path to `src` output directory (src/doc/rustc/src)")?;
|
||||||
|
|
||||||
let rustc =
|
let rustc =
|
||||||
PathBuf::from(std::env::var("RUSTC").expect("must pass RUSTC env var pointing to rustc"));
|
PathBuf::from(std::env::var("RUSTC").expect("must pass RUSTC env var pointing to rustc"));
|
||||||
|
let check_only = std::env::var("TARGET_CHECK_ONLY") == Ok("1".to_owned());
|
||||||
|
|
||||||
let targets = rustc_stdout(&rustc, &["--print", "target-list"]);
|
let targets = rustc_stdout(&rustc, &["--print", "target-list"]);
|
||||||
let targets = targets.lines().collect::<Vec<_>>();
|
let targets = targets.lines().collect::<Vec<_>>();
|
||||||
|
|
||||||
if !is_in_rust_lang_rust() {
|
|
||||||
match std::fs::create_dir("targets/src") {
|
|
||||||
Ok(()) => {}
|
|
||||||
Err(e) if e.kind() == io::ErrorKind::AlreadyExists => {}
|
|
||||||
e @ _ => e.wrap_err("failed creating src dir")?,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut info_patterns = parse::load_target_infos(Path::new(input_dir))
|
let mut info_patterns = parse::load_target_infos(Path::new(input_dir))
|
||||||
.wrap_err("failed loading target_info")?
|
.wrap_err("failed loading target_info")?
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|info| {
|
.map(|info| {
|
||||||
let metadata_used = vec![false; info.metadata.len()];
|
let metadata_used = vec![false; info.metadata.len()];
|
||||||
TargetPatternEntry {
|
TargetPatternEntry { info, used: false, footnotes_used: metadata_used }
|
||||||
info,
|
|
||||||
used: false,
|
|
||||||
metadata_used,
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
eprintln!("Collecting rustc information");
|
eprintln!("Collecting rustc information");
|
||||||
let rustc_infos = targets
|
let rustc_infos =
|
||||||
.par_iter()
|
targets.iter().map(|target| rustc_target_info(&rustc, target)).collect::<Vec<_>>();
|
||||||
.map(|target| rustc_target_info(&rustc, target))
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
let targets = targets
|
let targets = targets
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
|
@ -90,42 +67,39 @@ fn main() -> Result<()> {
|
||||||
.zip(rustc_infos)
|
.zip(rustc_infos)
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
eprintln!("Rendering targets");
|
eprintln!("Rendering targets check_only={check_only}");
|
||||||
|
let targets_dir = Path::new(output_src).join("platform-support").join("targets");
|
||||||
|
if !check_only {
|
||||||
|
std::fs::create_dir_all(&targets_dir).wrap_err("creating platform-support/targets dir")?;
|
||||||
|
}
|
||||||
for (info, rustc_info) in &targets {
|
for (info, rustc_info) in &targets {
|
||||||
let doc = render::render_target_md(info, rustc_info);
|
let doc = render::render_target_md(info, rustc_info);
|
||||||
|
|
||||||
std::fs::write(
|
if !check_only {
|
||||||
Path::new(output_src)
|
std::fs::write(targets_dir.join(format!("{}.md", info.name)), doc)
|
||||||
.join("platform-support")
|
.wrap_err("writing target file")?;
|
||||||
.join("targets")
|
}
|
||||||
.join(format!("{}.md", info.name)),
|
|
||||||
doc,
|
|
||||||
)
|
|
||||||
.wrap_err("writing target file")?;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for target_pattern in info_patterns {
|
for target_pattern in info_patterns {
|
||||||
if !target_pattern.used {
|
if !target_pattern.used {
|
||||||
bail!(
|
bail!("target pattern `{}` was never used", target_pattern.info.pattern);
|
||||||
"target pattern `{}` was never used",
|
|
||||||
target_pattern.info.pattern
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for (used, meta) in
|
for (used, meta) in
|
||||||
std::iter::zip(target_pattern.metadata_used, target_pattern.info.metadata)
|
std::iter::zip(target_pattern.footnotes_used, target_pattern.info.metadata)
|
||||||
{
|
{
|
||||||
if !used {
|
if !used {
|
||||||
bail!(
|
bail!(
|
||||||
"in target pattern `{}`, the metadata pattern `{}` was never used",
|
"in target pattern `{}`, the footnotes for target `{}` were never used",
|
||||||
target_pattern.info.pattern,
|
target_pattern.info.pattern,
|
||||||
meta.pattern
|
meta.target
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
render::render_static(Path::new(output_src), &targets)?;
|
render::render_static(check_only, Path::new(output_src), &targets)?;
|
||||||
|
|
||||||
eprintln!("Finished generating target docs");
|
eprintln!("Finished generating target docs");
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
@ -134,7 +108,7 @@ fn main() -> Result<()> {
|
||||||
struct TargetPatternEntry {
|
struct TargetPatternEntry {
|
||||||
info: ParsedTargetInfoFile,
|
info: ParsedTargetInfoFile,
|
||||||
used: bool,
|
used: bool,
|
||||||
metadata_used: Vec<bool>,
|
footnotes_used: Vec<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn target_doc_info(info_patterns: &mut [TargetPatternEntry], target: &str) -> TargetDocs {
|
fn target_doc_info(info_patterns: &mut [TargetPatternEntry], target: &str) -> TargetDocs {
|
||||||
|
|
@ -143,6 +117,7 @@ fn target_doc_info(info_patterns: &mut [TargetPatternEntry], target: &str) -> Ta
|
||||||
let mut sections = Vec::new();
|
let mut sections = Vec::new();
|
||||||
|
|
||||||
let mut metadata = None;
|
let mut metadata = None;
|
||||||
|
let mut footnotes = Vec::new();
|
||||||
|
|
||||||
for target_pattern_entry in info_patterns {
|
for target_pattern_entry in info_patterns {
|
||||||
if glob_match::glob_match(&target_pattern_entry.info.pattern, target) {
|
if glob_match::glob_match(&target_pattern_entry.info.pattern, target) {
|
||||||
|
|
@ -153,21 +128,34 @@ fn target_doc_info(info_patterns: &mut [TargetPatternEntry], target: &str) -> Ta
|
||||||
|
|
||||||
if let Some(pattern_value) = &target_pattern.tier {
|
if let Some(pattern_value) = &target_pattern.tier {
|
||||||
if tier.is_some() {
|
if tier.is_some() {
|
||||||
panic!("target {target} inherits a tier from multiple patterns, create a more specific pattern and add it there");
|
panic!(
|
||||||
|
"target {target} inherits a tier from multiple patterns, create a more specific pattern and add it there"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
tier = Some(pattern_value.clone());
|
tier = Some(pattern_value.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
for (section_name, content) in &target_pattern.sections {
|
for (section_name, content) in &target_pattern.sections {
|
||||||
if sections.iter().any(|(name, _)| name == section_name) {
|
if sections.iter().any(|(name, _)| name == section_name) {
|
||||||
panic!("target {target} inherits the section {section_name} from multiple patterns, create a more specific pattern and add it there");
|
panic!(
|
||||||
|
"target {target} inherits the section {section_name} from multiple patterns, create a more specific pattern and add it there"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
sections.push((section_name.clone(), content.clone()));
|
sections.push((section_name.clone(), content.clone()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(target_footnotes) = target_pattern.footnotes.get(target) {
|
||||||
|
target_pattern_entry.footnotes_used[i] = true;
|
||||||
|
|
||||||
|
if !footnotes.is_empty() {
|
||||||
|
panic!("target {target} is assigned metadata from more than one pattern");
|
||||||
|
}
|
||||||
|
footnotes = target_footnotes.clone();
|
||||||
|
}
|
||||||
|
|
||||||
for (i, metadata_pattern) in target_pattern.metadata.iter().enumerate() {
|
for (i, metadata_pattern) in target_pattern.metadata.iter().enumerate() {
|
||||||
if glob_match::glob_match(&metadata_pattern.pattern, target) {
|
if metadata_pattern.target == target {
|
||||||
target_pattern_entry.metadata_used[i] = true;
|
target_pattern_entry.footnotes_used[i] = true;
|
||||||
if metadata.is_some() {
|
if metadata.is_some() {
|
||||||
panic!("target {target} is assigned metadata from more than one pattern");
|
panic!("target {target} is assigned metadata from more than one pattern");
|
||||||
}
|
}
|
||||||
|
|
@ -182,18 +170,21 @@ fn target_doc_info(info_patterns: &mut [TargetPatternEntry], target: &str) -> Ta
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
TargetDocs {
|
TargetDocs { name: target.to_owned(), maintainers, sections, footnotes }
|
||||||
name: target.to_owned(),
|
|
||||||
maintainers,
|
|
||||||
tier,
|
|
||||||
sections,
|
|
||||||
metadata,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Information about a target obtained from rustc.
|
/// Information about a target obtained from rustc.
|
||||||
struct RustcTargetInfo {
|
struct RustcTargetInfo {
|
||||||
target_cfgs: Vec<(String, String)>,
|
target_cfgs: Vec<(String, String)>,
|
||||||
|
metadata: RustcTargetMetadata,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct RustcTargetMetadata {
|
||||||
|
description: Option<String>,
|
||||||
|
tier: Option<u8>,
|
||||||
|
host_tools: Option<bool>,
|
||||||
|
std: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get information about a target from rustc.
|
/// Get information about a target from rustc.
|
||||||
|
|
@ -213,7 +204,19 @@ fn rustc_target_info(rustc: &Path, target: &str) -> RustcTargetInfo {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
RustcTargetInfo { target_cfgs }
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct TargetJson {
|
||||||
|
metadata: RustcTargetMetadata,
|
||||||
|
}
|
||||||
|
|
||||||
|
let json_spec = rustc_stdout(
|
||||||
|
rustc,
|
||||||
|
&["-Zunstable-options", "--print", "target-spec-json", "--target", target],
|
||||||
|
);
|
||||||
|
let spec = serde_json::from_str::<TargetJson>(&json_spec);
|
||||||
|
|
||||||
|
RustcTargetInfo { target_cfgs, metadata: spec.metadata }
|
||||||
}
|
}
|
||||||
|
|
||||||
fn rustc_stdout(rustc: &Path, args: &[&str]) -> String {
|
fn rustc_stdout(rustc: &Path, args: &[&str]) -> String {
|
||||||
|
|
|
||||||
118
src/parse.rs
118
src/parse.rs
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
use eyre::{bail, OptionExt, Result, WrapErr};
|
use eyre::{bail, OptionExt, Result, WrapErr};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use std::{fs::DirEntry, path::Path};
|
use std::{collections::HashMap, fs::DirEntry, path::Path};
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Clone, Deserialize)]
|
#[derive(Debug, PartialEq, Clone, Deserialize)]
|
||||||
pub enum Tier {
|
pub enum Tier {
|
||||||
|
|
@ -20,9 +20,10 @@ pub struct ParsedTargetInfoFile {
|
||||||
pub tier: Option<Tier>,
|
pub tier: Option<Tier>,
|
||||||
pub maintainers: Vec<String>,
|
pub maintainers: Vec<String>,
|
||||||
pub sections: Vec<(String, String)>,
|
pub sections: Vec<(String, String)>,
|
||||||
pub metadata: Vec<ParsedTargetMetadata>,
|
pub footnotes: HashMap<String, Vec<String>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IMPORTANT: This is also documented in the README, keep it in sync.
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
#[serde(deny_unknown_fields)]
|
#[serde(deny_unknown_fields)]
|
||||||
struct Frontmatter {
|
struct Frontmatter {
|
||||||
|
|
@ -30,27 +31,19 @@ struct Frontmatter {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
maintainers: Vec<String>,
|
maintainers: Vec<String>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
metadata: Vec<ParsedTargetMetadata>,
|
footnotes: HashMap<String, Vec<String>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IMPORTANT: This is also documented in the README, keep it in sync.
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
#[serde(deny_unknown_fields)]
|
#[serde(deny_unknown_fields)]
|
||||||
pub struct ParsedTargetMetadata {
|
pub struct TargetFootnotes {
|
||||||
pub pattern: String,
|
pub target: String,
|
||||||
pub notes: String,
|
|
||||||
pub std: TriStateBool,
|
|
||||||
pub host: TriStateBool,
|
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub footnotes: Vec<Footnote>,
|
pub footnotes: Vec<String>,
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
|
||||||
#[serde(deny_unknown_fields)]
|
|
||||||
pub struct Footnote {
|
|
||||||
pub name: String,
|
|
||||||
pub content: String,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IMPORTANT: This is also documented in the README, keep it in sync.
|
||||||
#[derive(Debug, PartialEq, Clone, Copy, Deserialize)]
|
#[derive(Debug, PartialEq, Clone, Copy, Deserialize)]
|
||||||
#[serde(rename_all = "snake_case")]
|
#[serde(rename_all = "snake_case")]
|
||||||
pub enum TriStateBool {
|
pub enum TriStateBool {
|
||||||
|
|
@ -89,9 +82,7 @@ fn load_single_target_info(entry: &DirEntry) -> Result<ParsedTargetInfoFile> {
|
||||||
fn parse_file(name: &str, content: &str) -> Result<ParsedTargetInfoFile> {
|
fn parse_file(name: &str, content: &str) -> Result<ParsedTargetInfoFile> {
|
||||||
let mut frontmatter_splitter = content.split("---\n");
|
let mut frontmatter_splitter = content.split("---\n");
|
||||||
|
|
||||||
let frontmatter = frontmatter_splitter
|
let frontmatter = frontmatter_splitter.nth(1).ok_or_eyre("missing frontmatter")?;
|
||||||
.nth(1)
|
|
||||||
.ok_or_eyre("missing frontmatter")?;
|
|
||||||
|
|
||||||
let frontmatter_line_count = frontmatter.lines().count() + 2; // 2 from ---
|
let frontmatter_line_count = frontmatter.lines().count() + 2; // 2 from ---
|
||||||
|
|
||||||
|
|
@ -149,99 +140,16 @@ fn parse_file(name: &str, content: &str) -> Result<ParsedTargetInfoFile> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sections
|
sections.iter_mut().for_each(|section| section.1 = section.1.trim().to_owned());
|
||||||
.iter_mut()
|
|
||||||
.for_each(|section| section.1 = section.1.trim().to_owned());
|
|
||||||
|
|
||||||
Ok(ParsedTargetInfoFile {
|
Ok(ParsedTargetInfoFile {
|
||||||
pattern: name.to_owned(),
|
pattern: name.to_owned(),
|
||||||
maintainers: frontmatter.maintainers,
|
maintainers: frontmatter.maintainers,
|
||||||
tier: frontmatter.tier,
|
tier: frontmatter.tier,
|
||||||
sections,
|
sections,
|
||||||
metadata: frontmatter.metadata,
|
footnotes: frontmatter.footnotes,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests;
|
||||||
use crate::parse::Tier;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn no_frontmatter() {
|
|
||||||
let name = "archlinux-unknown-linux-gnu.md"; // arch linux is an arch, right?
|
|
||||||
let content = "";
|
|
||||||
assert!(super::parse_file(name, content).is_err());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn invalid_section() {
|
|
||||||
let name = "6502-nintendo-nes.md";
|
|
||||||
let content = "
|
|
||||||
---
|
|
||||||
---
|
|
||||||
|
|
||||||
## Not A Real Section
|
|
||||||
";
|
|
||||||
|
|
||||||
assert!(super::parse_file(name, content).is_err());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn wrong_header() {
|
|
||||||
let name = "x86_64-known-linux-gnu.md";
|
|
||||||
let content = "
|
|
||||||
---
|
|
||||||
---
|
|
||||||
|
|
||||||
# x86_64-known-linux-gnu
|
|
||||||
";
|
|
||||||
|
|
||||||
assert!(super::parse_file(name, content).is_err());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse_correctly() {
|
|
||||||
let name = "cat-unknown-linux-gnu.md";
|
|
||||||
let content = r#"
|
|
||||||
---
|
|
||||||
tier: "1" # first-class cats
|
|
||||||
maintainers: ["who maintains the cat?"]
|
|
||||||
---
|
|
||||||
## Requirements
|
|
||||||
|
|
||||||
This target mostly just meows and doesn't do much.
|
|
||||||
|
|
||||||
## Testing
|
|
||||||
|
|
||||||
You can pet the cat and it might respond positively.
|
|
||||||
|
|
||||||
## Cross compilation
|
|
||||||
|
|
||||||
If you're on a dog system, there might be conflicts with the cat, be careful.
|
|
||||||
But it should be possible.
|
|
||||||
"#;
|
|
||||||
|
|
||||||
let info = super::parse_file(name, content).unwrap();
|
|
||||||
|
|
||||||
assert_eq!(info.maintainers, vec!["who maintains the cat?"]);
|
|
||||||
assert_eq!(info.pattern, name);
|
|
||||||
assert_eq!(info.tier, Some(Tier::One));
|
|
||||||
assert_eq!(
|
|
||||||
info.sections,
|
|
||||||
vec![
|
|
||||||
(
|
|
||||||
"Requirements".to_owned(),
|
|
||||||
"This target mostly just meows and doesn't do much.".to_owned(),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"Testing".to_owned(),
|
|
||||||
"You can pet the cat and it might respond positively.".to_owned(),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"Cross compilation".to_owned(),
|
|
||||||
"If you're on a dog system, there might be conflicts with the cat, be careful.\nBut it should be possible.".to_owned(),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
80
src/parse/tests.rs
Normal file
80
src/parse/tests.rs
Normal file
|
|
@ -0,0 +1,80 @@
|
||||||
|
use crate::parse::Tier;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn no_frontmatter() {
|
||||||
|
let name = "archlinux-unknown-linux-gnu.md"; // arch linux is an arch, right?
|
||||||
|
let content = "";
|
||||||
|
assert!(super::parse_file(name, content).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn invalid_section() {
|
||||||
|
let name = "6502-nintendo-nes.md";
|
||||||
|
let content = "
|
||||||
|
---
|
||||||
|
---
|
||||||
|
|
||||||
|
## Not A Real Section
|
||||||
|
";
|
||||||
|
|
||||||
|
assert!(super::parse_file(name, content).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn wrong_header() {
|
||||||
|
let name = "x86_64-known-linux-gnu.md";
|
||||||
|
let content = "
|
||||||
|
---
|
||||||
|
---
|
||||||
|
|
||||||
|
# x86_64-known-linux-gnu
|
||||||
|
";
|
||||||
|
|
||||||
|
assert!(super::parse_file(name, content).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_correctly() {
|
||||||
|
let name = "cat-unknown-linux-gnu.md";
|
||||||
|
let content = r#"
|
||||||
|
---
|
||||||
|
tier: "1" # first-class cats
|
||||||
|
maintainers: ["who maintains the cat?"]
|
||||||
|
---
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
This target mostly just meows and doesn't do much.
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
You can pet the cat and it might respond positively.
|
||||||
|
|
||||||
|
## Cross compilation
|
||||||
|
|
||||||
|
If you're on a dog system, there might be conflicts with the cat, be careful.
|
||||||
|
But it should be possible.
|
||||||
|
"#;
|
||||||
|
|
||||||
|
let info = super::parse_file(name, content).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(info.maintainers, vec!["who maintains the cat?"]);
|
||||||
|
assert_eq!(info.pattern, name);
|
||||||
|
assert_eq!(info.tier, Some(Tier::One));
|
||||||
|
assert_eq!(
|
||||||
|
info.sections,
|
||||||
|
vec![
|
||||||
|
(
|
||||||
|
"Requirements".to_owned(),
|
||||||
|
"This target mostly just meows and doesn't do much.".to_owned(),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Testing".to_owned(),
|
||||||
|
"You can pet the cat and it might respond positively.".to_owned(),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Cross compilation".to_owned(),
|
||||||
|
"If you're on a dog system, there might be conflicts with the cat, be careful.\nBut it should be possible.".to_owned(),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -2,16 +2,13 @@ use eyre::{Context, OptionExt, Result};
|
||||||
use std::{fs, path::Path};
|
use std::{fs, path::Path};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
is_in_rust_lang_rust,
|
|
||||||
parse::{Footnote, Tier, TriStateBool},
|
parse::{Footnote, Tier, TriStateBool},
|
||||||
RustcTargetInfo, TargetDocs,
|
RustcTargetInfo, TargetDocs,
|
||||||
};
|
};
|
||||||
|
|
||||||
impl TargetDocs {
|
impl TargetDocs {
|
||||||
fn has_host_tools(&self) -> bool {
|
fn has_host_tools(&self) -> bool {
|
||||||
self.metadata
|
self.metadata.as_ref().map_or(false, |meta| meta.host == TriStateBool::True)
|
||||||
.as_ref()
|
|
||||||
.map_or(false, |meta| meta.host == TriStateBool::True)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -64,10 +61,7 @@ pub fn render_target_md(target: &TargetDocs, rustc_info: &RustcTargetInfo) -> St
|
||||||
section("Maintainers", &maintainers_content);
|
section("Maintainers", &maintainers_content);
|
||||||
|
|
||||||
for section_name in crate::SECTIONS {
|
for section_name in crate::SECTIONS {
|
||||||
let value = target
|
let value = target.sections.iter().find(|(name, _)| name == section_name);
|
||||||
.sections
|
|
||||||
.iter()
|
|
||||||
.find(|(name, _)| name == section_name);
|
|
||||||
|
|
||||||
let section_content = match value {
|
let section_content = match value {
|
||||||
Some((_, value)) => value.clone(),
|
Some((_, value)) => value.clone(),
|
||||||
|
|
@ -112,7 +106,11 @@ fn replace_section(prev_content: &str, section_name: &str, replacement: &str) ->
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Renders the non-target files like `SUMMARY.md` that depend on the target.
|
/// Renders the non-target files like `SUMMARY.md` that depend on the target.
|
||||||
pub fn render_static(src_output: &Path, targets: &[(TargetDocs, RustcTargetInfo)]) -> Result<()> {
|
pub fn render_static(
|
||||||
|
check_only: bool,
|
||||||
|
src_output: &Path,
|
||||||
|
targets: &[(TargetDocs, RustcTargetInfo)],
|
||||||
|
) -> Result<()> {
|
||||||
let targets_file = src_output.join("platform-support").join("targets.md");
|
let targets_file = src_output.join("platform-support").join("targets.md");
|
||||||
let old_targets = fs::read_to_string(&targets_file).wrap_err("reading summary file")?;
|
let old_targets = fs::read_to_string(&targets_file).wrap_err("reading summary file")?;
|
||||||
|
|
||||||
|
|
@ -125,26 +123,8 @@ pub fn render_static(src_output: &Path, targets: &[(TargetDocs, RustcTargetInfo)
|
||||||
let new_targets =
|
let new_targets =
|
||||||
replace_section(&old_targets, "TARGET", &target_list).wrap_err("replacing targets.md")?;
|
replace_section(&old_targets, "TARGET", &target_list).wrap_err("replacing targets.md")?;
|
||||||
|
|
||||||
fs::write(targets_file, new_targets).wrap_err("writing targets.md")?;
|
if !check_only {
|
||||||
|
fs::write(targets_file, new_targets).wrap_err("writing targets.md")?;
|
||||||
if !is_in_rust_lang_rust() {
|
|
||||||
fs::write(
|
|
||||||
"targets/src/information.md",
|
|
||||||
"\
|
|
||||||
# platform support generated
|
|
||||||
|
|
||||||
This is an experiment of what generated target tier documentation could look like.
|
|
||||||
|
|
||||||
See <https://github.com/Nilstrieb/target-tier-docs-experiment> for the source.
|
|
||||||
The README of the repo contains more information about the motivation and benefits.
|
|
||||||
|
|
||||||
Targets of interest with information filled out are any tvos targets like [aarch64-apple-tvos](./aarch64-apple-tvos.md)
|
|
||||||
and [powerpc64-ibm-aix](./powerpc64-ibm-aix.md).
|
|
||||||
|
|
||||||
But as you might notice, all targets are actually present with a stub :3.
|
|
||||||
",
|
|
||||||
)
|
|
||||||
.wrap_err("writing front page information about experiment")?;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let platform_support_main = src_output.join("platform-support.md");
|
let platform_support_main = src_output.join("platform-support.md");
|
||||||
|
|
@ -152,8 +132,11 @@ pub fn render_static(src_output: &Path, targets: &[(TargetDocs, RustcTargetInfo)
|
||||||
fs::read_to_string(&platform_support_main).wrap_err("reading platform-support.md")?;
|
fs::read_to_string(&platform_support_main).wrap_err("reading platform-support.md")?;
|
||||||
let platform_support_main_new =
|
let platform_support_main_new =
|
||||||
render_platform_support_tables(&platform_support_main_old, targets)?;
|
render_platform_support_tables(&platform_support_main_old, targets)?;
|
||||||
fs::write(platform_support_main, platform_support_main_new)
|
|
||||||
.wrap_err("writing platform-support.md")?;
|
if !check_only {
|
||||||
|
fs::write(platform_support_main, platform_support_main_new)
|
||||||
|
.wrap_err("writing platform-support.md")?;
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
@ -227,47 +210,35 @@ struct TierTable {
|
||||||
include_host: bool,
|
include_host: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_table<'a>(targets: &[(TargetDocs, RustcTargetInfo)], table: TierTable) -> Result<String> {
|
fn render_table(targets: &[(TargetDocs, RustcTargetInfo)], table: TierTable) -> Result<String> {
|
||||||
let mut rows = Vec::new();
|
let mut rows = Vec::new();
|
||||||
let mut all_footnotes = Vec::new();
|
let mut all_footnotes = Vec::new();
|
||||||
|
|
||||||
let targets = targets
|
let targets = targets.into_iter().filter(|target| (table.filter)(&target.0));
|
||||||
.into_iter()
|
|
||||||
.filter(|target| (table.filter)(&target.0));
|
|
||||||
|
|
||||||
for (target, _) in targets {
|
for (target, _) in targets {
|
||||||
let meta = target.metadata.as_ref();
|
let meta = target.metadata.as_ref();
|
||||||
|
|
||||||
let mut notes = meta
|
let mut notes = meta.map(|meta| meta.notes.as_str()).unwrap_or("unknown").to_owned();
|
||||||
.map(|meta| meta.notes.as_str())
|
|
||||||
.unwrap_or("unknown")
|
|
||||||
.to_owned();
|
|
||||||
|
|
||||||
if meta.map_or(false, |meta| !meta.footnotes.is_empty()) {
|
if meta.map_or(false, |meta| !meta.footnotes.is_empty()) {
|
||||||
let footnotes = &meta.unwrap().footnotes;
|
let footnotes = &meta.unwrap().footnotes;
|
||||||
all_footnotes.extend(footnotes);
|
all_footnotes.extend(footnotes);
|
||||||
let footnotes_str = footnotes
|
let footnotes_str =
|
||||||
.iter()
|
footnotes.iter().map(|footnote| footnote.reference()).collect::<Vec<_>>().join(" ");
|
||||||
.map(|footnote| footnote.reference())
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
.join(" ");
|
|
||||||
|
|
||||||
notes = format!("{notes} {footnotes_str}");
|
notes = format!("{notes} {footnotes_str}");
|
||||||
}
|
}
|
||||||
|
|
||||||
let std = if table.include_std {
|
let std = if table.include_std {
|
||||||
let std = meta
|
let std = meta.map(|meta| render_table_tri_state_bool(meta.std)).unwrap_or("?");
|
||||||
.map(|meta| render_table_tri_state_bool(meta.std))
|
|
||||||
.unwrap_or("?");
|
|
||||||
format!(" | {std}")
|
format!(" | {std}")
|
||||||
} else {
|
} else {
|
||||||
String::new()
|
String::new()
|
||||||
};
|
};
|
||||||
|
|
||||||
let host = if table.include_host {
|
let host = if table.include_host {
|
||||||
let host = meta
|
let host = meta.map(|meta| render_table_tri_state_bool(meta.host)).unwrap_or("?");
|
||||||
.map(|meta| render_table_tri_state_bool(meta.host))
|
|
||||||
.unwrap_or("?");
|
|
||||||
format!(" | {host}")
|
format!(" | {host}")
|
||||||
} else {
|
} else {
|
||||||
String::new()
|
String::new()
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue