build.rs 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444
  1. // Copyright 2019-2024 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, ConfigHandle, ConfigMetadata, FrontendDist, HookCommand},
  9. updater_signature::{secret_key as updater_secret_key, sign_file},
  10. },
  11. interface::{AppInterface, AppSettings, Interface},
  12. CommandExt, ConfigValue, Result,
  13. };
  14. use anyhow::{bail, Context};
  15. use base64::Engine;
  16. use clap::{builder::PossibleValue, ArgAction, Parser, ValueEnum};
  17. use std::{
  18. env::{set_current_dir, var},
  19. path::{Path, PathBuf},
  20. process::Command,
  21. str::FromStr,
  22. sync::OnceLock,
  23. };
  24. use tauri_bundler::{
  25. bundle::{bundle_project, PackageType},
  26. Bundle,
  27. };
  28. use tauri_utils::platform::Target;
  29. #[derive(Debug, Clone)]
  30. pub struct BundleFormat(PackageType);
  31. impl FromStr for BundleFormat {
  32. type Err = anyhow::Error;
  33. fn from_str(s: &str) -> Result<Self> {
  34. PackageType::from_short_name(s)
  35. .map(Self)
  36. .ok_or_else(|| anyhow::anyhow!("unknown bundle format {s}"))
  37. }
  38. }
  39. impl ValueEnum for BundleFormat {
  40. fn value_variants<'a>() -> &'a [Self] {
  41. static VARIANTS: OnceLock<Vec<BundleFormat>> = OnceLock::new();
  42. VARIANTS.get_or_init(|| PackageType::all().iter().map(|t| Self(*t)).collect())
  43. }
  44. fn to_possible_value(&self) -> Option<PossibleValue> {
  45. Some(PossibleValue::new(self.0.short_name()))
  46. }
  47. }
  48. #[derive(Debug, Clone, Parser)]
  49. #[clap(
  50. about = "Build your app in release mode and generate bundles and installers",
  51. long_about = "Build your app in release mode and generate bundles and installers. It makes use of the `build.frontendDist` property from your `tauri.conf.json` file. It also runs your `build.beforeBuildCommand` which usually builds your frontend into `build.frontendDist`. This will also run `build.beforeBundleCommand` before generating the bundles and installers of your app."
  52. )]
  53. pub struct Options {
  54. /// Binary to use to build the application, defaults to `cargo`
  55. #[clap(short, long)]
  56. pub runner: Option<String>,
  57. /// Builds with the debug flag
  58. #[clap(short, long)]
  59. pub debug: bool,
  60. /// Target triple to build against.
  61. ///
  62. /// It must be one of the values outputted by `$rustc --print target-list` or `universal-apple-darwin` for an universal macOS application.
  63. ///
  64. /// Note that compiling an universal macOS application requires both `aarch64-apple-darwin` and `x86_64-apple-darwin` targets to be installed.
  65. #[clap(short, long)]
  66. pub target: Option<String>,
  67. /// Space or comma separated list of features to activate
  68. #[clap(short, long, action = ArgAction::Append, num_args(0..))]
  69. pub features: Option<Vec<String>>,
  70. /// Space or comma separated list of bundles to package.
  71. ///
  72. /// Note that the `updater` bundle is not automatically added so you must specify it if the updater is enabled.
  73. #[clap(short, long, action = ArgAction::Append, num_args(0..), value_delimiter = ',')]
  74. pub bundles: Option<Vec<BundleFormat>>,
  75. /// Skip the bundling step even if `bundle > active` is `true` in tauri config.
  76. #[clap(long)]
  77. pub no_bundle: bool,
  78. /// JSON string or path to JSON file to merge with tauri.conf.json
  79. #[clap(short, long)]
  80. pub config: Option<ConfigValue>,
  81. /// Command line arguments passed to the runner. Use `--` to explicitly mark the start of the arguments.
  82. pub args: Vec<String>,
  83. /// Skip prompting for values
  84. #[clap(long, env = "CI")]
  85. pub ci: bool,
  86. }
  87. pub fn command(mut options: Options, verbosity: u8) -> Result<()> {
  88. let ci = options.ci;
  89. let target = options
  90. .target
  91. .as_deref()
  92. .map(Target::from_triple)
  93. .unwrap_or_else(Target::current);
  94. let config = get_config(target, options.config.as_ref().map(|c| &c.0))?;
  95. let mut interface = AppInterface::new(
  96. config.lock().unwrap().as_ref().unwrap(),
  97. options.target.clone(),
  98. )?;
  99. setup(&interface, &mut options, config.clone(), false)?;
  100. let config_guard = config.lock().unwrap();
  101. let config_ = config_guard.as_ref().unwrap();
  102. let app_settings = interface.app_settings();
  103. let interface_options = options.clone().into();
  104. let bin_path = app_settings.app_binary_path(&interface_options)?;
  105. let out_dir = bin_path.parent().unwrap();
  106. interface.build(interface_options)?;
  107. let app_settings = interface.app_settings();
  108. bundle(
  109. &options,
  110. verbosity,
  111. ci,
  112. &interface,
  113. &app_settings,
  114. config_,
  115. out_dir,
  116. )?;
  117. Ok(())
  118. }
  119. pub fn setup(
  120. interface: &AppInterface,
  121. options: &mut Options,
  122. config: ConfigHandle,
  123. mobile: bool,
  124. ) -> Result<()> {
  125. let tauri_path = tauri_dir();
  126. set_current_dir(tauri_path).with_context(|| "failed to change current working directory")?;
  127. let config_guard = config.lock().unwrap();
  128. let config_ = config_guard.as_ref().unwrap();
  129. let bundle_identifier_source = config_
  130. .find_bundle_identifier_overwriter()
  131. .unwrap_or_else(|| "tauri.conf.json".into());
  132. if config_.identifier == "com.tauri.dev" {
  133. log::error!(
  134. "You must change the bundle identifier in `{} identifier`. The default value `com.tauri.dev` is not allowed as it must be unique across applications.",
  135. bundle_identifier_source
  136. );
  137. std::process::exit(1);
  138. }
  139. if config_
  140. .identifier
  141. .chars()
  142. .any(|ch| !(ch.is_alphanumeric() || ch == '-' || ch == '.'))
  143. {
  144. log::error!(
  145. "The bundle identifier \"{}\" set in `{} identifier`. The bundle identifier string must contain only alphanumeric characters (A-Z, a-z, and 0-9), hyphens (-), and periods (.).",
  146. config_.identifier,
  147. bundle_identifier_source
  148. );
  149. std::process::exit(1);
  150. }
  151. if let Some(before_build) = config_.build.before_build_command.clone() {
  152. run_hook("beforeBuildCommand", before_build, interface, options.debug)?;
  153. }
  154. if let Some(FrontendDist::Directory(web_asset_path)) = &config_.build.frontend_dist {
  155. if !web_asset_path.exists() {
  156. let absolute_path = web_asset_path
  157. .parent()
  158. .and_then(|p| p.canonicalize().ok())
  159. .map(|p| p.join(web_asset_path.file_name().unwrap()))
  160. .unwrap_or_else(|| std::env::current_dir().unwrap().join(web_asset_path));
  161. return Err(anyhow::anyhow!(
  162. "Unable to find your web assets, did you forget to build your web app? Your frontendDist is set to \"{}\" (which is `{}`).",
  163. web_asset_path.display(), absolute_path.display(),
  164. ));
  165. }
  166. if web_asset_path.canonicalize()?.file_name() == Some(std::ffi::OsStr::new("src-tauri")) {
  167. return Err(anyhow::anyhow!(
  168. "The configured frontendDist is the `src-tauri` folder. Please isolate your web assets on a separate folder and update `tauri.conf.json > build > frontendDist`.",
  169. ));
  170. }
  171. let mut out_folders = Vec::new();
  172. for folder in &["node_modules", "src-tauri", "target"] {
  173. if web_asset_path.join(folder).is_dir() {
  174. out_folders.push(folder.to_string());
  175. }
  176. }
  177. if !out_folders.is_empty() {
  178. return Err(anyhow::anyhow!(
  179. "The configured frontendDist includes the `{:?}` {}. Please isolate your web assets on a separate folder and update `tauri.conf.json > build > frontendDist`.",
  180. out_folders,
  181. if out_folders.len() == 1 { "folder" }else { "folders" }
  182. )
  183. );
  184. }
  185. }
  186. if options.runner.is_none() {
  187. options.runner.clone_from(&config_.build.runner);
  188. }
  189. options
  190. .features
  191. .get_or_insert(Vec::new())
  192. .extend(config_.build.features.clone().unwrap_or_default());
  193. interface.build_options(&mut options.args, &mut options.features, mobile);
  194. Ok(())
  195. }
  196. fn bundle<A: AppSettings>(
  197. options: &Options,
  198. verbosity: u8,
  199. ci: bool,
  200. interface: &AppInterface,
  201. app_settings: &std::sync::Arc<A>,
  202. config: &ConfigMetadata,
  203. out_dir: &Path,
  204. ) -> crate::Result<()> {
  205. if options.no_bundle || (options.bundles.is_none() && !config.bundle.active) {
  206. return Ok(());
  207. }
  208. let package_types: Vec<PackageType> = if let Some(bundles) = &options.bundles {
  209. bundles.iter().map(|bundle| bundle.0).collect::<Vec<_>>()
  210. } else {
  211. config
  212. .bundle
  213. .targets
  214. .to_vec()
  215. .into_iter()
  216. .map(Into::into)
  217. .collect()
  218. };
  219. if package_types.is_empty() {
  220. return Ok(());
  221. }
  222. // if we have a package to bundle, let's run the `before_bundle_command`.
  223. if !package_types.is_empty() {
  224. if let Some(before_bundle) = config.build.before_bundle_command.clone() {
  225. run_hook(
  226. "beforeBundleCommand",
  227. before_bundle,
  228. interface,
  229. options.debug,
  230. )?;
  231. }
  232. }
  233. let mut settings = app_settings
  234. .get_bundler_settings(options.clone().into(), config, out_dir, package_types)
  235. .with_context(|| "failed to build bundler settings")?;
  236. settings.set_log_level(match verbosity {
  237. 0 => log::Level::Error,
  238. 1 => log::Level::Info,
  239. _ => log::Level::Trace,
  240. });
  241. // set env vars used by the bundler
  242. #[cfg(target_os = "linux")]
  243. {
  244. if config.bundle.linux.appimage.bundle_media_framework {
  245. std::env::set_var("APPIMAGE_BUNDLE_GSTREAMER", "1");
  246. }
  247. if let Some(open) = config.plugins.0.get("shell").and_then(|v| v.get("open")) {
  248. if open.as_bool().is_some_and(|x| x) || open.is_string() {
  249. std::env::set_var("APPIMAGE_BUNDLE_XDG_OPEN", "1");
  250. }
  251. }
  252. if settings.deep_link_protocols().is_some() {
  253. std::env::set_var("APPIMAGE_BUNDLE_XDG_MIME", "1");
  254. }
  255. }
  256. let bundles = bundle_project(settings)
  257. .map_err(|e| anyhow::anyhow!("{:#}", e))
  258. .with_context(|| "failed to bundle project")?;
  259. let update_enabled_bundles: Vec<&Bundle> = bundles
  260. .iter()
  261. .filter(|bundle| {
  262. matches!(
  263. bundle.package_type,
  264. PackageType::Updater | PackageType::Nsis | PackageType::WindowsMsi | PackageType::AppImage
  265. )
  266. })
  267. .collect();
  268. // Skip if no updater is active
  269. if !update_enabled_bundles.is_empty() {
  270. let updater_pub_key = config
  271. .plugins
  272. .0
  273. .get("updater")
  274. .and_then(|k| k.get("pubkey"))
  275. .and_then(|v| v.as_str())
  276. .map(|v| v.to_string());
  277. if let Some(pubkey) = updater_pub_key {
  278. // get the public key
  279. // check if pubkey points to a file...
  280. let maybe_path = Path::new(&pubkey);
  281. let pubkey = if maybe_path.exists() {
  282. std::fs::read_to_string(maybe_path)?
  283. } else {
  284. pubkey
  285. };
  286. // if no password provided we use an empty string
  287. let password = var("TAURI_SIGNING_PRIVATE_KEY_PASSWORD").ok().or_else(|| {
  288. if ci {
  289. Some("".into())
  290. } else {
  291. None
  292. }
  293. });
  294. // get the private key
  295. let secret_key = match var("TAURI_SIGNING_PRIVATE_KEY") {
  296. Ok(private_key) => {
  297. // check if private_key points to a file...
  298. let maybe_path = Path::new(&private_key);
  299. let private_key = if maybe_path.exists() {
  300. std::fs::read_to_string(maybe_path)?
  301. } else {
  302. private_key
  303. };
  304. updater_secret_key(private_key, password)
  305. }
  306. _ => Err(anyhow::anyhow!("A public key has been found, but no private key. Make sure to set `TAURI_SIGNING_PRIVATE_KEY` environment variable.")),
  307. }?;
  308. let pubkey = base64::engine::general_purpose::STANDARD.decode(pubkey)?;
  309. let pub_key_decoded = String::from_utf8_lossy(&pubkey);
  310. let public_key = minisign::PublicKeyBox::from_string(&pub_key_decoded)?.into_public_key()?;
  311. // make sure we have our package built
  312. let mut signed_paths = Vec::new();
  313. for bundle in update_enabled_bundles {
  314. // we expect to have only one path in the vec but we iter if we add
  315. // another type of updater package who require multiple file signature
  316. for path in bundle.bundle_paths.iter() {
  317. // sign our path from environment variables
  318. let (signature_path, signature) = sign_file(&secret_key, path)?;
  319. if signature.keynum() != public_key.keynum() {
  320. log::warn!("The updater secret key from `TAURI_PRIVATE_KEY` does not match the public key from `plugins > updater > pubkey`. If you are not rotating keys, this means your configuration is wrong and won't be accepted at runtime when performing update.");
  321. }
  322. signed_paths.push(signature_path);
  323. }
  324. }
  325. print_signed_updater_archive(&signed_paths)?;
  326. }
  327. }
  328. Ok(())
  329. }
  330. fn run_hook(name: &str, hook: HookCommand, interface: &AppInterface, debug: bool) -> Result<()> {
  331. let (script, script_cwd) = match hook {
  332. HookCommand::Script(s) if s.is_empty() => (None, None),
  333. HookCommand::Script(s) => (Some(s), None),
  334. HookCommand::ScriptWithOptions { script, cwd } => (Some(script), cwd.map(Into::into)),
  335. };
  336. let cwd = script_cwd.unwrap_or_else(|| app_dir().clone());
  337. if let Some(script) = script {
  338. log::info!(action = "Running"; "{} `{}`", name, script);
  339. let mut env = command_env(debug);
  340. env.extend(interface.env());
  341. log::debug!("Setting environment for hook {:?}", env);
  342. #[cfg(target_os = "windows")]
  343. let status = Command::new("cmd")
  344. .arg("/S")
  345. .arg("/C")
  346. .arg(&script)
  347. .current_dir(cwd)
  348. .envs(env)
  349. .piped()
  350. .with_context(|| format!("failed to run `{}` with `cmd /C`", script))?;
  351. #[cfg(not(target_os = "windows"))]
  352. let status = Command::new("sh")
  353. .arg("-c")
  354. .arg(&script)
  355. .current_dir(cwd)
  356. .envs(env)
  357. .piped()
  358. .with_context(|| format!("failed to run `{script}` with `sh -c`"))?;
  359. if !status.success() {
  360. bail!(
  361. "{} `{}` failed with exit code {}",
  362. name,
  363. script,
  364. status.code().unwrap_or_default()
  365. );
  366. }
  367. }
  368. Ok(())
  369. }
  370. fn print_signed_updater_archive(output_paths: &[PathBuf]) -> crate::Result<()> {
  371. use std::fmt::Write;
  372. if !output_paths.is_empty() {
  373. let pluralised = if output_paths.len() == 1 {
  374. "updater signature"
  375. } else {
  376. "updater signatures"
  377. };
  378. let mut printable_paths = String::new();
  379. for path in output_paths {
  380. writeln!(
  381. printable_paths,
  382. " {}",
  383. tauri_utils::display_path(path)
  384. )?;
  385. }
  386. log::info!( action = "Finished"; "{} {} at:\n{}", output_paths.len(), pluralised, printable_paths);
  387. }
  388. Ok(())
  389. }