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
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,32 @@ pub(super) fn detect_optional_feature_usage(
ctx.needs_wasm_runtime = true;
}

// Robust fallback for WebAssembly detection. The static lowering in
// `module_static.rs` sets `hir_module.uses_webassembly` for direct
// `WebAssembly.Module`/`instantiate`/etc. call sites, but a minified
// bundle can reach the `WebAssembly` global via dynamic property access
// (`const WA = WebAssembly; WA.Module(bytes)`, `globalThis.WebAssembly`,
// `globalThis["WebAssembly"]`) that lowers to an ordinary `PropertyGet`
// or `Ident` without hitting any set-site. The codegen still emits
// `js_webassembly_*` FFI calls for those paths, so without this fallback
// the `wasm-host` feature stays off and the link dies with
// `_js_webassembly_module_new` undefined. Mirror the fetch/crypto
// fallbacks above: scan the final HIR for the `WebAssembly` token.
// Over-matching only over-links the wasm host (a size cost); the rule
// is zero false negatives.
if !ctx.needs_wasm_runtime {
let hir_debug: String = format!(
"{:?}{:?}{:?}",
&hir_module.init, &hir_module.functions, &hir_module.classes
);
if hir_debug.contains("property: \"WebAssembly\"")
|| hir_debug.contains("class_name: \"WebAssembly\"")
|| hir_debug.contains("\"WebAssembly\"")
{
ctx.needs_wasm_runtime = true;
}
}

// Detect crypto.* builtin usage (randomBytes/randomUUID/sha256/md5 used
// without `import crypto`). The runtime symbols live behind the
// perry-stdlib `crypto` Cargo feature, so we need to flip that on for
Expand Down
148 changes: 147 additions & 1 deletion crates/perry/src/commands/compile/optimized_libs/no_auto.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,12 @@ use std::process::Command;

use crate::OutputFormat;

use super::super::library_search::{
android_cross_env, find_harmonyos_sdk, harmonyos_cross_env,
};
use super::super::{
find_perry_workspace_root, is_android_target, rust_target_triple, CompilationContext,
find_perry_workspace_root, is_android_target, is_windows_target, rust_target_triple,
CompilationContext,
};

