diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ab70f37..901d5f1c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,8 @@ Added support for proxying WebSockets. This was a long-standing feature request. - Added the `--proxy-ws` CLI option for enabling WebSocket proxying on a CLI defined proxy. - Added the `ws = true` field to the `Trunk.toml` `[[proxy]]` sections which will enable WebSocket proxying for proxies defined in the `Trunk.toml`. +- WASM files are now automatically optimized with `wasm-opt` to reduce the binary size. The optimization level can be set with the new `wasm-opt` argument of the `rust` asset link and`wasm-opt` binary is now required to be globally installed on the system. + ### fixed - Closed [#81](https://github.com/thedodd/trunk/issues/81): this is no longer needed as we now have support for WebSockets. HTTP2 is still outstanding, but that will not be a blocker for use from the web. - Closed [#95](https://github.com/thedodd/trunk/issues/95): fixed via a few small changes to precendce in routing. diff --git a/README.md b/README.md index da18e219..4cbb08ca 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,18 @@ Next, we will need to install `wasm-bindgen-cli`. In the future Trunk will handl cargo install wasm-bindgen-cli ``` +If using wasm-opt, we will need to install `wasm-opt` which is part of `binaryen`. On MacOS we can install +it with Homebrew: + +```sh +brew install binaryen +``` + +Some linux distributions provide a `binaryen` package in their package managers but if it's not +available, outdated or you're on Windows, then we can grab a +[pre-compiled release](https://github.com/WebAssembly/binaryen/releases), extract it and put the +`wasm-opt` binary in some location that's available on the PATH. + ### app setup Get setup with your favorite `wasm-bindgen` based framework. [Yew](https://github.com/yewstack/yew) & [Seed](https://github.com/seed-rs/seed) are the most popular options today, but there are others. Trunk will work with any `wasm-bindgen` based framework. The easiest way to ensure that your application launches properly is to [setup your app as an executable](https://doc.rust-lang.org/cargo/guide/project-layout.html) with a standard `main` function: @@ -107,6 +119,7 @@ Currently supported asset types: - ✅ `rust`: Trunk will compile the specified Cargo project as the main WASM application. This is optional. If not specified, Trunk will look for a `Cargo.toml` in the parent directory of the source HTML file. - `href`: (optional) the path to the `Cargo.toml` of the Rust project. If a directory is specified, then Trunk will look for the `Cargo.toml` in the given directory. If no value is specified, then Trunk will look for a `Cargo.toml` in the parent directory of the source HTML file. - `data-bin`: (optional) the name of the binary to compile and use as the main WASM application. If the Cargo project has multiple binaries, this value will be required for proper functionality. + - `wasm-opt`: (optional) run wasm-opt with the set optimization level. wasm-opt is **turned off by default** but that may change in the future. The possible values are `0`, `1`, `2`, `3`, `4`, `s`, `z` or an _empty value_ for wasm-opt's default. Set this option to `0` to disable wasm-opt explicitly. The values `1-4` are increasingly stronger optimization levels for speed. `s` and `z` (z means more optimization) optimize for binary size instead. - ✅ `sass`, `scss`: Trunk ships with a [built-in sass/scss compiler](https://github.com/compass-rs/sass-rs). Just link to your sass files from your source HTML, and Trunk will handle the rest. This content is hashed for cache control. The `href` attribute must be included in the link pointing to the sass/scss file to be processed. - ✅ `css`: Trunk will copy linked css files found in the source HTML without content modification. This content is hashed for cache control. The `href` attribute must be included in the link pointing to the css file to be processed. - In the future, Trunk will resolve local `@imports`, will handle minification (see [trunk#7](https://github.com/thedodd/trunk/issues/3)), and we may even look into a pattern where any CSS found in the source tree will be bundled, which would enable a nice zero-config "component styles" pattern. See [trunk#3](https://github.com/thedodd/trunk/issues/3) for more details. diff --git a/src/build.rs b/src/build.rs index 90ba6a3d..e56e2cd3 100644 --- a/src/build.rs +++ b/src/build.rs @@ -32,7 +32,7 @@ impl BuildSystem { /// Create a new instance from the raw components. /// /// Reducing the number of assumptions here should help us to stay flexible when adding new - /// commands, rafctoring and the like. + /// commands, refactoring and the like. pub async fn new(cfg: Arc, progress: ProgressBar, ignore_chan: Option>) -> Result { let html_pipeline = Arc::new(HtmlPipeline::new(cfg.clone(), progress.clone(), ignore_chan)?); Ok(Self { diff --git a/src/pipelines/rust_app.rs b/src/pipelines/rust_app.rs index 4ba102a6..9d7559ea 100644 --- a/src/pipelines/rust_app.rs +++ b/src/pipelines/rust_app.rs @@ -1,10 +1,11 @@ //! Rust application pipeline. use std::iter::Iterator; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use std::sync::Arc; +use std::{ffi::OsStr, str::FromStr}; -use anyhow::{anyhow, ensure, Context, Result}; +use anyhow::{anyhow, bail, ensure, Context, Result}; use async_process::{Command, Stdio}; use async_std::fs; use async_std::task::{spawn, JoinHandle}; @@ -32,6 +33,71 @@ pub struct RustApp { /// An optional binary name which will cause cargo & wasm-bindgen to process only the target /// binary. bin: Option, + /// An optional optimization setting that enables wasm-opt. Can be nothing, `0` (default), `1`, + /// `2`, `3`, `4`, `s or `z`. Using `0` disables wasm-opt completely. + wasm_opt: WasmOptLevel, +} + +/// Different optimization levels that can be configured with `wasm-opt`. +#[derive(PartialEq, Eq)] +enum WasmOptLevel { + /// Default optimization passes. + Default, + /// No optimization passes, skipping the wasp-opt step. + Off, + /// Run quick & useful optimizations. useful for iteration testing. + One, + /// Most optimizations, generally gets most performance. + Two, + /// Spend potentially a lot of time optimizing. + Three, + /// Also flatten the IR, which can take a lot more time and memory, but is useful on more nested + /// / complex / less-optimized input. + Four, + /// Default optimizations, focus on code size. + S, + /// Default optimizations, super-focusing on code size. + Z, +} + +impl FromStr for WasmOptLevel { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + Ok(match s { + "" => Self::Default, + "0" => Self::Off, + "1" => Self::One, + "2" => Self::Two, + "3" => Self::Three, + "4" => Self::Four, + "s" | "S" => Self::S, + "z" | "Z" => Self::Z, + _ => bail!("unknown wasm-opt level `{}`", s), + }) + } +} + +impl AsRef for WasmOptLevel { + fn as_ref(&self) -> &str { + match self { + Self::Default => "", + Self::Off => "0", + Self::One => "1", + Self::Two => "2", + Self::Three => "3", + Self::Four => "4", + Self::S => "s", + Self::Z => "z", + } + } +} + +impl Default for WasmOptLevel { + fn default() -> Self { + // Current default is off until automatic download of wasm-opt is implemented. + Self::Off + } } impl RustApp { @@ -56,6 +122,7 @@ impl RustApp { }) .unwrap_or_else(|| html_dir.join("Cargo.toml")); let bin = attrs.get("data-bin").map(|val| val.to_string()); + let wasm_opt = attrs.get("wasm-opt").map(|val| val.parse()).transpose()?.unwrap_or_default(); let manifest = CargoMetadata::new(&manifest_href).await?; let id = Some(id); @@ -66,6 +133,7 @@ impl RustApp { manifest, ignore_chan, bin, + wasm_opt, }) } @@ -81,6 +149,7 @@ impl RustApp { manifest, ignore_chan, bin: None, + wasm_opt: WasmOptLevel::default(), }) } @@ -91,7 +160,8 @@ impl RustApp { async fn build(mut self) -> Result { let (wasm, hashed_name) = self.cargo_build().await?; - let output = self.wasm_bindgen_build(wasm, hashed_name).await?; + let output = self.wasm_bindgen_build(wasm.as_ref(), &hashed_name).await?; + self.wasm_opt_build(&output.wasm_output).await?; Ok(TrunkLinkPipelineOutput::RustApp(output)) } @@ -176,7 +246,7 @@ impl RustApp { Ok((wasm, hashed_name)) } - async fn wasm_bindgen_build(&self, wasm: PathBuf, hashed_name: String) -> Result { + async fn wasm_bindgen_build(&self, wasm: &Path, hashed_name: &str) -> Result { self.progress.set_message("preparing for build"); // Ensure our output dir is in place. @@ -194,20 +264,7 @@ impl RustApp { // Invoke wasm-bindgen. self.progress.set_message("calling wasm-bindgen"); - let build_output = Command::new("wasm-bindgen") - .args(args.as_slice()) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .spawn() - .context("error spawning wasm-bindgen call")? - .output() - .await - .context("error during wasm-bindgen call")?; - ensure!( - build_output.status.success(), - "wasm-bindgen call returned a bad status {}", - String::from_utf8_lossy(&build_output.stderr), - ); + run_command("wasm-bindgen", &args).await?; self.progress.set_message("copying generated artifacts"); @@ -240,6 +297,43 @@ impl RustApp { wasm_output: hashed_wasm_name, }) } + + async fn wasm_opt_build(&self, hashed_name: &str) -> Result<()> { + // If opt level is off, we skip calling wasm-opt as it wouldn't have any effect. + if self.wasm_opt == WasmOptLevel::Off { + return Ok(()); + } + + self.progress.set_message("calling wasm-opt"); + + // Ensure our output dir is in place. + let mode_segment = if self.cfg.release { "release" } else { "debug" }; + let output = self.manifest.metadata.target_directory.join("wasm-opt").join(mode_segment); + fs::create_dir_all(&output).await.context("error creating wasm-opt output dir")?; + + // Build up args for calling wasm-opt. + let output = output.join(hashed_name); + let arg_output = format!("--output={}", output.display()); + let arg_opt_level = format!("-O{}", self.wasm_opt.as_ref()); + let target_wasm = self.cfg.staging_dist.join(hashed_name).to_string_lossy().to_string(); + let args = vec![&arg_output, &arg_opt_level, &target_wasm]; + + // Invoke wasm-opt. + run_command("wasm-opt", &args).await?; + + // Copy the generated WASM file to the dist dir. + self.progress.set_message("copying generated artifacts"); + fs::copy(output, self.cfg.staging_dist.join(hashed_name)) + .await + .context("error copying wasm file to dist dir")?; + + // Delete old un-optimized WASM file from the staging dir. + fs::remove_file(target_wasm) + .await + .context("error deleting un-optimized wasm file from dist dir")?; + + Ok(()) + } } /// The output of a cargo build pipeline. @@ -269,3 +363,25 @@ impl RustAppOutput { Ok(()) } } + +/// Run a global command with the given arguments and make sure it completes successfully. If it +/// fails an error is returned. +async fn run_command(name: &str, args: &[impl AsRef]) -> Result<()> { + let output = Command::new(name) + .args(args) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .with_context(|| format!("error spawning {} call", name))? + .output() + .await + .with_context(|| format!("error during {} call", name))?; + ensure!( + output.status.success(), + "{} call returned a bad status {}", + name, + String::from_utf8_lossy(&output.stderr), + ); + + Ok(()) +}