feat(bundler): support custom sign command on Windows (#9865)

* feat(bundler): support custom sign command on Windows

closes #7188
closes #9578

* fix double quotes

* fix build

* fix build

* clippy

* Update sign.rs

* clippy && replace `winreg` with `windows-registry`

* remove log [skip ci]

* Apply suggestions from code review

* tweak arg so path with spaces work on macOS

* create nsis toolset paths

---------

Co-authored-by: Lucas Nogueira <lucas@tauri.app>
This commit is contained in:
Amr Bashir 2024-05-24 16:25:13 +03:00 committed by GitHub
parent fc1543c65e
commit d6d3efbd12
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 310 additions and 181 deletions

View File

@ -0,0 +1,5 @@
---
"tauri-bundler": "patch:feat"
---
On Windows, add option to specify a custom signing command to be used. This opens an endless possibilities, for example use `osslsigncode` on non-Windows or use hardware tokens and HSM or even using Azure Trusted Signing.

View File

@ -0,0 +1,5 @@
---
"tauri-utils": "patch:feat"
---
Add `sign_command` in `WindowsConfig`

View File

@ -112,6 +112,7 @@
"certificateThumbprint": null, "certificateThumbprint": null,
"digestAlgorithm": null, "digestAlgorithm": null,
"nsis": null, "nsis": null,
"signCommand": null,
"timestampUrl": null, "timestampUrl": null,
"tsp": false, "tsp": false,
"webviewFixedRuntimePath": null, "webviewFixedRuntimePath": null,
@ -1619,6 +1620,7 @@
"certificateThumbprint": null, "certificateThumbprint": null,
"digestAlgorithm": null, "digestAlgorithm": null,
"nsis": null, "nsis": null,
"signCommand": null,
"timestampUrl": null, "timestampUrl": null,
"tsp": false, "tsp": false,
"webviewFixedRuntimePath": null, "webviewFixedRuntimePath": null,
@ -1977,6 +1979,13 @@
"type": "null" "type": "null"
} }
] ]
},
"signCommand": {
"description": "Specify a custom command to sign the binaries. This command needs to have a `%1` in it which is just a placeholder for the binary path, which we will detect and replace before calling the command.\n\nExample: ```text sign-cli --arg1 --arg2 %1 ```\n\nBy Default we use `signtool.exe` which can be found only on Windows so if you are on another platform and want to cross-compile and sign you will need to use another tool like `osslsigncode`.",
"type": [
"string",
"null"
]
} }
}, },
"additionalProperties": false "additionalProperties": false

View File

@ -859,6 +859,20 @@ pub struct WindowsConfig {
pub wix: Option<WixConfig>, pub wix: Option<WixConfig>,
/// Configuration for the installer generated with NSIS. /// Configuration for the installer generated with NSIS.
pub nsis: Option<NsisConfig>, pub nsis: Option<NsisConfig>,
/// Specify a custom command to sign the binaries.
/// This command needs to have a `%1` in it which is just a placeholder for the binary path,
/// which we will detect and replace before calling the command.
///
/// Example:
/// ```text
/// sign-cli --arg1 --arg2 %1
/// ```
///
/// By Default we use `signtool.exe` which can be found only on Windows so
/// if you are on another platform and want to cross-compile and sign you will
/// need to use another tool like `osslsigncode`.
#[serde(alias = "sign-command")]
pub sign_command: Option<String>,
} }
impl Default for WindowsConfig { impl Default for WindowsConfig {
@ -873,6 +887,7 @@ impl Default for WindowsConfig {
allow_downgrades: true, allow_downgrades: true,
wix: None, wix: None,
nsis: None, nsis: None,
sign_command: None,
} }
} }
} }

View File

@ -44,7 +44,7 @@ dunce = "1"
[target."cfg(target_os = \"windows\")".dependencies] [target."cfg(target_os = \"windows\")".dependencies]
uuid = { version = "1", features = [ "v4", "v5" ] } uuid = { version = "1", features = [ "v4", "v5" ] }
bitness = "0.4" bitness = "0.4"
winreg = "0.52" windows-registry = "0.1.1"
glob = "0.3" glob = "0.3"
[target."cfg(target_os = \"windows\")".dependencies.windows-sys] [target."cfg(target_os = \"windows\")".dependencies.windows-sys]

