Files
craft-libs/crates/proof/build.rs

201 lines
10 KiB
Rust
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Embed the Language data from a craft-i18n checkout (storytold/craft-i18n, `language-data/`).
//!
//! `CRAFT_I18N_DIR`, an absolute path to a craft-i18n checkout: `$OUT_DIR/language_data.rs` holds
//! one constant per file in [`FILES`], each an `include_str!`/`include_bytes!` of
//! `$CRAFT_I18N_DIR/language-data/<path>`. Unset, or not a checkout, or a file missing (a warning):
//! that constant is empty, and the crate has no embedded Language data for it (English spelling
//! falls back to an installed Hunspell dictionary or the crate's own copy of the Moby word list, and
//! hyphenation that needs the data is off). With `CRAFT_I18N_REQUIRED=1` (releases) any of those
//! is an error instead.
//!
//! When every file in [`FILES`] this build embeds (English and the [`opted_in`] locales) is
//! there, the `craft_proof_data` cfg is set (the tests that pin
//! results of the data use it); when none is, `craft_proof_no_data` (the tests of a build without
//! the data). A partial data folder sets neither, so neither set of tests runs against it.
//!
//! `include_str!` takes a string, so a `CRAFT_I18N_DIR` that isn't valid UTF-8 can't be embedded:
//! it is treated like a folder without the data.
//!
//! English: SCOWL's en-US, en-GB (-ise), en-CA and en-AU dictionaries. The SCOWL notice is
//! embedded once (the four upstream READMEs differ only in their title and build line).
use std::fmt::Write as _;
use std::path::{Path, PathBuf};
/// (constant, path under `language-data/`, bytes rather than text). The first path part is the
/// locale: `en-*` and our own `*-extra.txt` supplements are always embedded, the others as
/// [`opted_in`] says.
const FILES: &[(&str, &str, bool)] = &[
("EN_US_DIC", "en-us/en-us.dic", true),
("EN_US_AFF", "en-us/spelling.aff", false),
("EN_US_SPELLING", "en-us/spelling.dic", false),
("EN_GB_AFF", "en-gb/spelling.aff", false),
("EN_GB_SPELLING", "en-gb/spelling.dic", false),
("EN_CA_AFF", "en-ca/spelling.aff", false),
("EN_CA_SPELLING", "en-ca/spelling.dic", false),
("EN_AU_AFF", "en-au/spelling.aff", false),
("EN_AU_SPELLING", "en-au/spelling.dic", false),
("SCOWL_NOTICE", "en-us/LICENCE-SCOWL.txt", false),
("EN_US_PAT", "en-us/hyphenation.pat", false),
("EXTRA_EN", "en-us/en-extra.txt", false),
("EN_VARIANTS", "en-us/en-variants.txt", false),
("PL_PAT", "pl/hyphenation.pat", false),
("PT_PAT", "pt/hyphenation.pat", false),
("IT_PAT", "it/hyphenation.pat", false),
("DE_PAT", "de/hyphenation.pat", false),
("EXTRA_RU", "ru/ru-extra.txt", false),
];
/// The locales embedded by default besides English: every carry-over locale, until Language
/// packs are published and pinned (CP4.4 then makes the default English only).
const DEFAULT_EMBED: &[&str] = &["de", "pl", "pt", "it", "ru"];
/// The locales this build embeds besides English: `CRAFT_PROOF_EMBED`, comma-separated lowercase
/// locales (`de,pl`), `all`, or `en` (English and our supplements only; the others come from
/// Language packs). Unset: [`DEFAULT_EMBED`].
fn opted_in() -> Vec<String> {
let v = std::env::var("CRAFT_PROOF_EMBED").unwrap_or_default();
let named: Vec<String> = v.split(',').map(|l| l.trim().to_ascii_lowercase()).filter(|l| !l.is_empty()).collect();
if named.is_empty() { DEFAULT_EMBED.iter().map(|l| (*l).to_string()).collect() } else { named }
}
/// Our own word supplements (`*-extra.txt`): MIT, tiny, embedded in every build whatever the
/// locale; only upstream data waits for a Language build or a pack.
fn is_ours(rel: &str) -> bool {
rel.ends_with("-extra.txt")
}
/// Is the file `rel` (of `locale`) embedded in this build?
fn embeds(rel: &str, locale: &str, opted: &[String]) -> bool {
is_ours(rel) || locale.starts_with("en-") || opted.iter().any(|o| o == locale || o == "all")
}
fn main() {
println!("cargo::rerun-if-changed=build.rs");
println!("cargo::rerun-if-env-changed=CRAFT_I18N_DIR");
println!("cargo::rerun-if-env-changed=CRAFT_I18N_REQUIRED");
println!("cargo::rerun-if-env-changed=CRAFT_PROOF_EMBED");
println!("cargo::rustc-check-cfg=cfg(craft_proof_data)");
println!("cargo::rustc-check-cfg=cfg(craft_proof_no_data)");
println!("cargo::rustc-check-cfg=cfg(craft_proof_embed, values(\"pl\", \"pt\", \"it\", \"de\", \"ru\"))");
let opted = opted_in();
let required = std::env::var("CRAFT_I18N_REQUIRED").is_ok_and(|v| v.trim() == "1");
let level = if required { "error" } else { "warning" };
let dir = match std::env::var_os("CRAFT_I18N_DIR").filter(|d| !d.is_empty()) {
Some(d) => match language_data(&PathBuf::from(d)) {
Ok(dir) => Some(dir),
Err(e) => {
println!("cargo::{level}=CRAFT_I18N_DIR: {e}; craft-proof embeds no Language data");
None
}
},
None => {
if required {
println!("cargo::error=CRAFT_I18N_REQUIRED=1 but CRAFT_I18N_DIR is unset: a release must embed the Language data");
} else {
println!(
"cargo::warning=CRAFT_I18N_DIR is unset: craft-proof embeds no Language data (English spelling falls back to system Hunspell or the built-in Moby word list; no hyphenation)"
);
}
None
}
};
let mut o = String::from("// Generated by craft-proof's build.rs from CRAFT_I18N_DIR/language-data. Do not edit.\n");
// Files this build embeds when present (English and the opted-in locales), and those embedded.
let (mut wanted_files, mut embedded) = (0, 0);
let mut locales: Vec<String> = Vec::new();
for &(name, rel, bytes) in FILES {
let locale = rel.split('/').next().unwrap_or_default();
let wanted = embeds(rel, locale, &opted);
wanted_files += usize::from(wanted);
let file = dir.as_ref().filter(|_| wanted).map(|d| d.join(rel)).filter(|f| f.is_file());
if let (Some(d), None, true) = (&dir, &file, wanted) {
println!("cargo::{level}=CRAFT_I18N_DIR: {} is missing; craft-proof embeds the rest without it", d.join(rel).display());
}
let (ty, empty, mac) = if bytes { ("&[u8]", "b\"\"", "include_bytes") } else { ("&str", "\"\"", "include_str") };
// `language_data` only hands back a UTF-8 folder, so this is the path as it is.
let value = match file.as_ref().and_then(|f| f.to_str()) {
Some(f) => {
println!("cargo::rerun-if-changed={f}");
embedded += 1;
if !locale.starts_with("en-") {
println!("cargo::rustc-cfg=craft_proof_embed=\"{locale}\"");
}
// The locales whose upstream data is embedded (our supplements don't count).
if !is_ours(rel) && !locales.iter().any(|l| l == locale) {
locales.push(locale.to_string());
}
// `{:?}` escapes the path (Windows backslashes) as a Rust string literal.
format!("{mac}!({f:?})")
}
None => empty.to_string(),
};
let _ = writeln!(o, "/// `language-data/{rel}` (empty when it isn't embedded).\npub(crate) static {name}: {ty} = {value};");
}
if embedded > 0 && embedded == wanted_files {
println!("cargo::rustc-cfg=craft_proof_data");
} else if embedded == 0 {
println!("cargo::rustc-cfg=craft_proof_no_data");
}
let _ = writeln!(o, "/// The locales whose Language data is embedded.\npub(crate) static EMBEDDED_LOCALES: &[&str] = &{locales:?};");
// The licence texts of the embedded locales (`language-data/<locale>/LICENCE*`), for About ›
// Language data.
let mut licences = String::new();
if let Some(d) = &dir {
for locale in &locales {
let mut files: Vec<PathBuf> = std::fs::read_dir(d.join(locale))
.into_iter()
.flatten()
.filter_map(Result::ok)
.map(|e| e.path())
.filter(|p| p.is_file() && p.file_name().is_some_and(|n| n.to_string_lossy().to_ascii_uppercase().starts_with("LICENCE")))
.collect();
files.sort();
for f in files {
println!("cargo::rerun-if-changed={}", f.display());
// `include_str!` fails the whole build on a file that isn't UTF-8 (a Latin-1
// licence text) and can't name a path that isn't either: leave such a file out.
let Some(path) = f.to_str() else {
println!("cargo::{level}=CRAFT_I18N_DIR: the path {} is not UTF-8; its licence text is not embedded", f.display());
continue;
};
if let Err(e) = std::fs::read_to_string(&f) {
println!("cargo::{level}=CRAFT_I18N_DIR: {path} can't be read as text ({e}); its licence text is not embedded");
continue;
}
let title = format!("{locale}: {}", f.file_name().map(|n| n.to_string_lossy().into_owned()).unwrap_or_default());
let _ = write!(licences, "({title:?}, include_str!({path:?})), ");
}
}
}
let _ = writeln!(
o,
"/// (title, text) of each embedded locale's licence files.\npub(crate) static EMBEDDED_LICENCES: &[(&str, &str)] = &[{licences}];"
);
let out = PathBuf::from(std::env::var_os("OUT_DIR").unwrap_or_default()).join("language_data.rs");
if let Err(e) = std::fs::write(&out, o) {
println!("cargo::error=writing {}: {e}", out.display());
}
}
/// `dir/language-data`, when `dir` is an absolute path to a checkout that has one.
fn language_data(dir: &Path) -> Result<PathBuf, String> {
// Build scripts run in the crate's directory and `include_str!` resolves against the generated
// file, so a relative path would mean two different places. Not canonicalized: on Windows that
// gives `\\?\C:\…`, which `include_str!` can't read.
if !dir.is_absolute() {
return Err(format!("{} is not an absolute path", dir.display()));
}
if dir.to_str().is_none() {
return Err(format!("{} is not valid UTF-8, which include_str! needs", dir.display()));
}
let data = dir.join("language-data");
if !data.is_dir() {
// Notice when the checkout gains its Language data.
println!("cargo::rerun-if-changed={}", dir.display());
return Err(format!("{} has no language-data/ (a craft-i18n checkout older than its Language data?)", dir.display()));
}
println!("cargo::rerun-if-changed={}", data.display());
Ok(data)
}