Procházet zdrojové kódy

feat(cli): add feature to disable downloads (#3465)

* feat(cli): add feature to disable downloads

---------

Co-authored-by: Jonathan Kelley <jkelleyrtp@gmail.com>
Cathal před 5 měsíci
rodič
revize
c7951b7dd5

+ 1 - 1
.github/workflows/main.yml

@@ -193,7 +193,7 @@ jobs:
       - name: Build all flake outputs
         run: om ci
       - name: Ensure devShell has all build deps
-        run: nix develop -c cargo build -p dioxus-cli
+        run: nix develop -c cargo build -p dioxus-cli --features no-downloads
 
   playwright:
     if: github.event.pull_request.draft == false

+ 26 - 1
Cargo.lock

@@ -1613,7 +1613,7 @@ dependencies = [
  "rustc-hash 1.1.0",
  "shlex",
  "syn 2.0.90",
- "which",
+ "which 4.4.2",
 ]
 
 [[package]]
@@ -3546,6 +3546,7 @@ dependencies = [
  "uuid",
  "walkdir",
  "wasm-opt",
+ "which 7.0.1",
 ]
 
 [[package]]
@@ -4628,6 +4629,12 @@ dependencies = [
  "regex",
 ]
 
+[[package]]
+name = "env_home"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c7f84e12ccf0a7ddc17a6c41c93326024c42920d7ee630d04950e6926645c0fe"
+
 [[package]]
 name = "env_logger"
 version = "0.10.2"
@@ -14895,6 +14902,18 @@ dependencies = [
  "rustix",
 ]
 
+[[package]]
+name = "which"
+version = "7.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fb4a9e33648339dc1642b0e36e21b3385e6148e289226f657c809dee59df5028"
+dependencies = [
+ "either",
+ "env_home",
+ "rustix",
+ "winsafe",
+]
+
 [[package]]
 name = "whoami"
 version = "1.5.2"
@@ -15382,6 +15401,12 @@ dependencies = [
  "windows-sys 0.48.0",
 ]
 
+[[package]]
+name = "winsafe"
+version = "0.0.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904"
+
 [[package]]
 name = "write16"
 version = "1.0.0"

+ 4 - 0
packages/cli/Cargo.toml

@@ -73,6 +73,9 @@ reqwest = { workspace = true, features = [
 tower = { workspace = true }
 once_cell = "1.19.0"
 
+# path lookup
+which = { version = "7.0.1" }
+
 # plugin packages
 open = "5.0.1"
 cargo-generate = "=0.21.3"
@@ -129,6 +132,7 @@ default = []
 plugin = []
 tokio-console = ["dep:console-subscriber"]
 bundle = []
+no-downloads = []
 
 # when releasing dioxus, we want to enable wasm-opt
 # and then also maybe developing it too.

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

@@ -1,6 +1,6 @@
 use super::prerender::pre_render_static_routes;
 use super::templates::InfoPlistData;
-use crate::wasm_bindgen::WasmBindgenBuilder;
+use crate::wasm_bindgen::WasmBindgen;
 use crate::{BuildRequest, Platform};
 use crate::{Result, TraceSrc};
 use anyhow::Context;
@@ -616,7 +616,7 @@ impl AppBundle {
             .wasm_bindgen_version()
             .expect("this should have been checked by tool verification");
 
-        WasmBindgenBuilder::new(bindgen_version)
+        WasmBindgen::new(&bindgen_version)
             .input_path(&input_path)
             .target("web")
             .debug(keep_debug)
@@ -626,7 +626,6 @@ impl AppBundle {
             .remove_producers_section(!keep_debug)
             .out_name(&name)
             .out_dir(&bindgen_outdir)
-            .build()
             .run()
             .await
             .context("Failed to generate wasm-bindgen bindings")?;

+ 24 - 21
packages/cli/src/build/verify.rs

@@ -1,9 +1,8 @@
-use crate::{wasm_bindgen::WasmBindgen, BuildRequest, Platform, Result, RustupShow};
+use crate::{wasm_bindgen::WasmBindgen, BuildRequest, Error, Platform, Result, RustcDetails};
 use anyhow::{anyhow, Context};
-use tokio::process::Command;
 
 impl BuildRequest {
-    /// Install any tooling that might be required for this build.
+    /// Check for tooling that might be required for this build.
     ///
     /// This should generally be only called on the first build since it takes time to verify the tooling
     /// is in place, and we don't want to slow down subsequent builds.
@@ -15,7 +14,7 @@ impl BuildRequest {
             .initialize_profiles()
             .context("Failed to initialize profiles - dioxus can't build without them. You might need to initialize them yourself.")?;
 
-        let rustup = match RustupShow::from_cli().await {
+        let rustc = match RustcDetails::from_cli().await {
             Ok(out) => out,
             Err(err) => {
                 tracing::error!("Failed to verify tooling: {err}\ndx will proceed, but you might run into errors later.");
@@ -24,10 +23,10 @@ impl BuildRequest {
         };
 
         match self.build.platform() {
-            Platform::Web => self.verify_web_tooling(rustup).await?,
-            Platform::Ios => self.verify_ios_tooling(rustup).await?,
-            Platform::Android => self.verify_android_tooling(rustup).await?,
-            Platform::Linux => self.verify_linux_tooling(rustup).await?,
+            Platform::Web => self.verify_web_tooling(rustc).await?,
+            Platform::Ios => self.verify_ios_tooling(rustc).await?,
+            Platform::Android => self.verify_android_tooling(rustc).await?,
+            Platform::Linux => self.verify_linux_tooling(rustc).await?,
             Platform::MacOS => {}
             Platform::Windows => {}
             Platform::Server => {}
@@ -37,29 +36,33 @@ impl BuildRequest {
         Ok(())
     }
 
-    pub(crate) async fn verify_web_tooling(&self, rustup: RustupShow) -> Result<()> {
-        // Rust wasm32 target
-        if !rustup.has_wasm32_unknown_unknown() {
+    pub(crate) async fn verify_web_tooling(&self, rustc: RustcDetails) -> Result<()> {
+        // Install target using rustup.
+        #[cfg(not(feature = "no-downloads"))]
+        if !rustc.has_wasm32_unknown_unknown() {
             tracing::info!(
                 "Web platform requires wasm32-unknown-unknown to be installed. Installing..."
             );
-            let _ = Command::new("rustup")
+
+            let _ = tokio::process::Command::new("rustup")
                 .args(["target", "add", "wasm32-unknown-unknown"])
                 .output()
                 .await?;
         }
 
+        // Ensure target is installed.
+        if !rustc.has_wasm32_unknown_unknown() {
+            return Err(Error::Other(anyhow!(
+                "Missing target wasm32-unknown-unknown."
+            )));
+        }
+
         // Wasm bindgen
         let krate_bindgen_version = self.krate.wasm_bindgen_version().ok_or(anyhow!(
             "failed to detect wasm-bindgen version, unable to proceed"
         ))?;
 
-        let is_installed = WasmBindgen::verify_install(&krate_bindgen_version).await?;
-        if !is_installed {
-            WasmBindgen::install(&krate_bindgen_version)
-                .await
-                .context("failed to install wasm-bindgen-cli")?;
-        }
+        WasmBindgen::verify_install(&krate_bindgen_version).await?;
 
         Ok(())
     }
@@ -71,7 +74,7 @@ impl BuildRequest {
     /// We don't auto-install these yet since we're not doing an architecture check. We assume most users
     /// are running on an Apple Silicon Mac, but it would be confusing if we installed these when we actually
     /// should be installing the x86 versions.
-    pub(crate) async fn verify_ios_tooling(&self, _rustup: RustupShow) -> Result<()> {
+    pub(crate) async fn verify_ios_tooling(&self, _rustc: RustcDetails) -> Result<()> {
         // open the simulator
         // _ = tokio::process::Command::new("open")
         //     .arg("/Applications/Xcode.app/Contents/Developer/Applications/Simulator.app")
@@ -112,7 +115,7 @@ impl BuildRequest {
     ///
     /// will do its best to fill in the missing bits by exploring the sdk structure
     /// IE will attempt to use the Java installed from android studio if possible.
-    pub(crate) async fn verify_android_tooling(&self, _rustup: RustupShow) -> Result<()> {
+    pub(crate) async fn verify_android_tooling(&self, _rustc: RustcDetails) -> Result<()> {
         let result = self
             .krate
             .android_ndk()
@@ -134,7 +137,7 @@ impl BuildRequest {
     ///
     /// Eventually, we want to check for the prereqs for wry/tao as outlined by tauri:
     ///     https://tauri.app/start/prerequisites/
-    pub(crate) async fn verify_linux_tooling(&self, _rustup: RustupShow) -> Result<()> {
+    pub(crate) async fn verify_linux_tooling(&self, _rustc: RustcDetails) -> Result<()> {
         Ok(())
     }
 }

+ 1 - 1
packages/cli/src/dioxus_crate.rs

@@ -17,7 +17,7 @@ pub(crate) struct DioxusCrate {
     pub(crate) package: NodeId,
     pub(crate) config: DioxusConfig,
     pub(crate) target: Target,
-    pub(crate) settings: CliSettings,
+    pub(crate) settings: Arc<CliSettings>,
 }
 
 pub(crate) static PROFILE_WASM: &str = "wasm-dev";

+ 2 - 2
packages/cli/src/main.rs

@@ -15,7 +15,7 @@ mod filemap;
 mod logging;
 mod metadata;
 mod platform;
-mod rustup;
+mod rustc;
 mod serve;
 mod settings;
 mod wasm_bindgen;
@@ -29,7 +29,7 @@ pub(crate) use error::*;
 pub(crate) use filemap::*;
 pub(crate) use logging::*;
 pub(crate) use platform::*;
-pub(crate) use rustup::*;
+pub(crate) use rustc::*;
 pub(crate) use settings::*;
 
 #[tokio::main]

+ 31 - 0
packages/cli/src/rustc.rs

@@ -0,0 +1,31 @@
+use crate::Result;
+use anyhow::Context;
+use std::path::PathBuf;
+use tokio::process::Command;
+
+#[derive(Debug, Default)]
+pub struct RustcDetails {
+    pub sysroot: PathBuf,
+}
+
+impl RustcDetails {
+    /// Find the current sysroot location using the CLI
+    pub async fn from_cli() -> Result<RustcDetails> {
+        let output = Command::new("rustc")
+            .args(["--print", "sysroot"])
+            .output()
+            .await?;
+
+        let stdout =
+            String::from_utf8(output.stdout).context("Failed to extract rustc sysroot output")?;
+
+        let sysroot = PathBuf::from(stdout.trim());
+        Ok(Self { sysroot })
+    }
+
+    pub fn has_wasm32_unknown_unknown(&self) -> bool {
+        self.sysroot
+            .join("lib/rustlib/wasm32-unknown-unknown")
+            .exists()
+    }
+}

+ 0 - 170
packages/cli/src/rustup.rs

@@ -1,170 +0,0 @@
-use crate::Result;
-use anyhow::Context;
-use std::path::PathBuf;
-use tokio::process::Command;
-
-#[derive(Debug, Default)]
-pub struct RustupShow {
-    pub default_host: String,
-    pub rustup_home: PathBuf,
-    pub installed_toolchains: Vec<String>,
-    pub installed_targets: Vec<String>,
-    pub active_rustc: String,
-    pub active_toolchain: String,
-}
-impl RustupShow {
-    /// Collect the output of `rustup show` and parse it
-    pub async fn from_cli() -> Result<RustupShow> {
-        let output = Command::new("rustup").args(["show"]).output().await?;
-        let stdout =
-            String::from_utf8(output.stdout).context("Failed to parse rustup show output")?;
-
-        Ok(RustupShow::from_stdout(stdout))
-    }
-
-    /// Parse the output of `rustup show`
-    pub fn from_stdout(output: String) -> RustupShow {
-        // I apologize for this hand-rolled parser
-
-        let mut result = RustupShow::default();
-        let mut current_section = "";
-
-        for line in output.lines() {
-            let line = line.trim();
-            if line.is_empty() {
-                continue;
-            }
-
-            if line.starts_with("Default host: ") {
-                result.default_host = line.strip_prefix("Default host: ").unwrap().to_string();
-            } else if line.starts_with("rustup home: ") {
-                result.rustup_home =
-                    PathBuf::from(line.strip_prefix("rustup home: ").unwrap().trim());
-            } else if line == "installed toolchains" {
-                current_section = "toolchains";
-            } else if line == "installed targets for active toolchain" {
-                current_section = "targets";
-            } else if line == "active toolchain" {
-                current_section = "active_toolchain";
-            } else {
-                if line.starts_with("---") || line.is_empty() {
-                    continue;
-                }
-                match current_section {
-                    "toolchains" => result
-                        .installed_toolchains
-                        .push(line.trim_end_matches(" (default)").to_string()),
-                    "targets" => result.installed_targets.push(line.to_string()),
-                    "active_toolchain" => {
-                        if result.active_toolchain.is_empty() {
-                            result.active_toolchain = line.to_string();
-                        } else if line.starts_with("rustc ") {
-                            result.active_rustc = line.to_string();
-                        }
-                    }
-                    _ => {}
-                }
-            }
-        }
-
-        result
-    }
-
-    pub fn has_wasm32_unknown_unknown(&self) -> bool {
-        self.installed_targets
-            .contains(&"wasm32-unknown-unknown".to_string())
-    }
-}
-
-#[test]
-fn parses_rustup_show() {
-    let output = r#"
-Default host: aarch64-apple-darwin
-rustup home:  /Users/jonkelley/.rustup
-
-installed toolchains
---------------------
-
-stable-aarch64-apple-darwin (default)
-nightly-2021-07-06-aarch64-apple-darwin
-nightly-2021-09-24-aarch64-apple-darwin
-nightly-2022-03-10-aarch64-apple-darwin
-nightly-2023-03-18-aarch64-apple-darwin
-nightly-2024-01-11-aarch64-apple-darwin
-nightly-aarch64-apple-darwin
-1.58.1-aarch64-apple-darwin
-1.60.0-aarch64-apple-darwin
-1.68.2-aarch64-apple-darwin
-1.69.0-aarch64-apple-darwin
-1.71.1-aarch64-apple-darwin
-1.72.1-aarch64-apple-darwin
-1.73.0-aarch64-apple-darwin
-1.74.1-aarch64-apple-darwin
-1.77.2-aarch64-apple-darwin
-1.78.0-aarch64-apple-darwin
-1.79.0-aarch64-apple-darwin
-1.49-aarch64-apple-darwin
-1.55-aarch64-apple-darwin
-1.56-aarch64-apple-darwin
-1.57-aarch64-apple-darwin
-1.66-aarch64-apple-darwin
-1.69-aarch64-apple-darwin
-1.70-aarch64-apple-darwin
-1.74-aarch64-apple-darwin
-
-installed targets for active toolchain
---------------------------------------
-
-aarch64-apple-darwin
-aarch64-apple-ios
-aarch64-apple-ios-sim
-aarch64-linux-android
-aarch64-unknown-linux-gnu
-armv7-linux-androideabi
-i686-linux-android
-thumbv6m-none-eabi
-thumbv7em-none-eabihf
-wasm32-unknown-unknown
-x86_64-apple-darwin
-x86_64-apple-ios
-x86_64-linux-android
-x86_64-pc-windows-msvc
-x86_64-unknown-linux-gnu
-
-active toolchain
-----------------
-
-stable-aarch64-apple-darwin (default)
-rustc 1.79.0 (129f3b996 2024-06-10)
-"#;
-    let show = RustupShow::from_stdout(output.to_string());
-    assert_eq!(show.default_host, "aarch64-apple-darwin");
-    assert_eq!(show.rustup_home, PathBuf::from("/Users/jonkelley/.rustup"));
-    assert_eq!(
-        show.active_toolchain,
-        "stable-aarch64-apple-darwin (default)"
-    );
-    assert_eq!(show.active_rustc, "rustc 1.79.0 (129f3b996 2024-06-10)");
-    assert_eq!(show.installed_toolchains.len(), 26);
-    assert_eq!(show.installed_targets.len(), 15);
-    assert_eq!(
-        show.installed_targets,
-        vec![
-            "aarch64-apple-darwin".to_string(),
-            "aarch64-apple-ios".to_string(),
-            "aarch64-apple-ios-sim".to_string(),
-            "aarch64-linux-android".to_string(),
-            "aarch64-unknown-linux-gnu".to_string(),
-            "armv7-linux-androideabi".to_string(),
-            "i686-linux-android".to_string(),
-            "thumbv6m-none-eabi".to_string(),
-            "thumbv7em-none-eabihf".to_string(),
-            "wasm32-unknown-unknown".to_string(),
-            "x86_64-apple-darwin".to_string(),
-            "x86_64-apple-ios".to_string(),
-            "x86_64-linux-android".to_string(),
-            "x86_64-pc-windows-msvc".to_string(),
-            "x86_64-unknown-linux-gnu".to_string(),
-        ]
-    )
-}

+ 27 - 8
packages/cli/src/settings.rs

@@ -1,6 +1,7 @@
 use crate::{Result, TraceSrc};
+use once_cell::sync::Lazy;
 use serde::{Deserialize, Serialize};
-use std::{fs, path::PathBuf};
+use std::{fs, path::PathBuf, sync::Arc};
 use tracing::{error, trace, warn};
 
 const GLOBAL_SETTINGS_FILE_NAME: &str = "dioxus/settings.toml";
@@ -23,12 +24,16 @@ pub(crate) struct CliSettings {
     /// Describes the interval in seconds that the CLI should poll for file changes on WSL.
     #[serde(default = "default_wsl_file_poll_interval")]
     pub(crate) wsl_file_poll_interval: Option<u16>,
+    /// Use tooling from path rather than downloading them.
+    pub(crate) no_downloads: Option<bool>,
 }
 
 impl CliSettings {
     /// Load the settings from the local, global, or default config in that order
-    pub(crate) fn load() -> Self {
-        Self::from_global().unwrap_or_default()
+    pub(crate) fn load() -> Arc<Self> {
+        static SETTINGS: Lazy<Arc<CliSettings>> =
+            Lazy::new(|| Arc::new(CliSettings::from_global().unwrap_or_default()));
+        SETTINGS.clone()
     }
 
     /// Get the current settings structure from global.
@@ -58,7 +63,7 @@ impl CliSettings {
 
     /// Save the current structure to the global settings toml.
     /// This does not save to project-level settings.
-    pub(crate) fn save(self) -> Result<Self> {
+    pub(crate) fn save(&self) -> Result<()> {
         let path = Self::get_settings_path().ok_or_else(|| {
             error!(dx_src = ?TraceSrc::Dev, "failed to get settings path");
             anyhow::anyhow!("failed to get settings path")
@@ -90,7 +95,7 @@ impl CliSettings {
             return Err(anyhow::anyhow!("failed to save global cli settings: {e}").into());
         }
 
-        Ok(self)
+        Ok(())
     }
 
     /// Get the path to the settings toml file.
@@ -103,14 +108,28 @@ impl CliSettings {
         Some(path.join(GLOBAL_SETTINGS_FILE_NAME))
     }
 
-    /// Modify the settings toml file
+    /// Modify the settings toml file - doesn't change the settings for this session
     pub(crate) fn modify_settings(with: impl FnOnce(&mut CliSettings)) -> Result<()> {
-        let mut settings = Self::load();
-        with(&mut settings);
+        let mut _settings = CliSettings::load();
+        let settings: &mut CliSettings = Arc::make_mut(&mut _settings);
+        with(settings);
         settings.save()?;
 
         Ok(())
     }
+
+    /// Check if we should prefer to use the no-downloads feature
+    pub(crate) fn prefer_no_downloads() -> bool {
+        if cfg!(feature = "no-downloads") {
+            return true;
+        }
+
+        if std::env::var("NO_DOWNLOADS").is_ok() {
+            return true;
+        }
+
+        CliSettings::load().no_downloads.unwrap_or_default()
+    }
 }
 
 fn default_wsl_file_poll_interval() -> Option<u16> {

+ 240 - 202
packages/cli/src/wasm_bindgen.rs

@@ -1,9 +1,8 @@
+use crate::{CliSettings, Result};
 use anyhow::{anyhow, Context};
 use flate2::read::GzDecoder;
-use std::{
-    path::{Path, PathBuf},
-    process::Stdio,
-};
+use std::path::PathBuf;
+use std::{path::Path, process::Stdio};
 use tar::Archive;
 use tempfile::TempDir;
 use tokio::{fs, process::Command};
@@ -22,8 +21,78 @@ pub(crate) struct WasmBindgen {
 }
 
 impl WasmBindgen {
-    pub async fn run(&self) -> anyhow::Result<()> {
-        let binary = Self::final_binary(&self.version).await?;
+    pub fn new(version: &str) -> Self {
+        Self {
+            version: version.to_string(),
+            input_path: PathBuf::new(),
+            out_dir: PathBuf::new(),
+            out_name: String::new(),
+            target: String::new(),
+            debug: true,
+            keep_debug: true,
+            demangle: true,
+            remove_name_section: false,
+            remove_producers_section: false,
+        }
+    }
+
+    pub fn input_path(self, input_path: &Path) -> Self {
+        Self {
+            input_path: input_path.to_path_buf(),
+            ..self
+        }
+    }
+
+    pub fn out_dir(self, out_dir: &Path) -> Self {
+        Self {
+            out_dir: out_dir.to_path_buf(),
+            ..self
+        }
+    }
+
+    pub fn out_name(self, out_name: &str) -> Self {
+        Self {
+            out_name: out_name.to_string(),
+            ..self
+        }
+    }
+
+    pub fn target(self, target: &str) -> Self {
+        Self {
+            target: target.to_string(),
+            ..self
+        }
+    }
+
+    pub fn debug(self, debug: bool) -> Self {
+        Self { debug, ..self }
+    }
+
+    pub fn keep_debug(self, keep_debug: bool) -> Self {
+        Self { keep_debug, ..self }
+    }
+
+    pub fn demangle(self, demangle: bool) -> Self {
+        Self { demangle, ..self }
+    }
+
+    pub fn remove_name_section(self, remove_name_section: bool) -> Self {
+        Self {
+            remove_name_section,
+            ..self
+        }
+    }
+
+    pub fn remove_producers_section(self, remove_producers_section: bool) -> Self {
+        Self {
+            remove_producers_section,
+            ..self
+        }
+    }
+
+    /// Run the bindgen command with the current settings
+    pub async fn run(&self) -> Result<()> {
+        let binary = self.get_binary_path().await?;
 
         let mut args = Vec::new();
 
@@ -52,13 +121,13 @@ impl WasmBindgen {
             args.push("--remove-producers-section");
         }
 
-        // wbg generates typescript bindnings by default - we don't want those
-        args.push("--no-typescript");
-
         // Out name
         args.push("--out-name");
         args.push(&self.out_name);
 
+        // wbg generates typescript bindnings by default - we don't want those
+        args.push("--no-typescript");
+
         // Out dir
         let out_dir = self
             .out_dir
@@ -86,11 +155,17 @@ impl WasmBindgen {
         Ok(())
     }
 
-    /// Verify that the required wasm-bindgen version is installed.
-    pub async fn verify_install(version: &str) -> anyhow::Result<bool> {
-        let binary_name = Self::installed_bin_name(version);
-        let path = Self::install_dir().await?.join(binary_name);
-        Ok(path.exists())
+    /// Verify the installed version of wasm-bindgen-cli
+    ///
+    /// For local installations, this will check that the installed version matches the specified version.
+    /// For managed installations, this will check that the version managed by `dx` is the specified version.
+    pub async fn verify_install(version: &str) -> anyhow::Result<()> {
+        let settings = Self::new(version);
+        if CliSettings::prefer_no_downloads() {
+            settings.verify_local_install().await
+        } else {
+            settings.verify_managed_install().await
+        }
     }
 
     /// Install the specified wasm-bingen version.
@@ -101,47 +176,60 @@ impl WasmBindgen {
     /// 1. Direct GitHub release download.
     /// 2. `cargo binstall` if installed.
     /// 3. Compile from source with `cargo install`.
-    pub async fn install(version: &str) -> anyhow::Result<()> {
-        tracing::info!("Installing wasm-bindgen-cli@{version}...");
+    async fn install(&self) -> anyhow::Result<()> {
+        tracing::info!("Installing wasm-bindgen-cli@{}...", self.version);
 
         // Attempt installation from GitHub
-        if let Err(e) = Self::install_github(version).await {
-            tracing::error!("Failed to install wasm-bindgen-cli@{version}: {e}");
+        if let Err(e) = self.install_github().await {
+            tracing::error!("Failed to install wasm-bindgen-cli@{}: {e}", self.version);
         } else {
-            tracing::info!("wasm-bindgen-cli@{version} was successfully installed from GitHub.");
+            tracing::info!(
+                "wasm-bindgen-cli@{} was successfully installed from GitHub.",
+                self.version
+            );
             return Ok(());
         }
 
         // Attempt installation from binstall.
-        if let Err(e) = Self::install_binstall(version).await {
-            tracing::error!("Failed to install wasm-bindgen-cli@{version}: {e}");
-            tracing::info!("Failed to install prebuilt binary for wasm-bindgen-cli@{version}. Compiling from source instead. This may take a while.");
+        if let Err(e) = self.install_binstall().await {
+            tracing::error!("Failed to install wasm-bindgen-cli@{}: {e}", self.version);
+            tracing::info!("Failed to install prebuilt binary for wasm-bindgen-cli@{}. Compiling from source instead. This may take a while.", self.version);
         } else {
             tracing::info!(
-                "wasm-bindgen-cli@{version} was successfully installed from cargo-binstall."
+                "wasm-bindgen-cli@{} was successfully installed from cargo-binstall.",
+                self.version
             );
             return Ok(());
         }
 
         // Attempt installation from cargo.
-        Self::install_cargo(version)
+        self.install_cargo()
             .await
             .context("failed to install wasm-bindgen-cli from cargo")?;
 
-        tracing::info!("wasm-bindgen-cli@{version} was successfully installed from source.");
+        tracing::info!(
+            "wasm-bindgen-cli@{} was successfully installed from source.",
+            self.version
+        );
 
         Ok(())
     }
 
-    /// Try installing wasm-bindgen-cli from GitHub.
-    async fn install_github(version: &str) -> anyhow::Result<()> {
-        tracing::debug!("Attempting to install wasm-bindgen-cli@{version} from GitHub");
+    async fn install_github(&self) -> anyhow::Result<()> {
+        tracing::debug!(
+            "Attempting to install wasm-bindgen-cli@{} from GitHub",
+            self.version
+        );
 
-        let url = git_install_url(version)
-            .ok_or_else(|| anyhow!("no available GitHub binary for wasm-bindgen-cli@{version}"))?;
+        let url = self.git_install_url().ok_or_else(|| {
+            anyhow!(
+                "no available GitHub binary for wasm-bindgen-cli@{}",
+                self.version
+            )
+        })?;
 
         // Get the final binary location.
-        let final_binary = Self::final_binary(version).await?;
+        let binary_path = self.get_binary_path().await?;
 
         // Download then extract wasm-bindgen-cli.
         let bytes = reqwest::get(url).await?.bytes().await?;
@@ -154,22 +242,24 @@ impl WasmBindgen {
                     .as_ref()
                     .map(|e| {
                         e.path_bytes()
-                            .ends_with(Self::downloaded_bin_name().as_bytes())
+                            .ends_with(self.downloaded_bin_name().as_bytes())
                     })
                     .unwrap_or(false)
             })
             .context("Failed to find entry")??
-            .unpack(&final_binary)
+            .unpack(&binary_path)
             .context("failed to unpack wasm-bindgen-cli binary")?;
 
         Ok(())
     }
 
-    /// Try installing wasm-bindgen-cli through cargo-binstall.
-    async fn install_binstall(version: &str) -> anyhow::Result<()> {
-        tracing::debug!("Attempting to install wasm-bindgen-cli@{version} from cargo-binstall");
+    async fn install_binstall(&self) -> anyhow::Result<()> {
+        tracing::debug!(
+            "Attempting to install wasm-bindgen-cli@{} from cargo-binstall",
+            self.version
+        );
 
-        let package = Self::cargo_bin_name(version);
+        let package = self.cargo_bin_name();
         let tempdir = TempDir::new()?;
 
         // Run install command
@@ -183,24 +273,26 @@ impl WasmBindgen {
                 "--install-path",
             ])
             .arg(tempdir.path())
-            .stdout(Stdio::piped())
-            .stderr(Stdio::piped())
+            .stdout(std::process::Stdio::piped())
+            .stderr(std::process::Stdio::piped())
             .output()
             .await?;
 
         fs::copy(
-            tempdir.path().join(Self::downloaded_bin_name()),
-            Self::final_binary(version).await?,
+            tempdir.path().join(self.downloaded_bin_name()),
+            self.get_binary_path().await?,
         )
         .await?;
 
         Ok(())
     }
 
-    /// Try installing wasm-bindgen-cli from source using cargo install.
-    async fn install_cargo(version: &str) -> anyhow::Result<()> {
-        tracing::debug!("Attempting to install wasm-bindgen-cli@{version} from cargo-install");
-        let package = Self::cargo_bin_name(version);
+    async fn install_cargo(&self) -> anyhow::Result<()> {
+        tracing::debug!(
+            "Attempting to install wasm-bindgen-cli@{} from cargo-install",
+            self.version
+        );
+        let package = self.cargo_bin_name();
         let tempdir = TempDir::new()?;
 
         // Run install command
@@ -215,8 +307,8 @@ impl WasmBindgen {
                 "--root",
             ])
             .arg(tempdir.path())
-            .stdout(Stdio::piped())
-            .stderr(Stdio::piped())
+            .stdout(std::process::Stdio::piped())
+            .stderr(std::process::Stdio::piped())
             .output()
             .await
             .context("failed to install wasm-bindgen-cli from cargo-install")?;
@@ -225,8 +317,8 @@ impl WasmBindgen {
 
         // copy the wasm-bindgen out of the tempdir to the final location
         fs::copy(
-            tempdir.path().join("bin").join(Self::downloaded_bin_name()),
-            Self::final_binary(version).await?,
+            tempdir.path().join("bin").join(self.downloaded_bin_name()),
+            self.get_binary_path().await?,
         )
         .await
         .context("failed to copy wasm-bindgen binary")?;
@@ -234,162 +326,109 @@ impl WasmBindgen {
         Ok(())
     }
 
-    /// Get the installation directory for the wasm-bindgen executable.
-    async fn install_dir() -> anyhow::Result<PathBuf> {
-        let bindgen_dir = dirs::data_local_dir()
-            .expect("user should be running on a compatible operating system")
-            .join("dioxus/wasm-bindgen/");
-
-        fs::create_dir_all(&bindgen_dir).await?;
-
-        Ok(bindgen_dir)
-    }
+    async fn verify_local_install(&self) -> anyhow::Result<()> {
+        tracing::info!(
+            "Verifying wasm-bindgen-cli@{} is installed in the path",
+            self.version
+        );
 
-    /// Get the name of a potentially installed wasm-bindgen binary.
-    fn installed_bin_name(version: &str) -> String {
-        let mut name = format!("wasm-bindgen-{version}");
-        if cfg!(windows) {
-            name = format!("{name}.exe");
+        let binary = self.get_binary_path().await?;
+        let output = Command::new(binary)
+            .args(["--version"])
+            .output()
+            .await
+            .context("Failed to check wasm-bindgen-cli version")?;
+
+        let stdout = String::from_utf8(output.stdout)
+            .context("Failed to extract wasm-bindgen-cli output")?;
+
+        let installed_version = stdout.trim_start_matches("wasm-bindgen").trim();
+        if installed_version != self.version {
+            return Err(anyhow!(
+                "Incorrect wasm-bindgen-cli version: project requires version {} but version {} is installed",
+                self.version,
+                installed_version,
+            ));
         }
-        name
-    }
-
-    /// Get the crates.io package name of wasm-bindgen-cli.
-    fn cargo_bin_name(version: &str) -> String {
-        format!("wasm-bindgen-cli@{version}")
-    }
 
-    async fn final_binary(version: &str) -> Result<PathBuf, anyhow::Error> {
-        let installed_name = Self::installed_bin_name(version);
-        let install_dir = Self::install_dir().await?;
-        Ok(install_dir.join(installed_name))
-    }
-
-    fn downloaded_bin_name() -> &'static str {
-        if cfg!(windows) {
-            "wasm-bindgen.exe"
-        } else {
-            "wasm-bindgen"
-        }
+        Ok(())
     }
-}
 
-/// Get the GitHub installation URL for wasm-bindgen if it exists.
-fn git_install_url(version: &str) -> Option<String> {
-    let platform = if cfg!(all(target_os = "windows", target_arch = "x86_64")) {
-        "x86_64-pc-windows-msvc"
-    } else if cfg!(all(target_os = "linux", target_arch = "x86_64")) {
-        "x86_64-unknown-linux-musl"
-    } else if cfg!(all(target_os = "linux", target_arch = "aarch64")) {
-        "aarch64-unknown-linux-gnu"
-    } else if cfg!(all(target_os = "macos", target_arch = "x86_64")) {
-        "x86_64-apple-darwin"
-    } else if cfg!(all(target_os = "macos", target_arch = "aarch64")) {
-        "aarch64-apple-darwin"
-    } else {
-        return None;
-    };
-
-    Some(format!("https://github.com/rustwasm/wasm-bindgen/releases/download/{version}/wasm-bindgen-{version}-{platform}.tar.gz"))
-}
+    async fn verify_managed_install(&self) -> anyhow::Result<()> {
+        tracing::info!(
+            "Verifying wasm-bindgen-cli@{} is installed in the tool directory",
+            self.version
+        );
 
-/// A builder for WasmBindgen options.
-pub(crate) struct WasmBindgenBuilder {
-    version: String,
-    input_path: PathBuf,
-    out_dir: PathBuf,
-    out_name: String,
-    target: String,
-    debug: bool,
-    keep_debug: bool,
-    demangle: bool,
-    remove_name_section: bool,
-    remove_producers_section: bool,
-}
+        let binary_name = self.installed_bin_name();
+        let path = self.install_dir().await?.join(binary_name);
 
-impl WasmBindgenBuilder {
-    pub fn new(version: String) -> Self {
-        Self {
-            version,
-            input_path: PathBuf::new(),
-            out_dir: PathBuf::new(),
-            out_name: String::new(),
-            target: String::new(),
-            debug: true,
-            keep_debug: true,
-            demangle: true,
-            remove_name_section: false,
-            remove_producers_section: false,
+        if !path.exists() {
+            self.install().await?;
         }
-    }
 
-    pub fn build(self) -> WasmBindgen {
-        WasmBindgen {
-            version: self.version,
-            input_path: self.input_path,
-            out_dir: self.out_dir,
-            out_name: self.out_name,
-            target: self.target,
-            debug: self.debug,
-            keep_debug: self.keep_debug,
-            demangle: self.demangle,
-            remove_name_section: self.remove_name_section,
-            remove_producers_section: self.remove_producers_section,
-        }
+        Ok(())
     }
 
-    pub fn input_path(self, input_path: &Path) -> Self {
-        Self {
-            input_path: input_path.to_path_buf(),
-            ..self
+    async fn get_binary_path(&self) -> anyhow::Result<PathBuf> {
+        if CliSettings::prefer_no_downloads() {
+            which::which("wasm-bindgen")
+                .map_err(|_| anyhow!("Missing wasm-bindgen-cli@{}", self.version))
+        } else {
+            let installed_name = self.installed_bin_name();
+            let install_dir = self.install_dir().await?;
+            Ok(install_dir.join(installed_name))
         }
     }
 
-    pub fn out_dir(self, out_dir: &Path) -> Self {
-        Self {
-            out_dir: out_dir.to_path_buf(),
-            ..self
-        }
-    }
+    async fn install_dir(&self) -> anyhow::Result<PathBuf> {
+        let bindgen_dir = dirs::data_local_dir()
+            .expect("user should be running on a compatible operating system")
+            .join("dioxus/wasm-bindgen/");
 
-    pub fn out_name(self, out_name: &str) -> Self {
-        Self {
-            out_name: out_name.to_string(),
-            ..self
-        }
+        fs::create_dir_all(&bindgen_dir).await?;
+        Ok(bindgen_dir)
     }
 
-    pub fn target(self, target: &str) -> Self {
-        Self {
-            target: target.to_string(),
-            ..self
+    fn installed_bin_name(&self) -> String {
+        let mut name = format!("wasm-bindgen-{}", self.version);
+        if cfg!(windows) {
+            name = format!("{name}.exe");
         }
+        name
     }
 
-    pub fn debug(self, debug: bool) -> Self {
-        Self { debug, ..self }
-    }
-
-    pub fn keep_debug(self, keep_debug: bool) -> Self {
-        Self { keep_debug, ..self }
-    }
-
-    pub fn demangle(self, demangle: bool) -> Self {
-        Self { demangle, ..self }
+    fn cargo_bin_name(&self) -> String {
+        format!("wasm-bindgen-cli@{}", self.version)
     }
 
-    pub fn remove_name_section(self, remove_name_section: bool) -> Self {
-        Self {
-            remove_name_section,
-            ..self
+    fn downloaded_bin_name(&self) -> &'static str {
+        if cfg!(windows) {
+            "wasm-bindgen.exe"
+        } else {
+            "wasm-bindgen"
         }
     }
 
-    pub fn remove_producers_section(self, remove_producers_section: bool) -> Self {
-        Self {
-            remove_producers_section,
-            ..self
-        }
+    fn git_install_url(&self) -> Option<String> {
+        let platform = if cfg!(all(target_os = "windows", target_arch = "x86_64")) {
+            "x86_64-pc-windows-msvc"
+        } else if cfg!(all(target_os = "linux", target_arch = "x86_64")) {
+            "x86_64-unknown-linux-musl"
+        } else if cfg!(all(target_os = "linux", target_arch = "aarch64")) {
+            "aarch64-unknown-linux-gnu"
+        } else if cfg!(all(target_os = "macos", target_arch = "x86_64")) {
+            "x86_64-apple-darwin"
+        } else if cfg!(all(target_os = "macos", target_arch = "aarch64")) {
+            "aarch64-apple-darwin"
+        } else {
+            return None;
+        };
+
+        Some(format!(
+            "https://github.com/rustwasm/wasm-bindgen/releases/download/{}/wasm-bindgen-{}-{}.tar.gz",
+            self.version, self.version, platform
+        ))
     }
 }
 
@@ -401,57 +440,56 @@ mod test {
     /// Test the github installer.
     #[tokio::test]
     async fn test_github_install() {
+        let binary = WasmBindgen::new(VERSION);
         reset_test().await;
-        WasmBindgen::install_github(VERSION).await.unwrap();
+        binary.install_github().await.unwrap();
         test_verify_install().await;
-        verify_installation().await;
+        verify_installation(&binary).await;
     }
 
     /// Test the cargo installer.
     #[tokio::test]
     async fn test_cargo_install() {
+        let binary = WasmBindgen::new(VERSION);
         reset_test().await;
-        WasmBindgen::install_cargo(VERSION).await.unwrap();
+        binary.install_cargo().await.unwrap();
         test_verify_install().await;
-        verify_installation().await;
+        verify_installation(&binary).await;
     }
 
     // CI doesn't have binstall.
     // Test the binstall installer
     // #[tokio::test]
     // async fn test_binstall_install() {
+    //     let binary = WasmBindgen::new(VERSION);
     //     reset_test().await;
-    //     WasmBindgen::install_binstall(VERSION).await.unwrap();
+    //     binary.install_binstall().await.unwrap();
     //     test_verify_install().await;
-    //     verify_installation().await;
+    //     verify_installation(&binary).await;
     // }
 
-    /// Helper to test `WasmBindgen::verify_install` after an installation.
+    /// Helper to test `verify_install` after an installation.
     async fn test_verify_install() {
-        // Test install verification
-        let is_installed = WasmBindgen::verify_install(VERSION).await.unwrap();
-        assert!(
-            is_installed,
-            "wasm-bingen install verification returned false after installation"
-        );
+        WasmBindgen::verify_install(VERSION).await.unwrap();
     }
 
     /// Helper to test that the installed binary actually exists.
-    async fn verify_installation() {
-        let path = WasmBindgen::install_dir().await.unwrap();
-        let name = WasmBindgen::installed_bin_name(VERSION);
-        let binary = path.join(name);
+    async fn verify_installation(binary: &WasmBindgen) {
+        let path = binary.install_dir().await.unwrap();
+        let name = binary.installed_bin_name();
+        let binary_path = path.join(name);
         assert!(
-            binary.exists(),
+            binary_path.exists(),
             "wasm-bindgen binary doesn't exist after installation"
         );
     }
 
     /// Delete the installed binary. The temp folder should be automatically deleted.
     async fn reset_test() {
-        let path = WasmBindgen::install_dir().await.unwrap();
-        let name = WasmBindgen::installed_bin_name(VERSION);
-        let binary = path.join(name);
-        let _ = fs::remove_file(binary).await;
+        let binary = WasmBindgen::new(VERSION);
+        let path = binary.install_dir().await.unwrap();
+        let name = binary.installed_bin_name();
+        let binary_path = path.join(name);
+        let _ = tokio::fs::remove_file(binary_path).await;
     }
 }