View File

@ -63,8 +63,7 @@ pub fn bundle_project(settings: Settings) -> crate::Result<Vec<Bundle>> {
log::warn!("Cross-platform compilation is experimental and does not support all features. Please use a matching host system for full compatibility."); log::warn!("Cross-platform compilation is experimental and does not support all features. Please use a matching host system for full compatibility.");
} }
#[cfg(target_os = "windows")] if settings.can_sign() {
{
// Sign windows binaries before the bundling step in case neither wix and nsis bundles are enabled // Sign windows binaries before the bundling step in case neither wix and nsis bundles are enabled
for bin in settings.binaries() { for bin in settings.binaries() {
let bin_path = settings.binary_path(bin); let bin_path = settings.binary_path(bin);
@ -75,16 +74,24 @@ pub fn bundle_project(settings: Settings) -> crate::Result<Vec<Bundle>> {
for bin in settings.external_binaries() { for bin in settings.external_binaries() {
let path = bin?; let path = bin?;
let skip = std::env::var("TAURI_SKIP_SIDECAR_SIGNATURE_CHECK").map_or(false, |v| v == "true"); let skip = std::env::var("TAURI_SKIP_SIDECAR_SIGNATURE_CHECK").map_or(false, |v| v == "true");
if skip {
continue;
}
if !skip && windows::sign::verify(&path)? { #[cfg(windows)]
if windows::sign::verify(&path)? {
log::info!( log::info!(
"sidecar at \"{}\" already signed. Skipping...", "sidecar at \"{}\" already signed. Skipping...",
path.display() path.display()
) );
} else { continue;
}
windows::sign::try_sign(&path, &settings)?; windows::sign::try_sign(&path, &settings)?;
} }
} } else {
#[cfg(not(target_os = "windows"))]
log::warn!("Signing, by default, is only supported on Windows hosts, but you can specify a custom signing command in `bundler > windows > sign_command`, for now, skipping signing the installer...");
} }
for package_type in &package_types { for package_type in &package_types {

View File

@ -447,6 +447,20 @@ pub struct WindowsSettings {
/// ///
/// /// The default value of this flag is `true`. /// /// The default value of this flag is `true`.
pub allow_downgrades: bool, pub allow_downgrades: bool,
/// Specify a custom command to sign the binaries.
/// This command needs to have a `%1` in it which is just a placeholder for the binary path,
/// which we will detect and replace before calling the command.
///
/// Example:
/// ```text
/// sign-cli --arg1 --arg2 %1
/// ```
///
/// By Default we use `signtool.exe` which can be found only on Windows so
/// if you are on another platform and want to cross-compile and sign you will
/// need to use another tool like `osslsigncode`.
pub sign_command: Option<String>,
} }
impl Default for WindowsSettings { impl Default for WindowsSettings {
@ -462,6 +476,7 @@ impl Default for WindowsSettings {
webview_install_mode: Default::default(), webview_install_mode: Default::default(),
webview_fixed_runtime_path: None, webview_fixed_runtime_path: None,
allow_downgrades: true, allow_downgrades: true,
sign_command: None,
} }
} }
} }

View File

@ -6,7 +6,6 @@
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
pub mod msi; pub mod msi;
pub mod nsis; pub mod nsis;
#[cfg(target_os = "windows")]
pub mod sign; pub mod sign;
mod util; mod util;

View File

@ -798,7 +798,11 @@ pub fn build_wix_app_installer(
&msi_output_path, &msi_output_path,
)?; )?;
rename(&msi_output_path, &msi_path)?; rename(&msi_output_path, &msi_path)?;
if settings.can_sign() {
try_sign(&msi_path, settings)?; try_sign(&msi_path, settings)?;
}
output_paths.push(msi_path); output_paths.push(msi_path);
} }

View File

