Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Integrate wasm-opt into the rust-app pipeline #115

Merged
merged 9 commits into from
Mar 5, 2021
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ Subheadings to categorize changes are `added, changed, deprecated, removed, fixe
## Unreleased
### added
- Closed [#93](https://github.com/thedodd/trunk/issues/93): The `watch` and `serve` subcommands can now watch specific folder(s) or file(s) through the new `--watch <path>...` option.
- 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.

## 0.7.4
### fixed
Expand Down
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,18 @@ Next, we will need to install `wasm-bindgen-cli`. In the future Trunk will handl
cargo install wasm-bindgen-cli
```

In addition, we will need to install `wasm-opt` which is part of `binaryen`. On MacOS we can install
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we bundle the binary with trunk or download on command invocation (and cache it)?

Copy link
Contributor Author

@dnaka91 dnaka91 Jan 30, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As I wrote in the description I thought of adding that as a separate PR because it would involve wasm-bindgen-cli as well so we don't need any binaries present.

And there is #105 which would require to re-write download part again as the HTTP client would change as well.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Per my comments below, until we get to the point where we are actually downloading wasm-opt for the user, I would say that we should update this verbiage to something like: If using wasm-opt, we will need to install ....

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Definitely. Have been a bit occopied this week but I'll continue on this soon and adjust to your comments.

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:

Expand Down Expand Up @@ -106,6 +118,8 @@ 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 **run by
default** with the `-O` level and the possible values are `0`, `1`, `2`, `3`, `4`, `s` and `z`. Set this option to `0` to disable wasm-opt. The values `1-4` are increasingly stronger optimization levels for speed. `s` and `z` (z means more optimization) optimize for binary size instead.
thedodd marked this conversation as resolved.
Show resolved Hide resolved
- ✅ `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.
Expand Down
2 changes: 1 addition & 1 deletion src/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,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<RtcBuild>, progress: ProgressBar, ignore_chan: Option<Sender<PathBuf>>) -> Result<Self> {
let html_pipeline = Arc::new(HtmlPipeline::new(cfg.clone(), progress.clone(), ignore_chan)?);
Ok(Self {
Expand Down
83 changes: 66 additions & 17 deletions src/pipelines/rust_app.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
//! Rust application pipeline.

use std::path::PathBuf;
use std::sync::Arc;
use std::{ffi::OsStr, path::PathBuf};

use anyhow::{anyhow, ensure, Context, Result};
use async_process::{Command, Stdio};
Expand Down Expand Up @@ -32,6 +32,9 @@ pub struct RustApp {
/// An optional binary name which will cause cargo & wasm-bindgen to process only the target
/// binary.
bin: Option<String>,
/// An optional optimization setting that enables wasm-opt. Can be nothing (default), `0`, `1`,
/// `2`, `3`, `4`, `s or `z`. Use `0` to disable wasm-opt completely.
wasm_opt: Option<String>,
}

impl RustApp {
Expand All @@ -56,6 +59,7 @@ impl RustApp {
})
.unwrap_or_else(|| html_dir.join("Cargo.toml"));
let bin = el.attr("data-bin").map(|val| val.to_string());
let wasm_opt = el.attr("wasm-opt").map(|val| val.to_string());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's add a struct to wrap and validate this value. Something like (using https://jeltef.github.io/derive_more/derive_more/deref.html):

#[derive(Deref)]
#[deref(forward)]
struct WasmOptValue(String);

impl WasmOptValue {
    pub fn validate(val: &str) -> Result<Self> {
        // validate the value then wrap it in self
    }
}

Then we can update this line to be let wasm_opt = el.attr("wasm-opt").map(|val| WasmOptValue::validate(val)).transpose()?;.

Then, down in the code where it is actually used, we can simply do self.wasm_opt.as_deref().as_str() which is compatible with the AsRef<std::ffi::OsStr> constraint used below.

This will allow folks to see if they've supplied an incorrect value as soon as Trunk is invoked, instead of having to wait until the very end of the cargo build.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about creating an enum for the possible values and implement FromStr and AsRef<str> on it?
That would be a more type safe approach.

Initially I thought it's okay to let the user just pass any value as wasm-opt would just throw an error and we wouldn't have to adjust whenever wasm-opt changes its options. But especially this option probably never changes (or extremely rarely) and can be adjusted quickly.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I dig it. I was going to suggest that initially, but I wasn't sure if it would work with structopt. But if it does, then I definitely agree that it is a better approach.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just checked, FromStr is what structopt requires for the argument types.

https://docs.rs/structopt/0.3.21/structopt/#type-magic

So this should work just fine with enum and FromStr 👍

let manifest = CargoMetadata::new(&manifest_href).await?;
let id = Some(id);

Expand All @@ -66,6 +70,7 @@ impl RustApp {
manifest,
ignore_chan,
bin,
wasm_opt,
})
}

Expand All @@ -81,6 +86,7 @@ impl RustApp {
manifest,
ignore_chan,
bin: None,
wasm_opt: None,
})
}

Expand All @@ -91,7 +97,8 @@ impl RustApp {

async fn build(mut self) -> Result<TrunkLinkPipelineOutput> {
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))
}

Expand Down Expand Up @@ -173,7 +180,7 @@ impl RustApp {
Ok((wasm, hashed_name))
}

async fn wasm_bindgen_build(&self, wasm: PathBuf, hashed_name: String) -> Result<RustAppOutput> {
async fn wasm_bindgen_build(&self, wasm: &Path, hashed_name: &str) -> Result<RustAppOutput> {
self.progress.set_message("calling wasm-bindgen");

// Ensure our output dir is in place.
Expand All @@ -190,20 +197,7 @@ impl RustApp {
let args = vec!["--target=web", &arg_out_path, &arg_out_name, "--no-typescript", &target_wasm];

// Invoke 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?;

// Copy the generated WASM & JS loader to the dist dir.
self.progress.set_message("copying generated artifacts");
Expand Down Expand Up @@ -233,6 +227,39 @@ impl RustApp {
wasm_output: hashed_wasm_name,
})
}

async fn wasm_opt_build(&self, hashed_name: &str) -> Result<()> {
// Zero means no optimizations (valid arg for wasm-opt), so we can skip calling wasm-opt as
// it wouldn't have any effect.
if self.wasm_opt.as_deref() == Some("0") {
return Ok(());
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would say that we should do:

let wasm_opt = match self.wasm_opt.as_deref().as_str() {
    Some(val) if val != "0" => val,
    _ => return Ok(());
};

Basically this will allow Trunk workflows to continue to work even without wasm-opt present on their machine. If they decide that they want to start using it, then they can set it to a non-zero value, and they are g2g.

Once we implement the work to actually download wasm-opt for the user (and probably download wasm-bindgen-cli for them as well), then we can make this default to opt level 4 or something like that, as it will be mostly transparent to the user and will not break existing workflows.

Thoughts?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah that's probably a better approach and would not cause half the users to get errors when running Trunk after the update 😅.

I think "" (no opt level) should be the default as that already applies many optimizations (maybe even equivalent to 3 or 4) and wasm-pack does the same.


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_deref().unwrap_or_default());
let target_wasm = self.cfg.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.dist.join(hashed_name))
.await
.context("error copying wasm file to dist dir")?;
thedodd marked this conversation as resolved.
Show resolved Hide resolved

Ok(())
}
}

/// The output of a cargo build pipeline.
Expand Down Expand Up @@ -262,3 +289,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<OsStr>]) -> 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(())
}