Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changes/nsis-sign-plugins.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'tauri-bundler': 'patch:enhance'
---

Sign NSIS and WiX DLLs when bundling
5 changes: 5 additions & 0 deletions .changes/sign-dlls.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'tauri-bundler': 'patch:enhance'
---

Sign DLLs from resources.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions crates/tauri-bundler/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,9 @@ ar = "0.9"
md5 = "0.7"
rpm = { version = "0.16", features = ["bzip2-compression"] }

[target."cfg(unix)".dependencies]
which = "7"

[lib]
name = "tauri_bundler"
path = "src/lib.rs"
Expand Down
36 changes: 32 additions & 4 deletions crates/tauri-bundler/src/bundle/windows/msi/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -472,6 +472,16 @@ pub fn build_wix_app_installer(
}
fs::create_dir_all(&output_path)?;

// when we're performing code signing, we'll sign some WiX DLLs, so we make a local copy
let wix_toolset_path = if settings.can_sign() {
let wix_path = output_path.join("wix");
crate::utils::fs_utils::copy_dir(&wix_toolset_path, &wix_path)
.context("failed to copy wix directory")?;
wix_path
} else {
wix_toolset_path.to_path_buf()
};

let mut data = BTreeMap::new();

let silent_webview_install = if let WebviewInstallMode::DownloadBootstrapper { silent }
Expand Down Expand Up @@ -763,7 +773,11 @@ pub fn build_wix_app_installer(
let fragment = fragment_handlebars.render_template(&fragment_content, &data)?;
let mut extensions = Vec::new();
for cap in extension_regex.captures_iter(&fragment) {
extensions.push(wix_toolset_path.join(format!("Wix{}.dll", &cap[1])));
let path = wix_toolset_path.join(format!("Wix{}.dll", &cap[1]));
if settings.can_sign() {
try_sign(&path, settings)?;
}
extensions.push(path);
}
candle_inputs.push((fragment_path, extensions));
}
Expand All @@ -773,11 +787,18 @@ pub fn build_wix_app_installer(
fragment_extensions.insert(wix_toolset_path.join("WixUIExtension.dll"));
fragment_extensions.insert(wix_toolset_path.join("WixUtilExtension.dll"));

// sign default extensions
if settings.can_sign() {
for path in &fragment_extensions {
try_sign(&path, settings)?;
}
}

for (path, extensions) in candle_inputs {
for ext in &extensions {
fragment_extensions.insert(ext.clone());
}
run_candle(settings, wix_toolset_path, &output_path, path, extensions)?;
run_candle(settings, &wix_toolset_path, &output_path, path, extensions)?;
}

let mut output_paths = Vec::new();
Expand Down Expand Up @@ -853,7 +874,7 @@ pub fn build_wix_app_installer(
log::info!(action = "Running"; "light to produce {}", display_path(&msi_path));

run_light(
wix_toolset_path,
&wix_toolset_path,
&output_path,
arguments,
&(fragment_extensions.clone().into_iter().collect()),
Expand Down Expand Up @@ -968,9 +989,12 @@ fn generate_resource_data(settings: &Settings) -> crate::Result<ResourceMap> {
if added_resources.contains(&resource_path) {
continue;
}

added_resources.push(resource_path.clone());

if settings.can_sign() {
try_sign(&resource_path, settings)?;
}

let resource_entry = ResourceFile {
id: format!("I{}", Uuid::new_v4().as_simple()),
guid: Uuid::new_v4().to_string(),
Expand Down Expand Up @@ -1055,6 +1079,10 @@ fn generate_resource_data(settings: &Settings) -> crate::Result<ResourceMap> {
.to_string_lossy()
.into_owned();
if !added_resources.iter().any(|r| r.ends_with(&relative_path)) {
if settings.can_sign() {
try_sign(resource_path, settings)?;
}

dlls.push(ResourceFile {
id: format!("I{}", Uuid::new_v4().as_simple()),
guid: Uuid::new_v4().to_string(),
Expand Down
8 changes: 3 additions & 5 deletions crates/tauri-bundler/src/bundle/windows/nsis/installer.nsi
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ ${StrLoc}
!define COPYRIGHT "{{copyright}}"
!define OUTFILE "{{out_file}}"
!define ARCH "{{arch}}"
!define PLUGINSPATH "{{additional_plugins_path}}"
!define ADDITIONALPLUGINSPATH "{{additional_plugins_path}}"
!define ALLOWDOWNGRADES "{{allow_downgrades}}"
!define DISPLAYLANGUAGESELECTOR "{{display_language_selector}}"
!define INSTALLWEBVIEW2MODE "{{install_webview2_mode}}"
Expand Down Expand Up @@ -85,10 +85,8 @@ VIAddVersionKey "LegalCopyright" "${COPYRIGHT}"
VIAddVersionKey "FileVersion" "${VERSION}"
VIAddVersionKey "ProductVersion" "${VERSION}"

; Plugins path, currently exists for linux only
!if "${PLUGINSPATH}" != ""
!addplugindir "${PLUGINSPATH}"
!endif
# additional plugins
!addplugindir "${ADDITIONALPLUGINSPATH}"

; Uninstaller signing command
!if "${UNINSTALLERSIGNCOMMAND}" != ""
Expand Down
124 changes: 111 additions & 13 deletions crates/tauri-bundler/src/bundle/windows/nsis/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,11 +55,18 @@ const NSIS_REQUIRED_FILES: &[&str] = &[
"Include/nsDialogs.nsh",
"Include/WinMessages.nsh",
];
const NSIS_PLUGIN_FILES: &[&str] = &[
"NSISdl.dll",
"StartMenu.dll",
"System.dll",
"nsDialogs.dll",
Comment thread
amrbashir marked this conversation as resolved.
"additional/nsis_tauri_utils.dll",
];
#[cfg(not(target_os = "windows"))]
const NSIS_REQUIRED_FILES: &[&str] = &["Plugins/x86-unicode/nsis_tauri_utils.dll"];
const NSIS_REQUIRED_FILES: &[&str] = &["Plugins/x86-unicode/additional/nsis_tauri_utils.dll"];

const NSIS_REQUIRED_FILES_HASH: &[(&str, &str, &str, HashAlgorithm)] = &[(
"Plugins/x86-unicode/nsis_tauri_utils.dll",
"Plugins/x86-unicode/additional/nsis_tauri_utils.dll",
NSIS_TAURI_UTILS_URL,
NSIS_TAURI_UTILS_SHA1,
HashAlgorithm::Sha1,
Expand Down Expand Up @@ -96,7 +103,10 @@ pub fn bundle_project(settings: &Settings, updater: bool) -> crate::Result<Vec<P
log::warn!("NSIS directory contains mis-hashed files. Redownloading them.");
for (path, url, hash, hash_algorithm) in mismatched {
let data = download_and_verify(url, hash, *hash_algorithm)?;
fs::write(nsis_toolset_path.join(path), data)?;
let out_path = nsis_toolset_path.join(path);
std::fs::create_dir_all(out_path.parent().context("output path has no parent")?)
.context("failed to create file output directory")?;
fs::write(out_path, data).with_context(|| format!("failed to save {path}"))?;
}
}
}
Expand All @@ -116,6 +126,7 @@ fn get_and_extract_nsis(nsis_toolset_path: &Path, _tauri_tools_path: &Path) -> c
fs::rename(_tauri_tools_path.join("nsis-3.08"), nsis_toolset_path)?;
}

// download additional plugins
let nsis_plugins = nsis_toolset_path.join("Plugins");

let data = download_and_verify(
Expand All @@ -124,7 +135,7 @@ fn get_and_extract_nsis(nsis_toolset_path: &Path, _tauri_tools_path: &Path) -> c
HashAlgorithm::Sha1,
)?;

let target_folder = nsis_plugins.join("x86-unicode");
let target_folder = nsis_plugins.join("x86-unicode").join("additional");
fs::create_dir_all(&target_folder)?;
fs::write(target_folder.join("nsis_tauri_utils.dll"), data)?;

Expand Down Expand Up @@ -156,7 +167,7 @@ fn try_add_numeric_build_number(version_str: &str) -> anyhow::Result<String> {

fn build_nsis_app_installer(
settings: &Settings,
_nsis_toolset_path: &Path,
#[allow(unused_variables)] nsis_toolset_path: &Path,
tauri_tools_path: &Path,
updater: bool,
) -> crate::Result<Vec<PathBuf>> {
Expand All @@ -180,19 +191,83 @@ fn build_nsis_app_installer(
}
fs::create_dir_all(&output_path)?;

// we make a copy of the NSIS directory if we're going to sign its DLLs
// because we don't want to change the DLL hashes so the cache can reuse it
let maybe_plugin_copy_path = if settings.can_sign() {
// find nsis path
#[cfg(target_os = "linux")]
let system_nsis_toolset_path = std::env::var_os("NSIS_PATH")
.map(PathBuf::from)
.unwrap_or_else(|| PathBuf::from("/usr/share/nsis"));
#[cfg(target_os = "macos")]
let system_nsis_toolset_path = std::env::var_os("NSIS_PATH")
.map(PathBuf::from)
.ok_or_else(|| anyhow::anyhow!("failed to resolve NSIS path"))
.or_else(|_| {
let mut makensis_path =
which::which("makensis").context("failed to resolve `makensis`; did you install nsis? See https://tauri.app/distribute/windows-installer/#install-nsis for more information")?;
// homebrew installs it as a symlink
if makensis_path.is_symlink() {
// read_link might return a path relative to makensis_path so we must use join() and canonicalize
makensis_path = makensis_path
.parent()
.context("missing makensis parent")?
.join(std::fs::read_link(&makensis_path).context("failed to resolve makensis symlink")?)
.canonicalize()
.context("failed to resolve makensis path")?;
}
// file structure:
// ├── bin
// │ ├── makensis
// ├── share
// │ ├── nsis
let bin_folder = makensis_path.parent().context("missing makensis parent")?;
let root_folder = bin_folder.parent().context("missing makensis root")?;
crate::Result::Ok(root_folder.join("share").join("nsis"))
})?;
#[cfg(windows)]
let system_nsis_toolset_path = nsis_toolset_path.to_path_buf();

let plugins_path = output_path.join("Plugins");
// copy system plugins (we don't want to modify system installed DLLs, and on some systems there will even be permission errors if we try)
crate::utils::fs_utils::copy_dir(
&system_nsis_toolset_path.join("Plugins").join("x86-unicode"),
&plugins_path.join("x86-unicode"),
)
.context("failed to copy system NSIS Plugins folder to local copy")?;
// copy our downloaded DLLs
crate::utils::fs_utils::copy_dir(
&nsis_toolset_path
.join("Plugins")
.join("x86-unicode")
.join("additional"),
&plugins_path.join("x86-unicode").join("additional"),
)
.context("failed to copy additional NSIS Plugins folder to local copy")?;
Some(plugins_path)
} else {
// in this case plugin_copy_path can be None, we'll use the system default path
None
};

let mut data = BTreeMap::new();

let bundle_id = settings.bundle_identifier();
let manufacturer = settings
.publisher()
.unwrap_or_else(|| bundle_id.split('.').nth(1).unwrap_or(bundle_id));

#[cfg(not(target_os = "windows"))]
{
let mut dir = dirs::cache_dir().unwrap();
dir.extend(["tauri", "NSIS", "Plugins", "x86-unicode"]);
data.insert("additional_plugins_path", to_json(dir));
}
let additional_plugins_path = maybe_plugin_copy_path
.clone()
.unwrap_or_else(|| nsis_toolset_path.join("Plugins"))
.join("x86-unicode")
.join("additional");

data.insert(
"additional_plugins_path",
// either our Plugins copy (when signing) or the cache/Plugins/x86-unicode path
to_json(&additional_plugins_path),
);

data.insert("arch", to_json(arch));
data.insert("bundle_id", to_json(bundle_id));
Expand Down Expand Up @@ -526,13 +601,29 @@ fn build_nsis_app_installer(
));
fs::create_dir_all(nsis_installer_path.parent().unwrap())?;

log::info!(action = "Running"; "makensis.exe to produce {}", display_path(&nsis_installer_path));
if settings.can_sign() {
log::info!("Signing NSIS plugins");
for dll in NSIS_PLUGIN_FILES {
let path = additional_plugins_path.join(dll);
if path.exists() {
try_sign(&path, settings)?;
} else {
log::warn!("Could not find {}, skipping signing", path.display());
}
}
}

log::info!(action = "Running"; "makensis to produce {}", display_path(&nsis_installer_path));

#[cfg(target_os = "windows")]
let mut nsis_cmd = Command::new(_nsis_toolset_path.join("makensis.exe"));
let mut nsis_cmd = Command::new(nsis_toolset_path.join("makensis.exe"));
#[cfg(not(target_os = "windows"))]
let mut nsis_cmd = Command::new("makensis");

if let Some(plugins_path) = &maybe_plugin_copy_path {
nsis_cmd.env("NSISPLUGINS", plugins_path);
}

nsis_cmd
.args(["-INPUTCHARSET", "UTF8", "-OUTPUTCHARSET", "UTF8"])
.arg(match settings.log_level() {
Expand Down Expand Up @@ -628,6 +719,9 @@ fn generate_resource_data(settings: &Settings) -> crate::Result<ResourcesMap> {
let loader_path =
dunce::simplified(&settings.project_out_directory().join("WebView2Loader.dll")).to_path_buf();
if loader_path.exists() {
if settings.can_sign() {
try_sign(&loader_path, settings)?;
}
added_resources.push(loader_path.clone());
resources.insert(
loader_path,
Expand All @@ -650,6 +744,10 @@ fn generate_resource_data(settings: &Settings) -> crate::Result<ResourcesMap> {
}
added_resources.push(resource_path.clone());

if settings.can_sign() {
try_sign(&resource_path, settings)?;

@linguofeng linguofeng May 17, 2025

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

...

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you already identified the code so are you open to creating a PR as well?

}

let target_path = resource.target();
resources.insert(
resource_path,
Expand Down
4 changes: 2 additions & 2 deletions crates/tauri-bundler/src/bundle/windows/sign.rs
Original file line number Diff line number Diff line change
Expand Up @@ -241,9 +241,9 @@ pub fn sign<P: AsRef<Path>>(path: P, params: &SignParams) -> crate::Result<()> {
}
}

pub fn try_sign(file_path: &std::path::PathBuf, settings: &Settings) -> crate::Result<()> {
pub fn try_sign<P: AsRef<Path>>(file_path: P, settings: &Settings) -> crate::Result<()> {
if settings.can_sign() {
log::info!(action = "Signing"; "{}", tauri_utils::display_path(file_path));
log::info!(action = "Signing"; "{}", tauri_utils::display_path(file_path.as_ref()));
sign(file_path, &settings.sign_params())?;
}
Ok(())
Expand Down
5 changes: 1 addition & 4 deletions crates/tauri-bundler/src/utils/fs_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -111,9 +111,6 @@ pub fn copy_dir(from: &Path, to: &Path) -> crate::Result<()> {
"{from:?} is not a Directory"
)));
}
if to.exists() {
return Err(crate::Error::GenericError(format!("{to:?} already exists")));
}
let parent = to.parent().expect("No data in parent");
fs::create_dir_all(parent)?;
for entry in walkdir::WalkDir::new(from) {
Expand All @@ -129,7 +126,7 @@ pub fn copy_dir(from: &Path, to: &Path) -> crate::Result<()> {
symlink_file(&target, &dest_path)?;
}
} else if entry.file_type().is_dir() {
fs::create_dir(dest_path)?;
fs::create_dir_all(dest_path)?;
} else {
fs::copy(entry.path(), dest_path)?;
}
Expand Down
18 changes: 11 additions & 7 deletions examples/api/src-tauri/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,20 @@ fn main() {
)
.expect("failed to run tauri-build");

// workaround needed to prevent `STATUS_ENTRYPOINT_NOT_FOUND` error in tests
// see https://github.com/tauri-apps/tauri/pull/4383#issuecomment-1212221864
let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap();
let target_env = std::env::var("CARGO_CFG_TARGET_ENV");
let is_tauri_workspace = std::env::var("__TAURI_WORKSPACE__").is_ok_and(|v| v == "true");
if is_tauri_workspace && target_os == "windows" && Ok("msvc") == target_env.as_deref() {
embed_manifest_for_tests();
#[cfg(windows)]
{
// workaround needed to prevent `STATUS_ENTRYPOINT_NOT_FOUND` error in tests
// see https://github.com/tauri-apps/tauri/pull/4383#issuecomment-1212221864
let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap();
let target_env = std::env::var("CARGO_CFG_TARGET_ENV");
let is_tauri_workspace = std::env::var("__TAURI_WORKSPACE__").is_ok_and(|v| v == "true");
if is_tauri_workspace && target_os == "windows" && Ok("msvc") == target_env.as_deref() {
embed_manifest_for_tests();
}
}
}

#[cfg(windows)]
fn embed_manifest_for_tests() {
static WINDOWS_MANIFEST_FILE: &str = "windows-app-manifest.xml";

Expand Down