@ -2,8 +2,8 @@
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
#[cfg(target_os = "windows")]
use crate::bundle::windows::sign::{sign_command, try_sign}; use crate::bundle::windows::sign::{sign_command, try_sign};
use crate::{ use crate::{
bundle::{ bundle::{
common::CommandExt, common::CommandExt,
@ -67,6 +67,7 @@ pub fn bundle_project(settings: &Settings, updater: bool) -> crate::Result<Vec<P
let nsis_toolset_path = tauri_tools_path.join("NSIS"); let nsis_toolset_path = tauri_tools_path.join("NSIS");
if !nsis_toolset_path.exists() { if !nsis_toolset_path.exists() {
create_dir_all(&nsis_toolset_path)?;
get_and_extract_nsis(&nsis_toolset_path, &tauri_tools_path)?; get_and_extract_nsis(&nsis_toolset_path, &tauri_tools_path)?;
} else if NSIS_REQUIRED_FILES } else if NSIS_REQUIRED_FILES
.iter() .iter()
@ -114,12 +115,10 @@ fn get_and_extract_nsis(nsis_toolset_path: &Path, _tauri_tools_path: &Path) -> c
NSIS_TAURI_UTILS_SHA1, NSIS_TAURI_UTILS_SHA1,
HashAlgorithm::Sha1, HashAlgorithm::Sha1,
)?; )?;
write(
nsis_plugins let target_folder = nsis_plugins.join("x86-unicode");
.join("x86-unicode") create_dir_all(&target_folder)?;
.join("nsis_tauri_utils.dll"), write(target_folder.join("nsis_tauri_utils.dll"), data)?;
data,
)?;
Ok(()) Ok(())
} }
@ -163,9 +162,6 @@ fn build_nsis_app_installer(
log::info!("Target: {}", arch); log::info!("Target: {}", arch);
#[cfg(not(target_os = "windows"))]
log::info!("Code signing is currently only supported on Windows hosts, skipping...");
let output_path = settings.project_out_directory().join("nsis").join(arch); let output_path = settings.project_out_directory().join("nsis").join(arch);
if output_path.exists() { if output_path.exists() {
remove_dir_all(&output_path)?; remove_dir_all(&output_path)?;
@ -197,16 +193,9 @@ fn build_nsis_app_installer(
); );
data.insert("copyright", to_json(settings.copyright_string())); data.insert("copyright", to_json(settings.copyright_string()));
// Code signing is currently only supported on Windows hosts
#[cfg(target_os = "windows")]
if settings.can_sign() { if settings.can_sign() {
data.insert( let sign_cmd = format!("{:?}", sign_command("%1", &settings.sign_params())?);
"uninstaller_sign_cmd", data.insert("uninstaller_sign_cmd", to_json(sign_cmd));
to_json(format!(
"{:?}",
sign_command("%1", &settings.sign_params())?.0
)),
);
} }
let version = settings.version_string(); let version = settings.version_string();
@ -517,9 +506,12 @@ fn build_nsis_app_installer(
rename(nsis_output_path, &nsis_installer_path)?; rename(nsis_output_path, &nsis_installer_path)?;
// Code signing is currently only supported on Windows hosts if settings.can_sign() {
#[cfg(target_os = "windows")]
try_sign(&nsis_installer_path, settings)?; try_sign(&nsis_installer_path, settings)?;
} else {
#[cfg(not(target_os = "windows"))]
log::warn!("Signing, by default, is only supported on Windows hosts, but you can specify a custom signing command in `bundler > windows > sign_command`, for now, skipping signing the installer...");
}
Ok(vec![nsis_installer_path]) Ok(vec![nsis_installer_path])
} }

View File

