Skip to content

Commit 4d2253c

Browse files
committed
review: address pre-merge feedback on emscripten target
Working through reviewer-flagged items before landing: * Windows: `emcc` is invoked via the full path resolved by `which::which("emcc")` instead of the bare string "emcc". Stashed on the Build struct in `step_check_for_emcc` and threaded through `emcc_link` and `emcc_post_link`. The Rust process spawner doesn't honour PATHEXT for bare names, so `Command::new("emcc")` couldn't find `emcc.bat` on Windows; using the full path works since Rust 1.77's CVE-2024-24576 fix added explicit .bat handling for full paths. * tests: `prepend_path` (the `EMSCRIPTEN_BIN` companion helper, kept as a future debugging hook) now uses `std::env::join_paths` instead of a hardcoded `:` separator. Was a trap for the same Windows port. * tests: `make_node_driver` JSON-encodes the .mjs URL when interpolating it into the JS source, so Windows-style paths with backslashes can't break out of the JS string literal. * template: bumped `wasm-bindgen = "0.2"` to `"0.2.122"` so new projects scaffolded via `wasm-pack new --emscripten` pick up the emscripten output-mode fixes (wasm-bindgen#5156). Older versions produce broken JS/wasm under the emscripten target. * docs: corrected stale comment that referenced an "EmscriptenRuntime" interface decoration. The decoration was deliberately scaled back to not claim runtime members (`HEAP*`, `FS`, `ccall`/`cwrap`) on the factory return; the comment now reflects what the code actually does. * style: collapsed `Target::Bundler`, `Web`, and `Deno` post-link settings into a single match arm with an explanatory comment. They were three identical struct literals.
1 parent 876d396 commit 4d2253c

3 files changed

Lines changed: 65 additions & 23 deletions

File tree

src/command/build.rs

Lines changed: 49 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,13 @@ pub struct Build {
4545
pub panic_unwind: bool,
4646
target_triple: String,
4747
wasm_path: Option<String>,
48+
/// Path to the `emcc` binary as resolved by `step_check_for_emcc`,
49+
/// reused by the subsequent link/post-link steps. Resolved via
50+
/// `which::which("emcc")` so that on Windows the `PATHEXT` lookup
51+
/// finds `emcc.bat` — `Command::new("emcc")` would not, because
52+
/// Rust's process spawner doesn't honour `PATHEXT`. Always `Some`
53+
/// in the emscripten pipeline by the time the link step runs.
54+
emcc_path: Option<PathBuf>,
4855
}
4956

5057
/// What sort of output we're going to be generating and flags we're invoking
@@ -304,6 +311,7 @@ impl Build {
304311
extra_options,
305312
panic_unwind: build_opts.panic_unwind,
306313
wasm_path: None,
314+
emcc_path: None,
307315
})
308316
}
309317

@@ -633,6 +641,12 @@ impl Build {
633641
// older than 3.1.60 will likely fail at build time, so we warn
634642
// up front for clearer diagnostics.
635643
warn_if_emcc_too_old(&emcc_path);
644+
645+
// Stash the resolved path for the subsequent link / post-link
646+
// steps. On Windows, emcc is `emcc.bat`; `which::which` honours
647+
// `PATHEXT` but `Command::new("emcc")` would not, so we must
648+
// invoke via the full resolved path.
649+
self.emcc_path = Some(emcc_path);
636650
Ok(())
637651
}
638652

