1
0

workspace.rs 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452
  1. use crate::CliSettings;
  2. use crate::Result;
  3. use crate::{config::DioxusConfig, AndroidTools};
  4. use anyhow::Context;
  5. use ignore::gitignore::Gitignore;
  6. use krates::{semver::Version, KrateDetails};
  7. use krates::{Cmd, Krates, NodeId};
  8. use std::path::PathBuf;
  9. use std::sync::Arc;
  10. use std::{collections::HashSet, path::Path};
  11. use target_lexicon::Triple;
  12. use tokio::process::Command;
  13. pub struct Workspace {
  14. pub(crate) krates: Krates,
  15. pub(crate) settings: CliSettings,
  16. pub(crate) wasm_opt: Option<PathBuf>,
  17. pub(crate) sysroot: PathBuf,
  18. pub(crate) rustc_version: String,
  19. pub(crate) ignore: Gitignore,
  20. pub(crate) cargo_toml: cargo_toml::Manifest,
  21. pub(crate) android_tools: Option<Arc<AndroidTools>>,
  22. }
  23. impl Workspace {
  24. /// Load the workspace from the current directory. This is cached and will only be loaded once.
  25. pub async fn current() -> Result<Arc<Workspace>> {
  26. static WS: tokio::sync::Mutex<Option<Arc<Workspace>>> = tokio::sync::Mutex::const_new(None);
  27. // Lock the workspace to prevent multiple threads from loading it at the same time
  28. // If loading the workspace failed the first time, it won't be set and therefore permeate an error.
  29. let mut lock = WS.lock().await;
  30. if let Some(ws) = lock.as_ref() {
  31. return Ok(ws.clone());
  32. }
  33. let cmd = Cmd::new();
  34. let mut builder = krates::Builder::new();
  35. builder.workspace(true);
  36. let krates = builder
  37. .build(cmd, |_| {})
  38. .context("Failed to run cargo metadata")?;
  39. let settings = CliSettings::global_or_default();
  40. let sysroot = Command::new("rustc")
  41. .args(["--print", "sysroot"])
  42. .output()
  43. .await
  44. .map(|out| String::from_utf8(out.stdout))?
  45. .context("Failed to extract rustc sysroot output")?;
  46. let rustc_version = Command::new("rustc")
  47. .args(["--version"])
  48. .output()
  49. .await
  50. .map(|out| String::from_utf8(out.stdout))?
  51. .context("Failed to extract rustc version output")?;
  52. let wasm_opt = which::which("wasm-opt").ok();
  53. let ignore = Self::workspace_gitignore(krates.workspace_root().as_std_path());
  54. let cargo_toml =
  55. cargo_toml::Manifest::from_path(krates.workspace_root().join("Cargo.toml"))
  56. .context("Failed to load Cargo.toml")?;
  57. let android_tools = crate::build::get_android_tools();
  58. let workspace = Arc::new(Self {
  59. krates,
  60. settings,
  61. wasm_opt,
  62. sysroot: sysroot.trim().into(),
  63. rustc_version: rustc_version.trim().into(),
  64. ignore,
  65. cargo_toml,
  66. android_tools,
  67. });
  68. tracing::debug!(
  69. r#"Initialized workspace:
  70. • sysroot: {sysroot}
  71. • rustc version: {rustc_version}
  72. • workspace root: {workspace_root}
  73. • dioxus versions: [{dioxus_versions:?}]"#,
  74. sysroot = workspace.sysroot.display(),
  75. rustc_version = workspace.rustc_version,
  76. workspace_root = workspace.workspace_root().display(),
  77. dioxus_versions = workspace
  78. .dioxus_versions()
  79. .iter()
  80. .map(|v| v.to_string())
  81. .collect::<Vec<_>>()
  82. .join(", ")
  83. );
  84. lock.replace(workspace.clone());
  85. Ok(workspace)
  86. }
  87. pub fn android_tools(&self) -> Result<Arc<AndroidTools>> {
  88. Ok(self
  89. .android_tools
  90. .clone()
  91. .context("Android not installed properly. Please set the `ANDROID_NDK_HOME` environment variable to the root of your NDK installation.")?)
  92. }
  93. pub fn is_release_profile(&self, profile: &str) -> bool {
  94. // If the profile is "release" or ends with "-release" like the default platform release profiles,
  95. // always put it in the release category.
  96. if profile == "release" || profile.ends_with("-release") {
  97. return true;
  98. }
  99. // Check if the profile inherits from release by traversing the `inherits` chain
  100. let mut current_profile_name = profile;
  101. // Try to find the current profile in the custom profiles section
  102. while let Some(profile_settings) = self.cargo_toml.profile.custom.get(current_profile_name)
  103. {
  104. // Check what this profile inherits from
  105. match &profile_settings.inherits {
  106. // Otherwise, continue checking the profile it inherits from
  107. Some(inherits_name) => current_profile_name = inherits_name,
  108. // This profile doesn't explicitly inherit anything, so the chain ends here.
  109. // Since it didn't lead to "release", return false.
  110. None => break,
  111. }
  112. if current_profile_name == "release" {
  113. return true;
  114. }
  115. }
  116. false
  117. }
  118. pub fn check_dioxus_version_against_cli(&self) {
  119. let dx_semver = Version::parse(env!("CARGO_PKG_VERSION")).unwrap();
  120. let dioxus_versions = self.dioxus_versions();
  121. tracing::trace!("dx version: {}", dx_semver);
  122. tracing::trace!("dioxus versions: {:?}", dioxus_versions);
  123. // if there are no dioxus versions in the workspace, we don't need to check anything
  124. // dx is meant to be compatible with non-dioxus projects too.
  125. if dioxus_versions.is_empty() {
  126. return;
  127. }
  128. let min = dioxus_versions.iter().min().unwrap();
  129. let max = dioxus_versions.iter().max().unwrap();
  130. // If the minimum dioxus version is greater than the current cli version, warn the user
  131. if min > &dx_semver || max < &dx_semver {
  132. tracing::error!(
  133. r#"🚫dx and dioxus versions are incompatible!
  134. • dx version: {dx_semver}
  135. • dioxus versions: [{}]"#,
  136. dioxus_versions
  137. .iter()
  138. .map(|v| v.to_string())
  139. .collect::<Vec<_>>()
  140. .join(", ")
  141. );
  142. }
  143. }
  144. /// Get all the versions of dioxus in the workspace
  145. pub fn dioxus_versions(&self) -> Vec<Version> {
  146. let mut versions = HashSet::new();
  147. for krate in self.krates.krates() {
  148. if krate.name == "dioxus" {
  149. versions.insert(krate.version.clone());
  150. }
  151. }
  152. let mut versions = versions.into_iter().collect::<Vec<_>>();
  153. versions.sort();
  154. versions
  155. }
  156. #[allow(unused)]
  157. pub fn rust_lld(&self) -> PathBuf {
  158. self.sysroot
  159. .join("lib")
  160. .join("rustlib")
  161. .join(Triple::host().to_string())
  162. .join("bin")
  163. .join("rust-lld")
  164. }
  165. /// Return the path to the `cc` compiler
  166. ///
  167. /// This is used for the patching system to run the linker.
  168. /// We could also just use lld given to us by rust itself.
  169. pub fn cc(&self) -> PathBuf {
  170. PathBuf::from("cc")
  171. }
  172. /// The windows linker
  173. pub fn lld_link(&self) -> PathBuf {
  174. self.gcc_ld_dir().join("lld-link")
  175. }
  176. pub fn wasm_ld(&self) -> PathBuf {
  177. self.gcc_ld_dir().join("wasm-ld")
  178. }
  179. // wasm-ld: ./rustup/toolchains/nightly-x86_64-unknown-linux-gnu/bin/wasm-ld
  180. // rust-lld: ./rustup/toolchains/nightly-x86_64-unknown-linux-gnu/bin/rust-lld
  181. fn gcc_ld_dir(&self) -> PathBuf {
  182. self.sysroot
  183. .join("lib")
  184. .join("rustlib")
  185. .join(Triple::host().to_string())
  186. .join("bin")
  187. .join("gcc-ld")
  188. }
  189. pub fn has_wasm32_unknown_unknown(&self) -> bool {
  190. self.sysroot
  191. .join("lib/rustlib/wasm32-unknown-unknown")
  192. .exists()
  193. }
  194. /// Find the "main" package in the workspace. There might not be one!
  195. pub fn find_main_package(&self, package: Option<String>) -> Result<NodeId> {
  196. if let Some(package) = package {
  197. let mut workspace_members = self.krates.workspace_members();
  198. let found = workspace_members.find_map(|node| {
  199. if let krates::Node::Krate { id, krate, .. } = node {
  200. if krate.name == package {
  201. return Some(id);
  202. }
  203. }
  204. None
  205. });
  206. if found.is_none() {
  207. tracing::error!("Could not find package {package} in the workspace. Did you forget to add it to the workspace?");
  208. tracing::error!("Packages in the workspace:");
  209. for package in self.krates.workspace_members() {
  210. if let krates::Node::Krate { krate, .. } = package {
  211. tracing::error!("{}", krate.name());
  212. }
  213. }
  214. }
  215. let kid = found.ok_or_else(|| anyhow::anyhow!("Failed to find package {package}"))?;
  216. return Ok(self.krates.nid_for_kid(kid).unwrap());
  217. };
  218. // Otherwise find the package that is the closest parent of the current directory
  219. let current_dir = std::env::current_dir()?;
  220. let current_dir = current_dir.as_path();
  221. // Go through each member and find the path that is a parent of the current directory
  222. let mut closest_parent = None;
  223. for member in self.krates.workspace_members() {
  224. if let krates::Node::Krate { id, krate, .. } = member {
  225. let member_path = krate.manifest_path.parent().unwrap();
  226. if let Ok(path) = current_dir.strip_prefix(member_path.as_std_path()) {
  227. let len = path.components().count();
  228. match closest_parent {
  229. Some((_, closest_parent_len)) => {
  230. if len < closest_parent_len {
  231. closest_parent = Some((id, len));
  232. }
  233. }
  234. None => {
  235. closest_parent = Some((id, len));
  236. }
  237. }
  238. }
  239. }
  240. }
  241. let kid = closest_parent
  242. .map(|(id, _)| id)
  243. .with_context(|| {
  244. let bin_targets = self.krates.workspace_members().filter_map(|krate|match krate {
  245. krates::Node::Krate { krate, .. } if krate.targets.iter().any(|t| t.kind.contains(&krates::cm::TargetKind::Bin))=> {
  246. Some(format!("- {}", krate.name))
  247. }
  248. _ => None
  249. }).collect::<Vec<_>>();
  250. 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"))
  251. })?;
  252. let package = self.krates.nid_for_kid(kid).unwrap();
  253. Ok(package)
  254. }
  255. pub fn load_dioxus_config(&self, package: NodeId) -> Result<Option<DioxusConfig>> {
  256. // Walk up from the cargo.toml to the root of the workspace looking for Dioxus.toml
  257. let mut current_dir = self.krates[package]
  258. .manifest_path
  259. .parent()
  260. .unwrap()
  261. .as_std_path()
  262. .to_path_buf()
  263. .canonicalize()?;
  264. let workspace_path = self
  265. .krates
  266. .workspace_root()
  267. .as_std_path()
  268. .to_path_buf()
  269. .canonicalize()?;
  270. let mut dioxus_conf_file = None;
  271. while current_dir.starts_with(&workspace_path) {
  272. let config = ["Dioxus.toml", "dioxus.toml"]
  273. .into_iter()
  274. .map(|file| current_dir.join(file))
  275. .find(|path| path.is_file());
  276. // Try to find Dioxus.toml in the current directory
  277. if let Some(new_config) = config {
  278. dioxus_conf_file = Some(new_config.as_path().to_path_buf());
  279. break;
  280. }
  281. // If we can't find it, go up a directory
  282. current_dir = current_dir
  283. .parent()
  284. .context("Failed to find Dioxus.toml")?
  285. .to_path_buf();
  286. }
  287. let Some(dioxus_conf_file) = dioxus_conf_file else {
  288. return Ok(None);
  289. };
  290. toml::from_str::<DioxusConfig>(&std::fs::read_to_string(&dioxus_conf_file)?)
  291. .map_err(|err| {
  292. anyhow::anyhow!("Failed to parse Dioxus.toml at {dioxus_conf_file:?}: {err}").into()
  293. })
  294. .map(Some)
  295. }
  296. /// Create a new gitignore map for this target crate
  297. ///
  298. /// todo(jon): this is a bit expensive to build, so maybe we should cache it?
  299. pub fn workspace_gitignore(workspace_dir: &Path) -> Gitignore {
  300. let mut ignore_builder = ignore::gitignore::GitignoreBuilder::new(workspace_dir);
  301. ignore_builder.add(workspace_dir.join(".gitignore"));
  302. for path in Self::default_ignore_list() {
  303. ignore_builder
  304. .add_line(None, path)
  305. .expect("failed to add path to file excluded");
  306. }
  307. ignore_builder.build().unwrap()
  308. }
  309. pub fn ignore_for_krate(&self, path: &Path) -> ignore::gitignore::Gitignore {
  310. let mut ignore_builder = ignore::gitignore::GitignoreBuilder::new(path);
  311. for path in Self::default_ignore_list() {
  312. ignore_builder
  313. .add_line(None, path)
  314. .expect("failed to add path to file excluded");
  315. }
  316. ignore_builder.build().unwrap()
  317. }
  318. pub fn default_ignore_list() -> Vec<&'static str> {
  319. vec![
  320. ".git",
  321. ".github",
  322. ".vscode",
  323. "target",
  324. "node_modules",
  325. "dist",
  326. "*~",
  327. ".*",
  328. "*.lock",
  329. "*.log",
  330. ]
  331. }
  332. pub(crate) fn workspace_root(&self) -> PathBuf {
  333. self.krates.workspace_root().as_std_path().to_path_buf()
  334. }
  335. /// Returns the root of the crate that the command is run from, without calling `cargo metadata`
  336. ///
  337. /// If the command is run from the workspace root, this will return the top-level Cargo.toml
  338. pub(crate) fn crate_root_from_path() -> Result<PathBuf> {
  339. /// How many parent folders are searched for a `Cargo.toml`
  340. const MAX_ANCESTORS: u32 = 10;
  341. /// Checks if the directory contains `Cargo.toml`
  342. fn contains_manifest(path: &Path) -> bool {
  343. std::fs::read_dir(path)
  344. .map(|entries| {
  345. entries
  346. .filter_map(Result::ok)
  347. .any(|ent| &ent.file_name() == "Cargo.toml")
  348. })
  349. .unwrap_or(false)
  350. }
  351. // From the current directory we work our way up, looking for `Cargo.toml`
  352. std::env::current_dir()
  353. .ok()
  354. .and_then(|mut wd| {
  355. for _ in 0..MAX_ANCESTORS {
  356. if contains_manifest(&wd) {
  357. return Some(wd);
  358. }
  359. if !wd.pop() {
  360. break;
  361. }
  362. }
  363. None
  364. })
  365. .ok_or_else(|| {
  366. crate::Error::Cargo("Failed to find directory containing Cargo.toml".to_string())
  367. })
  368. }
  369. /// Returns the properly canonicalized path to the dx executable, used for linking and wrapping rustc
  370. pub(crate) fn path_to_dx() -> Result<PathBuf> {
  371. Ok(
  372. dunce::canonicalize(std::env::current_exe().context("Failed to find dx")?)
  373. .context("Failed to find dx")?,
  374. )
  375. }
  376. /// Returns the path to the dioxus home directory, used to install tools and other things
  377. pub(crate) fn dioxus_home_dir() -> PathBuf {
  378. dirs::data_local_dir()
  379. .map(|f| f.join("dioxus/"))
  380. .unwrap_or_else(|| dirs::home_dir().unwrap().join(".dioxus"))
  381. }
  382. }
  383. impl std::fmt::Debug for Workspace {
  384. fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
  385. f.debug_struct("Workspace")
  386. .field("krates", &"..")
  387. .field("settings", &self.settings)
  388. .field("rustc_version", &self.rustc_version)
  389. .field("sysroot", &self.sysroot)
  390. .field("wasm_opt", &self.wasm_opt)
  391. .finish()
  392. }
  393. }