diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 8f3258ff31..081e8fda2c 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -221,6 +221,7 @@ jobs: npm ci npm install -D @playwright/test npx playwright install --with-deps + rm -rf ./target/dx npx playwright test - uses: actions/upload-artifact@v4 if: always() diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 5c949b4912..b14512b7e8 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -112,7 +112,6 @@ jobs: checksum: sha256 manifest_path: packages/cli/Cargo.toml ref: refs/tags/${{ env.RELEASE_POST }} - features: optimizations zip: "all" # todo: these things diff --git a/packages/cli/Cargo.toml b/packages/cli/Cargo.toml index e3f44fc67d..02b2a3abca 100644 --- a/packages/cli/Cargo.toml +++ b/packages/cli/Cargo.toml @@ -154,13 +154,6 @@ tokio-console = ["dep:console-subscriber"] bundle = [] 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]] path = "src/main.rs" name = "dx" diff --git a/packages/cli/src/build/request.rs b/packages/cli/src/build/request.rs index ab16830b46..e9b3e1eac4 100644 --- a/packages/cli/src/build/request.rs +++ b/packages/cli/src/build/request.rs @@ -3217,8 +3217,7 @@ impl BuildRequest { // // 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. - let will_wasm_opt = (self.release || self.wasm_split) - && (self.workspace.wasm_opt.is_some() || cfg!(feature = "optimizations")); + let will_wasm_opt = self.release || self.wasm_split; let keep_debug = self.config.web.wasm_opt.debug || self.debug_symbols || self.wasm_split @@ -3266,7 +3265,7 @@ impl BuildRequest { if !will_wasm_opt { 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()); } diff --git a/packages/cli/src/wasm_opt.rs b/packages/cli/src/wasm_opt.rs index b4a75a70e6..f41d4f45d4 100644 --- a/packages/cli/src/wasm_opt.rs +++ b/packages/cli/src/wasm_opt.rs @@ -1,6 +1,11 @@ +use anyhow::{anyhow, Context}; +use flate2::read::GzDecoder; +use tar::Archive; +use tokio::fs; + use crate::config::WasmOptLevel; -use crate::{Result, WasmOptConfig}; -use std::path::Path; +use crate::{CliSettings, Result, WasmOptConfig, Workspace}; +use std::path::{Path, PathBuf}; /// Write these wasm bytes with a particular set of optimizations 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(()) } -#[allow(unreachable_code)] pub async fn optimize(input_path: &Path, output_path: &Path, cfg: &WasmOptConfig) -> Result<()> { - #[cfg(feature = "optimizations")] - return run_from_lib(input_path, output_path, cfg).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?; + let wasm_opt = WasmOpt::new(input_path, output_path, cfg).await?; + wasm_opt.optimize().await?; Ok(()) } -async fn run_locally(input_path: &Path, output_path: &Path, cfg: &WasmOptConfig) -> Result<()> { - // defaults needed by wasm-bindgen. - // 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 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"); +struct WasmOpt { + path: PathBuf, + input_path: PathBuf, + output_path: PathBuf, + cfg: WasmOptConfig, +} + +impl WasmOpt { + pub async fn new( + input_path: &Path, + output_path: &Path, + cfg: &WasmOptConfig, + ) -> anyhow::Result { + 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 { - args.push("--strip-debug"); - } else { - args.push("--debuginfo"); + /// Create the command to run wasm-opt + fn build_command(&self) -> tokio::process::Command { + // defaults needed by wasm-opt. + // 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 { - args.push(extra); + pub async fn optimize(&self) -> Result<()> { + 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 { - WasmOptLevel::Z => "-Oz", - WasmOptLevel::S => "-Os", - WasmOptLevel::Zero => "-O0", - WasmOptLevel::One => "-O1", - WasmOptLevel::Two => "-O2", - WasmOptLevel::Three => "-O3", - WasmOptLevel::Four => "-O4", +// Find the URL for the latest binaryen release that contains wasm-opt +async fn find_latest_wasm_opt_download_url() -> anyhow::Result { + let url = "https://api.github.com/repos/WebAssembly/binaryen/releases/latest"; + let client = reqwest::Client::new(); + let response = client + .get(url) + .header("User-Agent", "dioxus-cli") + .send() + .await? + .json::() + .await?; + let assets = response + .get("assets") + .and_then(|assets| assets.as_array()) + .ok_or_else(|| anyhow::anyhow!("Failed to parse assets"))?; + + // Find the platform identifier based on the current OS and architecture + let platform = if cfg!(all(target_os = "windows", target_arch = "x86_64")) { + "x86_64-windows" + } else if cfg!(all(target_os = "linux", target_arch = "x86_64")) { + "x86_64-linux" + } else if cfg!(all(target_os = "linux", target_arch = "aarch64")) { + "aarch64-linux" + } else if cfg!(all(target_os = "macos", target_arch = "x86_64")) { + "x86_64-macos" + } else if cfg!(all(target_os = "macos", target_arch = "aarch64")) { + "arm64-macos" + } else { + return Err(anyhow::anyhow!( + "Unknown platform for wasm-opt installation. Please install wasm-opt manually from https://github.com/WebAssembly/binaryen/releases and add it to your PATH." + )); }; - let res = tokio::process::Command::new("wasm-opt") - .arg(input_path) - .arg(level) - .arg("-o") - .arg(output_path) - .args(args) - .output() - .await?; + // Find the first asset with a name that contains the platform string + let asset = assets + .iter() + .find(|asset| { + asset + .get("name") + .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 + ) + })?; + + // Extract the download URL from the asset + let download_url = asset + .get("browser_download_url") + .and_then(|url| url.as_str()) + .ok_or_else(|| anyhow::anyhow!("Failed to get download URL for wasm-opt"))?; + + Ok(download_url.to_string()) +} - if !res.status.success() { - let err = String::from_utf8_lossy(&res.stderr); - tracing::error!("wasm-opt failed with status code {}: {}", res.status, err); +/// Get the path to the wasm-opt binary, downloading it if necessary +async fn get_binary_path() -> anyhow::Result { + 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"); + } + Ok(install_path) + } } +} - Ok(()) +async fn install_dir() -> anyhow::Result { + let bindgen_dir = Workspace::dioxus_home_dir().join("binaryen"); + fs::create_dir_all(&bindgen_dir).await?; + Ok(bindgen_dir) } -/// Use the `wasm_opt` crate -#[cfg(feature = "optimizations")] -async fn run_from_lib( - input_path: &Path, - output_path: &Path, - options: &WasmOptConfig, -) -> 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(), - }; +fn installed_bin_name() -> &'static str { + if cfg!(windows) { + "wasm-opt.exe" + } else { + "wasm-opt" + } +} - level - .enable_feature(wasm_opt::Feature::ReferenceTypes) - .enable_feature(wasm_opt::Feature::BulkMemory) - .enable_feature(wasm_opt::Feature::MutableGlobals) - .enable_feature(wasm_opt::Feature::TruncSat) - .add_pass(wasm_opt::Pass::MemoryPacking) - .debug_info(options.debug); - - for arg in options.extra_features.iter() { - if arg.starts_with("--enable-") { - let feature = arg.trim_start_matches("--enable-"); - if let Ok(feature) = wasm_opt::Feature::from_str(feature) { - level.enable_feature(feature); - } else { - tracing::warn!("Unknown wasm-opt feature: {}", feature); - } - } else if arg.starts_with("--disable-") { - 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); +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(()) } diff --git a/packages/playwright-tests/playwright.config.js b/packages/playwright-tests/playwright.config.js index 302a05e678..77ef8cc8e9 100644 --- a/packages/playwright-tests/playwright.config.js +++ b/packages/playwright-tests/playwright.config.js @@ -161,7 +161,7 @@ module.exports = defineConfig({ cwd: path.join(process.cwd(), "cli-optimization"), // Remove the cache folder for the cli-optimization build to force a full cache reset 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, timeout: 50 * 60 * 1000, reuseExistingServer: !process.env.CI, @@ -170,7 +170,7 @@ module.exports = defineConfig({ { cwd: path.join(process.cwd(), "wasm-split-harness"), 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, timeout: 50 * 60 * 1000, reuseExistingServer: !process.env.CI,