tauri_bundler/
bundle.rs

1// Copyright 2016-2019 Cargo-Bundle developers <https://github.com/burtonageo/cargo-bundle>
2// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
3// SPDX-License-Identifier: Apache-2.0
4// SPDX-License-Identifier: MIT
5
6mod category;
7#[cfg(target_os = "linux")]
8mod linux;
9#[cfg(target_os = "macos")]
10mod macos;
11mod platform;
12mod settings;
13mod updater_bundle;
14mod windows;
15
16use tauri_utils::display_path;
17
18pub use self::{
19  category::AppCategory,
20  settings::{
21    AppImageSettings, BundleBinary, BundleSettings, CustomSignCommandSettings, DebianSettings,
22    DmgSettings, MacOsSettings, PackageSettings, PackageType, Position, RpmSettings, Settings,
23    SettingsBuilder, Size, UpdaterSettings,
24  },
25};
26#[cfg(target_os = "macos")]
27use anyhow::Context;
28pub use settings::{NsisSettings, WindowsSettings, WixLanguage, WixLanguageConfig, WixSettings};
29
30use std::{fmt::Write, path::PathBuf};
31
32/// Generated bundle metadata.
33#[derive(Debug)]
34pub struct Bundle {
35  /// The package type.
36  pub package_type: PackageType,
37  /// All paths for this package.
38  pub bundle_paths: Vec<PathBuf>,
39}
40
41/// Bundles the project.
42/// Returns the list of paths where the bundles can be found.
43pub fn bundle_project(settings: &Settings) -> crate::Result<Vec<Bundle>> {
44  let mut package_types = settings.package_types()?;
45  if package_types.is_empty() {
46    return Ok(Vec::new());
47  }
48
49  package_types.sort_by_key(|a| a.priority());
50
51  let target_os = settings
52    .target()
53    .split('-')
54    .nth(2)
55    .unwrap_or(std::env::consts::OS)
56    .replace("darwin", "macos");
57
58  if target_os != std::env::consts::OS {
59    log::warn!("Cross-platform compilation is experimental and does not support all features. Please use a matching host system for full compatibility.");
60  }
61
62  // Sign windows binaries before the bundling step in case neither wix and nsis bundles are enabled
63  if target_os == "windows" {
64    if settings.can_sign() {
65      for bin in settings.binaries() {
66        let bin_path = settings.binary_path(bin);
67        windows::sign::try_sign(&bin_path, settings)?;
68      }
69
70      // Sign the sidecar binaries
71      for bin in settings.external_binaries() {
72        let path = bin?;
73        let skip = std::env::var("TAURI_SKIP_SIDECAR_SIGNATURE_CHECK").is_ok_and(|v| v == "true");
74        if skip {
75          continue;
76        }
77
78        #[cfg(windows)]
79        if windows::sign::verify(&path)? {
80          log::info!(
81            "sidecar at \"{}\" already signed. Skipping...",
82            path.display()
83          );
84          continue;
85        }
86
87        windows::sign::try_sign(&path, settings)?;
88      }
89    } else {
90      #[cfg(not(target_os = "windows"))]
91      log::warn!("Signing, by default, is only supported on Windows hosts, but you can specify a custom signing command in `bundler > windows > sign_command`, for now, skipping signing the installer...");
92    }
93  }
94
95  let mut bundles = Vec::<Bundle>::new();
96  for package_type in &package_types {
97    // bundle was already built! e.g. DMG already built .app
98    if bundles.iter().any(|b| b.package_type == *package_type) {
99      continue;
100    }
101
102    let bundle_paths = match package_type {
103      #[cfg(target_os = "macos")]
104      PackageType::MacOsBundle => macos::app::bundle_project(settings)?,
105      #[cfg(target_os = "macos")]
106      PackageType::IosBundle => macos::ios::bundle_project(settings)?,
107      // dmg is dependent of MacOsBundle, we send our bundles to prevent rebuilding
108      #[cfg(target_os = "macos")]
109      PackageType::Dmg => {
110        let bundled = macos::dmg::bundle_project(settings, &bundles)?;
111        if !bundled.app.is_empty() {
112          bundles.push(Bundle {
113            package_type: PackageType::MacOsBundle,
114            bundle_paths: bundled.app,
115          });
116        }
117        bundled.dmg
118      }
119
120      #[cfg(target_os = "windows")]
121      PackageType::WindowsMsi => windows::msi::bundle_project(settings, false)?,
122      PackageType::Nsis => windows::nsis::bundle_project(settings, false)?,
123
124      #[cfg(target_os = "linux")]
125      PackageType::Deb => linux::debian::bundle_project(settings)?,
126      #[cfg(target_os = "linux")]
127      PackageType::Rpm => linux::rpm::bundle_project(settings)?,
128      #[cfg(target_os = "linux")]
129      PackageType::AppImage => linux::appimage::bundle_project(settings)?,
130      _ => {
131        log::warn!("ignoring {}", package_type.short_name());
132        continue;
133      }
134    };
135
136    bundles.push(Bundle {
137      package_type: package_type.to_owned(),
138      bundle_paths,
139    });
140  }
141
142  if let Some(updater) = settings.updater() {
143    if package_types.iter().any(|package_type| {
144      if updater.v1_compatible {
145        matches!(
146          package_type,
147          PackageType::AppImage
148            | PackageType::MacOsBundle
149            | PackageType::Nsis
150            | PackageType::WindowsMsi
151            | PackageType::Deb
152        )
153      } else {
154        matches!(package_type, PackageType::MacOsBundle)
155      }
156    }) {
157      let updater_paths = updater_bundle::bundle_project(settings, &bundles)?;
158      bundles.push(Bundle {
159        package_type: PackageType::Updater,
160        bundle_paths: updater_paths,
161      });
162    } else if updater.v1_compatible
163      || !package_types.iter().any(|package_type| {
164        // Self contained updater, no need to zip
165        matches!(
166          package_type,
167          PackageType::AppImage | PackageType::Nsis | PackageType::WindowsMsi | PackageType::Deb
168        )
169      })
170    {
171      log::warn!("The bundler was configured to create updater artifacts but no updater-enabled targets were built. Please enable one of these targets: app, appimage, msi, nsis");
172    }
173    if updater.v1_compatible {
174      log::warn!("Legacy v1 compatible updater is deprecated and will be removed in v3, change bundle > createUpdaterArtifacts to true when your users are updated to the version with v2 updater plugin");
175    }
176  }
177
178  #[cfg(target_os = "macos")]
179  {
180    // Clean up .app if only building dmg or updater
181    if !package_types.contains(&PackageType::MacOsBundle) {
182      if let Some(app_bundle_paths) = bundles
183        .iter()
184        .position(|b| b.package_type == PackageType::MacOsBundle)
185        .map(|i| bundles.remove(i))
186        .map(|b| b.bundle_paths)
187      {
188        for app_bundle_path in &app_bundle_paths {
189          log::info!(action = "Cleaning"; "{}", app_bundle_path.display());
190          match app_bundle_path.is_dir() {
191            true => std::fs::remove_dir_all(app_bundle_path),
192            false => std::fs::remove_file(app_bundle_path),
193          }
194          .with_context(|| {
195            format!(
196              "Failed to clean the app bundle at {}",
197              app_bundle_path.display()
198            )
199          })?
200        }
201      }
202    }
203  }
204
205  if bundles.is_empty() {
206    return Err(anyhow::anyhow!("No bundles were built").into());
207  }
208
209  let bundles_wo_updater = bundles
210    .iter()
211    .filter(|b| b.package_type != PackageType::Updater)
212    .collect::<Vec<_>>();
213  let finished_bundles = bundles_wo_updater.len();
214  let pluralised = if finished_bundles == 1 {
215    "bundle"
216  } else {
217    "bundles"
218  };
219
220  let mut printable_paths = String::new();
221  for bundle in &bundles {
222    for path in &bundle.bundle_paths {
223      let note = if bundle.package_type == crate::PackageType::Updater {
224        " (updater)"
225      } else {
226        ""
227      };
228      let path_display = display_path(path);
229      writeln!(printable_paths, "        {path_display}{note}").unwrap();
230    }
231  }
232
233  log::info!(action = "Finished"; "{finished_bundles} {pluralised} at:\n{printable_paths}");
234
235  Ok(bundles)
236}
237
238/// Check to see if there are icons in the settings struct
239pub fn check_icons(settings: &Settings) -> crate::Result<bool> {
240  // make a peekable iterator of the icon_files
241  let mut iter = settings.icon_files().peekable();
242
243  // if iter's first value is a None then there are no Icon files in the settings struct
244  if iter.peek().is_none() {
245    Ok(false)
246  } else {
247    Ok(true)
248  }
249}