rust.rs 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489
  1. // Copyright 2019-2021 Tauri Programme within The Commons Conservancy
  2. // SPDX-License-Identifier: Apache-2.0
  3. // SPDX-License-Identifier: MIT
  4. use std::{
  5. fs::File,
  6. io::Read,
  7. path::{Path, PathBuf},
  8. process::Command,
  9. str::FromStr,
  10. };
  11. use anyhow::Context;
  12. #[cfg(target_os = "linux")]
  13. use heck::ToKebabCase;
  14. use serde::Deserialize;
  15. use crate::helpers::{app_paths::tauri_dir, config::Config, manifest::Manifest, Logger};
  16. use tauri_bundler::{
  17. AppCategory, BundleBinary, BundleSettings, DebianSettings, MacOsSettings, PackageSettings,
  18. UpdaterSettings, WindowsSettings, WixSettings,
  19. };
  20. /// The `workspace` section of the app configuration (read from Cargo.toml).
  21. #[derive(Clone, Debug, Deserialize)]
  22. struct WorkspaceSettings {
  23. /// the workspace members.
  24. members: Option<Vec<String>>,
  25. }
  26. #[derive(Clone, Debug, Deserialize)]
  27. struct BinarySettings {
  28. name: String,
  29. path: Option<String>,
  30. }
  31. /// The package settings.
  32. #[derive(Debug, Clone, Deserialize)]
  33. pub struct CargoPackageSettings {
  34. /// the package's name.
  35. pub name: Option<String>,
  36. /// the package's version.
  37. pub version: Option<String>,
  38. /// the package's description.
  39. pub description: Option<String>,
  40. /// the package's homepage.
  41. pub homepage: Option<String>,
  42. /// the package's authors.
  43. pub authors: Option<Vec<String>>,
  44. /// the default binary to run.
  45. pub default_run: Option<String>,
  46. }
  47. /// The Cargo settings (Cargo.toml root descriptor).
  48. #[derive(Clone, Debug, Deserialize)]
  49. struct CargoSettings {
  50. /// the package settings.
  51. ///
  52. /// it's optional because ancestor workspace Cargo.toml files may not have package info.
  53. package: Option<CargoPackageSettings>,
  54. /// the workspace settings.
  55. ///
  56. /// it's present if the read Cargo.toml belongs to a workspace root.
  57. workspace: Option<WorkspaceSettings>,
  58. /// the binary targets configuration.
  59. bin: Option<Vec<BinarySettings>>,
  60. }
  61. impl CargoSettings {
  62. /// Try to load a set of CargoSettings from a "Cargo.toml" file in the specified directory.
  63. fn load(dir: &Path) -> crate::Result<Self> {
  64. let toml_path = dir.join("Cargo.toml");
  65. let mut toml_str = String::new();
  66. let mut toml_file = File::open(toml_path).with_context(|| "failed to open Cargo.toml")?;
  67. toml_file
  68. .read_to_string(&mut toml_str)
  69. .with_context(|| "failed to read Cargo.toml")?;
  70. toml::from_str(&toml_str)
  71. .with_context(|| "failed to parse Cargo.toml")
  72. .map_err(Into::into)
  73. }
  74. }
  75. #[derive(Deserialize)]
  76. struct CargoBuildConfig {
  77. #[serde(rename = "target-dir")]
  78. target_dir: Option<String>,
  79. }
  80. #[derive(Deserialize)]
  81. struct CargoConfig {
  82. build: Option<CargoBuildConfig>,
  83. }
  84. pub fn build_project(
  85. runner: String,
  86. target: &Option<String>,
  87. features: Vec<String>,
  88. debug: bool,
  89. ) -> crate::Result<()> {
  90. let mut command = Command::new(&runner);
  91. command.args(&["build", "--features=custom-protocol"]);
  92. if let Some(target) = target {
  93. command.arg("--target");
  94. command.arg(target);
  95. }
  96. if !features.is_empty() {
  97. command.arg("--features");
  98. command.arg(features.join(","));
  99. }
  100. if !debug {
  101. command.arg("--release");
  102. }
  103. let status = command
  104. .status()
  105. .with_context(|| format!("failed to run {}", runner))?;
  106. if !status.success() {
  107. return Err(anyhow::anyhow!(format!(
  108. "Result of `{} build` operation was unsuccessful: {}",
  109. runner, status
  110. )));
  111. }
  112. Ok(())
  113. }
  114. pub struct AppSettings {
  115. cargo_settings: CargoSettings,
  116. cargo_package_settings: CargoPackageSettings,
  117. package_settings: PackageSettings,
  118. }
  119. impl AppSettings {
  120. pub fn new(config: &Config) -> crate::Result<Self> {
  121. let cargo_settings =
  122. CargoSettings::load(&tauri_dir()).with_context(|| "failed to load cargo settings")?;
  123. let cargo_package_settings = match &cargo_settings.package {
  124. Some(package_info) => package_info.clone(),
  125. None => {
  126. return Err(anyhow::anyhow!(
  127. "No package info in the config file".to_owned(),
  128. ))
  129. }
  130. };
  131. let package_settings = PackageSettings {
  132. product_name: config.package.product_name.clone().unwrap_or_else(|| {
  133. cargo_package_settings
  134. .name
  135. .clone()
  136. .expect("Cargo manifest must have the `package.name` field")
  137. }),
  138. version: config.package.version.clone().unwrap_or_else(|| {
  139. cargo_package_settings
  140. .version
  141. .clone()
  142. .expect("Cargo manifest must have the `package.version` field")
  143. }),
  144. description: cargo_package_settings
  145. .description
  146. .clone()
  147. .unwrap_or_default(),
  148. homepage: cargo_package_settings.homepage.clone(),
  149. authors: cargo_package_settings.authors.clone(),
  150. default_run: cargo_package_settings.default_run.clone(),
  151. };
  152. Ok(Self {
  153. cargo_settings,
  154. cargo_package_settings,
  155. package_settings,
  156. })
  157. }
  158. pub fn cargo_package_settings(&self) -> &CargoPackageSettings {
  159. &self.cargo_package_settings
  160. }
  161. pub fn get_bundle_settings(
  162. &self,
  163. config: &Config,
  164. manifest: &Manifest,
  165. ) -> crate::Result<BundleSettings> {
  166. tauri_config_to_bundle_settings(
  167. manifest,
  168. config.tauri.bundle.clone(),
  169. config.tauri.system_tray.clone(),
  170. config.tauri.updater.clone(),
  171. )
  172. }
  173. pub fn get_out_dir(&self, target: Option<String>, debug: bool) -> crate::Result<PathBuf> {
  174. let tauri_dir = tauri_dir();
  175. let workspace_dir = get_workspace_dir(&tauri_dir);
  176. get_target_dir(&workspace_dir, target, !debug)
  177. }
  178. pub fn get_package_settings(&self) -> PackageSettings {
  179. self.package_settings.clone()
  180. }
  181. pub fn get_binaries(&self, config: &Config) -> crate::Result<Vec<BundleBinary>> {
  182. let mut binaries: Vec<BundleBinary> = vec![];
  183. if let Some(bin) = &self.cargo_settings.bin {
  184. let default_run = self
  185. .package_settings
  186. .default_run
  187. .clone()
  188. .unwrap_or_else(|| "".to_string());
  189. for binary in bin {
  190. binaries.push(
  191. if Some(&binary.name) == self.cargo_package_settings.name.as_ref()
  192. || binary.name.as_str() == default_run
  193. {
  194. BundleBinary::new(
  195. config
  196. .package
  197. .binary_name()
  198. .unwrap_or_else(|| binary.name.clone()),
  199. true,
  200. )
  201. } else {
  202. BundleBinary::new(binary.name.clone(), false)
  203. }
  204. .set_src_path(binary.path.clone()),
  205. )
  206. }
  207. }
  208. let mut bins_path = tauri_dir();
  209. bins_path.push("src/bin");
  210. if let Ok(fs_bins) = std::fs::read_dir(bins_path) {
  211. for entry in fs_bins {
  212. let path = entry?.path();
  213. if let Some(name) = path.file_stem() {
  214. let bin_exists = binaries.iter().any(|bin| {
  215. bin.name() == name || path.ends_with(bin.src_path().unwrap_or(&"".to_string()))
  216. });
  217. if !bin_exists {
  218. binaries.push(BundleBinary::new(name.to_string_lossy().to_string(), false))
  219. }
  220. }
  221. }
  222. }
  223. if let Some(default_run) = self.package_settings.default_run.as_ref() {
  224. match binaries.iter_mut().find(|bin| bin.name() == default_run) {
  225. Some(bin) => {
  226. if let Some(bin_name) = config.package.binary_name() {
  227. bin.set_name(bin_name);
  228. }
  229. }
  230. None => {
  231. binaries.push(BundleBinary::new(
  232. config
  233. .package
  234. .binary_name()
  235. .unwrap_or_else(|| default_run.to_string()),
  236. true,
  237. ));
  238. }
  239. }
  240. }
  241. match binaries.len() {
  242. 0 => binaries.push(BundleBinary::new(
  243. #[cfg(target_os = "linux")]
  244. self.package_settings.product_name.to_kebab_case(),
  245. #[cfg(not(target_os = "linux"))]
  246. self.package_settings.product_name.clone(),
  247. true,
  248. )),
  249. 1 => binaries.get_mut(0).unwrap().set_main(true),
  250. _ => {}
  251. }
  252. Ok(binaries)
  253. }
  254. }
  255. /// This function determines where 'target' dir is and suffixes it with 'release' or 'debug'
  256. /// to determine where the compiled binary will be located.
  257. fn get_target_dir(
  258. project_root_dir: &Path,
  259. target: Option<String>,
  260. is_release: bool,
  261. ) -> crate::Result<PathBuf> {
  262. let mut path: PathBuf = match std::env::var_os("CARGO_TARGET_DIR") {
  263. Some(target_dir) => target_dir.into(),
  264. None => {
  265. let mut root_dir = project_root_dir.to_path_buf();
  266. let target_path: Option<PathBuf> = loop {
  267. // cargo reads configs under .cargo/config.toml or .cargo/config
  268. let mut cargo_config_path = root_dir.join(".cargo/config");
  269. if !cargo_config_path.exists() {
  270. cargo_config_path = root_dir.join(".cargo/config.toml");
  271. }
  272. // if the path exists, parse it
  273. if cargo_config_path.exists() {
  274. let mut config_str = String::new();
  275. let mut config_file = File::open(&cargo_config_path)
  276. .with_context(|| format!("failed to open {:?}", cargo_config_path))?;
  277. config_file
  278. .read_to_string(&mut config_str)
  279. .with_context(|| "failed to read cargo config file")?;
  280. let config: CargoConfig =
  281. toml::from_str(&config_str).with_context(|| "failed to parse cargo config file")?;
  282. if let Some(build) = config.build {
  283. if let Some(target_dir) = build.target_dir {
  284. break Some(target_dir.into());
  285. }
  286. }
  287. }
  288. if !root_dir.pop() {
  289. break None;
  290. }
  291. };
  292. target_path.unwrap_or_else(|| project_root_dir.join("target"))
  293. }
  294. };
  295. if let Some(ref triple) = target {
  296. path.push(triple);
  297. }
  298. path.push(if is_release { "release" } else { "debug" });
  299. Ok(path)
  300. }
  301. /// Walks up the file system, looking for a Cargo.toml file
  302. /// If one is found before reaching the root, then the current_dir's package belongs to that parent workspace if it's listed on [workspace.members].
  303. ///
  304. /// If this package is part of a workspace, returns the path to the workspace directory
  305. /// Otherwise returns the current directory.
  306. pub fn get_workspace_dir(current_dir: &Path) -> PathBuf {
  307. let mut dir = current_dir.to_path_buf();
  308. let project_path = dir.clone();
  309. let logger = Logger::new("tauri:rust");
  310. while dir.pop() {
  311. if dir.join("Cargo.toml").exists() {
  312. match CargoSettings::load(&dir) {
  313. Ok(cargo_settings) => {
  314. if let Some(workspace_settings) = cargo_settings.workspace {
  315. if let Some(members) = workspace_settings.members {
  316. if members.iter().any(|member| {
  317. glob::glob(&dir.join(member).to_string_lossy().into_owned())
  318. .unwrap()
  319. .any(|p| p.unwrap() == project_path)
  320. }) {
  321. return dir;
  322. }
  323. }
  324. }
  325. }
  326. Err(e) => {
  327. logger.warn(format!(
  328. "Found `{}`, which may define a parent workspace, but \
  329. failed to parse it. If this is indeed a parent workspace, undefined behavior may occur: \
  330. \n {:#}",
  331. dir.display(),
  332. e
  333. ));
  334. }
  335. }
  336. }
  337. }
  338. // Nothing found walking up the file system, return the starting directory
  339. current_dir.to_path_buf()
  340. }
  341. #[allow(unused_variables)]
  342. fn tauri_config_to_bundle_settings(
  343. manifest: &Manifest,
  344. config: crate::helpers::config::BundleConfig,
  345. system_tray_config: Option<crate::helpers::config::SystemTrayConfig>,
  346. updater_config: crate::helpers::config::UpdaterConfig,
  347. ) -> crate::Result<BundleSettings> {
  348. #[cfg(windows)]
  349. let windows_icon_path = PathBuf::from(
  350. config
  351. .icon
  352. .as_ref()
  353. .and_then(|icons| icons.iter().find(|i| i.ends_with(".ico")).cloned())
  354. .expect("the bundle config must have a `.ico` icon"),
  355. );
  356. #[cfg(not(windows))]
  357. let windows_icon_path = PathBuf::from("");
  358. #[allow(unused_mut)]
  359. let mut resources = config.resources.unwrap_or_default();
  360. #[allow(unused_mut)]
  361. let mut depends = config.deb.depends.unwrap_or_default();
  362. #[cfg(target_os = "linux")]
  363. {
  364. if let Some(system_tray_config) = &system_tray_config {
  365. let mut icon_path = system_tray_config.icon_path.clone();
  366. icon_path.set_extension("png");
  367. resources.push(icon_path.display().to_string());
  368. depends.push("libappindicator3-1".to_string());
  369. }
  370. // provides `libwebkit2gtk-4.0.so.37` and all `4.0` versions have the -37 package name
  371. depends.push("libwebkit2gtk-4.0-37".to_string());
  372. depends.push("libgtk-3-0".to_string());
  373. }
  374. let signing_identity = match std::env::var_os("APPLE_SIGNING_IDENTITY") {
  375. Some(signing_identity) => Some(
  376. signing_identity
  377. .to_str()
  378. .expect("failed to convert APPLE_SIGNING_IDENTITY to string")
  379. .to_string(),
  380. ),
  381. None => config.macos.signing_identity,
  382. };
  383. Ok(BundleSettings {
  384. identifier: config.identifier,
  385. icon: config.icon,
  386. resources: if resources.is_empty() {
  387. None
  388. } else {
  389. Some(resources)
  390. },
  391. copyright: config.copyright,
  392. category: match config.category {
  393. Some(category) => Some(AppCategory::from_str(&category).map_err(|e| match e {
  394. Some(e) => anyhow::anyhow!("invalid category, did you mean `{}`?", e),
  395. None => anyhow::anyhow!("invalid category"),
  396. })?),
  397. None => None,
  398. },
  399. short_description: config.short_description,
  400. long_description: config.long_description,
  401. external_bin: config.external_bin,
  402. deb: DebianSettings {
  403. depends: if depends.is_empty() {
  404. None
  405. } else {
  406. Some(depends)
  407. },
  408. use_bootstrapper: Some(config.deb.use_bootstrapper),
  409. files: config.deb.files,
  410. },
  411. macos: MacOsSettings {
  412. frameworks: config.macos.frameworks,
  413. minimum_system_version: config.macos.minimum_system_version,
  414. license: config.macos.license,
  415. use_bootstrapper: Some(config.macos.use_bootstrapper),
  416. exception_domain: config.macos.exception_domain,
  417. signing_identity,
  418. entitlements: config.macos.entitlements,
  419. info_plist_path: {
  420. let path = tauri_dir().join("Info.plist");
  421. if path.exists() {
  422. Some(path)
  423. } else {
  424. None
  425. }
  426. },
  427. },
  428. windows: WindowsSettings {
  429. timestamp_url: config.windows.timestamp_url,
  430. digest_algorithm: config.windows.digest_algorithm,
  431. certificate_thumbprint: config.windows.certificate_thumbprint,
  432. wix: config.windows.wix.map(|w| {
  433. let mut wix = WixSettings::from(w);
  434. wix.license = wix.license.map(|l| tauri_dir().join(l));
  435. wix
  436. }),
  437. icon_path: windows_icon_path,
  438. },
  439. updater: Some(UpdaterSettings {
  440. active: updater_config.active,
  441. // we set it to true by default we shouldn't have to use
  442. // unwrap_or as we have a default value but used to prevent any failing
  443. dialog: updater_config.dialog.unwrap_or(true),
  444. pubkey: updater_config.pubkey,
  445. endpoints: updater_config.endpoints,
  446. }),
  447. ..Default::default()
  448. })
  449. }