lib.rs 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664
  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::{Dependency, Manifest};
  8. use heck::AsShoutySnakeCase;
  9. use tauri_utils::{
  10. config::Config,
  11. resources::{external_binaries, resource_relpath, ResourcePaths},
  12. };
  13. use std::{
  14. collections::HashMap,
  15. env::var_os,
  16. fs::{read_to_string, write},
  17. path::{Path, PathBuf},
  18. };
  19. #[cfg(feature = "codegen")]
  20. mod codegen;
  21. /// Mobile build functions.
  22. pub mod mobile;
  23. mod static_vcruntime;
  24. #[cfg(feature = "codegen")]
  25. #[cfg_attr(doc_cfg, doc(cfg(feature = "codegen")))]
  26. pub use codegen::context::CodegenContext;
  27. fn copy_file(from: impl AsRef<Path>, to: impl AsRef<Path>) -> Result<()> {
  28. let from = from.as_ref();
  29. let to = to.as_ref();
  30. if !from.exists() {
  31. return Err(anyhow::anyhow!("{:?} does not exist", from));
  32. }
  33. if !from.is_file() {
  34. return Err(anyhow::anyhow!("{:?} is not a file", from));
  35. }
  36. let dest_dir = to.parent().expect("No data in parent");
  37. std::fs::create_dir_all(dest_dir)?;
  38. std::fs::copy(from, to)?;
  39. Ok(())
  40. }
  41. fn copy_binaries(
  42. binaries: ResourcePaths,
  43. target_triple: &str,
  44. path: &Path,
  45. package_name: Option<&String>,
  46. ) -> Result<()> {
  47. for src in binaries {
  48. let src = src?;
  49. println!("cargo:rerun-if-changed={}", src.display());
  50. let file_name = src
  51. .file_name()
  52. .expect("failed to extract external binary filename")
  53. .to_string_lossy()
  54. .replace(&format!("-{target_triple}"), "");
  55. if package_name.map_or(false, |n| n == &file_name) {
  56. return Err(anyhow::anyhow!(
  57. "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.",
  58. file_name
  59. ));
  60. }
  61. let dest = path.join(file_name);
  62. if dest.exists() {
  63. std::fs::remove_file(&dest).unwrap();
  64. }
  65. copy_file(&src, &dest)?;
  66. }
  67. Ok(())
  68. }
  69. /// Copies resources to a path.
  70. fn copy_resources(resources: ResourcePaths<'_>, path: &Path) -> Result<()> {
  71. for src in resources {
  72. let src = src?;
  73. println!("cargo:rerun-if-changed={}", src.display());
  74. let dest = path.join(resource_relpath(&src));
  75. copy_file(&src, dest)?;
  76. }
  77. Ok(())
  78. }
  79. // checks if the given Cargo feature is enabled.
  80. fn has_feature(feature: &str) -> bool {
  81. // when a feature is enabled, Cargo sets the `CARGO_FEATURE_<name` env var to 1
  82. // https://doc.rust-lang.org/cargo/reference/environment-variables.html#environment-variables-cargo-sets-for-build-scripts
  83. std::env::var(format!("CARGO_FEATURE_{}", AsShoutySnakeCase(feature)))
  84. .map(|x| x == "1")
  85. .unwrap_or(false)
  86. }
  87. // creates a cfg alias if `has_feature` is true.
  88. // `alias` must be a snake case string.
  89. fn cfg_alias(alias: &str, has_feature: bool) {
  90. if has_feature {
  91. println!("cargo:rustc-cfg={alias}");
  92. }
  93. }
  94. /// Attributes used on Windows.
  95. #[allow(dead_code)]
  96. #[derive(Debug, Default)]
  97. pub struct WindowsAttributes {
  98. window_icon_path: Option<PathBuf>,
  99. /// The path to the sdk location.
  100. ///
  101. /// For the GNU toolkit this has to be the path where MinGW put windres.exe and ar.exe.
  102. /// This could be something like: "C:\Program Files\mingw-w64\x86_64-5.3.0-win32-seh-rt_v4-rev0\mingw64\bin"
  103. ///
  104. /// For MSVC the Windows SDK has to be installed. It comes with the resource compiler rc.exe.
  105. /// This should be set to the root directory of the Windows SDK, e.g., "C:\Program Files (x86)\Windows Kits\10" or,
  106. /// 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"
  107. ///
  108. /// If it is left unset, it will look up a path in the registry, i.e. HKLM\SOFTWARE\Microsoft\Windows Kits\Installed Roots
  109. sdk_dir: Option<PathBuf>,
  110. /// A string containing an [application manifest] to be included with the application on Windows.
  111. ///
  112. /// Defaults to:
  113. /// ```ignore
  114. #[doc = include_str!("window-app-manifest.xml")]
  115. /// ```
  116. ///
  117. /// [application manifest]: https://learn.microsoft.com/en-us/windows/win32/sbscs/application-manifests
  118. app_manifest: Option<String>,
  119. }
  120. impl WindowsAttributes {
  121. /// Creates the default attribute set.
  122. pub fn new() -> Self {
  123. Self::default()
  124. }
  125. /// Sets the icon to use on the window. Currently only used on Windows.
  126. /// It must be in `ico` format. Defaults to `icons/icon.ico`.
  127. #[must_use]
  128. pub fn window_icon_path<P: AsRef<Path>>(mut self, window_icon_path: P) -> Self {
  129. self
  130. .window_icon_path
  131. .replace(window_icon_path.as_ref().into());
  132. self
  133. }
  134. /// Sets the sdk dir for windows. Currently only used on Windows. This must be a valid UTF-8
  135. /// path. Defaults to whatever the `winres` crate determines is best.
  136. #[must_use]
  137. pub fn sdk_dir<P: AsRef<Path>>(mut self, sdk_dir: P) -> Self {
  138. self.sdk_dir = Some(sdk_dir.as_ref().into());
  139. self
  140. }
  141. /// Sets the Windows app [manifest].
  142. ///
  143. /// # Example
  144. ///
  145. /// The following manifest will brand the exe as requesting administrator privileges.
  146. /// Thus, everytime it is executed, a Windows UAC dialog will appear.
  147. ///
  148. /// Note that you can move the manifest contents to a separate file and use `include_str!("manifest.xml")`
  149. /// instead of the inline string.
  150. ///
  151. /// ```rust,no_run
  152. /// let mut windows = tauri_build::WindowsAttributes::new();
  153. /// windows = windows.app_manifest(r#"
  154. /// <assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
  155. /// <trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
  156. /// <security>
  157. /// <requestedPrivileges>
  158. /// <requestedExecutionLevel level="requireAdministrator" uiAccess="false" />
  159. /// </requestedPrivileges>
  160. /// </security>
  161. /// </trustInfo>
  162. /// </assembly>
  163. /// "#);
  164. /// tauri_build::try_build(
  165. /// tauri_build::Attributes::new().windows_attributes(windows)
  166. /// ).expect("failed to run build script");
  167. /// ```
  168. ///
  169. /// Defaults to:
  170. /// ```ignore
  171. #[doc = include_str!("window-app-manifest.xml")]
  172. /// [manifest]: https://learn.microsoft.com/en-us/windows/win32/sbscs/application-manifests
  173. /// ```
  174. #[must_use]
  175. pub fn app_manifest<S: AsRef<str>>(mut self, manifest: S) -> Self {
  176. self.app_manifest = Some(manifest.as_ref().to_string());
  177. self
  178. }
  179. }
  180. /// The attributes used on the build.
  181. #[derive(Debug, Default)]
  182. pub struct Attributes {
  183. #[allow(dead_code)]
  184. windows_attributes: WindowsAttributes,
  185. }
  186. impl Attributes {
  187. /// Creates the default attribute set.
  188. pub fn new() -> Self {
  189. Self::default()
  190. }
  191. /// Sets the icon to use on the window. Currently only used on Windows.
  192. #[must_use]
  193. pub fn windows_attributes(mut self, windows_attributes: WindowsAttributes) -> Self {
  194. self.windows_attributes = windows_attributes;
  195. self
  196. }
  197. }
  198. /// Run all build time helpers for your Tauri Application.
  199. ///
  200. /// The current helpers include the following:
  201. /// * Generates a Windows Resource file when targeting Windows.
  202. ///
  203. /// # Platforms
  204. ///
  205. /// [`build()`] should be called inside of `build.rs` regardless of the platform:
  206. /// * New helpers may target more platforms in the future.
  207. /// * Platform specific code is handled by the helpers automatically.
  208. /// * A build script is required in order to activate some cargo environmental variables that are
  209. /// used when generating code and embedding assets - so [`build()`] may as well be called.
  210. ///
  211. /// In short, this is saying don't put the call to [`build()`] behind a `#[cfg(windows)]`.
  212. ///
  213. /// # Panics
  214. ///
  215. /// If any of the build time helpers fail, they will [`std::panic!`] with the related error message.
  216. /// This is typically desirable when running inside a build script; see [`try_build`] for no panics.
  217. pub fn build() {
  218. if let Err(error) = try_build(Attributes::default()) {
  219. let error = format!("{error:#}");
  220. println!("{error}");
  221. if error.starts_with("unknown field") {
  222. 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. ");
  223. println!(
  224. "Please try updating the Rust crates by running `cargo update` in the Tauri app folder."
  225. );
  226. }
  227. std::process::exit(1);
  228. }
  229. }
  230. /// Non-panicking [`build()`].
  231. #[allow(unused_variables)]
  232. pub fn try_build(attributes: Attributes) -> Result<()> {
  233. use anyhow::anyhow;
  234. println!("cargo:rerun-if-env-changed=TAURI_CONFIG");
  235. println!("cargo:rerun-if-changed=tauri.conf.json");
  236. #[cfg(feature = "config-json5")]
  237. println!("cargo:rerun-if-changed=tauri.conf.json5");
  238. #[cfg(feature = "config-toml")]
  239. println!("cargo:rerun-if-changed=Tauri.toml");
  240. let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap();
  241. let mobile = target_os == "ios" || target_os == "android";
  242. cfg_alias("desktop", !mobile);
  243. cfg_alias("mobile", mobile);
  244. let mut config = serde_json::from_value(tauri_utils::config::parse::read_from(
  245. std::env::current_dir().unwrap(),
  246. )?)?;
  247. if let Ok(env) = std::env::var("TAURI_CONFIG") {
  248. let merge_config: serde_json::Value = serde_json::from_str(&env)?;
  249. json_patch::merge(&mut config, &merge_config);
  250. }
  251. let config: Config = serde_json::from_value(config)?;
  252. let s = config.tauri.bundle.identifier.split('.');
  253. let last = s.clone().count() - 1;
  254. let mut android_package_prefix = String::new();
  255. for (i, w) in s.enumerate() {
  256. if i == 0 || i != last {
  257. android_package_prefix.push_str(w);
  258. android_package_prefix.push('_');
  259. }
  260. }
  261. android_package_prefix.pop();
  262. println!("cargo:rustc-env=TAURI_ANDROID_PACKAGE_PREFIX={android_package_prefix}");
  263. if let Some(project_dir) = var_os("TAURI_ANDROID_PROJECT_PATH").map(PathBuf::from) {
  264. let gradle_settings_path = project_dir.join("tauri.settings.gradle");
  265. let app_build_gradle_path = project_dir.join("app").join("tauri.build.gradle.kts");
  266. let mut gradle_settings =
  267. "// THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.\n".to_string();
  268. let mut app_build_gradle = "// THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
  269. val implementation by configurations
  270. dependencies {"
  271. .to_string();
  272. let plugins_json_path = project_dir.join(".tauri").join("plugins.json");
  273. let mut plugins: HashMap<String, mobile::PluginMetadata> = if plugins_json_path.exists() {
  274. let s = read_to_string(&plugins_json_path)?;
  275. serde_json::from_str(&s)?
  276. } else {
  277. Default::default()
  278. };
  279. plugins.insert(
  280. "tauri-android".into(),
  281. mobile::PluginMetadata {
  282. path: var_os("DEP_TAURI_ANDROID_LIBRARY_PATH").map(PathBuf::from).expect("missing `DEP_TAURI_ANDROID_LIBRARY_PATH` environment variable; did you add `tauri` as a dependency to this crate?"),
  283. },
  284. );
  285. for (plugin_name, plugin) in plugins {
  286. gradle_settings.push_str(&format!("include ':{plugin_name}'"));
  287. gradle_settings.push('\n');
  288. gradle_settings.push_str(&format!(
  289. "project(':{plugin_name}').projectDir = new File({:?})",
  290. tauri_utils::display_path(plugin.path)
  291. ));
  292. gradle_settings.push('\n');
  293. app_build_gradle.push('\n');
  294. app_build_gradle.push_str(&format!(r#" implementation(project(":{plugin_name}"))"#));
  295. }
  296. app_build_gradle.push_str("\n}");
  297. write(&gradle_settings_path, gradle_settings)
  298. .context("failed to write tauri.settings.gradle")?;
  299. write(&app_build_gradle_path, app_build_gradle)
  300. .context("failed to write tauri.build.gradle.kts")?;
  301. }
  302. cfg_alias("dev", !has_feature("custom-protocol"));
  303. let ws_path = get_workspace_dir()?;
  304. let mut manifest =
  305. Manifest::<cargo_toml::Value>::from_slice_with_metadata(&std::fs::read("Cargo.toml")?)?;
  306. if let Ok(ws_manifest) = Manifest::from_path(ws_path.join("Cargo.toml")) {
  307. Manifest::complete_from_path_and_workspace(
  308. &mut manifest,
  309. Path::new("Cargo.toml"),
  310. Some((&ws_manifest, ws_path.as_path())),
  311. )?;
  312. } else {
  313. Manifest::complete_from_path(&mut manifest, Path::new("Cargo.toml"))?;
  314. }
  315. if let Some(tauri_build) = manifest.build_dependencies.remove("tauri-build") {
  316. let error_message = check_features(&config, tauri_build, true);
  317. if !error_message.is_empty() {
  318. return Err(anyhow!("
  319. The `tauri-build` dependency features on the `Cargo.toml` file does not match the allowlist defined under `tauri.conf.json`.
  320. Please run `tauri dev` or `tauri build` or {}.
  321. ", error_message));
  322. }
  323. }
  324. if let Some(tauri) = manifest.dependencies.remove("tauri") {
  325. let error_message = check_features(&config, tauri, false);
  326. if !error_message.is_empty() {
  327. return Err(anyhow!("
  328. The `tauri` dependency features on the `Cargo.toml` file does not match the allowlist defined under `tauri.conf.json`.
  329. Please run `tauri dev` or `tauri build` or {}.
  330. ", error_message));
  331. }
  332. }
  333. let target_triple = std::env::var("TARGET").unwrap();
  334. println!("cargo:rustc-env=TAURI_TARGET_TRIPLE={target_triple}");
  335. let out_dir = PathBuf::from(std::env::var("OUT_DIR").unwrap());
  336. // TODO: far from ideal, but there's no other way to get the target dir, see <https://github.com/rust-lang/cargo/issues/5457>
  337. let target_dir = out_dir
  338. .parent()
  339. .unwrap()
  340. .parent()
  341. .unwrap()
  342. .parent()
  343. .unwrap();
  344. if let Some(paths) = &config.tauri.bundle.external_bin {
  345. copy_binaries(
  346. ResourcePaths::new(external_binaries(paths, &target_triple).as_slice(), true),
  347. &target_triple,
  348. target_dir,
  349. manifest.package.as_ref().map(|p| &p.name),
  350. )?;
  351. }
  352. #[allow(unused_mut, clippy::redundant_clone)]
  353. let mut resources = config.tauri.bundle.resources.clone().unwrap_or_default();
  354. if target_triple.contains("windows") {
  355. if let Some(fixed_webview2_runtime_path) =
  356. &config.tauri.bundle.windows.webview_fixed_runtime_path
  357. {
  358. resources.push(fixed_webview2_runtime_path.display().to_string());
  359. }
  360. }
  361. copy_resources(ResourcePaths::new(resources.as_slice(), true), target_dir)?;
  362. if target_triple.contains("darwin") {
  363. if let Some(version) = &config.tauri.bundle.macos.minimum_system_version {
  364. println!("cargo:rustc-env=MACOSX_DEPLOYMENT_TARGET={version}");
  365. }
  366. }
  367. if target_triple.contains("windows") {
  368. use semver::Version;
  369. use tauri_winres::{VersionInfo, WindowsResource};
  370. fn find_icon<F: Fn(&&String) -> bool>(config: &Config, predicate: F, default: &str) -> PathBuf {
  371. let icon_path = config
  372. .tauri
  373. .bundle
  374. .icon
  375. .iter()
  376. .find(|i| predicate(i))
  377. .cloned()
  378. .unwrap_or_else(|| default.to_string());
  379. icon_path.into()
  380. }
  381. let window_icon_path = attributes
  382. .windows_attributes
  383. .window_icon_path
  384. .unwrap_or_else(|| find_icon(&config, |i| i.ends_with(".ico"), "icons/icon.ico"));
  385. if target_triple.contains("windows") {
  386. if window_icon_path.exists() {
  387. let mut res = WindowsResource::new();
  388. if let Some(manifest) = attributes.windows_attributes.app_manifest {
  389. res.set_manifest(&manifest);
  390. } else {
  391. res.set_manifest(include_str!("window-app-manifest.xml"));
  392. }
  393. if let Some(sdk_dir) = &attributes.windows_attributes.sdk_dir {
  394. if let Some(sdk_dir_str) = sdk_dir.to_str() {
  395. res.set_toolkit_path(sdk_dir_str);
  396. } else {
  397. return Err(anyhow!(
  398. "sdk_dir path is not valid; only UTF-8 characters are allowed"
  399. ));
  400. }
  401. }
  402. if let Some(version) = &config.package.version {
  403. if let Ok(v) = Version::parse(version) {
  404. let version = v.major << 48 | v.minor << 32 | v.patch << 16;
  405. res.set_version_info(VersionInfo::FILEVERSION, version);
  406. res.set_version_info(VersionInfo::PRODUCTVERSION, version);
  407. }
  408. res.set("FileVersion", version);
  409. res.set("ProductVersion", version);
  410. }
  411. if let Some(product_name) = &config.package.product_name {
  412. res.set("ProductName", product_name);
  413. res.set("FileDescription", product_name);
  414. }
  415. res.set_icon_with_id(&window_icon_path.display().to_string(), "32512");
  416. res.compile().with_context(|| {
  417. format!(
  418. "failed to compile `{}` into a Windows Resource file during tauri-build",
  419. window_icon_path.display()
  420. )
  421. })?;
  422. } else {
  423. return Err(anyhow!(format!(
  424. "`{}` not found; required for generating a Windows Resource file during tauri-build",
  425. window_icon_path.display()
  426. )));
  427. }
  428. }
  429. let target_env = std::env::var("CARGO_CFG_TARGET_ENV").unwrap();
  430. match target_env.as_str() {
  431. "gnu" => {
  432. let target_arch = match std::env::var("CARGO_CFG_TARGET_ARCH").unwrap().as_str() {
  433. "x86_64" => Some("x64"),
  434. "x86" => Some("x86"),
  435. "aarch64" => Some("arm64"),
  436. arch => None,
  437. };
  438. if let Some(target_arch) = target_arch {
  439. for entry in std::fs::read_dir(target_dir.join("build"))? {
  440. let path = entry?.path();
  441. let webview2_loader_path = path
  442. .join("out")
  443. .join(target_arch)
  444. .join("WebView2Loader.dll");
  445. if path.to_string_lossy().contains("webview2-com-sys") && webview2_loader_path.exists()
  446. {
  447. std::fs::copy(webview2_loader_path, target_dir.join("WebView2Loader.dll"))?;
  448. break;
  449. }
  450. }
  451. }
  452. }
  453. "msvc" => {
  454. if std::env::var("STATIC_VCRUNTIME").map_or(false, |v| v == "true") {
  455. static_vcruntime::build();
  456. }
  457. }
  458. _ => (),
  459. }
  460. }
  461. Ok(())
  462. }
  463. #[derive(Debug, Default, PartialEq, Eq)]
  464. struct Diff {
  465. remove: Vec<String>,
  466. add: Vec<String>,
  467. }
  468. fn features_diff(current: &[String], expected: &[String]) -> Diff {
  469. let mut remove = Vec::new();
  470. let mut add = Vec::new();
  471. for feature in current {
  472. if !expected.contains(feature) {
  473. remove.push(feature.clone());
  474. }
  475. }
  476. for feature in expected {
  477. if !current.contains(feature) {
  478. add.push(feature.clone());
  479. }
  480. }
  481. Diff { remove, add }
  482. }
  483. fn check_features(config: &Config, dependency: Dependency, is_tauri_build: bool) -> String {
  484. use tauri_utils::config::{PatternKind, TauriConfig};
  485. let features = match dependency {
  486. Dependency::Simple(_) => Vec::new(),
  487. Dependency::Detailed(dep) => dep.features,
  488. Dependency::Inherited(dep) => dep.features,
  489. };
  490. let all_cli_managed_features = if is_tauri_build {
  491. vec!["isolation"]
  492. } else {
  493. TauriConfig::all_features()
  494. };
  495. let expected = if is_tauri_build {
  496. match config.tauri.pattern {
  497. PatternKind::Isolation { .. } => vec!["isolation".to_string()],
  498. _ => vec![],
  499. }
  500. } else {
  501. config
  502. .tauri
  503. .features()
  504. .into_iter()
  505. .map(|f| f.to_string())
  506. .collect::<Vec<String>>()
  507. };
  508. let diff = features_diff(
  509. &features
  510. .into_iter()
  511. .filter(|f| all_cli_managed_features.contains(&f.as_str()))
  512. .collect::<Vec<String>>(),
  513. &expected,
  514. );
  515. let mut error_message = String::new();
  516. if !diff.remove.is_empty() {
  517. error_message.push_str("remove the `");
  518. error_message.push_str(&diff.remove.join(", "));
  519. error_message.push_str(if diff.remove.len() == 1 {
  520. "` feature"
  521. } else {
  522. "` features"
  523. });
  524. if !diff.add.is_empty() {
  525. error_message.push_str(" and ");
  526. }
  527. }
  528. if !diff.add.is_empty() {
  529. error_message.push_str("add the `");
  530. error_message.push_str(&diff.add.join(", "));
  531. error_message.push_str(if diff.add.len() == 1 {
  532. "` feature"
  533. } else {
  534. "` features"
  535. });
  536. }
  537. error_message
  538. }
  539. #[derive(serde::Deserialize)]
  540. struct CargoMetadata {
  541. workspace_root: PathBuf,
  542. }
  543. fn get_workspace_dir() -> Result<PathBuf> {
  544. let output = std::process::Command::new("cargo")
  545. .args(["metadata", "--no-deps", "--format-version", "1"])
  546. .output()?;
  547. if !output.status.success() {
  548. return Err(anyhow::anyhow!(
  549. "cargo metadata command exited with a non zero exit code: {}",
  550. String::from_utf8(output.stderr)?
  551. ));
  552. }
  553. Ok(serde_json::from_slice::<CargoMetadata>(&output.stdout)?.workspace_root)
  554. }
  555. #[cfg(test)]
  556. mod tests {
  557. use super::Diff;
  558. #[test]
  559. fn array_diff() {
  560. for (current, expected, result) in [
  561. (vec![], vec![], Default::default()),
  562. (
  563. vec!["a".into()],
  564. vec![],
  565. Diff {
  566. remove: vec!["a".into()],
  567. add: vec![],
  568. },
  569. ),
  570. (vec!["a".into()], vec!["a".into()], Default::default()),
  571. (
  572. vec!["a".into(), "b".into()],
  573. vec!["a".into()],
  574. Diff {
  575. remove: vec!["b".into()],
  576. add: vec![],
  577. },
  578. ),
  579. (
  580. vec!["a".into(), "b".into()],
  581. vec!["a".into(), "c".into()],
  582. Diff {
  583. remove: vec!["b".into()],
  584. add: vec!["c".into()],
  585. },
  586. ),
  587. ] {
  588. assert_eq!(super::features_diff(&current, &expected), result);
  589. }
  590. }
  591. }