@ -3,18 +3,45 @@
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
use crate::{ #[cfg(windows)]
bundle::{common::CommandExt, windows::util}, use crate::bundle::windows::util;
Settings, use crate::{bundle::common::CommandExt, Settings};
}; use anyhow::Context;
use std::{ #[cfg(windows)]
path::{Path, PathBuf}, use std::path::PathBuf;
process::Command, #[cfg(windows)]
}; use std::sync::OnceLock;
use winreg::{ use std::{path::Path, process::Command};
enums::{HKEY_LOCAL_MACHINE, KEY_READ, KEY_WOW64_32KEY},
RegKey, impl Settings {
}; pub(crate) fn can_sign(&self) -> bool {
self.windows().sign_command.is_some() || self.windows().certificate_thumbprint.is_some()
}
pub(crate) fn sign_params(&self) -> SignParams {
SignParams {
product_name: self.product_name().into(),
digest_algorithm: self
.windows()
.digest_algorithm
.as_ref()
.map(|algorithm| algorithm.to_string())
.unwrap_or_else(|| "sha256".to_string()),
certificate_thumbprint: self
.windows()
.certificate_thumbprint
.clone()
.unwrap_or_default(),
timestamp_url: self
.windows()
.timestamp_url
.as_ref()
.map(|url| url.to_string()),
tsp: self.windows().tsp,
sign_command: self.windows().sign_command.clone(),
}
}
}
pub struct SignParams { pub struct SignParams {
pub product_name: String, pub product_name: String,
@ -22,35 +49,34 @@ pub struct SignParams {
pub certificate_thumbprint: String, pub certificate_thumbprint: String,
pub timestamp_url: Option<String>, pub timestamp_url: Option<String>,
pub tsp: bool, pub tsp: bool,
pub sign_command: Option<String>,
} }
#[cfg(windows)]
fn signtool() -> Option<PathBuf> {
// sign code forked from https://github.com/forbjok/rust-codesign // sign code forked from https://github.com/forbjok/rust-codesign
fn locate_signtool() -> crate::Result<PathBuf> { static SIGN_TOOL: OnceLock<crate::Result<PathBuf>> = OnceLock::new();
SIGN_TOOL
.get_or_init(|| {
const INSTALLED_ROOTS_REGKEY_PATH: &str = r"SOFTWARE\Microsoft\Windows Kits\Installed Roots"; const INSTALLED_ROOTS_REGKEY_PATH: &str = r"SOFTWARE\Microsoft\Windows Kits\Installed Roots";
const KITS_ROOT_REGVALUE_NAME: &str = r"KitsRoot10"; 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 // Open 32-bit HKLM "Installed Roots" key
let installed_roots_key = RegKey::predef(HKEY_LOCAL_MACHINE) let installed_roots_key = windows_registry::LOCAL_MACHINE
.open_subkey_with_flags(installed_roots_key_path, KEY_READ | KEY_WOW64_32KEY) .open(INSTALLED_ROOTS_REGKEY_PATH)
.map_err(|_| crate::Error::OpenRegistry(INSTALLED_ROOTS_REGKEY_PATH.to_string()))?; .map_err(|_| crate::Error::OpenRegistry(INSTALLED_ROOTS_REGKEY_PATH.to_string()))?;
// Get the Windows SDK root path // Get the Windows SDK root path
let kits_root_10_path: String = installed_roots_key let kits_root_10_path: String = installed_roots_key
.get_value(KITS_ROOT_REGVALUE_NAME) .get_string(KITS_ROOT_REGVALUE_NAME)
.map_err(|_| crate::Error::GetRegistryValue(KITS_ROOT_REGVALUE_NAME.to_string()))?; .map_err(|_| crate::Error::GetRegistryValue(KITS_ROOT_REGVALUE_NAME.to_string()))?;
// Construct Windows SDK bin path // Construct Windows SDK bin path
let kits_root_10_bin_path = Path::new(&kits_root_10_path).join("bin"); let kits_root_10_bin_path = Path::new(&kits_root_10_path).join("bin");
let mut installed_kits: Vec<String> = installed_roots_key let mut installed_kits: Vec<String> = installed_roots_key
.enum_keys() .keys()
/* Report and ignore errors, pass on values. */ .map_err(|_| crate::Error::FailedToEnumerateRegKeys)?
.filter_map(|res| match res {
Ok(v) => Some(v),
Err(_) => None,
})
.collect(); .collect();
// Sort installed kits // Sort installed kits
@ -85,13 +111,17 @@ fn locate_signtool() -> crate::Result<PathBuf> {
} }
Err(crate::Error::SignToolNotFound) Err(crate::Error::SignToolNotFound)
})
.as_ref()
.ok()
.cloned()
} }
/// Check if binary is already signed. /// Check if binary is already signed.
/// Used to skip sidecar binaries that are already signed. /// Used to skip sidecar binaries that are already signed.
#[cfg(windows)]
pub fn verify(path: &Path) -> crate::Result<bool> { pub fn verify(path: &Path) -> crate::Result<bool> {
// Construct SignTool command let signtool = signtool().ok_or(crate::Error::SignToolNotFound)?;
let signtool = locate_signtool()?;
let mut cmd = Command::new(signtool); let mut cmd = Command::new(signtool);
cmd.arg("verify"); cmd.arg("verify");
@ -101,9 +131,31 @@ pub fn verify(path: &Path) -> crate::Result<bool> {
Ok(cmd.status()?.success()) Ok(cmd.status()?.success())
} }
pub fn sign_command(path: &str, params: &SignParams) -> crate::Result<(Command, PathBuf)> { pub fn sign_command_custom<P: AsRef<Path>>(path: P, command: &str) -> crate::Result<Command> {
// Construct SignTool command let path = path.as_ref();
let signtool = locate_signtool()?;
let mut args = command.trim().split(' ');
let bin = args
.next()
.context("custom signing command doesn't contain a bin?")?;
let mut cmd = Command::new(bin);
for arg in args {
if arg == "%1" {
cmd.arg(path);
} else {
cmd.arg(arg);
}
}
Ok(cmd)
}
#[cfg(windows)]
pub fn sign_command_default<P: AsRef<Path>>(
path: P,
params: &SignParams,
) -> crate::Result<Command> {
let signtool = signtool().ok_or(crate::Error::SignToolNotFound)?;
let mut cmd = Command::new(&signtool); let mut cmd = Command::new(&signtool);
cmd.arg("sign"); cmd.arg("sign");
@ -120,17 +172,46 @@ pub fn sign_command(path: &str, params: &SignParams) -> crate::Result<(Command,
} }
} }
cmd.arg(path); cmd.arg(path.as_ref());
Ok((cmd, signtool)) Ok(cmd)
} }
pub fn sign<P: AsRef<Path>>(path: P, params: &SignParams) -> crate::Result<()> { pub fn sign_command<P: AsRef<Path>>(path: P, params: &SignParams) -> crate::Result<Command> {
let path_str = path.as_ref().to_str().unwrap(); match &params.sign_command {
Some(custom_command) => sign_command_custom(path, custom_command),
#[cfg(windows)]
None => sign_command_default(path, params),
log::info!(action = "Signing"; "{} with identity \"{}\"", path_str, params.certificate_thumbprint); // should not be reachable
#[cfg(not(windows))]
None => Ok(Command::new("")),
}
}
let (mut cmd, signtool) = sign_command(path_str, params)?; pub fn sign_custom<P: AsRef<Path>>(path: P, custom_command: &str) -> crate::Result<()> {
let path = path.as_ref();
log::info!(action = "Signing";"{} with a custom signing command", tauri_utils::display_path(path));
let mut cmd = sign_command_custom(path, custom_command)?;
let output = cmd.output_ok()?;
let stdout = String::from_utf8_lossy(output.stdout.as_slice()).into_owned();
log::info!("{:?}", stdout);
Ok(())
}
#[cfg(windows)]
pub fn sign_default<P: AsRef<Path>>(path: P, params: &SignParams) -> crate::Result<()> {
let signtool = signtool().ok_or(crate::Error::SignToolNotFound)?;
let path = path.as_ref();
log::info!(action = "Signing"; "{} with identity \"{}\"", tauri_utils::display_path(path), params.certificate_thumbprint);
let mut cmd = sign_command_default(path, params)?;
log::debug!("Running signtool {:?}", signtool); log::debug!("Running signtool {:?}", signtool);
// Execute SignTool command // Execute SignTool command
@ -142,31 +223,15 @@ pub fn sign<P: AsRef<Path>>(path: P, params: &SignParams) -> crate::Result<()> {
Ok(()) Ok(())
} }
impl Settings { pub fn sign<P: AsRef<Path>>(path: P, params: &SignParams) -> crate::Result<()> {
pub(crate) fn can_sign(&self) -> bool { match &params.sign_command {
self.windows().certificate_thumbprint.is_some() Some(custom_command) => sign_custom(path, custom_command),
} #[cfg(windows)]
pub(crate) fn sign_params(&self) -> SignParams { None => sign_default(path, params),
SignParams { // should not be reachable, as user should either use Windows
product_name: self.product_name().into(), // or specify a custom sign_command but we succeed anyways
digest_algorithm: self #[cfg(not(windows))]
.windows() None => Ok(()),
.digest_algorithm
.as_ref()
.map(|algorithm| algorithm.to_string())
.unwrap_or_else(|| "sha256".to_string()),
certificate_thumbprint: self
.windows()
.certificate_thumbprint
.clone()
.unwrap_or_default(),
timestamp_url: self
.windows()
.timestamp_url
.as_ref()
.map(|url| url.to_string()),
tsp: self.windows().tsp,
}
} }
} }

View File

@ -94,6 +94,9 @@ pub enum Error {
/// Failed to get registry value. /// Failed to get registry value.
#[error("failed to get {0} value on registry")] #[error("failed to get {0} value on registry")]
GetRegistryValue(String), GetRegistryValue(String),
/// Failed to enumerate registry keys.
#[error("failed to enumerate registry keys")]
FailedToEnumerateRegKeys,
/// Unsupported OS bitness. /// Unsupported OS bitness.
#[error("unsupported OS bitness")] #[error("unsupported OS bitness")]
UnsupportedBitness, UnsupportedBitness,

22
tooling/cli/Cargo.lock generated
View File

@ -4874,8 +4874,8 @@ dependencies = [
"ureq", "ureq",
"uuid", "uuid",
"walkdir", "walkdir",
"windows-registry",
"windows-sys 0.52.0", "windows-sys 0.52.0",
"winreg 0.52.0",
"zip", "zip",
] ]
@ -5952,6 +5952,16 @@ dependencies = [
"syn 2.0.52", "syn 2.0.52",
] ]
[[package]]
name = "windows-registry"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f721bc2e55efb506a1a395a545cb76c2481fb023d33b51f0050e7888716281cf"
dependencies = [
"windows-result",
"windows-targets 0.52.5",
]
[[package]] [[package]]
name = "windows-result" name = "windows-result"
version = "0.1.1" version = "0.1.1"
@ -6147,16 +6157,6 @@ dependencies = [
"windows-sys 0.48.0", "windows-sys 0.48.0",
] ]
[[package]]
name = "winreg"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a277a57398d4bfa075df44f501a17cfdf8542d224f0d36095a2adc7aee4ef0a5"
dependencies = [
"cfg-if",
"windows-sys 0.48.0",
]
[[package]] [[package]]
name = "winsafe" name = "winsafe"
version = "0.0.19" version = "0.0.19"

