diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index a6fd31d9ee..c5b9dedb0a 100644 --- a/.github/workflows/main.yml +++ b/.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 diff --git a/Cargo.lock b/Cargo.lock index 1721b4a37c..8d5f540148 100644 --- a/Cargo.lock +++ b/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" diff --git a/packages/cli/Cargo.toml b/packages/cli/Cargo.toml index 89235aa48a..e980168176 100644 --- a/packages/cli/Cargo.toml +++ b/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. diff --git a/packages/cli/src/build/bundle.rs b/packages/cli/src/build/bundle.rs index 85d9be6e98..21373fb575 100644 --- a/packages/cli/src/build/bundle.rs +++ b/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")?; diff --git a/packages/cli/src/build/verify.rs b/packages/cli/src/build/verify.rs index 8488526d60..c3724ee8be 100644 --- a/packages/cli/src/build/verify.rs +++ b/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(()) } } diff --git a/packages/cli/src/dioxus_crate.rs b/packages/cli/src/dioxus_crate.rs index eeaabf1778..3ac2eae10e 100644 --- a/packages/cli/src/dioxus_crate.rs +++ b/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, } pub(crate) static PROFILE_WASM: &str = "wasm-dev"; diff --git a/packages/cli/src/main.rs b/packages/cli/src/main.rs index 55f7005336..02cb5ae7a8 100644 --- a/packages/cli/src/main.rs +++ b/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] diff --git a/packages/cli/src/rustc.rs b/packages/cli/src/rustc.rs new file mode 100644 index 0000000000..ecde3a003b --- /dev/null +++ b/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 { + 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() + } +} diff --git a/packages/cli/src/rustup.rs b/packages/cli/src/rustup.rs deleted file mode 100644 index 380a20da99..0000000000 --- a/packages/cli/src/rustup.rs +++ /dev/null @@ -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, - pub installed_targets: Vec, - 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 { - 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(), - ] - ) -} diff --git a/packages/cli/src/settings.rs b/packages/cli/src/settings.rs index 7b91ed203f..1946619202 100644 --- a/packages/cli/src/settings.rs +++ b/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, + /// Use tooling from path rather than downloading them. + pub(crate) no_downloads: Option, } 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 { + static SETTINGS: Lazy> = + 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 { + 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 { diff --git a/packages/cli/src/wasm_bindgen.rs b/packages/cli/src/wasm_bindgen.rs index cd63e8b125..1a0c1fa7ba 100644 --- a/packages/cli/src/wasm_bindgen.rs +++ b/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 { - 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 { - 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 { - 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 { - 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 { + 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 { + 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 { + 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; } }