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, platform::Target as TargetPlatform};
17
18/// Patch a binary with bundle type information
19fn patch_binary(binary: &PathBuf, package_type: &PackageType) -> crate::Result<()> {
20  match package_type {
21    #[cfg(target_os = "linux")]
22    PackageType::AppImage | PackageType::Deb | PackageType::Rpm => {
23      log::info!(
24        "Patching binary {:?} for type {}",
25        binary,
26        package_type.short_name()
27      );
28      linux::patch_binary(binary, package_type)?;
29    }
30    PackageType::Nsis | PackageType::WindowsMsi => {
31      log::info!(
32        "Patching binary {:?} for type {}",
33        binary,
34        package_type.short_name()
35      );
36      windows::patch_binary(binary, package_type)?;
37    }
38    _ => (),
39  }
40
41  Ok(())
42}
43
44pub use self::{
45  category::AppCategory,
46  settings::{
47    AppImageSettings, BundleBinary, BundleSettings, CustomSignCommandSettings, DebianSettings,
48    DmgSettings, Entitlements, IosSettings, MacOsSettings, PackageSettings, PackageType, PlistKind,
49    Position, RpmSettings, Settings, SettingsBuilder, Size, UpdaterSettings,
50  },
51};
52pub use settings::{NsisSettings, WindowsSettings, WixLanguage, WixLanguageConfig, WixSettings};
53
54use std::{
55  fmt::Write,
56  io::{Seek, SeekFrom},
57  path::PathBuf,
58};
59
60/// Generated bundle metadata.
61#[derive(Debug)]
62pub struct Bundle {
63  /// The package type.
64  pub package_type: PackageType,
65  /// All paths for this package.
66  pub bundle_paths: Vec<PathBuf>,
67}
68
69/// Bundles the project.
70/// Returns the list of paths where the bundles can be found.
71pub fn bundle_project(settings: &Settings) -> crate::Result<Vec<Bundle>> {
72  let mut package_types = settings.package_types()?;
73  if package_types.is_empty() {
74    return Ok(Vec::new());
75  }
76
77  package_types.sort_by_key(|a| a.priority());
78
79  let target_os = settings.target_platform();
80
81  if *target_os != TargetPlatform::current() {
82    log::warn!("Cross-platform compilation is experimental and does not support all features. Please use a matching host system for full compatibility.");
83  }
84
85  // Sign windows binaries before the bundling step in case neither wix and nsis bundles are enabled
86  sign_binaries_if_needed(settings, target_os)?;
87
88  let main_binary = settings
89    .binaries()
90    .iter()
91    .find(|b| b.main())
92    .expect("Main binary missing in settings");
93  let main_binary_path = settings.binary_path(main_binary);
94
95  // When packaging multiple binary types, we make a copy of the unsigned main_binary so that we can
96  // restore it after each package_type step. This avoids two issues:
97  //  - modifying a signed binary without updating its PE checksum can break signature verification
98  //    - codesigning tools should handle calculating+updating this, we just need to ensure
99  //      (re)signing is performed after every `patch_binary()` operation
100  //  - signing an already-signed binary can result in multiple signatures, causing verification errors
101  let main_binary_reset_required = matches!(target_os, TargetPlatform::Windows)
102    && settings.windows().can_sign()
103    && package_types.len() > 1;
104  let mut unsigned_main_binary_copy = tempfile::tempfile()?;
105  if main_binary_reset_required {
106    let mut unsigned_main_binary = std::fs::File::open(&main_binary_path)?;
107    std::io::copy(&mut unsigned_main_binary, &mut unsigned_main_binary_copy)?;
108  }
109
110  let mut main_binary_signed = false;
111  let mut bundles = Vec::<Bundle>::new();
112  for package_type in &package_types {
113    // bundle was already built! e.g. DMG already built .app
114    if bundles.iter().any(|b| b.package_type == *package_type) {
115      continue;
116    }
117
118    if let Err(e) = patch_binary(&main_binary_path, package_type) {
119      log::warn!("Failed to add bundler type to the binary: {e}. Updater plugin may not be able to update this package. This shouldn't normally happen, please report it to https://github.com/tauri-apps/tauri/issues");
120    }
121
122    // sign main binary for every package type after patch
123    if matches!(target_os, TargetPlatform::Windows) && settings.windows().can_sign() {
124      if main_binary_signed && main_binary_reset_required {
125        let mut signed_main_binary = std::fs::OpenOptions::new()
126          .write(true)
127          .truncate(true)
128          .open(&main_binary_path)?;
129        unsigned_main_binary_copy.seek(SeekFrom::Start(0))?;
130        std::io::copy(&mut unsigned_main_binary_copy, &mut signed_main_binary)?;
131      }
132      windows::sign::try_sign(&main_binary_path, settings)?;
133      main_binary_signed = true;
134    }
135
136    let bundle_paths = match package_type {
137      #[cfg(target_os = "macos")]
138      PackageType::MacOsBundle => macos::app::bundle_project(settings)?,
139      #[cfg(target_os = "macos")]
140      PackageType::IosBundle => macos::ios::bundle_project(settings)?,
141      // dmg is dependent of MacOsBundle, we send our bundles to prevent rebuilding
142      #[cfg(target_os = "macos")]
143      PackageType::Dmg => {
144        let bundled = macos::dmg::bundle_project(settings, &bundles)?;
145        if !bundled.app.is_empty() {
146          bundles.push(Bundle {
147            package_type: PackageType::MacOsBundle,
148            bundle_paths: bundled.app,
149          });
150        }
151        bundled.dmg
152      }
153
154      #[cfg(target_os = "windows")]
155      PackageType::WindowsMsi => windows::msi::bundle_project(settings, false)?,
156      // note: don't restrict to windows as NSIS installers can be built in linux using cargo-xwin
157      PackageType::Nsis => windows::nsis::bundle_project(settings, false)?,
158
159      #[cfg(target_os = "linux")]
160      PackageType::Deb => linux::debian::bundle_project(settings)?,
161      #[cfg(target_os = "linux")]
162      PackageType::Rpm => linux::rpm::bundle_project(settings)?,
163      #[cfg(target_os = "linux")]
164      PackageType::AppImage => linux::appimage::bundle_project(settings)?,
165      _ => {
166        log::warn!("ignoring {}", package_type.short_name());
167        continue;
168      }
169    };
170
171    bundles.push(Bundle {
172      package_type: package_type.to_owned(),
173      bundle_paths,
174    });
175  }
176
177  if let Some(updater) = settings.updater() {
178    if package_types.iter().any(|package_type| {
179      if updater.v1_compatible {
180        matches!(
181          package_type,
182          PackageType::AppImage
183            | PackageType::MacOsBundle
184            | PackageType::Nsis
185            | PackageType::WindowsMsi
186            | PackageType::Deb
187        )
188      } else {
189        matches!(package_type, PackageType::MacOsBundle)
190      }
191    }) {
192      let updater_paths = updater_bundle::bundle_project(settings, &bundles)?;
193      bundles.push(Bundle {
194        package_type: PackageType::Updater,
195        bundle_paths: updater_paths,
196      });
197    } else if updater.v1_compatible
198      || !package_types.iter().any(|package_type| {
199        // Self contained updater, no need to zip
200        matches!(
201          package_type,
202          PackageType::AppImage | PackageType::Nsis | PackageType::WindowsMsi | PackageType::Deb
203        )
204      })
205    {
206      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");
207    }
208    if updater.v1_compatible {
209      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");
210    }
211  }
212
213  #[cfg(target_os = "macos")]
214  {
215    // Clean up .app if only building dmg or updater
216    if !package_types.contains(&PackageType::MacOsBundle) {
217      if let Some(app_bundle_paths) = bundles
218        .iter()
219        .position(|b| b.package_type == PackageType::MacOsBundle)
220        .map(|i| bundles.remove(i))
221        .map(|b| b.bundle_paths)
222      {
223        for app_bundle_path in &app_bundle_paths {
224          use crate::error::ErrorExt;
225
226          log::info!(action = "Cleaning"; "{}", app_bundle_path.display());
227          match app_bundle_path.is_dir() {
228            true => std::fs::remove_dir_all(app_bundle_path),
229            false => std::fs::remove_file(app_bundle_path),
230          }
231          .fs_context(
232            "failed to clean the app bundle",
233            app_bundle_path.to_path_buf(),
234          )?;
235        }
236      }
237    }
238  }
239
240  if bundles.is_empty() {
241    return Ok(bundles);
242  }
243
244  let finished_bundles = bundles
245    .iter()
246    .filter(|b| b.package_type != PackageType::Updater)
247    .count();
248  let pluralised = if finished_bundles == 1 {
249    "bundle"
250  } else {
251    "bundles"
252  };
253
254  let mut printable_paths = String::new();
255  for bundle in &bundles {
256    for path in &bundle.bundle_paths {
257      let note = if bundle.package_type == crate::PackageType::Updater {
258        " (updater)"
259      } else {
260        ""
261      };
262      let path_display = display_path(path);
263      writeln!(printable_paths, "        {path_display}{note}").unwrap();
264    }
265  }
266
267  log::info!(action = "Finished"; "{finished_bundles} {pluralised} at:\n{printable_paths}");
268
269  Ok(bundles)
270}
271
272fn sign_binaries_if_needed(settings: &Settings, target_os: &TargetPlatform) -> crate::Result<()> {
273  if matches!(target_os, TargetPlatform::Windows) {
274    if settings.windows().can_sign() {
275      if settings.no_sign() {
276        log::warn!("Skipping binary signing due to --no-sign flag.");
277        return Ok(());
278      }
279
280      for bin in settings.binaries() {
281        if bin.main() {
282          // we will sign the main binary after patching per "package type"
283          continue;
284        }
285        let bin_path = settings.binary_path(bin);
286        windows::sign::try_sign(&bin_path, settings)?;
287      }
288
289      // Sign the sidecar binaries
290      for bin in settings.external_binaries() {
291        let path = bin?;
292        let skip = std::env::var("TAURI_SKIP_SIDECAR_SIGNATURE_CHECK").is_ok_and(|v| v == "true");
293        if skip {
294          continue;
295        }
296
297        #[cfg(windows)]
298        if windows::sign::verify(&path)? {
299          log::info!(
300            "sidecar at \"{}\" already signed. Skipping...",
301            path.display()
302          );
303          continue;
304        }
305
306        windows::sign::try_sign(&path, settings)?;
307      }
308    } else {
309      #[cfg(not(target_os = "windows"))]
310      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...");
311    }
312  }
313
314  Ok(())
315}
316
317/// Check to see if there are icons in the settings struct
318pub fn check_icons(settings: &Settings) -> crate::Result<bool> {
319  // make a peekable iterator of the icon_files
320  let mut iter = settings.icon_files().peekable();
321
322  // if iter's first value is a None then there are no Icon files in the settings struct
323  if iter.peek().is_none() {
324    Ok(false)
325  } else {
326    Ok(true)
327  }
328}