fix(bundler): sign DLLs (#11676)

* fix: sign nsis plugin DLLs

* also sign DLLs on unix

* fix build

* create copy of nsis dir

* always make a copy of nsis (so linux works, permission error otherwise)

* fix windows build

* fix

* to_path_buf

* also create wix copy

* remove unused toolset change

* fix unused var

* fmt

* fix wix build

* fix build

* fix plugin copy

* fix conflict

* fix file download

---------

Co-authored-by: Lucas Nogueira <lucas@tauri.app>
This commit is contained in:
thewh1teagle
2025-04-14 00:46:14 +03:00
committed by GitHub
parent 8a1d490820
commit 8d994f60fe
10 changed files with 174 additions and 35 deletions

View File

@@ -0,0 +1,5 @@
---
'tauri-bundler': 'patch:enhance'
---
Sign NSIS and WiX DLLs when bundling

5
.changes/sign-dlls.md Normal file
View File

@@ -0,0 +1,5 @@
---
'tauri-bundler': 'patch:enhance'
---
Sign DLLs from resources.

1
Cargo.lock generated
View File

@@ -8503,6 +8503,7 @@ dependencies = [
"url",
"uuid",
"walkdir",
"which",
"windows-registry 0.5.0",
"windows-sys 0.59.0",
"zip 2.3.0",

View File

@@ -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"

View File

@@ -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 }
@@ -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));
}
@@ -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();
@@ -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()),
@@ -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(),
@@ -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(),

View File

@@ -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}}"
@@ -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}" != ""

View File

@@ -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",
"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,
@@ -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}"))?;
}
}
}
@@ -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(
@@ -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)?;
@@ -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>> {
@@ -180,6 +191,65 @@ 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();
@@ -187,12 +257,17 @@ fn build_nsis_app_installer(
.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));
@@ -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() {
@@ -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,
@@ -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)?;
}
let target_path = resource.target();
resources.insert(
resource_path,

View File

@@ -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(())

View File

@@ -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) {
@@ -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)?;
}

View File

@@ -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";