From 32d81661eedb7524b36f4c422606fa1e93ac917b Mon Sep 17 00:00:00 2001 From: Amr Bashir Date: Thu, 14 May 2026 18:20:22 +0300 Subject: [PATCH] feat: add Windows VC runtime linking and bundling options (#15372) --- .changes/bundle-vc-runtime.md | 7 + .changes/static-vc-runtime.md | 6 + .changes/tauri-build-static-vc-runtime.md | 5 + crates/tauri-build/src/lib.rs | 112 +++++++++++- crates/tauri-bundler/Cargo.toml | 4 +- crates/tauri-bundler/src/bundle.rs | 2 + crates/tauri-bundler/src/bundle/settings.rs | 8 + .../tauri-bundler/src/bundle/windows/mod.rs | 2 +- .../src/bundle/windows/msi/mod.rs | 17 +- .../src/bundle/windows/nsis/mod.rs | 18 +- .../tauri-bundler/src/bundle/windows/util.rs | 169 +++++++++++++++++- .../src/bundle/windows}/vswhere.exe | Bin crates/tauri-bundler/src/error.rs | 4 +- crates/tauri-cli/ENVIRONMENT_VARIABLES.md | 2 + crates/tauri-cli/config.schema.json | 35 +++- crates/tauri-cli/src/info/env_system.rs | 14 +- crates/tauri-cli/src/interface/rust.rs | 1 + .../tauri-cli/src/interface/rust/desktop.rs | 4 - .../schemas/config.schema.json | 35 +++- crates/tauri-utils/src/config.rs | 57 +++++- 20 files changed, 474 insertions(+), 28 deletions(-) create mode 100644 .changes/bundle-vc-runtime.md create mode 100644 .changes/static-vc-runtime.md create mode 100644 .changes/tauri-build-static-vc-runtime.md rename crates/{tauri-cli/scripts => tauri-bundler/src/bundle/windows}/vswhere.exe (100%) diff --git a/.changes/bundle-vc-runtime.md b/.changes/bundle-vc-runtime.md new file mode 100644 index 000000000000..15d7fa313c29 --- /dev/null +++ b/.changes/bundle-vc-runtime.md @@ -0,0 +1,7 @@ +--- +"tauri-bundler": "minor:feat" +"tauri-cli": "minor:feat" +"tauri-utils": "minor:feat" +--- + +Added `bundle.windows.bundleVCRuntime` to copy the Visual C++ runtime DLLs into Windows MSI and NSIS installers. The bundler locates the runtime through `VCTOOLS_REDIST_DIR` or the bundled `vswhere.exe`. diff --git a/.changes/static-vc-runtime.md b/.changes/static-vc-runtime.md new file mode 100644 index 000000000000..22f770e27167 --- /dev/null +++ b/.changes/static-vc-runtime.md @@ -0,0 +1,6 @@ +--- +"tauri-cli": "minor:feat" +"tauri-utils": "minor:feat" +--- + +Added `build.windows.staticVCRuntime` to control MSVC static runtime linking. The `STATIC_VCRUNTIME` environment variable is now deprecated and emits a migration warning when used. diff --git a/.changes/tauri-build-static-vc-runtime.md b/.changes/tauri-build-static-vc-runtime.md new file mode 100644 index 000000000000..98f0d0517137 --- /dev/null +++ b/.changes/tauri-build-static-vc-runtime.md @@ -0,0 +1,5 @@ +--- +"tauri-build": "minor:feat" +--- + +Added `tauri_build::WindowsAttributes::static_vc_runtime` to control MSVC static runtime linking from build scripts. diff --git a/crates/tauri-build/src/lib.rs b/crates/tauri-build/src/lib.rs index 6c171a3005f9..db62722ef275 100644 --- a/crates/tauri-build/src/lib.rs +++ b/crates/tauri-build/src/lib.rs @@ -215,6 +215,8 @@ fn cfg_alias(alias: &str, has_feature: bool) { #[derive(Debug)] pub struct WindowsAttributes { window_icon_path: Option, + /// Whether to statically link the Visual C++ runtime into the application binary on Windows MSVC targets + static_vc_runtime: Option, /// A string containing an [application manifest] to be included with the application on Windows. /// /// Defaults to: @@ -257,6 +259,7 @@ impl WindowsAttributes { pub fn new() -> Self { Self { window_icon_path: Default::default(), + static_vc_runtime: None, app_manifest: Some(include_str!("windows-app-manifest.xml").into()), append_rc_content: Vec::new(), } @@ -268,6 +271,7 @@ impl WindowsAttributes { Self { app_manifest: None, window_icon_path: Default::default(), + static_vc_runtime: None, append_rc_content: Vec::new(), } } @@ -282,6 +286,15 @@ impl WindowsAttributes { self } + /// Sets whether to statically link the Visual C++ runtime into the application binary on Windows MSVC targets. + /// + /// If unset, this is read from `build > windows > staticVCRuntime` in the Tauri configuration. + #[must_use] + pub fn static_vc_runtime(mut self, static_vc_runtime: bool) -> Self { + self.static_vc_runtime.replace(static_vc_runtime); + self + } + /// Sets the [application manifest] to be included with the application on Windows. /// /// Defaults to: @@ -489,6 +502,7 @@ pub fn try_build(attributes: Attributes) -> Result<()> { json_patch::merge(&mut config, &merge_config); } let config: Config = serde_json::from_value(config)?; + let static_vc_runtime = should_static_link_vc_runtime(&config, &attributes); let s = config.identifier.split('.'); let last = s.clone().count() - 1; @@ -705,7 +719,7 @@ pub fn try_build(attributes: Attributes) -> Result<()> { } } } - "msvc" if env::var_os("STATIC_VCRUNTIME").is_some_and(|v| v == "true") => { + "msvc" if static_vc_runtime => { static_vcruntime::build(); } _ => (), @@ -726,6 +740,20 @@ fn to_winres_version(v: &semver::Version) -> u64 { (v.major << 48) | (v.minor << 32) | (v.patch << 16) | build } +fn should_static_link_vc_runtime(config: &Config, attributes: &Attributes) -> bool { + if let Some(value) = env::var_os("STATIC_VCRUNTIME") { + println!( + "cargo:warning=STATIC_VCRUNTIME is deprecated; use build.windows.staticVCRuntime in tauri.conf.json or tauri_build::WindowsAttributes::static_vc_runtime instead." + ); + value != "false" + } else { + attributes + .windows_attributes + .static_vc_runtime + .unwrap_or(config.build.windows.static_vc_runtime) + } +} + #[cfg(test)] mod tests { use semver::Version; @@ -769,4 +797,86 @@ mod tests { (1 << 48) | (2 << 32) | (3 << 16) ); } + + #[test] + fn static_vc_runtime_chain() { + // 1. Nothing is set, should default to true + let config = tauri_utils::config::Config::default(); + let attributes = crate::Attributes::new(); + assert!(crate::should_static_link_vc_runtime(&config, &attributes)); + + // 2. Set to anything but "false" in env, should be true + std::env::set_var("STATIC_VCRUNTIME", "qweqe"); + let config = tauri_utils::config::Config::default(); + let attributes = crate::Attributes::new(); + assert!(crate::should_static_link_vc_runtime(&config, &attributes)); + std::env::remove_var("STATIC_VCRUNTIME"); + + // 3. Set to "false" in env, should be false + std::env::set_var("STATIC_VCRUNTIME", "false"); + let config = tauri_utils::config::Config::default(); + let attributes = crate::Attributes::new(); + assert!(!crate::should_static_link_vc_runtime(&config, &attributes)); + std::env::remove_var("STATIC_VCRUNTIME"); + + // 4. Set to true in attributes, should be true + let config = tauri_utils::config::Config::default(); + let attributes = crate::Attributes::new() + .windows_attributes(crate::WindowsAttributes::new().static_vc_runtime(true)); + assert!(crate::should_static_link_vc_runtime(&config, &attributes)); + + // 5. Set to false in attributes, should be false + let config = tauri_utils::config::Config::default(); + let attributes = crate::Attributes::new() + .windows_attributes(crate::WindowsAttributes::new().static_vc_runtime(false)); + assert!(!crate::should_static_link_vc_runtime(&config, &attributes)); + + // 6. Set to true in config, should be true + let config = tauri_utils::config::Config { + build: tauri_utils::config::BuildConfig { + windows: tauri_utils::config::WindowsBuildConfig { + static_vc_runtime: true, + }, + ..Default::default() + }, + ..Default::default() + }; + let attributes = crate::Attributes::new(); + assert!(crate::should_static_link_vc_runtime(&config, &attributes)); + + // 7. Set to false in config, should be false + let config = tauri_utils::config::Config { + build: tauri_utils::config::BuildConfig { + windows: tauri_utils::config::WindowsBuildConfig { + static_vc_runtime: false, + }, + ..Default::default() + }, + ..Default::default() + }; + let attributes = crate::Attributes::new(); + assert!(!crate::should_static_link_vc_runtime(&config, &attributes)); + + // 8. Set to true in config and false in attributes, should be false because attributes takes precedence over config + let config = tauri_utils::config::Config { + build: tauri_utils::config::BuildConfig { + windows: tauri_utils::config::WindowsBuildConfig { + static_vc_runtime: true, + }, + ..Default::default() + }, + ..Default::default() + }; + let attributes = crate::Attributes::new() + .windows_attributes(crate::WindowsAttributes::new().static_vc_runtime(false)); + assert!(!crate::should_static_link_vc_runtime(&config, &attributes)); + + // 9. Set to false in env and true in attributes, should be false because env takes precedence over attributes + std::env::set_var("STATIC_VCRUNTIME", "false"); + let config = tauri_utils::config::Config::default(); + let attributes = crate::Attributes::new() + .windows_attributes(crate::WindowsAttributes::new().static_vc_runtime(true)); + assert!(!crate::should_static_link_vc_runtime(&config, &attributes)); + std::env::remove_var("STATIC_VCRUNTIME"); + } } diff --git a/crates/tauri-bundler/Cargo.toml b/crates/tauri-bundler/Cargo.toml index 5dc1cdb117b3..ba4b38a684e9 100644 --- a/crates/tauri-bundler/Cargo.toml +++ b/crates/tauri-bundler/Cargo.toml @@ -50,7 +50,6 @@ plist = "1" [target."cfg(target_os = \"windows\")".dependencies] bitness = "0.4" windows-registry = "0.5" -glob = "0.3" [target."cfg(target_os = \"windows\")".dependencies.windows-sys] version = "0.60" @@ -67,6 +66,9 @@ ar = "0.9" md5 = "0.8" rpm = { version = "0.16", features = ["bzip2-compression"] } +[target."cfg(any(target_os = \"windows\", target_os = \"macos\", target_os = \"linux\"))".dependencies] +glob = "0.3" + [target."cfg(unix)".dependencies] which = "8" diff --git a/crates/tauri-bundler/src/bundle.rs b/crates/tauri-bundler/src/bundle.rs index ab0a45032f93..bda981e6fc4f 100644 --- a/crates/tauri-bundler/src/bundle.rs +++ b/crates/tauri-bundler/src/bundle.rs @@ -14,6 +14,8 @@ mod settings; mod updater_bundle; mod windows; +pub use windows::vswhere_path; + use tauri_utils::{display_path, platform::Target as TargetPlatform}; const BUNDLE_VAR_TOKEN: &[u8] = b"__TAURI_BUNDLE_TYPE_VAR_UNK"; diff --git a/crates/tauri-bundler/src/bundle/settings.rs b/crates/tauri-bundler/src/bundle/settings.rs index 7580a0adbc95..62e6557863a1 100644 --- a/crates/tauri-bundler/src/bundle/settings.rs +++ b/crates/tauri-bundler/src/bundle/settings.rs @@ -603,6 +603,13 @@ pub struct WindowsSettings { /// if the user's WebView2 is older than this version, /// the installer will try to trigger a WebView2 update. pub minimum_webview2_version: Option, + /// Whether to bundle the Visual C++ runtime DLLs alongside the application. + /// + /// This can be particularly useful when the application includes sidecars or DLLs that do not + /// statically link the Visual C++ runtime and require the runtime DLLs at runtime, and users + /// should not be required to install the Visual C++ Redistributable. This can also be useful + /// when `static_vc_runtime` is set to `false`. + pub bundle_vc_runtime: bool, } impl WindowsSettings { @@ -629,6 +636,7 @@ mod _default { allow_downgrades: true, sign_command: None, minimum_webview2_version: None, + bundle_vc_runtime: false, } } } diff --git a/crates/tauri-bundler/src/bundle/windows/mod.rs b/crates/tauri-bundler/src/bundle/windows/mod.rs index b92fb5f56216..2d59a28f7d00 100644 --- a/crates/tauri-bundler/src/bundle/windows/mod.rs +++ b/crates/tauri-bundler/src/bundle/windows/mod.rs @@ -11,6 +11,6 @@ pub mod sign; mod util; pub use util::{ - NSIS_OUTPUT_FOLDER_NAME, NSIS_UPDATER_OUTPUT_FOLDER_NAME, WIX_OUTPUT_FOLDER_NAME, + vswhere_path, NSIS_OUTPUT_FOLDER_NAME, NSIS_UPDATER_OUTPUT_FOLDER_NAME, WIX_OUTPUT_FOLDER_NAME, WIX_UPDATER_OUTPUT_FOLDER_NAME, }; diff --git a/crates/tauri-bundler/src/bundle/windows/msi/mod.rs b/crates/tauri-bundler/src/bundle/windows/msi/mod.rs index ad0533601686..2b7452291f40 100644 --- a/crates/tauri-bundler/src/bundle/windows/msi/mod.rs +++ b/crates/tauri-bundler/src/bundle/windows/msi/mod.rs @@ -9,7 +9,7 @@ use crate::{ windows::{ sign::{should_sign, try_sign}, util::{ - download_webview2_bootstrapper, download_webview2_offline_installer, + download_webview2_bootstrapper, download_webview2_offline_installer, vc_runtime_dlls, WIX_OUTPUT_FOLDER_NAME, WIX_UPDATER_OUTPUT_FOLDER_NAME, }, }, @@ -1066,6 +1066,21 @@ fn generate_resource_data(settings: &Settings) -> crate::Result { let mut dlls = Vec::new(); + if settings.windows().bundle_vc_runtime { + for dll in vc_runtime_dlls(settings.binary_arch())? { + let resource_path = dunce::simplified(&dll); + if added_resources.contains(&resource_path.to_path_buf()) { + continue; + } + added_resources.push(resource_path.to_path_buf()); + dlls.push(ResourceFile { + id: format!("I{}", Uuid::new_v4().as_simple()), + guid: Uuid::new_v4().to_string(), + path: resource_path.to_path_buf(), + }); + } + } + // TODO: The bundler should not include all DLLs it finds. Instead it should only include WebView2Loader.dll if present and leave the rest to the resources config. let out_dir = settings.project_out_directory(); for dll in glob::glob( diff --git a/crates/tauri-bundler/src/bundle/windows/nsis/mod.rs b/crates/tauri-bundler/src/bundle/windows/nsis/mod.rs index 0ad87c188cab..b78334426c6a 100644 --- a/crates/tauri-bundler/src/bundle/windows/nsis/mod.rs +++ b/crates/tauri-bundler/src/bundle/windows/nsis/mod.rs @@ -8,7 +8,7 @@ use crate::{ windows::{ sign::{should_sign, sign_command, try_sign}, util::{ - download_webview2_bootstrapper, download_webview2_offline_installer, + download_webview2_bootstrapper, download_webview2_offline_installer, vc_runtime_dlls, NSIS_OUTPUT_FOLDER_NAME, NSIS_UPDATER_OUTPUT_FOLDER_NAME, }, }, @@ -774,6 +774,22 @@ fn generate_resource_data(settings: &Settings) -> crate::Result { } } + if settings.windows().bundle_vc_runtime { + for dll in vc_runtime_dlls(settings.binary_arch())? { + let dll = dunce::simplified(&dll).to_path_buf(); + if added_resources.contains(&dll) { + continue; + } + let target = PathBuf::from( + dll + .file_name() + .expect("failed to extract Visual C++ runtime DLL filename"), + ); + added_resources.push(dll.clone()); + resources.insert(dll, (PathBuf::new(), target)); + } + } + for resource in settings.resource_files().iter() { let resource = resource?; diff --git a/crates/tauri-bundler/src/bundle/windows/util.rs b/crates/tauri-bundler/src/bundle/windows/util.rs index e716ad7bbef5..4e606f04e090 100644 --- a/crates/tauri-bundler/src/bundle/windows/util.rs +++ b/crates/tauri-bundler/src/bundle/windows/util.rs @@ -2,12 +2,16 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT +#[cfg(windows)] +use std::process::Command; use std::{ - fs::create_dir_all, + fs, + io::Write, path::{Path, PathBuf}, }; use ureq::ResponseExt; +use crate::bundle::settings::Arch; use crate::utils::http_utils::{base_ureq_agent, download}; pub const WEBVIEW2_BOOTSTRAPPER_URL: &str = "https://go.microsoft.com/fwlink/p/?LinkId=2124703"; @@ -22,6 +26,11 @@ 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"; +const VSWHERE: &[u8] = include_bytes!("vswhere.exe"); +const VCTOOLS_REDIST_DIR_ENV_VAR: &str = "VCTOOLS_REDIST_DIR"; +#[cfg(windows)] +const VC_REDIST_COMPONENT: &str = "Microsoft.VisualStudio.Component.VC.Redist.14.Latest"; + pub fn webview2_guid_path(url: &str) -> crate::Result<(String, String)> { let agent = base_ureq_agent(); let response = agent.head(url).call().map_err(Box::new)?; @@ -57,12 +66,168 @@ pub fn download_webview2_offline_installer(base_path: &Path, arch: &str) -> crat let dir_path = base_path.join(guid); let file_path = dir_path.join(filename); if !file_path.exists() { - create_dir_all(dir_path)?; + fs::create_dir_all(dir_path)?; std::fs::write(&file_path, download(url)?)?; } Ok(file_path) } +/// Finds the Visual C++ runtime DLLs for the given architecture. +pub fn vc_runtime_dlls(arch: Arch) -> crate::Result> { + let arch = vc_runtime_arch(arch)?; + let redist_dir = vc_redist_dir()?; + let runtime_dir = vc_runtime_dir(&redist_dir, arch)?; + + let dlls = glob::glob(&glob_path(&runtime_dir, "*.dll"))?.collect::, _>>()?; + if dlls.is_empty() { + return Err(crate::Error::GenericError(format!( + "no Visual C++ runtime DLLs found in `{}`", + runtime_dir.display() + ))); + } + + Ok(dlls) +} + +#[inline(always)] +fn vc_runtime_arch(arch: Arch) -> crate::Result<&'static str> { + match arch { + Arch::X86_64 => Ok("x64"), + Arch::X86 => Ok("x86"), + Arch::AArch64 => Ok("arm64"), + _ => Err(crate::Error::GenericError( + "bundling the Visual C++ runtime is only supported for Windows x86, x64 and arm64 targets" + .into(), + )), + } +} + +#[cfg(windows)] +fn visual_studio_dir() -> crate::Result { + let Some(vswhere) = vswhere_path() else { + return Err(crate::Error::GenericError( + "failed to prepare bundled vswhere.exe".into(), + )); + }; + + let output = Command::new(vswhere) + .args([ + "-latest", + "-prerelease", + "-products", + "*", + "-requires", + VC_REDIST_COMPONENT, + "-property", + "installationPath", + "-format", + "value", + "-utf8", + ]) + .output()?; + + if !output.status.success() { + return Err(crate::Error::GenericError(format!( + "failed to locate Visual Studio with the {VC_REDIST_COMPONENT} component" + ))); + } + + let stdout = String::from_utf8_lossy(&output.stdout); + let Some(vs_dir) = stdout.lines().map(str::trim).find(|line| !line.is_empty()) else { + return Err(crate::Error::GenericError(format!( + "failed to locate Visual Studio with the {VC_REDIST_COMPONENT} component" + ))); + }; + + Ok(PathBuf::from(vs_dir)) +} + +fn vc_redist_dir() -> crate::Result { + if let Ok(redist_dir) = std::env::var(VCTOOLS_REDIST_DIR_ENV_VAR) { + return Ok(PathBuf::from(redist_dir)); + } + + #[cfg(windows)] + { + let vs_dir = visual_studio_dir()?; + Ok(vs_dir.join("VC/Redist/MSVC")) + } + + #[cfg(not(windows))] + { + Err(crate::Error::GenericError(format!( + "failed to find Visual C++ runtime redist directory; set {VCTOOLS_REDIST_DIR_ENV_VAR} when bundling the Visual C++ runtime from non-Windows hosts" + ))) + } +} + +fn vc_runtime_dir(redist_dir: &Path, arch: &str) -> crate::Result { + let Some(latest_version_dir) = latest_vc_redist_version_dir(redist_dir)? else { + return Err(crate::Error::GenericError(format!( + "failed to find Visual C++ runtime versions in `{}`", + redist_dir.display() + ))); + }; + + let arch_dir = latest_version_dir.join(arch); + let Some(runtime_dir) = glob::glob(&glob_path(&arch_dir, "Microsoft.VC*.CRT"))? + .filter_map(Result::ok) + .find(|path| path.is_dir()) + else { + return Err(crate::Error::GenericError(format!( + "failed to find Visual C++ runtime directory for `{arch}` in `{}`", + arch_dir.display() + ))); + }; + + Ok(runtime_dir) +} + +fn latest_vc_redist_version_dir(redist_dir: &Path) -> crate::Result> { + let dir = fs::read_dir(redist_dir)? + .flatten() + .map(|entry| entry.path()) + .filter(|path| path.is_dir()) + .filter_map(|path| { + let version = path + .file_name()? + .to_str()? + .parse::() + .ok()?; + Some((version, path)) + }) + .max_by(|(a, _), (b, _)| a.cmp(b)) + .map(|(_, path)| path); + Ok(dir) +} + +/// Builds a glob pattern from a literal base path and an unescaped glob suffix. +/// +/// The base path is escaped so Visual Studio paths containing glob metacharacters are treated as +/// literal directories, while `pattern` remains active glob syntax. +fn glob_path(path: &Path, pattern: &str) -> String { + PathBuf::from(glob::Pattern::escape(&path.to_string_lossy())) + .join(pattern) + .to_string_lossy() + .into_owned() +} + +/// Returns the bundled `vswhere.exe` path. +/// +/// The executable is written to a temporary file so callers do not depend on a system-installed +/// `vswhere.exe`. +pub fn vswhere_path() -> Option { + let mut vswhere = std::env::temp_dir(); + vswhere.push("vswhere.exe"); + + if !vswhere.exists() { + let mut file = std::fs::File::create(&vswhere).ok()?; + file.write_all(VSWHERE).ok()?; + } + + Some(vswhere) +} + #[cfg(target_os = "windows")] pub fn processor_architecture<'a>() -> Option<&'a str> { use windows_sys::Win32::System::SystemInformation::{ diff --git a/crates/tauri-cli/scripts/vswhere.exe b/crates/tauri-bundler/src/bundle/windows/vswhere.exe similarity index 100% rename from crates/tauri-cli/scripts/vswhere.exe rename to crates/tauri-bundler/src/bundle/windows/vswhere.exe diff --git a/crates/tauri-bundler/src/error.rs b/crates/tauri-bundler/src/error.rs index 40547a3eea39..211b5148dcd9 100644 --- a/crates/tauri-bundler/src/error.rs +++ b/crates/tauri-bundler/src/error.rs @@ -79,11 +79,11 @@ pub enum Error { #[error("`{0}`")] HttpError(#[from] Box), /// Invalid glob pattern. - #[cfg(windows)] + #[cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))] #[error("{0}")] GlobPattern(#[from] glob::PatternError), /// Failed to use glob pattern. - #[cfg(windows)] + #[cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))] #[error("`{0}`")] Glob(#[from] glob::GlobError), /// Failed to parse the URL diff --git a/crates/tauri-cli/ENVIRONMENT_VARIABLES.md b/crates/tauri-cli/ENVIRONMENT_VARIABLES.md index 5a8a18100a18..f6f400ff7678 100644 --- a/crates/tauri-cli/ENVIRONMENT_VARIABLES.md +++ b/crates/tauri-cli/ENVIRONMENT_VARIABLES.md @@ -24,6 +24,8 @@ These environment variables are inputs to the CLI which may have an equivalent C - `TAURI_SIGNING_RPM_KEY` — The private GPG key used to sign the RPM bundle, exported to its ASCII-armored format. - `TAURI_SIGNING_RPM_KEY_PASSPHRASE` — The GPG key passphrase for `TAURI_SIGNING_RPM_KEY`, if needed. - `TAURI_WINDOWS_SIGNTOOL_PATH` — Specify a path to `signtool.exe` used for code signing the application on Windows. +- `STATIC_VCRUNTIME` — Deprecated. Set `build > windows > staticVCRuntime` in `tauri.conf.json` instead. +- `VCTOOLS_REDIST_DIR` — Override the Visual C++ redistributable root directory used when `bundle > windows > bundleVCRuntime` is enabled. If unset, Tauri uses its bundled `vswhere.exe` to locate Visual Studio and derive this directory. - `APPLE_CERTIFICATE` — Base64 encoded of the `.p12` certificate for code signing. To get this value, run `openssl base64 -in MyCertificate.p12 -out MyCertificate-base64.txt`. - `APPLE_CERTIFICATE_PASSWORD` — The password you used to export the certificate. - `APPLE_ID` — The Apple ID used to notarize the application. If this environment variable is provided, `APPLE_PASSWORD` and `APPLE_TEAM_ID` must also be set. Alternatively, `APPLE_API_KEY` and `APPLE_API_ISSUER` can be used to authenticate. diff --git a/crates/tauri-cli/config.schema.json b/crates/tauri-cli/config.schema.json index dbbd5dff137e..64a7109d95cc 100644 --- a/crates/tauri-cli/config.schema.json +++ b/crates/tauri-cli/config.schema.json @@ -71,7 +71,10 @@ "description": "The build configuration.", "default": { "additionalWatchFolders": [], - "removeUnusedCommands": false + "removeUnusedCommands": false, + "windows": { + "staticVCRuntime": true + } }, "allOf": [ { @@ -129,6 +132,7 @@ "useLocalToolsDir": false, "windows": { "allowDowngrades": true, + "bundleVCRuntime": false, "certificateThumbprint": null, "digestAlgorithm": null, "minimumWebview2Version": null, @@ -1971,6 +1975,17 @@ "items": { "type": "string" } + }, + "windows": { + "description": "Windows-specific build configuration.", + "default": { + "staticVCRuntime": true + }, + "allOf": [ + { + "$ref": "#/definitions/WindowsBuildConfig" + } + ] } }, "additionalProperties": false @@ -2098,6 +2113,18 @@ } ] }, + "WindowsBuildConfig": { + "description": "Windows-specific build configuration.", + "type": "object", + "properties": { + "staticVCRuntime": { + "description": "Whether to statically link the Visual C++ runtime into the application binary on Windows MSVC targets.", + "default": true, + "type": "boolean" + } + }, + "additionalProperties": false + }, "BundleConfig": { "description": "Configuration for tauri-bundler.\n\n See more: ", "type": "object", @@ -2229,6 +2256,7 @@ "description": "Configuration for the Windows bundles.", "default": { "allowDowngrades": true, + "bundleVCRuntime": false, "certificateThumbprint": null, "digestAlgorithm": null, "minimumWebview2Version": null, @@ -2749,6 +2777,11 @@ "type": "null" } ] + }, + "bundleVCRuntime": { + "description": "Whether to bundle the Visual C++ runtime DLLs alongside the application.\n\n This can be particularly useful when your application includes sidecars or DLLs that do\n not statically link the Visual C++ runtime and require the runtime DLLs at runtime, and\n you do not want to require users to install the Visual C++ Redistributable. This can also\n be useful when `build > windows > staticVCRuntime` is set to `false`.", + "default": false, + "type": "boolean" } }, "additionalProperties": false diff --git a/crates/tauri-cli/src/info/env_system.rs b/crates/tauri-cli/src/info/env_system.rs index 0087319eb820..fb429c0db8f5 100644 --- a/crates/tauri-cli/src/info/env_system.rs +++ b/crates/tauri-cli/src/info/env_system.rs @@ -17,20 +17,10 @@ struct VsInstanceInfo { display_name: String, } -#[cfg(windows)] -const VSWHERE: &[u8] = include_bytes!("../../scripts/vswhere.exe"); - #[cfg(windows)] fn build_tools_version() -> crate::Result> { - let mut vswhere = std::env::temp_dir(); - vswhere.push("vswhere.exe"); - - if !vswhere.exists() { - if let Ok(mut file) = std::fs::File::create(&vswhere) { - use std::io::Write; - let _ = file.write_all(VSWHERE); - } - } + let vswhere = + tauri_bundler::bundle::vswhere_path().context("failed to find or prepare vswhere.exe")?; // Check if there are Visual Studio installations that have the "MSVC - C++ Buildtools" and "Windows SDK" components. // Both the Windows 10 and Windows 11 SDKs work so we need to query it twice. diff --git a/crates/tauri-cli/src/interface/rust.rs b/crates/tauri-cli/src/interface/rust.rs index e18e8308f444..0b76a19ba0d7 100644 --- a/crates/tauri-cli/src/interface/rust.rs +++ b/crates/tauri-cli/src/interface/rust.rs @@ -1658,6 +1658,7 @@ fn tauri_config_to_bundle_settings( allow_downgrades: config.windows.allow_downgrades, sign_command: config.windows.sign_command.map(custom_sign_settings), minimum_webview2_version: config.windows.minimum_webview2_version, + bundle_vc_runtime: config.windows.bundle_vc_runtime, }, license: config.license.or_else(|| { settings diff --git a/crates/tauri-cli/src/interface/rust/desktop.rs b/crates/tauri-cli/src/interface/rust/desktop.rs index ddde0a4f0a44..ed252fb10c83 100644 --- a/crates/tauri-cli/src/interface/rust/desktop.rs +++ b/crates/tauri-cli/src/interface/rust/desktop.rs @@ -159,10 +159,6 @@ pub fn build( let out_dir = app_settings.out_dir(&options, tauri_dir)?; let bin_path = app_settings.app_binary_path(&options, tauri_dir)?; - if !std::env::var_os("STATIC_VCRUNTIME").is_some_and(|v| v == "false") { - std::env::set_var("STATIC_VCRUNTIME", "true"); - } - if options.target == Some("universal-apple-darwin".into()) { std::fs::create_dir_all(&out_dir) .fs_context("failed to create project out directory", out_dir.clone())?; diff --git a/crates/tauri-schema-generator/schemas/config.schema.json b/crates/tauri-schema-generator/schemas/config.schema.json index dbbd5dff137e..64a7109d95cc 100644 --- a/crates/tauri-schema-generator/schemas/config.schema.json +++ b/crates/tauri-schema-generator/schemas/config.schema.json @@ -71,7 +71,10 @@ "description": "The build configuration.", "default": { "additionalWatchFolders": [], - "removeUnusedCommands": false + "removeUnusedCommands": false, + "windows": { + "staticVCRuntime": true + } }, "allOf": [ { @@ -129,6 +132,7 @@ "useLocalToolsDir": false, "windows": { "allowDowngrades": true, + "bundleVCRuntime": false, "certificateThumbprint": null, "digestAlgorithm": null, "minimumWebview2Version": null, @@ -1971,6 +1975,17 @@ "items": { "type": "string" } + }, + "windows": { + "description": "Windows-specific build configuration.", + "default": { + "staticVCRuntime": true + }, + "allOf": [ + { + "$ref": "#/definitions/WindowsBuildConfig" + } + ] } }, "additionalProperties": false @@ -2098,6 +2113,18 @@ } ] }, + "WindowsBuildConfig": { + "description": "Windows-specific build configuration.", + "type": "object", + "properties": { + "staticVCRuntime": { + "description": "Whether to statically link the Visual C++ runtime into the application binary on Windows MSVC targets.", + "default": true, + "type": "boolean" + } + }, + "additionalProperties": false + }, "BundleConfig": { "description": "Configuration for tauri-bundler.\n\n See more: ", "type": "object", @@ -2229,6 +2256,7 @@ "description": "Configuration for the Windows bundles.", "default": { "allowDowngrades": true, + "bundleVCRuntime": false, "certificateThumbprint": null, "digestAlgorithm": null, "minimumWebview2Version": null, @@ -2749,6 +2777,11 @@ "type": "null" } ] + }, + "bundleVCRuntime": { + "description": "Whether to bundle the Visual C++ runtime DLLs alongside the application.\n\n This can be particularly useful when your application includes sidecars or DLLs that do\n not statically link the Visual C++ runtime and require the runtime DLLs at runtime, and\n you do not want to require users to install the Visual C++ Redistributable. This can also\n be useful when `build > windows > staticVCRuntime` is set to `false`.", + "default": false, + "type": "boolean" } }, "additionalProperties": false diff --git a/crates/tauri-utils/src/config.rs b/crates/tauri-utils/src/config.rs index a54c619e3f6f..f5483f3b96a1 100644 --- a/crates/tauri-utils/src/config.rs +++ b/crates/tauri-utils/src/config.rs @@ -1078,6 +1078,19 @@ pub struct WindowsConfig { /// need to use another tool like `osslsigncode`. #[serde(alias = "sign-command")] pub sign_command: Option, + /// Whether to bundle the Visual C++ runtime DLLs alongside the application. + /// + /// This can be particularly useful when your application includes sidecars or DLLs that do + /// not statically link the Visual C++ runtime and require the runtime DLLs at runtime, and + /// you do not want to require users to install the Visual C++ Redistributable. This can also + /// be useful when `build > windows > staticVCRuntime` is set to `false`. + #[serde( + default, + rename = "bundleVCRuntime", + alias = "bundle-vc-runtime", + alias = "bundleVcRuntime" + )] + pub bundle_vc_runtime: bool, } impl Default for WindowsConfig { @@ -1093,6 +1106,7 @@ impl Default for WindowsConfig { wix: None, nsis: None, sign_command: None, + bundle_vc_runtime: false, } } } @@ -3448,6 +3462,32 @@ pub struct BuildConfig { /// Additional paths to watch for changes when running `tauri dev`. #[serde(alias = "additional-watch-directories", default)] pub additional_watch_folders: Vec, + /// Windows-specific build configuration. + #[serde(default)] + pub windows: WindowsBuildConfig, +} + +/// Windows-specific build configuration. +#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] +#[cfg_attr(feature = "schema", derive(JsonSchema))] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct WindowsBuildConfig { + /// Whether to statically link the Visual C++ runtime into the application binary on Windows MSVC targets. + #[serde( + default = "default_true", + rename = "staticVCRuntime", + alias = "static-vc-runtime", + alias = "staticVcRuntime" + )] + pub static_vc_runtime: bool, +} + +impl Default for WindowsBuildConfig { + fn default() -> Self { + Self { + static_vc_runtime: true, + } + } } #[derive(Debug, PartialEq, Eq)] @@ -4126,6 +4166,7 @@ mod build { let features = quote!(None); let remove_unused_commands = quote!(false); let additional_watch_folders = quote!(Vec::new()); + let windows = &self.windows; literal_struct!( tokens, @@ -4138,7 +4179,20 @@ mod build { before_bundle_command, features, remove_unused_commands, - additional_watch_folders + additional_watch_folders, + windows + ); + } + } + + impl ToTokens for WindowsBuildConfig { + fn to_tokens(&self, tokens: &mut TokenStream) { + let static_vc_runtime = self.static_vc_runtime; + + literal_struct!( + tokens, + ::tauri::utils::config::WindowsBuildConfig, + static_vc_runtime ); } } @@ -4482,6 +4536,7 @@ mod test { features: None, remove_unused_commands: false, additional_watch_folders: Vec::new(), + windows: WindowsBuildConfig::default(), }; // create a bundle config