Browse Source

feat(bundler): Add support for creating NSIS bundles on unix hosts (#5788)

Co-authored-by: Lucas Nogueira <lucas@tauri.studio>
Fabian-Lars 2 years ago
parent
commit
60e6f6c3f1

+ 9 - 0
.changes/nsis-linux.md

@@ -0,0 +1,9 @@
+---
+"tauri-bundler": minor
+"tauri-utils": minor
+"cli.rs": minor
+"cli.js": minor
+"tauri-build": minor
+---
+
+Add initial support for building `nsis` bundles on non-Windows platforms.

+ 2 - 2
core/tauri-build/Cargo.toml

@@ -26,8 +26,8 @@ serde_json = "1"
 heck = "0.4"
 json-patch = "0.3"
 
-[target."cfg(windows)".dependencies]
-winres = "0.1"
+# dependencies for Windows targets
+tauri-winres = "0.1"
 semver = "1"
 
 [features]

+ 10 - 11
core/tauri-build/src/lib.rs

@@ -13,7 +13,6 @@ use std::path::{Path, PathBuf};
 
 #[cfg(feature = "codegen")]
 mod codegen;
-#[cfg(windows)]
 mod static_vcruntime;
 
 #[cfg(feature = "codegen")]
@@ -344,25 +343,25 @@ pub fn try_build(attributes: Attributes) -> Result<()> {
 
   #[allow(unused_mut, clippy::redundant_clone)]
   let mut resources = config.tauri.bundle.resources.clone().unwrap_or_default();
-  #[cfg(windows)]
-  if let Some(fixed_webview2_runtime_path) = &config.tauri.bundle.windows.webview_fixed_runtime_path
-  {
-    resources.push(fixed_webview2_runtime_path.display().to_string());
+  if target_triple.contains("windows") {
+    if let Some(fixed_webview2_runtime_path) =
+      &config.tauri.bundle.windows.webview_fixed_runtime_path
+    {
+      resources.push(fixed_webview2_runtime_path.display().to_string());
+    }
   }
   copy_resources(ResourcePaths::new(resources.as_slice(), true), target_dir)?;
 
-  #[cfg(target_os = "macos")]
-  {
-    if let Some(version) = config.tauri.bundle.macos.minimum_system_version {
+  if target_triple.contains("darwin") {
+    if let Some(version) = &config.tauri.bundle.macos.minimum_system_version {
       println!("cargo:rustc-env=MACOSX_DEPLOYMENT_TARGET={}", version);
     }
   }
 
-  #[cfg(windows)]
-  {
+  if target_triple.contains("windows") {
     use anyhow::Context;
     use semver::Version;
-    use winres::{VersionInfo, WindowsResource};
+    use tauri_winres::{VersionInfo, WindowsResource};
 
     fn find_icon<F: Fn(&&String) -> bool>(config: &Config, predicate: F, default: &str) -> PathBuf {
       let icon_path = config

+ 1 - 1
core/tauri-build/src/static_vcruntime.rs

@@ -48,7 +48,7 @@ fn override_msvcrt_lib() {
   let f = fs::OpenOptions::new()
     .write(true)
     .create_new(true)
-    .open(&path);
+    .open(path);
   if let Ok(mut f) = f {
     f.write_all(machine).unwrap();
     f.write_all(bytes).unwrap();

+ 11 - 10
examples/api/src-tauri/Cargo.lock

@@ -3191,7 +3191,7 @@ dependencies = [
  "serde_json",
  "tauri-codegen",
  "tauri-utils",
- "winres",
+ "tauri-winres",
 ]
 
 [[package]]
@@ -3296,6 +3296,16 @@ dependencies = [
  "windows 0.39.0",
 ]
 
+[[package]]
+name = "tauri-winres"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1b7a78dc04f75fb5ab815e66ac561c81e92a968a40f29e7c21afd152d694fad8"
+dependencies = [
+ "toml",
+ "version_check",
+]
+
 [[package]]
 name = "tauri-winrt-notification"
 version = "0.1.0"
@@ -4172,15 +4182,6 @@ dependencies = [
  "winapi",
 ]
 
-[[package]]
-name = "winres"
-version = "0.1.12"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b68db261ef59e9e52806f688020631e987592bd83619edccda9c47d42cde4f6c"
-dependencies = [
- "toml",
-]
-
 [[package]]
 name = "wry"
 version = "0.24.1"

+ 12 - 7
tooling/bundler/Cargo.toml

@@ -33,18 +33,23 @@ tempfile = "3.3.0"
 log = { version = "0.4.17", features = [ "kv_unstable" ] }
 dirs-next = "2.0"
 encoding_rs = "0.8"
+os_pipe = "1"
 
-[target."cfg(target_os = \"windows\")".dependencies]
+# dependencies for Windows targets
 attohttpc = "0.24"
+hex = "0.4"
+semver = "1"
+sha1 = "0.10"
+sha2 = "0.10"
+zip = "0.6"
+
+# dependencies for code signing on Windows hosts
+[target."cfg(target_os = \"windows\")".dependencies]
 uuid = { version = "1", features = [ "v4", "v5" ] }
 bitness = "0.4"
-winreg = "0.10"
-sha2 = "0.10"
-sha1 = "0.10"
-hex = "0.4"
+winreg = "0.10" # Can only be compiled for Windows hosts
 glob = "0.3"
-zip = "0.6"
-semver = "1"
+
 
 [target."cfg(target_os = \"macos\")".dependencies]
 icns = { package = "tauri-icns", version = "0.1" }

+ 17 - 5
tooling/bundler/src/bundle.rs

@@ -13,7 +13,6 @@ mod path_utils;
 mod platform;
 mod settings;
 mod updater_bundle;
-#[cfg(target_os = "windows")]
 mod windows;
 
 pub use self::{
@@ -43,25 +42,38 @@ pub fn bundle_project(settings: Settings) -> crate::Result<Vec<Bundle>> {
   let mut bundles = Vec::new();
   let package_types = settings.package_types()?;
 
+  let target_os = settings
+    .target()
+    .split('-')
+    .nth(2)
+    .unwrap_or(std::env::consts::OS)
+    .replace("darwin", "macos");
+
+  if target_os != std::env::consts::OS {
+    warn!("Cross-platform compilation is experimental and does not support all features. Please use a matching host system for full compatibility.");
+  }
+
   for package_type in &package_types {
     let bundle_paths = match package_type {
       #[cfg(target_os = "macos")]
       PackageType::MacOsBundle => macos::app::bundle_project(&settings)?,
       #[cfg(target_os = "macos")]
       PackageType::IosBundle => macos::ios::bundle_project(&settings)?,
+      // dmg is dependant of MacOsBundle, we send our bundles to prevent rebuilding
+      #[cfg(target_os = "macos")]
+      PackageType::Dmg => macos::dmg::bundle_project(&settings, &bundles)?,
+
       #[cfg(target_os = "windows")]
       PackageType::WindowsMsi => windows::msi::bundle_project(&settings, false)?,
-      #[cfg(target_os = "windows")]
       PackageType::Nsis => windows::nsis::bundle_project(&settings, false)?,
+
       #[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
-      #[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)?,
       _ => {

+ 13 - 1
tooling/bundler/src/bundle/common.rs

@@ -10,7 +10,7 @@ use std::{
   fs::{self, File},
   io::{self, BufReader, BufWriter},
   path::Path,
-  process::{Command, Output, Stdio},
+  process::{Command, ExitStatus, Output, Stdio},
   sync::{Arc, Mutex},
 };
 
@@ -136,10 +136,22 @@ pub fn copy_dir(from: &Path, to: &Path) -> crate::Result<()> {
 }
 
 pub trait CommandExt {
+  // The `pipe` function sets the stdout and stderr to properly
+  // show the command output in the Node.js wrapper.
+  fn piped(&mut self) -> std::io::Result<ExitStatus>;
   fn output_ok(&mut self) -> crate::Result<Output>;
 }
 
 impl CommandExt for Command {
+  fn piped(&mut self) -> std::io::Result<ExitStatus> {
+    self.stdout(os_pipe::dup_stdout()?);
+    self.stderr(os_pipe::dup_stderr()?);
+    let program = self.get_program().to_string_lossy().into_owned();
+    debug!(action = "Running"; "Command `{} {}`", program, self.get_args().map(|arg| arg.to_string_lossy()).fold(String::new(), |acc, arg| format!("{acc} {arg}")));
+
+    self.status().map_err(Into::into)
+  }
+
   fn output_ok(&mut self) -> crate::Result<Output> {
     let program = self.get_program().to_string_lossy().into_owned();
     debug!(action = "Running"; "Command `{} {}`", program, self.get_args().map(|arg| arg.to_string_lossy()).fold(String::new(), |acc, arg| format!("{} {}", acc, arg)));

+ 29 - 2
tooling/bundler/src/bundle/settings.rs

@@ -434,6 +434,8 @@ impl BundleBinary {
 /// The Settings exposed by the module.
 #[derive(Clone, Debug)]
 pub struct Settings {
+  /// The log level.
+  log_level: log::Level,
   /// the package settings.
   package: PackageSettings,
   /// the package types we're bundling.
@@ -453,6 +455,7 @@ pub struct Settings {
 /// A builder for [`Settings`].
 #[derive(Default)]
 pub struct SettingsBuilder {
+  log_level: Option<log::Level>,
   project_out_directory: Option<PathBuf>,
   package_types: Option<Vec<PackageType>>,
   package_settings: Option<PackageSettings>,
@@ -511,6 +514,13 @@ impl SettingsBuilder {
     self
   }
 
+  /// Sets the log level for spawned commands. Defaults to [`log::Level::Error`].
+  #[must_use]
+  pub fn log_level(mut self, level: log::Level) -> Self {
+    self.log_level.replace(level);
+    self
+  }
+
   /// Builds a Settings from the CLI args.
   ///
   /// Package settings will be read from Cargo.toml.
@@ -524,6 +534,7 @@ impl SettingsBuilder {
     };
 
     Ok(Settings {
+      log_level: self.log_level.unwrap_or(log::Level::Error),
       package: self.package_settings.expect("package settings is required"),
       package_types: self.package_types,
       project_out_directory: self
@@ -544,6 +555,16 @@ impl SettingsBuilder {
 }
 
 impl Settings {
+  /// Sets the log level for spawned commands.
+  pub fn set_log_level(&mut self, level: log::Level) {
+    self.log_level = level;
+  }
+
+  /// Returns the log level for spawned commands.
+  pub fn log_level(&self) -> log::Level {
+    self.log_level
+  }
+
   /// Returns the directory where the bundle should be placed.
   pub fn project_out_directory(&self) -> &Path {
     &self.project_out_directory
@@ -604,8 +625,14 @@ impl Settings {
   ///
   /// Fails if the host/target's native package type is not supported.
   pub fn package_types(&self) -> crate::Result<Vec<PackageType>> {
-    let target_os = std::env::consts::OS;
-    let mut platform_types = match target_os {
+    let target_os = self
+      .target
+      .split('-')
+      .nth(2)
+      .unwrap_or(std::env::consts::OS)
+      .replace("darwin", "macos");
+
+    let mut platform_types = match target_os.as_str() {
       "macos" => vec![PackageType::MacOsBundle, PackageType::Dmg],
       "ios" => vec![PackageType::IosBundle],
       "linux" => vec![PackageType::Deb, PackageType::AppImage],

+ 48 - 27
tooling/bundler/src/bundle/updater_bundle.rs

@@ -11,26 +11,48 @@ use super::macos::app;
 #[cfg(target_os = "linux")]
 use super::linux::appimage;
 
-use log::error;
-#[cfg(target_os = "windows")]
-use std::{fs::File, io::prelude::*};
-#[cfg(target_os = "windows")]
-use zip::write::FileOptions;
+use crate::{
+  bundle::{
+    windows::{
+      NSIS_OUTPUT_FOLDER_NAME, NSIS_UPDATER_OUTPUT_FOLDER_NAME, WIX_OUTPUT_FOLDER_NAME,
+      WIX_UPDATER_OUTPUT_FOLDER_NAME,
+    },
+    Bundle,
+  },
+  Settings,
+};
+
+use std::{
+  fs::{self, File},
+  io::{prelude::*, Write},
+  path::{Path, PathBuf},
+};
 
-use crate::{bundle::Bundle, Settings};
 use anyhow::Context;
 use log::info;
-use std::path::{Path, PathBuf};
-use std::{fs, io::Write};
+use zip::write::FileOptions;
 
 // Build update
 pub fn bundle_project(settings: &Settings, bundles: &[Bundle]) -> crate::Result<Vec<PathBuf>> {
-  if cfg!(unix) || cfg!(windows) || cfg!(macos) {
-    // Create our archive bundle
-    let bundle_result = bundle_update(settings, bundles)?;
-    Ok(bundle_result)
-  } else {
-    error!("Current platform do not support updates");
+  let target_os = settings
+    .target()
+    .split('-')
+    .nth(2)
+    .unwrap_or(std::env::consts::OS)
+    .replace("darwin", "macos");
+
+  if target_os == "windows" {
+    return bundle_update_windows(settings, bundles);
+  }
+
+  #[cfg(target_os = "macos")]
+  return bundle_update_macos(settings, bundles);
+  #[cfg(target_os = "linux")]
+  return bundle_update_linux(settings, bundles);
+
+  #[cfg(not(any(target_os = "macos", target_os = "linux")))]
+  {
+    log::error!("Current platform does not support updates");
     Ok(vec![])
   }
 }
@@ -38,7 +60,7 @@ pub fn bundle_project(settings: &Settings, bundles: &[Bundle]) -> crate::Result<
 // Create simple update-macos.tar.gz
 // This is the Mac OS App packaged
 #[cfg(target_os = "macos")]
-fn bundle_update(settings: &Settings, bundles: &[Bundle]) -> crate::Result<Vec<PathBuf>> {
+fn bundle_update_macos(settings: &Settings, bundles: &[Bundle]) -> crate::Result<Vec<PathBuf>> {
   use std::ffi::OsStr;
 
   // find our .app or rebuild our bundle
@@ -81,7 +103,7 @@ fn bundle_update(settings: &Settings, bundles: &[Bundle]) -> crate::Result<Vec<P
 // Right now in linux we hot replace the bin and request a restart
 // No assets are replaced
 #[cfg(target_os = "linux")]
-fn bundle_update(settings: &Settings, bundles: &[Bundle]) -> crate::Result<Vec<PathBuf>> {
+fn bundle_update_linux(settings: &Settings, bundles: &[Bundle]) -> crate::Result<Vec<PathBuf>> {
   use std::ffi::OsStr;
 
   // build our app actually we support only appimage on linux
@@ -122,10 +144,11 @@ fn bundle_update(settings: &Settings, bundles: &[Bundle]) -> crate::Result<Vec<P
 // Including the binary as root
 // Right now in windows we hot replace the bin and request a restart
 // No assets are replaced
-#[cfg(target_os = "windows")]
-fn bundle_update(settings: &Settings, bundles: &[Bundle]) -> crate::Result<Vec<PathBuf>> {
+fn bundle_update_windows(settings: &Settings, bundles: &[Bundle]) -> crate::Result<Vec<PathBuf>> {
   use crate::bundle::settings::WebviewInstallMode;
-  use crate::bundle::windows::{msi, nsis};
+  #[cfg(target_os = "windows")]
+  use crate::bundle::windows::msi;
+  use crate::bundle::windows::nsis;
   use crate::PackageType;
 
   // find our installers or rebuild
@@ -133,6 +156,7 @@ fn bundle_update(settings: &Settings, bundles: &[Bundle]) -> crate::Result<Vec<P
   let mut rebuild_installers = || -> crate::Result<()> {
     for bundle in bundles {
       match bundle.package_type {
+        #[cfg(target_os = "windows")]
         PackageType::WindowsMsi => bundle_paths.extend(msi::bundle_project(settings, true)?),
         PackageType::Nsis => bundle_paths.extend(nsis::bundle_project(settings, true)?),
         _ => {}
@@ -155,8 +179,7 @@ fn bundle_update(settings: &Settings, bundles: &[Bundle]) -> crate::Result<Vec<P
           PackageType::WindowsMsi | PackageType::Nsis
         )
       })
-      .map(|bundle| bundle.bundle_paths.clone())
-      .flatten()
+      .flat_map(|bundle| bundle.bundle_paths.clone())
       .collect::<Vec<_>>();
 
     // we expect our installer files to be on `bundle_paths`
@@ -177,14 +200,13 @@ fn bundle_update(settings: &Settings, bundles: &[Bundle]) -> crate::Result<Vec<P
           if let std::path::Component::Normal(name) = c {
             if let Some(name) = name.to_str() {
               // installers bundled for updater should be put in a directory named `${bundle_name}-updater`
-              if name == msi::UPDATER_OUTPUT_FOLDER_NAME || name == nsis::UPDATER_OUTPUT_FOLDER_NAME
-              {
+              if name == WIX_UPDATER_OUTPUT_FOLDER_NAME || name == NSIS_UPDATER_OUTPUT_FOLDER_NAME {
                 b = name.strip_suffix("-updater").unwrap().to_string();
                 p.push(&b);
                 return (p, b);
               }
 
-              if name == msi::OUTPUT_FOLDER_NAME || name == nsis::OUTPUT_FOLDER_NAME {
+              if name == WIX_OUTPUT_FOLDER_NAME || name == NSIS_OUTPUT_FOLDER_NAME {
                 b = name.to_string();
               }
             }
@@ -197,7 +219,7 @@ fn bundle_update(settings: &Settings, bundles: &[Bundle]) -> crate::Result<Vec<P
     info!(action = "Bundling"; "{}", archived_path.display());
 
     // Create our gzip file
-    create_zip(&source_path, &archived_path).with_context(|| "Failed to zip update MSI")?;
+    create_zip(&source_path, &archived_path).with_context(|| "Failed to zip update bundle")?;
 
     installers_archived_paths.push(archived_path);
   }
@@ -205,7 +227,6 @@ fn bundle_update(settings: &Settings, bundles: &[Bundle]) -> crate::Result<Vec<P
   Ok(installers_archived_paths)
 }
 
-#[cfg(target_os = "windows")]
 pub fn create_zip(src_file: &Path, dst_file: &Path) -> crate::Result<PathBuf> {
   let parent_dir = dst_file.parent().expect("No data in parent");
   fs::create_dir_all(parent_dir)?;
@@ -224,7 +245,7 @@ pub fn create_zip(src_file: &Path, dst_file: &Path) -> crate::Result<PathBuf> {
   let mut f = File::open(src_file)?;
   let mut buffer = Vec::new();
   f.read_to_end(&mut buffer)?;
-  zip.write_all(&*buffer)?;
+  zip.write_all(&buffer)?;
   buffer.clear();
 
   Ok(dst_file.to_owned())

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

@@ -3,7 +3,14 @@
 // SPDX-License-Identifier: Apache-2.0
 // SPDX-License-Identifier: MIT
 
+#[cfg(target_os = "windows")]
 pub mod msi;
 pub mod nsis;
+#[cfg(target_os = "windows")]
 pub mod sign;
+
 mod util;
+pub use util::{
+  NSIS_OUTPUT_FOLDER_NAME, NSIS_UPDATER_OUTPUT_FOLDER_NAME, WIX_OUTPUT_FOLDER_NAME,
+  WIX_UPDATER_OUTPUT_FOLDER_NAME,
+};

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

@@ -10,8 +10,6 @@ use log::warn;
 
 use std::{self, path::PathBuf};
 
-pub use wix::{OUTPUT_FOLDER_NAME, UPDATER_OUTPUT_FOLDER_NAME};
-
 const WIX_REQUIRED_FILES: &[&str] = &[
   "candle.exe",
   "candle.exe.config",

+ 21 - 23
tooling/bundler/src/bundle/windows/msi/wix.rs

@@ -9,7 +9,8 @@ use crate::bundle::{
   settings::Settings,
   windows::util::{
     download, download_and_verify, extract_zip, try_sign, HashAlgorithm, WEBVIEW2_BOOTSTRAPPER_URL,
-    WEBVIEW2_X64_INSTALLER_GUID, WEBVIEW2_X86_INSTALLER_GUID,
+    WEBVIEW2_X64_INSTALLER_GUID, WEBVIEW2_X86_INSTALLER_GUID, WIX_OUTPUT_FOLDER_NAME,
+    WIX_UPDATER_OUTPUT_FOLDER_NAME,
   },
 };
 use anyhow::{bail, Context};
@@ -27,9 +28,6 @@ use std::{
 use tauri_utils::{config::WebviewInstallMode, resources::resource_relpath};
 use uuid::Uuid;
 
-pub const OUTPUT_FOLDER_NAME: &str = "msi";
-pub const UPDATER_OUTPUT_FOLDER_NAME: &str = "msi-updater";
-
 // URLS for the WIX toolchain.  Can be used for cross-platform compilation.
 pub const WIX_URL: &str =
   "https://github.com/wixtoolset/wix3/releases/download/wix3112rtm/wix311-binaries.zip";
@@ -159,7 +157,7 @@ fn copy_icon(settings: &Settings, filename: &str, path: &Path) -> crate::Result<
   create_dir_all(&resource_dir)?;
   let icon_target_path = resource_dir.join(filename);
 
-  let icon_path = std::env::current_dir()?.join(&path);
+  let icon_path = std::env::current_dir()?.join(path);
 
   copy_file(
     icon_path,
@@ -202,9 +200,9 @@ fn app_installer_output_path(
   Ok(settings.project_out_directory().to_path_buf().join(format!(
     "bundle/{}/{}.msi",
     if updater {
-      UPDATER_OUTPUT_FOLDER_NAME
+      WIX_UPDATER_OUTPUT_FOLDER_NAME
     } else {
-      OUTPUT_FOLDER_NAME
+      WIX_OUTPUT_FOLDER_NAME
     },
     package_base_name
   )))
@@ -332,7 +330,7 @@ fn run_candle(
   let candle_exe = wix_toolset_path.join("candle.exe");
 
   info!(action = "Running"; "candle for {:?}", wxs_file_path);
-  let mut cmd = Command::new(&candle_exe);
+  let mut cmd = Command::new(candle_exe);
   for ext in extensions {
     cmd.arg("-ext");
     cmd.arg(ext);
@@ -368,7 +366,7 @@ fn run_light(
 
   args.extend(arguments);
 
-  let mut cmd = Command::new(&light_exe);
+  let mut cmd = Command::new(light_exe);
   for ext in extensions {
     cmd.arg("-ext");
     cmd.arg(ext);
@@ -416,7 +414,7 @@ pub fn build_wix_app_installer(
     .ok_or_else(|| anyhow::anyhow!("Failed to get main binary"))?;
   let app_exe_source = settings.binary_path(main_binary);
 
-  try_sign(&app_exe_source, &settings)?;
+  try_sign(&app_exe_source, settings)?;
 
   let output_path = settings.project_out_directory().join("wix").join(arch);
 
@@ -529,7 +527,7 @@ pub fn build_wix_app_installer(
       if license.ends_with(".rtf") {
         data.insert("license", to_json(license));
       } else {
-        let license_contents = read_to_string(&license)?;
+        let license_contents = read_to_string(license)?;
         let license_rtf = format!(
           r#"{{\rtf1\ansi\ansicpg1252\deff0\nouicompat\deflang1033{{\fonttbl{{\f0\fnil\fcharset0 Calibri;}}}}
 {{\*\generator Riched20 10.0.18362}}\viewkind4\uc1
@@ -569,24 +567,24 @@ pub fn build_wix_app_installer(
   )
   .to_string();
 
-  data.insert("upgrade_code", to_json(&upgrade_code.as_str()));
+  data.insert("upgrade_code", to_json(upgrade_code.as_str()));
   data.insert(
     "allow_downgrades",
     to_json(settings.windows().allow_downgrades),
   );
 
   let path_guid = generate_package_guid(settings).to_string();
-  data.insert("path_component_guid", to_json(&path_guid.as_str()));
+  data.insert("path_component_guid", to_json(path_guid.as_str()));
 
   let shortcut_guid = generate_package_guid(settings).to_string();
-  data.insert("shortcut_guid", to_json(&shortcut_guid.as_str()));
+  data.insert("shortcut_guid", to_json(shortcut_guid.as_str()));
 
   let app_exe_name = settings.main_binary_name().to_string();
-  data.insert("app_exe_name", to_json(&app_exe_name));
+  data.insert("app_exe_name", to_json(app_exe_name));
 
   let binaries = generate_binaries_data(settings)?;
 
-  let binaries_json = to_json(&binaries);
+  let binaries_json = to_json(binaries);
   data.insert("binaries", binaries_json);
 
   let resources = generate_resource_data(settings)?;
@@ -674,7 +672,7 @@ pub fn build_wix_app_installer(
       to_json(
         settings
           .updater()
-          .and_then(|updater| updater.msiexec_args.clone())
+          .and_then(|updater| updater.msiexec_args)
           .map(|args| args.join(" "))
           .unwrap_or_else(|| "/passive".to_string()),
       ),
@@ -689,7 +687,7 @@ pub fn build_wix_app_installer(
       .expect("Failed to setup Update Task handlebars");
     let temp_xml_path = output_path.join("update.xml");
     let update_content = skip_uac_task.render("update.xml", &data)?;
-    write(&temp_xml_path, update_content)?;
+    write(temp_xml_path, update_content)?;
 
     // Create the Powershell script to install the task
     let mut skip_uac_task_installer = Handlebars::new();
@@ -700,7 +698,7 @@ pub fn build_wix_app_installer(
       .expect("Failed to setup Update Task Installer handlebars");
     let temp_ps1_path = output_path.join("install-task.ps1");
     let install_script_content = skip_uac_task_installer.render("install-task.ps1", &data)?;
-    write(&temp_ps1_path, install_script_content)?;
+    write(temp_ps1_path, install_script_content)?;
 
     // Create the Powershell script to uninstall the task
     let mut skip_uac_task_uninstaller = Handlebars::new();
@@ -711,13 +709,13 @@ pub fn build_wix_app_installer(
       .expect("Failed to setup Update Task Uninstaller handlebars");
     let temp_ps1_path = output_path.join("uninstall-task.ps1");
     let install_script_content = skip_uac_task_uninstaller.render("uninstall-task.ps1", &data)?;
-    write(&temp_ps1_path, install_script_content)?;
+    write(temp_ps1_path, install_script_content)?;
 
     data.insert("enable_elevated_update_task", to_json(true));
   }
 
   let main_wxs_path = output_path.join("main.wxs");
-  write(&main_wxs_path, handlebars.render("main.wxs", &data)?)?;
+  write(main_wxs_path, handlebars.render("main.wxs", &data)?)?;
 
   let mut candle_inputs = vec![("main.wxs".into(), Vec::new())];
 
@@ -818,7 +816,7 @@ pub fn build_wix_app_installer(
       &msi_output_path,
     )?;
     rename(&msi_output_path, &msi_path)?;
-    try_sign(&msi_path, &settings)?;
+    try_sign(&msi_path, settings)?;
     output_paths.push(msi_path);
   }
 
@@ -1004,7 +1002,7 @@ fn generate_resource_data(settings: &Settings) -> crate::Result<ResourceMap> {
     let path = dll?;
     let resource_path = path.to_string_lossy().into_owned();
     let relative_path = path
-      .strip_prefix(&out_dir)
+      .strip_prefix(out_dir)
       .unwrap()
       .to_string_lossy()
       .into_owned();

+ 68 - 28
tooling/bundler/src/bundle/windows/nsis.rs

@@ -2,16 +2,20 @@
 // SPDX-License-Identifier: Apache-2.0
 // SPDX-License-Identifier: MIT
 
+#[cfg(target_os = "windows")]
+use crate::bundle::windows::util::try_sign;
 use crate::{
   bundle::{
     common::CommandExt,
     windows::util::{
-      download, download_and_verify, extract_zip, remove_unc_lossy, try_sign, HashAlgorithm,
-      WEBVIEW2_BOOTSTRAPPER_URL, WEBVIEW2_X64_INSTALLER_GUID, WEBVIEW2_X86_INSTALLER_GUID,
+      download, download_and_verify, extract_zip, remove_unc_lossy, HashAlgorithm,
+      NSIS_OUTPUT_FOLDER_NAME, NSIS_UPDATER_OUTPUT_FOLDER_NAME, WEBVIEW2_BOOTSTRAPPER_URL,
+      WEBVIEW2_X64_INSTALLER_GUID, WEBVIEW2_X86_INSTALLER_GUID,
     },
   },
   Settings,
 };
+
 use anyhow::Context;
 use handlebars::{to_json, Handlebars};
 use log::{info, warn};
@@ -27,12 +31,11 @@ use std::{
   process::Command,
 };
 
-pub const OUTPUT_FOLDER_NAME: &str = "nsis";
-pub const UPDATER_OUTPUT_FOLDER_NAME: &str = "nsis-updater";
-
 // URLS for the NSIS toolchain.
+#[cfg(target_os = "windows")]
 const NSIS_URL: &str =
   "https://sourceforge.net/projects/nsis/files/NSIS%203/3.08/nsis-3.08.zip/download";
+#[cfg(target_os = "windows")]
 const NSIS_SHA1: &str = "057e83c7d82462ec394af76c87d06733605543d4";
 const NSIS_NSCURL_URL: &str =
   "https://github.com/tauri-apps/binary-releases/releases/download/nsis-plugins-v0/NScurl-1.2022.6.7.zip";
@@ -43,6 +46,7 @@ const NSIS_SEMVER_COMPARE: &str =
   "https://github.com/tauri-apps/nsis-semvercompare/releases/download/v0.3.0/nsis_semvercompare.dll";
 const NSIS_SEMVER_COMPARE_SHA1: &str = "1789062E121AC392A6CBBE886F9B1443462912C2";
 
+#[cfg(target_os = "windows")]
 const NSIS_REQUIRED_FILES: &[&str] = &[
   "makensis.exe",
   "Bin/makensis.exe",
@@ -56,6 +60,13 @@ const NSIS_REQUIRED_FILES: &[&str] = &[
   "Include/FileFunc.nsh",
   "Include/x64.nsh",
 ];
+#[cfg(not(target_os = "windows"))]
+const NSIS_REQUIRED_FILES: &[&str] = &[
+  "Plugins/x86-unicode/NScurl.dll",
+  "Plugins/x86-unicode/ApplicationID.dll",
+  "Plugins/x86-unicode/nsProcess.dll",
+  "Plugins/x86-unicode/nsis_semvercompare.dll",
+];
 
 /// Runs all of the commands to build the NSIS installer.
 /// Returns a vector of PathBuf that shows where the NSIS installer was created.
@@ -78,13 +89,16 @@ pub fn bundle_project(settings: &Settings, updater: bool) -> crate::Result<Vec<P
 }
 
 // Gets NSIS and verifies the download via Sha1
-fn get_and_extract_nsis(nsis_toolset_path: &Path, tauri_tools_path: &Path) -> crate::Result<()> {
+fn get_and_extract_nsis(nsis_toolset_path: &Path, _tauri_tools_path: &Path) -> crate::Result<()> {
   info!("Verifying NSIS package");
 
-  let data = download_and_verify(NSIS_URL, NSIS_SHA1, HashAlgorithm::Sha1)?;
-  info!("extracting NSIS");
-  extract_zip(&data, tauri_tools_path)?;
-  rename(tauri_tools_path.join("nsis-3.08"), nsis_toolset_path)?;
+  #[cfg(target_os = "windows")]
+  {
+    let data = download_and_verify(NSIS_URL, NSIS_SHA1, HashAlgorithm::Sha1)?;
+    info!("extracting NSIS");
+    extract_zip(&data, _tauri_tools_path)?;
+    rename(_tauri_tools_path.join("nsis-3.08"), nsis_toolset_path)?;
+  }
 
   let nsis_plugins = nsis_toolset_path.join("Plugins");
 
@@ -127,7 +141,7 @@ fn get_and_extract_nsis(nsis_toolset_path: &Path, tauri_tools_path: &Path) -> cr
 
 fn build_nsis_app_installer(
   settings: &Settings,
-  nsis_toolset_path: &Path,
+  _nsis_toolset_path: &Path,
   tauri_tools_path: &Path,
   updater: bool,
 ) -> crate::Result<Vec<PathBuf>> {
@@ -144,14 +158,19 @@ fn build_nsis_app_installer(
 
   info!("Target: {}", arch);
 
-  let main_binary = settings
-    .binaries()
-    .iter()
-    .find(|bin| bin.main())
-    .ok_or_else(|| anyhow::anyhow!("Failed to get main binary"))?;
-  let app_exe_source = settings.binary_path(main_binary);
+  #[cfg(target_os = "windows")]
+  {
+    let main_binary = settings
+      .binaries()
+      .iter()
+      .find(|bin| bin.main())
+      .ok_or_else(|| anyhow::anyhow!("Failed to get main binary"))?;
+    let app_exe_source = settings.binary_path(main_binary);
+    try_sign(&app_exe_source, settings)?;
+  }
 
-  try_sign(&app_exe_source, &settings)?;
+  #[cfg(not(target_os = "windows"))]
+  info!("Code signing is currently only supported on Windows hosts, skipping...");
 
   let output_path = settings.project_out_directory().join("nsis").join(arch);
   if output_path.exists() {
@@ -164,13 +183,20 @@ fn build_nsis_app_installer(
   let bundle_id = settings.bundle_identifier();
   let manufacturer = bundle_id.split('.').nth(1).unwrap_or(bundle_id);
 
+  #[cfg(not(target_os = "windows"))]
+  {
+    let mut dir = dirs_next::cache_dir().unwrap();
+    dir.extend(["tauri", "NSIS", "Plugins", "x86-unicode"]);
+    data.insert("additional_plugins_path", to_json(dir));
+  }
+
   data.insert("arch", to_json(arch));
   data.insert("bundle_id", to_json(bundle_id));
   data.insert("manufacturer", to_json(manufacturer));
   data.insert("product_name", to_json(settings.product_name()));
 
   let version = settings.version_string();
-  data.insert("version", to_json(&version));
+  data.insert("version", to_json(version));
 
   data.insert(
     "allow_downgrades",
@@ -236,13 +262,13 @@ fn build_nsis_app_installer(
   );
   data.insert(
     "main_binary_path",
-    to_json(settings.binary_path(main_binary)),
+    to_json(settings.binary_path(main_binary).with_extension("exe")),
   );
 
   let out_file = "nsis-output.exe";
-  data.insert("out_file", to_json(&out_file));
+  data.insert("out_file", to_json(out_file));
 
-  let resources = generate_resource_data(&settings)?;
+  let resources = generate_resource_data(settings)?;
   data.insert("resources", to_json(resources));
 
   let binaries = generate_binaries_data(settings)?;
@@ -372,23 +398,37 @@ fn build_nsis_app_installer(
   let nsis_installer_path = settings.project_out_directory().to_path_buf().join(format!(
     "bundle/{}/{}.exe",
     if updater {
-      UPDATER_OUTPUT_FOLDER_NAME
+      NSIS_UPDATER_OUTPUT_FOLDER_NAME
     } else {
-      OUTPUT_FOLDER_NAME
+      NSIS_OUTPUT_FOLDER_NAME
     },
     package_base_name
   ));
   create_dir_all(nsis_installer_path.parent().unwrap())?;
 
   info!(action = "Running"; "makensis.exe to produce {}", nsis_installer_path.display());
-  Command::new(nsis_toolset_path.join("makensis.exe"))
-    .arg("/V4")
+
+  #[cfg(target_os = "windows")]
+  let mut nsis_cmd = Command::new(_nsis_toolset_path.join("makensis.exe"));
+  #[cfg(not(target_os = "windows"))]
+  let mut nsis_cmd = Command::new("makensis");
+
+  nsis_cmd
+    .arg(match settings.log_level() {
+      log::Level::Error => "-V1",
+      log::Level::Warn => "-V2",
+      log::Level::Info => "-V3",
+      _ => "-V4",
+    })
     .arg(installer_nsi_path)
     .current_dir(output_path)
-    .output_ok()
+    .piped()
     .context("error running makensis.exe")?;
 
-  rename(&nsis_output_path, &nsis_installer_path)?;
+  rename(nsis_output_path, &nsis_installer_path)?;
+
+  // Code signing is currently only supported on Windows hosts
+  #[cfg(target_os = "windows")]
   try_sign(&nsis_installer_path, settings)?;
 
   Ok(vec![nsis_installer_path])

+ 6 - 6
tooling/bundler/src/bundle/windows/sign.rs

@@ -103,16 +103,16 @@ pub fn sign<P: AsRef<Path>>(path: P, params: &SignParams) -> crate::Result<()> {
 
   let mut cmd = Command::new(signtool);
   cmd.arg("sign");
-  cmd.args(&["/fd", &params.digest_algorithm]);
-  cmd.args(&["/sha1", &params.certificate_thumbprint]);
-  cmd.args(&["/d", &params.product_name]);
+  cmd.args(["/fd", &params.digest_algorithm]);
+  cmd.args(["/sha1", &params.certificate_thumbprint]);
+  cmd.args(["/d", &params.product_name]);
 
   if let Some(ref timestamp_url) = params.timestamp_url {
     if params.tsp {
-      cmd.args(&["/tr", timestamp_url]);
-      cmd.args(&["/td", &params.digest_algorithm]);
+      cmd.args(["/tr", timestamp_url]);
+      cmd.args(["/td", &params.digest_algorithm]);
     } else {
-      cmd.args(&["/t", timestamp_url]);
+      cmd.args(["/t", timestamp_url]);
     }
   }
 

+ 4 - 0
tooling/bundler/src/bundle/windows/templates/installer.nsi

@@ -19,6 +19,7 @@ Var ReinstallPageCheck
 !define BUNDLEID "{{{bundle_id}}}"
 !define OUTFILE "{{{out_file}}}"
 !define ARCH "{{{arch}}}"
+!define PLUGINSPATH "{{{additional_plugins_path}}}"
 !define ALLOWDOWNGRADES "{{{allow_downgrades}}}"
 !define DISPLAYLANGUAGESELECTOR "{{{display_language_selector}}}"
 !define INSTALLWEBVIEW2MODE "{{{install_webview2_mode}}}"
@@ -31,6 +32,9 @@ Name "${PRODUCTNAME}"
 OutFile "${OUTFILE}"
 Unicode true
 SetCompressor /SOLID lzma
+!if "${PLUGINSPATH}" != ""
+    !addplugindir "${PLUGINSPATH}"
+!endif
 
 !if "${INSTALLMODE}" == "perMachine"
   RequestExecutionLevel highest

+ 13 - 6
tooling/bundler/src/bundle/windows/util.rs

@@ -12,14 +12,18 @@ use log::info;
 use sha2::Digest;
 use zip::ZipArchive;
 
+#[cfg(target_os = "windows")]
+use crate::bundle::windows::sign::{sign, SignParams};
+#[cfg(target_os = "windows")]
+use crate::Settings;
+
 pub const WEBVIEW2_BOOTSTRAPPER_URL: &str = "https://go.microsoft.com/fwlink/p/?LinkId=2124703";
 pub const WEBVIEW2_X86_INSTALLER_GUID: &str = "a17bde80-b5ab-47b5-8bbb-1cbe93fc6ec9";
 pub const WEBVIEW2_X64_INSTALLER_GUID: &str = "aa5fd9b3-dc11-4cbc-8343-a50f57b311e1";
-
-use crate::{
-  bundle::windows::sign::{sign, SignParams},
-  Settings,
-};
+pub const NSIS_OUTPUT_FOLDER_NAME: &str = "nsis";
+pub const NSIS_UPDATER_OUTPUT_FOLDER_NAME: &str = "nsis-updater";
+pub const WIX_OUTPUT_FOLDER_NAME: &str = "msi";
+pub const WIX_UPDATER_OUTPUT_FOLDER_NAME: &str = "msi-updater";
 
 pub fn download(url: &str) -> crate::Result<Vec<u8>> {
   info!(action = "Downloading"; "{}", url);
@@ -28,6 +32,7 @@ pub fn download(url: &str) -> crate::Result<Vec<u8>> {
 }
 
 pub enum HashAlgorithm {
+  #[cfg(target_os = "windows")]
   Sha256,
   Sha1,
 }
@@ -42,6 +47,7 @@ pub fn download_and_verify(
   info!("validating hash");
 
   match hash_algorithim {
+    #[cfg(target_os = "windows")]
     HashAlgorithm::Sha256 => {
       let hasher = sha2::Sha256::new();
       verify(&data, hash, hasher)?;
@@ -67,11 +73,12 @@ fn verify(data: &Vec<u8>, hash: &str, mut hasher: impl Digest) -> crate::Result<
   }
 }
 
+#[cfg(target_os = "windows")]
 pub fn try_sign(file_path: &PathBuf, settings: &Settings) -> crate::Result<()> {
   if let Some(certificate_thumbprint) = settings.windows().certificate_thumbprint.as_ref() {
     info!(action = "Signing"; "{}", file_path.display());
     sign(
-      &file_path,
+      file_path,
       &SignParams {
         product_name: settings.product_name().into(),
         digest_algorithm: settings

+ 0 - 3
tooling/bundler/src/error.rs

@@ -35,11 +35,9 @@ pub enum Error {
   #[error("`{0}`")]
   ConvertError(#[from] num::TryFromIntError),
   /// Zip error.
-  #[cfg(target_os = "windows")]
   #[error("`{0}`")]
   ZipError(#[from] zip::result::ZipError),
   /// Hex error.
-  #[cfg(target_os = "windows")]
   #[error("`{0}`")]
   HexError(#[from] hex::FromHexError),
   /// Handlebars template error.
@@ -53,7 +51,6 @@ pub enum Error {
   #[error("`{0}`")]
   RegexError(#[from] regex::Error),
   /// Failed to perform HTTP request.
-  #[cfg(windows)]
   #[error("`{0}`")]
   HttpError(#[from] attohttpc::Error),
   /// Invalid glob pattern.

+ 1 - 0
tooling/cli/Cargo.lock

@@ -3167,6 +3167,7 @@ dependencies = [
  "libflate",
  "log",
  "md5",
+ "os_pipe",
  "plist",
  "regex",
  "semver",

+ 8 - 2
tooling/cli/src/build.rs

@@ -59,7 +59,7 @@ pub struct Options {
   ci: bool,
 }
 
-pub fn command(mut options: Options) -> Result<()> {
+pub fn command(mut options: Options, verbosity: u8) -> Result<()> {
   options.ci = options.ci || std::env::var("CI").is_ok();
   let ci = options.ci;
 
@@ -224,10 +224,16 @@ pub fn command(mut options: Options) -> Result<()> {
       }
     }
 
-    let settings = app_settings
+    let mut settings = app_settings
       .get_bundler_settings(&options.into(), config_, out_dir, package_types)
       .with_context(|| "failed to build bundler settings")?;
 
+    settings.set_log_level(match verbosity {
+      0 => log::Level::Error,
+      1 => log::Level::Info,
+      _ => log::Level::Trace,
+    });
+
     // set env vars used by the bundler
     #[cfg(target_os = "linux")]
     {

+ 11 - 9
tooling/cli/src/interface/rust.rs

@@ -19,7 +19,6 @@ use std::{
 };
 
 use anyhow::Context;
-#[cfg(target_os = "linux")]
 use heck::ToKebabCase;
 use ignore::gitignore::{Gitignore, GitignoreBuilder};
 use log::{debug, error, info};
@@ -686,6 +685,8 @@ impl AppSettings for RustAppSettings {
     }
     .into();
 
+    let target_os = target.split('-').nth(2).unwrap_or(std::env::consts::OS);
+
     if let Some(bin) = &self.cargo_settings.bin {
       let default_run = self
         .package_settings
@@ -763,14 +764,15 @@ impl AppSettings for RustAppSettings {
 
     match binaries.len() {
       0 => binaries.push(BundleBinary::new(
-        #[cfg(target_os = "linux")]
-        self.package_settings.product_name.to_kebab_case(),
-        #[cfg(not(target_os = "linux"))]
-        format!(
-          "{}{}",
-          self.package_settings.product_name.clone(),
-          &binary_extension
-        ),
+        if target_os == "linux" {
+          self.package_settings.product_name.to_kebab_case()
+        } else {
+          format!(
+            "{}{}",
+            self.package_settings.product_name.clone(),
+            &binary_extension
+          )
+        },
         true,
       )),
       1 => binaries.get_mut(0).unwrap().set_main(true),

+ 25 - 6
tooling/cli/src/interface/rust/desktop.rs

@@ -2,7 +2,6 @@ use super::{AppSettings, DevChild, ExitReason, Options, RustAppSettings, Target}
 use crate::CommandExt;
 
 use anyhow::Context;
-#[cfg(target_os = "linux")]
 use heck::ToKebabCase;
 use shared_child::SharedChild;
 use std::{
@@ -26,6 +25,12 @@ pub fn run_dev<F: Fn(ExitStatus, ExitReason) + Send + Sync + 'static>(
   on_exit: F,
 ) -> crate::Result<DevChild> {
   let bin_path = app_settings.app_binary_path(&options)?;
+  let target_os = options
+    .target
+    .as_ref()
+    .and_then(|t| t.split('-').nth(2))
+    .unwrap_or(std::env::consts::OS)
+    .replace("darwin", "macos");
 
   let manually_killed_app = Arc::new(AtomicBool::default());
   let manually_killed_app_ = manually_killed_app.clone();
@@ -39,7 +44,7 @@ pub fn run_dev<F: Fn(ExitStatus, ExitReason) + Send + Sync + 'static>(
     move |status, reason| {
       if status.success() {
         let bin_path =
-          rename_app(&bin_path, product_name.as_deref()).expect("failed to rename app");
+          rename_app(target_os, &bin_path, product_name.as_deref()).expect("failed to rename app");
         let mut app = Command::new(bin_path);
         app.stdout(os_pipe::dup_stdout().unwrap());
         app.stderr(os_pipe::dup_stderr().unwrap());
@@ -95,6 +100,13 @@ pub fn build(
     std::env::set_var("STATIC_VCRUNTIME", "true");
   }
 
+  let target_os = options
+    .target
+    .as_ref()
+    .and_then(|t| t.split('-').nth(2))
+    .unwrap_or(std::env::consts::OS)
+    .replace("darwin", "macos");
+
   if options.target == Some("universal-apple-darwin".into()) {
     std::fs::create_dir_all(out_dir).with_context(|| "failed to create project out directory")?;
 
@@ -128,7 +140,7 @@ pub fn build(
       .with_context(|| "failed to build app")?;
   }
 
-  rename_app(&bin_path, product_name.as_deref())?;
+  rename_app(target_os, &bin_path, product_name.as_deref())?;
 
   Ok(())
 }
@@ -332,10 +344,17 @@ fn validate_target(available_targets: &Option<Vec<Target>>, target: &str) -> cra
   Ok(())
 }
 
-fn rename_app(bin_path: &Path, product_name: Option<&str>) -> crate::Result<PathBuf> {
+fn rename_app(
+  target_os: String,
+  bin_path: &Path,
+  product_name: Option<&str>,
+) -> crate::Result<PathBuf> {
   if let Some(product_name) = product_name {
-    #[cfg(target_os = "linux")]
-    let product_name = product_name.to_kebab_case();
+    let product_name = if target_os == "linux" {
+      product_name.to_kebab_case()
+    } else {
+      product_name.into()
+    };
 
     let product_path = bin_path
       .parent()

+ 1 - 1
tooling/cli/src/lib.rs

@@ -160,7 +160,7 @@ where
   }
 
   match cli.command {
-    Commands::Build(options) => build::command(options)?,
+    Commands::Build(options) => build::command(options, cli.verbose)?,
     Commands::Dev(options) => dev::command(options)?,
     Commands::Icon(options) => icon::command(options)?,
     Commands::Info(options) => info::command(options)?,