diff --git a/.changes/bundle-vc-runtime.md b/.changes/bundle-vc-runtime.md new file mode 100644 index 000000000000..97291e2b0313 --- /dev/null +++ b/.changes/bundle-vc-runtime.md @@ -0,0 +1,8 @@ +--- +"tauri-bundler": "minor:feat" +"tauri-cli": "minor:feat" +"@tauri-apps/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/no-redirection-bitmap-api.md b/.changes/no-redirection-bitmap-api.md new file mode 100644 index 000000000000..23aa252d9077 --- /dev/null +++ b/.changes/no-redirection-bitmap-api.md @@ -0,0 +1,5 @@ +--- +'@tauri-apps/api': 'minor:feat' +--- + +Added `noRedirectionBitmap` option to the `Window` and `WebviewWindow` constructors on Windows. diff --git a/.changes/no-redirection-bitmap-config.md b/.changes/no-redirection-bitmap-config.md new file mode 100644 index 000000000000..5264ba4c31a0 --- /dev/null +++ b/.changes/no-redirection-bitmap-config.md @@ -0,0 +1,6 @@ +--- +'tauri': 'minor:feat' +'tauri-utils': 'minor:feat' +--- + +Added `app > windows > noRedirectionBitmap` config option to disable the window redirection bitmap on Windows. diff --git a/.changes/no-redirection-bitmap.md b/.changes/no-redirection-bitmap.md new file mode 100644 index 000000000000..230cb4b4878c --- /dev/null +++ b/.changes/no-redirection-bitmap.md @@ -0,0 +1,7 @@ +--- +'tauri': 'minor:feat' +'tauri-runtime': 'minor:feat' +'tauri-runtime-wry': 'minor:feat' +--- + +Added `WindowBuilder/WebviewWindowBuilder::no_redirection_bitmap` method to disable the window redirection bitmap on Windows. diff --git a/.changes/static-vc-runtime.md b/.changes/static-vc-runtime.md new file mode 100644 index 000000000000..3bed5b70f7b1 --- /dev/null +++ b/.changes/static-vc-runtime.md @@ -0,0 +1,7 @@ +--- +"tauri-cli": "minor:feat" +"@tauri-apps/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 9e9914a6f8c9..d3957531ef80 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 8c6735bf2da9..a719796e1e2a 100644 --- a/crates/tauri-bundler/src/bundle.rs +++ b/crates/tauri-bundler/src/bundle.rs @@ -31,6 +31,7 @@ pub use { PackageType, PlistKind, Position, RpmSettings, Settings, SettingsBuilder, Size, UpdaterSettings, WindowsSettings, WixLanguage, WixLanguageConfig, WixSettings, }, + windows::vswhere_path, }; 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 67563de153a9..713bba81dfbf 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, }, }, @@ -802,6 +802,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 8a180612ef6e..8cf9c21a9a2d 100644 --- a/crates/tauri-bundler/src/error.rs +++ b/crates/tauri-bundler/src/error.rs @@ -80,11 +80,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 def6ba23c490..b0c57ae85032 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, @@ -374,7 +378,7 @@ "type": "boolean" }, "transparent": { - "description": "Whether the window is transparent or not.\n\n Note that on `macOS` this requires the `macos-private-api` feature flag, enabled under `tauri > macOSPrivateApi`.\n WARNING: Using private APIs on `macOS` prevents your application from being accepted to the `App Store`.", + "description": "Whether the window is transparent or not.\n\n Note that on `macOS` this requires the `macos-private-api` feature flag, enabled under `tauri > macOSPrivateApi`.\n WARNING: Using private APIs on `macOS` prevents your application from being accepted to the `App Store`.\n\n On Windows, using `noRedirectionBitmap` can help avoid a white flash when creating a transparent window.", "default": false, "type": "boolean" }, @@ -425,6 +429,11 @@ "null" ] }, + "noRedirectionBitmap": { + "description": "This sets `WS_EX_NOREDIRECTIONBITMAP`.\n\n This can avoid the white flash that may appear before the webview content is rendered\n when using a transparent window. **Windows only**.", + "default": false, + "type": "boolean" + }, "theme": { "description": "The initial window theme. Defaults to the system theme. Only implemented on Windows and macOS 10.14+.", "anyOf": [ @@ -1971,6 +1980,17 @@ "items": { "type": "string" } + }, + "windows": { + "description": "Windows-specific build configuration.", + "default": { + "staticVCRuntime": true + }, + "allOf": [ + { + "$ref": "#/definitions/WindowsBuildConfig" + } + ] } }, "additionalProperties": false @@ -2098,6 +2118,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 +2261,7 @@ "description": "Configuration for the Windows bundles.", "default": { "allowDowngrades": true, + "bundleVCRuntime": false, "certificateThumbprint": null, "digestAlgorithm": null, "minimumWebview2Version": null, @@ -2749,6 +2782,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 c83bdb941250..78fc38f8476a 100644 --- a/crates/tauri-cli/src/interface/rust.rs +++ b/crates/tauri-cli/src/interface/rust.rs @@ -1675,6 +1675,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 0073f235142b..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_none_or(|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-runtime-wry/src/lib.rs b/crates/tauri-runtime-wry/src/lib.rs index 3c5bef2db6d6..6337734c2d25 100644 --- a/crates/tauri-runtime-wry/src/lib.rs +++ b/crates/tauri-runtime-wry/src/lib.rs @@ -856,6 +856,7 @@ impl WindowBuilder for WindowBuilderWrapper { .content_protected(config.content_protected) .skip_taskbar(config.skip_taskbar) .theme(config.theme) + .no_redirection_bitmap(config.no_redirection_bitmap) .closable(config.closable) .maximizable(config.maximizable) .minimizable(config.minimizable) @@ -1214,6 +1215,14 @@ impl WindowBuilder for WindowBuilderWrapper { self } + fn no_redirection_bitmap(#[allow(unused_mut)] mut self, _enable: bool) -> Self { + #[cfg(windows)] + { + self.inner = self.inner.with_no_redirection_bitmap(_enable); + } + self + } + #[cfg(target_os = "android")] fn activity_name>(mut self, class_name: S) -> Self { self.inner = self.inner.with_activity_name(class_name.into()); diff --git a/crates/tauri-runtime/src/window.rs b/crates/tauri-runtime/src/window.rs index e20613ea260d..936280ca18c3 100644 --- a/crates/tauri-runtime/src/window.rs +++ b/crates/tauri-runtime/src/window.rs @@ -363,6 +363,9 @@ pub trait WindowBuilder: WindowBuilderBase { /// Whether the window should be transparent. If this is true, writing colors /// with alpha values different than `1.0` will produce a transparent window. + /// + /// On Windows, using `no_redirection_bitmap` can help avoid a white flash when + /// creating a transparent window. #[cfg(any(not(target_os = "macos"), feature = "macos-private-api"))] #[cfg_attr( docsrs, @@ -498,6 +501,13 @@ pub trait WindowBuilder: WindowBuilderBase { #[must_use] fn window_classname>(self, window_classname: S) -> Self; + /// This sets `WS_EX_NOREDIRECTIONBITMAP`. + /// + /// This can avoid the white flash that may appear before the webview content is rendered + /// when using a transparent window. **Windows only**. + #[must_use] + fn no_redirection_bitmap(self, enable: bool) -> Self; + /// The name of the activity to create for this webview window. #[cfg(target_os = "android")] fn activity_name>(self, class_name: S) -> Self; diff --git a/crates/tauri-schema-generator/schemas/config.schema.json b/crates/tauri-schema-generator/schemas/config.schema.json index def6ba23c490..b0c57ae85032 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, @@ -374,7 +378,7 @@ "type": "boolean" }, "transparent": { - "description": "Whether the window is transparent or not.\n\n Note that on `macOS` this requires the `macos-private-api` feature flag, enabled under `tauri > macOSPrivateApi`.\n WARNING: Using private APIs on `macOS` prevents your application from being accepted to the `App Store`.", + "description": "Whether the window is transparent or not.\n\n Note that on `macOS` this requires the `macos-private-api` feature flag, enabled under `tauri > macOSPrivateApi`.\n WARNING: Using private APIs on `macOS` prevents your application from being accepted to the `App Store`.\n\n On Windows, using `noRedirectionBitmap` can help avoid a white flash when creating a transparent window.", "default": false, "type": "boolean" }, @@ -425,6 +429,11 @@ "null" ] }, + "noRedirectionBitmap": { + "description": "This sets `WS_EX_NOREDIRECTIONBITMAP`.\n\n This can avoid the white flash that may appear before the webview content is rendered\n when using a transparent window. **Windows only**.", + "default": false, + "type": "boolean" + }, "theme": { "description": "The initial window theme. Defaults to the system theme. Only implemented on Windows and macOS 10.14+.", "anyOf": [ @@ -1971,6 +1980,17 @@ "items": { "type": "string" } + }, + "windows": { + "description": "Windows-specific build configuration.", + "default": { + "staticVCRuntime": true + }, + "allOf": [ + { + "$ref": "#/definitions/WindowsBuildConfig" + } + ] } }, "additionalProperties": false @@ -2098,6 +2118,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 +2261,7 @@ "description": "Configuration for the Windows bundles.", "default": { "allowDowngrades": true, + "bundleVCRuntime": false, "certificateThumbprint": null, "digestAlgorithm": null, "minimumWebview2Version": null, @@ -2749,6 +2782,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 9b82a3033036..29843a2ae53c 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, } } } @@ -2020,6 +2034,8 @@ pub struct WindowConfig { /// /// Note that on `macOS` this requires the `macos-private-api` feature flag, enabled under `tauri > macOSPrivateApi`. /// WARNING: Using private APIs on `macOS` prevents your application from being accepted to the `App Store`. + /// + /// On Windows, using `noRedirectionBitmap` can help avoid a white flash when creating a transparent window. #[serde(default)] pub transparent: bool, /// Whether the window is maximized or not. @@ -2052,6 +2068,12 @@ pub struct WindowConfig { pub skip_taskbar: bool, /// The name of the window class created on Windows to create the window. **Windows only**. pub window_classname: Option, + /// This sets `WS_EX_NOREDIRECTIONBITMAP`. + /// + /// This can avoid the white flash that may appear before the webview content is rendered + /// when using a transparent window. **Windows only**. + #[serde(default, alias = "no-redirection-bitmap")] + pub no_redirection_bitmap: bool, /// The initial window theme. Defaults to the system theme. Only implemented on Windows and macOS 10.14+. pub theme: Option, /// The style of the macOS title bar. @@ -2326,6 +2348,7 @@ impl Default for WindowConfig { content_protected: false, skip_taskbar: false, window_classname: None, + no_redirection_bitmap: false, theme: None, title_bar_style: Default::default(), traffic_light_position: None, @@ -3447,6 +3470,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)] @@ -3872,6 +3921,7 @@ mod build { let content_protected = self.content_protected; let skip_taskbar = self.skip_taskbar; let window_classname = opt_str_lit(self.window_classname.as_ref()); + let no_redirection_bitmap = self.no_redirection_bitmap; let theme = opt_lit(self.theme.as_ref()); let title_bar_style = &self.title_bar_style; let traffic_light_position = opt_lit(self.traffic_light_position.as_ref()); @@ -3937,6 +3987,7 @@ mod build { content_protected, skip_taskbar, window_classname, + no_redirection_bitmap, theme, title_bar_style, traffic_light_position, @@ -4125,6 +4176,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, @@ -4137,7 +4189,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 ); } } @@ -4481,6 +4546,7 @@ mod test { features: None, remove_unused_commands: false, additional_watch_folders: Vec::new(), + windows: WindowsBuildConfig::default(), }; // create a bundle config diff --git a/crates/tauri/src/test/mock_runtime.rs b/crates/tauri/src/test/mock_runtime.rs index c26f3cd7eca7..23982ea992f1 100644 --- a/crates/tauri/src/test/mock_runtime.rs +++ b/crates/tauri/src/test/mock_runtime.rs @@ -471,6 +471,10 @@ impl WindowBuilder for MockWindowBuilder { self } + fn no_redirection_bitmap(self, enable: bool) -> Self { + self + } + fn shadow(self, enable: bool) -> Self { self } diff --git a/crates/tauri/src/webview/webview_window.rs b/crates/tauri/src/webview/webview_window.rs index c61b96e60307..13b4982a198c 100644 --- a/crates/tauri/src/webview/webview_window.rs +++ b/crates/tauri/src/webview/webview_window.rs @@ -599,6 +599,16 @@ impl<'a, R: Runtime, M: Manager> WebviewWindowBuilder<'a, R, M> { self } + /// This sets `WS_EX_NOREDIRECTIONBITMAP`. + /// + /// This can avoid the white flash that may appear before the webview content is rendered + /// when using a transparent window. **Windows only**. + #[must_use] + pub fn no_redirection_bitmap(mut self, enable: bool) -> Self { + self.window_builder = self.window_builder.no_redirection_bitmap(enable); + self + } + /// Sets whether or not the window has shadow. /// /// ## Platform-specific @@ -1072,6 +1082,9 @@ impl> WebviewWindowBuilder<'_, R, M> { /// Whether the window should be transparent. If this is true, writing colors /// with alpha values different than `1.0` will produce a transparent window. + /// + /// On Windows, using `no_redirection_bitmap` can help avoid a white flash when + /// creating a transparent window. #[cfg(any(not(target_os = "macos"), feature = "macos-private-api"))] #[cfg_attr( docsrs, diff --git a/crates/tauri/src/window/mod.rs b/crates/tauri/src/window/mod.rs index 0aa26a13d467..e2f36441c778 100644 --- a/crates/tauri/src/window/mod.rs +++ b/crates/tauri/src/window/mod.rs @@ -611,6 +611,16 @@ impl<'a, R: Runtime, M: Manager> WindowBuilder<'a, R, M> { self } + /// This sets `WS_EX_NOREDIRECTIONBITMAP`. + /// + /// This can avoid the white flash that may appear before the webview content is rendered + /// when using a transparent window. **Windows only**. + #[must_use] + pub fn no_redirection_bitmap(mut self, enable: bool) -> Self { + self.window_builder = self.window_builder.no_redirection_bitmap(enable); + self + } + /// Sets whether or not the window has shadow. /// /// ## Platform-specific @@ -906,6 +916,9 @@ impl<'a, R: Runtime, M: Manager> WindowBuilder<'a, R, M> { /// Whether the window should be transparent. If this is true, writing colors /// with alpha values different than `1.0` will produce a transparent window. + /// + /// On Windows, using `no_redirection_bitmap` can help avoid a white flash when + /// creating a transparent window. #[cfg(any(not(target_os = "macos"), feature = "macos-private-api"))] #[cfg_attr( docsrs, diff --git a/packages/api/src/window.ts b/packages/api/src/window.ts index f04dbe50c03d..330c31d8eb18 100644 --- a/packages/api/src/window.ts +++ b/packages/api/src/window.ts @@ -2392,6 +2392,8 @@ interface WindowOptions { * Whether the window is transparent or not. * Note that on `macOS` this requires the `macos-private-api` feature flag, enabled under `tauri.conf.json > app > macOSPrivateApi`. * WARNING: Using private APIs on `macOS` prevents your application from being accepted to the `App Store`. + * + * On Windows, using `noRedirectionBitmap` can help avoid a white flash when creating a transparent window. */ transparent?: boolean /** Whether the window should be maximized upon creation or not. */ @@ -2408,6 +2410,13 @@ interface WindowOptions { contentProtected?: boolean /** Whether or not the window icon should be added to the taskbar. */ skipTaskbar?: boolean + /** + * This sets `WS_EX_NOREDIRECTIONBITMAP`. + * + * This can avoid the white flash that may appear before the webview content is rendered + * when using a transparent window. **Windows only**. + */ + noRedirectionBitmap?: boolean /** * Whether or not the window has shadow. *