rewrite it all

This commit is contained in:
nora 2024-02-07 18:38:36 +01:00
parent 761818fda2
commit b90a97face
6 changed files with 321 additions and 129 deletions

View file

@ -1,23 +1,30 @@
mod parse;
use std::{
io,
path::{Path, PathBuf},
process::Command,
};
use parse::ParsedTargetInfoFile;
use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
/// Information about a target obtained from `target_info.toml``.
struct TargetDocs {
name: String,
maintainers: Vec<String>,
requirements: Option<String>,
testing: Option<String>,
building_the_target: Option<String>,
cross_compilation: Option<String>,
building_rust_programs: Option<String>,
tier: u8,
sections: Vec<(String, String)>,
tier: String,
}
const SECTIONS: &[&str] = &[
"Requirements",
"Testing",
"Building",
"Cross compilation",
"Building Rust programs",
];
fn main() {
let rustc =
PathBuf::from(std::env::var("RUSTC").expect("must pass RUSTC env var pointing to rustc"));
@ -31,7 +38,11 @@ fn main() {
e @ _ => e.unwrap(),
}
let mut info_patterns = load_target_info_patterns();
let mut info_patterns = parse::load_target_infos(Path::new("target_info"))
.unwrap()
.into_iter()
.map(|info| TargetPatternEntry { info, used: false })
.collect::<Vec<_>>();
eprintln!("Collecting rustc information");
let rustc_infos = targets
@ -92,19 +103,18 @@ fn render_target_md(target: &TargetDocs, rustc_info: &RustcTargetInfo) -> String
doc.push_str(&maintainers_str);
let mut section = |value: &Option<String>, name| {
for section_name in SECTIONS {
let value = target
.sections
.iter()
.find(|(name, _)| name == section_name);
let section_str = match value {
Some(value) => format!("## {name}\n{value}\n"),
None => format!("## {name}\nUnknown.\n"),
Some((name, value)) => format!("## {name}\n{value}\n"),
None => format!("## {section_name}\nUnknown.\n"),
};
doc.push_str(&section_str)
};
section(&target.requirements, "Requirements");
section(&target.testing, "Testing");
section(&target.building_the_target, "Building");
section(&target.cross_compilation, "Cross Compilation");
section(&target.building_rust_programs, "Building Rust Programs");
}
let cfg_text = rustc_info
.target_cfgs
@ -114,7 +124,8 @@ fn render_target_md(target: &TargetDocs, rustc_info: &RustcTargetInfo) -> String
.join("\n");
let cfg_text =
format!("This target defines the following target-specific cfg values:\n{cfg_text}\n");
section(&Some(cfg_text), "cfg");
doc.push_str(&format!("## cfg\n{cfg_text}\n"));
doc
}
@ -158,51 +169,16 @@ But as you might notice, all targets are actually present with a stub :3.
// TODO: Render the nice table showing off all targets and their tier.
}
#[derive(Debug, serde::Deserialize)]
#[serde(deny_unknown_fields)]
struct TargetInfoTable {
target: Vec<TargetInfoPattern>,
}
#[derive(Debug, serde::Deserialize)]
#[serde(deny_unknown_fields)]
struct TargetInfoPattern {
pattern: String,
#[serde(default)]
maintainers: Vec<String>,
tier: Option<u8>,
requirements: Option<String>,
testing: Option<String>,
building_the_target: Option<String>,
cross_compilation: Option<String>,
building_rust_programs: Option<String>,
}
struct TargetPatternEntry {
info: TargetInfoPattern,
info: ParsedTargetInfoFile,
used: bool,
}
fn load_target_info_patterns() -> Vec<TargetPatternEntry> {
let file = include_str!("../target_info.toml");
let table = toml::from_str::<TargetInfoTable>(file).unwrap();
table
.target
.into_iter()
.map(|info| TargetPatternEntry { info, used: false })
.collect()
}
/// Gets the target information from `target_info.toml` by applying all patterns that match.
fn target_info(info_patterns: &mut [TargetPatternEntry], target: &str) -> TargetDocs {
let mut tier = None;
let mut maintainers = Vec::new();
let mut requirements = None;
let mut testing = None;
let mut building_the_target = None;
let mut cross_compilation = None;
let mut building_rust_programs = None;
let mut sections = Vec::new();
for target_pattern in info_patterns {
if glob_match::glob_match(&target_pattern.info.pattern, target) {
@ -211,41 +187,28 @@ fn target_info(info_patterns: &mut [TargetPatternEntry], target: &str) -> Target
maintainers.extend_from_slice(&target_pattern.maintainers);
fn set_once<T: Clone>(
target: &str,
pattern_value: &Option<T>,
to_insert: &mut Option<T>,
name: &str,
) {
if let Some(pattern_value) = pattern_value {
if to_insert.is_some() {
panic!("target {target} inherits a {name} from multiple patterns, create a more specific pattern and add it there");
}
*to_insert = Some(pattern_value.clone());
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");
}
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");
}
sections.push((section_name.clone(), content.clone()));
}
#[rustfmt::skip]
{
set_once(target, &target_pattern.tier, &mut tier, "tier");
set_once(target, &target_pattern.requirements, &mut requirements, "requirements");
set_once(target, &target_pattern.testing, &mut testing, "testing");
set_once(target, &target_pattern.building_the_target, &mut building_the_target, "building_the_target");
set_once(target, &target_pattern.cross_compilation, &mut cross_compilation, "cross_compilation");
set_once(target, &target_pattern.building_rust_programs, &mut building_rust_programs, "building_rust_programs");
};
}
}
TargetDocs {
name: target.to_owned(),
maintainers,
requirements,
testing,
building_the_target,
cross_compilation,
building_rust_programs,
// tier: tier.expect(&format!("no tier found for target {target}")),
tier: tier.unwrap_or(0),
tier: tier.unwrap_or("UNKNOWN".to_owned()),
sections,
}
}

