mirror of
https://github.com/storytold/figcraft.git
synced 2026-10-25 02:47:02 -06:00
175 lines
7.6 KiB
Rust
175 lines
7.6 KiB
Rust
//! 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/<code>.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::<Value>(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::<toml::Table>().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::<toml::Table>().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<T>(path: &std::path::Path, fallback: &str, parse: impl Fn(&str) -> Result<T, String>) -> Option<T> {
|
|
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<Value> {
|
|
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
|
|
}
|