Always download wasm opt from the binary (#4247)

* always download wasm opt from the binary

* clean up and pass clippy

* fix playwright commands

* don't cache dx target folder
This commit is contained in:
Evan Almloff authored and GitHub committed 2025-06-04 08:43:58 -05:00
1 parent c2a6c14305
commit 693f5c61b9
6 files changed
+227 -111

No files matched your search

+1
View File
@@ -221,6 +221,7 @@ jobs:
npm ci npm ci
npm install -D @playwright/test npm install -D @playwright/test
npx playwright install --with-deps npx playwright install --with-deps
rm -rf ./target/dx
npx playwright test npx playwright test
- uses: actions/upload-artifact@v4 - uses: actions/upload-artifact@v4
if: always() if: always()
-1
View File
@@ -112,7 +112,6 @@ jobs:
checksum: sha256 checksum: sha256
manifest_path: packages/cli/Cargo.toml manifest_path: packages/cli/Cargo.toml
ref: refs/tags/${{ env.RELEASE_POST }} ref: refs/tags/${{ env.RELEASE_POST }}
features: optimizations
zip: "all" zip: "all"
# todo: these things # todo: these things
-7
View File
@@ -154,13 +154,6 @@ tokio-console = ["dep:console-subscriber"]
bundle = [] bundle = []
no-downloads = [] no-downloads = []
# when releasing dioxus, we want to enable wasm-opt
# and then also maybe developing it too.
# making this optional cuts workspace deps down from 1000 to 500, so it's very nice for workspace adev
optimizations = ["wasm-opt", "asset-opt"]
asset-opt = []
wasm-opt = ["dep:wasm-opt"]
[[bin]] [[bin]]
path = "src/main.rs" path = "src/main.rs"
name = "dx" name = "dx"
+2 -3
View File
@@ -3210,8 +3210,7 @@ impl BuildRequest {
// //
// We leave demangling to false since it's faster and these tools seem to prefer the raw symbols. // We leave demangling to false since it's faster and these tools seem to prefer the raw symbols.
// todo(jon): investigate if the chrome extension needs them demangled or demangles them automatically. // todo(jon): investigate if the chrome extension needs them demangled or demangles them automatically.
let will_wasm_opt = (self.release || self.wasm_split) let will_wasm_opt = self.release || self.wasm_split;
&& (self.workspace.wasm_opt.is_some() || cfg!(feature = "optimizations"));
let keep_debug = self.config.web.wasm_opt.debug let keep_debug = self.config.web.wasm_opt.debug
|| self.debug_symbols || self.debug_symbols
|| self.wasm_split || self.wasm_split
@@ -3259,7 +3258,7 @@ impl BuildRequest {
if !will_wasm_opt { if !will_wasm_opt {
return Err(anyhow::anyhow!( return Err(anyhow::anyhow!(
"Bundle splitting requires wasm-opt to be installed or the CLI to be built with `--features optimizations`. Please install wasm-opt and try again." "Bundle splitting should automatically enable wasm-opt, but it was not enabled."
) )
.into()); .into());
} }
+222 -98
View File
@@ -1,6 +1,11 @@
use anyhow::{anyhow, Context};
use flate2::read::GzDecoder;
use tar::Archive;
use tokio::fs;
use crate::config::WasmOptLevel; use crate::config::WasmOptLevel;
use crate::{Result, WasmOptConfig}; use crate::{CliSettings, Result, WasmOptConfig, Workspace};
use std::path::Path; use std::path::{Path, PathBuf};
/// Write these wasm bytes with a particular set of optimizations /// Write these wasm bytes with a particular set of optimizations
pub async fn write_wasm(bytes: &[u8], output_path: &Path, cfg: &WasmOptConfig) -> Result<()> { pub async fn write_wasm(bytes: &[u8], output_path: &Path, cfg: &WasmOptConfig) -> Result<()> {
@@ -9,123 +14,242 @@ pub async fn write_wasm(bytes: &[u8], output_path: &Path, cfg: &WasmOptConfig) -
Ok(()) Ok(())
} }
#[allow(unreachable_code)]
pub async fn optimize(input_path: &Path, output_path: &Path, cfg: &WasmOptConfig) -> Result<()> { pub async fn optimize(input_path: &Path, output_path: &Path, cfg: &WasmOptConfig) -> Result<()> {
#[cfg(feature = "optimizations")] let wasm_opt = WasmOpt::new(input_path, output_path, cfg).await?;
return run_from_lib(input_path, output_path, cfg).await; wasm_opt.optimize().await?;
// It's okay not to run wasm-opt but we should *really* try it
if which::which("wasm-opt").is_err() {
tracing::warn!("wasm-opt not found and CLI is compiled without optimizations. Skipping optimization for {}", input_path.display());
return Ok(());
}
run_locally(input_path, output_path, cfg).await?;
Ok(()) Ok(())
} }
async fn run_locally(input_path: &Path, output_path: &Path, cfg: &WasmOptConfig) -> Result<()> { struct WasmOpt {
// defaults needed by wasm-bindgen. path: PathBuf,
// wasm is a moving target, and we add these by default since they progressively get enabled by default. input_path: PathBuf,
let mut args = vec![ output_path: PathBuf,
"--enable-reference-types", cfg: WasmOptConfig,
"--enable-bulk-memory", }
"--enable-mutable-globals",
"--enable-nontrapping-float-to-int",
];
if cfg.memory_packing { impl WasmOpt {
// needed for our current approach to bundle splitting to work properly pub async fn new(
// todo(jon): emit the main module's data section in chunks instead of all at once input_path: &Path,
args.push("--memory-packing"); output_path: &Path,
cfg: &WasmOptConfig,
) -> anyhow::Result<Self> {
let path = get_binary_path().await?;
Ok(Self {
path,
input_path: input_path.to_path_buf(),
output_path: output_path.to_path_buf(),
cfg: cfg.clone(),
})
} }
if !cfg.debug { /// Create the command to run wasm-opt
args.push("--strip-debug"); fn build_command(&self) -> tokio::process::Command {
} else { // defaults needed by wasm-opt.
args.push("--debuginfo"); // wasm is a moving target, and we add these by default since they progressively get enabled by default.
let mut args = vec![
"--enable-reference-types",
"--enable-bulk-memory",
"--enable-mutable-globals",
"--enable-nontrapping-float-to-int",
];
if self.cfg.memory_packing {
// needed for our current approach to bundle splitting to work properly
// todo(jon): emit the main module's data section in chunks instead of all at once
args.push("--memory-packing");
}
if !self.cfg.debug {
args.push("--strip-debug");
} else {
args.push("--debuginfo");
}
for extra in &self.cfg.extra_features {
args.push(extra);
}
let level = match self.cfg.level {
WasmOptLevel::Z => "-Oz",
WasmOptLevel::S => "-Os",
WasmOptLevel::Zero => "-O0",
WasmOptLevel::One => "-O1",
WasmOptLevel::Two => "-O2",
WasmOptLevel::Three => "-O3",
WasmOptLevel::Four => "-O4",
};
let mut command = tokio::process::Command::new(&self.path);
command
.arg(&self.input_path)
.arg(level)
.arg("-o")
.arg(&self.output_path)
.args(args);
command
} }
for extra in &cfg.extra_features { pub async fn optimize(&self) -> Result<()> {
args.push(extra); let mut command = self.build_command();
let res = command.output().await?;
if !res.status.success() {
let err = String::from_utf8_lossy(&res.stderr);
tracing::error!("wasm-opt failed with status code {}: {}", res.status, err);
}
Ok(())
} }
}
let level = match cfg.level { // Find the URL for the latest binaryen release that contains wasm-opt
WasmOptLevel::Z => "-Oz", async fn find_latest_wasm_opt_download_url() -> anyhow::Result<String> {
WasmOptLevel::S => "-Os", let url = "https://api.github.com/repos/WebAssembly/binaryen/releases/latest";
WasmOptLevel::Zero => "-O0", let client = reqwest::Client::new();
WasmOptLevel::One => "-O1", let response = client
WasmOptLevel::Two => "-O2", .get(url)
WasmOptLevel::Three => "-O3", .header("User-Agent", "dioxus-cli")
WasmOptLevel::Four => "-O4", .send()
}; .await?
.json::<serde_json::Value>()
let res = tokio::process::Command::new("wasm-opt")
.arg(input_path)
.arg(level)
.arg("-o")
.arg(output_path)
.args(args)
.output()
.await?; .await?;
let assets = response
.get("assets")
.and_then(|assets| assets.as_array())
.ok_or_else(|| anyhow::anyhow!("Failed to parse assets"))?;
if !res.status.success() { // Find the platform identifier based on the current OS and architecture
let err = String::from_utf8_lossy(&res.stderr); let platform = if cfg!(all(target_os = "windows", target_arch = "x86_64")) {
tracing::error!("wasm-opt failed with status code {}: {}", res.status, err); "x86_64-windows"
} } else if cfg!(all(target_os = "linux", target_arch = "x86_64")) {
"x86_64-linux"
Ok(()) } else if cfg!(all(target_os = "linux", target_arch = "aarch64")) {
} "aarch64-linux"
} else if cfg!(all(target_os = "macos", target_arch = "x86_64")) {
/// Use the `wasm_opt` crate "x86_64-macos"
#[cfg(feature = "optimizations")] } else if cfg!(all(target_os = "macos", target_arch = "aarch64")) {
async fn run_from_lib( "arm64-macos"
input_path: &Path, } else {
output_path: &Path, return Err(anyhow::anyhow!(
options: &WasmOptConfig, "Unknown platform for wasm-opt installation. Please install wasm-opt manually from https://github.com/WebAssembly/binaryen/releases and add it to your PATH."
) -> Result<()> { ));
use std::str::FromStr;
let mut level = match options.level {
WasmOptLevel::Z => wasm_opt::OptimizationOptions::new_optimize_for_size_aggressively(),
WasmOptLevel::S => wasm_opt::OptimizationOptions::new_optimize_for_size(),
WasmOptLevel::Zero => wasm_opt::OptimizationOptions::new_opt_level_0(),
WasmOptLevel::One => wasm_opt::OptimizationOptions::new_opt_level_1(),
WasmOptLevel::Two => wasm_opt::OptimizationOptions::new_opt_level_2(),
WasmOptLevel::Three => wasm_opt::OptimizationOptions::new_opt_level_3(),
WasmOptLevel::Four => wasm_opt::OptimizationOptions::new_opt_level_4(),
}; };
level // Find the first asset with a name that contains the platform string
.enable_feature(wasm_opt::Feature::ReferenceTypes) let asset = assets
.enable_feature(wasm_opt::Feature::BulkMemory) .iter()
.enable_feature(wasm_opt::Feature::MutableGlobals) .find(|asset| {
.enable_feature(wasm_opt::Feature::TruncSat) asset
.add_pass(wasm_opt::Pass::MemoryPacking) .get("name")
.debug_info(options.debug); .and_then(|name| name.as_str())
.is_some_and(|name| name.contains(platform))
})
.ok_or_else(|| {
anyhow::anyhow!(
"No suitable wasm-opt binary found for platform: {}. Please install wasm-opt manually from https://github.com/WebAssembly/binaryen/releases and add it to your PATH.",
platform
)
})?;
for arg in options.extra_features.iter() { // Extract the download URL from the asset
if arg.starts_with("--enable-") { let download_url = asset
let feature = arg.trim_start_matches("--enable-"); .get("browser_download_url")
if let Ok(feature) = wasm_opt::Feature::from_str(feature) { .and_then(|url| url.as_str())
level.enable_feature(feature); .ok_or_else(|| anyhow::anyhow!("Failed to get download URL for wasm-opt"))?;
} else {
tracing::warn!("Unknown wasm-opt feature: {}", feature); Ok(download_url.to_string())
}
/// Get the path to the wasm-opt binary, downloading it if necessary
async fn get_binary_path() -> anyhow::Result<PathBuf> {
let existing_path = which::which("wasm-opt");
match existing_path {
// If wasm-opt is already in the PATH, return its path
Ok(path) => Ok(path),
// If wasm-opt is not found in the path and we prefer no downloads, return an error
Err(_) if CliSettings::prefer_no_downloads() => Err(anyhow!("Missing wasm-opt")),
// Otherwise, try to install it
Err(_) => {
let install_dir = install_dir().await?;
let install_path = installed_bin_path(&install_dir);
if !install_path.exists() {
tracing::info!("Installing wasm-opt");
install_github(&install_dir).await?;
tracing::info!("wasm-opt installed from Github");
} }
} else if arg.starts_with("--disable-") { Ok(install_path)
let feature = arg.trim_start_matches("--disable-"); }
if let Ok(feature) = wasm_opt::Feature::from_str(feature) { }
level.disable_feature(feature); }
} else {
tracing::warn!("Unknown wasm-opt feature: {}", feature); async fn install_dir() -> anyhow::Result<PathBuf> {
let bindgen_dir = Workspace::dioxus_home_dir().join("binaryen");
fs::create_dir_all(&bindgen_dir).await?;
Ok(bindgen_dir)
}
fn installed_bin_name() -> &'static str {
if cfg!(windows) {
"wasm-opt.exe"
} else {
"wasm-opt"
}
}
fn installed_bin_path(install_dir: &Path) -> PathBuf {
let bin_name = installed_bin_name();
install_dir.join("bin").join(bin_name)
}
/// Install wasm-opt from GitHub releases into the specified directory
async fn install_github(install_dir: &Path) -> anyhow::Result<()> {
tracing::trace!("Attempting to install wasm-opt from GitHub");
let url = find_latest_wasm_opt_download_url()
.await
.context("Failed to find latest wasm-opt download URL")?;
tracing::trace!("Downloading wasm-opt from {}", url);
// Download the binaryen release archive into memory
let bytes = reqwest::get(url).await?.bytes().await?;
// We don't need the whole gzip archive, just the wasm-opt binary and the lib folder. We
// just extract those files from the archive.
let installed_bin_path = installed_bin_path(install_dir);
let lib_folder_name = "lib";
let installed_lib_path = install_dir.join(lib_folder_name);
// Create the lib and bin directories if they don't exist
for path in [installed_bin_path.parent(), Some(&installed_lib_path)]
.into_iter()
.flatten()
{
std::fs::create_dir_all(path)
.context(format!("Failed to create directory: {}", path.display()))?;
}
let mut archive = Archive::new(GzDecoder::new(bytes.as_ref()));
// Unpack the binary and library files from the archive
for mut entry in archive.entries()?.flatten() {
// Unpack the wasm-opt binary
if entry
.path_bytes()
.ends_with(installed_bin_name().as_bytes())
{
entry.unpack(&installed_bin_path)?;
}
// Unpack any files in the lib folder
else if let Ok(path) = entry.path() {
if path.components().any(|c| c.as_os_str() == lib_folder_name) {
if let Some(file_name) = path.file_name() {
entry.unpack(installed_lib_path.join(file_name))?;
}
} }
} }
} }
level
.run(input_path, output_path)
.map_err(|err| crate::Error::Other(anyhow::anyhow!(err)))?;
Ok(()) Ok(())
} }
@@ -161,7 +161,7 @@ module.exports = defineConfig({
cwd: path.join(process.cwd(), "cli-optimization"), cwd: path.join(process.cwd(), "cli-optimization"),
// Remove the cache folder for the cli-optimization build to force a full cache reset // Remove the cache folder for the cli-optimization build to force a full cache reset
command: command:
'cargo run --package dioxus-cli --release --features optimizations -- serve --addr "127.0.0.1" --port 8989', 'cargo run --package dioxus-cli --release -- serve --addr "127.0.0.1" --port 8989',
port: 8989, port: 8989,
timeout: 50 * 60 * 1000, timeout: 50 * 60 * 1000,
reuseExistingServer: !process.env.CI, reuseExistingServer: !process.env.CI,
@@ -170,7 +170,7 @@ module.exports = defineConfig({
{ {
cwd: path.join(process.cwd(), "wasm-split-harness"), cwd: path.join(process.cwd(), "wasm-split-harness"),
command: command:
'cargo run --package dioxus-cli --release --features optimizations -- serve --bin wasm-split-harness --platform web --addr "127.0.0.1" --port 8001 --wasm-split --profile wasm-split-release', 'cargo run --package dioxus-cli --release -- serve --bin wasm-split-harness --platform web --addr "127.0.0.1" --port 8001 --wasm-split --profile wasm-split-release',
port: 8001, port: 8001,
timeout: 50 * 60 * 1000, timeout: 50 * 60 * 1000,
reuseExistingServer: !process.env.CI, reuseExistingServer: !process.env.CI,