179
src/parse.rs Normal file
View file

@ -0,0 +1,179 @@
//! Suboptimal half-markdown parser that's just good-enough for this.
use eyre::{bail, OptionExt, Result, WrapErr};
use std::{fs::DirEntry, path::Path};
#[derive(Debug, PartialEq)]
pub struct ParsedTargetInfoFile {
pub pattern: String,
pub tier: Option<String>,
pub maintainers: Vec<String>,
pub sections: Vec<(String, String)>,
}
#[derive(serde::Deserialize)]
struct Frontmatter {
tier: Option<String>,
#[serde(default)]
maintainers: Vec<String>,
}
pub fn load_target_infos(directory: &Path) -> Result<Vec<ParsedTargetInfoFile>> {
let dir = std::fs::read_dir(directory).unwrap();
let mut infos = Vec::new();
for entry in dir {
let entry = entry?;
infos.push(
load_single_target_info(&entry)
.wrap_err_with(|| format!("loading {}", entry.path().display()))?,
)
}
Ok(infos)
}
fn load_single_target_info(entry: &DirEntry) -> Result<ParsedTargetInfoFile> {
let pattern = entry.file_name();
let name = pattern
.to_str()
.ok_or_eyre("file name is invalid utf8")?
.strip_suffix(".md")
.ok_or_eyre("target_info files must end with .md")?;
let content: String = std::fs::read_to_string(entry.path()).wrap_err("reading content")?;
parse_file(name, &content)
}
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 =
serde_yaml::from_str::<Frontmatter>(frontmatter).wrap_err("invalid frontmatter")?;
let body = frontmatter_splitter.next().ok_or_eyre("no body")?;
let mut sections = Vec::new();
for line in body.lines() {
if line.starts_with("#") {
if let Some(header) = line.strip_prefix("## ") {
if !crate::SECTIONS.contains(&header) {
bail!(
"`{header}` is not an allowed section name, must be one of {:?}",
super::SECTIONS
);
}
sections.push((header.to_owned(), String::new()));
} else {
bail!("the only allowed headings are `## `");
}
} else {
match sections.last_mut() {
Some((_, content)) => {
content.push_str(line);
content.push('\n');
}
None if line.trim().is_empty() => {}
None => bail!("line with content not allowed before the first heading"),
}
}
}
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,
})
}
#[cfg(test)]
mod tests {
#[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("1".to_owned()));
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(),
),
]
);
}
}