Skip to content

Commit 1e6c1de

Browse files
authored
feat: add --panic-unwind flag to build and test commands (#1572)
Adds a new --panic-unwind option to `wasm-pack build` and `wasm-pack test` that builds with `-Cpanic=unwind` instead of the default `panic=abort`. When the flag is set, cargo is invoked via `+nightly` with `-Z build-std=std,panic_unwind` and `RUSTFLAGS` is augmented with `-Cpanic=unwind` (preserving any user-provided `RUSTFLAGS`). The nightly toolchain, `rust-src` component, and nightly `wasm32-unknown-unknown` target are auto-installed via `rustup` if not already present. The stable rustc and wasm-target preflight checks are skipped when --panic-unwind is set, since they are irrelevant for the nightly invocation. This is a build-tool-level enabler: it produces a wasm artifact compiled with unwinding semantics. The actual conversion of caught panics into JavaScript exceptions is the responsibility of the consuming bindings layer (e.g. wasm-bindgen's catch-unwind support).
1 parent 88de54d commit 1e6c1de

6 files changed

Lines changed: 277 additions & 4 deletions

File tree

docs/src/commands/build.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,45 @@ example, to build the previous example using cargo's offline feature:
138138
wasm-pack build examples/js-hello-world --mode no-install -- --offline
139139
```
140140

141+
## Panic strategy
142+
143+
By default, Rust panics in WebAssembly compile with `panic=abort`, which aborts
144+
the WebAssembly instance on panic. The `--panic-unwind` flag changes this so
145+
panics can be caught at FFI boundaries and converted to JavaScript exceptions
146+
by tools like [`wasm-bindgen`'s catch-unwind support][wbg-catch-unwind].
147+
148+
```
149+
wasm-pack build --panic-unwind
150+
```
151+
152+
This flag:
153+
154+
- Invokes `cargo` with the **nightly** toolchain (`cargo +nightly build`).
155+
- Adds `-Z build-std=std,panic_unwind` to rebuild `std` with unwinding
156+
support.
157+
- Sets `RUSTFLAGS=-Cpanic=unwind` (preserving any user-provided `RUSTFLAGS`).
158+
159+
The first time you use `--panic-unwind`, `wasm-pack` will install any missing
160+
prerequisites via `rustup`:
161+
162+
- The nightly toolchain
163+
- The `rust-src` component for nightly
164+
- The `wasm32-unknown-unknown` target for nightly
165+
166+
If you are not using `rustup` you must install these prerequisites manually.
167+
See [Non-`rustup` setups][non-rustup].
168+
169+
> **Note:** `wasm-pack` only handles producing the `.wasm`. The actual
170+
> "panic = recoverable JavaScript exception" behaviour requires runtime glue
171+
> from your bindings layer (e.g. `wasm-bindgen`'s catch-unwind feature). With
172+
> just `--panic-unwind` and no runtime glue, panics still terminate the
173+
> instance — they are merely *unwound* rather than *aborted*.
174+
175+
`--panic-unwind` is also available for [`wasm-pack test`](./test.md).
176+
177+
[wbg-catch-unwind]: https://wasm-bindgen.github.io/wasm-bindgen/reference/catch-unwind.html
178+
[non-rustup]: ../prerequisites/non-rustup-setups.md
179+
141180
<hr style="font-size: 1.5em; margin-top: 2.5em"/>
142181

143182
<sup id="footnote-0">0</sup> If you need to include additional assets in the pkg

docs/src/commands/test.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,16 @@ Choose where to run your tests by passing in any combination of testing environm
3939
wasm-pack test --node --firefox --chrome --safari --headless
4040
```
4141

42+
## Panic strategy
43+
44+
The `test` command accepts the `--panic-unwind` flag, which builds the test
45+
binary with `panic=unwind` via the nightly toolchain and `-Z build-std`. See
46+
the [`build` command's documentation](./build.md#panic-strategy) for details.
47+
48+
```
49+
wasm-pack test --node --panic-unwind
50+
```
51+
4252
## Extra options
4353

4454
The `test` command can pass extra options straight to `cargo test` even if they are not

src/build/mod.rs

Lines changed: 48 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -80,12 +80,22 @@ pub fn cargo_build_wasm(
8080
profile: BuildProfile,
8181
extra_options: &[String],
8282
target_triple: &str,
83+
panic_unwind: bool,
8384
) -> Result<String> {
84-
let msg = format!("{}Compiling to Wasm...", emoji::CYCLONE);
85+
let msg = if panic_unwind {
86+
format!("{}Compiling to Wasm (with panic=unwind)...", emoji::CYCLONE)
87+
} else {
88+
format!("{}Compiling to Wasm...", emoji::CYCLONE)
89+
};
8590
PBAR.info(&msg);
8691

8792
let mut cmd = Command::new("cargo");
88-
cmd.current_dir(path).arg("build").arg("--lib");
93+
cmd.current_dir(path);
94+
// `+nightly` must be the first argument to cargo.
95+
if panic_unwind {
96+
cmd.arg("+nightly");
97+
}
98+
cmd.arg("build").arg("--lib");
8999

90100
if PBAR.quiet() {
91101
cmd.arg("--quiet");
@@ -114,6 +124,19 @@ pub fn cargo_build_wasm(
114124

115125
cmd.env("CARGO_BUILD_TARGET", target_triple);
116126

127+
if panic_unwind {
128+
cmd.arg("-Z").arg("build-std=std,panic_unwind");
129+
130+
// Append `-Cpanic=unwind` to any user-provided RUSTFLAGS.
131+
let existing = std::env::var("RUSTFLAGS").unwrap_or_default();
132+
let combined = if existing.is_empty() {
133+
"-Cpanic=unwind".to_string()
134+
} else {
135+
format!("{existing} -Cpanic=unwind")
136+
};
137+
cmd.env("RUSTFLAGS", combined);
138+
}
139+
117140
// The `cargo` command is executed inside the directory at `path`, so relative paths set via extra options won't work.
118141
// To remedy the situation, all detected paths are converted to absolute paths.
119142
let mut handle_path = false;
@@ -191,15 +214,24 @@ pub fn cargo_build_wasm(
191214
/// * `path`: Path to the crate directory to build tests.
192215
/// * `debug`: Whether to build tests in `debug` mode.
193216
/// * `extra_options`: Additional parameters to pass to `cargo` when building tests.
217+
/// * `target_triple`: The wasm target triple to build for (e.g.
218+
/// `wasm32-unknown-unknown` or `wasm64-unknown-unknown`).
219+
/// * `panic_unwind`: Whether to build tests with `panic=unwind` via the nightly
220+
/// toolchain and `-Z build-std`.
194221
pub fn cargo_build_wasm_tests(
195222
path: &Path,
196223
debug: bool,
197224
extra_options: &[String],
198225
target_triple: &str,
226+
panic_unwind: bool,
199227
) -> Result<()> {
200228
let mut cmd = Command::new("cargo");
201-
202-
cmd.current_dir(path).arg("build").arg("--tests");
229+
cmd.current_dir(path);
230+
// `+nightly` must be the first argument to cargo.
231+
if panic_unwind {
232+
cmd.arg("+nightly");
233+
}
234+
cmd.arg("build").arg("--tests");
203235

204236
if PBAR.quiet() {
205237
cmd.arg("--quiet");
@@ -211,6 +243,18 @@ pub fn cargo_build_wasm_tests(
211243

212244
cmd.env("CARGO_BUILD_TARGET", target_triple);
213245

246+
if panic_unwind {
247+
cmd.arg("-Z").arg("build-std=std,panic_unwind");
248+
249+
let existing = std::env::var("RUSTFLAGS").unwrap_or_default();
250+
let combined = if existing.is_empty() {
251+
"-Cpanic=unwind".to_string()
252+
} else {
253+
format!("{existing} -Cpanic=unwind")
254+
};
255+
cmd.env("RUSTFLAGS", combined);
256+
}
257+
214258
cmd.args(extra_options);
215259

216260
child::run(cmd, "cargo build").context("Compilation of your program failed")?;

src/build/wasm_target.rs

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,3 +160,136 @@ fn rustup_add_wasm_target(target: &str) -> Result<()> {
160160

161161
Ok(())
162162
}
163+
164+
const NIGHTLY_TOOLCHAIN: &str = "nightly";
165+
166+
/// Ensure that the nightly toolchain is installed and has the `rust-src`
167+
/// component and `wasm32-unknown-unknown` target, all of which are required
168+
/// for `-Z build-std` (used by `--panic-unwind`). Missing components are
169+
/// installed automatically via `rustup`.
170+
pub fn check_nightly_prerequisites() -> Result<()> {
171+
let msg = format!(
172+
"{}Checking nightly toolchain prerequisites for panic=unwind...",
173+
emoji::TARGET
174+
);
175+
PBAR.info(&msg);
176+
177+
let nightly_sysroot = get_nightly_sysroot()?;
178+
if !nightly_sysroot.exists() {
179+
install_nightly_toolchain()?;
180+
}
181+
182+
if !has_rust_src_component()? {
183+
install_rust_src_component()?;
184+
}
185+
186+
if !does_nightly_wasm32_target_exist() {
187+
rustup_add_wasm_target_nightly()?;
188+
}
189+
190+
Ok(())
191+
}
192+
193+
fn get_nightly_sysroot() -> Result<PathBuf> {
194+
let command = Command::new("rustc")
195+
.args(["+nightly", "--print", "sysroot"])
196+
.output()?;
197+
198+
if command.status.success() {
199+
Ok(String::from_utf8(command.stdout)?.trim().into())
200+
} else {
201+
Err(anyhow!(
202+
"Getting nightly rustc's sysroot wasn't successful. Got {}",
203+
command.status
204+
))
205+
}
206+
}
207+
208+
fn install_nightly_toolchain() -> Result<()> {
209+
let msg = format!(
210+
"{}Installing nightly toolchain via rustup...",
211+
emoji::TARGET
212+
);
213+
PBAR.info(&msg);
214+
215+
let mut cmd = Command::new("rustup");
216+
cmd.arg("toolchain").arg("install").arg(NIGHTLY_TOOLCHAIN);
217+
child::run(cmd, "rustup").context("Installing the nightly toolchain with rustup")?;
218+
219+
Ok(())
220+
}
221+
222+
fn has_rust_src_component() -> Result<bool> {
223+
let command = Command::new("rustup")
224+
.args(["component", "list", "--toolchain", NIGHTLY_TOOLCHAIN])
225+
.output()?;
226+
227+
if !command.status.success() {
228+
return Ok(false);
229+
}
230+
231+
let stdout = String::from_utf8(command.stdout)?;
232+
Ok(stdout
233+
.lines()
234+
.any(|line| line.starts_with("rust-src") && line.contains("(installed)")))
235+
}
236+
237+
fn install_rust_src_component() -> Result<()> {
238+
let msg = format!(
239+
"{}Installing rust-src component for nightly toolchain...",
240+
emoji::TARGET
241+
);
242+
PBAR.info(&msg);
243+
244+
let mut cmd = Command::new("rustup");
245+
cmd.arg("component")
246+
.arg("add")
247+
.arg("rust-src")
248+
.arg("--toolchain")
249+
.arg(NIGHTLY_TOOLCHAIN);
250+
child::run(cmd, "rustup").context("Adding the rust-src component with rustup")?;
251+
252+
Ok(())
253+
}
254+
255+
fn does_nightly_wasm32_target_exist() -> bool {
256+
let command = Command::new("rustc")
257+
.args([
258+
"+nightly",
259+
"--target",
260+
"wasm32-unknown-unknown",
261+
"--print",
262+
"target-libdir",
263+
])
264+
.output();
265+
266+
match command {
267+
Ok(output) if output.status.success() => {
268+
let path: PathBuf = String::from_utf8(output.stdout)
269+
.ok()
270+
.map(|s| s.trim().into())
271+
.unwrap_or_default();
272+
path.exists()
273+
}
274+
_ => false,
275+
}
276+
}
277+
278+
fn rustup_add_wasm_target_nightly() -> Result<()> {
279+
let msg = format!(
280+
"{}Adding wasm32-unknown-unknown target for nightly toolchain...",
281+
emoji::TARGET
282+
);
283+
PBAR.info(&msg);
284+
285+
let mut cmd = Command::new("rustup");
286+
cmd.arg("target")
287+
.arg("add")
288+
.arg("wasm32-unknown-unknown")
289+
.arg("--toolchain")
290+
.arg(NIGHTLY_TOOLCHAIN);
291+
child::run(cmd, "rustup")
292+
.context("Adding the wasm32-unknown-unknown target for nightly with rustup")?;
293+
294+
Ok(())
295+
}

src/command/build.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ pub struct Build {
4141
pub bindgen: Option<install::Status>,
4242
pub cache: Cache,
4343
pub extra_options: Vec<String>,
44+
pub panic_unwind: bool,
4445
target_triple: String,
4546
wasm_path: Option<String>,
4647
}
@@ -184,6 +185,15 @@ pub struct BuildOptions {
184185
/// Option to skip optimization with wasm-opt
185186
pub no_opt: bool,
186187

188+
#[clap(long = "panic-unwind")]
189+
/// Build with panic=unwind. Requires the nightly Rust toolchain; uses
190+
/// `-Z build-std` to rebuild `std` with `-Cpanic=unwind` so panics can be
191+
/// caught at FFI boundaries instead of aborting the WebAssembly instance.
192+
/// The nightly toolchain, `rust-src` component, and nightly
193+
/// `wasm32-unknown-unknown` target will be installed via `rustup` if not
194+
/// already present.
195+
pub panic_unwind: bool,
196+
187197
/// List of extra options to pass to `cargo build`
188198
pub extra_options: Vec<String>,
189199
}
@@ -207,6 +217,7 @@ impl Default for BuildOptions {
207217
profile: None,
208218
out_dir: String::new(),
209219
out_name: None,
220+
panic_unwind: false,
210221
extra_options: Vec::new(),
211222
}
212223
}
@@ -278,6 +289,7 @@ impl Build {
278289
cache: cache::get_wasm_pack_cache()?,
279290
target_triple: target_triple.to_owned(),
280291
extra_options,
292+
panic_unwind: build_opts.panic_unwind,
281293
wasm_path: None,
282294
})
283295
}
@@ -364,6 +376,12 @@ impl Build {
364376
}
365377

366378
fn step_check_rustc_version(&mut self) -> Result<()> {
379+
// The stable rustc version is irrelevant when --panic-unwind is set,
380+
// since cargo will be invoked via `+nightly`.
381+
if self.panic_unwind {
382+
info!("Skipping rustc version check (using nightly via --panic-unwind).");
383+
return Ok(());
384+
}
367385
info!("Checking rustc version...");
368386
let version = build::check_rustc_version()?;
369387
let msg = format!("rustc version is {}.", version);
@@ -379,6 +397,12 @@ impl Build {
379397
}
380398

381399
fn step_check_for_wasm_target(&mut self) -> Result<()> {
400+
if self.panic_unwind {
401+
info!("Checking nightly toolchain prerequisites for panic=unwind...");
402+
build::wasm_target::check_nightly_prerequisites()?;
403+
info!("Nightly prerequisites check was successful.");
404+
return Ok(());
405+
}
382406
info!("Checking for wasm-target...");
383407
build::wasm_target::check_for_wasm_target(&self.target_triple)?;
384408
info!("Checking for wasm-target was successful.");
@@ -392,6 +416,7 @@ impl Build {
392416
self.profile.clone(),
393417
&self.extra_options,
394418
&self.target_triple,
419+
self.panic_unwind,
395420
)?;
396421
info!("wasm built at {wasm_path:#?}.");
397422
self.wasm_path = Some(wasm_path);

0 commit comments

Comments
 (0)