diff --git a/Cargo.lock b/Cargo.lock index 0e0659e..356d939 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1448,8 +1448,9 @@ dependencies = [ [[package]] name = "wacore" -version = "0.6.0" -source = "git+https://github.com/oxidezap/whatsapp-rust?branch=main#3237c720adb5f6d735741605da8b38d4199f9dfa" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6139a98fece24956f1d6bf164c82c5daefb43834813d96a879b41e010707c2c2" dependencies = [ "aes", "anyhow", @@ -1496,8 +1497,9 @@ dependencies = [ [[package]] name = "wacore-appstate" -version = "0.6.0" -source = "git+https://github.com/oxidezap/whatsapp-rust?branch=main#3237c720adb5f6d735741605da8b38d4199f9dfa" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5afb5372c68d1cda49b7e86a81a01302b7c71b78e8555178c7cf3be01f09ecc3" dependencies = [ "anyhow", "buffa", @@ -1517,8 +1519,9 @@ dependencies = [ [[package]] name = "wacore-binary" -version = "0.6.0" -source = "git+https://github.com/oxidezap/whatsapp-rust?branch=main#3237c720adb5f6d735741605da8b38d4199f9dfa" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e56452f93ac1e6167a6ebafec23971fe1ca9332b27cc887520f4a6753956fde" dependencies = [ "bytes", "compact_str", @@ -1534,8 +1537,9 @@ dependencies = [ [[package]] name = "wacore-derive" -version = "0.6.0" -source = "git+https://github.com/oxidezap/whatsapp-rust?branch=main#3237c720adb5f6d735741605da8b38d4199f9dfa" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53cba66b01df4bef777c3d131af434dea0f3c8a7da2e42d7fb74e11fd65fb094" dependencies = [ "proc-macro2", "quote", @@ -1544,8 +1548,9 @@ dependencies = [ [[package]] name = "wacore-libsignal" -version = "0.6.0" -source = "git+https://github.com/oxidezap/whatsapp-rust?branch=main#3237c720adb5f6d735741605da8b38d4199f9dfa" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbf8e90eebdf77703305089c1dfe672dddf203e0238e8f47163f76e7f9d5c713" dependencies = [ "aes", "async-lock", @@ -1575,8 +1580,9 @@ dependencies = [ [[package]] name = "wacore-noise" -version = "0.6.0" -source = "git+https://github.com/oxidezap/whatsapp-rust?branch=main#3237c720adb5f6d735741605da8b38d4199f9dfa" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a15b03c1e5e4bbf20d2d875c96649aaff82d149f025ae3a1421a1b6a9c426114" dependencies = [ "anyhow", "buffa", @@ -1603,8 +1609,9 @@ dependencies = [ [[package]] name = "waproto" -version = "0.6.0" -source = "git+https://github.com/oxidezap/whatsapp-rust?branch=main#3237c720adb5f6d735741605da8b38d4199f9dfa" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd19a927d4781e92382a478d234f74baeacb4822ceeb45c665bb9cb321622c35" dependencies = [ "buffa", "buffa-build", @@ -1734,8 +1741,9 @@ dependencies = [ [[package]] name = "whatsapp-rust" -version = "0.6.0" -source = "git+https://github.com/oxidezap/whatsapp-rust?branch=main#3237c720adb5f6d735741605da8b38d4199f9dfa" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cb354c32641cfe832bf5c5767a016762234c5efd1b4ce979e2b3e32175d96bc" dependencies = [ "anyhow", "async-channel", diff --git a/Cargo.toml b/Cargo.toml index 1079840..c73f427 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -97,7 +97,7 @@ web-sys = { version = "0.3", features = [ "WritableStream", "Window", ] } -whatsapp-rust = { git = "https://github.com/oxidezap/whatsapp-rust", branch = "main", default-features = false, features = ["danger-skip-cert-chain-verify", "legacy-session-interop"] } +whatsapp-rust = { version = "0.7.0", default-features = false, features = ["danger-skip-cert-chain-verify", "legacy-session-interop"] } # Runs the crate's `#[test]` unit tests on wasm32 in Node via # `wasm-pack test --node` — plain `cargo test` can't, because the crate only diff --git a/codegen/src/main.rs b/codegen/src/main.rs index 373ce11..a1e883d 100644 --- a/codegen/src/main.rs +++ b/codegen/src/main.rs @@ -10,46 +10,135 @@ use std::path::{Path, PathBuf}; use syn::{Attribute, Fields, GenericArgument, Item, PathArguments, Type, TypePath}; 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. +/// Where the core's sources live, as two roots rather than one: published +/// crates put `wacore` and `whatsapp-rust` side by side in the registry, while +/// a git checkout or a working clone nests `wacore/` inside the repository. +struct Sources { + wacore_src: PathBuf, + core_src: PathBuf, +} + +/// Locate the core's sources on disk. /// /// 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 +/// it — `gen:bridge-types` runs `cargo fetch` first for that. When the sources +/// are absent every lookup here 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"); - if let Ok(lock_content) = std::fs::read_to_string(lock_path) { - // Find commit hash from: source = "git+https://...whatsapp-rust?...#" - for line in lock_content.lines() { - if line.contains("whatsapp-rust") - && line.contains("#") - && let Some(hash_start) = line.rfind('#') - { - let full_hash = line[hash_start + 1..].trim_end_matches('"'); - // Cargo checkouts use first 7 chars of the commit hash as dir name - let short_hash = &full_hash[..7]; - - // Search for the checkout dir in ~/.cargo/git/checkouts/ - let cargo_home = std::env::var("CARGO_HOME").unwrap_or_else(|_| { - let home = std::env::var("HOME").expect("HOME not set"); - format!("{}/.cargo", home) - }); - let checkouts = PathBuf::from(&cargo_home).join("git/checkouts"); - - if let Ok(entries) = std::fs::read_dir(&checkouts) { - for entry in entries.flatten() { - let name = entry.file_name(); - if name.to_string_lossy().starts_with("whatsapp-rust-") { - let candidate = entry.path().join(short_hash); - if candidate.exists() { - eprintln!("Using Cargo git cache: {}", candidate.display()); - return candidate; - } +fn find_sources() -> Sources { + let lock = std::fs::read_to_string("../Cargo.lock").unwrap_or_default(); + + // Registry layout: each crate is unpacked on its own, so the two roots are + // siblings named `-`. + if let (Some(core), Some(wacore)) = ( + locked_registry_version(&lock, "whatsapp-rust"), + locked_registry_version(&lock, "wacore"), + ) && let Some(registry) = registry_root_with(&core, &wacore) + { + eprintln!("Using registry: {}", registry.display()); + return Sources { + wacore_src: registry.join(format!("wacore-{wacore}")).join("src"), + core_src: registry.join(format!("whatsapp-rust-{core}")).join("src"), + }; + } + + // Repository layout: one root, `wacore/` nested inside it. + let root = find_repository_root(&lock); + Sources { + wacore_src: root.join("wacore/src"), + core_src: root.join("src"), + } +} + +/// A package as the lock file describes it. `source` is absent for path +/// dependencies and patches, and names the registry or git remote otherwise. +struct LockedPackage { + version: String, + source: Option, +} + +/// Parse `[[package]]` stanzas rather than scanning loose lines, so a version +/// is never read from one package and a source from another. +fn locked_package(lock: &str, crate_name: &str) -> Option { + for stanza in lock.split("[[package]]") { + let field = |key: &str| { + stanza.lines().find_map(|line| { + line.trim() + .strip_prefix(&format!("{key} = ")) + .map(|value| value.trim_matches('"').to_string()) + }) + }; + if field("name").as_deref() == Some(crate_name) { + return Some(LockedPackage { + version: field("version")?, + source: field("source"), + }); + } + } + None +} + +/// The version to look for in a registry checkout, or `None` when the lock +/// points somewhere else. +/// +/// A dependency temporarily on a git revision or a patched path can share a +/// version number with a registry copy still sitting in the cache. Reading that +/// copy would generate declarations for a revision Cargo is not going to +/// compile, silently and without falling back. +fn locked_registry_version(lock: &str, crate_name: &str) -> Option { + let package = locked_package(lock, crate_name)?; + match package.source { + Some(source) if source.starts_with("registry+") => Some(package.version), + _ => None, + } +} + +/// Registry roots that hold both packages. `registry/src` can carry several — +/// a private registry beside crates.io, or leftovers from an older protocol — +/// and only some of them will have what is being looked for. +fn registry_root_with(core: &str, wacore: &str) -> Option { + let cargo_home = std::env::var("CARGO_HOME").unwrap_or_else(|_| { + let home = std::env::var("HOME").expect("HOME not set"); + format!("{home}/.cargo") + }); + std::fs::read_dir(PathBuf::from(cargo_home).join("registry/src")) + .ok()? + .flatten() + .map(|entry| entry.path()) + .find(|root| { + root.join(format!("whatsapp-rust-{core}")).is_dir() + && root.join(format!("wacore-{wacore}")).is_dir() + }) +} + +/// Git checkout keyed by the locked commit, else a working clone beside this +/// repository. +fn find_repository_root(lock: &str) -> PathBuf { + for line in lock.lines() { + if line.contains("whatsapp-rust") + && line.contains('#') + && let Some(hash_start) = line.rfind('#') + { + let full_hash = line[hash_start + 1..].trim_end_matches('"'); + // Cargo checkouts use the first 7 chars of the commit hash. + let short_hash = &full_hash[..7.min(full_hash.len())]; + let cargo_home = std::env::var("CARGO_HOME").unwrap_or_else(|_| { + let home = std::env::var("HOME").expect("HOME not set"); + format!("{home}/.cargo") + }); + let checkouts = PathBuf::from(&cargo_home).join("git/checkouts"); + if let Ok(entries) = std::fs::read_dir(&checkouts) { + for entry in entries.flatten() { + if entry + .file_name() + .to_string_lossy() + .starts_with("whatsapp-rust-") + { + let candidate = entry.path().join(short_hash); + if candidate.exists() { + eprintln!("Using Cargo git cache: {}", candidate.display()); + return candidate; } } } @@ -57,7 +146,6 @@ fn find_whatsapp_rust_root() -> PathBuf { } } - // Fallback to local clone let fallback = PathBuf::from("../../whatsapp-rust"); eprintln!("Using local path: {}", fallback.display()); fallback @@ -66,38 +154,38 @@ fn find_whatsapp_rust_root() -> PathBuf { /// 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 +/// Every directory the generator reads is checked, not just the first: wacore +/// alone contributes most of the types, so a tree missing only the core's own +/// `src/` would produce a plausible file and slip past an emptiness check. +fn require_sources(sources: &Sources) { + let required = [ + sources.wacore_src.clone(), + sources.core_src.join("features"), + sources.core_src.join("types"), + sources.core_src.join("send"), + ]; + let missing: Vec = required .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\ + "whatsapp-rust sources are incomplete\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(), + `cargo fetch` has to have populated them — that is what `gen:bridge-types` runs \n\ + first. A sibling clone of whatsapp-rust also works.", 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"); + let sources = find_sources(); + require_sources(&sources); + let wacore_dir = sources.wacore_src.clone(); + let src_dir = sources.core_src.clone(); let mut all_types = BTreeMap::new(); @@ -137,11 +225,15 @@ fn main() { // 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()); + eprintln!( + "parsed {} types from {} and {}", + all_types.len(), + sources.wacore_src.display(), + sources.core_src.display() + ); assert!( !all_types.is_empty(), - "no types parsed from {}: refusing to generate an empty type file", - root.display() + "no types parsed: refusing to generate an empty type file" ); // Build TypeScript content