This commit is contained in:
nora 2024-03-16 11:08:34 +01:00
parent a6884454c0
commit 720b23bd8e
7 changed files with 249 additions and 295 deletions

View file

@ -2,33 +2,27 @@ mod parse;
mod render;
use std::{
io,
path::{Path, PathBuf},
process::Command,
};
use eyre::{bail, Context, OptionExt, Result};
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 {
name: String,
maintainers: Vec<String>,
sections: Vec<(String, String)>,
tier: Option<Tier>,
// TODO: Make this mandatory.
metadata: Option<TargetMetadata>,
}
/// Metadata for the table
struct TargetMetadata {
notes: String,
std: TriStateBool,
host: TriStateBool,
footnotes: Vec<Footnote>,
footnotes: Vec<String>,
}
/// 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] = &[
"Overview",
"Requirements",
@ -38,51 +32,34 @@ const SECTIONS: &[&str] = &[
"Building Rust programs",
];
fn is_in_rust_lang_rust() -> bool {
std::env::var("RUST_LANG_RUST") == Ok("1".to_owned())
}
fn main() -> Result<()> {
let args = std::env::args().collect::<Vec<_>>();
let input_dir = args
.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
.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 =
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 = 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))
.wrap_err("failed loading target_info")?
.into_iter()
.map(|info| {
let metadata_used = vec![false; info.metadata.len()];
TargetPatternEntry {
info,
used: false,
metadata_used,
}
TargetPatternEntry { info, used: false, footnotes_used: metadata_used }
})
.collect::<Vec<_>>();
eprintln!("Collecting rustc information");
let rustc_infos = targets
.par_iter()
.map(|target| rustc_target_info(&rustc, target))
.collect::<Vec<_>>();
let rustc_infos =
targets.iter().map(|target| rustc_target_info(&rustc, target)).collect::<Vec<_>>();
let targets = targets
.into_iter()
@ -90,42 +67,39 @@ fn main() -> Result<()> {
.zip(rustc_infos)
.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 {
let doc = render::render_target_md(info, rustc_info);
std::fs::write(
Path::new(output_src)
.join("platform-support")
.join("targets")
.join(format!("{}.md", info.name)),
doc,
)
.wrap_err("writing target file")?;
if !check_only {
std::fs::write(targets_dir.join(format!("{}.md", info.name)), doc)
.wrap_err("writing target file")?;
}
}
for target_pattern in info_patterns {
if !target_pattern.used {
bail!(
"target pattern `{}` was never used",
target_pattern.info.pattern
);
bail!("target pattern `{}` was never used", target_pattern.info.pattern);
}
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 {
bail!(
"in target pattern `{}`, the metadata pattern `{}` was never used",
"in target pattern `{}`, the footnotes for target `{}` were never used",
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");
Ok(())
@ -134,7 +108,7 @@ fn main() -> Result<()> {
struct TargetPatternEntry {
info: ParsedTargetInfoFile,
used: bool,
metadata_used: Vec<bool>,
footnotes_used: Vec<bool>,
}
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 metadata = None;
let mut footnotes = Vec::new();
for target_pattern_entry in info_patterns {
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 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());
}
for (section_name, content) in &target_pattern.sections {
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()));
}
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() {
if glob_match::glob_match(&metadata_pattern.pattern, target) {
target_pattern_entry.metadata_used[i] = true;
if metadata_pattern.target == target {
target_pattern_entry.footnotes_used[i] = true;
if metadata.is_some() {
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 {
name: target.to_owned(),
maintainers,
tier,
sections,
metadata,
}
TargetDocs { name: target.to_owned(), maintainers, sections, footnotes }
}
/// Information about a target obtained from rustc.
struct RustcTargetInfo {
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.
@ -213,7 +204,19 @@ fn rustc_target_info(rustc: &Path, target: &str) -> RustcTargetInfo {
}
})
.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 {

View file

@ -2,7 +2,7 @@
use eyre::{bail, OptionExt, Result, WrapErr};
use serde::Deserialize;
use std::{fs::DirEntry, path::Path};
use std::{collections::HashMap, fs::DirEntry, path::Path};
#[derive(Debug, PartialEq, Clone, Deserialize)]
pub enum Tier {
@ -20,9 +20,10 @@ pub struct ParsedTargetInfoFile {
pub tier: Option<Tier>,
pub maintainers: Vec<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)]
#[serde(deny_unknown_fields)]
struct Frontmatter {
@ -30,27 +31,19 @@ struct Frontmatter {
#[serde(default)]
maintainers: Vec<String>,
#[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)]
#[serde(deny_unknown_fields)]
pub struct ParsedTargetMetadata {
pub pattern: String,
pub notes: String,
pub std: TriStateBool,
pub host: TriStateBool,
pub struct TargetFootnotes {
pub target: String,
#[serde(default)]
pub footnotes: Vec<Footnote>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Footnote {
pub name: String,
pub content: String,
pub footnotes: Vec<String>,
}
// IMPORTANT: This is also documented in the README, keep it in sync.
#[derive(Debug, PartialEq, Clone, Copy, Deserialize)]
#[serde(rename_all = "snake_case")]
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> {
let mut frontmatter_splitter = content.split("---\n");
let frontmatter = frontmatter_splitter
.nth(1)
.ok_or_eyre("missing frontmatter")?;
let frontmatter = frontmatter_splitter.nth(1).ok_or_eyre("missing frontmatter")?;
let frontmatter_line_count = frontmatter.lines().count() + 2; // 2 from ---
@ -149,99 +140,16 @@ fn parse_file(name: &str, content: &str) -> Result<ParsedTargetInfoFile> {
}
}
sections
.iter_mut()
.for_each(|section| section.1 = section.1.trim().to_owned());
sections.iter_mut().for_each(|section| section.1 = section.1.trim().to_owned());
Ok(ParsedTargetInfoFile {
pattern: name.to_owned(),
maintainers: frontmatter.maintainers,
tier: frontmatter.tier,
sections,
metadata: frontmatter.metadata,
footnotes: frontmatter.footnotes,
})
}
#[cfg(test)]
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(),
),
]
);
}
}
mod tests;

80
src/parse/tests.rs Normal file
View 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(),
),
]
);
}

