diff --git a/CLAUDE.md b/CLAUDE.md index ae340d3b58..c8048821af 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -8,7 +8,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co Perry is a native TypeScript compiler written in Rust that compiles TypeScript source code directly to native executables. It uses SWC for TypeScript parsing and LLVM for code generation. -**Current Version:** 0.5.1271 +**Current Version:** 0.5.1272 ## TypeScript Parity Status diff --git a/Cargo.lock b/Cargo.lock index e159822af1..4f2a20c510 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5503,7 +5503,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "perry" -version = "0.5.1271" +version = "0.5.1272" dependencies = [ "anyhow", "base64", @@ -5563,14 +5563,14 @@ dependencies = [ [[package]] name = "perry-api-manifest" -version = "0.5.1271" +version = "0.5.1272" dependencies = [ "serde", ] [[package]] name = "perry-audio-miniaudio" -version = "0.5.1271" +version = "0.5.1272" dependencies = [ "cc", "libc", @@ -5578,7 +5578,7 @@ dependencies = [ [[package]] name = "perry-codegen" -version = "0.5.1271" +version = "0.5.1272" dependencies = [ "anyhow", "log", @@ -5592,7 +5592,7 @@ dependencies = [ [[package]] name = "perry-codegen-arkts" -version = "0.5.1271" +version = "0.5.1272" dependencies = [ "anyhow", "perry-hir", @@ -5600,7 +5600,7 @@ dependencies = [ [[package]] name = "perry-codegen-glance" -version = "0.5.1271" +version = "0.5.1272" dependencies = [ "anyhow", "perry-hir", @@ -5608,7 +5608,7 @@ dependencies = [ [[package]] name = "perry-codegen-js" -version = "0.5.1271" +version = "0.5.1272" dependencies = [ "anyhow", "perry-dispatch", @@ -5617,7 +5617,7 @@ dependencies = [ [[package]] name = "perry-codegen-swiftui" -version = "0.5.1271" +version = "0.5.1272" dependencies = [ "anyhow", "perry-hir", @@ -5625,7 +5625,7 @@ dependencies = [ [[package]] name = "perry-codegen-wasm" -version = "0.5.1271" +version = "0.5.1272" dependencies = [ "anyhow", "base64", @@ -5637,7 +5637,7 @@ dependencies = [ [[package]] name = "perry-codegen-wear-tiles" -version = "0.5.1271" +version = "0.5.1272" dependencies = [ "anyhow", "perry-hir", @@ -5645,7 +5645,7 @@ dependencies = [ [[package]] name = "perry-container-compose" -version = "0.5.1271" +version = "0.5.1272" dependencies = [ "anyhow", "async-trait", @@ -5674,14 +5674,14 @@ dependencies = [ [[package]] name = "perry-container-e2e" -version = "0.5.1271" +version = "0.5.1272" dependencies = [ "anyhow", ] [[package]] name = "perry-diagnostics" -version = "0.5.1271" +version = "0.5.1272" dependencies = [ "serde", "serde_json", @@ -5689,7 +5689,7 @@ dependencies = [ [[package]] name = "perry-dispatch" -version = "0.5.1271" +version = "0.5.1272" [[package]] name = "perry-doc-fixture-my-bindings" @@ -5700,7 +5700,7 @@ dependencies = [ [[package]] name = "perry-doc-tests" -version = "0.5.1271" +version = "0.5.1272" dependencies = [ "anyhow", "clap", @@ -5715,7 +5715,7 @@ dependencies = [ [[package]] name = "perry-ext-ads" -version = "0.5.1271" +version = "0.5.1272" dependencies = [ "block2", "objc2", @@ -5725,7 +5725,7 @@ dependencies = [ [[package]] name = "perry-ext-argon2" -version = "0.5.1271" +version = "0.5.1272" dependencies = [ "argon2", "perry-ffi", @@ -5733,7 +5733,7 @@ dependencies = [ [[package]] name = "perry-ext-axios" -version = "0.5.1271" +version = "0.5.1272" dependencies = [ "perry-ffi", "reqwest", @@ -5742,7 +5742,7 @@ dependencies = [ [[package]] name = "perry-ext-bcrypt" -version = "0.5.1271" +version = "0.5.1272" dependencies = [ "bcrypt", "perry-ffi", @@ -5750,7 +5750,7 @@ dependencies = [ [[package]] name = "perry-ext-better-sqlite3" -version = "0.5.1271" +version = "0.5.1272" dependencies = [ "perry-ffi", "rusqlite", @@ -5758,7 +5758,7 @@ dependencies = [ [[package]] name = "perry-ext-cheerio" -version = "0.5.1271" +version = "0.5.1272" dependencies = [ "perry-ffi", "scraper", @@ -5766,7 +5766,7 @@ dependencies = [ [[package]] name = "perry-ext-commander" -version = "0.5.1271" +version = "0.5.1272" dependencies = [ "perry-ffi", "perry-runtime", @@ -5774,7 +5774,7 @@ dependencies = [ [[package]] name = "perry-ext-cron" -version = "0.5.1271" +version = "0.5.1272" dependencies = [ "chrono", "cron", @@ -5784,7 +5784,7 @@ dependencies = [ [[package]] name = "perry-ext-dayjs" -version = "0.5.1271" +version = "0.5.1272" dependencies = [ "chrono", "perry-ffi", @@ -5792,7 +5792,7 @@ dependencies = [ [[package]] name = "perry-ext-decimal" -version = "0.5.1271" +version = "0.5.1272" dependencies = [ "perry-ffi", "rust_decimal", @@ -5800,7 +5800,7 @@ dependencies = [ [[package]] name = "perry-ext-dotenv" -version = "0.5.1271" +version = "0.5.1272" dependencies = [ "perry-ffi", "serde_json", @@ -5808,7 +5808,7 @@ dependencies = [ [[package]] name = "perry-ext-ethers" -version = "0.5.1271" +version = "0.5.1272" dependencies = [ "perry-ffi", "rand 0.10.1", @@ -5816,7 +5816,7 @@ dependencies = [ [[package]] name = "perry-ext-events" -version = "0.5.1271" +version = "0.5.1272" dependencies = [ "perry-ffi", "perry-runtime", @@ -5824,14 +5824,14 @@ dependencies = [ [[package]] name = "perry-ext-exponential-backoff" -version = "0.5.1271" +version = "0.5.1272" dependencies = [ "perry-ffi", ] [[package]] name = "perry-ext-fastify" -version = "0.5.1271" +version = "0.5.1272" dependencies = [ "bytes", "http-body-util", @@ -5849,7 +5849,7 @@ dependencies = [ [[package]] name = "perry-ext-fetch" -version = "0.5.1271" +version = "0.5.1272" dependencies = [ "bytes", "lazy_static", @@ -5862,7 +5862,7 @@ dependencies = [ [[package]] name = "perry-ext-http" -version = "0.5.1271" +version = "0.5.1272" dependencies = [ "bytes", "h2", @@ -5886,7 +5886,7 @@ dependencies = [ [[package]] name = "perry-ext-ioredis" -version = "0.5.1271" +version = "0.5.1272" dependencies = [ "lazy_static", "perry-ffi", @@ -5896,7 +5896,7 @@ dependencies = [ [[package]] name = "perry-ext-jsonwebtoken" -version = "0.5.1271" +version = "0.5.1272" dependencies = [ "base64", "jsonwebtoken", @@ -5907,7 +5907,7 @@ dependencies = [ [[package]] name = "perry-ext-lru-cache" -version = "0.5.1271" +version = "0.5.1272" dependencies = [ "lru", "perry-ffi", @@ -5916,7 +5916,7 @@ dependencies = [ [[package]] name = "perry-ext-moment" -version = "0.5.1271" +version = "0.5.1272" dependencies = [ "chrono", "perry-ffi", @@ -5924,7 +5924,7 @@ dependencies = [ [[package]] name = "perry-ext-mongodb" -version = "0.5.1271" +version = "0.5.1272" dependencies = [ "bson", "futures-util", @@ -5936,7 +5936,7 @@ dependencies = [ [[package]] name = "perry-ext-mysql2" -version = "0.5.1271" +version = "0.5.1272" dependencies = [ "chrono", "perry-ffi", @@ -5946,7 +5946,7 @@ dependencies = [ [[package]] name = "perry-ext-nanoid" -version = "0.5.1271" +version = "0.5.1272" dependencies = [ "nanoid", "perry-ffi", @@ -5955,7 +5955,7 @@ dependencies = [ [[package]] name = "perry-ext-net" -version = "0.5.1271" +version = "0.5.1272" dependencies = [ "bytes", "perry-ffi", @@ -5968,7 +5968,7 @@ dependencies = [ [[package]] name = "perry-ext-node-forge" -version = "0.5.1271" +version = "0.5.1272" dependencies = [ "const-oid 0.9.6", "der 0.7.10", @@ -5987,7 +5987,7 @@ dependencies = [ [[package]] name = "perry-ext-nodemailer" -version = "0.5.1271" +version = "0.5.1272" dependencies = [ "lettre", "perry-ffi", @@ -5997,7 +5997,7 @@ dependencies = [ [[package]] name = "perry-ext-pdf" -version = "0.5.1271" +version = "0.5.1272" dependencies = [ "perry-ffi", "printpdf", @@ -6005,7 +6005,7 @@ dependencies = [ [[package]] name = "perry-ext-pg" -version = "0.5.1271" +version = "0.5.1272" dependencies = [ "perry-ffi", "sqlx", @@ -6014,7 +6014,7 @@ dependencies = [ [[package]] name = "perry-ext-ratelimit" -version = "0.5.1271" +version = "0.5.1272" dependencies = [ "governor", "perry-ffi", @@ -6022,7 +6022,7 @@ dependencies = [ [[package]] name = "perry-ext-sharp" -version = "0.5.1271" +version = "0.5.1272" dependencies = [ "fast_image_resize", "image", @@ -6032,14 +6032,14 @@ dependencies = [ [[package]] name = "perry-ext-slugify" -version = "0.5.1271" +version = "0.5.1272" dependencies = [ "perry-ffi", ] [[package]] name = "perry-ext-streams" -version = "0.5.1271" +version = "0.5.1272" dependencies = [ "lazy_static", "perry-ffi", @@ -6048,7 +6048,7 @@ dependencies = [ [[package]] name = "perry-ext-undici" -version = "0.5.1271" +version = "0.5.1272" dependencies = [ "perry-ffi", "perry-runtime", @@ -6057,7 +6057,7 @@ dependencies = [ [[package]] name = "perry-ext-uuid" -version = "0.5.1271" +version = "0.5.1272" dependencies = [ "perry-ffi", "uuid", @@ -6065,7 +6065,7 @@ dependencies = [ [[package]] name = "perry-ext-validator" -version = "0.5.1271" +version = "0.5.1272" dependencies = [ "perry-ffi", "regex", @@ -6075,7 +6075,7 @@ dependencies = [ [[package]] name = "perry-ext-ws" -version = "0.5.1271" +version = "0.5.1272" dependencies = [ "futures-util", "lazy_static", @@ -6088,7 +6088,7 @@ dependencies = [ [[package]] name = "perry-ext-zlib" -version = "0.5.1271" +version = "0.5.1272" dependencies = [ "brotli", "flate2", @@ -6098,7 +6098,7 @@ dependencies = [ [[package]] name = "perry-ffi" -version = "0.5.1271" +version = "0.5.1272" dependencies = [ "dashmap", "once_cell", @@ -6107,7 +6107,7 @@ dependencies = [ [[package]] name = "perry-hir" -version = "0.5.1271" +version = "0.5.1272" dependencies = [ "anyhow", "perry-api-manifest", @@ -6125,7 +6125,7 @@ dependencies = [ [[package]] name = "perry-parser" -version = "0.5.1271" +version = "0.5.1272" dependencies = [ "anyhow", "perry-diagnostics", @@ -6137,7 +6137,7 @@ dependencies = [ [[package]] name = "perry-runtime" -version = "0.5.1271" +version = "0.5.1272" dependencies = [ "anyhow", "base64", @@ -6178,14 +6178,14 @@ dependencies = [ [[package]] name = "perry-runtime-static" -version = "0.5.1271" +version = "0.5.1272" dependencies = [ "perry-runtime", ] [[package]] name = "perry-stdlib" -version = "0.5.1271" +version = "0.5.1272" dependencies = [ "aes 0.8.4", "aes 0.9.1", @@ -6280,14 +6280,14 @@ dependencies = [ [[package]] name = "perry-stdlib-static" -version = "0.5.1271" +version = "0.5.1272" dependencies = [ "perry-stdlib", ] [[package]] name = "perry-transform" -version = "0.5.1271" +version = "0.5.1272" dependencies = [ "anyhow", "perry-hir", @@ -6296,14 +6296,14 @@ dependencies = [ [[package]] name = "perry-ui" -version = "0.5.1271" +version = "0.5.1272" dependencies = [ "perry-ui-model", ] [[package]] name = "perry-ui-android" -version = "0.5.1271" +version = "0.5.1272" dependencies = [ "base64", "itoa", @@ -6320,7 +6320,7 @@ dependencies = [ [[package]] name = "perry-ui-geisterhand" -version = "0.5.1271" +version = "0.5.1272" dependencies = [ "rand 0.10.1", "serde", @@ -6330,7 +6330,7 @@ dependencies = [ [[package]] name = "perry-ui-gtk4" -version = "0.5.1271" +version = "0.5.1272" dependencies = [ "base64", "cairo-rs 0.22.0", @@ -6353,7 +6353,7 @@ dependencies = [ [[package]] name = "perry-ui-ios" -version = "0.5.1271" +version = "0.5.1272" dependencies = [ "base64", "block2", @@ -6369,7 +6369,7 @@ dependencies = [ [[package]] name = "perry-ui-macos" -version = "0.5.1271" +version = "0.5.1272" dependencies = [ "base64", "block2", @@ -6384,7 +6384,7 @@ dependencies = [ [[package]] name = "perry-ui-model" -version = "0.5.1271" +version = "0.5.1272" [[package]] name = "perry-ui-test" @@ -6395,11 +6395,11 @@ dependencies = [ [[package]] name = "perry-ui-testkit" -version = "0.5.1271" +version = "0.5.1272" [[package]] name = "perry-ui-tvos" -version = "0.5.1271" +version = "0.5.1272" dependencies = [ "base64", "block2", @@ -6415,7 +6415,7 @@ dependencies = [ [[package]] name = "perry-ui-visionos" -version = "0.5.1271" +version = "0.5.1272" dependencies = [ "base64", "block2", @@ -6431,7 +6431,7 @@ dependencies = [ [[package]] name = "perry-ui-watchos" -version = "0.5.1271" +version = "0.5.1272" dependencies = [ "block2", "libc", @@ -6444,7 +6444,7 @@ dependencies = [ [[package]] name = "perry-ui-windows" -version = "0.5.1271" +version = "0.5.1272" dependencies = [ "base64", "libc", @@ -6461,14 +6461,14 @@ dependencies = [ [[package]] name = "perry-ui-windows-winui" -version = "0.5.1271" +version = "0.5.1272" dependencies = [ "perry-ui-windows", ] [[package]] name = "perry-updater" -version = "0.5.1271" +version = "0.5.1272" dependencies = [ "anyhow", "base64", @@ -6484,7 +6484,7 @@ dependencies = [ [[package]] name = "perry-wasm-host" -version = "0.5.1271" +version = "0.5.1272" dependencies = [ "wasmi", ] diff --git a/Cargo.toml b/Cargo.toml index 7534801e3b..d157d3f773 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -292,7 +292,7 @@ codegen-units = 16 codegen-units = 16 [workspace.package] -version = "0.5.1271" +version = "0.5.1272" edition = "2021" license = "MIT" repository = "https://github.com/PerryTS/perry" diff --git a/changelog.d/7118-auto-provision-ext-bindings.md b/changelog.d/7118-auto-provision-ext-bindings.md new file mode 100644 index 0000000000..48749c72f2 --- /dev/null +++ b/changelog.d/7118-auto-provision-ext-bindings.md @@ -0,0 +1,19 @@ +`perry compile` now auto-detects, auto-builds, and auto-links every native ext +binding and the wasm host a program actually references — no manual +`cargo build -p …` prebuild, no `PERRY_FORCE_WELL_KNOWN`, and no dependence on a +module being outside `compilePackages`. + +- Link: codegen now routes any emitted `js__*` FFI (`js_ioredis_*`, + `js_undici_*`, `js_node_forge_*`) to its well-known wrapper off provenance + alone, so an AOT-compiled `iovalkey`/`undici`/`node-forge` (a + `compilePackages` member that never appears in any import set) links its + `perry-ext-*` staticlib instead of failing with + `Undefined symbols: _js_ioredis_new`. +- Build: routed CPU-only ext staticlibs are auto-built from workspace source + when missing (the shared-tokio ones were already rebuilt in the auto-optimize + invocation), and `perry-wasm-host` is auto-built whenever the program uses + `WebAssembly.*`, so `libperry_wasm_host.a not found` can no longer happen on a + normal compile. +- Maintainer audit: propagate custom `CARGO_TARGET_DIR` wasm-host artifacts + through symbol scanning, link manifests, and the final link, and reject + incomplete HarmonyOS SDKs before configuring their cross-build environment. diff --git a/crates/perry-codegen/src/ext_registry.rs b/crates/perry-codegen/src/ext_registry.rs index 7280a4abe9..b65d1ffa12 100644 --- a/crates/perry-codegen/src/ext_registry.rs +++ b/crates/perry-codegen/src/ext_registry.rs @@ -44,6 +44,17 @@ //! live exclusively (or primarily) outside `perry-runtime`. Symbols //! served by both `perry-runtime` AND a wrapper crate (most of `js_*`) //! aren't in the table; they resolve from the always-linked runtime. +//! +//! ## Prefix net (ext bindings with a `js__*` convention) +//! +//! The exact table above is hand-maintained per symbol, which is right for +//! bindings whose symbol names don't map cleanly to a binding key +//! (`js_node_http_*` → `http`). But a large family of data-store / client +//! wrappers name every symbol `js__*` 1:1 with the binding key +//! (`js_ioredis_*` → `perry-ext-ioredis`). Those are routed generically by +//! [`EXT_PREFIX_REGISTRY`], so an AOT-compiled `iovalkey`/`undici`/`node-forge` +//! (a `perry.compilePackages` member that never appears in any import set) +//! still flips its wrapper onto the link line off the emitted FFI alone. use std::cell::RefCell; use std::collections::HashSet; @@ -576,6 +587,43 @@ const FFI_REGISTRY: &[(&str, OwnerKind)] = &[ ("js_mysql2_connection_end", OwnerKind::WellKnown("mysql2")), ]; +/// Prefix-based routing for ext-binding FFI whose emitted symbols follow the +/// stable `js__*` naming convention 1:1 with a `perry-ext-*` wrapper. +/// +/// The per-symbol [`FFI_REGISTRY`] above is exhaustive but hand-maintained: it +/// only routes the exact symbols someone remembered to add. That is fine for +/// bindings whose symbol names DON'T map cleanly to the binding key +/// (`js_node_http_*` → `http`, `js_event_emitter_*` → `events`), but it silently +/// loses whole families whose symbols DO follow the convention. A program that +/// AOT-compiles `iovalkey` (in `perry.compilePackages`) lowers its client usage +/// to `js_ioredis_new` — a symbol that is in NEITHER the exact table nor any +/// import set — so the well-known flip never fires and the link dies with +/// `Undefined symbols: _js_ioredis_new` even though `perry-ext-ioredis` exists. +/// +/// This prefix net closes that gap generally: ANY emitted `js__*` +/// symbol for a listed ext binding flips its well-known wrapper onto the link +/// line off codegen provenance alone — no source-level import, no +/// `compilePackages` exclusion, no membership in the well-known iteration set +/// required. Adding a symbol to one of these families needs no table edit. +/// +/// The routing key is the exact `well_known_bindings.toml` binding key, so the +/// driver's existing routing (`build_optimized_libs`) resolves the crate/lib and +/// strips the duplicate perry-stdlib feature with no further wiring. Prefixes +/// are checked only AFTER the exact table and the crypto net, so a symbol that +/// wants bespoke routing can always override its family by adding an exact row. +#[rustfmt::skip] +const EXT_PREFIX_REGISTRY: &[(&str, &str)] = &[ + // Redis / Valkey RESP client (perry-ext-ioredis). `ioredis`, `iovalkey`, + // and `valkey` all share this wrapper + the `js_ioredis_*` surface, so the + // single `ioredis` binding key covers every RESP package that lowers here. + ("js_ioredis_", "ioredis"), + // undici HTTP/1.1 client + Agent/ProxyAgent (perry-ext-undici). + ("js_undici_", "undici"), + // node-forge PKI subset — RSA keygen, X.509 build/sign, PEM + // (perry-ext-node-forge). Hyphenated package → underscored prefix. + ("js_node_forge_", "node-forge"), +]; + /// Process-wide collector of provider keys observed during codegen. /// Populated by [`record_ffi_call`] from `LlBlock::call` / `call_void`. /// Drained by [`take_used_providers`] right before `build_optimized_libs`. @@ -656,6 +704,30 @@ pub(crate) fn record_ffi_call(symbol: &str) { set.insert("js_crypto_"); } }); + return; + } + + // Ext-binding prefix net: an emitted `js__*` symbol flips its + // well-known wrapper onto the link line off codegen provenance alone. This + // is what lets an AOT-compiled `iovalkey` / `undici` / `node-forge` (in + // `perry.compilePackages`, so never in any import set) still link its + // `perry-ext-*` staticlib. The MODULE_CAPTURE marker is the matched prefix + // itself: replaying it (object-cache manifest, #6439) re-enters this arm + // and reproduces the same owner — `"js_ioredis_".starts_with("js_ioredis_")`. + for (prefix, binding) in EXT_PREFIX_REGISTRY { + if symbol.starts_with(prefix) { + let owner = OwnerKind::WellKnown(binding); + { + let mut guard = USED_PROVIDERS.lock().expect("USED_PROVIDERS poisoned"); + guard.get_or_insert_with(HashSet::new).insert(owner); + } + MODULE_CAPTURE.with(|cell| { + if let Some(set) = cell.borrow_mut().as_mut() { + set.insert(prefix); + } + }); + return; + } } } @@ -1023,4 +1095,78 @@ mod tests { assert_symbol_routes_to(symbol, OwnerKind::WellKnown("net")); } } + + /// The measured `_js_ioredis_new` link gap: an AOT-compiled `iovalkey` + /// (a `perry.compilePackages` member, so never in `native_module_imports` + /// and never in the well-known iteration set) lowers its client usage to + /// `js_ioredis_new`. The prefix net must route the WHOLE `js_ioredis_*` / + /// `js_undici_*` / `js_node_forge_*` family to its well-known wrapper off + /// codegen provenance alone — no exact-table row per symbol required. + #[test] + fn emitted_ext_prefix_symbols_route_to_well_known_binding() { + let _guard = PROVIDER_TEST_LOCK + .lock() + .expect("provider test lock poisoned"); + for (symbol, binding) in [ + ("js_ioredis_new", "ioredis"), + ("js_ioredis_set", "ioredis"), + ("js_ioredis_hgetall", "ioredis"), + ("js_undici_request", "undici"), + ("js_undici_proxy_agent_new", "undici"), + ("js_node_forge_generate_key_pair", "node-forge"), + ("js_node_forge_create_certificate", "node-forge"), + ] { + assert_symbol_routes_to(symbol, OwnerKind::WellKnown(binding)); + } + } + + /// The prefix net must NOT swallow symbols that are not part of a listed + /// ext family: an unknown `js_*` symbol records no provider, and a + /// prefix-family symbol must never leak into a DIFFERENT binding. + #[test] + fn ext_prefix_net_does_not_over_match() { + let _guard = PROVIDER_TEST_LOCK + .lock() + .expect("provider test lock poisoned"); + let _ = take_used_providers(); + record_ffi_call("js_ioredis_new"); + let got = take_used_providers(); + assert!(got.contains(&OwnerKind::WellKnown("ioredis"))); + assert!( + !got.contains(&OwnerKind::WellKnown("undici")), + "ioredis symbol must not flip undici, got {got:?}" + ); + + let _ = take_used_providers(); + record_ffi_call("js_some_unrelated_runtime_symbol"); + assert!( + take_used_providers().is_empty(), + "an unlisted js_* symbol must record no provider" + ); + } + + /// The prefix-family capture marker (the matched prefix) must survive an + /// object-cache round trip: replaying it reproduces the same well-known + /// flip, so a warm `.o` cache links identically to a cold one (#6439 shape). + #[test] + fn ext_prefix_marker_survives_module_capture_replay() { + let _guard = PROVIDER_TEST_LOCK + .lock() + .expect("provider test lock poisoned"); + let _ = take_used_providers(); + + begin_module_capture(); + record_ffi_call("js_ioredis_new"); + record_ffi_call("js_ioredis_get"); // same family: one marker, deduped + let captured = take_module_capture(); + assert_eq!(captured, vec!["js_ioredis_"]); + + let _ = take_used_providers(); + replay_ffi_symbols(captured); + let got = take_used_providers(); + assert!( + got.contains(&OwnerKind::WellKnown("ioredis")), + "replaying the captured prefix marker must reproduce the ioredis flip, got {got:?}" + ); + } } diff --git a/crates/perry-runtime/src/os_network.rs b/crates/perry-runtime/src/os_network.rs index a045d49391..ee2603306b 100644 --- a/crates/perry-runtime/src/os_network.rs +++ b/crates/perry-runtime/src/os_network.rs @@ -31,6 +31,15 @@ fn fallback_mac_address() -> String { "00:00:00:00:00:00".to_string() } +// Callers are `collect_link_macs` (macOS/BSD), `collect_windows_adapters` +// (Windows), and the unit tests. On Linux/Android the MAC is read as a +// preformatted string from `/sys`, so this helper has no caller there and +// would trip `-D dead-code`; gate it to the union of its real callers. +#[cfg(any( + windows, + all(unix, not(any(target_os = "linux", target_os = "android"))), + test +))] fn format_mac_address(bytes: &[u8]) -> String { if bytes.len() != 6 { return fallback_mac_address(); diff --git a/crates/perry/src/commands/compile.rs b/crates/perry/src/commands/compile.rs index 529875c6ca..092a0e1e9a 100644 --- a/crates/perry/src/commands/compile.rs +++ b/crates/perry/src/commands/compile.rs @@ -75,9 +75,9 @@ use init_order::{classify_eager_modules, topo_sort_non_entry_modules}; pub use library_search::find_library; pub(crate) use library_search::host_target_triple; use library_search::{ - build_geisterhand_libs, find_geisterhand_library, find_geisterhand_runtime, - find_geisterhand_stdlib, find_geisterhand_ui, find_harmonyos_sdk, find_lld_link, - find_llvm_tool, find_msvc_lib_paths, find_msvc_link_exe, find_perry_windows_sdk, + build_geisterhand_libs, build_wasm_host_library, find_geisterhand_library, + find_geisterhand_runtime, find_geisterhand_stdlib, find_geisterhand_ui, find_harmonyos_sdk, + find_lld_link, find_llvm_tool, find_msvc_lib_paths, find_msvc_link_exe, find_perry_windows_sdk, find_runtime_library, find_stdlib_library, find_ui_library, find_wasm_host_library, find_windows_archiver, windows_archiver_command, windows_default_output_extension, windows_pe_subsystem_flag, windows_subsystem_needs_ui, diff --git a/crates/perry/src/commands/compile/library_search.rs b/crates/perry/src/commands/compile/library_search.rs index 5128c9931d..32ec8de951 100644 --- a/crates/perry/src/commands/compile/library_search.rs +++ b/crates/perry/src/commands/compile/library_search.rs @@ -1209,6 +1209,110 @@ pub(super) fn find_wasm_host_library(target: Option<&str>) -> Option { find_library(lib_name, target) } +/// Auto-provision the wasmi WebAssembly host staticlib (issue #76 follow-up). +/// +/// A bare `perry compile` of a program that references `WebAssembly.*` sets +/// `ctx.needs_wasm_runtime`, which pulls `perry-runtime/wasm-host` (so the +/// runtime carries the `perry_wasm_host_*` extern references) and links +/// `libperry_wasm_host.a`. But nothing BUILT that staticlib: it lives in the +/// standalone `perry-wasm-host` crate (isolated so non-wasm builds don't drag +/// in wasmi), and the auto-optimize runtime/stdlib rebuild never touches it. +/// The result was a hard `libperry_wasm_host.a not found` error that forced a +/// manual `cargo build --release -p perry-wasm-host` prebuild. +/// +/// This builds it on demand from workspace source — a plain leaf +/// `cargo build --release -p perry-wasm-host` into `target/release` (the same +/// mechanism `build_missing_prebuilt_ext_lib` uses for CPU-only ext wrappers), +/// which is exactly where `find_wasm_host_library` then locates it (including +/// via the `CARGO_MANIFEST_DIR/../../target/release` candidate when perry runs +/// out-of-tree). Returns the resolved path, or `None` when there's no workspace +/// source to build from or the build fails (the caller then surfaces the +/// original not-found guidance). A no-op when the archive already exists — +/// callers guard on `find_wasm_host_library(...).is_none()` first. +pub(super) fn build_wasm_host_library( + target: Option<&str>, + format: OutputFormat, + verbose: u8, +) -> Option { + let workspace_root = find_perry_workspace_root()?; + let crate_dir = workspace_root.join("crates").join("perry-wasm-host"); + if !crate_dir.is_dir() { + if matches!(format, OutputFormat::Text) && verbose > 0 { + eprintln!( + " wasm-host: skipping auto-build — crate source not found at {}", + crate_dir.display() + ); + } + return None; + } + + if matches!(format, OutputFormat::Text) { + println!(" wasm-host: building perry-wasm-host from workspace source"); + } + + let mut cargo_cmd = Command::new("cargo"); + cargo_cmd + .current_dir(&workspace_root) + .arg("build") + .arg("--release") + .arg("-p") + .arg("perry-wasm-host"); + if let Some(triple) = rust_target_triple(target) { + cargo_cmd.arg("--target").arg(triple); + } + + match cargo_cmd.status() { + Ok(status) if status.success() => {} + Ok(status) => { + if matches!(format, OutputFormat::Text) { + eprintln!(" wasm-host: cargo build failed ({status})"); + } + return None; + } + Err(err) => { + if matches!(format, OutputFormat::Text) { + eprintln!(" wasm-host: failed to spawn cargo ({err})"); + } + return None; + } + } + + // Prefer the explicit output path cargo just wrote (respecting + // CARGO_TARGET_DIR + cross-target triple subdir) so the returned archive + // is unambiguously the one we built. Fall back to the standard search so a + // non-standard layout still resolves. + let lib_name = match target { + Some("windows") | Some("windows-winui") => "perry_wasm_host.lib", + #[cfg(target_os = "windows")] + None => "perry_wasm_host.lib", + _ => "libperry_wasm_host.a", + }; + let mut release_dir = match std::env::var_os("CARGO_TARGET_DIR") { + Some(raw) if !raw.is_empty() => { + let path = PathBuf::from(raw); + if path.is_absolute() { + path + } else { + workspace_root.join(path) + } + } + _ => workspace_root.join("target"), + }; + if let Some(triple) = rust_target_triple(target) { + release_dir = release_dir.join(triple); + } + let built = release_dir.join("release").join(lib_name); + if built.exists() { + return Some(built); + } + + let found = find_wasm_host_library(target); + if found.is_none() && matches!(format, OutputFormat::Text) && verbose > 0 { + eprintln!(" wasm-host: cargo finished but {lib_name} was not located"); + } + found +} + /// Find the UI library for linking (optional - only needed when perry/ui is imported). /// /// HarmonyOS is intentionally absent: there is no `perry-ui-harmonyos` @@ -1255,22 +1359,28 @@ pub(super) fn find_ui_library(target: Option<&str>) -> Option { /// Studio's default install locations per platform. Returns `None` if nothing /// resembling an OHOS SDK is present; the caller is expected to surface a /// remediation message naming the env var. +fn is_usable_harmonyos_native_sdk(path: &Path) -> bool { + let bin = path.join("llvm").join("bin"); + let has_clang = bin.join("clang").is_file() || bin.join("clang.exe").is_file(); + has_clang && path.join("sysroot").is_dir() +} + pub(super) fn find_harmonyos_sdk() -> Option { fn normalize(p: PathBuf) -> Option { // Accept either `` or `/native` — we want the `native` dir // so callers can unconditionally join `llvm/bin/clang` and `sysroot`. - if p.join("llvm").join("bin").exists() && p.join("sysroot").exists() { + if is_usable_harmonyos_native_sdk(&p) { return Some(p); } let native = p.join("native"); - if native.join("llvm").join("bin").exists() && native.join("sysroot").exists() { + if is_usable_harmonyos_native_sdk(&native) { return Some(native); } // DevEco's layout nests the API-level dir: /openharmony//native if let Ok(entries) = std::fs::read_dir(p.join("openharmony")) { for entry in entries.flatten() { let candidate = entry.path().join("native"); - if candidate.join("llvm").join("bin").exists() { + if is_usable_harmonyos_native_sdk(&candidate) { return Some(candidate); } } @@ -1421,6 +1531,25 @@ mod android_cross_env_tests { } } +#[cfg(test)] +mod harmonyos_sdk_tests { + use super::is_usable_harmonyos_native_sdk; + + #[test] + fn native_sdk_requires_both_clang_and_sysroot() { + let sdk = tempfile::tempdir().expect("tempdir"); + let bin = sdk.path().join("llvm/bin"); + std::fs::create_dir_all(&bin).expect("create llvm bin"); + assert!(!is_usable_harmonyos_native_sdk(sdk.path())); + + std::fs::write(bin.join("clang"), b"").expect("create clang"); + assert!(!is_usable_harmonyos_native_sdk(sdk.path())); + + std::fs::create_dir(sdk.path().join("sysroot")).expect("create sysroot"); + assert!(is_usable_harmonyos_native_sdk(sdk.path())); + } +} + /// Cross-compile env vars to pass to `cargo build` so `cc-rs` picks up the /// OHOS SDK's clang + musl sysroot for any C source in dependency build.rs /// scripts (notably `libmimalloc-sys`, which needs `pthread.h`). @@ -1435,8 +1564,17 @@ pub(super) fn harmonyos_cross_env( Some("harmonyos-simulator") => ("x86_64-unknown-linux-ohos", "x86_64-linux-ohos"), _ => ("aarch64-unknown-linux-ohos", "aarch64-linux-ohos"), }; - let clang = sdk_native.join("llvm").join("bin").join("clang"); - let clangpp = sdk_native.join("llvm").join("bin").join("clang++"); + let bin = sdk_native.join("llvm").join("bin"); + let clang = if bin.join("clang").is_file() { + bin.join("clang") + } else { + bin.join("clang.exe") + }; + let clangpp = if bin.join("clang++").is_file() { + bin.join("clang++") + } else { + bin.join("clang++.exe") + }; let sysroot = sdk_native.join("sysroot"); let cflags = format!( "--target={} --sysroot={} -D__MUSL__", diff --git a/crates/perry/src/commands/compile/optimized_libs/driver.rs b/crates/perry/src/commands/compile/optimized_libs/driver.rs index f6f1c8dcbf..bded177b15 100644 --- a/crates/perry/src/commands/compile/optimized_libs/driver.rs +++ b/crates/perry/src/commands/compile/optimized_libs/driver.rs @@ -174,15 +174,51 @@ pub(crate) fn build_optimized_libs( // exists on disk (partial build / release tarball // missing the wrapper). if !needs_shared_tokio { - let Some(lib_path) = super::super::well_known::bundled_staticlib_path_for_target( + // CPU-only wrapper: prefer the workspace-built copy on disk. + // When it's missing (fresh checkout, or a `compilePackages` + // binding that a human never prebuilt — the measured + // perry-ext-node-forge gap), auto-build it from workspace + // source instead of silently falling back to the perry-stdlib + // copy. A bare `perry compile` of a program whose codegen + // emitted `js_node_forge_*` must not require a manual + // `cargo build -p perry-ext-node-forge` first. `build_missing_ + // prebuilt_ext_lib` is the same plain-build mechanism the + // no-auto path already uses (a leaf `cargo build --release -p + // ` into `target/release`) — CPU-only wrappers don't + // need the shared-tokio invocation, so this stays isolated + // from the specialized runtime/stdlib rebuild. + let lib_path = super::super::well_known::bundled_staticlib_path_for_target( workspace_root, binding, rust_target_triple(target), - ) else { + ) + .or_else(|| { + let filename = super::super::well_known::ext_staticlib_filename( + &binding.lib, + rust_target_triple(target), + ); + if matches!(format, OutputFormat::Text) && verbose > 0 { + eprintln!( + " well-known: `lib{}.a` not on disk for `{}` — auto-building \ + `{}` from workspace source.", + binding.lib, module, binding.krate + ); + } + super::no_auto::build_missing_prebuilt_ext_lib( + workspace_root, + binding, + &filename, + target, + format, + verbose, + ) + }); + let Some(lib_path) = lib_path else { if matches!(format, OutputFormat::Text) && verbose > 0 { eprintln!( " well-known: skipping `{}` — bundled `lib{}.a` not found \ - in target/release; falling back to perry-stdlib copy.", + in target/release and the auto-build produced nothing; \ + falling back to perry-stdlib copy.", module, binding.lib ); } diff --git a/crates/perry/src/commands/compile/optimized_libs/no_auto.rs b/crates/perry/src/commands/compile/optimized_libs/no_auto.rs index 73c608bb3a..11eaf30b22 100644 --- a/crates/perry/src/commands/compile/optimized_libs/no_auto.rs +++ b/crates/perry/src/commands/compile/optimized_libs/no_auto.rs @@ -5,7 +5,9 @@ use std::process::Command; use crate::OutputFormat; -use super::super::{find_perry_workspace_root, rust_target_triple, CompilationContext}; +use super::super::{ + find_perry_workspace_root, is_android_target, rust_target_triple, CompilationContext, +}; /// Resolve well-known wrapper archives without rebuilding runtime/stdlib. /// @@ -174,6 +176,39 @@ pub(crate) fn build_missing_prebuilt_ext_lib( if let Some(triple) = rust_target_triple(target) { cargo_cmd.arg("--target").arg(triple); } + // Cross-compile envs, mirroring `build_optimized_libs`: without the OHOS + // SDK / Android NDK clang on the compile env, a `--target harmonyos` / + // Android auto-build of a C-dependent CPU-only wrapper (e.g. sharp) fails + // in build.rs before we can fall back. Apply the same target-specific env + // the specialized invocation uses so the auto-build can actually succeed. + if matches!(target, Some("harmonyos") | Some("harmonyos-simulator")) { + match super::super::library_search::find_harmonyos_sdk() { + Some(sdk) => { + for (k, v) in super::super::library_search::harmonyos_cross_env(&sdk, target) { + cargo_cmd.env(k, v); + } + } + None => { + if matches!(format, OutputFormat::Text) && verbose > 0 { + eprintln!( + " well-known (no-auto): skipping `{}` — OHOS SDK not found (set \ + OHOS_SDK_HOME); falling back.", + binding.krate + ); + } + return None; + } + } + } + if is_android_target(target) { + if let Some(ndk) = std::env::var_os("ANDROID_NDK_HOME") { + for (k, v) in + super::super::library_search::android_cross_env(std::path::Path::new(&ndk), target) + { + cargo_cmd.env(k, v); + } + } + } let status = match cargo_cmd.status() { Ok(status) => status, diff --git a/crates/perry/src/commands/compile/optimized_libs/tests.rs b/crates/perry/src/commands/compile/optimized_libs/tests.rs index cd6863c30e..a380bc7775 100644 --- a/crates/perry/src/commands/compile/optimized_libs/tests.rs +++ b/crates/perry/src/commands/compile/optimized_libs/tests.rs @@ -319,6 +319,38 @@ fn undici_needs_shared_tokio() { assert!(binding_needs_shared_tokio("undici")); } +/// The emitted-FFI → link derivation resolves to real well-known bindings. +/// The codegen prefix net routes `js_ioredis_*` / `js_undici_*` / +/// `js_node_forge_*` to these binding keys; each must exist in the shipped +/// `well_known_bindings.toml` and map to its `perry-ext-*` crate, or the +/// driver's routing loop would silently drop the flip. +#[test] +fn ext_prefix_binding_keys_resolve_to_wrapper_crates() { + for (key, krate) in [ + ("ioredis", "perry-ext-ioredis"), + ("undici", "perry-ext-undici"), + ("node-forge", "perry-ext-node-forge"), + ] { + let binding = super::super::well_known::lookup_well_known(key) + .unwrap_or_else(|| panic!("`{key}` must be a well-known binding")); + assert_eq!(binding.krate, krate, "binding `{key}` routes to `{krate}`"); + } +} + +/// The auto-build selection split: ioredis/undici carry their own tokio and +/// must ride the shared auto-optimize invocation, while node-forge is CPU-only +/// (routes async through perry-stdlib's spawn_blocking shim) and is auto-built +/// by the isolated leaf-build path in the driver's CPU-only branch. +#[test] +fn ext_binding_build_routing_split() { + assert!(binding_needs_shared_tokio("ioredis")); + assert!(binding_needs_shared_tokio("undici")); + assert!( + !binding_needs_shared_tokio("node-forge"), + "node-forge is CPU-only and must not ride the shared-tokio invocation" + ); +} + #[test] fn unknown_modules_default_to_workspace_path() { // Defensive default: if a module isn't in the allowlist, @@ -683,6 +715,84 @@ printf '!\n' > "$CARGO_TARGET_DIR/release/libperry_ext_http.a" ); } +/// Issue #76 follow-up: a bare `perry compile` of a `WebAssembly.*` program +/// must auto-build `perry-wasm-host` instead of hard-failing with +/// `libperry_wasm_host.a not found`. The build is a plain leaf +/// `cargo build --release -p perry-wasm-host` into `target/release`; drive it +/// with a fake cargo (so no real toolchain runs) and assert the resolved path. +#[cfg(unix)] +#[test] +fn wasm_host_auto_build_produces_staticlib_from_workspace_source() { + use std::os::unix::fs::PermissionsExt; + + let _guard = env_lock(); + let old_path = std::env::var_os("PATH"); + let old_cargo_target_dir = std::env::var_os("CARGO_TARGET_DIR"); + let old_workspace_root = std::env::var_os("PERRY_WORKSPACE_ROOT"); + + let workspace = tempfile::tempdir().expect("tempdir"); + // A crate dir + workspace marker so `find_perry_workspace_root` (via + // PERRY_WORKSPACE_ROOT) and the `crate_dir.is_dir()` guard both pass. + for dir in [ + "crates/perry-wasm-host", + "crates/perry-runtime", + "crates/perry-ui-geisterhand", + ] { + std::fs::create_dir_all(workspace.path().join(dir)).expect("mkdir workspace marker"); + } + + let fake_bin = workspace.path().join("fake-bin"); + std::fs::create_dir_all(&fake_bin).expect("mkdir fake bin"); + let fake_cargo = fake_bin.join("cargo"); + std::fs::write( + &fake_cargo, + r#"#!/bin/sh +case "$*" in + *"-p perry-wasm-host"*) ;; + *) exit 43 ;; +esac +mkdir -p "$CARGO_TARGET_DIR/release" +printf '!\n' > "$CARGO_TARGET_DIR/release/libperry_wasm_host.a" +"#, + ) + .expect("write fake cargo"); + let mut perms = std::fs::metadata(&fake_cargo) + .expect("fake cargo metadata") + .permissions(); + perms.set_mode(0o755); + std::fs::set_permissions(&fake_cargo, perms).expect("chmod fake cargo"); + + let target_dir = workspace.path().join("out-target"); + let test_path = match old_path.as_ref() { + Some(path) => { + let mut paths = vec![fake_bin.clone()]; + paths.extend(std::env::split_paths(path)); + std::env::join_paths(paths).expect("join PATH") + } + None => fake_bin.clone().into_os_string(), + }; + std::env::set_var("PATH", test_path); + std::env::set_var("CARGO_TARGET_DIR", &target_dir); + std::env::set_var("PERRY_WORKSPACE_ROOT", workspace.path()); + + let got = super::super::library_search::build_wasm_host_library(None, OutputFormat::Json, 0); + + set_env_var("PATH", old_path.as_deref().and_then(|v| v.to_str())); + set_env_var( + "CARGO_TARGET_DIR", + old_cargo_target_dir.as_deref().and_then(|v| v.to_str()), + ); + set_env_var( + "PERRY_WORKSPACE_ROOT", + old_workspace_root.as_deref().and_then(|v| v.to_str()), + ); + + assert_eq!( + got.expect("missing wasm host lib should be built from workspace source"), + target_dir.join("release/libperry_wasm_host.a") + ); +} + /// Binary/workspace skew: a cross-feature the on-disk checkout's /// perry-runtime doesn't declare must be dropped (and reported), not passed /// through to fail the entire cargo resolve — that failure's prebuilt diff --git a/crates/perry/src/commands/compile/resolve.rs b/crates/perry/src/commands/compile/resolve.rs index 8762d88a40..4a43004f35 100644 --- a/crates/perry/src/commands/compile/resolve.rs +++ b/crates/perry/src/commands/compile/resolve.rs @@ -938,6 +938,11 @@ fn resolve_exports_with_conditions( /// perry_fn_…unicorn_magic…__toPath`). Matches the `resolve_subpath_import` /// condition order (chalk's `#supports-color` `{ node, default }`); the two /// resolvers must agree. +// First-match convenience wrapper. Current call sites use +// `resolve_exports_candidates` / `resolve_exports_with_conditions` directly, so +// this has no non-test caller and would trip `-D dead-code`; kept as the +// documented single-result entry point (mirrors `resolve_subpath_import`). +#[allow(dead_code)] pub(super) fn resolve_exports(exports: &serde_json::Value, subpath: &str) -> Option { resolve_exports_with_conditions( exports, diff --git a/crates/perry/src/commands/compile/run_pipeline.rs b/crates/perry/src/commands/compile/run_pipeline.rs index d9f047e535..1c560337be 100644 --- a/crates/perry/src/commands/compile/run_pipeline.rs +++ b/crates/perry/src/commands/compile/run_pipeline.rs @@ -4734,6 +4734,24 @@ pub fn run_with_parse_cache( } } + // Issue #76 follow-up — auto-provision the wasmi host staticlib. A + // program that references `WebAssembly.*` sets `ctx.needs_wasm_runtime` + // (feature_detect.rs), which links `libperry_wasm_host.a`. That archive + // lives in the standalone `perry-wasm-host` crate and is built by NOTHING + // in the normal compile path — not the auto-optimize runtime/stdlib + // rebuild, not codegen — so a bare `perry compile` used to die with + // `libperry_wasm_host.a not found` until a human ran + // `cargo build --release -p perry-wasm-host`. Build it on demand here so + // both the symbol-stub scan below and the final link resolve it. No-op + // when it already exists (or when the program doesn't use wasm). + let use_wasm_host = ctx.needs_wasm_runtime || args.enable_wasm_runtime; + let wasm_host_lib_resolved = if use_wasm_host { + find_wasm_host_library(target.as_deref()) + .or_else(|| build_wasm_host_library(target.as_deref(), format, verbose)) + } else { + None + }; + // Auto-mode: pick the smallest matching (features, panic) profile // for this binary and rebuild perry-runtime + perry-stdlib in a // hash-keyed target dir. Both halves fall back to the prebuilt full @@ -4775,12 +4793,7 @@ pub fn run_with_parse_cache( // being linked, scan its archive so the `perry_wasm_host_*` symbols // are recognised as defined and we don't synthesise empty stubs that // would shadow the real implementations. - let use_wasm_host = ctx.needs_wasm_runtime || args.enable_wasm_runtime; - let wasm_host_lib_path = if use_wasm_host { - find_wasm_host_library(target.as_deref()) - } else { - None - }; + let wasm_host_lib_path = wasm_host_lib_resolved.clone(); let mut all_scan_paths: Vec = obj_paths.clone(); if let Some(ref p) = runtime_lib_path { all_scan_paths.push(p.clone()); @@ -5456,10 +5469,8 @@ pub fn run_with_parse_cache( if let Some(p) = &stdlib_lib_resolved { push_archive(&mut link_archives, "stdlib", p); } - if ctx.needs_wasm_runtime || args.enable_wasm_runtime { - if let Some(p) = find_wasm_host_library(target.as_deref()) { - push_archive(&mut link_archives, "wasm-host", &p); - } + if let Some(p) = &wasm_host_lib_resolved { + push_archive(&mut link_archives, "wasm-host", p); } if ctx.needs_ui { if let Some(p) = find_ui_library(target.as_deref()) { @@ -5791,8 +5802,8 @@ pub fn run_with_parse_cache( // a hard error when codegen detected `WebAssembly.*` usage, otherwise the // flag-only case silently degrades to None (the user will hit a link // error on first use, with the symbol name as the breadcrumb). - let wasm_host_lib = if ctx.needs_wasm_runtime || args.enable_wasm_runtime { - match find_wasm_host_library(target.as_deref()) { + let wasm_host_lib = if use_wasm_host { + match wasm_host_lib_resolved { Some(lib) => { if let OutputFormat::Text = format { println!("Using wasmi WebAssembly host runtime"); diff --git a/crates/perry/src/commands/compile/well_known.rs b/crates/perry/src/commands/compile/well_known.rs index 05b34435e6..f76404dea7 100644 --- a/crates/perry/src/commands/compile/well_known.rs +++ b/crates/perry/src/commands/compile/well_known.rs @@ -34,16 +34,23 @@ pub struct WellKnownBinding { /// gate in `scripts/binding_pins.mjs`. `None` for entries exempt /// from pinning (`node_builtin`, `alias_of`, and perry-owned /// packages). + // Provenance metadata parsed from `well_known_bindings.toml` and consulted + // by the lock-step review gate + unit tests, not by the compile/link path, + // so these carry no reader in the `perry` bins build and would trip + // `-D dead-code` without an explicit allow. + #[allow(dead_code)] pub upstream: Option, /// `true` when this binding ports a Node.js **builtin** module /// (`node:zlib`/`net`/`http`/…) rather than a third-party npm /// package. Its upstream is Node core, not an npm dist, so it /// carries no npm provenance pin. + #[allow(dead_code)] pub node_builtin: bool, /// When set, this row is an **alias** for another binding (a /// package subpath like `mysql2/promise`, or a bare-name alias like /// `fetch` → `node-fetch`) and shares that binding's provenance /// instead of carrying its own pin. + #[allow(dead_code)] pub alias_of: Option, } diff --git a/docs/api/perry.d.ts b/docs/api/perry.d.ts index 54d8383746..de0a0738c2 100644 --- a/docs/api/perry.d.ts +++ b/docs/api/perry.d.ts @@ -1,6 +1,6 @@ // Auto-generated from Perry's API manifest (#465). Do not edit by hand. // Source: perry-api-manifest::API_MANIFEST -// Coverage: 1989 entries across 120 modules +// Coverage: 1998 entries across 121 modules type PerryU32 = number & { readonly __perryU32?: never }; type PerryU64 = number & { readonly __perryU64?: never }; @@ -2186,6 +2186,25 @@ declare module "node-fetch" { export default function (...args: any[]): any; } +declare module "node-forge" { + /** stdlib */ + export function certificateFromPem(...args: any[]): any; + /** stdlib */ + export function certificateToPem(...args: any[]): any; + /** stdlib */ + export function create(...args: any[]): any; + /** stdlib */ + export function createCertificate(...args: any[]): any; + /** stdlib */ + export function generateKeyPair(...args: any[]): any; + /** stdlib */ + export function privateKeyFromPem(...args: any[]): any; + /** stdlib */ + export function privateKeyToPem(...args: any[]): any; + /** stdlib */ + export function publicKeyToPem(...args: any[]): any; +} + declare module "node-pty" { /** stdlib */ const _default: any; @@ -3857,6 +3876,8 @@ declare module "tls" { /** stdlib */ export function connect(p0: any, p1: any, p2: any, p3: any): any; /** stdlib */ + export function convertALPNProtocols(protocols: any, out: any): any; + /** stdlib */ export function createSecureContext(options: any): any; /** stdlib */ export function createServer(options: any, secureConnectionListener: any): any; diff --git a/docs/src/api/reference.md b/docs/src/api/reference.md index bb55e9466d..54fbc4e236 100644 --- a/docs/src/api/reference.md +++ b/docs/src/api/reference.md @@ -2,7 +2,7 @@ This page is auto-generated from Perry's compile-time API manifest (`perry-api-manifest::API_MANIFEST`). It is the source of truth for what `perry compile` accepts; references to symbols not listed here produce `R005 UnimplementedApi` (issue #463). Stubs (#464) are flagged ⚠ — they link cleanly but no-op at runtime on the chosen target. -Total: 2899 entries across 122 modules. +Total: 2912 entries across 123 modules. ## Modules @@ -63,6 +63,7 @@ Total: 2899 entries across 122 modules. - [`net`](#net) - [`node-cron`](#node-cron) - [`node-fetch`](#node-fetch) +- [`node-forge`](#node-forge) - [`node-pty`](#node-pty) - [`nodemailer`](#nodemailer) - [`os`](#os) @@ -2174,6 +2175,23 @@ Total: 2899 entries across 122 modules. - `default` — module +## `node-forge` + +### Methods + +- `certificateFromPem` — module +- `certificateToPem` — module +- `create` — module +- `createCertificate` — module +- `generateKeyPair` — module +- `privateKeyFromPem` — module +- `privateKeyToPem` — module +- `publicKeyToPem` — module +- `setExtensions` — instance *(class: `Certificate`)* +- `setIssuer` — instance *(class: `Certificate`)* +- `setSubject` — instance *(class: `Certificate`)* +- `sign` — instance *(class: `Certificate`)* + ## `node-pty` ### Methods @@ -3461,6 +3479,7 @@ Total: 2899 entries across 122 modules. - `checkServerIdentity` — module - `close` — instance *(class: `Server`)* - `connect` — module +- `convertALPNProtocols` — module - `createSecureContext` — module - `createServer` — module - `eventNames` — instance *(class: `Server`)* diff --git a/test-parity/gap_snapshot.json b/test-parity/gap_snapshot.json index f5d0e46515..858df1980e 100644 --- a/test-parity/gap_snapshot.json +++ b/test-parity/gap_snapshot.json @@ -128,13 +128,6 @@ "added": "2026-07-04", "category": "module-inventory", "reason": "node:v8 second cluster (#3680+); standing per #5917." - }, - "test_gap_zlib_3285_params": { - "status": "parity_fail", - "issue": "3285", - "added": "2026-07-04", - "category": "bug-open", - "reason": "zlib params (#3285); standing per #5917." } } } diff --git a/workspace-architecture.json b/workspace-architecture.json index 6dfa49001f..eeeb25e6c2 100644 --- a/workspace-architecture.json +++ b/workspace-architecture.json @@ -1,9 +1,13 @@ { "schema_version": 1, "expected_default_members": [ - "perry" + "perry", + "perry-container-compose" + ], + "allowed_binding_runtime_dependencies": [ + "perry-ext-node-forge", + "perry-ext-undici" ], - "allowed_binding_runtime_dependencies": [], "ci": { "linux_host_excluded_members": [ "perry-ui-android", @@ -21,7 +25,7 @@ ] }, "baseline": { - "workspace_members": 76, + "workspace_members": 78, "default_dependency_closure": [ "perry", "perry-api-manifest", @@ -32,6 +36,7 @@ "perry-codegen-swiftui", "perry-codegen-wasm", "perry-codegen-wear-tiles", + "perry-container-compose", "perry-diagnostics", "perry-dispatch", "perry-hir", @@ -61,7 +66,7 @@ "perry-updater" ], "decision_counts": { - "externalize": 29, + "externalize": 31, "keep": 42, "merge": 1, "remove": 1, @@ -197,6 +202,10 @@ "category": "binding", "decision": "externalize" }, + "perry-ext-node-forge": { + "category": "binding", + "decision": "externalize" + }, "perry-ext-undici": { "category": "binding", "decision": "externalize"