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

* feat(cli): add feature to disable downloads

---------

Co-authored-by: Jonathan Kelley <jkelleyrtp@gmail.com>
This commit is contained in:
CathalandJonathan Kelley authored and GitHub committed 2025-01-16 16:49:30 -08:00
1 parent cd121f2828
commit c7951b7dd5
11 files changed
+485 -536

No files matched your search

+1 -1
View File
@@ -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
Generated
+26 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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()
}
}
-170
View File
@@ -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
View File
@@ -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> {
+367 -329
View File
@@ -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,295 +21,9 @@ pub(crate) struct WasmBindgen {
}
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");
}
// 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);
// 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<bool> {
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<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)
}
/// 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<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"
}
}
}
/// 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"))
}
/// 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 {
pub fn new(version: &str) -> Self {
Self {
version,
version: version.to_string(),
input_path: PathBuf::new(),
out_dir: PathBuf::new(),
out_name: String::new(),
@@ -323,21 +36,6 @@ impl WasmBindgenBuilder {
}
}
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(),
@@ -391,6 +89,347 @@ impl WasmBindgenBuilder {
..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();
// 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);
// wbg generates typescript bindnings by default - we don't want those
args.push("--no-typescript");
// 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 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.
///
/// 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`.
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 verify_local_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 verify_managed_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<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))
}
}
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/");
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<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
))
}
}
#[cfg(test)]
@@ -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;
}
}