@@ -665,8 +679,12 @@ impl Build {
665679
.as_ref()
666680
.context("step_emcc_link called before cargo build set wasm_path")?
667681
.clone();
682+
let emcc = self
683+
.emcc_path
684+
.as_ref()
685+
.context("step_emcc_link called before step_check_for_emcc set emcc_path")?;
668686
let out_wasm = self.intermediate_wasm_path();
669-
emcc_link(&lib_path, &out_wasm)?;
687+
emcc_link(emcc, &lib_path, &out_wasm)?;
670688
// step_run_wasm_bindgen reads `wasm_path` as its input — point it at
671689
// the freshly-linked wasm so wasm-bindgen processes the right file.
672690
self.wasm_path = Some(out_wasm.to_string_lossy().into_owned());
@@ -725,7 +743,12 @@ impl Build {
725743
// we linked arbitrary C/C++ exports, which the wasm-pack flow
726744
// doesn't support today. Avoiding it also sidesteps the emcc
727745
// assertion on wasm-bindgen-style multi-value-return exports.
746+
let emcc = self
747+
.emcc_path
748+
.as_ref()
749+
.context("step_emcc_post_link called before step_check_for_emcc set emcc_path")?;
728750
emcc_post_link(
751+
emcc,
729752
&in_wasm,
730753
&in_library,
731754
extern_pre.as_deref(),
@@ -736,10 +759,14 @@ impl Build {
736759
info!("emcc --post-link produced {out_js:?}.");
737760

738761
// Decorate wasm-bindgen's bare .d.ts with the emscripten-shaped
739-
// factory: an `EmscriptenRuntime` interface for the runtime members
740-
// we surface, a `MainModule` intersection type, and a default-export
741-
// factory declaration. After this `import M from "./<name>.mjs"`
742-
// type-checks against the produced module.
762+
// factory: a `MainModule` alias for the wasm-bindgen surface and a
763+
// default-export factory declaration. After this
764+
// `import M from "./<name>.mjs"` type-checks against the produced
765+
// module. We deliberately don't claim emscripten runtime members
766+
// (`HEAP*`, `FS`, `ccall`/`cwrap`) on the factory return — they
767+
// exist on the underlying module but aren't part of the typed API
768+
// wasm-pack ships; users who need them can extend the `.d.ts` in
769+
// pkg/.
743770
if !self.disable_dts && bindgen_dts.exists() {
744771
decorate_bindgen_dts_for_emscripten(&bindgen_dts)?;
745772
}
@@ -968,7 +995,11 @@ fn emcc_missing_install_message() -> String {
968995
/// generate any JS runtime. `-sEXPORTED_FUNCTIONS=` is populated by
969996
/// scanning the staticlib for the symbols wasm-bindgen needs so they
970997
/// survive dead-stripping.
971-
fn emcc_link(lib_path: &str, out_wasm: &PathBuf) -> Result<()> {
998+
///
999+
/// `emcc` is the resolved emcc binary path (via `which::which`), not the
1000+
/// bare string `"emcc"` — `Command::new("emcc")` doesn't honour `PATHEXT`
1001+
/// on Windows where emcc ships as `emcc.bat`.
1002+
fn emcc_link(emcc: &Path, lib_path: &str, out_wasm: &PathBuf) -> Result<()> {
9721003
if let Some(parent) = out_wasm.parent() {
9731004
std::fs::create_dir_all(parent)
9741005
.with_context(|| format!("creating intermediate dir {parent:?}"))?;
@@ -977,7 +1008,7 @@ fn emcc_link(lib_path: &str, out_wasm: &PathBuf) -> Result<()> {
9771008
let exports = collect_wasm_bindgen_exports(lib_path)?;
9781009
let exports_joined = exports.join(",");
9791010

980-
let mut cmd = Command::new("emcc");
1011+
let mut cmd = Command::new(emcc);
9811012
cmd.arg(lib_path)
9821013
.arg("--no-entry")
9831014
.arg("--oformat=bare")
@@ -1064,7 +1095,12 @@ fn emcc_post_link_settings_for(target: Target) -> Result<EmccPostLinkSettings> {
10641095
// Node — useful for tests, CLIs, and SSR. The `nodejs` target stays
10651096
// node-only since that's its explicit intent.
10661097
Ok(match target {
1067-
Target::Bundler | Target::Web => EmccPostLinkSettings {
1098+
// `Bundler`, `Web`, and `Deno` collapse to the same emcc settings
1099+
// — `web,node` ENVIRONMENT, no source-phase imports, `.mjs` —
1100+
// because the differentiation between them happens earlier
1101+
// (wasm-bindgen produces target-specific JS glue) and emcc's
1102+
// post-link sees an already-finished module.
1103+
Target::Bundler | Target::Web | Target::Deno => EmccPostLinkSettings {
10681104
environment: "web,node",
10691105
source_phase_imports: false,
10701106
extension: "mjs",
@@ -1079,11 +1115,6 @@ fn emcc_post_link_settings_for(target: Target) -> Result<EmccPostLinkSettings> {
10791115
source_phase_imports: false,
10801116
extension: "mjs",
10811117
},
1082-
Target::Deno => EmccPostLinkSettings {
1083-
environment: "web,node",
1084-
source_phase_imports: false,
1085-
extension: "mjs",
1086-
},
10871118
Target::NoModules => bail!(
10881119
"`--target no-modules` is not supported for wasm32-unknown-emscripten builds. \
10891120
The emscripten toolchain produces module-shaped output only. \
@@ -1127,15 +1158,19 @@ fn emcc_opt_level_for(profile: &BuildProfile) -> &'static str {
11271158
/// where ESM `import` statements must live. wasm-bindgen routes module
11281159
/// imports there because they can't legally appear inside emcc's
11291160
/// `--js-library` content (that gets evaluated inside the wrapper).
1161+
///
1162+
/// `emcc` is the resolved emcc binary path (via `which::which`); see
1163+
/// `emcc_link` for the Windows-`PATHEXT` rationale.
11301164
fn emcc_post_link(
1165+
emcc: &Path,
11311166
in_wasm: &Path,
11321167
in_library: &Path,
11331168
extern_pre: Option<&Path>,
11341169
out_js: &Path,
11351170
settings: &EmccPostLinkSettings,
11361171
opt_level: &str,
11371172
) -> Result<()> {
1138-
let mut cmd = Command::new("emcc");
1173+
let mut cmd = Command::new(emcc);
11391174
cmd.arg("--post-link")
11401175
.arg(in_wasm)
11411176
.arg("--js-library")

tests/all/emscripten.rs

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -54,15 +54,15 @@ fn patched_emcc_bin() -> Option<std::path::PathBuf> {
5454

5555
/// Prepend `dir` to `$PATH` on `cmd`. Companion to `patched_emcc_bin` for
5656
/// injecting an alternative emcc directory ahead of the inherited PATH.
57+
/// Uses `std::env::join_paths` so the platform-correct separator (`;` on
58+
/// Windows, `:` elsewhere) is applied.
5759
#[allow(dead_code)]
5860
fn prepend_path(cmd: &mut Command, dir: &Path) {
5961
let existing = std::env::var_os("PATH").unwrap_or_default();
60-
let mut new_path = std::ffi::OsString::from(dir);
61-
if !existing.is_empty() {
62-
new_path.push(":");
63-
new_path.push(existing);
64-
}
65-
cmd.env("PATH", new_path);
62+
let mut entries = vec![dir.to_path_buf()];
63+
entries.extend(std::env::split_paths(&existing));
64+
let joined = std::env::join_paths(entries).expect("PATH entries should be valid");
65+
cmd.env("PATH", joined);
6666
}
6767

6868
/// Skip the calling test (with an explanatory message) if emcc isn't
@@ -87,10 +87,14 @@ macro_rules! skip_without_emcc {
8787
/// one node invocation so we catch interactions between different codegen
8888
/// paths (e.g. heap reallocation invalidating cached views).
8989
fn make_node_driver(mjs_path: &Path) -> String {
90+
// JSON-encode the path so backslashes (Windows) and unusual characters
91+
// can't break out of the JS string literal.
92+
let mjs_json = serde_json::to_string(&format!("file://{}", mjs_path.display()))
93+
.expect("path should serialise");
9094
format!(
9195
r#"
9296
globalThis.rs_test_doubler = (n) => n * 2;
93-
const {{ default: M }} = await import('file://{mjs}');
97+
const {{ default: M }} = await import({mjs});
9498
const m = await M();
9599
96100
function expect(name, got, want) {{
@@ -148,7 +152,7 @@ fn make_node_driver(mjs_path: &Path) -> String {
148152
const calc = new m.app.math.Calc(21);
149153
expect('Calc.double', calc.double(), 42);
150154
"#,
151-
mjs = mjs_path.display(),
155+
mjs = mjs_json,
152156
)
153157
}
154158

wasm-pack-emscripten-template/Cargo.toml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,10 @@ edition = "2021"
1010
crate-type = ["staticlib"]
1111

1212
[dependencies]
13-
wasm-bindgen = "0.2"
13+
# 0.2.122 is the floor that ships the emscripten output-mode fixes
14+
# (wasm-bindgen#5156); older releases will produce broken JS/wasm under
15+
# the emscripten target.
16+
wasm-bindgen = "0.2.122"
1417

1518
[profile.release]
1619
# Tell `rustc` to optimize for small code size; emcc will further optimize at

0 commit comments

Comments
 (0)