Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
13 changes: 13 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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.
Expand Down
59 changes: 57 additions & 2 deletions codegen/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down Expand Up @@ -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<String> = 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);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Fetch the core before requiring its checkout

On a fresh clone or GitHub-hosted runner, bun run build invokes gen:bridge-types before any root-crate Cargo command can materialize the whatsapp-rust checkout, so this call panics when no sibling clone exists. In particular, the build-and-test job and the separate publish job go from bun install directly to this build; Swatinem/rust-cache caches Cargo's git database/build outputs rather than guaranteeing the working checkout expected here. Add an explicit cargo fetch/cargo metadata step before these builds, or make the generator populate the checkout itself, so the documented full-build command and release publish work on clean machines.

AGENTS.md reference: AGENTS.md:L97-L103

Useful? React with 👍 / 👎.

let wacore_dir = root.join("wacore/src");
let src_dir = root.join("src");

Expand All @@ -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() {
Expand All @@ -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();

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down