Add support for automatically executing `wasm-opt`

This commit adds support for automatically executing the `wasm-opt`
binary from the [Binaryen project][binaryen]. By default `wasm-pack`
will now, in release and profiling modes, execute `wasm-opt -O` over the
final binary. The goal here is to enable optimizations that further
reduce binary size or improve runtime. In the long run it's expected
that `wasm-opt`'s optimizations may mostly make their way into LLVM, but
it's empirically true today that `wasm-opt` plus LLVM is the best
combination for size and speed today.

A configuration section for `wasm-opt` has been added as [previously
proposed][fitzgen], namely:

```toml
[package.metadata.wasm-pack.profile.release]
wasm-opt = ['-Os']
```

The `wasm-opt` binary is downloaded from Binaryen's [own
releases](https://github.com/webassembly/binaryen/releases). They're
available for the same platforms that we download predownloaded binaries
for `wasm-bindgen` on. We'll also opportunistically use `wasm-opt` in
`PATH` if it's available. If we're untable to run `wasm-opt`, though, a
warning diagnostic is printed informing such.

Closes #159

[binaryen]: https://github.com/webassembly/binaryen
[fitzgen]: https://github.com/rustwasm/wasm-pack/issues/159#issuecomment-454888890
master
Alex Crichton 6 years ago
parent 7dd889b4be
commit 9b74e43d2d
  1. 17
      docs/src/cargo-toml-configuration.md
  2. 10
      src/child.rs
  3. 43
      src/command/build.rs
  4. 1
      src/lib.rs
  5. 31
      src/manifest/mod.rs
  6. 108
      src/wasm_opt.rs
  7. 1
      tests/all/main.rs
  8. 13
      tests/all/utils/fixture.rs
  9. 189
      tests/all/wasm_opt.rs

@ -9,6 +9,17 @@ the `--dev`, `--profiling`, and `--release` flags passed to `wasm-pack build`.
The available configuration options and their default values are shown below:
```toml
[package.metadata.wasm-pack.profile.dev]
# Should `wasm-opt` be used to further optimize the wasm binary generated after
# the Rust compiler has finished? Using `wasm-opt` can often further decrease
# binary size or do clever tricks that haven't made their way into LLVM yet.
#
# Configuration can be set to `false` if you want to disable it (as is the
# default for the dev profile), or it can be an array of strings which are
# explicit arguments to pass to `wasm-opt`. For example `['-Os']` would optimize
# for size while `['-O4']` would execute very expensive optimizations passes
wasm-opt = false
[package.metadata.wasm-pack.profile.dev.wasm-bindgen]
# Should we enable wasm-bindgen's debug assertions in its generated JS glue?
debug-js-glue = true
@ -17,11 +28,17 @@ demangle-name-section = true
# Should we emit the DWARF debug info custom sections?
dwarf-debug-info = false
[package.metadata.wasm-pack.profile.profiling]
wasm-opt = ['-O']
[package.metadata.wasm-pack.profile.profiling.wasm-bindgen]
debug-js-glue = false
demangle-name-section = true
dwarf-debug-info = false
[package.metadata.wasm-pack.profile.release]
wasm-opt = ['-O']
[package.metadata.wasm-pack.profile.release.wasm-bindgen]
debug-js-glue = false
demangle-name-section = true

@ -33,9 +33,10 @@ pub fn run(mut command: Command, command_name: &str) -> Result<(), Error> {
Ok(())
} else {
bail!(
"failed to execute `{}`: exited with {}",
"failed to execute `{}`: exited with {}\n full command: {:?}",
command_name,
status
status,
command,
)
}
}
@ -53,9 +54,10 @@ pub fn run_capture_stdout(mut command: Command, command_name: &str) -> Result<St
Ok(String::from_utf8_lossy(&output.stdout).into_owned())
} else {
bail!(
"failed to execute `{}`: exited with {}",
"failed to execute `{}`: exited with {}\n full command: {:?}",
command_name,
output.status
output.status,
command,
)
}
}

@ -1,5 +1,6 @@
//! Implementation of the `wasm-pack build` command.
use crate::wasm_opt;
use binary_install::{Cache, Download};
use bindgen;
use build;
@ -47,6 +48,16 @@ pub enum BuildMode {
Force,
}
impl BuildMode {
fn install_permitted(&self) -> bool {
match self {
BuildMode::Normal => true,
BuildMode::Force => true,
BuildMode::Noinstall => false,
}
}
}
impl Default for BuildMode {
fn default() -> BuildMode {
BuildMode::Normal
@ -284,6 +295,7 @@ impl Build {
step_copy_license,
step_install_wasm_bindgen,
step_run_wasm_bindgen,
step_run_wasm_opt,
step_create_json,
]);
steps
@ -366,13 +378,11 @@ impl Build {
let lockfile = Lockfile::new(&self.crate_data)?;
let bindgen_version = lockfile.require_wasm_bindgen()?;
info!("Installing wasm-bindgen-cli...");
let install_permitted = match self.mode {
BuildMode::Normal => true,
BuildMode::Force => true,
BuildMode::Noinstall => false,
};
let bindgen =
bindgen::install_wasm_bindgen(&self.cache, &bindgen_version, install_permitted)?;
let bindgen = bindgen::install_wasm_bindgen(
&self.cache,
&bindgen_version,
self.mode.install_permitted(),
)?;
self.bindgen = Some(bindgen);
info!("Installing wasm-bindgen-cli was successful.");
Ok(())
@ -392,4 +402,23 @@ impl Build {
info!("wasm bindings were built at {:#?}.", &self.out_dir);
Ok(())
}
fn step_run_wasm_opt(&mut self) -> Result<(), Error> {
let args = match self
.crate_data
.configured_profile(self.profile)
.wasm_opt_args()
{
Some(args) => args,
None => return Ok(()),
};
info!("executing wasm-opt with {:?}", args);
wasm_opt::run(
&self.cache,
&self.out_dir,
&args,
self.mode.install_permitted(),
)?;
Ok(())
}
}

@ -39,6 +39,7 @@ pub mod progressbar;
pub mod readme;
pub mod target;
pub mod test;
pub mod wasm_opt;
use progressbar::ProgressOutput;

@ -104,6 +104,8 @@ impl Default for CargoWasmPackProfiles {
pub struct CargoWasmPackProfile {
#[serde(default, rename = "wasm-bindgen")]
wasm_bindgen: CargoWasmPackProfileWasmBindgen,
#[serde(default, rename = "wasm-opt")]
wasm_opt: Option<CargoWasmPackProfileWasmOpt>,
}
#[derive(Default, Deserialize)]
@ -233,6 +235,19 @@ impl Crate {
}
}
#[derive(Clone, Deserialize)]
#[serde(untagged)]
enum CargoWasmPackProfileWasmOpt {
Enabled(bool),
ExplicitArgs(Vec<String>),
}
impl Default for CargoWasmPackProfileWasmOpt {
fn default() -> Self {
CargoWasmPackProfileWasmOpt::Enabled(false)
}
}
impl CargoWasmPackProfile {
fn default_dev() -> Self {
CargoWasmPackProfile {
@ -241,6 +256,7 @@ impl CargoWasmPackProfile {
demangle_name_section: Some(true),
dwarf_debug_info: Some(false),
},
wasm_opt: None,
}
}
@ -251,6 +267,7 @@ impl CargoWasmPackProfile {
demangle_name_section: Some(true),
dwarf_debug_info: Some(false),
},
wasm_opt: Some(CargoWasmPackProfileWasmOpt::Enabled(true)),
}
}
@ -261,6 +278,7 @@ impl CargoWasmPackProfile {
demangle_name_section: Some(true),
dwarf_debug_info: Some(false),
},
wasm_opt: Some(CargoWasmPackProfileWasmOpt::Enabled(true)),
}
}
@ -300,6 +318,10 @@ impl CargoWasmPackProfile {
d!(wasm_bindgen.debug_js_glue);
d!(wasm_bindgen.demangle_name_section);
d!(wasm_bindgen.dwarf_debug_info);
if self.wasm_opt.is_none() {
self.wasm_opt = defaults.wasm_opt.clone();
}
}
/// Get this profile's configured `[wasm-bindgen.debug-js-glue]` value.
@ -316,6 +338,15 @@ impl CargoWasmPackProfile {
pub fn wasm_bindgen_dwarf_debug_info(&self) -> bool {
self.wasm_bindgen.dwarf_debug_info.unwrap()
}
/// Get this profile's configured arguments for `wasm-opt`, if enabled.
pub fn wasm_opt_args(&self) -> Option<Vec<String>> {
match self.wasm_opt.as_ref()? {
CargoWasmPackProfileWasmOpt::Enabled(false) => None,
CargoWasmPackProfileWasmOpt::Enabled(true) => Some(vec!["-O".to_string()]),
CargoWasmPackProfileWasmOpt::ExplicitArgs(s) => Some(s.clone()),
}
}
}
struct NpmData {

@ -0,0 +1,108 @@
//! Support for downloading and executing `wasm-opt`
use crate::child;
use crate::emoji;
use crate::target;
use crate::PBAR;
use binary_install::Cache;
use log::debug;
use std::path::{Path, PathBuf};
use std::process::Command;
/// Execute `wasm-opt` over wasm binaries found in `out_dir`, downloading if
/// necessary into `cache`. Passes `args` to each invocation of `wasm-opt`.
pub fn run(
cache: &Cache,
out_dir: &Path,
args: &[String],
install_permitted: bool,
) -> Result<(), failure::Error> {
let wasm_opt = match find_wasm_opt(cache, install_permitted)? {
WasmOpt::Found(path) => path,
WasmOpt::CannotInstall => {
PBAR.info("Skipping wasm-opt as no downloading was requested");
return Ok(());
}
WasmOpt::PlatformNotSupported => {
PBAR.info("Skipping wasm-opt because it is not supported on this platform");
return Ok(());
}
};
PBAR.info("Optimizing wasm binaries with `wasm-opt`...");
for file in out_dir.read_dir()? {
let file = file?;
let path = file.path();
if path.extension().and_then(|s| s.to_str()) != Some("wasm") {
continue;
}
let tmp = path.with_extension("wasm-opt.wasm");
let mut cmd = Command::new(&wasm_opt);
cmd.arg(&path).arg("-o").arg(&tmp).args(args);
child::run(cmd, "wasm-opt")?;
std::fs::rename(&tmp, &path)?;
}
Ok(())
}
/// Possible results of `find_wasm_opt`
pub enum WasmOpt {
/// Couldn't install wasm-opt because downloads are forbidden
CannotInstall,
/// The current platform doesn't support precompiled binaries
PlatformNotSupported,
/// We found `wasm-opt` at the specified path
Found(PathBuf),
}
/// Attempts to find `wasm-opt` in `PATH` locally, or failing that downloads a
/// precompiled binary.
///
/// Returns `Some` if a binary was found or it was successfully downloaded.
/// Returns `None` if a binary wasn't found in `PATH` and this platform doesn't
/// have precompiled binaries. Returns an error if we failed to download the
/// binary.
pub fn find_wasm_opt(cache: &Cache, install_permitted: bool) -> Result<WasmOpt, failure::Error> {
// First attempt to look up in PATH. If found assume it works.
if let Ok(path) = which::which("wasm-opt") {
debug!("found wasm-opt at {:?}", path);
return Ok(WasmOpt::Found(path));
}
// ... and if that fails download a precompiled version.
let target = if target::LINUX && target::x86_64 {
"x86_64-linux"
} else if target::MACOS && target::x86_64 {
"x86_64-apple-darwin"
} else if target::WINDOWS && target::x86_64 {
"x86_64-windows"
} else {
return Ok(WasmOpt::PlatformNotSupported);
};
let url = format!(
"https://github.com/WebAssembly/binaryen/releases/download/{vers}/binaryen-{vers}-{target}.tar.gz",
vers = "version_78",
target = target,
);
let download = |permit_install| cache.download(permit_install, "wasm-opt", &["wasm-opt"], &url);
let dl = match download(false)? {
Some(dl) => dl,
None if !install_permitted => return Ok(WasmOpt::CannotInstall),
None => {
let msg = format!("{}Installing wasm-opt...", emoji::DOWN_ARROW);
PBAR.info(&msg);
match download(install_permitted)? {
Some(dl) => dl,
None => return Ok(WasmOpt::CannotInstall),
}
}
};
Ok(WasmOpt::Found(dl.binary("wasm-opt")?))
}

@ -19,4 +19,5 @@ mod manifest;
mod readme;
mod test;
mod utils;
mod wasm_opt;
mod webdriver;

@ -214,6 +214,10 @@ impl Fixture {
/// Takes care not to re-install for every fixture, but only the one time
/// for the whole test suite.
pub fn install_local_wasm_bindgen(&self) -> PathBuf {
// If wasm-bindgen is being used then it's very likely wasm-opt is going
// to be used as well.
self.install_wasm_opt();
static INSTALL_WASM_BINDGEN: Once = ONCE_INIT;
let cache = self.cache();
let version = "0.2.37";
@ -236,6 +240,15 @@ impl Fixture {
download().unwrap().binary("wasm-bindgen").unwrap()
}
pub fn install_wasm_opt(&self) {
static INSTALL_WASM_OPT: Once = ONCE_INIT;
let cache = self.cache();
INSTALL_WASM_OPT.call_once(|| {
wasm_pack::wasm_opt::find_wasm_opt(&cache, true).unwrap();
});
}
/// Download `geckodriver` and return its path.
///
/// Takes care to ensure that only one `geckodriver` is downloaded for the whole

@ -0,0 +1,189 @@
use assert_cmd::prelude::*;
use predicates::prelude::*;
use utils;
#[test]
fn off_in_dev() {
let fixture = utils::fixture::Fixture::new();
fixture.readme().cargo_toml("foo").file("src/lib.rs", "");
fixture.install_local_wasm_bindgen();
fixture.install_wasm_opt();
fixture
.wasm_pack()
.arg("build")
.arg("--dev")
.assert()
.stderr(predicates::str::contains("wasm-opt").not())
.success();
}
#[test]
fn on_in_release() {
let fixture = utils::fixture::Fixture::new();
fixture.readme().cargo_toml("foo").file("src/lib.rs", "");
fixture.install_local_wasm_bindgen();
fixture.install_wasm_opt();
fixture
.wasm_pack()
.arg("build")
.assert()
.stderr(predicates::str::contains("wasm-opt"))
.success();
}
#[test]
fn disable_in_release() {
let fixture = utils::fixture::Fixture::new();
fixture
.readme()
.file(
"Cargo.toml",
r#"
[package]
authors = []
description = ""
license = "MIT"
name = "foo"
repository = ""
version = "0.1.0"
[lib]
crate-type = ["cdylib"]
[dependencies]
wasm-bindgen = "0.2"
[package.metadata.wasm-pack.profile.release]
wasm-opt = false
"#,
)
.file("src/lib.rs", "");
fixture.install_local_wasm_bindgen();
fixture.install_wasm_opt();
fixture
.wasm_pack()
.arg("build")
.assert()
.stderr(predicates::str::contains("wasm-opt").not())
.success();
}
#[test]
fn enable_in_dev() {
let fixture = utils::fixture::Fixture::new();
fixture
.readme()
.file(
"Cargo.toml",
r#"
[package]
authors = []
description = ""
license = "MIT"
name = "foo"
repository = ""
version = "0.1.0"
[lib]
crate-type = ["cdylib"]
[dependencies]
wasm-bindgen = "0.2"
[package.metadata.wasm-pack.profile.dev]
wasm-opt = true
"#,
)
.file("src/lib.rs", "");
fixture.install_local_wasm_bindgen();
fixture.install_wasm_opt();
fixture
.wasm_pack()
.arg("build")
.arg("--dev")
.assert()
.stderr(predicates::str::contains(
"Optimizing wasm binaries with `wasm-opt`",
))
.success();
}
#[test]
fn custom_args() {
let fixture = utils::fixture::Fixture::new();
fixture
.readme()
.file(
"Cargo.toml",
r#"
[package]
authors = []
description = ""
license = "MIT"
name = "foo"
repository = ""
version = "0.1.0"
[lib]
crate-type = ["cdylib"]
[dependencies]
wasm-bindgen = "0.2"
[package.metadata.wasm-pack.profile.release]
wasm-opt = ['--not-accepted-argument']
"#,
)
.file("src/lib.rs", "");
fixture.install_local_wasm_bindgen();
fixture.install_wasm_opt();
fixture
.wasm_pack()
.arg("build")
.assert()
.stderr(predicates::str::contains("--not-accepted-argument"))
.failure();
}
#[test]
fn misconfigured() {
let fixture = utils::fixture::Fixture::new();
fixture
.readme()
.file(
"Cargo.toml",
r#"
[package]
authors = []
description = ""
license = "MIT"
name = "foo"
repository = ""
version = "0.1.0"
[lib]
crate-type = ["cdylib"]
[dependencies]
wasm-bindgen = "0.2"
[package.metadata.wasm-pack.profile.release]
wasm-opt = 32
"#,
)
.file("src/lib.rs", "");
fixture.install_local_wasm_bindgen();
fixture.install_wasm_opt();
fixture
.wasm_pack()
.arg("build")
.assert()
.stderr(predicates::str::contains("failed to parse manifest"))
.failure();
}
Loading…
Cancel
Save