lib.rs 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607
  1. // Copyright 2019-2023 Tauri Programme within The Commons Conservancy
  2. // SPDX-License-Identifier: Apache-2.0
  3. // SPDX-License-Identifier: MIT
  4. #![cfg_attr(doc_cfg, feature(doc_cfg))]
  5. use anyhow::Context;
  6. pub use anyhow::Result;
  7. use cargo_toml::Manifest;
  8. use heck::AsShoutySnakeCase;
  9. use tauri_utils::{
  10. config::{BundleResources, Config, WebviewInstallMode},
  11. resources::{external_binaries, ResourcePaths},
  12. };
  13. use std::path::{Path, PathBuf};
  14. mod allowlist;
  15. #[cfg(feature = "codegen")]
  16. mod codegen;
  17. mod static_vcruntime;
  18. #[cfg(feature = "codegen")]
  19. #[cfg_attr(doc_cfg, doc(cfg(feature = "codegen")))]
  20. pub use codegen::context::CodegenContext;
  21. fn copy_file(from: impl AsRef<Path>, to: impl AsRef<Path>) -> Result<()> {
  22. let from = from.as_ref();
  23. let to = to.as_ref();
  24. if !from.exists() {
  25. return Err(anyhow::anyhow!("{:?} does not exist", from));
  26. }
  27. if !from.is_file() {
  28. return Err(anyhow::anyhow!("{:?} is not a file", from));
  29. }
  30. let dest_dir = to.parent().expect("No data in parent");
  31. std::fs::create_dir_all(dest_dir)?;
  32. std::fs::copy(from, to)?;
  33. Ok(())
  34. }
  35. fn copy_binaries(
  36. binaries: ResourcePaths,
  37. target_triple: &str,
  38. path: &Path,
  39. package_name: Option<&String>,
  40. ) -> Result<()> {
  41. for src in binaries {
  42. let src = src?;
  43. println!("cargo:rerun-if-changed={}", src.display());
  44. let file_name = src
  45. .file_name()
  46. .expect("failed to extract external binary filename")
  47. .to_string_lossy()
  48. .replace(&format!("-{target_triple}"), "");
  49. if package_name.map_or(false, |n| n == &file_name) {
  50. return Err(anyhow::anyhow!(
  51. "Cannot define a sidecar with the same name as the Cargo package name `{}`. Please change the sidecar name in the filesystem and the Tauri configuration.",
  52. file_name
  53. ));
  54. }
  55. let dest = path.join(file_name);
  56. if dest.exists() {
  57. std::fs::remove_file(&dest).unwrap();
  58. }
  59. copy_file(&src, &dest)?;
  60. }
  61. Ok(())
  62. }
  63. /// Copies resources to a path.
  64. fn copy_resources(resources: ResourcePaths<'_>, path: &Path) -> Result<()> {
  65. for resource in resources.iter() {
  66. let resource = resource?;
  67. println!("cargo:rerun-if-changed={}", resource.path().display());
  68. copy_file(resource.path(), path.join(resource.target()))?;
  69. }
  70. Ok(())
  71. }
  72. #[cfg(unix)]
  73. fn symlink_dir(src: &Path, dst: &Path) -> std::io::Result<()> {
  74. std::os::unix::fs::symlink(src, dst)
  75. }
  76. /// Makes a symbolic link to a directory.
  77. #[cfg(windows)]
  78. fn symlink_dir(src: &Path, dst: &Path) -> std::io::Result<()> {
  79. std::os::windows::fs::symlink_dir(src, dst)
  80. }
  81. /// Makes a symbolic link to a file.
  82. #[cfg(unix)]
  83. fn symlink_file(src: &Path, dst: &Path) -> std::io::Result<()> {
  84. std::os::unix::fs::symlink(src, dst)
  85. }
  86. /// Makes a symbolic link to a file.
  87. #[cfg(windows)]
  88. fn symlink_file(src: &Path, dst: &Path) -> std::io::Result<()> {
  89. std::os::windows::fs::symlink_file(src, dst)
  90. }
  91. fn copy_dir(from: &Path, to: &Path) -> Result<()> {
  92. for entry in walkdir::WalkDir::new(from) {
  93. let entry = entry?;
  94. debug_assert!(entry.path().starts_with(from));
  95. let rel_path = entry.path().strip_prefix(from)?;
  96. let dest_path = to.join(rel_path);
  97. if entry.file_type().is_symlink() {
  98. let target = std::fs::read_link(entry.path())?;
  99. if entry.path().is_dir() {
  100. symlink_dir(&target, &dest_path)?;
  101. } else {
  102. symlink_file(&target, &dest_path)?;
  103. }
  104. } else if entry.file_type().is_dir() {
  105. std::fs::create_dir(dest_path)?;
  106. } else {
  107. std::fs::copy(entry.path(), dest_path)?;
  108. }
  109. }
  110. Ok(())
  111. }
  112. // Copies the framework under `{src_dir}/{framework}.framework` to `{dest_dir}/{framework}.framework`.
  113. fn copy_framework_from(src_dir: &Path, framework: &str, dest_dir: &Path) -> Result<bool> {
  114. let src_name = format!("{}.framework", framework);
  115. let src_path = src_dir.join(&src_name);
  116. if src_path.exists() {
  117. copy_dir(&src_path, &dest_dir.join(&src_name))?;
  118. Ok(true)
  119. } else {
  120. Ok(false)
  121. }
  122. }
  123. // Copies the macOS application bundle frameworks to the target folder
  124. fn copy_frameworks(dest_dir: &Path, frameworks: &[String]) -> Result<()> {
  125. std::fs::create_dir_all(dest_dir).with_context(|| {
  126. format!(
  127. "Failed to create frameworks output directory at {:?}",
  128. dest_dir
  129. )
  130. })?;
  131. for framework in frameworks.iter() {
  132. if framework.ends_with(".framework") {
  133. let src_path = PathBuf::from(framework);
  134. let src_name = src_path
  135. .file_name()
  136. .expect("Couldn't get framework filename");
  137. let dest_path = dest_dir.join(src_name);
  138. copy_dir(&src_path, &dest_path)?;
  139. continue;
  140. } else if framework.ends_with(".dylib") {
  141. let src_path = PathBuf::from(framework);
  142. if !src_path.exists() {
  143. return Err(anyhow::anyhow!("Library not found: {}", framework));
  144. }
  145. let src_name = src_path.file_name().expect("Couldn't get library filename");
  146. let dest_path = dest_dir.join(src_name);
  147. copy_file(&src_path, &dest_path)?;
  148. continue;
  149. } else if framework.contains('/') {
  150. return Err(anyhow::anyhow!(
  151. "Framework path should have .framework extension: {}",
  152. framework
  153. ));
  154. }
  155. if let Some(home_dir) = dirs_next::home_dir() {
  156. if copy_framework_from(&home_dir.join("Library/Frameworks/"), framework, dest_dir)? {
  157. continue;
  158. }
  159. }
  160. if copy_framework_from(&PathBuf::from("/Library/Frameworks/"), framework, dest_dir)?
  161. || copy_framework_from(
  162. &PathBuf::from("/Network/Library/Frameworks/"),
  163. framework,
  164. dest_dir,
  165. )?
  166. {
  167. continue;
  168. }
  169. }
  170. Ok(())
  171. }
  172. // checks if the given Cargo feature is enabled.
  173. fn has_feature(feature: &str) -> bool {
  174. // when a feature is enabled, Cargo sets the `CARGO_FEATURE_<name` env var to 1
  175. // https://doc.rust-lang.org/cargo/reference/environment-variables.html#environment-variables-cargo-sets-for-build-scripts
  176. std::env::var(format!("CARGO_FEATURE_{}", AsShoutySnakeCase(feature)))
  177. .map(|x| x == "1")
  178. .unwrap_or(false)
  179. }
  180. // creates a cfg alias if `has_feature` is true.
  181. // `alias` must be a snake case string.
  182. fn cfg_alias(alias: &str, has_feature: bool) {
  183. println!("cargo:rustc-check-cfg=cfg({alias})");
  184. if has_feature {
  185. println!("cargo:rustc-cfg={alias}");
  186. }
  187. }
  188. /// Attributes used on Windows.
  189. #[allow(dead_code)]
  190. #[derive(Debug, Default)]
  191. pub struct WindowsAttributes {
  192. window_icon_path: Option<PathBuf>,
  193. /// The path to the sdk location.
  194. ///
  195. /// For the GNU toolkit this has to be the path where MinGW put windres.exe and ar.exe.
  196. /// This could be something like: "C:\Program Files\mingw-w64\x86_64-5.3.0-win32-seh-rt_v4-rev0\mingw64\bin"
  197. ///
  198. /// For MSVC the Windows SDK has to be installed. It comes with the resource compiler rc.exe.
  199. /// This should be set to the root directory of the Windows SDK, e.g., "C:\Program Files (x86)\Windows Kits\10" or,
  200. /// if multiple 10 versions are installed, set it directly to the correct bin directory "C:\Program Files (x86)\Windows Kits\10\bin\10.0.14393.0\x64"
  201. ///
  202. /// If it is left unset, it will look up a path in the registry, i.e. HKLM\SOFTWARE\Microsoft\Windows Kits\Installed Roots
  203. sdk_dir: Option<PathBuf>,
  204. /// A string containing an [application manifest] to be included with the application on Windows.
  205. ///
  206. /// Defaults to:
  207. /// ```text
  208. #[doc = include_str!("window-app-manifest.xml")]
  209. /// ```
  210. ///
  211. /// ## Warning
  212. ///
  213. /// if you are using tauri's dialog APIs, you need to specify a dependency on Common Control v6 by adding the following to your custom manifest:
  214. /// ```text
  215. /// <dependency>
  216. /// <dependentAssembly>
  217. /// <assemblyIdentity
  218. /// type="win32"
  219. /// name="Microsoft.Windows.Common-Controls"
  220. /// version="6.0.0.0"
  221. /// processorArchitecture="*"
  222. /// publicKeyToken="6595b64144ccf1df"
  223. /// language="*"
  224. /// />
  225. /// </dependentAssembly>
  226. /// </dependency>
  227. /// ```
  228. ///
  229. /// [application manifest]: https://learn.microsoft.com/en-us/windows/win32/sbscs/application-manifests
  230. app_manifest: Option<String>,
  231. }
  232. impl WindowsAttributes {
  233. /// Creates the default attribute set.
  234. pub fn new() -> Self {
  235. Self::default()
  236. }
  237. /// Sets the icon to use on the window. Currently only used on Windows.
  238. /// It must be in `ico` format. Defaults to `icons/icon.ico`.
  239. #[must_use]
  240. pub fn window_icon_path<P: AsRef<Path>>(mut self, window_icon_path: P) -> Self {
  241. self
  242. .window_icon_path
  243. .replace(window_icon_path.as_ref().into());
  244. self
  245. }
  246. /// Sets the sdk dir for windows. Currently only used on Windows. This must be a valid UTF-8
  247. /// path. Defaults to whatever the `winres` crate determines is best.
  248. #[must_use]
  249. pub fn sdk_dir<P: AsRef<Path>>(mut self, sdk_dir: P) -> Self {
  250. self.sdk_dir = Some(sdk_dir.as_ref().into());
  251. self
  252. }
  253. /// Sets the [application manifest] to be included with the application on Windows.
  254. ///
  255. /// Defaults to:
  256. /// ```text
  257. #[doc = include_str!("window-app-manifest.xml")]
  258. /// ```
  259. ///
  260. /// ## Warning
  261. ///
  262. /// if you are using tauri's dialog APIs, you need to specify a dependency on Common Control v6 by adding the following to your custom manifest:
  263. /// ```text
  264. /// <dependency>
  265. /// <dependentAssembly>
  266. /// <assemblyIdentity
  267. /// type="win32"
  268. /// name="Microsoft.Windows.Common-Controls"
  269. /// version="6.0.0.0"
  270. /// processorArchitecture="*"
  271. /// publicKeyToken="6595b64144ccf1df"
  272. /// language="*"
  273. /// />
  274. /// </dependentAssembly>
  275. /// </dependency>
  276. /// ```
  277. ///
  278. /// # Example
  279. ///
  280. /// The following manifest will brand the exe as requesting administrator privileges.
  281. /// Thus, everytime it is executed, a Windows UAC dialog will appear.
  282. ///
  283. /// ```rust,no_run
  284. /// let mut windows = tauri_build::WindowsAttributes::new();
  285. /// windows = windows.app_manifest(r#"
  286. /// <assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
  287. /// <trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
  288. /// <security>
  289. /// <requestedPrivileges>
  290. /// <requestedExecutionLevel level="requireAdministrator" uiAccess="false" />
  291. /// </requestedPrivileges>
  292. /// </security>
  293. /// </trustInfo>
  294. /// </assembly>
  295. /// "#);
  296. /// let attrs = tauri_build::Attributes::new().windows_attributes(windows);
  297. /// tauri_build::try_build(attrs).expect("failed to run build script");
  298. /// ```
  299. ///
  300. /// Note that you can move the manifest contents to a separate file and use `include_str!("manifest.xml")`
  301. /// instead of the inline string.
  302. ///
  303. /// [manifest]: https://learn.microsoft.com/en-us/windows/win32/sbscs/application-manifests
  304. #[must_use]
  305. pub fn app_manifest<S: AsRef<str>>(mut self, manifest: S) -> Self {
  306. self.app_manifest = Some(manifest.as_ref().to_string());
  307. self
  308. }
  309. }
  310. /// The attributes used on the build.
  311. #[derive(Debug, Default)]
  312. pub struct Attributes {
  313. #[allow(dead_code)]
  314. windows_attributes: WindowsAttributes,
  315. }
  316. impl Attributes {
  317. /// Creates the default attribute set.
  318. pub fn new() -> Self {
  319. Self::default()
  320. }
  321. /// Sets the icon to use on the window. Currently only used on Windows.
  322. #[must_use]
  323. pub fn windows_attributes(mut self, windows_attributes: WindowsAttributes) -> Self {
  324. self.windows_attributes = windows_attributes;
  325. self
  326. }
  327. }
  328. /// Run all build time helpers for your Tauri Application.
  329. ///
  330. /// The current helpers include the following:
  331. /// * Generates a Windows Resource file when targeting Windows.
  332. ///
  333. /// # Platforms
  334. ///
  335. /// [`build()`] should be called inside of `build.rs` regardless of the platform:
  336. /// * New helpers may target more platforms in the future.
  337. /// * Platform specific code is handled by the helpers automatically.
  338. /// * A build script is required in order to activate some cargo environmental variables that are
  339. /// used when generating code and embedding assets - so [`build()`] may as well be called.
  340. ///
  341. /// In short, this is saying don't put the call to [`build()`] behind a `#[cfg(windows)]`.
  342. ///
  343. /// # Panics
  344. ///
  345. /// If any of the build time helpers fail, they will [`std::panic!`] with the related error message.
  346. /// This is typically desirable when running inside a build script; see [`try_build`] for no panics.
  347. pub fn build() {
  348. if let Err(error) = try_build(Attributes::default()) {
  349. let error = format!("{error:#}");
  350. println!("{error}");
  351. if error.starts_with("unknown field") {
  352. print!("found an unknown configuration field. This usually means that you are using a CLI version that is newer than `tauri-build` and is incompatible. ");
  353. println!(
  354. "Please try updating the Rust crates by running `cargo update` in the Tauri app folder."
  355. );
  356. }
  357. std::process::exit(1);
  358. }
  359. }
  360. /// Non-panicking [`build()`].
  361. #[allow(unused_variables)]
  362. pub fn try_build(attributes: Attributes) -> Result<()> {
  363. use anyhow::anyhow;
  364. println!("cargo:rerun-if-env-changed=TAURI_CONFIG");
  365. println!("cargo:rerun-if-changed=tauri.conf.json");
  366. #[cfg(feature = "config-json5")]
  367. println!("cargo:rerun-if-changed=tauri.conf.json5");
  368. #[cfg(feature = "config-toml")]
  369. println!("cargo:rerun-if-changed=Tauri.toml");
  370. let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap();
  371. let mobile = target_os == "ios" || target_os == "android";
  372. cfg_alias("desktop", !mobile);
  373. cfg_alias("mobile", mobile);
  374. let mut config = serde_json::from_value(tauri_utils::config::parse::read_from(
  375. std::env::current_dir().unwrap(),
  376. )?)?;
  377. if let Ok(env) = std::env::var("TAURI_CONFIG") {
  378. let merge_config: serde_json::Value = serde_json::from_str(&env)?;
  379. json_patch::merge(&mut config, &merge_config);
  380. }
  381. let config: Config = serde_json::from_value(config)?;
  382. cfg_alias("dev", !has_feature("custom-protocol"));
  383. let ws_path = get_workspace_dir()?;
  384. let mut manifest =
  385. Manifest::<cargo_toml::Value>::from_slice_with_metadata(&std::fs::read("Cargo.toml")?)?;
  386. allowlist::check(&config, &mut manifest)?;
  387. let target_triple = std::env::var("TARGET").unwrap();
  388. let out_dir = PathBuf::from(std::env::var("OUT_DIR").unwrap());
  389. // TODO: far from ideal, but there's no other way to get the target dir, see <https://github.com/rust-lang/cargo/issues/5457>
  390. let target_dir = out_dir
  391. .parent()
  392. .unwrap()
  393. .parent()
  394. .unwrap()
  395. .parent()
  396. .unwrap();
  397. if let Some(paths) = &config.tauri.bundle.external_bin {
  398. copy_binaries(
  399. ResourcePaths::new(external_binaries(paths, &target_triple).as_slice(), true),
  400. &target_triple,
  401. target_dir,
  402. manifest.package.as_ref().map(|p| &p.name),
  403. )?;
  404. }
  405. #[allow(unused_mut, clippy::redundant_clone)]
  406. let mut resources = config
  407. .tauri
  408. .bundle
  409. .resources
  410. .clone()
  411. .unwrap_or_else(|| BundleResources::List(Vec::new()));
  412. if target_triple.contains("windows") {
  413. if let Some(fixed_webview2_runtime_path) =
  414. match &config.tauri.bundle.windows.webview_fixed_runtime_path {
  415. Some(path) => Some(path),
  416. None => match &config.tauri.bundle.windows.webview_install_mode {
  417. WebviewInstallMode::FixedRuntime { path } => Some(path),
  418. _ => None,
  419. },
  420. }
  421. {
  422. resources.push(fixed_webview2_runtime_path.display().to_string());
  423. }
  424. }
  425. match resources {
  426. BundleResources::List(res) => {
  427. copy_resources(ResourcePaths::new(res.as_slice(), true), target_dir)?
  428. }
  429. BundleResources::Map(map) => copy_resources(ResourcePaths::from_map(&map, true), target_dir)?,
  430. }
  431. if target_triple.contains("darwin") {
  432. if let Some(frameworks) = &config.tauri.bundle.macos.frameworks {
  433. if !frameworks.is_empty() {
  434. let frameworks_dir = target_dir.parent().unwrap().join("Frameworks");
  435. let _ = std::fs::remove_dir_all(&frameworks_dir);
  436. // copy frameworks to the root `target` folder (instead of `target/debug` for instance)
  437. // because the rpath is set to `@executable_path/../Frameworks`.
  438. copy_frameworks(&frameworks_dir, frameworks)?;
  439. // If we have frameworks, we need to set the @rpath
  440. // https://github.com/tauri-apps/tauri/issues/7710
  441. println!("cargo:rustc-link-arg=-Wl,-rpath,@executable_path/../Frameworks");
  442. }
  443. }
  444. if let Some(version) = &config.tauri.bundle.macos.minimum_system_version {
  445. println!("cargo:rustc-env=MACOSX_DEPLOYMENT_TARGET={version}");
  446. }
  447. }
  448. if target_triple.contains("windows") {
  449. use semver::Version;
  450. use tauri_winres::{VersionInfo, WindowsResource};
  451. fn find_icon<F: Fn(&&String) -> bool>(config: &Config, predicate: F, default: &str) -> PathBuf {
  452. let icon_path = config
  453. .tauri
  454. .bundle
  455. .icon
  456. .iter()
  457. .find(|i| predicate(i))
  458. .cloned()
  459. .unwrap_or_else(|| default.to_string());
  460. icon_path.into()
  461. }
  462. let window_icon_path = attributes
  463. .windows_attributes
  464. .window_icon_path
  465. .unwrap_or_else(|| find_icon(&config, |i| i.ends_with(".ico"), "icons/icon.ico"));
  466. if window_icon_path.exists() {
  467. let mut res = WindowsResource::new();
  468. if let Some(manifest) = attributes.windows_attributes.app_manifest {
  469. res.set_manifest(&manifest);
  470. } else {
  471. res.set_manifest(include_str!("window-app-manifest.xml"));
  472. }
  473. if let Some(version_str) = &config.package.version {
  474. if let Ok(v) = Version::parse(version_str) {
  475. let version = v.major << 48 | v.minor << 32 | v.patch << 16;
  476. res.set_version_info(VersionInfo::FILEVERSION, version);
  477. res.set_version_info(VersionInfo::PRODUCTVERSION, version);
  478. }
  479. res.set("FileVersion", version_str);
  480. res.set("ProductVersion", version_str);
  481. }
  482. if let Some(product_name) = &config.package.product_name {
  483. res.set("ProductName", product_name);
  484. }
  485. if let Some(short_description) = &config.tauri.bundle.short_description {
  486. res.set("FileDescription", short_description);
  487. }
  488. if let Some(copyright) = &config.tauri.bundle.copyright {
  489. res.set("LegalCopyright", copyright);
  490. }
  491. res.set_icon_with_id(&window_icon_path.display().to_string(), "32512");
  492. res.compile().with_context(|| {
  493. format!(
  494. "failed to compile `{}` into a Windows Resource file during tauri-build",
  495. window_icon_path.display()
  496. )
  497. })?;
  498. } else {
  499. return Err(anyhow!(format!(
  500. "`{}` not found; required for generating a Windows Resource file during tauri-build",
  501. window_icon_path.display()
  502. )));
  503. }
  504. let target_env = std::env::var("CARGO_CFG_TARGET_ENV").unwrap();
  505. match target_env.as_str() {
  506. "gnu" => {
  507. let target_arch = match std::env::var("CARGO_CFG_TARGET_ARCH").unwrap().as_str() {
  508. "x86_64" => Some("x64"),
  509. "x86" => Some("x86"),
  510. "aarch64" => Some("arm64"),
  511. arch => None,
  512. };
  513. if let Some(target_arch) = target_arch {
  514. for entry in std::fs::read_dir(target_dir.join("build"))? {
  515. let path = entry?.path();
  516. let webview2_loader_path = path
  517. .join("out")
  518. .join(target_arch)
  519. .join("WebView2Loader.dll");
  520. if path.to_string_lossy().contains("webview2-com-sys") && webview2_loader_path.exists()
  521. {
  522. std::fs::copy(webview2_loader_path, target_dir.join("WebView2Loader.dll"))?;
  523. break;
  524. }
  525. }
  526. }
  527. }
  528. "msvc" => {
  529. if std::env::var("STATIC_VCRUNTIME").map_or(false, |v| v == "true") {
  530. static_vcruntime::build();
  531. }
  532. }
  533. _ => (),
  534. }
  535. }
  536. Ok(())
  537. }
  538. #[derive(serde::Deserialize)]
  539. struct CargoMetadata {
  540. workspace_root: PathBuf,
  541. }
  542. fn get_workspace_dir() -> Result<PathBuf> {
  543. let output = std::process::Command::new("cargo")
  544. .args(["metadata", "--no-deps", "--format-version", "1"])
  545. .output()?;
  546. if !output.status.success() {
  547. return Err(anyhow::anyhow!(
  548. "cargo metadata command exited with a non zero exit code: {}",
  549. String::from_utf8(output.stderr)?
  550. ));
  551. }
  552. Ok(serde_json::from_slice::<CargoMetadata>(&output.stdout)?.workspace_root)
  553. }