use crate::styles::GLOW_STYLE; use crate::CliSettings; use crate::Result; use crate::{config::DioxusConfig, AndroidTools}; use anyhow::{bail, Context}; use ignore::gitignore::Gitignore; use krates::{semver::Version, KrateDetails, LockOptions}; use krates::{Cmd, Krates, NodeId}; use std::sync::Arc; use std::{collections::HashSet, path::Path}; use std::{path::PathBuf, time::Duration}; use target_lexicon::Triple; use tokio::process::Command; pub struct Workspace { pub(crate) krates: Krates, pub(crate) settings: CliSettings, pub(crate) wasm_opt: Option, pub(crate) sysroot: PathBuf, pub(crate) rustc_version: String, pub(crate) ignore: Gitignore, pub(crate) cargo_toml: cargo_toml::Manifest, pub(crate) android_tools: Option>, } impl Workspace { /// Load the workspace from the current directory. This is cached and will only be loaded once. pub async fn current() -> Result> { static WS: tokio::sync::Mutex>> = tokio::sync::Mutex::const_new(None); // Lock the workspace to prevent multiple threads from loading it at the same time // If loading the workspace failed the first time, it won't be set and therefore permeate an error. let mut lock = WS.lock().await; if let Some(ws) = lock.as_ref() { return Ok(ws.clone()); } let krates_future = tokio::task::spawn_blocking(|| { let manifest_options = crate::logging::VERBOSITY.get().unwrap(); let lock_options = LockOptions { frozen: manifest_options.frozen, locked: manifest_options.locked, offline: manifest_options.offline, }; let mut cmd = Cmd::new(); cmd.lock_opts(lock_options); let mut builder = krates::Builder::new(); builder.workspace(true); let res = builder.build(cmd, |_| {})?; if !lock_options.offline { if let Ok(res) = std::env::var("SIMULATE_SLOW_NETWORK") { std::thread::sleep(Duration::from_secs(res.parse().unwrap_or(5))); } } Ok(res) as Result }); let spin_future = async move { tokio::time::sleep(Duration::from_millis(1000)).await; eprintln!("{GLOW_STYLE}warning{GLOW_STYLE:#}: Waiting for cargo-metadata..."); tokio::time::sleep(Duration::from_millis(2000)).await; for x in 1..=100 { tokio::time::sleep(Duration::from_millis(2000)).await; eprintln!("{GLOW_STYLE}warning{GLOW_STYLE:#}: (Try {x}) Taking a while..."); if x % 10 == 0 { eprintln!("{GLOW_STYLE}warning{GLOW_STYLE:#}: maybe check your network connection or build lock?"); } } }; let krates = tokio::select! { f = krates_future => { let res = f?; if let Err(krates::Error::Metadata(e)) = res { bail!("{e}"); } res? }, _ = spin_future => bail!("cargo metadata took too long to respond, try again with --offline"), }; let settings = CliSettings::global_or_default(); let sysroot = Self::get_rustc_sysroot() .await .context("Failed to get rustc sysroot")?; let rustc_version = Self::get_rustc_version() .await .context("Failed to get rustc version")?; let wasm_opt = which::which("wasm-opt").ok(); let ignore = Self::workspace_gitignore(krates.workspace_root().as_std_path()); let cargo_toml = crate::cargo_toml::load_manifest_from_path( krates.workspace_root().join("Cargo.toml").as_std_path(), ) .context("Failed to load Cargo.toml")?; let android_tools = crate::build::get_android_tools(); let workspace = Arc::new(Self { krates, settings, wasm_opt, sysroot: sysroot.trim().into(), rustc_version: rustc_version.trim().into(), ignore, cargo_toml, android_tools, }); tracing::debug!( r#"Initialized workspace: • sysroot: {sysroot} • rustc version: {rustc_version} • workspace root: {workspace_root} • dioxus versions: [{dioxus_versions:?}]"#, sysroot = workspace.sysroot.display(), rustc_version = workspace.rustc_version, workspace_root = workspace.workspace_root().display(), dioxus_versions = workspace .dioxus_versions() .iter() .map(|v| v.to_string()) .collect::>() .join(", ") ); lock.replace(workspace.clone()); Ok(workspace) } pub fn android_tools(&self) -> Result> { self .android_tools .clone() .context("Android not installed properly. Please set the `ANDROID_NDK_HOME` environment variable to the root of your NDK installation.") } pub fn is_release_profile(&self, profile: &str) -> bool { // If the profile is "release" or ends with "-release" like the default platform release profiles, // always put it in the release category. if profile == "release" || profile.ends_with("-release") { return true; } // Check if the profile inherits from release by traversing the `inherits` chain let mut current_profile_name = profile; // Try to find the current profile in the custom profiles section while let Some(profile_settings) = self.cargo_toml.profile.custom.get(current_profile_name) { // Check what this profile inherits from match &profile_settings.inherits { // Otherwise, continue checking the profile it inherits from Some(inherits_name) => current_profile_name = inherits_name, // This profile doesn't explicitly inherit anything, so the chain ends here. // Since it didn't lead to "release", return false. None => break, } if current_profile_name == "release" { return true; } } false } pub fn check_dioxus_version_against_cli(&self) { let dx_semver = Version::parse(env!("CARGO_PKG_VERSION")).unwrap(); let dioxus_versions = self.dioxus_versions(); tracing::trace!("dx version: {}", dx_semver); tracing::trace!("dioxus versions: {:?}", dioxus_versions); // if there are no dioxus versions in the workspace, we don't need to check anything // dx is meant to be compatible with non-dioxus projects too. if dioxus_versions.is_empty() { return; } let min = dioxus_versions.iter().min().unwrap(); let max = dioxus_versions.iter().max().unwrap(); // If the minimum dioxus version is greater than the current cli version, warn the user if min > &dx_semver || max < &dx_semver || dioxus_versions.iter().any(|f| f.pre != dx_semver.pre) { tracing::error!( r#"🚫dx and dioxus versions are incompatible! • dx version: {dx_semver} • dioxus versions: [{}]"#, dioxus_versions .iter() .map(|v| v.to_string()) .collect::>() .join(", ") ); } } /// Get all the versions of dioxus in the workspace pub fn dioxus_versions(&self) -> Vec { let mut versions = HashSet::new(); for krate in self.krates.krates() { if krate.name == "dioxus" { versions.insert(krate.version.clone()); } } let mut versions = versions.into_iter().collect::>(); versions.sort(); versions } #[allow(unused)] pub fn rust_lld(&self) -> PathBuf { self.sysroot .join("lib") .join("rustlib") .join(Triple::host().to_string()) .join("bin") .join("rust-lld") } /// Return the path to the `cc` compiler /// /// This is used for the patching system to run the linker. /// We could also just use lld given to us by rust itself. pub fn cc(&self) -> PathBuf { PathBuf::from("cc") } /// The windows linker pub fn lld_link(&self) -> PathBuf { self.gcc_ld_dir().join("lld-link") } pub fn wasm_ld(&self) -> PathBuf { self.gcc_ld_dir().join("wasm-ld") } pub fn select_ranlib() -> Option { // prefer the modern llvm-ranlib if they have it which::which("llvm-ranlib") .or_else(|_| which::which("ranlib")) .ok() } /// Return the version of the wasm-bindgen crate if it exists pub fn wasm_bindgen_version(&self) -> Option { self.krates .krates_by_name("wasm-bindgen") .next() .map(|krate| krate.krate.version.to_string()) } // wasm-ld: ./rustup/toolchains/nightly-x86_64-unknown-linux-gnu/bin/wasm-ld // rust-lld: ./rustup/toolchains/nightly-x86_64-unknown-linux-gnu/bin/rust-lld fn gcc_ld_dir(&self) -> PathBuf { self.sysroot .join("lib") .join("rustlib") .join(Triple::host().to_string()) .join("bin") .join("gcc-ld") } /// Find the "main" package in the workspace. There might not be one! pub fn find_main_package(&self, package: Option) -> Result { if let Some(package) = package { let mut workspace_members = self.krates.workspace_members(); let found = workspace_members.find_map(|node| { if let krates::Node::Krate { id, krate, .. } = node { if krate.name == package { return Some(id); } } None }); if found.is_none() { tracing::error!("Could not find package {package} in the workspace. Did you forget to add it to the workspace?"); tracing::error!("Packages in the workspace:"); for package in self.krates.workspace_members() { if let krates::Node::Krate { krate, .. } = package { tracing::error!("{}", krate.name()); } } } let kid = found.ok_or_else(|| anyhow::anyhow!("Failed to find package {package}"))?; return Ok(self.krates.nid_for_kid(kid).unwrap()); }; // if we have default members specified, try them first if let Some(ws) = &self.cargo_toml.workspace { for default in &ws.default_members { let mut workspace_members = self.krates.workspace_members(); let default_member_path = std::fs::canonicalize(default).unwrap(); let found = workspace_members.find_map(|node| { if let krates::Node::Krate { id, krate, .. } = node { // Skip this default member if it doesn't have any binary targets if !krate .targets .iter() .any(|t| t.kind.contains(&krates::cm::TargetKind::Bin)) { return None; } let member_path = std::fs::canonicalize(krate.manifest_path.parent().unwrap()).unwrap(); if member_path == default_member_path { return Some(id); } } None }); if let Some(kid) = found { return Ok(self.krates.nid_for_kid(kid).unwrap()); } } } // Otherwise find the package that is the closest parent of the current directory let current_dir = std::env::current_dir()?; let current_dir = current_dir.as_path(); // Go through each member and find the path that is a parent of the current directory let mut closest_parent = None; for member in self.krates.workspace_members() { if let krates::Node::Krate { id, krate, .. } = member { let member_path = krate.manifest_path.parent().unwrap(); if let Ok(path) = current_dir.strip_prefix(member_path.as_std_path()) { let len = path.components().count(); match closest_parent { Some((_, closest_parent_len)) => { if len < closest_parent_len { closest_parent = Some((id, len)); } } None => { closest_parent = Some((id, len)); } } } } } let kid = closest_parent .map(|(id, _)| id) .with_context(|| { let bin_targets = self.krates.workspace_members().filter_map(|krate|match krate { krates::Node::Krate { krate, .. } if krate.targets.iter().any(|t| t.kind.contains(&krates::cm::TargetKind::Bin))=> { Some(format!("- {}", krate.name)) } _ => None }).collect::>(); format!("Failed to find binary package to build.\nYou need to either run dx from inside a binary crate or specify a binary package to build with the `--package` flag. Try building again with one of the binary packages in the workspace:\n{}", bin_targets.join("\n")) })?; let package = self.krates.nid_for_kid(kid).unwrap(); Ok(package) } pub fn load_dioxus_config(&self, package: NodeId) -> Result> { // Walk up from the cargo.toml to the root of the workspace looking for Dioxus.toml let mut current_dir = self.krates[package] .manifest_path .parent() .unwrap() .as_std_path() .to_path_buf() .canonicalize()?; let workspace_path = self .krates .workspace_root() .as_std_path() .to_path_buf() .canonicalize()?; let mut dioxus_conf_file = None; while current_dir.starts_with(&workspace_path) { let config = ["Dioxus.toml", "dioxus.toml"] .into_iter() .map(|file| current_dir.join(file)) .find(|path| path.is_file()); // Try to find Dioxus.toml in the current directory if let Some(new_config) = config { dioxus_conf_file = Some(new_config.as_path().to_path_buf()); break; } // If we can't find it, go up a directory current_dir = current_dir .parent() .context("Failed to find Dioxus.toml")? .to_path_buf(); } let Some(dioxus_conf_file) = dioxus_conf_file else { return Ok(None); }; toml::from_str::(&std::fs::read_to_string(&dioxus_conf_file)?) .map_err(|err| { anyhow::anyhow!("Failed to parse Dioxus.toml at {dioxus_conf_file:?}: {err}") }) .map(Some) } /// Create a new gitignore map for this target crate /// /// todo(jon): this is a bit expensive to build, so maybe we should cache it? pub fn workspace_gitignore(workspace_dir: &Path) -> Gitignore { let mut ignore_builder = ignore::gitignore::GitignoreBuilder::new(workspace_dir); ignore_builder.add(workspace_dir.join(".gitignore")); for path in Self::default_ignore_list() { ignore_builder .add_line(None, path) .expect("failed to add path to file excluded"); } ignore_builder.build().unwrap() } pub fn ignore_for_krate(&self, path: &Path) -> ignore::gitignore::Gitignore { let mut ignore_builder = ignore::gitignore::GitignoreBuilder::new(path); for path in Self::default_ignore_list() { ignore_builder .add_line(None, path) .expect("failed to add path to file excluded"); } ignore_builder.build().unwrap() } pub fn default_ignore_list() -> Vec<&'static str> { vec![ ".git", ".github", ".vscode", "target", "node_modules", "dist", "*~", ".*", "*.lock", "*.log", ] } pub(crate) fn workspace_root(&self) -> PathBuf { self.krates.workspace_root().as_std_path().to_path_buf() } /// Returns the root of the crate that the command is run from, without calling `cargo metadata` /// /// If the command is run from the workspace root, this will return the top-level Cargo.toml pub(crate) fn crate_root_from_path() -> Result { /// How many parent folders are searched for a `Cargo.toml` const MAX_ANCESTORS: u32 = 10; /// Checks if the directory contains `Cargo.toml` fn contains_manifest(path: &Path) -> bool { std::fs::read_dir(path) .map(|entries| { entries .filter_map(Result::ok) .any(|ent| &ent.file_name() == "Cargo.toml") }) .unwrap_or(false) } // From the current directory we work our way up, looking for `Cargo.toml` std::env::current_dir() .ok() .and_then(|mut wd| { for _ in 0..MAX_ANCESTORS { if contains_manifest(&wd) { return Some(wd); } if !wd.pop() { break; } } None }) .context("Failed to find directory containing Cargo.toml") } pub async fn get_xcode_path() -> Option { let xcode = Command::new("xcode-select") .arg("-p") .output() .await .ok() .map(|s| String::from_utf8_lossy(&s.stdout).trim().to_string().into()); xcode } pub async fn get_rustc_sysroot() -> Result { let sysroot = Command::new("rustc") .args(["--print", "sysroot"]) .output() .await .map(|out| String::from_utf8(out.stdout).map(|s| s.trim().to_string()))? .context("Failed to extract rustc sysroot output")?; Ok(sysroot) } pub async fn get_rustc_version() -> Result { let rustc_version = Command::new("rustc") .args(["--version"]) .output() .await .map(|out| String::from_utf8(out.stdout))? .context("Failed to extract rustc version output")?; Ok(rustc_version) } /// Returns the properly canonicalized path to the dx executable, used for linking and wrapping rustc pub(crate) fn path_to_dx() -> Result { dunce::canonicalize(std::env::current_exe().context("Failed to find dx")?) .context("Failed to find dx") } /// Returns the path to the dioxus data directory, used to install tools, store configs, and other things /// /// On macOS, we prefer to not put this dir in Application Support, but rather in the home directory. /// On Windows, we prefer to keep it in the home directory so the `dx` install dir matches the install script. pub(crate) fn dioxus_data_dir() -> PathBuf { static DX_HOME: std::sync::OnceLock = std::sync::OnceLock::new(); DX_HOME .get_or_init(|| { if let Some(path) = std::env::var_os("DX_HOME") { return PathBuf::from(path); } if cfg!(target_os = "macos") || cfg!(target_os = "windows") { dirs::home_dir().unwrap().join(".dx") } else { dirs::data_dir() .or_else(dirs::home_dir) .unwrap() .join(".dx") } }) .to_path_buf() } pub(crate) fn global_settings_file() -> PathBuf { Self::dioxus_data_dir().join("settings.toml") } /// The path where components downloaded from git are cached pub(crate) fn component_cache_dir() -> PathBuf { Self::dioxus_data_dir().join("components") } /// Get the path to a specific component in the cache pub(crate) fn component_cache_path(git: &str, rev: Option<&str>) -> PathBuf { use std::hash::Hasher; let mut hasher = std::hash::DefaultHasher::new(); std::hash::Hash::hash(git, &mut hasher); if let Some(rev) = rev { std::hash::Hash::hash(rev, &mut hasher); } let hash = hasher.finish(); Self::component_cache_dir().join(format!("{hash:016x}")) } } impl std::fmt::Debug for Workspace { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("Workspace") .field("krates", &"..") .field("settings", &self.settings) .field("rustc_version", &self.rustc_version) .field("sysroot", &self.sysroot) .field("wasm_opt", &self.wasm_opt) .finish() } }