rust.rs 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122
  1. // Copyright 2019-2023 Tauri Programme within The Commons Conservancy
  2. // SPDX-License-Identifier: Apache-2.0
  3. // SPDX-License-Identifier: MIT
  4. use std::{
  5. collections::HashMap,
  6. ffi::OsStr,
  7. fs::{File, FileType},
  8. io::{BufRead, Read, Write},
  9. path::{Path, PathBuf},
  10. process::{Command, ExitStatus},
  11. str::FromStr,
  12. sync::{
  13. atomic::{AtomicBool, Ordering},
  14. mpsc::sync_channel,
  15. Arc, Mutex,
  16. },
  17. time::{Duration, Instant},
  18. };
  19. use anyhow::Context;
  20. use heck::ToKebabCase;
  21. use ignore::gitignore::{Gitignore, GitignoreBuilder};
  22. use log::{debug, error, info};
  23. use notify::RecursiveMode;
  24. use notify_debouncer_mini::new_debouncer;
  25. use serde::Deserialize;
  26. use shared_child::SharedChild;
  27. use tauri_bundler::{
  28. AppCategory, BundleBinary, BundleSettings, DebianSettings, MacOsSettings, PackageSettings,
  29. UpdaterSettings, WindowsSettings,
  30. };
  31. use tauri_utils::config::parse::is_configuration_file;
  32. use super::{AppSettings, ExitReason, Interface};
  33. use crate::helpers::{
  34. app_paths::{app_dir, tauri_dir},
  35. config::{nsis_settings, reload as reload_config, wix_settings, BundleResources, Config},
  36. };
  37. use tauri_utils::display_path;
  38. mod cargo_config;
  39. mod desktop;
  40. mod manifest;
  41. use cargo_config::Config as CargoConfig;
  42. use manifest::{rewrite_manifest, Manifest};
  43. #[derive(Debug, Clone)]
  44. pub struct Options {
  45. pub runner: Option<String>,
  46. pub debug: bool,
  47. pub target: Option<String>,
  48. pub features: Option<Vec<String>>,
  49. pub args: Vec<String>,
  50. pub config: Option<String>,
  51. pub no_watch: bool,
  52. }
  53. impl From<crate::build::Options> for Options {
  54. fn from(options: crate::build::Options) -> Self {
  55. Self {
  56. runner: options.runner,
  57. debug: options.debug,
  58. target: options.target,
  59. features: options.features,
  60. args: options.args,
  61. config: options.config,
  62. no_watch: true,
  63. }
  64. }
  65. }
  66. impl From<crate::dev::Options> for Options {
  67. fn from(options: crate::dev::Options) -> Self {
  68. Self {
  69. runner: options.runner,
  70. debug: !options.release_mode,
  71. target: options.target,
  72. features: options.features,
  73. args: options.args,
  74. config: options.config,
  75. no_watch: options.no_watch,
  76. }
  77. }
  78. }
  79. pub struct DevChild {
  80. manually_killed_app: Arc<AtomicBool>,
  81. build_child: Arc<SharedChild>,
  82. app_child: Arc<Mutex<Option<Arc<SharedChild>>>>,
  83. }
  84. impl DevChild {
  85. fn kill(&self) -> std::io::Result<()> {
  86. if let Some(child) = &*self.app_child.lock().unwrap() {
  87. child.kill()?;
  88. } else {
  89. self.build_child.kill()?;
  90. }
  91. self.manually_killed_app.store(true, Ordering::Relaxed);
  92. Ok(())
  93. }
  94. fn try_wait(&self) -> std::io::Result<Option<ExitStatus>> {
  95. if let Some(child) = &*self.app_child.lock().unwrap() {
  96. child.try_wait()
  97. } else {
  98. self.build_child.try_wait()
  99. }
  100. }
  101. }
  102. #[derive(Debug)]
  103. pub struct Target {
  104. name: String,
  105. installed: bool,
  106. }
  107. pub struct Rust {
  108. app_settings: RustAppSettings,
  109. config_features: Vec<String>,
  110. product_name: Option<String>,
  111. available_targets: Option<Vec<Target>>,
  112. }
  113. impl Interface for Rust {
  114. type AppSettings = RustAppSettings;
  115. fn new(config: &Config, target: Option<String>) -> crate::Result<Self> {
  116. let manifest = {
  117. let (tx, rx) = sync_channel(1);
  118. let mut watcher = new_debouncer(Duration::from_secs(1), None, move |r| {
  119. if let Ok(events) = r {
  120. let _ = tx.send(events);
  121. }
  122. })
  123. .unwrap();
  124. watcher
  125. .watcher()
  126. .watch(&tauri_dir().join("Cargo.toml"), RecursiveMode::Recursive)?;
  127. let manifest = rewrite_manifest(config)?;
  128. let now = Instant::now();
  129. let timeout = Duration::from_secs(2);
  130. loop {
  131. if now.elapsed() >= timeout {
  132. break;
  133. }
  134. if rx.try_recv().is_ok() {
  135. break;
  136. }
  137. }
  138. manifest
  139. };
  140. if let Some(minimum_system_version) = &config.tauri.bundle.macos.minimum_system_version {
  141. std::env::set_var("MACOSX_DEPLOYMENT_TARGET", minimum_system_version);
  142. }
  143. Ok(Self {
  144. app_settings: RustAppSettings::new(config, manifest, target)?,
  145. config_features: config.build.features.clone().unwrap_or_default(),
  146. product_name: config.package.product_name.clone(),
  147. available_targets: None,
  148. })
  149. }
  150. fn app_settings(&self) -> &Self::AppSettings {
  151. &self.app_settings
  152. }
  153. fn build(&mut self, mut options: Options) -> crate::Result<()> {
  154. options
  155. .features
  156. .get_or_insert(Vec::new())
  157. .push("custom-protocol".into());
  158. desktop::build(
  159. options,
  160. &self.app_settings,
  161. self.product_name.clone(),
  162. &mut self.available_targets,
  163. self.config_features.clone(),
  164. )?;
  165. Ok(())
  166. }
  167. fn dev<F: Fn(ExitStatus, ExitReason) + Send + Sync + 'static>(
  168. &mut self,
  169. options: Options,
  170. on_exit: F,
  171. ) -> crate::Result<()> {
  172. let on_exit = Arc::new(on_exit);
  173. let on_exit_ = on_exit.clone();
  174. if options.no_watch {
  175. let (tx, rx) = sync_channel(1);
  176. self.run_dev(options, move |status, reason| {
  177. tx.send(()).unwrap();
  178. on_exit_(status, reason)
  179. })?;
  180. rx.recv().unwrap();
  181. Ok(())
  182. } else {
  183. let child = self.run_dev(options.clone(), move |status, reason| {
  184. on_exit_(status, reason)
  185. })?;
  186. self.run_dev_watcher(child, options, on_exit)
  187. }
  188. }
  189. fn env(&self) -> HashMap<&str, String> {
  190. let mut env = HashMap::new();
  191. env.insert(
  192. "TAURI_TARGET_TRIPLE",
  193. self.app_settings.target_triple.clone(),
  194. );
  195. let mut s = self.app_settings.target_triple.split('-');
  196. let (arch, _, host) = (s.next().unwrap(), s.next().unwrap(), s.next().unwrap());
  197. env.insert(
  198. "TAURI_ARCH",
  199. match arch {
  200. // keeps compatibility with old `std::env::consts::ARCH` implementation
  201. "i686" | "i586" => "x86".into(),
  202. a => a.into(),
  203. },
  204. );
  205. env.insert(
  206. "TAURI_PLATFORM",
  207. match host {
  208. // keeps compatibility with old `std::env::consts::OS` implementation
  209. "darwin" => "macos".into(),
  210. "ios-sim" => "ios".into(),
  211. "androideabi" => "android".into(),
  212. h => h.into(),
  213. },
  214. );
  215. env.insert(
  216. "TAURI_FAMILY",
  217. match host {
  218. "windows" => "windows".into(),
  219. _ => "unix".into(),
  220. },
  221. );
  222. match host {
  223. "linux" => env.insert("TAURI_PLATFORM_TYPE", "Linux".into()),
  224. "windows" => env.insert("TAURI_PLATFORM_TYPE", "Windows_NT".into()),
  225. "darwin" => env.insert("TAURI_PLATFORM_TYPE", "Darwin".into()),
  226. _ => None,
  227. };
  228. env
  229. }
  230. }
  231. struct IgnoreMatcher(Vec<Gitignore>);
  232. impl IgnoreMatcher {
  233. fn is_ignore(&self, path: &Path, is_dir: bool) -> bool {
  234. for gitignore in &self.0 {
  235. if gitignore.matched(path, is_dir).is_ignore() {
  236. return true;
  237. }
  238. }
  239. false
  240. }
  241. }
  242. fn build_ignore_matcher(dir: &Path) -> IgnoreMatcher {
  243. let mut matchers = Vec::new();
  244. // ignore crate doesn't expose an API to build `ignore::gitignore::GitIgnore`
  245. // with custom ignore file names so we have to walk the directory and collect
  246. // our custom ignore files and add it using `ignore::gitignore::GitIgnoreBuilder::add`
  247. for entry in ignore::WalkBuilder::new(dir)
  248. .require_git(false)
  249. .ignore(false)
  250. .overrides(
  251. ignore::overrides::OverrideBuilder::new(dir)
  252. .add(".taurignore")
  253. .unwrap()
  254. .build()
  255. .unwrap(),
  256. )
  257. .build()
  258. .flatten()
  259. {
  260. let path = entry.path();
  261. if path.file_name() == Some(OsStr::new(".taurignore")) {
  262. let mut ignore_builder = GitignoreBuilder::new(path.parent().unwrap());
  263. ignore_builder.add(path);
  264. if let Ok(ignore_file) = std::env::var("TAURI_DEV_WATCHER_IGNORE_FILE") {
  265. ignore_builder.add(dir.join(ignore_file));
  266. }
  267. for line in crate::dev::TAURI_DEV_WATCHER_GITIGNORE.lines().flatten() {
  268. let _ = ignore_builder.add_line(None, &line);
  269. }
  270. matchers.push(ignore_builder.build().unwrap());
  271. }
  272. }
  273. IgnoreMatcher(matchers)
  274. }
  275. fn lookup<F: FnMut(FileType, PathBuf)>(dir: &Path, mut f: F) {
  276. let mut default_gitignore = std::env::temp_dir();
  277. default_gitignore.push(".tauri-dev");
  278. let _ = std::fs::create_dir_all(&default_gitignore);
  279. default_gitignore.push(".gitignore");
  280. if !default_gitignore.exists() {
  281. if let Ok(mut file) = std::fs::File::create(default_gitignore.clone()) {
  282. let _ = file.write_all(crate::dev::TAURI_DEV_WATCHER_GITIGNORE);
  283. }
  284. }
  285. let mut builder = ignore::WalkBuilder::new(dir);
  286. builder.add_custom_ignore_filename(".taurignore");
  287. let _ = builder.add_ignore(default_gitignore);
  288. if let Ok(ignore_file) = std::env::var("TAURI_DEV_WATCHER_IGNORE_FILE") {
  289. builder.add_ignore(ignore_file);
  290. }
  291. builder.require_git(false).ignore(false).max_depth(Some(1));
  292. for entry in builder.build().flatten() {
  293. f(entry.file_type().unwrap(), dir.join(entry.path()));
  294. }
  295. }
  296. impl Rust {
  297. fn run_dev<F: Fn(ExitStatus, ExitReason) + Send + Sync + 'static>(
  298. &mut self,
  299. mut options: Options,
  300. on_exit: F,
  301. ) -> crate::Result<DevChild> {
  302. let mut args = Vec::new();
  303. let mut run_args = Vec::new();
  304. let mut reached_run_args = false;
  305. for arg in options.args.clone() {
  306. if reached_run_args {
  307. run_args.push(arg);
  308. } else if arg == "--" {
  309. reached_run_args = true;
  310. } else {
  311. args.push(arg);
  312. }
  313. }
  314. if !args.contains(&"--no-default-features".into()) {
  315. let manifest_features = self.app_settings.manifest.features();
  316. let enable_features: Vec<String> = manifest_features
  317. .get("default")
  318. .cloned()
  319. .unwrap_or_default()
  320. .into_iter()
  321. .filter(|feature| {
  322. if let Some(manifest_feature) = manifest_features.get(feature) {
  323. !manifest_feature.contains(&"tauri/custom-protocol".into())
  324. } else {
  325. feature != "tauri/custom-protocol"
  326. }
  327. })
  328. .collect();
  329. args.push("--no-default-features".into());
  330. if !enable_features.is_empty() {
  331. options
  332. .features
  333. .get_or_insert(Vec::new())
  334. .extend(enable_features);
  335. }
  336. }
  337. options.args = args;
  338. desktop::run_dev(
  339. options,
  340. run_args,
  341. &mut self.available_targets,
  342. self.config_features.clone(),
  343. &self.app_settings,
  344. self.product_name.clone(),
  345. on_exit,
  346. )
  347. }
  348. fn run_dev_watcher<F: Fn(ExitStatus, ExitReason) + Send + Sync + 'static>(
  349. &mut self,
  350. child: DevChild,
  351. options: Options,
  352. on_exit: Arc<F>,
  353. ) -> crate::Result<()> {
  354. let process = Arc::new(Mutex::new(child));
  355. let (tx, rx) = sync_channel(1);
  356. let app_path = app_dir();
  357. let tauri_path = tauri_dir();
  358. let workspace_path = get_workspace_dir()?;
  359. let watch_folders = if tauri_path == workspace_path {
  360. vec![tauri_path]
  361. } else {
  362. let cargo_settings = CargoSettings::load(&workspace_path)?;
  363. cargo_settings
  364. .workspace
  365. .as_ref()
  366. .map(|w| {
  367. w.members
  368. .clone()
  369. .unwrap_or_default()
  370. .into_iter()
  371. .map(|p| workspace_path.join(p))
  372. .collect()
  373. })
  374. .unwrap_or_else(|| vec![tauri_path])
  375. };
  376. let watch_folders = watch_folders.iter().map(Path::new).collect::<Vec<_>>();
  377. let common_ancestor = common_path::common_path_all(watch_folders.clone()).unwrap();
  378. let ignore_matcher = build_ignore_matcher(&common_ancestor);
  379. let mut watcher = new_debouncer(Duration::from_secs(1), None, move |r| {
  380. if let Ok(events) = r {
  381. tx.send(events).unwrap()
  382. }
  383. })
  384. .unwrap();
  385. for path in watch_folders {
  386. if !ignore_matcher.is_ignore(path, true) {
  387. info!("Watching {} for changes...", display_path(path));
  388. lookup(path, |file_type, p| {
  389. if p != path {
  390. debug!("Watching {} for changes...", display_path(&p));
  391. let _ = watcher.watcher().watch(
  392. &p,
  393. if file_type.is_dir() {
  394. RecursiveMode::Recursive
  395. } else {
  396. RecursiveMode::NonRecursive
  397. },
  398. );
  399. }
  400. });
  401. }
  402. }
  403. loop {
  404. if let Ok(events) = rx.recv() {
  405. for event in events {
  406. let on_exit = on_exit.clone();
  407. let event_path = event.path;
  408. if !ignore_matcher.is_ignore(&event_path, event_path.is_dir()) {
  409. if is_configuration_file(&event_path) {
  410. match reload_config(options.config.as_deref()) {
  411. Ok(config) => {
  412. info!("Tauri configuration changed. Rewriting manifest...");
  413. self.app_settings.manifest =
  414. rewrite_manifest(config.lock().unwrap().as_ref().unwrap())?
  415. }
  416. Err(err) => {
  417. let p = process.lock().unwrap();
  418. let is_building_app = p.app_child.lock().unwrap().is_none();
  419. if is_building_app {
  420. p.kill().with_context(|| "failed to kill app process")?;
  421. }
  422. error!("{}", err);
  423. }
  424. }
  425. } else {
  426. info!(
  427. "File {} changed. Rebuilding application...",
  428. display_path(event_path.strip_prefix(app_path).unwrap_or(&event_path))
  429. );
  430. // When tauri.conf.json is changed, rewrite_manifest will be called
  431. // which will trigger the watcher again
  432. // So the app should only be started when a file other than tauri.conf.json is changed
  433. let mut p = process.lock().unwrap();
  434. p.kill().with_context(|| "failed to kill app process")?;
  435. // wait for the process to exit
  436. loop {
  437. if let Ok(Some(_)) = p.try_wait() {
  438. break;
  439. }
  440. }
  441. *p = self.run_dev(options.clone(), move |status, reason| {
  442. on_exit(status, reason)
  443. })?;
  444. }
  445. }
  446. }
  447. }
  448. }
  449. }
  450. }
  451. // Taken from https://github.com/rust-lang/cargo/blob/70898e522116f6c23971e2a554b2dc85fd4c84cd/src/cargo/util/toml/mod.rs#L1008-L1065
  452. /// Enum that allows for the parsing of `field.workspace = true` in a Cargo.toml
  453. ///
  454. /// It allows for things to be inherited from a workspace or defined as needed
  455. #[derive(Clone, Debug)]
  456. pub enum MaybeWorkspace<T> {
  457. Workspace(TomlWorkspaceField),
  458. Defined(T),
  459. }
  460. impl<'de, T: Deserialize<'de>> serde::de::Deserialize<'de> for MaybeWorkspace<T> {
  461. fn deserialize<D>(deserializer: D) -> Result<MaybeWorkspace<T>, D::Error>
  462. where
  463. D: serde::de::Deserializer<'de>,
  464. {
  465. let value = serde_value::Value::deserialize(deserializer)?;
  466. if let Ok(workspace) = TomlWorkspaceField::deserialize(
  467. serde_value::ValueDeserializer::<D::Error>::new(value.clone()),
  468. ) {
  469. return Ok(MaybeWorkspace::Workspace(workspace));
  470. }
  471. T::deserialize(serde_value::ValueDeserializer::<D::Error>::new(value))
  472. .map(MaybeWorkspace::Defined)
  473. }
  474. }
  475. impl<T> MaybeWorkspace<T> {
  476. fn resolve(
  477. self,
  478. label: &str,
  479. get_ws_field: impl FnOnce() -> anyhow::Result<T>,
  480. ) -> anyhow::Result<T> {
  481. match self {
  482. MaybeWorkspace::Defined(value) => Ok(value),
  483. MaybeWorkspace::Workspace(TomlWorkspaceField { workspace: true }) => {
  484. get_ws_field().context(format!(
  485. "error inheriting `{label}` from workspace root manifest's `workspace.package.{label}`"
  486. ))
  487. }
  488. MaybeWorkspace::Workspace(TomlWorkspaceField { workspace: false }) => Err(anyhow::anyhow!(
  489. "`workspace=false` is unsupported for `package.{}`",
  490. label,
  491. )),
  492. }
  493. }
  494. fn _as_defined(&self) -> Option<&T> {
  495. match self {
  496. MaybeWorkspace::Workspace(_) => None,
  497. MaybeWorkspace::Defined(defined) => Some(defined),
  498. }
  499. }
  500. }
  501. #[derive(Deserialize, Clone, Debug)]
  502. pub struct TomlWorkspaceField {
  503. workspace: bool,
  504. }
  505. /// The `workspace` section of the app configuration (read from Cargo.toml).
  506. #[derive(Clone, Debug, Deserialize)]
  507. struct WorkspaceSettings {
  508. /// the workspace members.
  509. members: Option<Vec<String>>,
  510. package: Option<WorkspacePackageSettings>,
  511. }
  512. #[derive(Clone, Debug, Deserialize)]
  513. struct WorkspacePackageSettings {
  514. authors: Option<Vec<String>>,
  515. description: Option<String>,
  516. homepage: Option<String>,
  517. version: Option<String>,
  518. }
  519. #[derive(Clone, Debug, Deserialize)]
  520. struct BinarySettings {
  521. name: String,
  522. path: Option<String>,
  523. }
  524. /// The package settings.
  525. #[derive(Debug, Clone, Deserialize)]
  526. #[serde(rename_all = "kebab-case")]
  527. pub struct CargoPackageSettings {
  528. /// the package's name.
  529. pub name: Option<String>,
  530. /// the package's version.
  531. pub version: Option<MaybeWorkspace<String>>,
  532. /// the package's description.
  533. pub description: Option<MaybeWorkspace<String>>,
  534. /// the package's homepage.
  535. pub homepage: Option<MaybeWorkspace<String>>,
  536. /// the package's authors.
  537. pub authors: Option<MaybeWorkspace<Vec<String>>>,
  538. /// the default binary to run.
  539. pub default_run: Option<String>,
  540. }
  541. /// The Cargo settings (Cargo.toml root descriptor).
  542. #[derive(Clone, Debug, Deserialize)]
  543. struct CargoSettings {
  544. /// the package settings.
  545. ///
  546. /// it's optional because ancestor workspace Cargo.toml files may not have package info.
  547. package: Option<CargoPackageSettings>,
  548. /// the workspace settings.
  549. ///
  550. /// it's present if the read Cargo.toml belongs to a workspace root.
  551. workspace: Option<WorkspaceSettings>,
  552. /// the binary targets configuration.
  553. bin: Option<Vec<BinarySettings>>,
  554. }
  555. impl CargoSettings {
  556. /// Try to load a set of CargoSettings from a "Cargo.toml" file in the specified directory.
  557. fn load(dir: &Path) -> crate::Result<Self> {
  558. let toml_path = dir.join("Cargo.toml");
  559. let mut toml_str = String::new();
  560. let mut toml_file = File::open(toml_path).with_context(|| "failed to open Cargo.toml")?;
  561. toml_file
  562. .read_to_string(&mut toml_str)
  563. .with_context(|| "failed to read Cargo.toml")?;
  564. toml::from_str(&toml_str)
  565. .with_context(|| "failed to parse Cargo.toml")
  566. .map_err(Into::into)
  567. }
  568. }
  569. pub struct RustAppSettings {
  570. manifest: Manifest,
  571. cargo_settings: CargoSettings,
  572. cargo_package_settings: CargoPackageSettings,
  573. package_settings: PackageSettings,
  574. cargo_config: CargoConfig,
  575. target_triple: String,
  576. }
  577. impl AppSettings for RustAppSettings {
  578. fn get_package_settings(&self) -> PackageSettings {
  579. self.package_settings.clone()
  580. }
  581. fn get_bundle_settings(
  582. &self,
  583. config: &Config,
  584. features: &[String],
  585. ) -> crate::Result<BundleSettings> {
  586. tauri_config_to_bundle_settings(
  587. &self.manifest,
  588. features,
  589. config.tauri.bundle.clone(),
  590. config.tauri.system_tray.clone(),
  591. config.tauri.updater.clone(),
  592. )
  593. }
  594. fn app_binary_path(&self, options: &Options) -> crate::Result<PathBuf> {
  595. let bin_name = self
  596. .cargo_package_settings()
  597. .name
  598. .clone()
  599. .expect("Cargo manifest must have the `package.name` field");
  600. let out_dir = self
  601. .out_dir(options.target.clone(), get_profile(options))
  602. .with_context(|| "failed to get project out directory")?;
  603. let binary_extension: String = if self.target_triple.contains("windows") {
  604. "exe"
  605. } else {
  606. ""
  607. }
  608. .into();
  609. Ok(out_dir.join(bin_name).with_extension(binary_extension))
  610. }
  611. fn get_binaries(&self, config: &Config, target: &str) -> crate::Result<Vec<BundleBinary>> {
  612. let mut binaries: Vec<BundleBinary> = vec![];
  613. let binary_extension: String = if target.contains("windows") {
  614. ".exe"
  615. } else {
  616. ""
  617. }
  618. .into();
  619. let target_os = target.split('-').nth(2).unwrap_or(std::env::consts::OS);
  620. if let Some(bin) = &self.cargo_settings.bin {
  621. let default_run = self
  622. .package_settings
  623. .default_run
  624. .clone()
  625. .unwrap_or_default();
  626. for binary in bin {
  627. binaries.push(
  628. if Some(&binary.name) == self.cargo_package_settings.name.as_ref()
  629. || binary.name.as_str() == default_run
  630. {
  631. BundleBinary::new(
  632. format!(
  633. "{}{}",
  634. config
  635. .package
  636. .binary_name()
  637. .unwrap_or_else(|| binary.name.clone()),
  638. &binary_extension
  639. ),
  640. true,
  641. )
  642. } else {
  643. BundleBinary::new(
  644. format!("{}{}", binary.name.clone(), &binary_extension),
  645. false,
  646. )
  647. }
  648. .set_src_path(binary.path.clone()),
  649. )
  650. }
  651. }
  652. let mut bins_path = tauri_dir();
  653. bins_path.push("src/bin");
  654. if let Ok(fs_bins) = std::fs::read_dir(bins_path) {
  655. for entry in fs_bins {
  656. let path = entry?.path();
  657. if let Some(name) = path.file_stem() {
  658. let bin_exists = binaries.iter().any(|bin| {
  659. bin.name() == name || path.ends_with(bin.src_path().unwrap_or(&"".to_string()))
  660. });
  661. if !bin_exists {
  662. binaries.push(BundleBinary::new(
  663. format!("{}{}", name.to_string_lossy(), &binary_extension),
  664. false,
  665. ))
  666. }
  667. }
  668. }
  669. }
  670. if let Some(default_run) = self.package_settings.default_run.as_ref() {
  671. match binaries.iter_mut().find(|bin| bin.name() == default_run) {
  672. Some(bin) => {
  673. if let Some(bin_name) = config.package.binary_name() {
  674. bin.set_name(bin_name);
  675. }
  676. }
  677. None => {
  678. binaries.push(BundleBinary::new(
  679. format!(
  680. "{}{}",
  681. config
  682. .package
  683. .binary_name()
  684. .unwrap_or_else(|| default_run.to_string()),
  685. &binary_extension
  686. ),
  687. true,
  688. ));
  689. }
  690. }
  691. }
  692. match binaries.len() {
  693. 0 => binaries.push(BundleBinary::new(
  694. if target_os == "linux" {
  695. self.package_settings.product_name.to_kebab_case()
  696. } else {
  697. format!(
  698. "{}{}",
  699. self.package_settings.product_name.clone(),
  700. &binary_extension
  701. )
  702. },
  703. true,
  704. )),
  705. 1 => binaries.get_mut(0).unwrap().set_main(true),
  706. _ => {}
  707. }
  708. Ok(binaries)
  709. }
  710. }
  711. impl RustAppSettings {
  712. pub fn new(config: &Config, manifest: Manifest, target: Option<String>) -> crate::Result<Self> {
  713. let cargo_settings =
  714. CargoSettings::load(&tauri_dir()).with_context(|| "failed to load cargo settings")?;
  715. let cargo_package_settings = match &cargo_settings.package {
  716. Some(package_info) => package_info.clone(),
  717. None => {
  718. return Err(anyhow::anyhow!(
  719. "No package info in the config file".to_owned(),
  720. ))
  721. }
  722. };
  723. let ws_package_settings = CargoSettings::load(&get_workspace_dir()?)
  724. .with_context(|| "failed to load cargo settings from workspace root")?
  725. .workspace
  726. .and_then(|v| v.package);
  727. let package_settings = PackageSettings {
  728. product_name: config.package.product_name.clone().unwrap_or_else(|| {
  729. cargo_package_settings
  730. .name
  731. .clone()
  732. .expect("Cargo manifest must have the `package.name` field")
  733. }),
  734. version: config.package.version.clone().unwrap_or_else(|| {
  735. cargo_package_settings
  736. .version
  737. .clone()
  738. .expect("Cargo manifest must have the `package.version` field")
  739. .resolve("version", || {
  740. ws_package_settings
  741. .as_ref()
  742. .and_then(|p| p.version.clone())
  743. .ok_or_else(|| anyhow::anyhow!("Couldn't inherit value for `version` from workspace"))
  744. })
  745. .expect("Cargo project does not have a version")
  746. }),
  747. description: cargo_package_settings
  748. .description
  749. .clone()
  750. .map(|description| {
  751. description
  752. .resolve("description", || {
  753. ws_package_settings
  754. .as_ref()
  755. .and_then(|v| v.description.clone())
  756. .ok_or_else(|| {
  757. anyhow::anyhow!("Couldn't inherit value for `description` from workspace")
  758. })
  759. })
  760. .unwrap()
  761. })
  762. .unwrap_or_default(),
  763. homepage: cargo_package_settings.homepage.clone().map(|homepage| {
  764. homepage
  765. .resolve("homepage", || {
  766. ws_package_settings
  767. .as_ref()
  768. .and_then(|v| v.homepage.clone())
  769. .ok_or_else(|| {
  770. anyhow::anyhow!("Couldn't inherit value for `homepage` from workspace")
  771. })
  772. })
  773. .unwrap()
  774. }),
  775. authors: cargo_package_settings.authors.clone().map(|authors| {
  776. authors
  777. .resolve("authors", || {
  778. ws_package_settings
  779. .as_ref()
  780. .and_then(|v| v.authors.clone())
  781. .ok_or_else(|| anyhow::anyhow!("Couldn't inherit value for `authors` from workspace"))
  782. })
  783. .unwrap()
  784. }),
  785. default_run: cargo_package_settings.default_run.clone(),
  786. };
  787. let cargo_config = CargoConfig::load(&tauri_dir())?;
  788. let target_triple = target.unwrap_or_else(|| {
  789. cargo_config
  790. .build()
  791. .target()
  792. .map(|t| t.to_string())
  793. .unwrap_or_else(|| {
  794. let output = Command::new("rustc")
  795. .args(["-vV"])
  796. .output()
  797. .expect("\"rustc\" could not be found, did you install Rust?");
  798. let stdout = String::from_utf8_lossy(&output.stdout);
  799. stdout
  800. .split('\n')
  801. .find(|l| l.starts_with("host:"))
  802. .unwrap()
  803. .replace("host:", "")
  804. .trim()
  805. .to_string()
  806. })
  807. });
  808. Ok(Self {
  809. manifest,
  810. cargo_settings,
  811. cargo_package_settings,
  812. package_settings,
  813. cargo_config,
  814. target_triple,
  815. })
  816. }
  817. pub fn cargo_package_settings(&self) -> &CargoPackageSettings {
  818. &self.cargo_package_settings
  819. }
  820. pub fn out_dir(&self, target: Option<String>, profile: String) -> crate::Result<PathBuf> {
  821. get_target_dir(
  822. target
  823. .as_deref()
  824. .or_else(|| self.cargo_config.build().target()),
  825. profile,
  826. )
  827. }
  828. }
  829. #[derive(Deserialize)]
  830. struct CargoMetadata {
  831. target_directory: PathBuf,
  832. workspace_root: PathBuf,
  833. }
  834. fn get_cargo_metadata() -> crate::Result<CargoMetadata> {
  835. let output = Command::new("cargo")
  836. .args(["metadata", "--no-deps", "--format-version", "1"])
  837. .current_dir(tauri_dir())
  838. .output()?;
  839. if !output.status.success() {
  840. return Err(anyhow::anyhow!(
  841. "cargo metadata command exited with a non zero exit code: {}",
  842. String::from_utf8(output.stderr)?
  843. ));
  844. }
  845. Ok(serde_json::from_slice(&output.stdout)?)
  846. }
  847. /// This function determines the 'target' directory and suffixes it with the profile
  848. /// to determine where the compiled binary will be located.
  849. fn get_target_dir(target: Option<&str>, profile: String) -> crate::Result<PathBuf> {
  850. let mut path = get_cargo_metadata()
  851. .with_context(|| "failed to get cargo metadata")?
  852. .target_directory;
  853. if let Some(triple) = target {
  854. path.push(triple);
  855. }
  856. path.push(profile);
  857. Ok(path)
  858. }
  859. /// Executes `cargo metadata` to get the workspace directory.
  860. pub fn get_workspace_dir() -> crate::Result<PathBuf> {
  861. Ok(
  862. get_cargo_metadata()
  863. .with_context(|| "failed to get cargo metadata")?
  864. .workspace_root,
  865. )
  866. }
  867. pub fn get_profile(options: &Options) -> String {
  868. options
  869. .args
  870. .iter()
  871. .position(|a| a == "--profile")
  872. .map(|i| options.args[i + 1].clone())
  873. .unwrap_or_else(|| if options.debug { "debug" } else { "release" }.into())
  874. }
  875. #[allow(unused_variables)]
  876. fn tauri_config_to_bundle_settings(
  877. manifest: &Manifest,
  878. features: &[String],
  879. config: crate::helpers::config::BundleConfig,
  880. system_tray_config: Option<crate::helpers::config::SystemTrayConfig>,
  881. updater_config: crate::helpers::config::UpdaterConfig,
  882. ) -> crate::Result<BundleSettings> {
  883. let enabled_features = manifest.all_enabled_features(features);
  884. #[cfg(windows)]
  885. let windows_icon_path = PathBuf::from(
  886. config
  887. .icon
  888. .iter()
  889. .find(|i| i.ends_with(".ico"))
  890. .cloned()
  891. .expect("the bundle config must have a `.ico` icon"),
  892. );
  893. #[cfg(not(windows))]
  894. let windows_icon_path = PathBuf::from("");
  895. #[allow(unused_mut)]
  896. let mut resources = config
  897. .resources
  898. .unwrap_or(BundleResources::List(Vec::new()));
  899. #[allow(unused_mut)]
  900. let mut depends = config.deb.depends.unwrap_or_default();
  901. #[cfg(target_os = "linux")]
  902. {
  903. if let Some(system_tray_config) = &system_tray_config {
  904. let tray = std::env::var("TAURI_TRAY").unwrap_or_else(|_| "ayatana".to_string());
  905. if tray == "ayatana" {
  906. depends.push("libayatana-appindicator3-1".into());
  907. } else {
  908. depends.push("libappindicator3-1".into());
  909. }
  910. }
  911. // provides `libwebkit2gtk-4.0.so.37` and all `4.0` versions have the -37 package name
  912. depends.push("libwebkit2gtk-4.0-37".to_string());
  913. depends.push("libgtk-3-0".to_string());
  914. }
  915. #[cfg(windows)]
  916. {
  917. if let Some(webview_fixed_runtime_path) = &config.windows.webview_fixed_runtime_path {
  918. resources.push(webview_fixed_runtime_path.display().to_string());
  919. } else if let crate::helpers::config::WebviewInstallMode::FixedRuntime { path } =
  920. &config.windows.webview_install_mode
  921. {
  922. resources.push(path.display().to_string());
  923. }
  924. }
  925. let signing_identity = match std::env::var_os("APPLE_SIGNING_IDENTITY") {
  926. Some(signing_identity) => Some(
  927. signing_identity
  928. .to_str()
  929. .expect("failed to convert APPLE_SIGNING_IDENTITY to string")
  930. .to_string(),
  931. ),
  932. None => config.macos.signing_identity,
  933. };
  934. let provider_short_name = match std::env::var_os("APPLE_PROVIDER_SHORT_NAME") {
  935. Some(provider_short_name) => Some(
  936. provider_short_name
  937. .to_str()
  938. .expect("failed to convert APPLE_PROVIDER_SHORT_NAME to string")
  939. .to_string(),
  940. ),
  941. None => config.macos.provider_short_name,
  942. };
  943. let (resources, resources_map) = match resources {
  944. BundleResources::List(paths) => (Some(paths), None),
  945. BundleResources::Map(map) => (None, Some(map)),
  946. };
  947. Ok(BundleSettings {
  948. identifier: Some(config.identifier),
  949. publisher: config.publisher,
  950. icon: Some(config.icon),
  951. resources,
  952. resources_map,
  953. copyright: config.copyright,
  954. category: match config.category {
  955. Some(category) => Some(AppCategory::from_str(&category).map_err(|e| match e {
  956. Some(e) => anyhow::anyhow!("invalid category, did you mean `{}`?", e),
  957. None => anyhow::anyhow!("invalid category"),
  958. })?),
  959. None => None,
  960. },
  961. short_description: config.short_description,
  962. long_description: config.long_description,
  963. external_bin: config.external_bin,
  964. deb: DebianSettings {
  965. depends: if depends.is_empty() {
  966. None
  967. } else {
  968. Some(depends)
  969. },
  970. files: config.deb.files,
  971. desktop_template: config.deb.desktop_template,
  972. },
  973. macos: MacOsSettings {
  974. frameworks: config.macos.frameworks,
  975. minimum_system_version: config.macos.minimum_system_version,
  976. license: config.macos.license,
  977. exception_domain: config.macos.exception_domain,
  978. signing_identity,
  979. provider_short_name,
  980. entitlements: config.macos.entitlements,
  981. info_plist_path: {
  982. let path = tauri_dir().join("Info.plist");
  983. if path.exists() {
  984. Some(path)
  985. } else {
  986. None
  987. }
  988. },
  989. },
  990. windows: WindowsSettings {
  991. timestamp_url: config.windows.timestamp_url,
  992. tsp: config.windows.tsp,
  993. digest_algorithm: config.windows.digest_algorithm,
  994. certificate_thumbprint: config.windows.certificate_thumbprint,
  995. wix: config.windows.wix.map(|w| {
  996. let mut wix = wix_settings(w);
  997. wix.license = wix.license.map(|l| tauri_dir().join(l));
  998. wix
  999. }),
  1000. nsis: config.windows.nsis.map(nsis_settings),
  1001. icon_path: windows_icon_path,
  1002. webview_install_mode: config.windows.webview_install_mode,
  1003. webview_fixed_runtime_path: config.windows.webview_fixed_runtime_path,
  1004. allow_downgrades: config.windows.allow_downgrades,
  1005. },
  1006. updater: Some(UpdaterSettings {
  1007. active: updater_config.active,
  1008. // we set it to true by default we shouldn't have to use
  1009. // unwrap_or as we have a default value but used to prevent any failing
  1010. dialog: updater_config.dialog,
  1011. pubkey: updater_config.pubkey,
  1012. endpoints: updater_config
  1013. .endpoints
  1014. .map(|endpoints| endpoints.iter().map(|e| e.to_string()).collect()),
  1015. msiexec_args: Some(updater_config.windows.install_mode.msiexec_args()),
  1016. }),
  1017. ..Default::default()
  1018. })
  1019. }