123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452 |
- use crate::CliSettings;
- use crate::Result;
- use crate::{config::DioxusConfig, AndroidTools};
- use anyhow::Context;
- use ignore::gitignore::Gitignore;
- use krates::{semver::Version, KrateDetails};
- use krates::{Cmd, Krates, NodeId};
- use std::path::PathBuf;
- use std::sync::Arc;
- use std::{collections::HashSet, path::Path};
- use target_lexicon::Triple;
- use tokio::process::Command;
- pub struct Workspace {
- pub(crate) krates: Krates,
- pub(crate) settings: CliSettings,
- pub(crate) wasm_opt: Option<PathBuf>,
- 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<Arc<AndroidTools>>,
- }
- impl Workspace {
- /// Load the workspace from the current directory. This is cached and will only be loaded once.
- pub async fn current() -> Result<Arc<Workspace>> {
- static WS: tokio::sync::Mutex<Option<Arc<Workspace>>> = 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 cmd = Cmd::new();
- let mut builder = krates::Builder::new();
- builder.workspace(true);
- let krates = builder
- .build(cmd, |_| {})
- .context("Failed to run cargo metadata")?;
- let settings = CliSettings::global_or_default();
- let sysroot = Command::new("rustc")
- .args(["--print", "sysroot"])
- .output()
- .await
- .map(|out| String::from_utf8(out.stdout))?
- .context("Failed to extract rustc sysroot output")?;
- let rustc_version = Command::new("rustc")
- .args(["--version"])
- .output()
- .await
- .map(|out| String::from_utf8(out.stdout))?
- .context("Failed to extract rustc version output")?;
- let wasm_opt = which::which("wasm-opt").ok();
- let ignore = Self::workspace_gitignore(krates.workspace_root().as_std_path());
- let cargo_toml =
- cargo_toml::Manifest::from_path(krates.workspace_root().join("Cargo.toml"))
- .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::<Vec<_>>()
- .join(", ")
- );
- lock.replace(workspace.clone());
- Ok(workspace)
- }
- pub fn android_tools(&self) -> Result<Arc<AndroidTools>> {
- Ok(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 {
- tracing::error!(
- r#"🚫dx and dioxus versions are incompatible!
- • dx version: {dx_semver}
- • dioxus versions: [{}]"#,
- dioxus_versions
- .iter()
- .map(|v| v.to_string())
- .collect::<Vec<_>>()
- .join(", ")
- );
- }
- }
- /// Get all the versions of dioxus in the workspace
- pub fn dioxus_versions(&self) -> Vec<Version> {
- 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::<Vec<_>>();
- 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")
- }
- // 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")
- }
- pub fn has_wasm32_unknown_unknown(&self) -> bool {
- self.sysroot
- .join("lib/rustlib/wasm32-unknown-unknown")
- .exists()
- }
- /// Find the "main" package in the workspace. There might not be one!
- pub fn find_main_package(&self, package: Option<String>) -> Result<NodeId> {
- 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());
- };
- // 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::<Vec<_>>();
- 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<Option<DioxusConfig>> {
- // 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::<DioxusConfig>(&std::fs::read_to_string(&dioxus_conf_file)?)
- .map_err(|err| {
- anyhow::anyhow!("Failed to parse Dioxus.toml at {dioxus_conf_file:?}: {err}").into()
- })
- .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<PathBuf> {
- /// 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
- })
- .ok_or_else(|| {
- crate::Error::Cargo("Failed to find directory containing Cargo.toml".to_string())
- })
- }
- /// Returns the properly canonicalized path to the dx executable, used for linking and wrapping rustc
- pub(crate) fn path_to_dx() -> Result<PathBuf> {
- Ok(
- dunce::canonicalize(std::env::current_exe().context("Failed to find dx")?)
- .context("Failed to find dx")?,
- )
- }
- /// Returns the path to the dioxus home directory, used to install tools and other things
- pub(crate) fn dioxus_home_dir() -> PathBuf {
- dirs::data_local_dir()
- .map(|f| f.join("dioxus/"))
- .unwrap_or_else(|| dirs::home_dir().unwrap().join(".dioxus"))
- }
- }
- 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()
- }
- }
|