diff --git a/Cargo.lock b/Cargo.lock index fa2e7155..8c59306d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2363,6 +2363,17 @@ dependencies = [ "velopack", ] +[[package]] +name = "velopack_wix" +version = "0.0.0-local" +dependencies = [ + "anyhow", + "remove_dir_all", + "velopack", + "velopack_bins", + "windows", +] + [[package]] name = "version_check" version = "0.9.5" diff --git a/Cargo.toml b/Cargo.toml index cc14fdbd..92b294a3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,7 @@ members = [ "src/lib-rust", "src/lib-nodejs/velopack_nodeffi", "src/lib-cpp", + "src/wix-dll", ] exclude = [ "samples/RustIced", @@ -24,6 +25,7 @@ rust-version = "1.75" [workspace.dependencies] velopack = { path = "src/lib-rust", features = ["file-logging", "public-utils"] } +velopack_bins = { path = "src/bins" } log = "0.4" log-derive = "0.4.1" ureq = "3.0" diff --git a/src/bins/src/commands/apply_windows_impl.rs b/src/bins/src/commands/apply_windows_impl.rs index 550decf5..218bd2a3 100644 --- a/src/bins/src/commands/apply_windows_impl.rs +++ b/src/bins/src/commands/apply_windows_impl.rs @@ -68,7 +68,12 @@ pub fn apply_package_impl(old_locator: &VelopackLocator, package: &PathBuf, run_ info!("Applying package {} to current: {}", new_version, old_version); - if !crate::windows::prerequisite::prompt_and_install_all_missing(&new_app_manifest, Some(&old_version))? { + if !crate::windows::prerequisite::prompt_and_install_all_missing( + &new_app_manifest.title, + &new_version.to_string(), + &new_app_manifest.runtime_dependencies, + Some(&old_version), + )? { bail!("Stopping apply. Pre-requisites are missing and user cancelled."); } @@ -121,8 +126,9 @@ pub fn apply_package_impl(old_locator: &VelopackLocator, package: &PathBuf, run_ // fifth, we try to replace the current dir with temp_path_new // if this fails we will yolo a rollback... info!("Replacing current dir with {:?}", &temp_path_new); - shared::retry_io_ex(|| fs::rename(&temp_path_new, ¤t_dir), 1000, 30) - .context("Unable to complete the update, and the app was left in a broken state. You may need to re-install or repair this application manually.")?; + shared::retry_io_ex(|| fs::rename(&temp_path_new, ¤t_dir), 1000, 30).context( + "Unable to complete the update, and the app was left in a broken state. You may need to re-install or repair this application manually.", + )?; // if !requires_robocopy { // // if we didn't need robocopy for the backup, we don't need it for the deploy hopefully diff --git a/src/bins/src/commands/install.rs b/src/bins/src/commands/install.rs index f19ea231..9c76fb36 100644 --- a/src/bins/src/commands/install.rs +++ b/src/bins/src/commands/install.rs @@ -30,7 +30,7 @@ pub fn install(pkg: &mut BundleZip, install_to: Option<&PathBuf>, start_args: Op info!(" Package Machine Architecture: {}", &app.machine_architecture); info!(" Package Runtime Dependencies: {}", &app.runtime_dependencies); - if !windows::prerequisite::prompt_and_install_all_missing(&app, None)? { + if !windows::prerequisite::prompt_and_install_all_missing(&app.title, &app.version.to_string(), &app.runtime_dependencies, None)? { info!("Cancelling setup. Pre-requisites not installed."); return Ok(()); } diff --git a/src/bins/src/shared/dialogs_windows.rs b/src/bins/src/shared/dialogs_windows.rs index 64cf7f86..e279de48 100644 --- a/src/bins/src/shared/dialogs_windows.rs +++ b/src/bins/src/shared/dialogs_windows.rs @@ -18,20 +18,15 @@ use windows::{ }, }; -pub fn show_restart_required(app: &Manifest) { +pub fn show_restart_required(app_name: &str, app_version: &str) { show_warn( - format!("{} Setup {}", app.title, app.version).as_str(), + format!("{} Setup {}", app_name, app_version).as_str(), Some("Restart Required"), "A restart is required before Setup can continue. Please restart your computer and try again.", ); } -pub fn show_update_missing_dependencies_dialog( - app: &Manifest, - depedency_string: &str, - from: &semver::Version, - to: &semver::Version, -) -> bool { +pub fn show_update_missing_dependencies_dialog(app_name: &str, depedency_string: &str, from_ver: &str, to_ver: &str) -> bool { if get_silent() { // this has different behavior to show_setup_missing_dependencies_dialog, // if silent is true then we will bail because the app is probably exiting @@ -41,27 +36,23 @@ pub fn show_update_missing_dependencies_dialog( } show_ok_cancel( - format!("{} Update", app.title).as_str(), - Some(format!("{} would like to update from {} to {}", app.title, from, to).as_str()), - format!( - "{} {to} has missing dependencies which need to be installed: {}, would you like to continue?", - app.title, depedency_string - ) - .as_str(), + format!("{} Update", app_name).as_str(), + Some(format!("{} would like to update from {} to {}", app_name, from_ver, to_ver).as_str()), + format!("{} {to_ver} has missing dependencies which need to be installed: {}, would you like to continue?", app_name, depedency_string) + .as_str(), Some("Install & Update"), ) } -pub fn show_setup_missing_dependencies_dialog(app: &Manifest, depedency_string: &str) -> bool { +pub fn show_setup_missing_dependencies_dialog(app_name: &str, app_version: &str, depedency_string: &str) -> bool { if get_silent() { return true; } show_ok_cancel( - format!("{} Setup {}", app.title, app.version).as_str(), - Some(format!("{} has missing system dependencies.", app.title).as_str()), - format!("{} requires the following packages to be installed: {}, would you like to continue?", app.title, depedency_string) - .as_str(), + format!("{} Setup {}", app_name, app_version).as_str(), + Some(format!("{} has missing system dependencies.", app_name).as_str()), + format!("{} requires the following packages to be installed: {}, would you like to continue?", app_name, depedency_string).as_str(), Some("Install"), ) } @@ -259,14 +250,7 @@ pub fn generate_confirm( Ok(DialogResult::from_win(pnbutton)) } -pub fn generate_alert( - title: &str, - header: Option<&str>, - body: &str, - ok_text: Option<&str>, - btns: DialogButton, - ico: DialogIcon, -) -> Result<()> { +pub fn generate_alert(title: &str, header: Option<&str>, body: &str, ok_text: Option<&str>, btns: DialogButton, ico: DialogIcon) -> Result<()> { let _ = generate_confirm(title, header, body, ok_text, btns, ico)?; Ok(()) } @@ -274,7 +258,6 @@ pub fn generate_alert( #[ignore] #[test] fn show_all_windows_dialogs() { - use semver::Version; let app = Manifest { id: "test.app".to_string(), title: "Test Application".to_string(), @@ -285,9 +268,9 @@ fn show_all_windows_dialogs() { ..Default::default() }; - show_restart_required(&app); - show_update_missing_dependencies_dialog(&app, "net8-x64", &Version::new(1, 0, 0), &Version::new(2, 0, 0)); - show_setup_missing_dependencies_dialog(&app, "net8-x64"); + show_restart_required(&app.title, &app.version.to_string()); + show_update_missing_dependencies_dialog(&app.title, "net8-x64", "1.0.0", "2.0.0"); + show_setup_missing_dependencies_dialog(&app.title, &app.version.to_string(), "net8-x64"); show_uninstall_complete_with_errors_dialog("Test Application", Some(&PathBuf::from("C:\\audio.log"))); show_processes_locking_folder_dialog(&app.title, &app.version.to_string(), "TestProcess1, TestProcess2"); show_overwrite_repair_dialog(&app, &PathBuf::from("C:\\Program Files\\TestApp"), false); diff --git a/src/bins/src/windows/prerequisite.rs b/src/bins/src/windows/prerequisite.rs index 88767936..5dbf56a2 100644 --- a/src/bins/src/windows/prerequisite.rs +++ b/src/bins/src/windows/prerequisite.rs @@ -1,11 +1,16 @@ use super::{runtimes, splash}; use crate::shared::dialogs; use anyhow::Result; -use velopack::{bundle, download}; +use velopack::download; -pub fn prompt_and_install_all_missing(app: &bundle::Manifest, updating_from: Option<&semver::Version>) -> Result { +pub fn prompt_and_install_all_missing( + app_name: &str, + app_version: &str, + dependencies: &str, + updating_from: Option<&semver::Version>, +) -> Result { info!("Checking application pre-requisites..."); - let dependencies = super::runtimes::parse_dependency_list(&app.runtime_dependencies); + let dependencies = super::runtimes::parse_dependency_list(dependencies); let mut missing: Vec<&Box> = Vec::new(); let mut missing_str = String::new(); @@ -25,12 +30,12 @@ pub fn prompt_and_install_all_missing(app: &bundle::Manifest, updating_from: Opt if !missing.is_empty() { if let Some(from_version) = updating_from { - if !dialogs::show_update_missing_dependencies_dialog(&app, &missing_str, &from_version, &app.version) { + if !dialogs::show_update_missing_dependencies_dialog(app_name, &missing_str, &from_version.to_string(), app_version) { error!("User cancelled pre-requisite installation."); return Ok(false); } } else { - if !dialogs::show_setup_missing_dependencies_dialog(&app, &missing_str) { + if !dialogs::show_setup_missing_dependencies_dialog(app_name, app_version, &missing_str) { error!("User cancelled pre-requisite installation."); return Ok(false); } @@ -68,7 +73,7 @@ pub fn prompt_and_install_all_missing(app: &bundle::Manifest, updating_from: Opt let result = dep.install(&exe_path, quiet)?; if result == runtimes::RuntimeInstallResult::RestartRequired { warn!("A restart is required to complete the installation of {}.", dep.display_name()); - dialogs::show_restart_required(&app); + dialogs::show_restart_required(&app_name, app_version); return Ok(false); } } diff --git a/src/lib-rust/src/download.rs b/src/lib-rust/src/download.rs index 2aedb56c..d7d5ab79 100644 --- a/src/lib-rust/src/download.rs +++ b/src/lib-rust/src/download.rs @@ -5,10 +5,11 @@ use std::path::Path; use crate::{misc, Error}; /// Downloads a file from a URL and writes it to a file while reporting progress from 0-100. -pub fn download_url_to_file(url: &str, file_path: &Path, mut progress: A) -> Result<(), Error> +pub fn download_url_to_file>(url: &str, file_path: S, mut progress: A) -> Result<(), Error> where A: FnMut(i16), { + let file_path = file_path.as_ref(); let agent = get_download_agent()?; let (head, body) = agent.get(url).call()?.into_parts(); diff --git a/src/vpk/Velopack.Packaging.Windows/Commands/WindowsPackCommandRunner.cs b/src/vpk/Velopack.Packaging.Windows/Commands/WindowsPackCommandRunner.cs index d439d171..4728d4d5 100644 --- a/src/vpk/Velopack.Packaging.Windows/Commands/WindowsPackCommandRunner.cs +++ b/src/vpk/Velopack.Packaging.Windows/Commands/WindowsPackCommandRunner.cs @@ -175,8 +175,13 @@ public class WindowsPackCommandRunner : PackageBuilder protected override Task CreateSetupPackage(Action progress, string releasePkg, string packDir, string targetSetupExe, Func createAsset) { - var setupExeProgress = Options.BuildMsi ? CoreUtil.CreateProgressDelegate(progress, 0, 50) : progress; - var msiProgress = CoreUtil.CreateProgressDelegate(progress, 50, 100); + var setupExeProgress = Options.BuildMsi + ? CoreUtil.CreateProgressDelegate(progress, 0, 33) + : CoreUtil.CreateProgressDelegate(progress, 0, 66); + var msiProgress = CoreUtil.CreateProgressDelegate(progress, 33, 66); + var signingProgress = CoreUtil.CreateProgressDelegate(progress, 66, 100); + + List filesToSign = new(); var bundledZip = new ZipPackage(releasePkg); IoUtil.Retry(() => File.Copy(HelperFile.SetupPath, targetSetupExe, true)); @@ -191,11 +196,9 @@ public class WindowsPackCommandRunner : PackageBuilder editor.Commit(); setupExeProgress(25); - Log.Debug($"Creating Setup bundle"); + Log.Debug("Creating Setup bundle"); SetupBundle.CreatePackageBundle(targetSetupExe, releasePkg); - setupExeProgress(50); - Log.Debug("Signing Setup bundle"); - SignFilesImpl(CoreUtil.CreateProgressDelegate(setupExeProgress, 50, 100), targetSetupExe); + filesToSign.Add(targetSetupExe); Log.Info($"Setup bundle created '{Path.GetFileName(targetSetupExe)}'."); setupExeProgress(100); @@ -205,11 +208,17 @@ public class WindowsPackCommandRunner : PackageBuilder var portablePackage = new DirectoryInfo(Path.Combine(TempDir.FullName, "CreatePortablePackage")); if (portablePackage.Exists) { CompileWixTemplateToMsi(msiProgress, portablePackage, msiPath); + Log.Info($"MSI created '{Path.GetFileName(msiPath)}'."); + filesToSign.Add(msiPath); + msiProgress(100); } else { Log.Warn("Portable package not found, skipping MSI creation."); } } + Log.Debug("Signing Setup files"); + SignFilesImpl(signingProgress, filesToSign.ToArray()); + progress(100); return Task.CompletedTask; } @@ -232,7 +241,11 @@ public class WindowsPackCommandRunner : PackageBuilder // create a .portable file to indicate this is a portable package File.Create(Path.Combine(dir.FullName, ".portable")).Close(); - await EasyZip.CreateZipFromDirectoryAsync(Log.ToVelopackLogger(), outputPath, dir.FullName, CoreUtil.CreateProgressDelegate(progress, 40, 100)); + await EasyZip.CreateZipFromDirectoryAsync( + Log.ToVelopackLogger(), + outputPath, + dir.FullName, + CoreUtil.CreateProgressDelegate(progress, 40, 100)); progress(100); } @@ -350,7 +363,12 @@ public class WindowsPackCommandRunner : PackageBuilder } var licenseRtfPath = GetLicenseRtfFile(); - var templateData = MsiBuilder.ConvertOptionsToTemplateData(portableDirectory, GetShortcuts(), licenseRtfPath, GetRuntimeDependencies(), Options); + var templateData = MsiBuilder.ConvertOptionsToTemplateData( + portableDirectory, + GetShortcuts(), + licenseRtfPath, + GetRuntimeDependencies(), + Options); MsiBuilder.CompileWixMsi(Log, templateData, progress, msiFilePath); } diff --git a/src/vpk/Velopack.Packaging.Windows/Msi/MsiBuilder.cs b/src/vpk/Velopack.Packaging.Windows/Msi/MsiBuilder.cs index 5a3e11bb..72b94d99 100644 --- a/src/vpk/Velopack.Packaging.Windows/Msi/MsiBuilder.cs +++ b/src/vpk/Velopack.Packaging.Windows/Msi/MsiBuilder.cs @@ -29,7 +29,8 @@ public static class MsiBuilder return (template(data), locale(data)); } - public static MsiTemplateData ConvertOptionsToTemplateData(DirectoryInfo portableDir, ShortcutLocation shortcuts, string licenseRtfPath, string runtimeDeps, + public static MsiTemplateData ConvertOptionsToTemplateData(DirectoryInfo portableDir, ShortcutLocation shortcuts, string licenseRtfPath, + string runtimeDeps, WindowsPackOptions options) { // WiX Identifiers may contain ASCII characters A-Z, a-z, digits, underscores (_), or @@ -38,9 +39,9 @@ public static class MsiBuilder if (char.GetUnicodeCategory(wixId[0]) == UnicodeCategory.DecimalDigitNumber) wixId = "_" + wixId; + var parsedVersion = SemanticVersion.Parse(options.PackVersion); var msiVersion = options.MsiVersionOverride; if (string.IsNullOrWhiteSpace(msiVersion)) { - var parsedVersion = SemanticVersion.Parse(options.PackVersion); msiVersion = $"{parsedVersion.Major}.{parsedVersion.Minor}.{parsedVersion.Patch}.0"; } @@ -54,6 +55,7 @@ public static class MsiBuilder AppPublisher = options.PackAuthors ?? options.PackId, AppTitle = options.PackTitle ?? options.PackId, AppMsiVersion = msiVersion, + AppVersion = parsedVersion.ToFullString(), SourceDirectoryPath = portableDir.FullName, Is64Bit = options.TargetRuntime.Architecture is not RuntimeCpu.x86, CultureLCID = CultureInfo.GetCultureInfo("en-US").TextInfo.ANSICodePage, @@ -116,7 +118,8 @@ public static class MsiBuilder string[] manifestResourceNames = assy.GetManifestResourceNames(); string resourceNameFull = manifestResourceNames.SingleOrDefault(name => name.EndsWith(resourceName)); if (string.IsNullOrEmpty(resourceNameFull)) - throw new InvalidOperationException($"Resource '{resourceName}' not found in assembly. Available resources: {string.Join(", ", manifestResourceNames)}"); + throw new InvalidOperationException( + $"Resource '{resourceName}' not found in assembly. Available resources: {string.Join(", ", manifestResourceNames)}"); using var stream = assy.GetManifestResourceStream(resourceNameFull); if (stream == null) diff --git a/src/vpk/Velopack.Packaging.Windows/Msi/MsiTemplateData.cs b/src/vpk/Velopack.Packaging.Windows/Msi/MsiTemplateData.cs index 8a63985c..f2de6343 100644 --- a/src/vpk/Velopack.Packaging.Windows/Msi/MsiTemplateData.cs +++ b/src/vpk/Velopack.Packaging.Windows/Msi/MsiTemplateData.cs @@ -20,6 +20,7 @@ public class MsiTemplateData public string AppPublisher; public string AppPublisherSanitized => MsiUtil.SanitizeDirectoryString(AppPublisher); public string AppMsiVersion; + public string AppVersion; public string StubFileName; public string RuntimeDependencies; @@ -52,9 +53,4 @@ public class MsiTemplateData public bool HasSideBannerImage => !string.IsNullOrWhiteSpace(SideBannerImagePath) && File.Exists(SideBannerImagePath); public string SideBannerImagePath; - - public string WelcomeNextPage => HasLicense ? "LicenseAgreementDlg" : LicenseNextPage; - public string LicenseNextPage => InstallLocationEither ? "InstallScopeDlg" : "VerifyReadyDlg"; - public string InstallScopePrevPage => HasLicense ? "LicenseAgreementDlg" : "WelcomeDlg"; - public string VerifyReadyPrevPage => InstallLocationEither ? "InstallScopeDlg" : InstallScopePrevPage; } \ No newline at end of file diff --git a/src/vpk/Velopack.Packaging.Windows/Msi/Templates/MsiTemplate.hbs b/src/vpk/Velopack.Packaging.Windows/Msi/Templates/MsiTemplate.hbs index a77c631f..23509bd5 100644 --- a/src/vpk/Velopack.Packaging.Windows/Msi/Templates/MsiTemplate.hbs +++ b/src/vpk/Velopack.Packaging.Windows/Msi/Templates/MsiTemplate.hbs @@ -10,8 +10,7 @@ - + @@ -20,14 +19,10 @@ {{#if DesktopShortcut}} - + - @@ -36,14 +31,10 @@ {{#if StartMenuShortcut}} - + - @@ -86,40 +77,46 @@ {{/if}} - + - - - - + + + + + + + + + - - + + + + + + + + + + + - - - + + @@ -155,49 +152,6 @@ - - - - - - - - - - - - - - - - - - - - - @@ -209,22 +163,39 @@ + Condition="NOT WIXUI_DONTVALIDATEPATH AND WIXUI_INSTALLDIR_VALID<>1"/> - - + + {{#if HasLicense}} + + {{else}} + {{#if InstallLocationEither}} + + {{else}} + + {{/if}} + {{/if}} + + {{#if HasLicense}} - + {{#if InstallLocationEither}} + + {{else}} + + {{/if}} {{/if}} {{#if InstallLocationEither}} - + {{#if HasLicense}} + + {{else}} + + {{/if}} - + {{/if}} {{#if InstallLocationCurrentUserOnly}} @@ -260,19 +230,25 @@ {{/if}} - + {{#if InstallLocationEither}} + + {{else}} + {{#if HasLicense}} + + {{else}} + + {{/if}} + {{/if}} + - - - - - diff --git a/src/wix-dll/Cargo.toml b/src/wix-dll/Cargo.toml new file mode 100644 index 00000000..8b41287c --- /dev/null +++ b/src/wix-dll/Cargo.toml @@ -0,0 +1,25 @@ + +[package] +name = "velopack_wix" +publish = false +version.workspace = true +authors.workspace = true +homepage.workspace = true +repository.workspace = true +documentation.workspace = true +keywords.workspace = true +categories.workspace = true +license.workspace = true +edition.workspace = true +rust-version.workspace = true + +[lib] +path = "src/lib.rs" +crate-type = ["cdylib"] + +[dependencies] +anyhow.workspace = true +velopack.workspace = true +velopack_bins.workspace = true +remove_dir_all.workspace = true +windows = { workspace = true, features = ["Win32_Foundation"] } diff --git a/src/wix-dll/src/lib.rs b/src/wix-dll/src/lib.rs new file mode 100644 index 00000000..ccc07feb --- /dev/null +++ b/src/wix-dll/src/lib.rs @@ -0,0 +1,101 @@ +mod msi; +use msi::*; + +use std::{ffi::c_uint, path::PathBuf}; +use velopack::process; +use velopack_bins::{dialogs, windows::prerequisite}; +use windows::Win32::{ + Foundation::{ERROR_INSTALL_USEREXIT, ERROR_SUCCESS}, + System::ApplicationInstallationAndServicing::MSIHANDLE, +}; + +#[no_mangle] +pub extern "system" fn EarlyBootstrap(h_install: MSIHANDLE) -> c_uint { + let dependencies = msi_get_property(h_install, "RustRuntimeDependencies"); + let app_name = msi_get_property(h_install, "RustAppTitle"); + let app_version = msi_get_property(h_install, "RustAppVersion"); + + show_debug_message( + "EarlyBootstrap", + format!("RustRuntimeDependencies={:?} RustAppTitle={:?} RustAppVersion={:?}", dependencies, app_name, app_version), + ); + + if let Some(dependencies) = dependencies { + let app_name = app_name.unwrap_or("Application".into()); + let app_version = app_version.unwrap_or("0.0.0".into()); + match prerequisite::prompt_and_install_all_missing(&app_name, &app_version, &dependencies, None) { + Ok(true) => ERROR_SUCCESS.0, + Ok(false) => ERROR_INSTALL_USEREXIT.0, + Err(e) => { + let title = format!("{} Setup", app_name); + let err = format!("An error occurred: {}", e); + dialogs::show_error(&title, Some("Setup can not continue"), &err); + ERROR_INSTALL_USEREXIT.0 + } + } + } else { + ERROR_SUCCESS.0 + } +} + +#[no_mangle] +pub extern "system" fn CleanupDeferred(h_install: MSIHANDLE) -> c_uint { + let custom_data = msi_get_property(h_install, "CustomActionData"); + show_debug_message("CleanupDeferred", format!("CustomActionData={:?}", custom_data)); + + if let Some(custom_data) = custom_data { + // custom data will be a list delimited by " (0x22) + let mut custom_data = custom_data.split('"'); + let install_dir = custom_data.next(); + let app_id = custom_data.next(); + + show_debug_message("CleanupDeferred", format!("install_dir={:?}, app_id={:?}", install_dir, app_id)); + + if let Some(install_dir) = install_dir { + if let Err(e) = remove_dir_all::remove_dir_all(install_dir) { + show_debug_message("CleanupDeferred", format!("Failed to remove install directory: {}", e)); + } + } + + if let Some(app_id) = app_id { + let temp_dir = std::env::temp_dir(); + if let Err(e) = remove_dir_all::remove_dir_all(temp_dir.join(format!("velopack_{}", app_id))) { + show_debug_message("CleanupDeferred", format!("Failed to remove temp directory: {}", e)); + } + } + + show_debug_message("CleanupDeferred", "Done!".to_string()); + } + + ERROR_SUCCESS.0 +} + +#[no_mangle] +pub extern "system" fn LaunchApplication(h_install: MSIHANDLE) -> c_uint { + let install_dir = msi_get_property(h_install, "INSTALLFOLDER"); + let stub_file = msi_get_property(h_install, "RustStubFileName"); + + show_debug_message("LaunchApplication", format!("INSTALLFOLDER={:?}, RustStubFileName={:?}", install_dir, stub_file)); + + if let Some(install_dir) = install_dir { + if let Some(stub_file) = stub_file { + let stub_path = PathBuf::from(&install_dir).join(stub_file); + if let Err(e) = process::run_process(stub_path, vec![], Some(&install_dir), false, None) { + show_debug_message("LaunchApplication", format!("Failed to launch application: {}", e)); + } + } + } + + ERROR_SUCCESS.0 +} + +#[cfg(debug_assertions)] +fn show_debug_message(fn_name: &str, message: String) { + let message = format!("{}: {}", fn_name, message); + dialogs::show_warn(fn_name, None, &message); +} + +#[cfg(not(debug_assertions))] +fn show_debug_message(fn_name: &str, message: String) { + // no-op +} diff --git a/src/wix-dll/src/msi.rs b/src/wix-dll/src/msi.rs new file mode 100644 index 00000000..114e129a --- /dev/null +++ b/src/wix-dll/src/msi.rs @@ -0,0 +1,164 @@ +#![allow(dead_code)] +use velopack::wide_strings::*; +use windows::{ + core::PWSTR, + Win32::{Foundation::ERROR_SUCCESS, System::ApplicationInstallationAndServicing::*, UI::WindowsAndMessaging::*}, +}; + +pub fn msi_get_property>(h_install: MSIHANDLE, name: S) -> Option { + let name = string_to_wide(name.as_ref()); + let mut empty = string_to_wide(""); + let mut size = 0u32; + + unsafe { + let _ = MsiGetPropertyW(h_install, name.as_pcwstr(), Some(empty.as_pwstr()), Some(&mut size)); + // show_error(h_install, format!("prop1: {ret} size1: {size}")); //234 + + if size == 0 { + return None; // No data found + } + + size += 1; // +1 for null terminator + + let mut buf = vec![0u16; size as usize]; + let ret2 = MsiGetPropertyW(h_install, name.as_pcwstr(), Some(PWSTR(buf.as_mut_ptr())), Some(&mut size)); + // show_error(h_install, format!("prop2: {ret2} size2: {size}")); //234 + + if ret2 == ERROR_SUCCESS.0 { + Some(wide_to_string_lossy(buf)) + } else { + None // Failed to get property + } + } +} + +pub fn msi_set_property_string, S2: AsRef>(h_install: MSIHANDLE, name: S1, value: S2) { + let name = string_to_wide(name.as_ref()); + let value = string_to_wide(value.as_ref()); + unsafe { + let _ = MsiSetPropertyW(h_install, name.as_pcwstr(), value.as_pcwstr()); + } +} + +pub fn msi_set_property_bool>(h_install: MSIHANDLE, name: S1, value: bool) { + let name = string_to_wide(name.as_ref()); + let value = string_to_wide(if value { "1" } else { "" }); + unsafe { + let _ = MsiSetPropertyW(h_install, name.as_pcwstr(), value.as_pcwstr()); + } +} + +pub fn msi_set_property_i32>(h_install: MSIHANDLE, name: S1, value: i32) { + let name = string_to_wide(name.as_ref()); + let value = string_to_wide(value.to_string()); + unsafe { + let _ = MsiSetPropertyW(h_install, name.as_pcwstr(), value.as_pcwstr()); + } +} + +pub fn msi_show_question>(h_install: MSIHANDLE, message: S) -> bool { + let isnt_message = INSTALLMESSAGE_USER.0 | MB_OKCANCEL.0 as i32 | MB_ICONQUESTION.0 as i32; + let res = unsafe { show_dialog_impl(h_install, message, isnt_message) }; + res == IDOK.0 +} + +pub fn msi_show_info>(h_install: MSIHANDLE, message: S) { + let isnt_message = INSTALLMESSAGE_USER.0 | MB_OK.0 as i32 | MB_ICONINFORMATION.0 as i32; + unsafe { show_dialog_impl(h_install, message, isnt_message) }; +} + +pub fn msi_show_warn>(h_install: MSIHANDLE, message: S) { + let isnt_message = INSTALLMESSAGE_USER.0 | MB_OK.0 as i32 | MB_ICONWARNING.0 as i32; + unsafe { show_dialog_impl(h_install, message, isnt_message) }; +} + +pub fn msi_show_error>(h_install: MSIHANDLE, message: S) { + let isnt_message = INSTALLMESSAGE_ERROR.0 | MB_OK.0 as i32 | MB_ICONERROR.0 as i32; + unsafe { show_dialog_impl(h_install, message, isnt_message) }; +} + +unsafe fn show_dialog_impl>(h_install: MSIHANDLE, message: S, flags: i32) -> i32 { + let message = string_to_wide(message.as_ref()); + let rec = MsiCreateRecord(1); + MsiRecordSetStringW(rec, 0, message.as_pcwstr()); + let ret = MsiProcessMessage(h_install, INSTALLMESSAGE(flags), rec); + MsiCloseHandle(rec); + ret +} + +// https://learn.microsoft.com/en-us/windows/win32/api/msiquery/nf-msiquery-msiprocessmessage#record-fields-for-progress-bar-messages +// https://learn.microsoft.com/en-us/windows/win32/msi/adding-custom-actions-to-the-progressbar +pub struct ProgressContext { + h_install: MSIHANDLE, + current_ticks: i32, + total_ticks: i32, +} + +impl ProgressContext { + pub fn new(h_install: MSIHANDLE, jobs: usize) -> Self { + let jobs = jobs as i32; // Convert job index to i32 for calculations + Self { h_install, current_ticks: 0, total_ticks: 100 * jobs } + } + + pub fn reset(&mut self) { + unsafe { + progress_reset(self.h_install, self.total_ticks); + self.current_ticks = 0; + } + } + + pub fn set_progress(&mut self, progress: i32, job: usize) { + let job = job as i32; // Convert job index to i32 for calculations + let progress = progress + (job * 100); // Adjust progress based on the job index + unsafe { + if progress > self.current_ticks { + // If the progress is greater than the current ticks, we increment the progress bar + let diff = progress - self.current_ticks; + progress_increment(self.h_install, diff); + self.current_ticks += diff; + } + // else { + // // If the progress is less than the current ticks, we reset the progress bar + // progress_reset(self.h_install, self.total_ticks); + // progress_increment(self.h_install, progress); + // self.current_ticks = progress; + // } + }; + } +} + +unsafe fn progress_reset(h_install: MSIHANDLE, ticks: i32) { + let rec = MsiCreateRecord(3); + MsiRecordSetInteger(rec, 1, 0); // reset command + MsiRecordSetInteger(rec, 2, ticks); // expected number of ticks + MsiRecordSetInteger(rec, 3, 0); // forward progress bar (left to right) + MsiProcessMessage(h_install, INSTALLMESSAGE_PROGRESS, rec); + MsiCloseHandle(rec); +} + +// unsafe fn progress_set_explicit_progress(h_install: MSIHANDLE) { +// let rec = MsiCreateRecord(3); +// MsiRecordSetInteger(rec, 1, 1); // information command +// MsiRecordSetInteger(rec, 2, 1); // explicit progress +// MsiRecordSetInteger(rec, 3, 0); // unused +// MsiProcessMessage(h_install, INSTALLMESSAGE_PROGRESS, rec); +// MsiCloseHandle(rec); +// } + +unsafe fn progress_increment(h_install: MSIHANDLE, ticks: i32) { + let rec = MsiCreateRecord(3); + MsiRecordSetInteger(rec, 1, 2); // increment command + MsiRecordSetInteger(rec, 2, ticks); // ticks to increment + MsiRecordSetInteger(rec, 3, 0); // unused + MsiProcessMessage(h_install, INSTALLMESSAGE_PROGRESS, rec); + MsiCloseHandle(rec); +} + +// unsafe fn progress_add_extra_ticks(h_install: MSIHANDLE, ticks: i32) { +// let rec = MsiCreateRecord(3); +// MsiRecordSetInteger(rec, 1, 3); // add ticks command +// MsiRecordSetInteger(rec, 2, ticks); // ticks to add to the total progress +// MsiRecordSetInteger(rec, 3, 0); // unused +// MsiProcessMessage(h_install, INSTALLMESSAGE_PROGRESS, rec); +// MsiCloseHandle(rec); +// }