//! Bake the contributor credits into the binary as static Rust tables (craftrules //! standards/contributors.md). Nothing is read at run time. //! //! - contributors/contributors.json: commit stats per GitHub username, generated by craftrules //! scripts/contributors.py, which also marks whose people.toml entry it verified. //! - contributors/people.toml: the names people chose for themselves. A name is used only when the //! JSON says the entry was verified (`people_entry`). //! //! A missing or malformed file gives empty tables (or no names) and a warning. //! //! Also bakes the UI string catalogs (`locales/.toml`, see docs/translating.md) into sorted //! static tables for `i18n`. use std::fmt::Write as _; use serde_json::Value; fn main() { let dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../contributors"); let json = read(&dir.join("contributors.json"), "the About window shows no contributors", |t| { serde_json::from_str::(t).map_err(|e| e.to_string()) }) .unwrap_or(Value::Null); let people = read(&dir.join("people.toml"), "contributors are shown by username only", |t| t.parse::().map_err(|e| e.to_string())) .unwrap_or_default(); let out = std::path::PathBuf::from(std::env::var_os("OUT_DIR").unwrap_or_default()).join("credits.rs"); if let Err(e) = std::fs::write(&out, generate(&json, &people)) { println!("cargo::warning=writing {}: {e}", out.display()); } let out = out.with_file_name("locales.rs"); if let Err(e) = std::fs::write(&out, locales()) { println!("cargo::warning=writing {}: {e}", out.display()); } } /// `pub static LANGS: &[Lang]`: every `locales/*.toml`, English first, each table sorted by key. fn locales() -> String { let dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("locales"); println!("cargo::rerun-if-changed={}", dir.display()); let mut files: Vec<_> = std::fs::read_dir(&dir).into_iter().flatten().flatten().map(|e| e.path()).filter(|p| p.extension().is_some_and(|x| x == "toml")).collect(); // By code, so "es" comes before "es-419". files.sort_by_key(|p| (p.file_stem().is_none_or(|s| s != "en"), p.file_stem().map(ToOwned::to_owned))); let langs: Vec<_> = files .into_iter() .filter_map(|path| { let t = read(&path, "the language is left out", |t| t.parse::().map_err(|e| e.to_string()))?; Some((path.file_stem().map(|s| s.to_string_lossy().into_owned()).unwrap_or_default(), path, t)) }) .collect(); let mut o = String::from( "pub static LANGS: &[Lang] = &[ ", ); for (code, path, t) in &langs { let meta = |k: &str| t.get("meta").and_then(|m| m.get(k)).and_then(toml::Value::as_str).unwrap_or("").to_string(); // A regional catalog ("es-419") overlays its language's ("es"), if there is one. let parent = code.split_once('-').and_then(|(base, _)| langs.iter().position(|(c, ..)| c == base)).unwrap_or(0); let _ = writeln!(o, " Lang {{ code: {code:?}, name: {:?}, draft: {}, parent: {parent}, ui: &[", meta("name"), !meta("status").is_empty()); table(&mut o, path, t.get("ui")); let _ = writeln!(o, " ], commands: &["); table(&mut o, path, t.get("commands")); let _ = writeln!(o, " ], menu: &["); table(&mut o, path, t.get("menu")); let _ = writeln!(o, " ] }},"); } o.push_str( "]; ", ); o } /// The rows of one catalog table: strings or plural forms (`other` required). fn table(o: &mut String, path: &std::path::Path, t: Option<&toml::Value>) { let Some(t) = t.and_then(toml::Value::as_table) else { return }; // Sorted by key (byte order), as the lookup's binary search needs. let mut rows: Vec<_> = t.iter().collect(); rows.sort_by(|a, b| a.0.cmp(b.0)); for (k, v) in rows { let s = |k: &str| v.get(k).and_then(toml::Value::as_str); match (v.as_str(), s("one"), s("other")) { (Some(text), ..) => _ = writeln!(o, " ({k:?}, Text::Plain({text:?})),"), (None, one, Some(other)) => { _ = writeln!( o, " ({k:?}, Text::Plural {{ one: {}, few: {}, many: {}, other: {other:?} }}),", name(one), name(s("few")), name(s("many")), ); } _ => println!("cargo::warning={}: `{k}` is neither a string nor plural forms with `other`; left out", path.display()), } } } fn read(path: &std::path::Path, fallback: &str, parse: impl Fn(&str) -> Result) -> Option { println!("cargo::rerun-if-changed={}", path.display()); let r = std::fs::read_to_string(path).map_err(|e| e.to_string()).and_then(|t| parse(&t)); if let Err(e) = &r { println!("cargo::warning={}: {e}; {fallback}", path.display()); } r.ok() } /// A non-empty string field of someone's people.toml entry, as a Rust `Option<&str>` literal. fn name(v: Option<&str>) -> String { match v.map(str::trim).filter(|s| !s.is_empty()) { Some(x) => format!("Some({x:?})"), None => "None".into(), } } fn s(v: &Value, k: &str) -> String { format!("{:?}", v.get(k).and_then(Value::as_str).unwrap_or("")) } fn n(v: &Value, k: &str) -> u64 { v.get(k).and_then(Value::as_u64).unwrap_or(0) } fn rows(json: &Value, k: &str) -> Vec { json.get(k).and_then(Value::as_array).cloned().unwrap_or_default() } fn generate(json: &Value, people: &toml::Table) -> String { let mut o = String::new(); let _ = writeln!(o, "pub static TOTAL_COMMITS: u64 = {};", n(json, "commits")); let _ = writeln!(o, "pub static CONTRIBUTORS: &[Contributor] = &["); for c in rows(json, "contributors") { let login = c.get("login").and_then(Value::as_str).unwrap_or(""); if login.is_empty() { continue; } // Names only from a verified, self-chosen people.toml entry. let entry = people.get("people").and_then(|p| p.get(login)).filter(|_| c.get("people_entry").and_then(Value::as_bool) == Some(true)); let field = |k: &str| entry.and_then(|e| e.get(k)).and_then(toml::Value::as_str); let synced = entry.and_then(|e| e.get("sync_github_name")).and_then(toml::Value::as_bool) == Some(true); let display = field("display_name") .filter(|s| !s.trim().is_empty()) .or_else(|| if synced { c.get("github_name").and_then(Value::as_str) } else { None }); let _ = writeln!( o, " Contributor {{ login: {login:?}, display_name: {}, real_name: {}, prs: {}, commits: {}, lines_added: {}, lines_deleted: {}, binary_added: {}, binary_deleted: {}, first_commit: {}, last_commit: {} }},", name(display), name(field("real_name")), n(&c, "prs"), n(&c, "commits"), n(&c, "lines_added"), n(&c, "lines_deleted"), n(&c, "binary_added"), n(&c, "binary_deleted"), s(&c, "first_commit"), s(&c, "last_commit"), ); } let _ = writeln!(o, "];"); let _ = writeln!(o, "pub static MODELS: &[Model] = &["); for m in rows(json, "models") { let _ = writeln!( o, " Model {{ company: {}, model: {}, version: {}, commits: {}, lines_added: {}, lines_deleted: {} }},", s(&m, "company"), s(&m, "model"), s(&m, "version"), n(&m, "commits"), n(&m, "lines_added"), n(&m, "lines_deleted"), ); } let _ = writeln!(o, "];"); o }