/// Resolve well-known wrapper archives without rebuilding runtime/stdlib.
Expand All @@ -18,6 +22,17 @@ use super::super::{
/// archives, but when the Perry workspace source is available, build a missing
/// wrapper once in the caller's cargo target dir so fresh dev checkouts still
/// link no-auto parity cases correctly.
///
/// When the program references `WebAssembly.*` (or `--enable-wasm-runtime` was
/// passed, which folds into `ctx.needs_wasm_runtime`), the prebuilt
/// `libperry_runtime.a` is insufficient: `wasm-host` is deliberately kept out
/// of perry-runtime's `default` feature set so non-wasm programs don't pay for
/// wasmi. The no-auto path can't enable a cargo feature on an already-built
/// archive, so it does a targeted rebuild of just `perry-runtime-static` with
/// default features + `perry-runtime/wasm-host` into a dedicated target dir.
/// This is the same on-demand build pattern `build_missing_prebuilt_ext_lib`
/// uses for CPU-only ext wrappers. `perry-wasm-host` has no tokio dep, so
/// there is no #507 shared-tokio concern with the prebuilt stdlib.
pub(crate) fn resolve_no_auto_optimized_libs(
ctx: &CompilationContext,
target: Option<&str>,
Expand All @@ -32,13 +47,144 @@ pub(crate) fn resolve_no_auto_optimized_libs(
} else {
Vec::new()
};
// Issue #76 — the prebuilt runtime is built WITHOUT `wasm-host` (kept
// out of `default` to avoid wasmi bloat on non-wasm programs). When the
// program uses `WebAssembly.*`, rebuild just the runtime with the
// feature on so `js_webassembly_*` symbols are defined. The prebuilt
// stdlib is unaffected (wasm-host only adds a module to perry-runtime).
let runtime = if ctx.needs_wasm_runtime {
build_wasm_host_runtime(target, format, verbose)
} else {
None
};
OptimizedLibs {
runtime,
prefer_well_known_before_stdlib: !well_known_libs.is_empty(),
well_known_libs,
..OptimizedLibs::empty()
}
}

/// Build `perry-runtime-static` with default features + `perry-runtime/wasm-host`
/// into a dedicated target dir so the prebuilt `libperry_runtime.a` is not
/// clobbered. Returns the path to the rebuilt archive, or `None` when there's
/// no workspace source or the build fails (the caller falls back to the
/// prebuilt runtime, which will fail to link with `_js_webassembly_*`
/// undefined — the error message points the user at the cause).
fn build_wasm_host_runtime(
target: Option<&str>,
format: OutputFormat,
verbose: u8,
) -> Option<PathBuf> {
let workspace_root = find_perry_workspace_root()?;
let crate_dir = workspace_root.join("crates").join("perry-runtime-static");
if !crate_dir.is_dir() {
if matches!(format, OutputFormat::Text) && verbose > 0 {
eprintln!(
" wasm-host (no-auto): skipping runtime rebuild — crate source not found at {}",
crate_dir.display()
);
}
return None;
}

if matches!(format, OutputFormat::Text) {
println!(
" wasm-host (no-auto): rebuilding perry-runtime-static with wasm-host feature"
);
}

// Use a dedicated target dir so the prebuilt libperry_runtime.a in
// target/release is not overwritten. Cargo's incremental cache makes
// repeat builds a no-op.
let wasm_host_target_dir = workspace_root.join("target").join("perry-wasm-host-runtime");

let mut cargo_cmd = Command::new("cargo");
cargo_cmd
.current_dir(&workspace_root)
.env("CARGO_TARGET_DIR", &wasm_host_target_dir)
.arg("build")
.arg("--release")
.arg("-p")
.arg("perry-runtime-static")
.arg("--features")
.arg("perry-runtime/wasm-host");
if let Some(triple) = rust_target_triple(target) {
cargo_cmd.arg("--target").arg(triple);
}
// Cross-compile envs — mirror `build_missing_prebuilt_ext_lib` so a
// `--target harmonyos` / Android rebuild of the runtime (which has C
// deps via libmimalloc-sys) can succeed.
if matches!(target, Some("harmonyos") | Some("harmonyos-simulator")) {
match find_harmonyos_sdk() {
Some(sdk) => {
for (k, v) in harmonyos_cross_env(&sdk, target) {
cargo_cmd.env(k, v);
}
}
None => {
if matches!(format, OutputFormat::Text) && verbose > 0 {
eprintln!(
" wasm-host (no-auto): skipping runtime rebuild — OHOS SDK not found (set OHOS_SDK_HOME)"
);
}
return None;
}
}
}
if is_android_target(target) {
if let Some(ndk) = std::env::var_os("ANDROID_NDK_HOME") {
for (k, v) in
android_cross_env(std::path::Path::new(&ndk), target)
{
cargo_cmd.env(k, v);
}
}
}

match cargo_cmd.status() {
Ok(status) if status.success() => {}
Ok(status) => {
if matches!(format, OutputFormat::Text) {
eprintln!(
" wasm-host (no-auto): cargo build for perry-runtime-static failed ({status})"
);
}
return None;
}
Err(err) => {
if matches!(format, OutputFormat::Text) {
eprintln!(
" wasm-host (no-auto): failed to spawn cargo ({err})"
);
}
return None;
}
}

let lib_name = if is_windows_target(target) {
"perry_runtime.lib"
} else {
"libperry_runtime.a"
};
let mut release_dir = wasm_host_target_dir;
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);
}

if matches!(format, OutputFormat::Text) && verbose > 0 {
eprintln!(
" wasm-host (no-auto): cargo finished but {lib_name} was not produced at {}",
built.display()
);
}
None
}

/// #2532 / #3954 — resolve the `perry-ext-*` staticlibs a program needs
/// while runtime/stdlib auto-specialization is disabled.
///
Expand Down
72 changes: 72 additions & 0 deletions crates/perry/src/commands/compile/optimized_libs/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1182,3 +1182,75 @@ fn ext_zlib_covers_every_stdlib_symbol_the_flip_strips() {
them; a list that outlives its entries stops being a ratchet."
);
}

