diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 99e0ab9..45dac46 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -93,6 +93,13 @@ jobs: - name: Build the wasm package run: bun run build + # `bun run build` regenerates the type file. If what it produced differs + # from what is committed, the tarball's .d.ts would not be the reviewed + # one — which is how a build with missing sources shipped a .d.ts + # referencing types it no longer declared. + - name: Fail if the generated types drifted + run: git diff --exit-code -- src/generated_types.rs + - name: Run Tests with Bun run: bun test env: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index fb97b9f..fcc738b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -243,6 +243,13 @@ jobs: - name: Build the wasm package run: bun run build + # `bun run build` regenerates the type file. If what it produced differs + # from what is committed, the tarball's .d.ts would not be the reviewed + # one — which is how a build with missing sources shipped a .d.ts + # referencing types it no longer declared. + - name: Fail if the generated types drifted + run: git diff --exit-code -- src/generated_types.rs + - run: bun test - name: Rust unit tests (wasm32 in Node) @@ -322,6 +329,12 @@ jobs: - name: Build the wasm package run: bun run build + # The tarball is built from this step's output, so the same drift + # check applies here: publish what was reviewed, not what a runner + # happened to regenerate. + - name: Fail if the generated types drifted + run: git diff --exit-code -- src/generated_types.rs + # `--ignore-scripts` below suppresses the prepack hook, and prepack is # where the tarball guard lives — run it directly so a build that leaks # pkg/ or duplicate files still fails before publishing. diff --git a/codegen/src/main.rs b/codegen/src/main.rs index b54e878..373ce11 100644 --- a/codegen/src/main.rs +++ b/codegen/src/main.rs @@ -12,6 +12,14 @@ use walkdir::WalkDir; /// Find whatsapp-rust source dir from Cargo's git cache by parsing Cargo.lock. /// Falls back to `../../whatsapp-rust/` for local development. +/// +/// This crate does not depend on whatsapp-rust, so `cargo run` never fetches +/// it: the sources have to already be on disk. On a machine where they are not +/// — a CI runner with a cold cache, where `gen` runs before anything downloads +/// the core — every lookup below misses, and `WalkDir` over a missing directory +/// yields nothing rather than erroring. That silently produced a type file with +/// almost everything missing, which still compiled and shipped a `.d.ts` +/// referencing types it no longer declared. Hence `require_sources`. fn find_whatsapp_rust_root() -> PathBuf { // Try to find it in Cargo's git checkout cache let lock_path = Path::new("../Cargo.lock"); @@ -55,8 +63,39 @@ fn find_whatsapp_rust_root() -> PathBuf { fallback } +/// Refuse to generate from sources that are not there. Parsing nothing is not +/// an empty result, it is a broken one. +/// +/// Every path the generator reads is checked, not just the first: `wacore` +/// alone contributes enough types that a checkout missing only `src/` would +/// produce a plausible-looking file and slip past an emptiness check. +fn require_sources(root: &Path) { + let missing: Vec = REQUIRED_SOURCES + .iter() + .map(|rel| root.join(rel)) + .filter(|path| !path.exists()) + .map(|path| path.display().to_string()) + .collect(); + + assert!( + missing.is_empty(), + "whatsapp-rust sources are incomplete at {}\n\ + Missing: {}\n\ + This generator reads the core's sources off disk rather than depending on it, so \n\ + `cargo fetch` has to have populated the git checkout — that is what `gen:bridge-types` \n\ + runs first. A sibling clone of whatsapp-rust also works.", + root.display(), + missing.join(", ") + ); +} + +/// Everything `main` parses. Kept next to the guard so adding a source without +/// guarding it is a visible omission rather than a silent one. +const REQUIRED_SOURCES: [&str; 4] = ["wacore/src", "src/features", "src/types", "src/send"]; + fn main() { let root = find_whatsapp_rust_root(); + require_sources(&root); let wacore_dir = root.join("wacore/src"); let src_dir = root.join("src"); @@ -71,8 +110,12 @@ fn main() { parse_file(entry.path(), &mut all_types); } - // Parse whatsapp-rust feature types - let feature_dirs = ["features", "types"]; + // Parse whatsapp-rust feature types. `send` replaces a hardcoded read of + // `send.rs`, which the core has since split into a module: the old path + // stopped matching and the call did nothing. Nothing was lost — the types + // it named are not `Serialize`, so this generator never emitted them — but + // pointing at what exists keeps the guard above honest. + let feature_dirs = ["features", "types", "send"]; for dir in feature_dirs { let path = src_dir.join(dir); if path.exists() { @@ -89,6 +132,18 @@ fn main() { // Also parse send.rs for SendOptions/RevokeType parse_file(&src_dir.join("send.rs"), &mut all_types); + // Reported so a collapse is visible in the build log. Deliberately not + // asserted against a floor: the real count is what it is, and a threshold + // close to it breaks every legitimate change to the core. What guards the + // output is `require_sources` above and the drift check in CI, which + // compares against the committed file instead of guessing a number. + eprintln!("parsed {} types from {}", all_types.len(), root.display()); + assert!( + !all_types.is_empty(), + "no types parsed from {}: refusing to generate an empty type file", + root.display() + ); + // Build TypeScript content let mut ts = String::new(); diff --git a/package.json b/package.json index f472aaf..b6b740c 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "scripts": { "bench": "bun run build && bun run benches/binary.ts && bun run benches/signal.ts && bun run benches/curve.ts && bun run benches/crypto.ts", "bench:node": "bun run build && node --expose-gc benches/binary.ts && node --expose-gc benches/signal.ts && node --expose-gc benches/curve.ts && node --expose-gc benches/crypto.ts", - "gen:bridge-types": "cd codegen && cargo run -q --bin gen-types --target $(rustc -vV | grep host | cut -d' ' -f2) > ../src/generated_types.rs.tmp && mv ../src/generated_types.rs.tmp ../src/generated_types.rs", + "gen:bridge-types": "cargo fetch && cd codegen && cargo run -q --bin gen-types --target $(rustc -vV | grep host | cut -d' ' -f2) > ../src/generated_types.rs.tmp && mv ../src/generated_types.rs.tmp ../src/generated_types.rs", "gen:proto-codec": "bun run scripts/gen-ts-proto.ts", "gen:proto-types": "bun run scripts/gen-protobufjs-dts.ts", "gen": "bun run gen:bridge-types && bun run gen:proto-codec && bun run gen:proto-types",