bundle.rs 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307
  1. // Copyright 2019-2024 Tauri Programme within The Commons Conservancy
  2. // SPDX-License-Identifier: Apache-2.0
  3. // SPDX-License-Identifier: MIT
  4. use std::{
  5. path::{Path, PathBuf},
  6. str::FromStr,
  7. sync::OnceLock,
  8. };
  9. use anyhow::Context;
  10. use clap::{builder::PossibleValue, ArgAction, Parser, ValueEnum};
  11. use tauri_bundler::PackageType;
  12. use tauri_utils::platform::Target;
  13. use crate::{
  14. helpers::{
  15. self,
  16. app_paths::tauri_dir,
  17. config::{get as get_config, ConfigMetadata},
  18. updater_signature,
  19. },
  20. interface::{AppInterface, AppSettings, Interface},
  21. ConfigValue,
  22. };
  23. #[derive(Debug, Clone)]
  24. pub struct BundleFormat(PackageType);
  25. impl FromStr for BundleFormat {
  26. type Err = anyhow::Error;
  27. fn from_str(s: &str) -> crate::Result<Self> {
  28. PackageType::from_short_name(s)
  29. .map(Self)
  30. .ok_or_else(|| anyhow::anyhow!("unknown bundle format {s}"))
  31. }
  32. }
  33. impl ValueEnum for BundleFormat {
  34. fn value_variants<'a>() -> &'a [Self] {
  35. static VARIANTS: OnceLock<Vec<BundleFormat>> = OnceLock::new();
  36. VARIANTS.get_or_init(|| PackageType::all().iter().map(|t| Self(*t)).collect())
  37. }
  38. fn to_possible_value(&self) -> Option<PossibleValue> {
  39. Some(PossibleValue::new(self.0.short_name()))
  40. }
  41. }
  42. #[derive(Debug, Parser, Clone)]
  43. #[clap(
  44. about = "Generate bundles and installers for your app (already built by `tauri build`)",
  45. long_about = "Generate bundles and installers for your app (already built by `tauri build`). This run `build.beforeBundleCommand` before generating the bundles and installers of your app."
  46. )]
  47. pub struct Options {
  48. /// Builds with the debug flag
  49. #[clap(short, long)]
  50. pub debug: bool,
  51. /// Space or comma separated list of bundles to package.
  52. ///
  53. /// Note that the `updater` bundle is not automatically added so you must specify it if the updater is enabled.
  54. #[clap(short, long, action = ArgAction::Append, num_args(0..), value_delimiter = ',')]
  55. pub bundles: Option<Vec<BundleFormat>>,
  56. /// JSON string or path to JSON file to merge with tauri.conf.json
  57. #[clap(short, long)]
  58. pub config: Option<ConfigValue>,
  59. /// Space or comma separated list of features, should be the same features passed to `tauri build` if any.
  60. #[clap(short, long, action = ArgAction::Append, num_args(0..))]
  61. pub features: Option<Vec<String>>,
  62. /// Target triple to build against.
  63. ///
  64. /// It must be one of the values outputted by `$rustc --print target-list` or `universal-apple-darwin` for an universal macOS application.
  65. ///
  66. /// Note that compiling an universal macOS application requires both `aarch64-apple-darwin` and `x86_64-apple-darwin` targets to be installed.
  67. #[clap(short, long)]
  68. pub target: Option<String>,
  69. /// Skip prompting for values
  70. #[clap(long, env = "CI")]
  71. pub ci: bool,
  72. }
  73. impl From<crate::build::Options> for Options {
  74. fn from(value: crate::build::Options) -> Self {
  75. Self {
  76. bundles: value.bundles,
  77. target: value.target,
  78. features: value.features,
  79. debug: value.debug,
  80. ci: value.ci,
  81. config: value.config,
  82. }
  83. }
  84. }
  85. pub fn command(options: Options, verbosity: u8) -> crate::Result<()> {
  86. crate::helpers::app_paths::resolve();
  87. let ci = options.ci;
  88. let target = options
  89. .target
  90. .as_deref()
  91. .map(Target::from_triple)
  92. .unwrap_or_else(Target::current);
  93. let config = get_config(target, options.config.as_ref().map(|c| &c.0))?;
  94. let interface = AppInterface::new(
  95. config.lock().unwrap().as_ref().unwrap(),
  96. options.target.clone(),
  97. )?;
  98. let tauri_path = tauri_dir();
  99. std::env::set_current_dir(tauri_path)
  100. .with_context(|| "failed to change current working directory")?;
  101. let config_guard = config.lock().unwrap();
  102. let config_ = config_guard.as_ref().unwrap();
  103. let app_settings = interface.app_settings();
  104. let interface_options = options.clone().into();
  105. let out_dir = app_settings.out_dir(&interface_options)?;
  106. bundle(
  107. &options,
  108. verbosity,
  109. ci,
  110. &interface,
  111. &app_settings,
  112. config_,
  113. &out_dir,
  114. )
  115. }
  116. #[allow(clippy::too_many_arguments)]
  117. pub fn bundle<A: AppSettings>(
  118. options: &Options,
  119. verbosity: u8,
  120. ci: bool,
  121. interface: &AppInterface,
  122. app_settings: &std::sync::Arc<A>,
  123. config: &ConfigMetadata,
  124. out_dir: &Path,
  125. ) -> crate::Result<()> {
  126. let package_types: Vec<PackageType> = if let Some(bundles) = &options.bundles {
  127. bundles.iter().map(|bundle| bundle.0).collect::<Vec<_>>()
  128. } else {
  129. config
  130. .bundle
  131. .targets
  132. .to_vec()
  133. .into_iter()
  134. .map(Into::into)
  135. .collect()
  136. };
  137. if package_types.is_empty() {
  138. return Ok(());
  139. }
  140. // if we have a package to bundle, let's run the `before_bundle_command`.
  141. if !package_types.is_empty() {
  142. if let Some(before_bundle) = config.build.before_bundle_command.clone() {
  143. helpers::run_hook(
  144. "beforeBundleCommand",
  145. before_bundle,
  146. interface,
  147. options.debug,
  148. )?;
  149. }
  150. }
  151. let mut settings = app_settings
  152. .get_bundler_settings(options.clone().into(), config, out_dir, package_types)
  153. .with_context(|| "failed to build bundler settings")?;
  154. settings.set_log_level(match verbosity {
  155. 0 => log::Level::Error,
  156. 1 => log::Level::Info,
  157. _ => log::Level::Trace,
  158. });
  159. // set env vars used by the bundler
  160. #[cfg(target_os = "linux")]
  161. {
  162. if config.bundle.linux.appimage.bundle_media_framework {
  163. std::env::set_var("APPIMAGE_BUNDLE_GSTREAMER", "1");
  164. }
  165. if let Some(open) = config.plugins.0.get("shell").and_then(|v| v.get("open")) {
  166. if open.as_bool().is_some_and(|x| x) || open.is_string() {
  167. std::env::set_var("APPIMAGE_BUNDLE_XDG_OPEN", "1");
  168. }
  169. }
  170. if settings.deep_link_protocols().is_some() {
  171. std::env::set_var("APPIMAGE_BUNDLE_XDG_MIME", "1");
  172. }
  173. }
  174. let bundles = tauri_bundler::bundle_project(&settings)
  175. .map_err(|e| match e {
  176. tauri_bundler::Error::BundlerError(e) => e,
  177. e => anyhow::anyhow!("{e:#}"),
  178. })
  179. .with_context(|| "failed to bundle project")?;
  180. sign_updaters(settings, bundles, ci)?;
  181. Ok(())
  182. }
  183. fn sign_updaters(
  184. settings: tauri_bundler::Settings,
  185. bundles: Vec<tauri_bundler::Bundle>,
  186. ci: bool,
  187. ) -> crate::Result<()> {
  188. let Some(update_settings) = settings.updater() else {
  189. // Updater not enabled
  190. return Ok(());
  191. };
  192. let update_enabled_bundles: Vec<&tauri_bundler::Bundle> = bundles
  193. .iter()
  194. .filter(|bundle| {
  195. matches!(
  196. bundle.package_type,
  197. PackageType::Updater | PackageType::Nsis | PackageType::WindowsMsi | PackageType::AppImage
  198. )
  199. })
  200. .collect();
  201. if update_enabled_bundles.is_empty() {
  202. return Ok(());
  203. }
  204. // get the public key
  205. let pubkey = &update_settings.pubkey;
  206. // check if pubkey points to a file...
  207. let maybe_path = Path::new(pubkey);
  208. let pubkey = if maybe_path.exists() {
  209. std::fs::read_to_string(maybe_path)?
  210. } else {
  211. pubkey.to_string()
  212. };
  213. // if no password provided we use an empty string
  214. let password = std::env::var("TAURI_SIGNING_PRIVATE_KEY_PASSWORD")
  215. .ok()
  216. .or_else(|| if ci { Some("".into()) } else { None });
  217. // get the private key
  218. let private_key = std::env::var("TAURI_SIGNING_PRIVATE_KEY")
  219. .map_err(|_| anyhow::anyhow!("A public key has been found, but no private key. Make sure to set `TAURI_SIGNING_PRIVATE_KEY` environment variable."))?;
  220. // check if private_key points to a file...
  221. let maybe_path = Path::new(&private_key);
  222. let private_key = if maybe_path.exists() {
  223. std::fs::read_to_string(maybe_path)
  224. .with_context(|| format!("faild to read {}", maybe_path.display()))?
  225. } else {
  226. private_key
  227. };
  228. let secret_key =
  229. updater_signature::secret_key(private_key, password).context("failed to decode secret key")?;
  230. let public_key = updater_signature::pub_key(pubkey).context("failed to decode pubkey")?;
  231. let mut signed_paths = Vec::new();
  232. for bundle in update_enabled_bundles {
  233. // we expect to have only one path in the vec but we iter if we add
  234. // another type of updater package who require multiple file signature
  235. for path in &bundle.bundle_paths {
  236. // sign our path from environment variables
  237. let (signature_path, signature) = updater_signature::sign_file(&secret_key, path)?;
  238. if signature.keynum() != public_key.keynum() {
  239. log::warn!("The updater secret key from `TAURI_SIGNING_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.");
  240. }
  241. signed_paths.push(signature_path);
  242. }
  243. }
  244. print_signed_updater_archive(&signed_paths)?;
  245. Ok(())
  246. }
  247. fn print_signed_updater_archive(output_paths: &[PathBuf]) -> crate::Result<()> {
  248. use std::fmt::Write;
  249. if !output_paths.is_empty() {
  250. let finished_bundles = output_paths.len();
  251. let pluralised = if finished_bundles == 1 {
  252. "updater signature"
  253. } else {
  254. "updater signatures"
  255. };
  256. let mut printable_paths = String::new();
  257. for path in output_paths {
  258. writeln!(
  259. printable_paths,
  260. " {}",
  261. tauri_utils::display_path(path)
  262. )?;
  263. }
  264. log::info!( action = "Finished"; "{finished_bundles} {pluralised} at:\n{printable_paths}");
  265. }
  266. Ok(())
  267. }