build.rs 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437
  1. // Copyright 2019-2023 Tauri Programme within The Commons Conservancy
  2. // SPDX-License-Identifier: Apache-2.0
  3. // SPDX-License-Identifier: MIT
  4. use crate::{
  5. helpers::{
  6. app_paths::{app_dir, tauri_dir},
  7. command_env,
  8. config::{get as get_config, AppUrl, HookCommand, WindowUrl, MERGE_CONFIG_EXTENSION_NAME},
  9. updater_signature::{read_key_from_file, secret_key as updater_secret_key, sign_file},
  10. },
  11. interface::{AppInterface, AppSettings, Interface},
  12. CommandExt, Result,
  13. };
  14. use anyhow::{bail, Context};
  15. use base64::Engine;
  16. use clap::{ArgAction, Parser};
  17. use log::{debug, error, info, warn};
  18. use std::{
  19. env::{set_current_dir, var_os},
  20. path::{Path, PathBuf},
  21. process::Command,
  22. };
  23. use tauri_bundler::bundle::{bundle_project, Bundle, PackageType};
  24. #[derive(Debug, Clone, Parser)]
  25. #[clap(about = "Tauri build")]
  26. pub struct Options {
  27. /// Binary to use to build the application, defaults to `cargo`
  28. #[clap(short, long)]
  29. pub runner: Option<String>,
  30. /// Builds with the debug flag
  31. #[clap(short, long)]
  32. pub debug: bool,
  33. /// Target triple to build against.
  34. ///
  35. /// It must be one of the values outputted by `$rustc --print target-list` or `universal-apple-darwin` for an universal macOS application.
  36. ///
  37. /// Note that compiling an universal macOS application requires both `aarch64-apple-darwin` and `x86_64-apple-darwin` targets to be installed.
  38. #[clap(short, long)]
  39. pub target: Option<String>,
  40. /// Space or comma separated list of features to activate
  41. #[clap(short, long, action = ArgAction::Append, num_args(0..))]
  42. pub features: Option<Vec<String>>,
  43. /// Space or comma separated list of bundles to package.
  44. ///
  45. /// Each bundle must be one of `deb`, `appimage`, `msi`, `app` or `dmg` on MacOS and `updater` on all platforms.
  46. /// If `none` is specified, the bundler will be skipped.
  47. ///
  48. /// Note that the `updater` bundle is not automatically added so you must specify it if the updater is enabled.
  49. #[clap(short, long, action = ArgAction::Append, num_args(0..))]
  50. pub bundles: Option<Vec<String>>,
  51. /// JSON string or path to JSON file to merge with tauri.conf.json
  52. #[clap(short, long)]
  53. pub config: Option<String>,
  54. /// Command line arguments passed to the runner
  55. pub args: Vec<String>,
  56. /// Skip prompting for values
  57. #[clap(long)]
  58. ci: bool,
  59. }
  60. pub fn command(mut options: Options, verbosity: u8) -> Result<()> {
  61. options.ci = options.ci || std::env::var("CI").is_ok();
  62. let ci = options.ci;
  63. let (merge_config, merge_config_path) = if let Some(config) = &options.config {
  64. if config.starts_with('{') {
  65. (Some(config.to_string()), None)
  66. } else {
  67. (
  68. Some(
  69. std::fs::read_to_string(config).with_context(|| "failed to read custom configuration")?,
  70. ),
  71. Some(config.clone()),
  72. )
  73. }
  74. } else {
  75. (None, None)
  76. };
  77. options.config = merge_config;
  78. let tauri_path = tauri_dir();
  79. set_current_dir(tauri_path).with_context(|| "failed to change current working directory")?;
  80. let config = get_config(options.config.as_deref())?;
  81. let config_guard = config.lock().unwrap();
  82. let config_ = config_guard.as_ref().unwrap();
  83. let bundle_identifier_source = match config_.find_bundle_identifier_overwriter() {
  84. Some(source) if source == MERGE_CONFIG_EXTENSION_NAME => merge_config_path.unwrap_or(source),
  85. Some(source) => source,
  86. None => "tauri.conf.json".into(),
  87. };
  88. if config_.tauri.bundle.identifier == "com.tauri.dev" {
  89. error!(
  90. "You must change the bundle identifier in `{} > tauri > bundle > identifier`. The default value `com.tauri.dev` is not allowed as it must be unique across applications.",
  91. bundle_identifier_source
  92. );
  93. std::process::exit(1);
  94. }
  95. if config_
  96. .tauri
  97. .bundle
  98. .identifier
  99. .chars()
  100. .any(|ch| !(ch.is_alphanumeric() || ch == '-' || ch == '.'))
  101. {
  102. error!(
  103. "The bundle identifier \"{}\" set in `{} > tauri > bundle > identifier`. The bundle identifier string must contain only alphanumeric characters (A–Z, a–z, and 0–9), hyphens (-), and periods (.).",
  104. config_.tauri.bundle.identifier,
  105. bundle_identifier_source
  106. );
  107. std::process::exit(1);
  108. }
  109. let mut interface = AppInterface::new(config_, options.target.clone())?;
  110. let app_settings = interface.app_settings();
  111. let interface_options = options.clone().into();
  112. if let Some(before_build) = config_.build.before_build_command.clone() {
  113. run_hook(
  114. "beforeBuildCommand",
  115. before_build,
  116. &interface,
  117. options.debug,
  118. )?;
  119. }
  120. if let AppUrl::Url(WindowUrl::App(web_asset_path)) = &config_.build.dist_dir {
  121. if !web_asset_path.exists() {
  122. return Err(anyhow::anyhow!(
  123. "Unable to find your web assets, did you forget to build your web app? Your distDir is set to \"{:?}\".",
  124. web_asset_path
  125. ));
  126. }
  127. if web_asset_path.canonicalize()?.file_name() == Some(std::ffi::OsStr::new("src-tauri")) {
  128. return Err(anyhow::anyhow!(
  129. "The configured distDir is the `src-tauri` folder.
  130. Please isolate your web assets on a separate folder and update `tauri.conf.json > build > distDir`.",
  131. ));
  132. }
  133. let mut out_folders = Vec::new();
  134. for folder in &["node_modules", "src-tauri", "target"] {
  135. if web_asset_path.join(folder).is_dir() {
  136. out_folders.push(folder.to_string());
  137. }
  138. }
  139. if !out_folders.is_empty() {
  140. return Err(anyhow::anyhow!(
  141. "The configured distDir includes the `{:?}` {}. Please isolate your web assets on a separate folder and update `tauri.conf.json > build > distDir`.",
  142. out_folders,
  143. if out_folders.len() == 1 { "folder" }else { "folders" }
  144. )
  145. );
  146. }
  147. }
  148. if options.runner.is_none() {
  149. options.runner = config_.build.runner.clone();
  150. }
  151. options
  152. .features
  153. .get_or_insert(Vec::new())
  154. .extend(config_.build.features.clone().unwrap_or_default());
  155. let bin_path = app_settings.app_binary_path(&interface_options)?;
  156. let out_dir = bin_path.parent().unwrap();
  157. interface.build(interface_options)?;
  158. let app_settings = interface.app_settings();
  159. if config_.tauri.bundle.active {
  160. let package_types = if let Some(names) = &options.bundles {
  161. let mut types = vec![];
  162. for name in names
  163. .iter()
  164. .flat_map(|n| n.split(',').map(|s| s.to_string()).collect::<Vec<String>>())
  165. {
  166. if name == "none" {
  167. break;
  168. }
  169. match PackageType::from_short_name(&name) {
  170. Some(package_type) => {
  171. types.push(package_type);
  172. }
  173. None => {
  174. return Err(anyhow::anyhow!(format!(
  175. "Unsupported bundle format: {name}"
  176. )));
  177. }
  178. }
  179. }
  180. Some(types)
  181. } else {
  182. let targets = config_.tauri.bundle.targets.to_vec();
  183. if targets.is_empty() {
  184. None
  185. } else {
  186. Some(targets.into_iter().map(Into::into).collect())
  187. }
  188. };
  189. if let Some(types) = &package_types {
  190. if config_.tauri.updater.active && !types.contains(&PackageType::Updater) {
  191. warn!("The updater is enabled but the bundle target list does not contain `updater`, so the updater artifacts won't be generated.");
  192. }
  193. }
  194. // if we have a package to bundle, let's run the `before_bundle_command`.
  195. if package_types.as_ref().map_or(true, |p| !p.is_empty()) {
  196. if let Some(before_bundle) = config_.build.before_bundle_command.clone() {
  197. run_hook(
  198. "beforeBundleCommand",
  199. before_bundle,
  200. &interface,
  201. options.debug,
  202. )?;
  203. }
  204. }
  205. let mut settings = app_settings
  206. .get_bundler_settings(&options.into(), config_, out_dir, package_types)
  207. .with_context(|| "failed to build bundler settings")?;
  208. settings.set_log_level(match verbosity {
  209. 0 => log::Level::Error,
  210. 1 => log::Level::Info,
  211. _ => log::Level::Trace,
  212. });
  213. // set env vars used by the bundler
  214. #[cfg(target_os = "linux")]
  215. {
  216. use crate::helpers::config::ShellAllowlistOpen;
  217. if matches!(
  218. config_.tauri.allowlist.shell.open,
  219. ShellAllowlistOpen::Flag(true) | ShellAllowlistOpen::Validate(_)
  220. ) {
  221. std::env::set_var("APPIMAGE_BUNDLE_XDG_OPEN", "1");
  222. }
  223. if config_.tauri.system_tray.is_some() {
  224. if let Ok(tray) = std::env::var("TAURI_TRAY") {
  225. std::env::set_var(
  226. "TRAY_LIBRARY_PATH",
  227. if tray == "ayatana" {
  228. format!(
  229. "{}/libayatana-appindicator3.so.1",
  230. pkgconfig_utils::get_library_path("ayatana-appindicator3-0.1")
  231. .expect("failed to get ayatana-appindicator library path using pkg-config.")
  232. )
  233. } else {
  234. format!(
  235. "{}/libappindicator3.so.1",
  236. pkgconfig_utils::get_library_path("appindicator3-0.1")
  237. .expect("failed to get libappindicator-gtk library path using pkg-config.")
  238. )
  239. },
  240. );
  241. } else {
  242. std::env::set_var(
  243. "TRAY_LIBRARY_PATH",
  244. pkgconfig_utils::get_appindicator_library_path(),
  245. );
  246. }
  247. }
  248. if config_.tauri.bundle.appimage.bundle_media_framework {
  249. std::env::set_var("APPIMAGE_BUNDLE_GSTREAMER", "1");
  250. }
  251. }
  252. let bundles = bundle_project(settings)
  253. .map_err(|e| anyhow::anyhow!("{:#}", e))
  254. .with_context(|| "failed to bundle project")?;
  255. let updater_bundles: Vec<&Bundle> = bundles
  256. .iter()
  257. .filter(|bundle| bundle.package_type == PackageType::Updater)
  258. .collect();
  259. // If updater is active and we bundled it
  260. if config_.tauri.updater.active && !updater_bundles.is_empty() {
  261. // if no password provided we use an empty string
  262. let password = var_os("TAURI_KEY_PASSWORD")
  263. .map(|v| v.to_str().unwrap().to_string())
  264. .or_else(|| if ci { Some("".into()) } else { None });
  265. // get the private key
  266. let secret_key = if let Some(mut private_key) =
  267. var_os("TAURI_PRIVATE_KEY").map(|v| v.to_str().unwrap().to_string())
  268. {
  269. // check if env var points to a file..
  270. let pk_dir = Path::new(&private_key);
  271. // Check if user provided a path or a key
  272. // We validate if the path exist or not.
  273. if pk_dir.exists() {
  274. // read file content and use it as private key
  275. private_key = read_key_from_file(pk_dir)?;
  276. }
  277. updater_secret_key(private_key, password)
  278. } else {
  279. Err(anyhow::anyhow!("A public key has been found, but no private key. Make sure to set `TAURI_PRIVATE_KEY` environment variable."))
  280. }?;
  281. let pubkey =
  282. base64::engine::general_purpose::STANDARD.decode(&config_.tauri.updater.pubkey)?;
  283. let pub_key_decoded = String::from_utf8_lossy(&pubkey);
  284. let public_key = minisign::PublicKeyBox::from_string(&pub_key_decoded)?.into_public_key()?;
  285. // make sure we have our package built
  286. let mut signed_paths = Vec::new();
  287. for elem in updater_bundles {
  288. // we expect to have only one path in the vec but we iter if we add
  289. // another type of updater package who require multiple file signature
  290. for path in elem.bundle_paths.iter() {
  291. // sign our path from environment variables
  292. let (signature_path, signature) = sign_file(&secret_key, path)?;
  293. if signature.keynum() != public_key.keynum() {
  294. return Err(anyhow::anyhow!(
  295. "The updater secret key from `TAURI_PRIVATE_KEY` does not match the public key defined in `tauri.conf.json > tauri > updater > pubkey`."
  296. ));
  297. }
  298. signed_paths.append(&mut vec![signature_path]);
  299. }
  300. }
  301. print_signed_updater_archive(&signed_paths)?;
  302. }
  303. }
  304. Ok(())
  305. }
  306. fn run_hook(name: &str, hook: HookCommand, interface: &AppInterface, debug: bool) -> Result<()> {
  307. let (script, script_cwd) = match hook {
  308. HookCommand::Script(s) if s.is_empty() => (None, None),
  309. HookCommand::Script(s) => (Some(s), None),
  310. HookCommand::ScriptWithOptions { script, cwd } => (Some(script), cwd.map(Into::into)),
  311. };
  312. let cwd = script_cwd.unwrap_or_else(|| app_dir().clone());
  313. if let Some(script) = script {
  314. info!(action = "Running"; "{} `{}`", name, script);
  315. let mut env = command_env(debug);
  316. env.extend(interface.env());
  317. debug!("Setting environment for hook {:?}", env);
  318. #[cfg(target_os = "windows")]
  319. let status = Command::new("cmd")
  320. .arg("/S")
  321. .arg("/C")
  322. .arg(&script)
  323. .current_dir(cwd)
  324. .envs(env)
  325. .piped()
  326. .with_context(|| format!("failed to run `{}` with `cmd /C`", script))?;
  327. #[cfg(not(target_os = "windows"))]
  328. let status = Command::new("sh")
  329. .arg("-c")
  330. .arg(&script)
  331. .current_dir(cwd)
  332. .envs(env)
  333. .piped()
  334. .with_context(|| format!("failed to run `{script}` with `sh -c`"))?;
  335. if !status.success() {
  336. bail!(
  337. "{} `{}` failed with exit code {}",
  338. name,
  339. script,
  340. status.code().unwrap_or_default()
  341. );
  342. }
  343. }
  344. Ok(())
  345. }
  346. fn print_signed_updater_archive(output_paths: &[PathBuf]) -> crate::Result<()> {
  347. use std::fmt::Write;
  348. if !output_paths.is_empty() {
  349. let pluralised = if output_paths.len() == 1 {
  350. "updater signature"
  351. } else {
  352. "updater signatures"
  353. };
  354. let mut printable_paths = String::new();
  355. for path in output_paths {
  356. writeln!(
  357. printable_paths,
  358. " {}",
  359. tauri_utils::display_path(path)
  360. )?;
  361. }
  362. info!( action = "Finished"; "{} {} at:\n{}", output_paths.len(), pluralised, printable_paths);
  363. }
  364. Ok(())
  365. }
  366. #[cfg(target_os = "linux")]
  367. mod pkgconfig_utils {
  368. use std::{path::PathBuf, process::Command};
  369. pub fn get_appindicator_library_path() -> PathBuf {
  370. match get_library_path("ayatana-appindicator3-0.1") {
  371. Some(p) => format!("{p}/libayatana-appindicator3.so.1").into(),
  372. None => match get_library_path("appindicator3-0.1") {
  373. Some(p) => format!("{p}/libappindicator3.so.1").into(),
  374. None => panic!("Can't detect any appindicator library"),
  375. },
  376. }
  377. }
  378. /// Gets the folder in which a library is located using `pkg-config`.
  379. pub fn get_library_path(name: &str) -> Option<String> {
  380. let mut cmd = Command::new("pkg-config");
  381. cmd.env("PKG_CONFIG_ALLOW_SYSTEM_LIBS", "1");
  382. cmd.arg("--libs-only-L");
  383. cmd.arg(name);
  384. if let Ok(output) = cmd.output() {
  385. if !output.stdout.is_empty() {
  386. // output would be "-L/path/to/library\n"
  387. let word = output.stdout[2..].to_vec();
  388. return Some(String::from_utf8_lossy(&word).trim().to_string());
  389. } else {
  390. None
  391. }
  392. } else {
  393. None
  394. }
  395. }
  396. }