v0.24.5
This commit is contained in:
commit
5e23e9af02
240 files changed
+99957
No files matched your search
@@ -0,0 +1,135 @@
|
||||
//! Build extensible language packs and pinned, authenticated Swagger UI assets.
|
||||
use serde_json::{json, Value};
|
||||
use std::{collections::HashSet, env, fs, path::PathBuf, process::Command};
|
||||
|
||||
// Prepare the actual embedded bytes here, not in a dependency's earlier build.
|
||||
// Cargo config discovery is deliberately irrelevant to the pinned default.
|
||||
fn swagger_assets(
|
||||
root: &std::path::Path,
|
||||
out: &std::path::Path,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
for name in [
|
||||
"SWAGGER_UI_DOWNLOAD_URL",
|
||||
"SWAGGER_UI_ARCHIVE_SHA256",
|
||||
"RUST_PROXY_PYTHON",
|
||||
"CARGO_NET_OFFLINE",
|
||||
] {
|
||||
println!("cargo:rerun-if-env-changed={name}");
|
||||
}
|
||||
let helper = root.join("scripts/swagger_assets.py");
|
||||
println!("cargo:rerun-if-changed={}", helper.display());
|
||||
let interpreters = match env::var_os("RUST_PROXY_PYTHON") {
|
||||
Some(value) if !value.is_empty() => vec![value],
|
||||
_ => vec!["python3".into(), "python".into()],
|
||||
};
|
||||
for interpreter in interpreters {
|
||||
match Command::new(&interpreter).arg(&helper).arg("--out-dir").arg(out).status() {
|
||||
Ok(status) if status.success() => return Ok(()),
|
||||
Ok(_) => return Err("Swagger UI asset preparation failed; see the diagnostic above and docs/API_ACCESS.md".into()),
|
||||
Err(error) if error.kind() == std::io::ErrorKind::NotFound => continue,
|
||||
Err(error) => return Err(error.into()),
|
||||
}
|
||||
}
|
||||
Err("Python 3.10+ is required for build assets; install python3 or set RUST_PROXY_PYTHON to its executable".into())
|
||||
}
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let root = PathBuf::from(
|
||||
env::var_os("CARGO_MANIFEST_DIR").ok_or("Cargo did not supply CARGO_MANIFEST_DIR")?,
|
||||
);
|
||||
let out = PathBuf::from(env::var_os("OUT_DIR").ok_or("Cargo did not supply OUT_DIR")?);
|
||||
swagger_assets(&root, &out)?;
|
||||
let langs = root.join("langs");
|
||||
println!("cargo:rerun-if-changed={}", langs.display());
|
||||
let mut paths: Vec<_> = fs::read_dir(&langs)
|
||||
.expect("langs directory is required")
|
||||
.map(|entry| entry.expect("read language entry").path())
|
||||
.filter(|path| path.extension().is_some_and(|ext| ext == "json"))
|
||||
.collect();
|
||||
paths.sort();
|
||||
assert!(
|
||||
!paths.is_empty() && paths.len() <= 128,
|
||||
"expected 1..128 language packs"
|
||||
);
|
||||
let mut languages = Vec::new();
|
||||
let mut locales = HashSet::new();
|
||||
for path in paths {
|
||||
println!("cargo:rerun-if-changed={}", path.display());
|
||||
let id = path
|
||||
.file_stem()
|
||||
.unwrap()
|
||||
.to_str()
|
||||
.expect("language filename must be UTF-8");
|
||||
assert!(
|
||||
id.len() <= 64
|
||||
&& id
|
||||
.bytes()
|
||||
.all(|b| b.is_ascii_alphanumeric() || b == b'_' || b == b'-'),
|
||||
"invalid language filename: {}",
|
||||
path.display()
|
||||
);
|
||||
let content = fs::read(&path).expect("read language JSON");
|
||||
assert!(content.len() <= 262_144, "language pack exceeds 256 KiB");
|
||||
let pack: Value =
|
||||
serde_json::from_slice(&content).unwrap_or_else(|e| panic!("{}: {e}", path.display()));
|
||||
let meta = pack["meta"]
|
||||
.as_object()
|
||||
.expect("language meta object is required");
|
||||
for key in ["locale", "name", "native_name", "direction"] {
|
||||
assert!(
|
||||
meta.get(key)
|
||||
.and_then(Value::as_str)
|
||||
.is_some_and(|v| !v.is_empty() && v.len() <= 128),
|
||||
"{id}: missing/invalid {key}"
|
||||
);
|
||||
}
|
||||
assert!(
|
||||
matches!(meta["direction"].as_str(), Some("ltr" | "rtl")),
|
||||
"{id}: invalid direction"
|
||||
);
|
||||
let locale = meta["locale"].as_str().unwrap();
|
||||
assert!(
|
||||
locale.split('-').all(|part| !part.is_empty()
|
||||
&& part.len() <= 8
|
||||
&& part.bytes().all(|b| b.is_ascii_alphanumeric())),
|
||||
"{id}: use a BCP-47 style locale"
|
||||
);
|
||||
assert!(
|
||||
locales.insert(locale.to_ascii_lowercase()),
|
||||
"duplicate locale {locale}"
|
||||
);
|
||||
let translations = pack["translations"]
|
||||
.as_object()
|
||||
.expect("translations object is required");
|
||||
assert!(
|
||||
translations.len() <= 4096
|
||||
&& translations.iter().all(|(k, v)| !k.is_empty()
|
||||
&& k.len() <= 128
|
||||
&& v.as_str().is_some_and(|s| s.len() <= 16_384)),
|
||||
"{id}: translations must be bounded strings"
|
||||
);
|
||||
languages.push(json!({"id":id,"meta":meta,"translations":translations}));
|
||||
}
|
||||
assert!(
|
||||
languages.iter().any(|p| p["id"] == "en_US"),
|
||||
"en_US.json is the required fallback pack"
|
||||
);
|
||||
let login_path = root.join("src/admin/web/login-keys.json");
|
||||
println!("cargo:rerun-if-changed={}", login_path.display());
|
||||
let login_keys: HashSet<String> =
|
||||
serde_json::from_slice(&fs::read(login_path).expect("read public translation keys"))
|
||||
.expect("parse public translation keys");
|
||||
let mut login_languages = languages.clone();
|
||||
for language in &mut login_languages {
|
||||
language["translations"]
|
||||
.as_object_mut()
|
||||
.expect("translations object")
|
||||
.retain(|key, _| login_keys.contains(key));
|
||||
}
|
||||
let login_bundle =
|
||||
serde_json::to_vec(&json!({"default":"en_US","languages":login_languages})).unwrap();
|
||||
fs::write(out.join("login-langs.json"), login_bundle).expect("write login translations");
|
||||
let bundle = serde_json::to_vec(&json!({"default":"en_US","languages":languages})).unwrap();
|
||||
assert!(bundle.len() <= 8_388_608, "language bundle exceeds 8 MiB");
|
||||
fs::write(out.join("langs.json"), bundle).expect("write language bundle");
|
||||
Ok(())
|
||||
}
|
||||
Reference in new issue
Block a user