/// Regression: a program that references `WebAssembly.*` must get the
/// `perry-runtime/wasm-host` cross feature so the runtime archive carries
/// `js_webassembly_*` symbol definitions. Without it the link fails with
/// `_js_webassembly_module_new` undefined (issue #76).
#[test]
fn wasm_usage_enables_wasm_host_cross_feature() {
let workspace_root = find_perry_workspace_root().expect("workspace root");
let mut ctx = CompilationContext::new(workspace_root);
ctx.needs_wasm_runtime = true;

let features = compute_required_features(
&ctx.native_module_imports,
ctx.uses_fetch,
ctx.uses_crypto_builtins,
);
let cross = auto_optimized_cross_features(&ctx, &features, &[]);
assert!(
cross.iter().any(|f| f == "perry-runtime/wasm-host"),
"needs_wasm_runtime=true must add perry-runtime/wasm-host to the \
cross-feature set so the runtime archive defines js_webassembly_* \
symbols; got: {cross:?}"
);
}

/// Regression: a program that does NOT reference `WebAssembly.*` must NOT
/// get the `perry-runtime/wasm-host` cross feature — non-wasm programs
/// don't pay for wasmi (the feature is deliberately kept out of `default`).
#[test]
fn non_wasm_usage_does_not_enable_wasm_host_cross_feature() {
let workspace_root = find_perry_workspace_root().expect("workspace root");
let ctx = CompilationContext::new(workspace_root);

let features = compute_required_features(
&ctx.native_module_imports,
ctx.uses_fetch,
ctx.uses_crypto_builtins,
);
let cross = auto_optimized_cross_features(&ctx, &features, &[]);
assert!(
!cross.iter().any(|f| f == "perry-runtime/wasm-host"),
"needs_wasm_runtime=false must NOT add perry-runtime/wasm-host — \
non-wasm programs must not link the wasmi host; got: {cross:?}"
);
}

/// Regression: the auto-optimize cache key must differ between a wasm-using
/// program and a non-wasm program so cargo doesn't serve a cached non-wasm
/// runtime archive (missing `js_webassembly_*`) to a wasm program, or vice
/// versa (an archive carrying unresolved `perry_wasm_host_*` refs).
#[test]
fn wasm_usage_changes_auto_optimize_cache_key() {
let workspace_root = find_perry_workspace_root().expect("workspace root");
let ctx_no_wasm = CompilationContext::new(workspace_root.clone());
let mut ctx_wasm = CompilationContext::new(workspace_root);
ctx_wasm.needs_wasm_runtime = true;

let features = compute_required_features(
&ctx_no_wasm.native_module_imports,
ctx_no_wasm.uses_fetch,
ctx_no_wasm.uses_crypto_builtins,
);
let feature_arg = features_to_cargo_arg(&features);
let key_no_wasm =
auto_optimized_cache_key(&feature_arg, true, false, None, &ctx_no_wasm);
let key_wasm =
auto_optimized_cache_key(&feature_arg, true, false, None, &ctx_wasm);
assert_ne!(
key_no_wasm, key_wasm,
"wasm usage must change the cache key so the target dirs don't collide"
);
}
14 changes: 13 additions & 1 deletion crates/perry/src/commands/compile/run_pipeline.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5285,7 +5285,19 @@ pub fn run_with_parse_cache(
// both the symbol-stub scan below and the final link resolve it. Cargo's
// freshness check makes this a no-op when it is already current; programs
// that do not use wasm skip the check entirely.
let use_wasm_host = ctx.needs_wasm_runtime || args.enable_wasm_runtime;
//
// `--enable-wasm-runtime` is an explicit override: fold it into
// `ctx.needs_wasm_runtime` so EVERY downstream path (the `wasm-host`
// cargo feature in `auto_optimized_cross_features`, the no-auto runtime
// rebuild, the library link, and the symbol-stub scan) treats it the
// same as auto-detected `WebAssembly.*` usage. Without this fold the
// flag only linked `libperry_wasm_host.a` but never enabled the
// `perry-runtime/wasm-host` cargo feature, so `js_webassembly_*`
// symbols stayed undefined in the runtime archive.
if args.enable_wasm_runtime {
ctx.needs_wasm_runtime = true;
}
let use_wasm_host = ctx.needs_wasm_runtime;
let wasm_host_lib_resolved = if use_wasm_host {
// Prefer a Cargo freshness check when workspace source is available.
// Merely finding an archive is insufficient after the host ABI grows:
Expand Down
Loading