diff --git a/Cargo.lock b/Cargo.lock index 7f29ed1..3176133 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -248,7 +248,7 @@ dependencies = [ "ahash 0.8.12", "chrono", "either", - "indexmap 2.11.0", + "indexmap 2.11.1", "itertools 0.13.0", "nom", "serde", @@ -258,7 +258,7 @@ dependencies = [ [[package]] name = "builder" -version = "0.1.26" +version = "0.1.27" dependencies = [ "builder-assemble", "builder-command", @@ -277,7 +277,7 @@ dependencies = [ [[package]] name = "builder-assemble" -version = "0.1.26" +version = "0.1.27" dependencies = [ "base64", "builder-command", @@ -289,15 +289,16 @@ dependencies = [ [[package]] name = "builder-command" -version = "0.1.26" +version = "0.1.27" dependencies = [ "camino-fs", + "fs-err 3.1.1", "log", ] [[package]] name = "builder-copy" -version = "0.1.26" +version = "0.1.27" dependencies = [ "builder-command", "common", @@ -306,7 +307,7 @@ dependencies = [ [[package]] name = "builder-fontforge" -version = "0.1.26" +version = "0.1.27" dependencies = [ "builder-command", "camino-fs", @@ -317,7 +318,7 @@ dependencies = [ [[package]] name = "builder-localized" -version = "0.1.26" +version = "0.1.27" dependencies = [ "builder-command", "camino-fs", @@ -328,7 +329,7 @@ dependencies = [ [[package]] name = "builder-sass" -version = "0.1.26" +version = "0.1.27" dependencies = [ "builder-command", "common", @@ -340,7 +341,7 @@ dependencies = [ [[package]] name = "builder-swift-package" -version = "0.1.26" +version = "0.1.27" dependencies = [ "builder-command", "common", @@ -350,7 +351,7 @@ dependencies = [ [[package]] name = "builder-uniffi" -version = "0.1.26" +version = "0.1.27" dependencies = [ "builder-command", "camino-fs", @@ -361,13 +362,14 @@ dependencies = [ [[package]] name = "builder-wasm" -version = "0.1.26" +version = "0.1.27" dependencies = [ "anyhow", "base64", "builder-command", "camino-fs", "common", + "fs-err 3.1.1", "log", "seahash", "tempfile", @@ -496,9 +498,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.36" +version = "1.2.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5252b3d2648e5eedbc1a6f501e3c795e07025c1e93bbf8bbdd6eef7f447a6d54" +checksum = "65193589c6404eb80b450d618eaf9a2cafaaafd57ecce47370519ef674a7bd44" dependencies = [ "find-msvc-tools", "jobserver", @@ -575,7 +577,7 @@ checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" [[package]] name = "common" -version = "0.1.26" +version = "0.1.27" dependencies = [ "anyhow", "base64", @@ -583,6 +585,7 @@ dependencies = [ "builder-command", "camino-fs", "flate2", + "fs-err 3.1.1", "icu_locid", "log", "seahash", @@ -727,11 +730,12 @@ dependencies = [ [[package]] name = "cxx" -version = "1.0.179" +version = "1.0.184" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85b04ade63e106c145cdcd3482932299c2dcd36b15f1d5c596c06edf365966fe" +checksum = "be4a0beb369d20d0de6aa7084ee523e4c9a31d7d8c61ba357b119bb574d7f368" dependencies = [ "cc", + "cxx-build", "cxxbridge-cmd", "cxxbridge-flags", "cxxbridge-macro", @@ -741,13 +745,13 @@ dependencies = [ [[package]] name = "cxx-build" -version = "1.0.179" +version = "1.0.184" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba16a03d510b0e52be1952831ae1656e3ac9f6ac0c293a7b261670a0c7ff3b54" +checksum = "27d955b93e56a8e45cbc34df0ae920d8b5ad01541a4571222c78527c00e1a40a" dependencies = [ "cc", "codespan-reporting", - "indexmap 2.11.0", + "indexmap 2.11.1", "proc-macro2", "quote", "scratch", @@ -756,13 +760,13 @@ dependencies = [ [[package]] name = "cxxbridge-cmd" -version = "1.0.179" +version = "1.0.184" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c3aefd0d58a2df90cd67fe2eccdf7fb6e6905e9886ae5feb2258ce0dec23063" +checksum = "052f6c468d9dabdc2b8b228bcb2d7843b2bea0f3fb9c4e2c6ba5852574ec0150" dependencies = [ "clap", "codespan-reporting", - "indexmap 2.11.0", + "indexmap 2.11.1", "proc-macro2", "quote", "syn 2.0.106", @@ -770,17 +774,17 @@ dependencies = [ [[package]] name = "cxxbridge-flags" -version = "1.0.179" +version = "1.0.184" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b735d976ca632f9e94b9de9b0ab8283de65e2641e7ae9d5abc3e6b467ea6673" +checksum = "0fd145fa180986cb8002c63217d03b2c782fdcd5fa323adcd1f62d2d6ece6144" [[package]] name = "cxxbridge-macro" -version = "1.0.179" +version = "1.0.184" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42fffb686dc0b7bab364ee41fcf5a04c295c7e7a5f07cd38155c1c3bda437666" +checksum = "02ac4a3bc4484a2daa0a8421c9588bd26522be9682a2fe02c7087bc4e8bc3c60" dependencies = [ - "indexmap 2.11.0", + "indexmap 2.11.1", "proc-macro2", "quote", "rustversion", @@ -963,12 +967,12 @@ dependencies = [ [[package]] name = "errno" -version = "0.3.13" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.0", ] [[package]] @@ -1072,7 +1076,7 @@ dependencies = [ "cfg-if", "libc", "r-efi", - "wasi 0.14.4+wasi-0.2.4", + "wasi 0.14.5+wasi-0.2.4", ] [[package]] @@ -1121,7 +1125,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d9e3df7f0222ce5184154973d247c591d9aadc28ce7a73c6cd31100c9facff6" dependencies = [ "codemap", - "indexmap 2.11.0", + "indexmap 2.11.1", "lasso", "once_cell", "phf", @@ -1168,9 +1172,9 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "iana-time-zone" -version = "0.1.63" +version = "0.1.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" +checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -1336,9 +1340,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.11.0" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2481980430f9f78649238835720ddccc57e52df14ffce1c6f37391d61b563e9" +checksum = "206a8042aec68fa4a62e8d3f7aa4ceb508177d9324faf261e1959e495b7a1921" dependencies = [ "equivalent", "hashbrown 0.15.5", @@ -1448,7 +1452,7 @@ dependencies = [ "dashmap", "data-encoding", "getrandom 0.2.16", - "indexmap 2.11.0", + "indexmap 2.11.1", "itertools 0.10.5", "lazy_static", "lightningcss-derive", @@ -1484,9 +1488,9 @@ dependencies = [ [[package]] name = "linux-raw-sys" -version = "0.9.4" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" [[package]] name = "litemap" @@ -1914,15 +1918,15 @@ checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" [[package]] name = "rustix" -version = "1.0.8" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8" +checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" dependencies = [ "bitflags", "errno", "libc", "linux-raw-sys", - "windows-sys 0.60.2", + "windows-sys 0.61.0", ] [[package]] @@ -2200,15 +2204,15 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" [[package]] name = "tempfile" -version = "3.21.0" +version = "3.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15b61f8f20e3a6f7e0649d825294eaf317edce30f82cf6026e7e4cb9222a7d1e" +checksum = "84fa4d11fadde498443cca10fd3ac23c951f0dc59e080e9f4b93d4df4e4eea53" dependencies = [ "fastrand", "getrandom 0.3.3", "once_cell", "rustix", - "windows-sys 0.60.2", + "windows-sys 0.61.0", ] [[package]] @@ -2371,7 +2375,7 @@ version = "0.22.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ - "indexmap 2.11.0", + "indexmap 2.11.1", "serde", "serde_spanned", "toml_datetime", @@ -2393,9 +2397,9 @@ checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" [[package]] name = "unicode-ident" -version = "1.0.18" +version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" [[package]] name = "unicode-segmentation" @@ -2429,7 +2433,7 @@ dependencies = [ "glob", "goblin", "heck 0.5.0", - "indexmap 2.11.0", + "indexmap 2.11.1", "once_cell", "serde", "tempfile", @@ -2448,7 +2452,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04f4f224becf14885c10e6e400b95cc4d1985738140cb194ccc2044563f8a56b" dependencies = [ "anyhow", - "indexmap 2.11.0", + "indexmap 2.11.1", "proc-macro2", "quote", "syn 2.0.106", @@ -2474,7 +2478,7 @@ checksum = "4b147e133ad7824e32426b90bc41fda584363563f2ba747f590eca1fd6fd14e6" dependencies = [ "anyhow", "heck 0.5.0", - "indexmap 2.11.0", + "indexmap 2.11.1", "tempfile", "uniffi_internal_macros", ] @@ -2575,9 +2579,18 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasi" -version = "0.14.4+wasi-0.2.4" +version = "0.14.5+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4494f6290a82f5fe584817a676a34b9d6763e8d9d18204009fb31dceca98fd4" +dependencies = [ + "wasip2", +] + +[[package]] +name = "wasip2" +version = "1.0.0+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88a5f4a424faf49c3c2c344f166f0662341d470ea185e939657aaff130f0ec4a" +checksum = "03fa2761397e5bd52002cd7e73110c71af2109aca4e521a9f40473fe685b0a24" dependencies = [ "wit-bindgen", ] @@ -2743,7 +2756,7 @@ dependencies = [ "ahash 0.8.12", "bitflags", "hashbrown 0.14.5", - "indexmap 2.11.0", + "indexmap 2.11.1", "semver", "serde", ] @@ -2779,13 +2792,13 @@ dependencies = [ [[package]] name = "windows-core" -version = "0.61.2" +version = "0.62.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +checksum = "57fe7168f7de578d2d8a05b07fd61870d2e73b4020e9f49aa00da8471723497c" dependencies = [ "windows-implement", "windows-interface", - "windows-link 0.1.3", + "windows-link 0.2.0", "windows-result", "windows-strings", ] @@ -2826,20 +2839,20 @@ checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65" [[package]] name = "windows-result" -version = "0.3.4" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +checksum = "7084dcc306f89883455a206237404d3eaf961e5bd7e0f312f7c91f57eb44167f" dependencies = [ - "windows-link 0.1.3", + "windows-link 0.2.0", ] [[package]] name = "windows-strings" -version = "0.4.2" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +checksum = "7218c655a553b0bed4426cf54b20d7ba363ef543b52d515b3e48d7fd55318dda" dependencies = [ - "windows-link 0.1.3", + "windows-link 0.2.0", ] [[package]] @@ -3201,7 +3214,7 @@ checksum = "12598812502ed0105f607f941c386f43d441e00148fce9dec3ca5ffb0bde9308" dependencies = [ "arbitrary", "crc32fast", - "indexmap 2.11.0", + "indexmap 2.11.1", "memchr", ] diff --git a/Cargo.toml b/Cargo.toml index ba7195b..2e12195 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,7 +5,7 @@ resolver = "2" members = ["crates/*"] [workspace.package] -version = "0.1.26" +version = "0.1.27" repository = "https://github.com/human-solutions/builder" license = "MIT" edition = "2024" @@ -41,6 +41,7 @@ brotli = "8.0" camino-fs = "0.1" cargo_metadata = "0.22" flate2 = "1.1" +fs-err = "3.1" grass = "0.13" icu_locid = "1.5" lightningcss = { version = "1.0.0-alpha.67", features = ["browserslist"] } diff --git a/crates/builder/src/main.rs b/crates/builder/src/main.rs index 9bf384d..737989a 100644 --- a/crates/builder/src/main.rs +++ b/crates/builder/src/main.rs @@ -3,6 +3,7 @@ use std::env; use builder_command::{BuilderCmd, Cmd}; use camino_fs::*; +use common::site_fs; use common::{LOG_LEVEL, RELEASE, setup_logging}; fn main() { @@ -56,4 +57,9 @@ pub fn run(builder: BuilderCmd) { Cmd::SwiftPackage(cmd) => builder_swift_package::run(cmd), } } + + // Finalize hash output files after all commands have completed + if let Err(e) = site_fs::finalize_hash_outputs() { + eprintln!("Failed to write hash output files: {}", e); + } } diff --git a/crates/command/Cargo.toml b/crates/command/Cargo.toml index feac514..28c0d2a 100644 --- a/crates/command/Cargo.toml +++ b/crates/command/Cargo.toml @@ -10,4 +10,5 @@ version.workspace = true [dependencies] camino-fs.workspace = true +fs-err.workspace = true log.workspace = true diff --git a/crates/command/src/lib.rs b/crates/command/src/lib.rs index 4ffa3e0..caf0229 100644 --- a/crates/command/src/lib.rs +++ b/crates/command/src/lib.rs @@ -14,11 +14,11 @@ pub use assemble::AssembleCmd; use camino_fs::Utf8PathBuf; pub use copy::CopyCmd; pub use fontforge::FontForgeCmd; +use fs_err as fs; pub use localized::LocalizedCmd; use log::LevelFilter; pub use out::{Encoding, Output}; pub use sass::SassCmd; -use std::fs; pub use swift_package::SwiftPackageCmd; pub use uniffi::UniffiCmd; pub use wasm::{DebugSymbolsMode, Profile, WasmProcessingCmd}; @@ -32,10 +32,10 @@ pub enum LogLevel { #[derive(Debug, Clone, PartialEq, Eq)] pub enum LogDestination { - Cargo, // via cargo::warning - File(Utf8PathBuf), // given a path - Terminal, // standard output - TerminalPlain, // standard output, designed for when run in a Command that adds it's own prefixes to the logs + Cargo, // via cargo::warning + File(Utf8PathBuf), // given a path + Terminal, // standard output + TerminalPlain, // standard output, designed for when run in a Command that adds it's own prefixes to the logs } impl LogLevel { @@ -196,7 +196,7 @@ impl Display for BuilderCmd { LogLevel::Trace => "trace", }; writeln!(f, "log_level={}", log_level_str)?; - + let log_destination_str = match &self.log_destination { LogDestination::Cargo => "cargo".to_string(), LogDestination::File(path) => format!("file:{}", path), @@ -204,7 +204,7 @@ impl Display for BuilderCmd { LogDestination::TerminalPlain => "terminal_plain".to_string(), }; writeln!(f, "log_destination={}", log_destination_str)?; - + writeln!(f, "release={}", self.release)?; writeln!(f, "builder_toml={}", self.builder_toml)?; for cmd in &self.cmds { @@ -328,7 +328,9 @@ fn roundtrip() { .add_copy(CopyCmd::default()) .add_swift_package(SwiftPackageCmd::default()) .log_level(LogLevel::Verbose) - .log_destination(LogDestination::File(camino_fs::Utf8PathBuf::from("/tmp/builder.log"))) + .log_destination(LogDestination::File(camino_fs::Utf8PathBuf::from( + "/tmp/builder.log", + ))) .release(true) .builder_toml("builder.toml"); diff --git a/crates/command/src/out.rs b/crates/command/src/out.rs index 8287e84..c93a8f7 100644 --- a/crates/command/src/out.rs +++ b/crates/command/src/out.rs @@ -67,6 +67,9 @@ pub struct Output { all_encodings: bool, pub checksum: bool, + + /// Optional path to write file hashes as a Rust file + pub hash_output_path: Option, } impl Output { @@ -79,6 +82,7 @@ impl Output { uncompressed: false, all_encodings: false, checksum: false, + hash_output_path: None, } } @@ -91,6 +95,7 @@ impl Output { uncompressed: true, all_encodings: true, checksum: true, + hash_output_path: None, } } @@ -103,6 +108,7 @@ impl Output { uncompressed: true, all_encodings: true, checksum: false, + hash_output_path: None, } } @@ -111,6 +117,11 @@ impl Output { self } + pub fn hash_output_path>(mut self, path: P) -> Self { + self.hash_output_path = Some(path.into()); + self + } + pub fn uncompressed(&self) -> bool { // if none are set, then default to uncompressed let default_uncompressed = !self.uncompressed && !self.brotli && !self.gzip; @@ -150,7 +161,10 @@ impl Display for Output { write!(f, "gzip={}\t", self.gzip)?; write!(f, "uncompressed={}\t", self.uncompressed)?; write!(f, "all_encodings={}\t", self.all_encodings)?; - write!(f, "checksum={}", self.checksum)?; + write!(f, "checksum={}\t", self.checksum)?; + if let Some(hash_output_path) = &self.hash_output_path { + write!(f, "hash_output_path={}\t", hash_output_path)?; + } Ok(()) } } @@ -160,7 +174,7 @@ impl FromStr for Output { fn from_str(s: &str) -> Result { let mut cmd = Output::default(); - for item in s.split('\t') { + for item in s.split('\t').filter(|s| !s.is_empty()) { let (key, value) = item.split_once('=').unwrap(); match key { @@ -171,6 +185,7 @@ impl FromStr for Output { "uncompressed" => cmd.uncompressed = value.parse().unwrap(), "all_encodings" => cmd.all_encodings = value.parse().unwrap(), "checksum" => cmd.checksum = value.parse().unwrap(), + "hash_output_path" => cmd.hash_output_path = Some(value.into()), _ => panic!("unknown key: {}", key), } } diff --git a/crates/common/Cargo.toml b/crates/common/Cargo.toml index d596c70..8093ab8 100644 --- a/crates/common/Cargo.toml +++ b/crates/common/Cargo.toml @@ -12,11 +12,12 @@ builder-command = { path = "../command" } anyhow.workspace = true base64.workspace = true +brotli.workspace = true camino-fs.workspace = true +flate2.workspace = true +fs-err.workspace = true icu_locid.workspace = true log.workspace = true -brotli.workspace = true -flate2.workspace = true seahash.workspace = true simplelog.workspace = true time.workspace = true diff --git a/crates/common/src/hash_output.rs b/crates/common/src/hash_output.rs new file mode 100644 index 0000000..7c728b3 --- /dev/null +++ b/crates/common/src/hash_output.rs @@ -0,0 +1,197 @@ +use anyhow::Result; +use camino_fs::{Utf8Path, Utf8PathExt}; +use std::collections::BTreeMap; + +/// Converts a file path to a valid Rust constant name +/// Removes hash part, converts to uppercase, and replaces invalid characters with underscores +fn file_path_to_const_name(file_path: &str) -> String { + let mut const_name = String::new(); + + // Split the path and remove any hash parts (parts ending with '=') + for part in file_path.split('/') { + if !const_name.is_empty() { + const_name.push('_'); + } + + // Split by '.' to separate name, potential hash, and extension + let parts: Vec<&str> = part.split('.').collect(); + + if parts.len() >= 2 { + // Add the name part + const_name.push_str(&sanitize_for_const(parts[0])); + + // Find the extension (last part that's not a hash) + let mut ext_parts = Vec::new(); + for p in parts.iter().skip(1) { + if !p.ends_with('=') { + ext_parts.push(*p); + } + } + + // Add extension parts + for ext in ext_parts { + const_name.push('_'); + const_name.push_str(&sanitize_for_const(ext)); + } + } else { + const_name.push_str(&sanitize_for_const(part)); + } + } + + const_name.to_uppercase() +} + +/// Sanitizes a string part for use in a Rust constant name +fn sanitize_for_const(s: &str) -> String { + s.chars() + .map(|c| if c.is_ascii_alphanumeric() { c } else { '_' }) + .collect() +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct HashEntry { + pub file_path: String, + pub hash: String, +} + +impl HashEntry { + pub fn new, H: Into>(file_path: P, hash: H) -> Self { + Self { + file_path: file_path.into(), + hash: hash.into(), + } + } +} + +/// Collects hash entries and writes them to a Rust file +pub struct HashCollector { + entries: BTreeMap, +} + +impl HashCollector { + pub fn new() -> Self { + Self { + entries: BTreeMap::new(), + } + } + + pub fn add_entry, H: Into>(&mut self, file_path: P, hash: H) { + self.entries.insert(file_path.into(), hash.into()); + } + + pub fn write_to_rust_file(&self, output_path: &Utf8Path) -> Result<()> { + let rust_content = self.generate_rust_code(); + + // Ensure parent directory exists + if let Some(parent) = output_path.parent() { + parent.mkdirs()?; + } + + output_path.write(&rust_content)?; + Ok(()) + } + + fn generate_rust_code(&self) -> String { + let mut content = String::new(); + + content.push_str("// This file is auto-generated by the builder tool.\n"); + content.push_str("// Do not edit manually - it will be overwritten.\n\n"); + + if self.entries.is_empty() { + content.push_str("// No files with hashes were generated.\n"); + } else { + for (file_path, hash) in &self.entries { + let const_name = file_path_to_const_name(file_path); + content.push_str(&format!("pub const {}: &str = \"{}\";\n", const_name, hash)); + } + } + + content + } +} + +impl Default for HashCollector { + fn default() -> Self { + Self::new() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_file_path_to_const_name() { + // Basic file names + assert_eq!(file_path_to_const_name("style.css"), "STYLE_CSS"); + assert_eq!(file_path_to_const_name("script.js"), "SCRIPT_JS"); + + // Files with paths + assert_eq!( + file_path_to_const_name("assets/style.css"), + "ASSETS_STYLE_CSS" + ); + assert_eq!( + file_path_to_const_name("js/modules/app.js"), + "JS_MODULES_APP_JS" + ); + + // Files with hashes (should be removed) + assert_eq!(file_path_to_const_name("style.abc123=.css"), "STYLE_CSS"); + assert_eq!( + file_path_to_const_name("assets/script.def456=.js"), + "ASSETS_SCRIPT_JS" + ); + + // Special characters (should be sanitized) + assert_eq!(file_path_to_const_name("my-file.css"), "MY_FILE_CSS"); + assert_eq!(file_path_to_const_name("file@2x.png"), "FILE_2X_PNG"); + } + + #[test] + fn test_sanitize_for_const() { + assert_eq!(sanitize_for_const("hello"), "hello"); + assert_eq!(sanitize_for_const("hello-world"), "hello_world"); + assert_eq!(sanitize_for_const("file@2x"), "file_2x"); + assert_eq!(sanitize_for_const("123abc"), "123abc"); + } + + #[test] + fn test_hash_collector_empty() { + let collector = HashCollector::new(); + let rust_code = collector.generate_rust_code(); + assert!(rust_code.contains("// No files with hashes were generated.")); + } + + #[test] + fn test_hash_collector_with_entries() { + let mut collector = HashCollector::new(); + collector.add_entry("style.css", "abc123"); + collector.add_entry("script.js", "def456"); + + let rust_code = collector.generate_rust_code(); + assert!(rust_code.contains("pub const STYLE_CSS: &str = \"abc123\";")); + assert!(rust_code.contains("pub const SCRIPT_JS: &str = \"def456\";")); + assert!(!rust_code.contains("FILE_HASHES")); // Should not contain the old array format + } + + #[test] + fn test_hash_collector_with_complex_paths() { + let mut collector = HashCollector::new(); + collector.add_entry("assets/styles/main.css", "hash1"); + collector.add_entry("js/app.min.js", "hash2"); + collector.add_entry("images/logo@2x.png", "hash3"); + + let rust_code = collector.generate_rust_code(); + assert!(rust_code.contains("pub const ASSETS_STYLES_MAIN_CSS: &str = \"hash1\";")); + assert!(rust_code.contains("pub const JS_APP_MIN_JS: &str = \"hash2\";")); + assert!(rust_code.contains("pub const IMAGES_LOGO_2X_PNG: &str = \"hash3\";")); + } + + #[test] + fn test_hash_entry_creation() { + let entry = HashEntry::new("test.txt", "hash123"); + assert_eq!(entry.file_path, "test.txt"); + assert_eq!(entry.hash, "hash123"); + } +} diff --git a/crates/common/src/hash_output_integration_test.rs b/crates/common/src/hash_output_integration_test.rs new file mode 100644 index 0000000..de1e47d --- /dev/null +++ b/crates/common/src/hash_output_integration_test.rs @@ -0,0 +1,76 @@ +#[cfg(test)] +mod tests { + use super::super::{ + hash_output::HashCollector, + site_fs::{SiteFile, finalize_hash_outputs, write_file_to_site}, + }; + use builder_command::Output; + use camino_fs::{Utf8PathBuf, Utf8PathExt}; + + #[test] + fn test_hash_output_integration() { + // Create a temporary directory for the test + let temp_dir = Utf8PathBuf::from("target/test_hash_integration"); + let hash_file = Utf8PathBuf::from("target/test_hashes.rs"); + + // Clean up any existing files + if temp_dir.exists() { + temp_dir.rm().unwrap(); + } + if hash_file.exists() { + hash_file.rm().unwrap(); + } + + temp_dir.mkdirs().unwrap(); + + // Create output configuration with checksum and hash output enabled + let output = Output::new_compress_and_sum(&temp_dir).hash_output_path(&hash_file); + let output_config = [output]; + + // Write test files + let css_content = b"body { color: blue; }"; + let css_file = SiteFile::new("style", "css"); + write_file_to_site(&css_file, css_content, &output_config); + + let js_content = b"console.log('Hello, world!');"; + let js_file = SiteFile::new("script", "js"); + write_file_to_site(&js_file, js_content, &output_config); + + // Finalize hash outputs + finalize_hash_outputs().unwrap(); + + // Verify the hash file was created and contains expected content + assert!(hash_file.exists(), "Hash output file should be created"); + + let hash_file_content = hash_file.read_string().unwrap(); + assert!(hash_file_content.contains("pub const SCRIPT_JS: &str =")); + assert!(hash_file_content.contains("pub const STYLE_CSS: &str =")); + assert!(!hash_file_content.contains("FILE_HASHES")); // Should not contain the old format + + // Clean up + temp_dir.rm().unwrap(); + hash_file.rm().unwrap(); + } + + #[test] + fn test_hash_collector_direct() { + let mut collector = HashCollector::new(); + collector.add_entry("test.css", "abc123"); + collector.add_entry("test.js", "def456"); + + let temp_file = Utf8PathBuf::from("target/test_direct_hash.rs"); + if temp_file.exists() { + temp_file.rm().unwrap(); + } + + collector.write_to_rust_file(&temp_file).unwrap(); + + assert!(temp_file.exists()); + let content = temp_file.read_string().unwrap(); + assert!(content.contains("pub const TEST_CSS: &str = \"abc123\";")); + assert!(content.contains("pub const TEST_JS: &str = \"def456\";")); + + // Clean up + temp_file.rm().unwrap(); + } +} diff --git a/crates/common/src/lib.rs b/crates/common/src/lib.rs index 53c30f0..0ade83a 100644 --- a/crates/common/src/lib.rs +++ b/crates/common/src/lib.rs @@ -1,14 +1,16 @@ mod envargs; mod ext; +pub mod hash_output; +mod hash_output_integration_test; pub mod out; pub mod site_fs; use builder_command::{LogDestination, LogLevel}; +use fs_err::OpenOptions; use log::{Log, Metadata, Record}; use simplelog::{ ColorChoice, ConfigBuilder, TermLogger, TerminalMode, WriteLogger, format_description, }; -use std::fs::OpenOptions; use std::sync::OnceLock; use time::OffsetDateTime; @@ -156,8 +158,20 @@ impl Log for CargoLogger { log::Level::Trace => "TRACE", }; + let msg = record.args().to_string(); + + // Filter out excessive wasm_bindgen logs at DEBUG level unless in Trace mode + if record.level() == log::Level::Debug + && !is_trace() + && let Some(module_path) = record.module_path() + && (module_path.starts_with("wasm_bindgen_cli_support") + || module_path.starts_with("walrus")) + { + return; + } + // Route through cargo warning system - println!("cargo:warning=[{}] {}", level_str, record.args()); + println!("cargo:warning=[{}] {}", level_str, msg); } } @@ -171,10 +185,13 @@ pub fn setup_logging(level: LogLevel, destination: LogDestination) { let mut log_config_builder = ConfigBuilder::new(); - // Filter out walrus debug logs when in verbose mode - if matches!(level, LogLevel::Verbose | LogLevel::Trace) { + // Filter out walrus and wasm_bindgen debug logs when in verbose mode (only show in trace) + if matches!(level, LogLevel::Verbose) { log_config_builder.add_filter_ignore_str("walrus"); log_config_builder.add_filter_ignore_str("wasm_bindgen_cli_support"); + log_config_builder.add_filter_ignore_str("wasm_bindgen"); + } else if matches!(level, LogLevel::Trace) { + // Show everything in trace mode, including wasm_bindgen internals } let log_config = log_config_builder diff --git a/crates/common/src/out.rs b/crates/common/src/out.rs index 1797cd6..ecb0375 100644 --- a/crates/common/src/out.rs +++ b/crates/common/src/out.rs @@ -1,6 +1,6 @@ +use fs_err::File; use std::{ collections::HashMap, - fs::File, hash::Hasher, io::{Cursor, Write}, }; diff --git a/crates/common/src/site_fs/mod.rs b/crates/common/src/site_fs/mod.rs index d08a76e..199a6b7 100644 --- a/crates/common/src/site_fs/mod.rs +++ b/crates/common/src/site_fs/mod.rs @@ -4,6 +4,7 @@ mod encoding; #[cfg(test)] mod tests; +use crate::hash_output::HashCollector; use crate::{debug, is_trace, log_trace}; pub use anyhow::Result; pub use asset::Asset; @@ -14,7 +15,25 @@ use camino_fs::*; pub use encoding::AssetEncodings; use icu_locid::LanguageIdentifier; use seahash::SeaHasher; -use std::{collections::BTreeMap, hash::Hasher}; +use std::sync::OnceLock; +use std::{collections::BTreeMap, hash::Hasher, sync::Mutex}; + +// Global hash collector for tracking file hashes across all output operations +static HASH_COLLECTORS: OnceLock>> = OnceLock::new(); + +fn get_hash_collectors() -> &'static Mutex> { + HASH_COLLECTORS.get_or_init(|| Mutex::new(BTreeMap::new())) +} + +/// Finalizes hash collection and writes all accumulated hashes to their respective output files +pub fn finalize_hash_outputs() -> Result<()> { + let collectors = get_hash_collectors().lock().unwrap(); + for (output_path, collector) in collectors.iter() { + collector.write_to_rust_file(output_path)?; + log_trace!("SITE_FS", "Wrote hash file to: {}", output_path); + } + Ok(()) +} pub fn parse_site(root: &Utf8Path) -> Result> { let mut assets: BTreeMap = Default::default(); @@ -109,9 +128,35 @@ pub fn write_file_to_site(site_file: &SiteFile, bytes: &[u8], output: &[Output]) let asset = AssetPath { subdir, name_ext: site_file.clone(), - checksum, + checksum: checksum.clone(), }; + // Collect hash information if hash_output_path is configured + if let Some(hash_output_path) = &out.hash_output_path + && let Some(hash) = &checksum + { + let mut collectors = get_hash_collectors().lock().unwrap(); + let collector = collectors.entry(hash_output_path.clone()).or_default(); + + // Create the file path for the hash entry (relative to site root) + let file_path = if asset.subdir.as_str().is_empty() { + format!("{}.{}", asset.name_ext.name, asset.name_ext.ext) + } else { + format!( + "{}/{}.{}", + asset.subdir, asset.name_ext.name, asset.name_ext.ext + ) + }; + + collector.add_entry(file_path, hash); + log_trace!( + "SITE_FS", + "Added hash entry for: {} -> {}", + asset.name_ext, + hash + ); + } + // remove any files that have the same name and extension out.dir .join(&asset.subdir) @@ -180,6 +225,41 @@ pub fn write_translations>( } else { None }; + + // Collect hash information for translations if hash_output_path is configured + if let Some(hash_output_path) = &out.hash_output_path + && let Some(hash) = &checksum + { + let mut collectors = get_hash_collectors().lock().unwrap(); + let collector = collectors.entry(hash_output_path.clone()).or_default(); + + for (lang, _) in lang_and_bytes { + let file_path = if site_file.site_dir.is_some() { + format!( + "{}/{}.{}/{}.{}", + site_file.site_dir.as_ref().unwrap(), + site_file.name, + site_file.ext, + lang, + site_file.ext + ) + } else { + format!( + "{}.{}/{}.{}", + site_file.name, site_file.ext, lang, site_file.ext + ) + }; + collector.add_entry(file_path, hash); + log_trace!( + "SITE_FS", + "Added translation hash entry for: {} ({}) -> {}", + site_file, + lang, + hash + ); + } + } + let mut asset = TranslatedAssetPath { site_file, checksum, diff --git a/crates/copy/src/lib.rs b/crates/copy/src/lib.rs index 6a121b5..ce45407 100644 --- a/crates/copy/src/lib.rs +++ b/crates/copy/src/lib.rs @@ -1,11 +1,16 @@ use builder_command::CopyCmd; -use common::{Timer, log_command, log_operation}; use common::site_fs::copy_files_to_site; +use common::{Timer, log_command, log_operation}; pub fn run(cmd: &CopyCmd) { let _timer = Timer::new("COPY processing"); log_command!("COPY", "Copying files from: {}", cmd.src_dir); - log_operation!("COPY", "Recursive: {}, Extensions: {:?}", cmd.recursive, cmd.file_extensions); + log_operation!( + "COPY", + "Recursive: {}, Extensions: {:?}", + cmd.recursive, + cmd.file_extensions + ); log_operation!("COPY", "Output destinations: {}", cmd.output.len()); if !cmd.src_dir.exists() { diff --git a/crates/fontforge/src/lib.rs b/crates/fontforge/src/lib.rs index e72e5f1..61377ab 100644 --- a/crates/fontforge/src/lib.rs +++ b/crates/fontforge/src/lib.rs @@ -2,8 +2,8 @@ use std::process::Command; use builder_command::FontForgeCmd; use camino_fs::*; -use common::{Timer, log_command, log_operation, log_trace}; use common::site_fs::{SiteFile, write_file_to_site}; +use common::{Timer, log_command, log_operation, log_trace}; pub fn run(cmd: &FontForgeCmd) { let _timer = Timer::new("FONTFORGE processing"); @@ -17,10 +17,15 @@ pub fn run(cmd: &FontForgeCmd) { if !sfd_file.exists() { panic!("Font file not found: {:?}", sfd_file); } - + let sfd_bytes = sfd_file.read_bytes().unwrap(); let hash = format!("{:x}", seahash::hash(&sfd_bytes)); - log_operation!("FONTFORGE", "Font file hash: {} ({} bytes)", hash, sfd_bytes.len()); + log_operation!( + "FONTFORGE", + "Font file hash: {} ({} bytes)", + hash, + sfd_bytes.len() + ); let sfd_dir = sfd_file.parent().unwrap(); @@ -49,7 +54,10 @@ pub fn run(cmd: &FontForgeCmd) { // copy otf file to font directory (only macos) if cfg!(target_os = "macos") { - log_operation!("FONTFORGE", "Installing font to macOS system (target_os=macos)"); + log_operation!( + "FONTFORGE", + "Installing font to macOS system (target_os=macos)" + ); macos_install_font(&otf_file, name); } log_trace!("FONTFORGE", "Removing temporary OTF file: {}", otf_file); @@ -62,7 +70,12 @@ pub fn run(cmd: &FontForgeCmd) { let woff2_path = sfd_dir.join(&woff2_filename); let bytes = woff2_path.read_bytes().unwrap(); - log_operation!("FONTFORGE", "Writing WOFF2 output: {} ({} bytes)", woff2_filename, bytes.len()); + log_operation!( + "FONTFORGE", + "Writing WOFF2 output: {} ({} bytes)", + woff2_filename, + bytes.len() + ); let site_file = SiteFile::new(name, "woff2"); write_file_to_site(&site_file, &bytes, &cmd.output); } @@ -70,9 +83,15 @@ pub fn run(cmd: &FontForgeCmd) { fn generate_woff2_otf(sfd_dir: &Utf8Path, name: &str) { let ff = format!("Open('{name}.sfd'); Generate('{name}.woff2'); Generate('{name}.otf')"); - log_operation!("FONTFORGE", "Generating {}.woff2 and {}.otf from {}.sfd", name, name, name); + log_operation!( + "FONTFORGE", + "Generating {}.woff2 and {}.otf from {}.sfd", + name, + name, + name + ); log_trace!("FONTFORGE", "FontForge command: {}", ff); - + let cmd = Command::new("fontforge") .args(["-lang=ff", "-c", &ff]) .current_dir(sfd_dir) @@ -82,7 +101,7 @@ fn generate_woff2_otf(sfd_dir: &Utf8Path, name: &str) { if !cmd.success() { panic!("FontForge command failed") } - + log_operation!("FONTFORGE", "FontForge generation completed successfully"); } @@ -92,7 +111,7 @@ fn macos_install_font(otf_file: &Utf8Path, name: &str) { .join("Library/Fonts") .join(name) .with_extension("otf"); - + log_trace!("FONTFORGE", "Installing font: {} -> {}", otf_file, dest); otf_file.cp(&dest).unwrap(); log_operation!("FONTFORGE", "Font installed to macOS Library/Fonts"); diff --git a/crates/localized/src/lib.rs b/crates/localized/src/lib.rs index 4594f77..255f3dd 100644 --- a/crates/localized/src/lib.rs +++ b/crates/localized/src/lib.rs @@ -3,33 +3,42 @@ mod tests; use builder_command::LocalizedCmd; use camino_fs::*; -use common::{Timer, log_command, log_operation, log_trace}; use common::site_fs::write_translations; +use common::{Timer, log_command, log_operation, log_trace}; use icu_locid::LanguageIdentifier; pub fn run(cmd: &LocalizedCmd) { let _timer = Timer::new("LOCALIZED processing"); - log_command!("LOCALIZED", "Processing localized files from: {}", cmd.input_dir); - log_operation!("LOCALIZED", "File extension: {}, Output destinations: {}", cmd.file_extension, cmd.output.len()); + log_command!( + "LOCALIZED", + "Processing localized files from: {}", + cmd.input_dir + ); + log_operation!( + "LOCALIZED", + "File extension: {}, Output destinations: {}", + cmd.file_extension, + cmd.output.len() + ); let variants = get_variants(cmd); log_operation!("LOCALIZED", "Found {} language variants", variants.len()); - + if variants.is_empty() { log_command!("LOCALIZED", "No matching files found, skipping processing"); return; } - + for (lang, content) in &variants { log_trace!("LOCALIZED", "Variant: {} ({} bytes)", lang, content.len()); } - + let name = format!( "{name}.{ext}", name = cmd.input_dir.file_name().unwrap(), ext = cmd.file_extension ); - + log_operation!("LOCALIZED", "Writing translations as: {}", name); write_translations(&name, &variants, &cmd.output); } @@ -48,7 +57,12 @@ fn get_variants(cmd: &LocalizedCmd) -> Vec<(LanguageIdentifier, Vec)> { let loc = file.file_stem().unwrap(); let langid: LanguageIdentifier = loc.parse().unwrap(); let content = file.read_bytes().unwrap(); - log_trace!("LOCALIZED", "Processing file: {} -> language: {}", file, langid); + log_trace!( + "LOCALIZED", + "Processing file: {} -> language: {}", + file, + langid + ); variants.push((langid, content)); } else if file.is_file() { log_trace!("LOCALIZED", "Skipping file (extension mismatch): {}", file); diff --git a/crates/swift_package/src/lib.rs b/crates/swift_package/src/lib.rs index 8eba441..5feedec 100644 --- a/crates/swift_package/src/lib.rs +++ b/crates/swift_package/src/lib.rs @@ -1,16 +1,25 @@ use builder_command::SwiftPackageCmd; -use common::{is_release, is_verbose, Timer, log_command, log_operation}; +use common::{Timer, is_release, is_verbose, log_command, log_operation}; use swift_package::{CliArgs, build_cli}; pub fn run(cmd: &SwiftPackageCmd) { let _timer = Timer::new("SWIFT_PACKAGE processing"); - log_command!("SWIFT_PACKAGE", "Building Swift package from: {}", cmd.manifest_dir); - + log_command!( + "SWIFT_PACKAGE", + "Building Swift package from: {}", + cmd.manifest_dir + ); + let verbose_level = if is_verbose() { 1 } else { 0 }; let release_mode = is_release(); - - log_operation!("SWIFT_PACKAGE", "Configuration: release={}, verbose={}", release_mode, verbose_level > 0); - + + log_operation!( + "SWIFT_PACKAGE", + "Configuration: release={}, verbose={}", + release_mode, + verbose_level > 0 + ); + let cli = CliArgs { quiet: false, package: None, @@ -24,8 +33,11 @@ pub fn run(cmd: &SwiftPackageCmd) { target_dir: None, manifest_path: Some(cmd.manifest_dir.clone()), }; - + log_operation!("SWIFT_PACKAGE", "Executing swift-package build command"); build_cli(cli).unwrap(); - log_operation!("SWIFT_PACKAGE", "Swift package build completed successfully"); + log_operation!( + "SWIFT_PACKAGE", + "Swift package build completed successfully" + ); } diff --git a/crates/uniffi/src/lib.rs b/crates/uniffi/src/lib.rs index bcad08d..294f432 100644 --- a/crates/uniffi/src/lib.rs +++ b/crates/uniffi/src/lib.rs @@ -12,12 +12,18 @@ pub fn run(cmd: &UniffiCmd) { log_operation!("UNIFFI", "UDL file: {}", cmd.udl_file); log_operation!("UNIFFI", "Output directory: {}", cmd.out_dir); log_operation!("UNIFFI", "Kotlin: {}, Swift: {}", cmd.kotlin, cmd.swift); - + let udl_copy = cmd.out_dir.join(cmd.udl_file.file_name().unwrap()); let cli_copy = cmd.out_dir.join("self.json"); let conf_copy = cmd.out_dir.join("uniffi.toml"); - - log_trace!("UNIFFI", "Checking cache files: udl={}, cli={}, config={}", udl_copy, cli_copy, conf_copy); + + log_trace!( + "UNIFFI", + "Checking cache files: udl={}, cli={}, config={}", + udl_copy, + cli_copy, + conf_copy + ); if udl_copy.exists() && cli_copy.exists() { let udl_ref_bytes = udl_copy.read_bytes().unwrap(); @@ -48,13 +54,16 @@ pub fn run(cmd: &UniffiCmd) { log_operation!("UNIFFI", "CLI parameters changed, regenerating bindings"); } (_, _, false) => { - log_operation!("UNIFFI", "Configuration file changed, regenerating bindings"); + log_operation!( + "UNIFFI", + "Configuration file changed, regenerating bindings" + ); } } } else { log_operation!("UNIFFI", "First time processing, setting up cache files"); } - + log_operation!("UNIFFI", "Setting up output directory and cache files"); cmd.out_dir.mkdirs().unwrap(); cmd.udl_file.cp(&udl_copy).unwrap(); @@ -65,7 +74,11 @@ pub fn run(cmd: &UniffiCmd) { cli_copy.write(cmd.to_string()).unwrap(); if cmd.kotlin { - log_operation!("UNIFFI", "Generating Kotlin bindings for library: {}", cmd.library_name); + log_operation!( + "UNIFFI", + "Generating Kotlin bindings for library: {}", + cmd.library_name + ); generate_external_bindings( &KotlinBindingGenerator, &cmd.udl_file, @@ -79,7 +92,11 @@ pub fn run(cmd: &UniffiCmd) { log_operation!("UNIFFI", "Kotlin bindings generation completed"); } if cmd.swift { - log_operation!("UNIFFI", "Generating Swift bindings for library: {}", cmd.library_name); + log_operation!( + "UNIFFI", + "Generating Swift bindings for library: {}", + cmd.library_name + ); generate_external_bindings( &SwiftBindingGenerator, &cmd.udl_file, diff --git a/crates/wasm/Cargo.toml b/crates/wasm/Cargo.toml index d3b9b49..ebe8c0a 100644 --- a/crates/wasm/Cargo.toml +++ b/crates/wasm/Cargo.toml @@ -14,6 +14,7 @@ builder-command = { path = "../command" } anyhow.workspace = true base64.workspace = true camino-fs.workspace = true +fs-err.workspace = true log.workspace = true seahash.workspace = true tempfile.workspace = true diff --git a/crates/wasm/src/dwarf.rs b/crates/wasm/src/dwarf.rs index a8f30c0..64ff266 100644 --- a/crates/wasm/src/dwarf.rs +++ b/crates/wasm/src/dwarf.rs @@ -1,7 +1,5 @@ -use std::{ - fs::File, - io::{BufReader, BufWriter}, -}; +use fs_err::File; +use std::io::{BufReader, BufWriter}; use anyhow::bail; use camino_fs::*; @@ -14,6 +12,98 @@ use wasmbin::{ fn as_custom_section(section: &Section) -> Option<&CustomSection> { section.try_as()?.try_contents().ok() } + +/// Get section type name and size information for debug output +fn get_section_debug_info(section: &Section) -> (String, Option) { + let section_name = match section { + Section::Custom(_) => { + if let Some(custom) = as_custom_section(section) { + format!("Custom({})", custom.name()) + } else { + "Custom(invalid)".to_string() + } + } + Section::Type(_) => "Type".to_string(), + Section::Import(_) => "Import".to_string(), + Section::Function(_) => "Function".to_string(), + Section::Table(_) => "Table".to_string(), + Section::Memory(_) => "Memory".to_string(), + Section::Global(_) => "Global".to_string(), + Section::Export(_) => "Export".to_string(), + Section::Start(_) => "Start".to_string(), + Section::Element(_) => "Element".to_string(), + Section::DataCount(_) => "DataCount".to_string(), + Section::Code(_) => "Code".to_string(), + Section::Data(_) => "Data".to_string(), + }; + + // Try to get size information by checking if we can get raw bytes + let size = match section { + Section::Custom(blob) => blob.try_as_raw().ok().map(|bytes| bytes.len()), + Section::Type(blob) => blob.try_as_raw().ok().map(|bytes| bytes.len()), + Section::Import(blob) => blob.try_as_raw().ok().map(|bytes| bytes.len()), + Section::Function(blob) => blob.try_as_raw().ok().map(|bytes| bytes.len()), + Section::Table(blob) => blob.try_as_raw().ok().map(|bytes| bytes.len()), + Section::Memory(blob) => blob.try_as_raw().ok().map(|bytes| bytes.len()), + Section::Global(blob) => blob.try_as_raw().ok().map(|bytes| bytes.len()), + Section::Export(blob) => blob.try_as_raw().ok().map(|bytes| bytes.len()), + Section::Start(blob) => blob.try_as_raw().ok().map(|bytes| bytes.len()), + Section::Element(blob) => blob.try_as_raw().ok().map(|bytes| bytes.len()), + Section::DataCount(blob) => blob.try_as_raw().ok().map(|bytes| bytes.len()), + Section::Code(blob) => blob.try_as_raw().ok().map(|bytes| bytes.len()), + Section::Data(blob) => blob.try_as_raw().ok().map(|bytes| bytes.len()), + }; + + (section_name, size) +} + +/// Debug output all WASM sections and their sizes +/// +/// Example output: +/// ```text +/// === WASM Sections Debug (original module) === +/// Total sections: 8 +/// 0: Type (142 bytes) +/// 1: Import (89 bytes) +/// 2: Function (45 bytes) +/// 3: Memory (3 bytes) +/// 4: Global (15 bytes) +/// 5: Export (234 bytes) +/// 6: Code (12856 bytes) +/// 7: Custom(.debug_info) (3456 bytes) +/// Total measured section size: 16840 bytes (8/8 sections) +/// === End WASM Sections Debug === +/// ``` +fn debug_wasm_sections(module: &Module, context: &str) { + log::debug!("=== WASM Sections Debug ({}) ===", context); + log::debug!("Total sections: {}", module.sections.len()); + + let mut total_size = 0usize; + let mut sections_with_size = 0usize; + + for (index, section) in module.sections.iter().enumerate() { + let (section_name, size) = get_section_debug_info(section); + + if let Some(size_bytes) = size { + log::debug!(" {}: {} ({} bytes)", index, section_name, size_bytes); + total_size += size_bytes; + sections_with_size += 1; + } else { + log::debug!(" {}: {} (size unknown)", index, section_name); + } + } + + if sections_with_size > 0 { + log::debug!( + "Total measured section size: {} bytes ({}/{} sections)", + total_size, + sections_with_size, + module.sections.len() + ); + } + log::debug!("=== End WASM Sections Debug ==="); +} + /// /// Returns `true` if this section should be stripped. fn is_strippable_section(section: &Section, strip_names: bool) -> bool { @@ -32,6 +122,9 @@ pub fn split_debug_symbols( let mut module = Module::decode_from(BufReader::new(File::open(wasm_path)?)) .map_err(|e| anyhow::anyhow!("Failed to decode WASM module: {}", e))?; + // Debug output: Original module sections + debug_wasm_sections(&module, "original module"); + let mut has_dwarf = false; let mut has_build_id = false; for section in module.sections.iter().filter_map(as_custom_section) { @@ -51,7 +144,10 @@ pub fn split_debug_symbols( bail!("WASM file already has a build id"); } - let build_id = Uuid::new_v4().as_bytes().to_vec(); + let build_num = Uuid::new_v4(); + log::info!("Using build id: {build_num}"); + + let build_id = build_num.as_bytes().to_vec(); module .sections .push(CustomSection::BuildId(build_id.clone()).into()); @@ -68,10 +164,14 @@ pub fn split_debug_symbols( .sections .retain(|section| !is_strippable_section(section, true)); - let is_adjacent = matches!((wasm_debug_path.parent(), wasm_path.parent()), (Some(p1), Some(p2)) if p1 == p2); + let is_adjacent = + matches!((wasm_debug_path.parent(), wasm_path.parent()), (Some(p1), Some(p2)) if p1 == p2); + // Add external debug info reference if we have a debug file if is_adjacent { let file_name = wasm_debug_path.file_name().unwrap().to_string(); + log::info!("Adding external debug info file name: {}", file_name); + module .sections .push(CustomSection::ExternalDebugInfo(file_name.into()).into()); diff --git a/crates/wasm/src/lib.rs b/crates/wasm/src/lib.rs index 60553a2..16d5cdc 100644 --- a/crates/wasm/src/lib.rs +++ b/crates/wasm/src/lib.rs @@ -6,7 +6,7 @@ use builder_command::{DebugSymbolsMode, WasmProcessingCmd}; use camino_fs::*; use common::site_fs::{SiteFile, write_file_to_site}; use common::{Timer, log_command, log_operation, log_trace}; -use std::{fs::File, hash::Hasher}; +use std::hash::Hasher; use wasm_opt::OptimizationOptions; use crate::dwarf::split_debug_symbols; @@ -63,7 +63,8 @@ pub fn run(cmd: &WasmProcessingCmd) { .write("this file has the mtime of the last time the wasm was built") .unwrap(); } - let wasm_mtime_file = File::open(&wasm_mtime_path).unwrap(); + // we use the std::fs as fs_err doesn't support setting mtime + let wasm_mtime_file = std::fs::File::open(&wasm_mtime_path).unwrap(); wasm_mtime_file.set_modified(wasm_mtime).unwrap(); let keep_debug = !matches!(cmd.debug_symbols, DebugSymbolsMode::Strip);