View File

@ -112,6 +112,7 @@
"certificateThumbprint": null, "certificateThumbprint": null,
"digestAlgorithm": null, "digestAlgorithm": null,
"nsis": null, "nsis": null,
"signCommand": null,
"timestampUrl": null, "timestampUrl": null,
"tsp": false, "tsp": false,
"webviewFixedRuntimePath": null, "webviewFixedRuntimePath": null,
@ -1619,6 +1620,7 @@
"certificateThumbprint": null, "certificateThumbprint": null,
"digestAlgorithm": null, "digestAlgorithm": null,
"nsis": null, "nsis": null,
"signCommand": null,
"timestampUrl": null, "timestampUrl": null,
"tsp": false, "tsp": false,
"webviewFixedRuntimePath": null, "webviewFixedRuntimePath": null,
@ -1977,6 +1979,13 @@
"type": "null" "type": "null"
} }
] ]
},
"signCommand": {
"description": "Specify a custom command to sign the binaries. This command needs to have a `%1` in it which is just a placeholder for the binary path, which we will detect and replace before calling the command.\n\nExample: ```text sign-cli --arg1 --arg2 %1 ```\n\nBy Default we use `signtool.exe` which can be found only on Windows so if you are on another platform and want to cross-compile and sign you will need to use another tool like `osslsigncode`.",
"type": [
"string",
"null"
]
} }
}, },
"additionalProperties": false "additionalProperties": false

View File

@ -1420,6 +1420,7 @@ fn tauri_config_to_bundle_settings(
webview_install_mode: config.windows.webview_install_mode, webview_install_mode: config.windows.webview_install_mode,
webview_fixed_runtime_path: config.windows.webview_fixed_runtime_path, webview_fixed_runtime_path: config.windows.webview_fixed_runtime_path,
allow_downgrades: config.windows.allow_downgrades, allow_downgrades: config.windows.allow_downgrades,
sign_command: config.windows.sign_command,
}, },
license: config.license.or_else(|| { license: config.license.or_else(|| {
settings settings