Преглед изворни кода

refactor(bundler): finish initial documentation, reorganize modules (#1662)

Lucas Fernandes Nogueira пре 4 година
родитељ
комит
fcee4c25fc
31 измењених фајлова са 724 додато и 632 уклоњено
  1. 5 0
      .changes/bundler-docs.md
  2. 5 0
      .changes/bundler-package-types.md
  3. 7 8
      tooling/bundler/Cargo.toml
  4. 27 22
      tooling/bundler/src/bundle.rs
  5. 1 0
      tooling/bundler/src/bundle/category.rs
  6. 4 36
      tooling/bundler/src/bundle/common.rs
  7. 6 3
      tooling/bundler/src/bundle/linux/appimage.rs
  8. 1 1
      tooling/bundler/src/bundle/linux/debian.rs
  9. 3 0
      tooling/bundler/src/bundle/linux/mod.rs
  10. 0 0
      tooling/bundler/src/bundle/linux/rpm.rs
  11. 0 0
      tooling/bundler/src/bundle/linux/templates/appimage
  12. 4 406
      tooling/bundler/src/bundle/macos/app.rs
  13. 3 3
      tooling/bundler/src/bundle/macos/dmg.rs
  14. 1 2
      tooling/bundler/src/bundle/macos/ios.rs
  15. 4 0
      tooling/bundler/src/bundle/macos/mod.rs
  16. 412 0
      tooling/bundler/src/bundle/macos/sign.rs
  17. 0 0
      tooling/bundler/src/bundle/macos/templates/dmg/bundle_dmg
  18. 0 0
      tooling/bundler/src/bundle/macos/templates/dmg/dmg-license.py
  19. 0 0
      tooling/bundler/src/bundle/macos/templates/dmg/template.applescript
  20. 40 12
      tooling/bundler/src/bundle/settings.rs
  21. 7 9
      tooling/bundler/src/bundle/updater_bundle.rs
  22. 2 0
      tooling/bundler/src/bundle/windows/mod.rs
  23. 3 1
      tooling/bundler/src/bundle/windows/msi.rs
  24. 3 113
      tooling/bundler/src/bundle/windows/msi/wix.rs
  25. 117 0
      tooling/bundler/src/bundle/windows/sign.rs
  26. 0 0
      tooling/bundler/src/bundle/windows/templates/main.wxs
  27. 29 10
      tooling/bundler/src/error.rs
  28. 14 0
      tooling/bundler/src/lib.rs
  29. 16 3
      tooling/cli.rs/src/build.rs
  30. 1 1
      tooling/cli.rs/src/build/rust.rs
  31. 9 2
      tooling/cli.rs/src/helpers/updater_signature.rs

+ 5 - 0
.changes/bundler-docs.md

@@ -0,0 +1,5 @@
+---
+"tauri-bundler": patch
+---
+
+Adds basic library documentation.

+ 5 - 0
.changes/bundler-package-types.md

@@ -0,0 +1,5 @@
+---
+"tauri-bundler": patch
+---
+
+The `PackageTypes` enum now includes all options, including Windows packages.

+ 7 - 8
tooling/bundler/Cargo.toml

@@ -16,8 +16,6 @@ edition = "2018"
 
 [dependencies]
 ar = "0.8.0"
-chrono = "0.4"
-dirs-next = "2.0.0"
 glob = "0.3.0"
 icns = "0.3"
 image = "0.23.14"
@@ -35,18 +33,19 @@ walkdir = "2"
 handlebars = { version = "3.5" }
 zip = { version = "0.5" }
 tempfile = "3.2.0"
-regex = { version = "1" }
+regex = "1"
 
 [target."cfg(target_os = \"windows\")".dependencies]
-attohttpc = { version = "0.17.0" }
-regex = { version = "1" }
+attohttpc = "0.17"
 uuid = { version = "0.8", features = [ "v5" ] }
 bitness = "0.4"
 winreg = "0.8"
+sha2 = "0.9"
+hex = "0.4"
 
-[target."cfg(not(target_os = \"linux\"))".dependencies]
-sha2 = { version = "0.9" }
-hex = { version = "0.4" }
+[target."cfg(target_os = \"macos\")".dependencies]
+chrono = "0.4"
+dirs-next = "2.0"
 
 [lib]
 name = "tauri_bundler"

+ 27 - 22
tooling/bundler/src/bundle.rs

@@ -2,42 +2,37 @@
 // SPDX-License-Identifier: Apache-2.0
 // SPDX-License-Identifier: MIT
 
-mod appimage_bundle;
 mod category;
-pub mod common;
-mod deb_bundle;
-mod dmg_bundle;
-mod ios_bundle;
-mod macos_bundle;
-#[cfg(target_os = "windows")]
-mod msi_bundle;
+mod common;
+#[cfg(target_os = "linux")]
+mod linux;
+#[cfg(target_os = "macos")]
+mod macos;
 mod path_utils;
 mod platform;
-mod rpm_bundle;
 mod settings;
 mod updater_bundle;
 #[cfg(target_os = "windows")]
-mod wix;
+mod windows;
 
 pub use self::{
   category::AppCategory,
-  common::{print_error, print_info},
   settings::{
     BundleBinary, BundleSettings, DebianSettings, MacOsSettings, PackageSettings, PackageType,
     Settings, SettingsBuilder, UpdaterSettings,
   },
 };
-#[cfg(windows)]
 pub use settings::{WindowsSettings, WixSettings};
 
-use common::print_finished;
+use common::{print_finished, print_info};
 
 use std::path::PathBuf;
 
+/// Generated bundle metadata.
 pub struct Bundle {
-  // the package type
+  /// The package type.
   pub package_type: PackageType,
-  /// all paths for this package
+  /// All paths for this package.
   pub bundle_paths: Vec<PathBuf>,
 }
 
@@ -49,17 +44,27 @@ pub fn bundle_project(settings: Settings) -> crate::Result<Vec<Bundle>> {
 
   for package_type in &package_types {
     let bundle_paths = match package_type {
-      PackageType::MacOsBundle => macos_bundle::bundle_project(&settings)?,
-      PackageType::IosBundle => ios_bundle::bundle_project(&settings)?,
+      #[cfg(target_os = "macos")]
+      PackageType::MacOsBundle => macos::app::bundle_project(&settings)?,
+      #[cfg(target_os = "macos")]
+      PackageType::IosBundle => macos::ios::bundle_project(&settings)?,
       #[cfg(target_os = "windows")]
-      PackageType::WindowsMsi => msi_bundle::bundle_project(&settings)?,
-      PackageType::Deb => deb_bundle::bundle_project(&settings)?,
-      PackageType::Rpm => rpm_bundle::bundle_project(&settings)?,
-      PackageType::AppImage => appimage_bundle::bundle_project(&settings)?,
+      PackageType::WindowsMsi => windows::msi::bundle_project(&settings)?,
+      #[cfg(target_os = "linux")]
+      PackageType::Deb => linux::debian::bundle_project(&settings)?,
+      #[cfg(target_os = "linux")]
+      PackageType::Rpm => linux::rpm::bundle_project(&settings)?,
+      #[cfg(target_os = "linux")]
+      PackageType::AppImage => linux::appimage::bundle_project(&settings)?,
       // dmg is dependant of MacOsBundle, we send our bundles to prevent rebuilding
-      PackageType::Dmg => dmg_bundle::bundle_project(&settings, &bundles)?,
+      #[cfg(target_os = "macos")]
+      PackageType::Dmg => macos::dmg::bundle_project(&settings, &bundles)?,
       // updater is dependant of multiple bundle, we send our bundles to prevent rebuilding
       PackageType::Updater => updater_bundle::bundle_project(&settings, &bundles)?,
+      _ => {
+        print_info(&format!("ignoring {:?}", package_type))?;
+        continue;
+      }
     };
 
     bundles.push(Bundle {

+ 1 - 0
tooling/bundler/src/bundle/category.rs

@@ -13,6 +13,7 @@ const MACOS_APP_CATEGORY_PREFIX: &str = "public.app-category.";
 // that don't fit these; we should add those here too.
 /// The possible app categories.
 /// Corresponds to `LSApplicationCategoryType` on macOS and the GNOME desktop categories on Debian.
+#[allow(missing_docs)]
 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
 pub enum AppCategory {
   Business,

+ 4 - 36
tooling/bundler/src/bundle/common.rs

@@ -37,6 +37,7 @@ pub fn create_file(path: &Path) -> crate::Result<BufWriter<File>> {
 
 /// Makes a symbolic link to a directory.
 #[cfg(unix)]
+#[allow(dead_code)]
 fn symlink_dir(src: &Path, dst: &Path) -> io::Result<()> {
   std::os::unix::fs::symlink(src, dst)
 }
@@ -49,6 +50,7 @@ fn symlink_dir(src: &Path, dst: &Path) -> io::Result<()> {
 
 /// Makes a symbolic link to a file.
 #[cfg(unix)]
+#[allow(dead_code)]
 fn symlink_file(src: &Path, dst: &Path) -> io::Result<()> {
   std::os::unix::fs::symlink(src, dst)
 }
@@ -87,6 +89,7 @@ pub fn copy_file(from: impl AsRef<Path>, to: impl AsRef<Path>) -> crate::Result<
 /// parent directories of the destination path as necessary.  Fails if the
 /// source path is not a directory or doesn't exist, or if the destination path
 /// already exists.
+#[allow(dead_code)]
 pub fn copy_dir(from: &Path, to: &Path) -> crate::Result<()> {
   if !from.exists() {
     return Err(crate::Error::GenericError(format!(
@@ -174,22 +177,6 @@ pub fn print_finished(bundles: &[crate::bundle::Bundle]) -> crate::Result<()> {
   Ok(())
 }
 
-/// Prints a message to stderr, in the same format that `cargo` uses,
-/// indicating that we have finished the the given signatures.
-pub fn print_signed_updater_archive(output_paths: &[PathBuf]) -> crate::Result<()> {
-  let pluralised = if output_paths.len() == 1 {
-    "updater archive"
-  } else {
-    "updater archives"
-  };
-  let msg = format!("{} {} at:", output_paths.len(), pluralised);
-  print_progress("Signed", &msg)?;
-  for path in output_paths {
-    println!("        {}", path.display());
-  }
-  Ok(())
-}
-
 /// Prints a formatted bundle progress to stderr.
 fn print_progress(step: &str, msg: &str) -> crate::Result<()> {
   let mut output = StandardStream::stderr(ColorChoice::Always);
@@ -202,6 +189,7 @@ fn print_progress(step: &str, msg: &str) -> crate::Result<()> {
 }
 
 /// Prints a warning message to stderr, in the same format that `cargo` uses.
+#[allow(dead_code)]
 pub fn print_warning(message: &str) -> crate::Result<()> {
   let mut output = StandardStream::stderr(ColorChoice::Always);
   let _ = output.set_color(ColorSpec::new().set_fg(Some(Color::Yellow)).set_bold(true));
@@ -223,26 +211,6 @@ pub fn print_info(message: &str) -> crate::Result<()> {
   Ok(())
 }
 
-/// Prints an error to stderr, in the same format that `cargo` uses.
-pub fn print_error(error: &anyhow::Error) -> crate::Result<()> {
-  let mut output = StandardStream::stderr(ColorChoice::Always);
-  let _ = output.set_color(ColorSpec::new().set_fg(Some(Color::Red)).set_bold(true));
-  write!(output, "error:")?;
-  output.reset()?;
-  let _ = output.set_color(ColorSpec::new().set_bold(true));
-  writeln!(output, " {}", error)?;
-  output.reset()?;
-  for cause in error.chain().skip(1) {
-    writeln!(output, "  Caused by: {}", cause)?;
-  }
-  // Add Backtrace once its stable.
-  // if let Some(backtrace) = error.backtrace() {
-  //   writeln!(output, "{:?}", backtrace)?;
-  // }
-  output.flush()?;
-  std::process::exit(1)
-}
-
 pub fn execute_with_verbosity(cmd: &mut Command, settings: &Settings) -> crate::Result<()> {
   let stdio_config = if settings.is_verbose() {
     Stdio::piped

+ 6 - 3
tooling/bundler/src/bundle/appimage_bundle.rs → tooling/bundler/src/bundle/linux/appimage.rs

@@ -2,7 +2,10 @@
 // SPDX-License-Identifier: Apache-2.0
 // SPDX-License-Identifier: MIT
 
-use super::{common, deb_bundle, path_utils};
+use super::{
+  super::{common, path_utils},
+  debian,
+};
 use crate::Settings;
 
 use handlebars::Handlebars;
@@ -34,8 +37,8 @@ pub fn bundle_project(settings: &Settings) -> crate::Result<Vec<PathBuf>> {
   let package_dir = settings.project_out_directory().join("bundle/appimage_deb");
 
   // generate deb_folder structure
-  let (_, icons) = deb_bundle::generate_data(settings, &package_dir)?;
-  let icons: Vec<deb_bundle::DebIcon> = icons.into_iter().collect();
+  let (_, icons) = debian::generate_data(settings, &package_dir)?;
+  let icons: Vec<debian::DebIcon> = icons.into_iter().collect();
 
   let output_path = settings.project_out_directory().join("bundle/appimage");
   if output_path.exists() {

+ 1 - 1
tooling/bundler/src/bundle/deb_bundle.rs → tooling/bundler/src/bundle/linux/debian.rs

@@ -22,7 +22,7 @@
 // metadata, as well as generating the md5sums file.  Currently we do not
 // generate postinst or prerm files.
 
-use super::common;
+use super::super::common;
 use crate::Settings;
 
 use anyhow::Context;

+ 3 - 0
tooling/bundler/src/bundle/linux/mod.rs

@@ -0,0 +1,3 @@
+pub mod appimage;
+pub mod debian;
+pub mod rpm;

+ 0 - 0
tooling/bundler/src/bundle/rpm_bundle.rs → tooling/bundler/src/bundle/linux/rpm.rs


+ 0 - 0
tooling/bundler/src/bundle/templates/appimage → tooling/bundler/src/bundle/linux/templates/appimage


+ 4 - 406
tooling/bundler/src/bundle/macos_bundle.rs → tooling/bundler/src/bundle/macos/app.rs

@@ -21,7 +21,10 @@
 // Currently, cargo-bundle does not support Frameworks, nor does it support placing arbitrary
 // files into the `Contents` directory of the bundle.
 
-use super::common;
+use super::{
+  super::common,
+  sign::{notarize, notarize_auth_args, setup_keychain_if_needed, sign},
+};
 use crate::Settings;
 
 use anyhow::Context;
@@ -36,8 +39,6 @@ use std::{
   process::{Command, Stdio},
 };
 
-use regex::Regex;
-
 /// Bundles the project.
 /// Returns a vector of PathBuf that shows where the .app was created.
 pub fn bundle_project(settings: &Settings) -> crate::Result<Vec<PathBuf>> {
@@ -107,409 +108,6 @@ pub fn bundle_project(settings: &Settings) -> crate::Result<Vec<PathBuf>> {
   Ok(vec![app_bundle_path])
 }
 
-pub fn sign(
-  path_to_sign: PathBuf,
-  identity: &str,
-  settings: &Settings,
-  is_an_executable: bool,
-) -> crate::Result<()> {
-  common::print_info(format!(r#"signing with identity "{}""#, identity).as_str())?;
-  let mut args = vec!["--force", "-s", identity];
-  if let Some(entitlements_path) = &settings.macos().entitlements {
-    common::print_info(format!("using entitlements file at {}", entitlements_path).as_str())?;
-    args.push("--entitlements");
-    args.push(entitlements_path);
-  }
-
-  if is_an_executable {
-    args.push("--options");
-    args.push("runtime");
-  }
-
-  if path_to_sign.is_dir() {
-    args.push("--deep");
-  }
-
-  let status = Command::new("codesign")
-    .args(args)
-    .arg(path_to_sign.to_string_lossy().to_string())
-    .status()?;
-
-  if !status.success() {
-    return Err(anyhow::anyhow!("failed to sign app").into());
-  }
-
-  Ok(())
-}
-
-fn notarize(
-  app_bundle_path: PathBuf,
-  auth_args: Vec<String>,
-  settings: &Settings,
-) -> crate::Result<()> {
-  let identifier = settings.bundle_identifier();
-
-  let bundle_stem = app_bundle_path
-    .file_stem()
-    .expect("failed to get bundle filename");
-
-  let tmp_dir = tempfile::tempdir()?;
-  let zip_path = tmp_dir
-    .path()
-    .join(format!("{}.zip", bundle_stem.to_string_lossy()));
-  let zip_args = vec![
-    "-c",
-    "-k",
-    "--keepParent",
-    "--sequesterRsrc",
-    app_bundle_path
-      .to_str()
-      .expect("failed to convert bundle_path to string"),
-    zip_path
-      .to_str()
-      .expect("failed to convert zip_path to string"),
-  ];
-
-  // use ditto to create a PKZip almost identical to Finder
-  // this remove almost 99% of false alarm in notarization
-  let zip_app = Command::new("ditto")
-    .args(zip_args)
-    .stderr(Stdio::inherit())
-    .status()?;
-
-  if !zip_app.success() {
-    return Err(anyhow::anyhow!("failed to zip app with ditto").into());
-  }
-
-  // sign the zip file
-  if let Some(identity) = &settings.macos().signing_identity {
-    sign(zip_path.clone(), identity, &settings, false)?;
-  };
-
-  let notarize_args = vec![
-    "altool",
-    "--notarize-app",
-    "-f",
-    zip_path
-      .to_str()
-      .expect("failed to convert zip_path to string"),
-    "--primary-bundle-id",
-    identifier,
-  ];
-  common::print_info("notarizing app")?;
-  let output = Command::new("xcrun")
-    .args(notarize_args)
-    .args(auth_args.clone())
-    .stderr(Stdio::inherit())
-    .output()?;
-
-  if !output.status.success() {
-    return Err(
-      anyhow::anyhow!(format!(
-        "failed to upload app to Apple's notarization servers. {}",
-        std::str::from_utf8(&output.stdout)?
-      ))
-      .into(),
-    );
-  }
-
-  let stdout = std::str::from_utf8(&output.stdout)?;
-  if let Some(uuid) = Regex::new(r"\nRequestUUID = (.+?)\n")?
-    .captures_iter(stdout)
-    .next()
-  {
-    common::print_info("notarization started; waiting for Apple response...")?;
-    let uuid = uuid[1].to_string();
-    get_notarization_status(uuid, auth_args)?;
-    staple_app(app_bundle_path.clone())?;
-  } else {
-    return Err(
-      anyhow::anyhow!(format!(
-        "failed to parse RequestUUID from upload output. {}",
-        stdout
-      ))
-      .into(),
-    );
-  }
-
-  Ok(())
-}
-
-fn staple_app(mut app_bundle_path: PathBuf) -> crate::Result<()> {
-  let app_bundle_path_clone = app_bundle_path.clone();
-  let filename = app_bundle_path_clone
-    .file_name()
-    .expect("failed to get bundle filename")
-    .to_str()
-    .expect("failed to convert bundle filename to string");
-
-  app_bundle_path.pop();
-
-  let output = Command::new("xcrun")
-    .args(vec!["stapler", "staple", "-v", filename])
-    .current_dir(app_bundle_path)
-    .stderr(Stdio::inherit())
-    .output()?;
-
-  if !output.status.success() {
-    Err(
-      anyhow::anyhow!(format!(
-        "failed to staple app. {}",
-        std::str::from_utf8(&output.stdout)?
-      ))
-      .into(),
-    )
-  } else {
-    Ok(())
-  }
-}
-
-fn get_notarization_status(uuid: String, auth_args: Vec<String>) -> crate::Result<()> {
-  std::thread::sleep(std::time::Duration::from_secs(10));
-  let output = Command::new("xcrun")
-    .args(vec!["altool", "--notarization-info", &uuid])
-    .args(auth_args.clone())
-    .stderr(Stdio::inherit())
-    .output()?;
-
-  if !output.status.success() {
-    get_notarization_status(uuid, auth_args)
-  } else {
-    let stdout = std::str::from_utf8(&output.stdout)?;
-    if let Some(status) = Regex::new(r"\n *Status: (.+?)\n")?
-      .captures_iter(stdout)
-      .next()
-    {
-      let status = status[1].to_string();
-      if status == "in progress" {
-        get_notarization_status(uuid, auth_args)
-      } else if status == "invalid" {
-        Err(
-          anyhow::anyhow!(format!(
-            "Apple failed to notarize your app. {}",
-            std::str::from_utf8(&output.stdout)?
-          ))
-          .into(),
-        )
-      } else if status != "success" {
-        Err(
-          anyhow::anyhow!(format!(
-            "Unknown notarize status {}. {}",
-            status,
-            std::str::from_utf8(&output.stdout)?
-          ))
-          .into(),
-        )
-      } else {
-        Ok(())
-      }
-    } else {
-      get_notarization_status(uuid, auth_args)
-    }
-  }
-}
-
-fn notarize_auth_args() -> crate::Result<Vec<String>> {
-  match (
-    std::env::var_os("APPLE_ID"),
-    std::env::var_os("APPLE_PASSWORD"),
-  ) {
-    (Some(apple_id), Some(apple_password)) => {
-      let apple_id = apple_id
-        .to_str()
-        .expect("failed to convert APPLE_ID to string")
-        .to_string();
-      let apple_password = apple_password
-        .to_str()
-        .expect("failed to convert APPLE_PASSWORD to string")
-        .to_string();
-      Ok(vec![
-        "-u".to_string(),
-        apple_id,
-        "-p".to_string(),
-        apple_password,
-      ])
-    }
-    _ => {
-      match (std::env::var_os("APPLE_API_KEY"), std::env::var_os("APPLE_API_ISSUER")) {
-        (Some(api_key), Some(api_issuer)) => {
-          let api_key = api_key.to_str().expect("failed to convert APPLE_API_KEY to string").to_string();
-          let api_issuer = api_issuer.to_str().expect("failed to convert APPLE_API_ISSUER to string").to_string();
-          Ok(vec!["--apiKey".to_string(), api_key, "--apiIssuer".to_string(), api_issuer])
-        },
-        _ => Err(anyhow::anyhow!("no APPLE_ID & APPLE_PASSWORD or APPLE_API_KEY & APPLE_API_ISSUER environment variables found").into())
-      }
-    }
-  }
-}
-
-// Import certificate from ENV variables.
-// APPLE_CERTIFICATE is the p12 certificate base64 encoded.
-// By example you can use; openssl base64 -in MyCertificate.p12 -out MyCertificate-base64.txt
-// Then use the value of the base64 in APPLE_CERTIFICATE env variable.
-// You need to set APPLE_CERTIFICATE_PASSWORD to the password you set when youy exported your certificate.
-// https://help.apple.com/xcode/mac/current/#/dev154b28f09 see: `Export a signing certificate`
-fn setup_keychain_if_needed() -> crate::Result<()> {
-  match (
-    std::env::var_os("APPLE_CERTIFICATE"),
-    std::env::var_os("APPLE_CERTIFICATE_PASSWORD"),
-  ) {
-    (Some(certificate_encoded), Some(certificate_password)) => {
-      // we delete any previous version of our keychain if present
-      delete_keychain_if_needed();
-      common::print_info("setup keychain from environment variables...")?;
-
-      let key_chain_id = "tauri-build.keychain";
-      let key_chain_name = "tauri-build";
-      let tmp_dir = tempfile::tempdir()?;
-      let cert_path = tmp_dir
-        .path()
-        .join("cert.p12")
-        .to_string_lossy()
-        .to_string();
-      let cert_path_tmp = tmp_dir
-        .path()
-        .join("cert.p12.tmp")
-        .to_string_lossy()
-        .to_string();
-      let certificate_encoded = certificate_encoded
-        .to_str()
-        .expect("failed to convert APPLE_CERTIFICATE to string")
-        .as_bytes();
-
-      let certificate_password = certificate_password
-        .to_str()
-        .expect("failed to convert APPLE_CERTIFICATE_PASSWORD to string")
-        .to_string();
-
-      // as certificate contain whitespace decoding may be broken
-      // https://github.com/marshallpierce/rust-base64/issues/105
-      // we'll use builtin base64 command from the OS
-      let mut tmp_cert = File::create(cert_path_tmp.clone())?;
-      tmp_cert.write_all(certificate_encoded)?;
-
-      let decode_certificate = Command::new("base64")
-        .args(vec!["--decode", "-i", &cert_path_tmp, "-o", &cert_path])
-        .stderr(Stdio::piped())
-        .status()?;
-
-      if !decode_certificate.success() {
-        return Err(anyhow::anyhow!("failed to decode certificate",).into());
-      }
-
-      let create_key_chain = Command::new("security")
-        .args(vec!["create-keychain", "-p", key_chain_name, key_chain_id])
-        .stdout(Stdio::piped())
-        .stderr(Stdio::piped())
-        .status()?;
-
-      if !create_key_chain.success() {
-        return Err(anyhow::anyhow!("failed to create keychain",).into());
-      }
-
-      let set_default_keychain = Command::new("security")
-        .args(vec!["default-keychain", "-s", key_chain_id])
-        .stdout(Stdio::piped())
-        .stderr(Stdio::piped())
-        .status()?;
-
-      if !set_default_keychain.success() {
-        return Err(anyhow::anyhow!("failed to set default keychain",).into());
-      }
-
-      let unlock_keychain = Command::new("security")
-        .args(vec!["unlock-keychain", "-p", key_chain_name, key_chain_id])
-        .stdout(Stdio::piped())
-        .stderr(Stdio::piped())
-        .status()?;
-
-      if !unlock_keychain.success() {
-        return Err(anyhow::anyhow!("failed to set unlock keychain",).into());
-      }
-
-      let import_certificate = Command::new("security")
-        .arg("import")
-        .arg(cert_path)
-        .arg("-k")
-        .arg(key_chain_id)
-        .arg("-P")
-        .arg(certificate_password)
-        .arg("-T")
-        .arg("/usr/bin/codesign")
-        .arg("-T")
-        .arg("/usr/bin/pkgbuild")
-        .arg("-T")
-        .arg("/usr/bin/productbuild")
-        .stderr(Stdio::inherit())
-        .output()?;
-
-      if !import_certificate.status.success() {
-        return Err(
-          anyhow::anyhow!(format!(
-            "failed to import keychain certificate {:?}",
-            std::str::from_utf8(&import_certificate.stdout)
-          ))
-          .into(),
-        );
-      }
-
-      let settings_keychain = Command::new("security")
-        .args(vec![
-          "set-keychain-settings",
-          "-t",
-          "3600",
-          "-u",
-          key_chain_id,
-        ])
-        .stdout(Stdio::piped())
-        .stderr(Stdio::piped())
-        .status()?;
-
-      if !settings_keychain.success() {
-        return Err(anyhow::anyhow!("failed to set keychain settings",).into());
-      }
-
-      let partition_list = Command::new("security")
-        .args(vec![
-          "set-key-partition-list",
-          "-S",
-          "apple-tool:,apple:,codesign:",
-          "-s",
-          "-k",
-          key_chain_name,
-          key_chain_id,
-        ])
-        .stdout(Stdio::piped())
-        .stderr(Stdio::piped())
-        .status()?;
-
-      if !partition_list.success() {
-        return Err(anyhow::anyhow!("failed to set keychain settings",).into());
-      }
-
-      Ok(())
-    }
-    // skip it
-    _ => Ok(()),
-  }
-}
-
-fn delete_keychain_if_needed() {
-  if let (Some(_cert), Some(_password)) = (
-    std::env::var_os("APPLE_CERTIFICATE"),
-    std::env::var_os("APPLE_CERTIFICATE_PASSWORD"),
-  ) {
-    let key_chain_id = "tauri-build.keychain";
-    // delete keychain if needed and skip any error
-    let _result = Command::new("security")
-      .arg("delete-keychain")
-      .arg(key_chain_id)
-      .stdout(Stdio::piped())
-      .stderr(Stdio::piped())
-      .status();
-  }
-}
-
 // Copies the app's binaries to the bundle.
 fn copy_binaries_to_bundle(bundle_directory: &Path, settings: &Settings) -> crate::Result<()> {
   let dest_dir = bundle_directory.join("MacOS");

+ 3 - 3
tooling/bundler/src/bundle/dmg_bundle.rs → tooling/bundler/src/bundle/macos/dmg.rs

@@ -2,7 +2,7 @@
 // SPDX-License-Identifier: Apache-2.0
 // SPDX-License-Identifier: MIT
 
-use super::{common, macos_bundle};
+use super::{super::common, app};
 use crate::{bundle::Bundle, PackageType::MacOsBundle, Settings};
 
 use anyhow::Context;
@@ -24,7 +24,7 @@ pub fn bundle_project(settings: &Settings, bundles: &[Bundle]) -> crate::Result<
     .count()
     == 0
   {
-    macos_bundle::bundle_project(settings)?;
+    app::bundle_project(settings)?;
   }
 
   // get the target path
@@ -145,7 +145,7 @@ pub fn bundle_project(settings: &Settings, bundles: &[Bundle]) -> crate::Result<
 
   // Sign DMG if needed
   if let Some(identity) = &settings.macos().signing_identity {
-    crate::bundle::macos_bundle::sign(dmg_path.clone(), identity, &settings, false)?;
+    super::sign::sign(dmg_path.clone(), identity, &settings, false)?;
   }
   Ok(vec![dmg_path])
 }

+ 1 - 2
tooling/bundler/src/bundle/ios_bundle.rs → tooling/bundler/src/bundle/macos/ios.rs

@@ -12,8 +12,7 @@
 // See https://developer.apple.com/go/?id=bundle-structure for a full
 // explanation.
 
-use super::common;
-use crate::Settings;
+use crate::{bundle::common, Settings};
 
 use anyhow::Context;
 use image::{self, png::PngDecoder, GenericImageView, ImageDecoder};

+ 4 - 0
tooling/bundler/src/bundle/macos/mod.rs

@@ -0,0 +1,4 @@
+pub mod app;
+pub mod dmg;
+pub mod ios;
+pub mod sign;

+ 412 - 0
tooling/bundler/src/bundle/macos/sign.rs

@@ -0,0 +1,412 @@
+use std::{
+  fs::File,
+  io::prelude::*,
+  path::PathBuf,
+  process::{Command, Stdio},
+};
+
+use crate::{bundle::common, Settings};
+use regex::Regex;
+
+// Import certificate from ENV variables.
+// APPLE_CERTIFICATE is the p12 certificate base64 encoded.
+// By example you can use; openssl base64 -in MyCertificate.p12 -out MyCertificate-base64.txt
+// Then use the value of the base64 in APPLE_CERTIFICATE env variable.
+// You need to set APPLE_CERTIFICATE_PASSWORD to the password you set when youy exported your certificate.
+// https://help.apple.com/xcode/mac/current/#/dev154b28f09 see: `Export a signing certificate`
+pub fn setup_keychain_if_needed() -> crate::Result<()> {
+  match (
+    std::env::var_os("APPLE_CERTIFICATE"),
+    std::env::var_os("APPLE_CERTIFICATE_PASSWORD"),
+  ) {
+    (Some(certificate_encoded), Some(certificate_password)) => {
+      // we delete any previous version of our keychain if present
+      delete_keychain_if_needed();
+      common::print_info("setup keychain from environment variables...")?;
+
+      let key_chain_id = "tauri-build.keychain";
+      let key_chain_name = "tauri-build";
+      let tmp_dir = tempfile::tempdir()?;
+      let cert_path = tmp_dir
+        .path()
+        .join("cert.p12")
+        .to_string_lossy()
+        .to_string();
+      let cert_path_tmp = tmp_dir
+        .path()
+        .join("cert.p12.tmp")
+        .to_string_lossy()
+        .to_string();
+      let certificate_encoded = certificate_encoded
+        .to_str()
+        .expect("failed to convert APPLE_CERTIFICATE to string")
+        .as_bytes();
+
+      let certificate_password = certificate_password
+        .to_str()
+        .expect("failed to convert APPLE_CERTIFICATE_PASSWORD to string")
+        .to_string();
+
+      // as certificate contain whitespace decoding may be broken
+      // https://github.com/marshallpierce/rust-base64/issues/105
+      // we'll use builtin base64 command from the OS
+      let mut tmp_cert = File::create(cert_path_tmp.clone())?;
+      tmp_cert.write_all(certificate_encoded)?;
+
+      let decode_certificate = Command::new("base64")
+        .args(vec!["--decode", "-i", &cert_path_tmp, "-o", &cert_path])
+        .stderr(Stdio::piped())
+        .status()?;
+
+      if !decode_certificate.success() {
+        return Err(anyhow::anyhow!("failed to decode certificate",).into());
+      }
+
+      let create_key_chain = Command::new("security")
+        .args(vec!["create-keychain", "-p", key_chain_name, key_chain_id])
+        .stdout(Stdio::piped())
+        .stderr(Stdio::piped())
+        .status()?;
+
+      if !create_key_chain.success() {
+        return Err(anyhow::anyhow!("failed to create keychain",).into());
+      }
+
+      let set_default_keychain = Command::new("security")
+        .args(vec!["default-keychain", "-s", key_chain_id])
+        .stdout(Stdio::piped())
+        .stderr(Stdio::piped())
+        .status()?;
+
+      if !set_default_keychain.success() {
+        return Err(anyhow::anyhow!("failed to set default keychain",).into());
+      }
+
+      let unlock_keychain = Command::new("security")
+        .args(vec!["unlock-keychain", "-p", key_chain_name, key_chain_id])
+        .stdout(Stdio::piped())
+        .stderr(Stdio::piped())
+        .status()?;
+
+      if !unlock_keychain.success() {
+        return Err(anyhow::anyhow!("failed to set unlock keychain",).into());
+      }
+
+      let import_certificate = Command::new("security")
+        .arg("import")
+        .arg(cert_path)
+        .arg("-k")
+        .arg(key_chain_id)
+        .arg("-P")
+        .arg(certificate_password)
+        .arg("-T")
+        .arg("/usr/bin/codesign")
+        .arg("-T")
+        .arg("/usr/bin/pkgbuild")
+        .arg("-T")
+        .arg("/usr/bin/productbuild")
+        .stderr(Stdio::inherit())
+        .output()?;
+
+      if !import_certificate.status.success() {
+        return Err(
+          anyhow::anyhow!(format!(
+            "failed to import keychain certificate {:?}",
+            std::str::from_utf8(&import_certificate.stdout)
+          ))
+          .into(),
+        );
+      }
+
+      let settings_keychain = Command::new("security")
+        .args(vec![
+          "set-keychain-settings",
+          "-t",
+          "3600",
+          "-u",
+          key_chain_id,
+        ])
+        .stdout(Stdio::piped())
+        .stderr(Stdio::piped())
+        .status()?;
+
+      if !settings_keychain.success() {
+        return Err(anyhow::anyhow!("failed to set keychain settings",).into());
+      }
+
+      let partition_list = Command::new("security")
+        .args(vec![
+          "set-key-partition-list",
+          "-S",
+          "apple-tool:,apple:,codesign:",
+          "-s",
+          "-k",
+          key_chain_name,
+          key_chain_id,
+        ])
+        .stdout(Stdio::piped())
+        .stderr(Stdio::piped())
+        .status()?;
+
+      if !partition_list.success() {
+        return Err(anyhow::anyhow!("failed to set keychain settings",).into());
+      }
+
+      Ok(())
+    }
+    // skip it
+    _ => Ok(()),
+  }
+}
+
+pub fn delete_keychain_if_needed() {
+  if let (Some(_cert), Some(_password)) = (
+    std::env::var_os("APPLE_CERTIFICATE"),
+    std::env::var_os("APPLE_CERTIFICATE_PASSWORD"),
+  ) {
+    let key_chain_id = "tauri-build.keychain";
+    // delete keychain if needed and skip any error
+    let _result = Command::new("security")
+      .arg("delete-keychain")
+      .arg(key_chain_id)
+      .stdout(Stdio::piped())
+      .stderr(Stdio::piped())
+      .status();
+  }
+}
+
+pub fn sign(
+  path_to_sign: PathBuf,
+  identity: &str,
+  settings: &Settings,
+  is_an_executable: bool,
+) -> crate::Result<()> {
+  common::print_info(format!(r#"signing with identity "{}""#, identity).as_str())?;
+  let mut args = vec!["--force", "-s", identity];
+  if let Some(entitlements_path) = &settings.macos().entitlements {
+    common::print_info(format!("using entitlements file at {}", entitlements_path).as_str())?;
+    args.push("--entitlements");
+    args.push(entitlements_path);
+  }
+
+  if is_an_executable {
+    args.push("--options");
+    args.push("runtime");
+  }
+
+  if path_to_sign.is_dir() {
+    args.push("--deep");
+  }
+
+  let status = Command::new("codesign")
+    .args(args)
+    .arg(path_to_sign.to_string_lossy().to_string())
+    .status()?;
+
+  if !status.success() {
+    return Err(anyhow::anyhow!("failed to sign app").into());
+  }
+
+  Ok(())
+}
+
+pub fn notarize(
+  app_bundle_path: PathBuf,
+  auth_args: Vec<String>,
+  settings: &Settings,
+) -> crate::Result<()> {
+  let identifier = settings.bundle_identifier();
+
+  let bundle_stem = app_bundle_path
+    .file_stem()
+    .expect("failed to get bundle filename");
+
+  let tmp_dir = tempfile::tempdir()?;
+  let zip_path = tmp_dir
+    .path()
+    .join(format!("{}.zip", bundle_stem.to_string_lossy()));
+  let zip_args = vec![
+    "-c",
+    "-k",
+    "--keepParent",
+    "--sequesterRsrc",
+    app_bundle_path
+      .to_str()
+      .expect("failed to convert bundle_path to string"),
+    zip_path
+      .to_str()
+      .expect("failed to convert zip_path to string"),
+  ];
+
+  // use ditto to create a PKZip almost identical to Finder
+  // this remove almost 99% of false alarm in notarization
+  let zip_app = Command::new("ditto")
+    .args(zip_args)
+    .stderr(Stdio::inherit())
+    .status()?;
+
+  if !zip_app.success() {
+    return Err(anyhow::anyhow!("failed to zip app with ditto").into());
+  }
+
+  // sign the zip file
+  if let Some(identity) = &settings.macos().signing_identity {
+    sign(zip_path.clone(), identity, &settings, false)?;
+  };
+
+  let notarize_args = vec![
+    "altool",
+    "--notarize-app",
+    "-f",
+    zip_path
+      .to_str()
+      .expect("failed to convert zip_path to string"),
+    "--primary-bundle-id",
+    identifier,
+  ];
+  common::print_info("notarizing app")?;
+  let output = Command::new("xcrun")
+    .args(notarize_args)
+    .args(auth_args.clone())
+    .stderr(Stdio::inherit())
+    .output()?;
+
+  if !output.status.success() {
+    return Err(
+      anyhow::anyhow!(format!(
+        "failed to upload app to Apple's notarization servers. {}",
+        std::str::from_utf8(&output.stdout)?
+      ))
+      .into(),
+    );
+  }
+
+  let stdout = std::str::from_utf8(&output.stdout)?;
+  if let Some(uuid) = Regex::new(r"\nRequestUUID = (.+?)\n")?
+    .captures_iter(stdout)
+    .next()
+  {
+    common::print_info("notarization started; waiting for Apple response...")?;
+    let uuid = uuid[1].to_string();
+    get_notarization_status(uuid, auth_args)?;
+    staple_app(app_bundle_path.clone())?;
+  } else {
+    return Err(
+      anyhow::anyhow!(format!(
+        "failed to parse RequestUUID from upload output. {}",
+        stdout
+      ))
+      .into(),
+    );
+  }
+
+  Ok(())
+}
+
+fn staple_app(mut app_bundle_path: PathBuf) -> crate::Result<()> {
+  let app_bundle_path_clone = app_bundle_path.clone();
+  let filename = app_bundle_path_clone
+    .file_name()
+    .expect("failed to get bundle filename")
+    .to_str()
+    .expect("failed to convert bundle filename to string");
+
+  app_bundle_path.pop();
+
+  let output = Command::new("xcrun")
+    .args(vec!["stapler", "staple", "-v", filename])
+    .current_dir(app_bundle_path)
+    .stderr(Stdio::inherit())
+    .output()?;
+
+  if !output.status.success() {
+    Err(
+      anyhow::anyhow!(format!(
+        "failed to staple app. {}",
+        std::str::from_utf8(&output.stdout)?
+      ))
+      .into(),
+    )
+  } else {
+    Ok(())
+  }
+}
+
+fn get_notarization_status(uuid: String, auth_args: Vec<String>) -> crate::Result<()> {
+  std::thread::sleep(std::time::Duration::from_secs(10));
+  let output = Command::new("xcrun")
+    .args(vec!["altool", "--notarization-info", &uuid])
+    .args(auth_args.clone())
+    .stderr(Stdio::inherit())
+    .output()?;
+
+  if !output.status.success() {
+    get_notarization_status(uuid, auth_args)
+  } else {
+    let stdout = std::str::from_utf8(&output.stdout)?;
+    if let Some(status) = Regex::new(r"\n *Status: (.+?)\n")?
+      .captures_iter(stdout)
+      .next()
+    {
+      let status = status[1].to_string();
+      if status == "in progress" {
+        get_notarization_status(uuid, auth_args)
+      } else if status == "invalid" {
+        Err(
+          anyhow::anyhow!(format!(
+            "Apple failed to notarize your app. {}",
+            std::str::from_utf8(&output.stdout)?
+          ))
+          .into(),
+        )
+      } else if status != "success" {
+        Err(
+          anyhow::anyhow!(format!(
+            "Unknown notarize status {}. {}",
+            status,
+            std::str::from_utf8(&output.stdout)?
+          ))
+          .into(),
+        )
+      } else {
+        Ok(())
+      }
+    } else {
+      get_notarization_status(uuid, auth_args)
+    }
+  }
+}
+
+pub fn notarize_auth_args() -> crate::Result<Vec<String>> {
+  match (
+    std::env::var_os("APPLE_ID"),
+    std::env::var_os("APPLE_PASSWORD"),
+  ) {
+    (Some(apple_id), Some(apple_password)) => {
+      let apple_id = apple_id
+        .to_str()
+        .expect("failed to convert APPLE_ID to string")
+        .to_string();
+      let apple_password = apple_password
+        .to_str()
+        .expect("failed to convert APPLE_PASSWORD to string")
+        .to_string();
+      Ok(vec![
+        "-u".to_string(),
+        apple_id,
+        "-p".to_string(),
+        apple_password,
+      ])
+    }
+    _ => {
+      match (std::env::var_os("APPLE_API_KEY"), std::env::var_os("APPLE_API_ISSUER")) {
+        (Some(api_key), Some(api_issuer)) => {
+          let api_key = api_key.to_str().expect("failed to convert APPLE_API_KEY to string").to_string();
+          let api_issuer = api_issuer.to_str().expect("failed to convert APPLE_API_ISSUER to string").to_string();
+          Ok(vec!["--apiKey".to_string(), api_key, "--apiIssuer".to_string(), api_issuer])
+        },
+        _ => Err(anyhow::anyhow!("no APPLE_ID & APPLE_PASSWORD or APPLE_API_KEY & APPLE_API_ISSUER environment variables found").into())
+      }
+    }
+  }
+}

+ 0 - 0
tooling/bundler/src/bundle/templates/dmg/bundle_dmg → tooling/bundler/src/bundle/macos/templates/dmg/bundle_dmg


+ 0 - 0
tooling/bundler/src/bundle/templates/dmg/dmg-license.py → tooling/bundler/src/bundle/macos/templates/dmg/dmg-license.py


+ 0 - 0
tooling/bundler/src/bundle/templates/dmg/template.applescript → tooling/bundler/src/bundle/macos/templates/dmg/template.applescript


+ 40 - 12
tooling/bundler/src/bundle/settings.rs

@@ -18,7 +18,6 @@ pub enum PackageType {
   /// The iOS app bundle.
   IosBundle,
   /// The Windows bundle (.msi).
-  #[cfg(target_os = "windows")]
   WindowsMsi,
   /// The Linux Debian package bundle (.deb).
   Deb,
@@ -40,7 +39,6 @@ impl PackageType {
     match name {
       "deb" => Some(PackageType::Deb),
       "ios" => Some(PackageType::IosBundle),
-      #[cfg(target_os = "windows")]
       "msi" => Some(PackageType::WindowsMsi),
       "app" => Some(PackageType::MacOsBundle),
       "rpm" => Some(PackageType::Rpm),
@@ -57,7 +55,6 @@ impl PackageType {
     match *self {
       PackageType::Deb => "deb",
       PackageType::IosBundle => "ios",
-      #[cfg(target_os = "windows")]
       PackageType::WindowsMsi => "msi",
       PackageType::MacOsBundle => "app",
       PackageType::Rpm => "rpm",
@@ -74,13 +71,19 @@ impl PackageType {
 }
 
 const ALL_PACKAGE_TYPES: &[PackageType] = &[
+  #[cfg(target_os = "linux")]
   PackageType::Deb,
+  #[cfg(target_os = "macos")]
   PackageType::IosBundle,
   #[cfg(target_os = "windows")]
   PackageType::WindowsMsi,
+  #[cfg(target_os = "macos")]
   PackageType::MacOsBundle,
+  #[cfg(target_os = "linux")]
   PackageType::Rpm,
+  #[cfg(target_os = "macos")]
   PackageType::Dmg,
+  #[cfg(target_os = "linux")]
   PackageType::AppImage,
   PackageType::Updater,
 ];
@@ -162,32 +165,44 @@ pub struct MacOsSettings {
   ///
   /// This allows communication to the outside world e.g. a web server you're shipping.
   pub exception_domain: Option<String>,
+  /// Code signing identity.
   pub signing_identity: Option<String>,
+  /// Path to the entitlements.plist file.
   pub entitlements: Option<String>,
 }
 
-#[cfg(windows)]
+/// Settings specific to the WiX implementation.
 #[derive(Clone, Debug, Default)]
 pub struct WixSettings {
   /// By default, the bundler uses an internal template.
   /// This option allows you to define your own wix file.
   pub template: Option<PathBuf>,
+  /// A list of paths to .wxs files with WiX fragments to use.
   pub fragment_paths: Vec<PathBuf>,
+  /// The ComponentGroup element ids you want to reference from the fragments.
   pub component_group_refs: Vec<String>,
+  /// The Component element ids you want to reference from the fragments.
   pub component_refs: Vec<String>,
+  /// The FeatureGroup element ids you want to reference from the fragments.
   pub feature_group_refs: Vec<String>,
+  /// The Feature element ids you want to reference from the fragments.
   pub feature_refs: Vec<String>,
+  /// The Merge element ids you want to reference from the fragments.
   pub merge_refs: Vec<String>,
+  /// Disables the Webview2 runtime installation after app install.
   pub skip_webview_install: bool,
 }
 
 /// The Windows bundle settings.
-#[cfg(windows)]
 #[derive(Clone, Debug, Default)]
 pub struct WindowsSettings {
+  /// The file digest algorithm to use for creating file signatures. Required for code signing. SHA-256 is recommended.
   pub digest_algorithm: Option<String>,
+  /// The SHA1 hash of the signing certificate.
   pub certificate_thumbprint: Option<String>,
+  /// Server to use during timestamping.
   pub timestamp_url: Option<String>,
+  /// WiX configuration.
   pub wix: Option<WixSettings>,
 }
 
@@ -228,13 +243,13 @@ pub struct BundleSettings {
   pub deb: DebianSettings,
   /// MacOS-specific settings.
   pub macos: MacOsSettings,
-  // Updater configuration
+  /// Updater configuration.
   pub updater: Option<UpdaterSettings>,
   /// Windows-specific settings.
-  #[cfg(windows)]
   pub windows: WindowsSettings,
 }
 
+/// A binary to bundle.
 #[derive(Clone, Debug)]
 pub struct BundleBinary {
   name: String,
@@ -243,6 +258,7 @@ pub struct BundleBinary {
 }
 
 impl BundleBinary {
+  /// Creates a new bundle binary.
   pub fn new(name: String, main: bool) -> Self {
     Self {
       name: if cfg!(windows) {
@@ -255,30 +271,35 @@ impl BundleBinary {
     }
   }
 
+  /// Sets the src path of the binary.
   pub fn set_src_path(mut self, src_path: Option<String>) -> Self {
     self.src_path = src_path;
     self
   }
 
+  /// Mark the binary as the main executable.
   pub fn set_main(&mut self, main: bool) {
     self.main = main;
   }
 
+  /// Sets the binary name.
   pub fn set_name(&mut self, name: String) {
     self.name = name;
   }
 
+  /// Returns the binary name.
   pub fn name(&self) -> &str {
     &self.name
   }
 
-  #[cfg(windows)]
+  /// Returns the binary `main` flag.
   pub fn main(&self) -> bool {
     self.main
   }
 
-  pub fn src_path(&self) -> &Option<String> {
-    &self.src_path
+  /// Returns the binary source path.
+  pub fn src_path(&self) -> Option<&String> {
+    self.src_path.as_ref()
   }
 }
 
@@ -301,6 +322,7 @@ pub struct Settings {
   binaries: Vec<BundleBinary>,
 }
 
+/// A builder for [`Settings`].
 #[derive(Default)]
 pub struct SettingsBuilder {
   project_out_directory: Option<PathBuf>,
@@ -312,10 +334,12 @@ pub struct SettingsBuilder {
 }
 
 impl SettingsBuilder {
+  /// Creates the default settings builder.
   pub fn new() -> Self {
     Default::default()
   }
 
+  /// Sets the project output directory. It's used as current working directory.
   pub fn project_out_directory<P: AsRef<Path>>(mut self, path: P) -> Self {
     self
       .project_out_directory
@@ -323,26 +347,31 @@ impl SettingsBuilder {
     self
   }
 
+  /// Enables verbose output.
   pub fn verbose(mut self) -> Self {
     self.verbose = true;
     self
   }
 
+  /// Sets the package types to create.
   pub fn package_types(mut self, package_types: Vec<PackageType>) -> Self {
     self.package_types = Some(package_types);
     self
   }
 
+  /// Sets the package settings.
   pub fn package_settings(mut self, settings: PackageSettings) -> Self {
     self.package_settings.replace(settings);
     self
   }
 
+  /// Sets the bundle settings.
   pub fn bundle_settings(mut self, settings: BundleSettings) -> Self {
     self.bundle_settings = settings;
     self
   }
 
+  /// Sets the binaries to bundle.
   pub fn binaries(mut self, binaries: Vec<BundleBinary>) -> Self {
     self.binaries = binaries;
     self
@@ -398,6 +427,7 @@ impl Settings {
     path
   }
 
+  /// Returns the list of binaries to bundle.
   pub fn binaries(&self) -> &Vec<BundleBinary> {
     &self.binaries
   }
@@ -417,7 +447,6 @@ impl Settings {
       "macos" => vec![PackageType::MacOsBundle, PackageType::Dmg],
       "ios" => vec![PackageType::IosBundle],
       "linux" => vec![PackageType::Deb, PackageType::AppImage],
-      #[cfg(target_os = "windows")]
       "windows" => vec![PackageType::WindowsMsi],
       os => {
         return Err(crate::Error::GenericError(format!(
@@ -578,7 +607,6 @@ impl Settings {
   }
 
   /// Returns the Windows settings.
-  #[cfg(windows)]
   pub fn windows(&self) -> &WindowsSettings {
     &self.bundle_settings.windows
   }

+ 7 - 9
tooling/bundler/src/bundle/updater_bundle.rs

@@ -5,17 +5,15 @@
 use super::common;
 
 #[cfg(target_os = "macos")]
-use super::macos_bundle;
+use super::macos::app;
 
 #[cfg(target_os = "linux")]
-use super::appimage_bundle;
+use super::linux::appimage;
 
 #[cfg(target_os = "windows")]
-use super::msi_bundle;
+use super::windows::msi;
 #[cfg(target_os = "windows")]
-use std::fs::File;
-#[cfg(target_os = "windows")]
-use std::io::prelude::*;
+use std::{fs::File, io::prelude::*};
 #[cfg(target_os = "windows")]
 use zip::write::FileOptions;
 
@@ -56,7 +54,7 @@ fn bundle_update(settings: &Settings, bundles: &[Bundle]) -> crate::Result<Vec<P
         .find(|path| path.extension() == Some(OsStr::new("app")))
     }) {
     Some(path) => vec![path.clone()],
-    None => macos_bundle::bundle_project(settings)?,
+    None => app::bundle_project(settings)?,
   };
 
   // we expect our .app to be on bundle_path[0]
@@ -96,7 +94,7 @@ fn bundle_update(settings: &Settings, bundles: &[Bundle]) -> crate::Result<Vec<P
         .find(|path| path.extension() == Some(OsStr::new("AppImage")))
     }) {
     Some(path) => vec![path.clone()],
-    None => appimage_bundle::bundle_project(settings)?,
+    None => appimage::bundle_project(settings)?,
   };
 
   // we expect our .app to be on bundle[0]
@@ -135,7 +133,7 @@ fn bundle_update(settings: &Settings, bundles: &[Bundle]) -> crate::Result<Vec<P
         .find(|path| path.extension() == Some(OsStr::new("msi")))
     }) {
     Some(path) => vec![path.clone()],
-    None => msi_bundle::bundle_project(settings)?,
+    None => msi::bundle_project(settings)?,
   };
 
   // we expect our .msi to be on bundle_path[0]

+ 2 - 0
tooling/bundler/src/bundle/windows/mod.rs

@@ -0,0 +1,2 @@
+pub mod msi;
+pub mod sign;

+ 3 - 1
tooling/bundler/src/bundle/msi_bundle.rs → tooling/bundler/src/bundle/windows/msi.rs

@@ -2,7 +2,9 @@
 // SPDX-License-Identifier: Apache-2.0
 // SPDX-License-Identifier: MIT
 
-use super::{settings::Settings, wix};
+mod wix;
+
+use crate::Settings;
 
 use std::{self, path::PathBuf};
 

+ 3 - 113
tooling/bundler/src/bundle/wix.rs → tooling/bundler/src/bundle/windows/msi/wix.rs

@@ -2,7 +2,8 @@
 // SPDX-License-Identifier: Apache-2.0
 // SPDX-License-Identifier: MIT
 
-use super::{
+use super::super::sign::{sign, SignParams};
+use crate::bundle::{
   common,
   path_utils::{copy_file, FileOpts},
   settings::Settings,
@@ -23,12 +24,6 @@ use std::{
   process::{Command, Stdio},
 };
 
-use bitness::{self, Bitness};
-use winreg::{
-  enums::{HKEY_LOCAL_MACHINE, KEY_READ, KEY_WOW64_32KEY},
-  RegKey,
-};
-
 // URLS for the WIX toolchain.  Can be used for crossplatform compilation.
 pub const WIX_URL: &str =
   "https://github.com/wixtoolset/wix3/releases/download/wix3112rtm/wix311-binaries.zip";
@@ -94,12 +89,6 @@ struct ResourceDirectory {
   directories: Vec<ResourceDirectory>,
 }
 
-pub struct SignParams {
-  pub digest_algorithm: String,
-  pub certificate_thumbprint: String,
-  pub timestamp_url: Option<String>,
-}
-
 impl ResourceDirectory {
   /// Adds a file to this directory descriptor.
   fn add_file(&mut self, file: ResourceFile) {
@@ -489,7 +478,7 @@ pub fn build_wix_app_installer(
 
   if !has_custom_template {
     handlebars
-      .register_template_string("main.wxs", include_str!("templates/main.wxs"))
+      .register_template_string("main.wxs", include_str!("../templates/main.wxs"))
       .map_err(|e| e.to_string())
       .expect("Failed to setup handlebar template");
   }
@@ -530,105 +519,6 @@ pub fn build_wix_app_installer(
   Ok(target)
 }
 
-// sign code forked from https://github.com/forbjok/rust-codesign
-fn locate_signtool() -> crate::Result<PathBuf> {
-  const INSTALLED_ROOTS_REGKEY_PATH: &str = r"SOFTWARE\Microsoft\Windows Kits\Installed Roots";
-  const KITS_ROOT_REGVALUE_NAME: &str = r"KitsRoot10";
-
-  let installed_roots_key_path = Path::new(INSTALLED_ROOTS_REGKEY_PATH);
-
-  // Open 32-bit HKLM "Installed Roots" key
-  let installed_roots_key = RegKey::predef(HKEY_LOCAL_MACHINE)
-    .open_subkey_with_flags(installed_roots_key_path, KEY_READ | KEY_WOW64_32KEY)
-    .map_err(|_| crate::Error::OpenRegistry(INSTALLED_ROOTS_REGKEY_PATH.to_string()))?;
-
-  // Get the Windows SDK root path
-  let kits_root_10_path: String = installed_roots_key
-    .get_value(KITS_ROOT_REGVALUE_NAME)
-    .map_err(|_| crate::Error::GetRegistryValue(KITS_ROOT_REGVALUE_NAME.to_string()))?;
-
-  // Construct Windows SDK bin path
-  let kits_root_10_bin_path = Path::new(&kits_root_10_path).join("bin");
-
-  let mut installed_kits: Vec<String> = installed_roots_key
-    .enum_keys()
-    /* Report and ignore errors, pass on values. */
-    .filter_map(|res| match res {
-      Ok(v) => Some(v),
-      Err(_) => None,
-    })
-    .collect();
-
-  // Sort installed kits
-  installed_kits.sort();
-
-  /* Iterate through installed kit version keys in reverse (from newest to oldest),
-  adding their bin paths to the list.
-  Windows SDK 10 v10.0.15063.468 and later will have their signtools located there. */
-  let mut kit_bin_paths: Vec<PathBuf> = installed_kits
-    .iter()
-    .rev()
-    .map(|kit| kits_root_10_bin_path.join(kit))
-    .collect();
-
-  /* Add kits root bin path.
-  For Windows SDK 10 versions earlier than v10.0.15063.468, signtool will be located there. */
-  kit_bin_paths.push(kits_root_10_bin_path);
-
-  // Choose which version of SignTool to use based on OS bitness
-  let arch_dir = match bitness::os_bitness().expect("failed to get os bitness") {
-    Bitness::X86_32 => "x86",
-    Bitness::X86_64 => "x64",
-    _ => return Err(crate::Error::UnsupportedBitness),
-  };
-
-  /* Iterate through all bin paths, checking for existence of a SignTool executable. */
-  for kit_bin_path in &kit_bin_paths {
-    /* Construct SignTool path. */
-    let signtool_path = kit_bin_path.join(arch_dir).join("signtool.exe");
-
-    /* Check if SignTool exists at this location. */
-    if signtool_path.exists() {
-      // SignTool found. Return it.
-      return Ok(signtool_path);
-    }
-  }
-
-  Err(crate::Error::SignToolNotFound)
-}
-
-fn sign<P: AsRef<Path>>(path: P, params: &SignParams) -> crate::Result<()> {
-  // Convert path to string reference, as we need to pass it as a commandline parameter to signtool
-  let path_str = path.as_ref().to_str().unwrap();
-
-  // Construct SignTool command
-  let signtool = locate_signtool()?;
-  common::print_info(format!("running signtool {:?}", signtool).as_str())?;
-  let mut cmd = Command::new(signtool);
-  cmd.arg("sign");
-  cmd.args(&["/fd", &params.digest_algorithm]);
-  cmd.args(&["/sha1", &params.certificate_thumbprint]);
-
-  if let Some(ref timestamp_url) = params.timestamp_url {
-    cmd.args(&["/t", timestamp_url]);
-  }
-
-  cmd.arg(path_str);
-
-  // Execute SignTool command
-  let output = cmd.output()?;
-
-  if !output.status.success() {
-    let stderr = String::from_utf8_lossy(output.stderr.as_slice()).into_owned();
-    return Err(crate::Error::Sign(stderr));
-  }
-
-  let stdout = String::from_utf8_lossy(output.stdout.as_slice()).into_owned();
-  common::print_info(format!("{:?}", stdout).as_str())?;
-
-  Ok(())
-}
-
 /// Generates the data required for the external binaries and extra binaries bundling.
 fn generate_binaries_data(settings: &Settings) -> crate::Result<Vec<Binary>> {
   let mut binaries = Vec::new();

+ 117 - 0
tooling/bundler/src/bundle/windows/sign.rs

@@ -0,0 +1,117 @@
+use std::{
+  path::{Path, PathBuf},
+  process::Command,
+};
+
+use crate::bundle::common;
+
+use bitness::{self, Bitness};
+use winreg::{
+  enums::{HKEY_LOCAL_MACHINE, KEY_READ, KEY_WOW64_32KEY},
+  RegKey,
+};
+
+pub struct SignParams {
+  pub digest_algorithm: String,
+  pub certificate_thumbprint: String,
+  pub timestamp_url: Option<String>,
+}
+
+// sign code forked from https://github.com/forbjok/rust-codesign
+fn locate_signtool() -> crate::Result<PathBuf> {
+  const INSTALLED_ROOTS_REGKEY_PATH: &str = r"SOFTWARE\Microsoft\Windows Kits\Installed Roots";
+  const KITS_ROOT_REGVALUE_NAME: &str = r"KitsRoot10";
+
+  let installed_roots_key_path = Path::new(INSTALLED_ROOTS_REGKEY_PATH);
+
+  // Open 32-bit HKLM "Installed Roots" key
+  let installed_roots_key = RegKey::predef(HKEY_LOCAL_MACHINE)
+    .open_subkey_with_flags(installed_roots_key_path, KEY_READ | KEY_WOW64_32KEY)
+    .map_err(|_| crate::Error::OpenRegistry(INSTALLED_ROOTS_REGKEY_PATH.to_string()))?;
+
+  // Get the Windows SDK root path
+  let kits_root_10_path: String = installed_roots_key
+    .get_value(KITS_ROOT_REGVALUE_NAME)
+    .map_err(|_| crate::Error::GetRegistryValue(KITS_ROOT_REGVALUE_NAME.to_string()))?;
+
+  // Construct Windows SDK bin path
+  let kits_root_10_bin_path = Path::new(&kits_root_10_path).join("bin");
+
+  let mut installed_kits: Vec<String> = installed_roots_key
+    .enum_keys()
+    /* Report and ignore errors, pass on values. */
+    .filter_map(|res| match res {
+      Ok(v) => Some(v),
+      Err(_) => None,
+    })
+    .collect();
+
+  // Sort installed kits
+  installed_kits.sort();
+
+  /* Iterate through installed kit version keys in reverse (from newest to oldest),
+  adding their bin paths to the list.
+  Windows SDK 10 v10.0.15063.468 and later will have their signtools located there. */
+  let mut kit_bin_paths: Vec<PathBuf> = installed_kits
+    .iter()
+    .rev()
+    .map(|kit| kits_root_10_bin_path.join(kit))
+    .collect();
+
+  /* Add kits root bin path.
+  For Windows SDK 10 versions earlier than v10.0.15063.468, signtool will be located there. */
+  kit_bin_paths.push(kits_root_10_bin_path);
+
+  // Choose which version of SignTool to use based on OS bitness
+  let arch_dir = match bitness::os_bitness().expect("failed to get os bitness") {
+    Bitness::X86_32 => "x86",
+    Bitness::X86_64 => "x64",
+    _ => return Err(crate::Error::UnsupportedBitness),
+  };
+
+  /* Iterate through all bin paths, checking for existence of a SignTool executable. */
+  for kit_bin_path in &kit_bin_paths {
+    /* Construct SignTool path. */
+    let signtool_path = kit_bin_path.join(arch_dir).join("signtool.exe");
+
+    /* Check if SignTool exists at this location. */
+    if signtool_path.exists() {
+      // SignTool found. Return it.
+      return Ok(signtool_path);
+    }
+  }
+
+  Err(crate::Error::SignToolNotFound)
+}
+
+pub fn sign<P: AsRef<Path>>(path: P, params: &SignParams) -> crate::Result<()> {
+  // Convert path to string reference, as we need to pass it as a commandline parameter to signtool
+  let path_str = path.as_ref().to_str().unwrap();
+
+  // Construct SignTool command
+  let signtool = locate_signtool()?;
+  common::print_info(format!("running signtool {:?}", signtool).as_str())?;
+  let mut cmd = Command::new(signtool);
+  cmd.arg("sign");
+  cmd.args(&["/fd", &params.digest_algorithm]);
+  cmd.args(&["/sha1", &params.certificate_thumbprint]);
+
+  if let Some(ref timestamp_url) = params.timestamp_url {
+    cmd.args(&["/t", timestamp_url]);
+  }
+
+  cmd.arg(path_str);
+
+  // Execute SignTool command
+  let output = cmd.output()?;
+
+  if !output.status.success() {
+    let stderr = String::from_utf8_lossy(output.stderr.as_slice()).into_owned();
+    return Err(crate::Error::Sign(stderr));
+  }
+
+  let stdout = String::from_utf8_lossy(output.stdout.as_slice()).into_owned();
+  common::print_info(format!("{:?}", stdout).as_str())?;
+
+  Ok(())
+}

+ 0 - 0
tooling/bundler/src/bundle/templates/main.wxs → tooling/bundler/src/bundle/windows/templates/main.wxs


+ 29 - 10
tooling/bundler/src/error.rs

@@ -5,77 +5,96 @@
 use std::{io, num, path};
 use thiserror::Error as DeriveError;
 
+/// Errors returned by the bundler.
 #[derive(Debug, DeriveError)]
 pub enum Error {
+  /// Bundler error.
   #[error("{0}")]
   BundlerError(#[from] anyhow::Error),
+  /// Failed to use glob pattern.
   #[error("`{0}`")]
   GlobError(#[from] glob::GlobError),
+  /// Invalid glob pattern.
   #[error("`{0}`")]
   GlobPatternError(#[from] glob::PatternError),
+  /// I/O error.
   #[error("`{0}`")]
   IoError(#[from] io::Error),
+  /// Image error.
   #[error("`{0}`")]
   ImageError(#[from] image::ImageError),
+  /// TOML error.
   #[error("`{0}`")]
   TomlError(#[from] toml::de::Error),
+  /// Error walking directory.
   #[error("`{0}`")]
   WalkdirError(#[from] walkdir::Error),
+  /// Strip prefix error.
   #[error("`{0}`")]
   StripError(#[from] path::StripPrefixError),
+  /// Number parse error.
   #[error("`{0}`")]
   ConvertError(#[from] num::TryFromIntError),
+  /// Zip error.
   #[error("`{0}`")]
   ZipError(#[from] zip::result::ZipError),
-  #[cfg(not(target_os = "linux"))]
+  /// Hex error.
+  #[cfg(target_os = "windows")]
   #[error("`{0}`")]
   HexError(#[from] hex::FromHexError),
+  /// Handlebars template error.
   #[error("`{0}`")]
   HandleBarsError(#[from] handlebars::RenderError),
+  /// JSON error.
   #[error("`{0}`")]
   JsonError(#[from] serde_json::error::Error),
+  /// Regex error.
   #[error("`{0}`")]
   RegexError(#[from] regex::Error),
+  /// Failed to perform HTTP request.
   #[cfg(windows)]
   #[error("`{0}`")]
   HttpError(#[from] attohttpc::Error),
+  /// Failed to validate downloaded file hash.
   #[error("hash mismatch of downloaded file")]
   HashError,
+  /// Unsupported architecture.
   #[error("Architecture Error: `{0}`")]
   ArchError(String),
-  #[error(
-    "Couldn't get tauri config; please specify the TAURI_CONFIG or TAURI_DIR environment variables"
-  )]
-  EnvironmentError,
+  /// Couldn't find icons.
   #[error("Could not find Icon paths.  Please make sure they exist in the tauri config JSON file")]
   IconPathError,
+  /// Error on path util operation.
   #[error("Path Error:`{0}`")]
   PathUtilError(String),
+  /// Error on shell script.
   #[error("Shell Scripting Error:`{0}`")]
   ShellScriptError(String),
+  /// Generic error.
   #[error("`{0}`")]
   GenericError(String),
   /// No bundled project found for the updater.
   #[error("Unable to find a bundled project for the updater")]
   UnableToFindProject,
+  /// String is not UTF-8.
   #[error("string is not UTF-8")]
   Utf8(#[from] std::str::Utf8Error),
   /// Windows SignTool not found.
-  #[cfg(target_os = "windows")]
   #[error("SignTool not found")]
   SignToolNotFound,
-  #[cfg(target_os = "windows")]
+  /// Failed to open Windows registry.
   #[error("failed to open registry {0}")]
   OpenRegistry(String),
-  #[cfg(target_os = "windows")]
+  /// Failed to get registry value.
   #[error("failed to get {0} value on registry")]
   GetRegistryValue(String),
-  #[cfg(target_os = "windows")]
+  /// Unsupported OS bitness.
   #[error("unsupported OS bitness")]
   UnsupportedBitness,
-  #[cfg(target_os = "windows")]
+  /// Failed to sign application.
   #[error("failed to sign app: {0}")]
   Sign(String),
 }
 
+/// Convenient type alias of Result type.
 pub type Result<T> = anyhow::Result<T, Error>;

+ 14 - 0
tooling/bundler/src/lib.rs

@@ -2,6 +2,20 @@
 // SPDX-License-Identifier: Apache-2.0
 // SPDX-License-Identifier: MIT
 
+#![warn(missing_docs, rust_2018_idioms)]
+
+//! The Tauri bundler is a tool that generates installers or app bundles for executables.
+//! It supports auto updating through [tauri](https://docs.rs/tauri).
+//!
+//! # Platform support
+//! - macOS
+//!   - DMG and App bundles
+//! - Linux
+//!   - Appimage and Debian packages
+//! - Windows
+//!   - MSI using WiX
+
+/// The bundle API.
 pub mod bundle;
 mod error;
 pub use bundle::*;

+ 16 - 3
tooling/cli.rs/src/build.rs

@@ -2,9 +2,7 @@
 // SPDX-License-Identifier: Apache-2.0
 // SPDX-License-Identifier: MIT
 
-use tauri_bundler::bundle::{
-  bundle_project, common::print_signed_updater_archive, PackageType, SettingsBuilder,
-};
+use tauri_bundler::bundle::{bundle_project, PackageType, SettingsBuilder};
 
 use crate::helpers::{
   app_paths::{app_dir, tauri_dir},
@@ -189,3 +187,18 @@ impl Build {
     Ok(())
   }
 }
+
+fn print_signed_updater_archive(output_paths: &[PathBuf]) -> crate::Result<()> {
+  let pluralised = if output_paths.len() == 1 {
+    "updater archive"
+  } else {
+    "updater archives"
+  };
+  let msg = format!("{} {} at:", output_paths.len(), pluralised);
+  let logger = Logger::new("Signed");
+  logger.log(&msg);
+  for path in output_paths {
+    println!("        {}", path.display());
+  }
+  Ok(())
+}

+ 1 - 1
tooling/cli.rs/src/build/rust.rs

@@ -201,7 +201,7 @@ impl AppSettings {
         let path = entry?.path();
         if let Some(name) = path.file_stem() {
           let bin_exists = binaries.iter().any(|bin| {
-            bin.name() == name || path.ends_with(bin.src_path().as_ref().unwrap_or(&"".to_string()))
+            bin.name() == name || path.ends_with(bin.src_path().unwrap_or(&"".to_string()))
           });
           if !bin_exists {
             binaries.push(BundleBinary::new(name.to_string_lossy().to_string(), false))

+ 9 - 2
tooling/cli.rs/src/helpers/updater_signature.rs

@@ -9,12 +9,11 @@ use minisign::{sign, KeyPair as KP, SecretKeyBox};
 use std::{
   env::var_os,
   fs::{self, File, OpenOptions},
-  io::{BufReader, Write},
+  io::{BufReader, BufWriter, Write},
   path::{Path, PathBuf},
   str,
   time::{SystemTime, UNIX_EPOCH},
 };
-use tauri_bundler::bundle::common::create_file;
 
 /// A key pair (`PublicKey` and `SecretKey`).
 #[derive(Clone, Debug)]
@@ -23,6 +22,14 @@ pub struct KeyPair {
   pub sk: String,
 }
 
+fn create_file(path: &Path) -> crate::Result<BufWriter<File>> {
+  if let Some(parent) = path.parent() {
+    fs::create_dir_all(&parent)?;
+  }
+  let file = File::create(path)?;
+  Ok(BufWriter::new(file))
+}
+
 /// Generate base64 encoded keypair
 pub fn generate_key(password: Option<String>) -> crate::Result<KeyPair> {
   let KP { pk, sk } = KP::generate_encrypted_keypair(password).unwrap();