This commit is contained in:
Mateusz Gruszczyński
2026-09-01 15:38:46 +02:00
parent 1392114c1e
commit 3000dbaf01
26 changed files with 7520 additions and 2299 deletions
+70 -19
View File
@@ -1,5 +1,8 @@
use serde_json::{json, Value};
use std::{env, fs, path::{Path, PathBuf}};
use std::{
env, fs,
path::{Path, PathBuf},
};
fn required_string<'a>(meta: &'a Value, key: &str, file: &str) -> &'a str {
meta.get(key)
@@ -8,7 +11,6 @@ fn required_string<'a>(meta: &'a Value, key: &str, file: &str) -> &'a str {
.unwrap_or_else(|| panic!("{file}: meta.{key} must be a non-empty string"))
}
fn fnv1a_update(mut hash: u64, bytes: &[u8]) -> u64 {
for byte in bytes {
hash ^= u64::from(*byte);
@@ -69,14 +71,24 @@ fn main() {
let sw_path = web_dir.join("sw.js");
let manifest_path = web_dir.join("manifest.webmanifest");
let favicon_path = web_dir.join("favicon.svg");
for path in [&theme_init_path, &styles_path, &index_path, &not_found_path, &sw_path, &manifest_path, &favicon_path] {
for path in [
&theme_init_path,
&styles_path,
&index_path,
&not_found_path,
&sw_path,
&manifest_path,
&favicon_path,
] {
println!("cargo:rerun-if-changed={}", path.display());
}
let app_js = bundle_app_js(&web_dir);
let app_js_hash = content_hash(app_js.as_bytes());
let theme_init_bytes = fs::read(&theme_init_path).unwrap_or_else(|error| panic!("cannot read {}: {error}", theme_init_path.display()));
let styles_bytes = fs::read(&styles_path).unwrap_or_else(|error| panic!("cannot read {}: {error}", styles_path.display()));
let theme_init_bytes = fs::read(&theme_init_path)
.unwrap_or_else(|error| panic!("cannot read {}: {error}", theme_init_path.display()));
let styles_bytes = fs::read(&styles_path)
.unwrap_or_else(|error| panic!("cannot read {}: {error}", styles_path.display()));
let theme_init_hash = content_hash(&theme_init_bytes);
let styles_hash = content_hash(&styles_bytes);
let app_js_asset_path = format!("/app-{}.js", &app_js_hash[..12]);
@@ -84,8 +96,17 @@ fn main() {
let styles_asset_path = format!("/styles-{}.css", &styles_hash[..12]);
let mut build_hash = fnv1a_update(0xcbf29ce484222325u64, app_js.as_bytes());
for path in [&theme_init_path, &styles_path, &index_path, &not_found_path, &sw_path, &manifest_path, &favicon_path] {
let bytes = fs::read(path).unwrap_or_else(|error| panic!("cannot read {}: {error}", path.display()));
for path in [
&theme_init_path,
&styles_path,
&index_path,
&not_found_path,
&sw_path,
&manifest_path,
&favicon_path,
] {
let bytes = fs::read(path)
.unwrap_or_else(|error| panic!("cannot read {}: {error}", path.display()));
build_hash = fnv1a_update(build_hash, &bytes);
}
@@ -107,9 +128,18 @@ fn main() {
for path in files {
println!("cargo:rerun-if-changed={}", path.display());
let filename = path.file_name().and_then(|v| v.to_str()).expect("UTF-8 language filename");
let stem = path.file_stem().and_then(|v| v.to_str()).expect("UTF-8 language code");
if !stem.chars().all(|ch| ch.is_ascii_alphanumeric() || ch == '-' || ch == '_') {
let filename = path
.file_name()
.and_then(|v| v.to_str())
.expect("UTF-8 language filename");
let stem = path
.file_stem()
.and_then(|v| v.to_str())
.expect("UTF-8 language code");
if !stem
.chars()
.all(|ch| ch.is_ascii_alphanumeric() || ch == '-' || ch == '_')
{
panic!("{filename}: filename may only contain ASCII letters, digits, '-' and '_'");
}
@@ -118,15 +148,20 @@ fn main() {
build_hash = fnv1a_update(build_hash, source.as_bytes());
let document: Value = serde_json::from_str(&source)
.unwrap_or_else(|error| panic!("{filename}: invalid JSON: {error}"));
let root_object = document.as_object().unwrap_or_else(|| panic!("{filename}: language pack root must be an object"));
let unexpected: Vec<&str> = root_object.keys()
let root_object = document
.as_object()
.unwrap_or_else(|| panic!("{filename}: language pack root must be an object"));
let unexpected: Vec<&str> = root_object
.keys()
.filter(|key| key.as_str() != "meta" && key.as_str() != "translations")
.map(|key| key.as_str())
.collect();
if !unexpected.is_empty() {
panic!("{filename}: unexpected top-level keys {:?}; translation keys must be inside 'translations'", unexpected);
}
let meta = document.get("meta").unwrap_or_else(|| panic!("{filename}: missing meta object"));
let meta = document
.get("meta")
.unwrap_or_else(|| panic!("{filename}: missing meta object"));
let code = required_string(meta, "code", filename);
let name = required_string(meta, "name", filename);
let native_name = required_string(meta, "native_name", filename);
@@ -158,15 +193,31 @@ fn main() {
let manifest_json = serde_json::to_string(&json!({
"default": "en",
"languages": manifest_languages
})).expect("serialize language manifest");
}))
.expect("serialize language manifest");
let mut generated = String::new();
generated.push_str("// @generated by build.rs - do not edit.\n");
generated.push_str(&format!("pub const APP_JS_ASSET_PATH: &str = {:?};\n", app_js_asset_path));
generated.push_str(&format!("pub const THEME_INIT_ASSET_PATH: &str = {:?};\n", theme_init_asset_path));
generated.push_str(&format!("pub const STYLES_CSS_ASSET_PATH: &str = {:?};\n", styles_asset_path));
generated.push_str(&format!("pub const ASSET_BUILD_ID: &str = \"{:016x}\";\n", build_hash));
generated.push_str(&format!("pub const LANGUAGE_MANIFEST_JSON: &str = {:?};\n", manifest_json));
generated.push_str(&format!(
"pub const APP_JS_ASSET_PATH: &str = {:?};\n",
app_js_asset_path
));
generated.push_str(&format!(
"pub const THEME_INIT_ASSET_PATH: &str = {:?};\n",
theme_init_asset_path
));
generated.push_str(&format!(
"pub const STYLES_CSS_ASSET_PATH: &str = {:?};\n",
styles_asset_path
));
generated.push_str(&format!(
"pub const ASSET_BUILD_ID: &str = \"{:016x}\";\n",
build_hash
));
generated.push_str(&format!(
"pub const LANGUAGE_MANIFEST_JSON: &str = {:?};\n",
manifest_json
));
generated.push_str("pub const LANGUAGE_ASSETS: &[(&str, &str)] = &[\n");
for (code, path) in assets {
generated.push_str(&format!(