Ver Fonte

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
Evan Almloff há 3 semanas atrás
pai
commit
693f5c61b9

+ 1 - 0
.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()

+ 0 - 1
.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

+ 0 - 7
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"

+ 2 - 3
packages/cli/src/build/request.rs

@@ -3210,8 +3210,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
@@ -3259,7 +3258,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());
             }

+ 221 - 97
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<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 {
-        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<String> {
+    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::<serde_json::Value>()
+        .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<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");
+            }
+            Ok(install_path)
+        }
     }
+}
 
-    Ok(())
+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)
 }
 
-/// 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(())
 }

+ 2 - 2
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,