From bcde22f2b966df4a7c85a090624f92a2ea3658a6 Mon Sep 17 00:00:00 2001 From: Cathal Mullan Date: Sat, 28 Dec 2024 15:00:42 +0000 Subject: [PATCH] feat(cli): add feature to disable downloads --- .github/workflows/main.yml | 2 +- Cargo.lock | 27 +- packages/cli/Cargo.toml | 4 + packages/cli/src/build/bundle.rs | 2 +- packages/cli/src/build/verify.rs | 47 +- packages/cli/src/main.rs | 6 +- packages/cli/src/rustc.rs | 31 ++ packages/cli/src/rustup.rs | 170 ------- packages/cli/src/tools/mod.rs | 1 + .../cli/src/tools/wasm_bindgen/managed.rs | 306 ++++++++++++ packages/cli/src/tools/wasm_bindgen/mod.rs | 201 ++++++++ packages/cli/src/tools/wasm_bindgen/path.rs | 49 ++ packages/cli/src/wasm_bindgen.rs | 454 ------------------ 13 files changed, 649 insertions(+), 651 deletions(-) create mode 100644 packages/cli/src/rustc.rs delete mode 100644 packages/cli/src/rustup.rs create mode 100644 packages/cli/src/tools/mod.rs create mode 100644 packages/cli/src/tools/wasm_bindgen/managed.rs create mode 100644 packages/cli/src/tools/wasm_bindgen/mod.rs create mode 100644 packages/cli/src/tools/wasm_bindgen/path.rs delete mode 100644 packages/cli/src/wasm_bindgen.rs diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 8887f2d543..81129810dd 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 ac6e8d73ad..bce4f92ccd 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]] @@ -3530,6 +3530,7 @@ dependencies = [ "uuid", "walkdir", "wasm-opt", + "which 7.0.1", ] [[package]] @@ -4588,6 +4589,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" @@ -15239,6 +15246,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" @@ -15674,6 +15693,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 bd9751d8bc..d6823b87a3 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", optional = true } + # plugin packages open = "5.0.1" cargo-generate = "=0.21.3" @@ -128,6 +131,7 @@ default = [] plugin = [] tokio-console = ["dep:console-subscriber"] bundle = [] +no-downloads = ["dep:which"] # 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 c0e765595b..5f79ce09be 100644 --- a/packages/cli/src/build/bundle.rs +++ b/packages/cli/src/build/bundle.rs @@ -1,5 +1,5 @@ use super::templates::InfoPlistData; -use crate::wasm_bindgen::WasmBindgenBuilder; +use crate::tools::wasm_bindgen::WasmBindgenBuilder; use crate::{BuildRequest, Platform}; use crate::{Result, TraceSrc}; use anyhow::Context; diff --git a/packages/cli/src/build/verify.rs b/packages/cli/src/build/verify.rs index 8488526d60..4ea7173429 100644 --- a/packages/cli/src/build/verify.rs +++ b/packages/cli/src/build/verify.rs @@ -1,9 +1,10 @@ -use crate::{wasm_bindgen::WasmBindgen, BuildRequest, Platform, Result, RustupShow}; +use crate::{ + tools::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 +16,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 +25,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 +38,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 +76,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 +117,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 +139,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/main.rs b/packages/cli/src/main.rs index 3f92e9d0f5..94675a2f12 100644 --- a/packages/cli/src/main.rs +++ b/packages/cli/src/main.rs @@ -15,10 +15,10 @@ mod filemap; mod logging; mod metadata; mod platform; -mod rustup; +mod rustc; mod serve; mod settings; -mod wasm_bindgen; +mod tools; pub(crate) use build::*; pub(crate) use cli::*; @@ -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/tools/mod.rs b/packages/cli/src/tools/mod.rs new file mode 100644 index 0000000000..dc393ba3b2 --- /dev/null +++ b/packages/cli/src/tools/mod.rs @@ -0,0 +1 @@ +pub(crate) mod wasm_bindgen; diff --git a/packages/cli/src/tools/wasm_bindgen/managed.rs b/packages/cli/src/tools/wasm_bindgen/managed.rs new file mode 100644 index 0000000000..1904895615 --- /dev/null +++ b/packages/cli/src/tools/wasm_bindgen/managed.rs @@ -0,0 +1,306 @@ +use super::WasmBindgenBinary; +use anyhow::{anyhow, Context}; +use flate2::read::GzDecoder; +use std::path::PathBuf; +use tar::Archive; +use tempfile::TempDir; +use tokio::{fs, process::Command}; + +pub(super) struct ManagedBinary { + version: String, +} + +impl WasmBindgenBinary for ManagedBinary { + fn new(version: &str) -> Self { + Self { + version: version.to_string(), + } + } + + async fn verify_install(&self) -> anyhow::Result<()> { + tracing::info!( + "Verifying wasm-bindgen-cli@{} is installed in the tool directory", + self.version + ); + + let binary_name = self.installed_bin_name(); + let path = self.install_dir().await?.join(binary_name); + + if !path.exists() { + self.install().await?; + } + + Ok(()) + } + + async fn get_binary_path(&self) -> anyhow::Result { + let installed_name = self.installed_bin_name(); + let install_dir = self.install_dir().await?; + Ok(install_dir.join(installed_name)) + } +} + +impl ManagedBinary { + 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().await { + tracing::error!("Failed to install wasm-bindgen-cli@{}: {e}", self.version); + } else { + 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().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@{} was successfully installed from cargo-binstall.", + self.version + ); + return Ok(()); + } + + // Attempt installation from cargo. + self.install_cargo() + .await + .context("failed to install wasm-bindgen-cli from cargo")?; + + tracing::info!( + "wasm-bindgen-cli@{} was successfully installed from source.", + self.version + ); + + Ok(()) + } + + async fn install_github(&self) -> anyhow::Result<()> { + tracing::debug!( + "Attempting to install wasm-bindgen-cli@{} from GitHub", + self.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 binary_path = self.get_binary_path().await?; + + // Download then extract wasm-bindgen-cli. + let bytes = reqwest::get(url).await?.bytes().await?; + + // Unpack the first tar entry to the final binary location + Archive::new(GzDecoder::new(bytes.as_ref())) + .entries()? + .find(|entry| { + entry + .as_ref() + .map(|e| { + e.path_bytes() + .ends_with(self.downloaded_bin_name().as_bytes()) + }) + .unwrap_or(false) + }) + .context("Failed to find entry")?? + .unpack(&binary_path) + .context("failed to unpack wasm-bindgen-cli binary")?; + + Ok(()) + } + + 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(); + let tempdir = TempDir::new()?; + + // Run install command + Command::new("cargo") + .args([ + "binstall", + &package, + "--no-confirm", + "--force", + "--no-track", + "--install-path", + ]) + .arg(tempdir.path()) + .stdout(std::process::Stdio::piped()) + .stderr(std::process::Stdio::piped()) + .output() + .await?; + + fs::copy( + tempdir.path().join(self.downloaded_bin_name()), + self.get_binary_path().await?, + ) + .await?; + + Ok(()) + } + + 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 + Command::new("cargo") + .args([ + "install", + &package, + "--bin", + "wasm-bindgen", + "--no-track", + "--force", + "--root", + ]) + .arg(tempdir.path()) + .stdout(std::process::Stdio::piped()) + .stderr(std::process::Stdio::piped()) + .output() + .await + .context("failed to install wasm-bindgen-cli from cargo-install")?; + + tracing::info!("Copying into path: {}", tempdir.path().display()); + + // copy the wasm-bindgen out of the tempdir to the final location + fs::copy( + tempdir.path().join("bin").join(self.downloaded_bin_name()), + self.get_binary_path().await?, + ) + .await + .context("failed to copy wasm-bindgen binary")?; + + Ok(()) + } + + 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/"); + + fs::create_dir_all(&bindgen_dir).await?; + Ok(bindgen_dir) + } + + fn installed_bin_name(&self) -> String { + let mut name = format!("wasm-bindgen-{}", self.version); + if cfg!(windows) { + name = format!("{name}.exe"); + } + name + } + + fn cargo_bin_name(&self) -> String { + format!("wasm-bindgen-cli@{}", self.version) + } + + fn downloaded_bin_name(&self) -> &'static str { + if cfg!(windows) { + "wasm-bindgen.exe" + } else { + "wasm-bindgen" + } + } + + 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 + )) + } +} + +#[cfg(test)] +mod test { + use super::*; + const VERSION: &str = "0.2.99"; + + /// Test the github installer. + #[tokio::test] + async fn test_github_install() { + let binary = ManagedBinary::new(VERSION); + reset_test().await; + binary.install_github().await.unwrap(); + test_verify_install().await; + verify_installation(&binary).await; + } + + /// Test the cargo installer. + #[tokio::test] + async fn test_cargo_install() { + let binary = ManagedBinary::new(VERSION); + reset_test().await; + binary.install_cargo().await.unwrap(); + test_verify_install().await; + verify_installation(&binary).await; + } + + // CI doesn't have binstall. + // Test the binstall installer + // #[tokio::test] + // async fn test_binstall_install() { + // let binary = ManagedBinary::new(VERSION); + // reset_test().await; + // binary.install_binstall().await.unwrap(); + // test_verify_install().await; + // verify_installation(&binary).await; + // } + + /// Helper to test `verify_install` after an installation. + async fn test_verify_install() { + let binary = ManagedBinary::new(VERSION); + binary.verify_install().await.unwrap(); + } + + /// Helper to test that the installed binary actually exists. + async fn verify_installation(binary: &ManagedBinary) { + let path = binary.install_dir().await.unwrap(); + let name = binary.installed_bin_name(); + let binary_path = path.join(name); + assert!( + 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 binary = ManagedBinary::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; + } +} diff --git a/packages/cli/src/tools/wasm_bindgen/mod.rs b/packages/cli/src/tools/wasm_bindgen/mod.rs new file mode 100644 index 0000000000..8aeadb8fa6 --- /dev/null +++ b/packages/cli/src/tools/wasm_bindgen/mod.rs @@ -0,0 +1,201 @@ +use crate::Result; +use std::{ + path::{Path, PathBuf}, + process::Stdio, +}; +use tokio::process::Command; + +#[cfg(not(feature = "no-downloads"))] +mod managed; +#[cfg(feature = "no-downloads")] +mod path; + +#[cfg(not(feature = "no-downloads"))] +type Binary = managed::ManagedBinary; +#[cfg(feature = "no-downloads")] +type Binary = path::PathBinary; + +pub(crate) trait WasmBindgenBinary { + fn new(version: &str) -> Self; + async fn verify_install(&self) -> anyhow::Result<()>; + async fn get_binary_path(&self) -> anyhow::Result; +} + +pub(crate) struct WasmBindgen { + 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, +} + +impl WasmBindgen { + pub async fn run(&self) -> Result<()> { + let binary = Binary::new(&self.version).get_binary_path().await?; + let mut args = Vec::new(); + + // Target + args.push("--target"); + args.push(&self.target); + + // Options + if self.debug { + args.push("--debug"); + } + + if !self.demangle { + args.push("--no-demangle"); + } + + if self.keep_debug { + args.push("--keep-debug"); + } + + if self.remove_name_section { + args.push("--remove-name-section"); + } + + if self.remove_producers_section { + args.push("--remove-producers-section"); + } + + // Out name + args.push("--out-name"); + args.push(&self.out_name); + + // Out dir + let out_dir = self + .out_dir + .to_str() + .expect("input_path should be valid utf8"); + + args.push("--out-dir"); + args.push(out_dir); + + // Input path + let input_path = self + .input_path + .to_str() + .expect("input_path should be valid utf8"); + args.push(input_path); + + // Run bindgen + Command::new(binary) + .args(args) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .output() + .await?; + + Ok(()) + } + + pub async fn verify_install(version: &str) -> anyhow::Result<()> { + Binary::new(version).verify_install().await + } +} + +/// 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, +} + +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, + } + } + + 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, + } + } + + 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 + } + } +} diff --git a/packages/cli/src/tools/wasm_bindgen/path.rs b/packages/cli/src/tools/wasm_bindgen/path.rs new file mode 100644 index 0000000000..b692378ebe --- /dev/null +++ b/packages/cli/src/tools/wasm_bindgen/path.rs @@ -0,0 +1,49 @@ +use super::WasmBindgenBinary; +use anyhow::{anyhow, Context}; +use std::path::PathBuf; +use tokio::process::Command; + +pub(super) struct PathBinary { + version: String, +} + +impl WasmBindgenBinary for PathBinary { + fn new(version: &str) -> Self { + Self { + version: version.to_string(), + } + } + + async fn verify_install(&self) -> anyhow::Result<()> { + tracing::info!( + "Verifying wasm-bindgen-cli@{} is installed in the path", + self.version + ); + + 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, + )); + } + + Ok(()) + } + + async fn get_binary_path(&self) -> anyhow::Result { + which::which("wasm-bindgen") + .map_err(|_| anyhow!("Missing wasm-bindgen-cli@{}", self.version)) + } +} diff --git a/packages/cli/src/wasm_bindgen.rs b/packages/cli/src/wasm_bindgen.rs deleted file mode 100644 index 436689661f..0000000000 --- a/packages/cli/src/wasm_bindgen.rs +++ /dev/null @@ -1,454 +0,0 @@ -use anyhow::{anyhow, Context}; -use flate2::read::GzDecoder; -use std::{ - path::{Path, PathBuf}, - process::Stdio, -}; -use tar::Archive; -use tempfile::TempDir; -use tokio::{fs, process::Command}; - -pub(crate) struct WasmBindgen { - 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, -} - -impl WasmBindgen { - pub async fn run(&self) -> anyhow::Result<()> { - let binary = Self::final_binary(&self.version).await?; - - let mut args = Vec::new(); - - // Target - args.push("--target"); - args.push(&self.target); - - // Options - if self.debug { - args.push("--debug"); - } - - if !self.demangle { - args.push("--no-demangle"); - } - - if self.keep_debug { - args.push("--keep-debug"); - } - - if self.remove_name_section { - args.push("--remove-name-section"); - } - - if self.remove_producers_section { - args.push("--remove-producers-section"); - } - - // Out name - args.push("--out-name"); - args.push(&self.out_name); - - // Out dir - let out_dir = self - .out_dir - .to_str() - .expect("input_path should be valid utf8"); - - args.push("--out-dir"); - args.push(out_dir); - - // Input path - let input_path = self - .input_path - .to_str() - .expect("input_path should be valid utf8"); - args.push(input_path); - - // Run bindgen - Command::new(binary) - .args(args) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .output() - .await?; - - 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()) - } - - /// Install the specified wasm-bingen version. - /// - /// This will overwrite any existing wasm-bindgen binaries of the same version. - /// - /// This will attempt to install wasm-bindgen from: - /// 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}..."); - - // Attempt installation from GitHub - if let Err(e) = Self::install_github(version).await { - tracing::error!("Failed to install wasm-bindgen-cli@{version}: {e}"); - } else { - tracing::info!("wasm-bindgen-cli@{version} was successfully installed from GitHub."); - 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."); - } else { - tracing::info!( - "wasm-bindgen-cli@{version} was successfully installed from cargo-binstall." - ); - return Ok(()); - } - - // Attempt installation from cargo. - Self::install_cargo(version) - .await - .context("failed to install wasm-bindgen-cli from cargo")?; - - tracing::info!("wasm-bindgen-cli@{version} was successfully installed from source."); - - 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"); - - let url = git_install_url(version) - .ok_or_else(|| anyhow!("no available GitHub binary for wasm-bindgen-cli@{version}"))?; - - // Get the final binary location. - let final_binary = Self::final_binary(version).await?; - - // Download then extract wasm-bindgen-cli. - let bytes = reqwest::get(url).await?.bytes().await?; - - // Unpack the first tar entry to the final binary location - Archive::new(GzDecoder::new(bytes.as_ref())) - .entries()? - .find(|entry| { - entry - .as_ref() - .map(|e| { - e.path_bytes() - .ends_with(Self::downloaded_bin_name().as_bytes()) - }) - .unwrap_or(false) - }) - .context("Failed to find entry")?? - .unpack(&final_binary) - .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"); - - let package = Self::cargo_bin_name(version); - let tempdir = TempDir::new()?; - - // Run install command - Command::new("cargo") - .args([ - "binstall", - &package, - "--no-confirm", - "--force", - "--no-track", - "--install-path", - ]) - .arg(tempdir.path()) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .output() - .await?; - - fs::copy( - tempdir.path().join(Self::downloaded_bin_name()), - Self::final_binary(version).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); - let tempdir = TempDir::new()?; - - // Run install command - Command::new("cargo") - .args([ - "install", - &package, - "--bin", - "wasm-bindgen", - "--no-track", - "--force", - "--root", - ]) - .arg(tempdir.path()) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .output() - .await - .context("failed to install wasm-bindgen-cli from cargo-install")?; - - tracing::info!("Copying into path: {}", tempdir.path().display()); - - // 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?, - ) - .await - .context("failed to copy wasm-bindgen binary")?; - - 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) - } - - /// 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"); - } - 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" - } - } -} - -/// 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")) -} - -/// 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, -} - -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, - } - } - - 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, - } - } - - 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 - } - } -} - -#[cfg(test)] -mod test { - use super::*; - const VERSION: &str = "0.2.99"; - - /// Test the github installer. - #[tokio::test] - async fn test_github_install() { - reset_test().await; - WasmBindgen::install_github(VERSION).await.unwrap(); - test_verify_install().await; - verify_installation().await; - } - - /// Test the cargo installer. - #[tokio::test] - async fn test_cargo_install() { - reset_test().await; - WasmBindgen::install_cargo(VERSION).await.unwrap(); - test_verify_install().await; - verify_installation().await; - } - - // CI doesn't have binstall. - // Test the binstall installer - // #[tokio::test] - // async fn test_binstall_install() { - // reset_test().await; - // WasmBindgen::install_binstall(VERSION).await.unwrap(); - // test_verify_install().await; - // verify_installation().await; - // } - - /// Helper to test `WasmBindgen::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" - ); - } - - /// 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); - assert!( - binary.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; - } -}