View file

@ -2,16 +2,13 @@ use eyre::{Context, OptionExt, Result};
use std::{fs, path::Path};
use crate::{
is_in_rust_lang_rust,
parse::{Footnote, Tier, TriStateBool},
RustcTargetInfo, TargetDocs,
};
impl TargetDocs {
fn has_host_tools(&self) -> bool {
self.metadata
.as_ref()
.map_or(false, |meta| meta.host == TriStateBool::True)
self.metadata.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);
for section_name in crate::SECTIONS {
let value = target
.sections
.iter()
.find(|(name, _)| name == section_name);
let value = target.sections.iter().find(|(name, _)| name == section_name);
let section_content = match value {
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.
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 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 =
replace_section(&old_targets, "TARGET", &target_list).wrap_err("replacing targets.md")?;
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")?;
if !check_only {
fs::write(targets_file, new_targets).wrap_err("writing targets.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")?;
let platform_support_main_new =
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(())
}
@ -227,47 +210,35 @@ struct TierTable {
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 all_footnotes = Vec::new();
let targets = targets
.into_iter()
.filter(|target| (table.filter)(&target.0));
let targets = targets.into_iter().filter(|target| (table.filter)(&target.0));
for (target, _) in targets {
let meta = target.metadata.as_ref();
let mut notes = meta
.map(|meta| meta.notes.as_str())
.unwrap_or("unknown")
.to_owned();
let mut notes = meta.map(|meta| meta.notes.as_str()).unwrap_or("unknown").to_owned();
if meta.map_or(false, |meta| !meta.footnotes.is_empty()) {
let footnotes = &meta.unwrap().footnotes;
all_footnotes.extend(footnotes);
let footnotes_str = footnotes
.iter()
.map(|footnote| footnote.reference())
.collect::<Vec<_>>()
.join(" ");
let footnotes_str =
footnotes.iter().map(|footnote| footnote.reference()).collect::<Vec<_>>().join(" ");
notes = format!("{notes} {footnotes_str}");
}
let std = if table.include_std {
let std = meta
.map(|meta| render_table_tri_state_bool(meta.std))
.unwrap_or("?");
let std = meta.map(|meta| render_table_tri_state_bool(meta.std)).unwrap_or("?");
format!(" | {std}")
} else {
String::new()
};
let host = if table.include_host {
let host = meta
.map(|meta| render_table_tri_state_bool(meta.host))
.unwrap_or("?");
let host = meta.map(|meta| render_table_tri_state_bool(meta.host)).unwrap_or("?");
format!(" | {host}")
} else {
String::new()