Add bootstrapping and replace custom actions with rust dll

This commit is contained in:
Caelan Sayler
2025-05-26 17:43:27 +01:00
parent da3f8e4afd
commit 408ea6043b
14 changed files with 448 additions and 157 deletions

11
Cargo.lock generated
View File

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

View File

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

View File

@@ -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, &current_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, &current_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

View File

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

View File

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

View File

@@ -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<bool> {
pub fn prompt_and_install_all_missing(
app_name: &str,
app_version: &str,
dependencies: &str,
updating_from: Option<&semver::Version>,
) -> Result<bool> {
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<dyn runtimes::RuntimeInfo>> = 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);
}
}

View File

@@ -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<A>(url: &str, file_path: &Path, mut progress: A) -> Result<(), Error>
pub fn download_url_to_file<A, S: AsRef<Path>>(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();

View File

@@ -175,8 +175,13 @@ public class WindowsPackCommandRunner : PackageBuilder<WindowsPackOptions>
protected override Task CreateSetupPackage(Action<int> progress, string releasePkg, string packDir, string targetSetupExe,
Func<string, VelopackAssetType, string> 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<string> filesToSign = new();
var bundledZip = new ZipPackage(releasePkg);
IoUtil.Retry(() => File.Copy(HelperFile.SetupPath, targetSetupExe, true));
@@ -191,11 +196,9 @@ public class WindowsPackCommandRunner : PackageBuilder<WindowsPackOptions>
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<WindowsPackOptions>
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<WindowsPackOptions>
// 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<WindowsPackOptions>
}
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);
}

View File

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

View File

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

View File

@@ -10,8 +10,7 @@
<Media Id="1" Cabinet="app.cab" EmbedCab="yes"/>
<StandardDirectory Id="TARGETDIR">
<Directory Id="INSTALLFOLDER" Name="{{AppTitleSanitized}}"
ComponentGuidGenerationSeed="{{ComponentGenerationSeedGuid}}">
<Directory Id="INSTALLFOLDER" Name="{{AppTitleSanitized}}" ComponentGuidGenerationSeed="{{ComponentGenerationSeedGuid}}">
<Directory Name="current"/>
<Directory Id="PACKAGES_DIR" Name="packages"/>
</Directory>
@@ -20,14 +19,10 @@
{{#if DesktopShortcut}}
<StandardDirectory Id="DesktopFolder">
<Component Id="ApplicationDesktopShortcut">
<Shortcut Id="ApplicationDesktopShortcut"
Name="{{AppTitle}}"
Description="Desktop shortcut for {{AppTitle}}"
Target="[INSTALLFOLDER]{{StubFileName}}"
WorkingDirectory="INSTALLFOLDER"/>
<Shortcut Id="ApplicationDesktopShortcut" Name="{{AppTitle}}" Description="Desktop shortcut for {{AppTitle}}"
Target="[INSTALLFOLDER]{{StubFileName}}" WorkingDirectory="INSTALLFOLDER"/>
<RemoveFolder Id="CleanUpDesktopShortcut" Directory="INSTALLFOLDER" On="uninstall"/>
<RegistryValue Root="HKCU"
Key="Software&#92;{{AppPublisherSanitized}}&#92;{{AppId}}.DesktopShortcut"
<RegistryValue Root="HKCU" Key="Software&#92;{{AppPublisherSanitized}}&#92;{{AppId}}.DesktopShortcut"
Name="installed" Type="integer" Value="1" KeyPath="yes"/>
</Component>
</StandardDirectory>
@@ -36,14 +31,10 @@
{{#if StartMenuShortcut}}
<StandardDirectory Id="StartMenuFolder">
<Component Id="ApplicationStartMenuShortcut">
<Shortcut Id="ApplicationStartMenuShortcut"
Name="{{AppTitle}}"
Description="Start Menu shortcut for {{AppTitle}}"
Target="[INSTALLFOLDER]{{StubFileName}}"
WorkingDirectory="INSTALLFOLDER"/>
<Shortcut Id="ApplicationStartMenuShortcut" Name="{{AppTitle}}" Description="Start Menu shortcut for {{AppTitle}}"
Target="[INSTALLFOLDER]{{StubFileName}}" WorkingDirectory="INSTALLFOLDER"/>
<RemoveFolder Id="CleanUpStartMenuShortcut" Directory="INSTALLFOLDER" On="uninstall"/>
<RegistryValue Root="HKCU"
Key="Software&#92;{{AppPublisherSanitized}}&#92;{{AppId}}.StartMenuShortcut"
<RegistryValue Root="HKCU" Key="Software&#92;{{AppPublisherSanitized}}&#92;{{AppId}}.StartMenuShortcut"
Name="installed" Type="integer" Value="1" KeyPath="yes"/>
</Component>
</StandardDirectory>
@@ -86,40 +77,46 @@
{{/if}}
<UI>
<ui:WixUI Id="WixUI_Velopack"
InstallDirectory="INSTALLFOLDER"/>
<ui:WixUI Id="WixUI_Velopack" InstallDirectory="INSTALLFOLDER"/>
<Publish Dialog="ExitDialog"
Control="Finish"
Event="DoAction"
Value="LaunchApplication"
<Publish Dialog="ExitDialog" Control="Finish" Event="DoAction" Value="RustLaunchApplication"
Condition="WIXUI_EXITDIALOGOPTIONALCHECKBOX = 1 and NOT Installed"/>
</UI>
<Files Include="{{SourceDirectoryPath}}\**"/>
<CustomAction Id="RemoveAppDirectory" Directory="INSTALLFOLDER" Impersonate="no"
ExeCommand="cmd.exe /C rmdir /S /Q &quot;[INSTALLFOLDER]&quot;" Execute="deferred"
Return="ignore"/>
<CustomAction Id="RemoveTempDirectory" Directory="TempFolder" Impersonate="yes"
ExeCommand="cmd.exe /C rmdir /S /Q &quot;%TEMP%\velopack_{{AppId}}&quot;"
Execute="deferred" Return="ignore"/>
<CustomAction Id="LaunchApplication" Directory="INSTALLFOLDER" Impersonate="yes"
ExeCommand="&quot;[INSTALLFOLDER]{{StubFileName}}&quot;" Execute="immediate" Return="ignore"/>
<!-- <CustomAction Id="RemoveAppDirectory" Directory="INSTALLFOLDER" Impersonate="no"-->
<!-- ExeCommand="cmd.exe /C rmdir /S /Q &quot;[INSTALLFOLDER]&quot;" Execute="deferred" Return="ignore"/>-->
<!-- <CustomAction Id="RemoveTempDirectory" Directory="TempFolder" Impersonate="yes"-->
<!-- ExeCommand="cmd.exe /C rmdir /S /Q &quot;%TEMP%\velopack_{{AppId}}&quot;" Execute="deferred" Return="ignore"/>-->
<!-- <CustomAction Id="LaunchApplication" Directory="INSTALLFOLDER" Impersonate="yes"-->
<!-- ExeCommand="&quot;[INSTALLFOLDER]{{StubFileName}}&quot;" Execute="immediate" Return="ignore"/>-->
<!-- Add our custom Rust module for custom actions -->
<Binary Id="RustDll" SourceFile="{{RustNativeModulePath}}"/>
<Property Id="RustAppId" Value="{{AppTitle}}"/>
<Property Id="RustAppTitle" Value="{{AppTitle}}"/>
<Property Id="RustAppVersion" Value="{{AppVersion}}"/>
<Property Id="RustRuntimeDependencies" Value="{{RuntimeDependencies}}"/>
<CustomAction Id="RustBootstrap" BinaryRef="RustDll" DllEntry="Bootstrap" Execute="immediate" Return="check"/>
<CustomAction Id="RustCheckMissing" BinaryRef="RustDll" DllEntry="CheckMissing" Execute="immediate"
<Property Id="RustStubFileName" Value="{{StubFileName}}"/>
<CustomAction Id="RustEarlyBootstrap" BinaryRef="RustDll" DllEntry="EarlyBootstrap" Execute="immediate" Return="check"/>
<!-- deferred actions do not have access to msi properties and read all data from "CustomActionData" which is a string,
so we delimit with &quot; which is unlikely (or disallowed) from to appearing in filenames / paths.-->
<CustomAction Id="SetRustCleanupData" Property="RustCleanup" Value="[INSTALLFOLDER]&quot;[RustAppId]" Execute="immediate"
Return="check"/>
<CustomAction Id="RustCleanup" BinaryRef="RustDll" DllEntry="CleanupDeferred" Execute="deferred" Impersonate="no" Return="ignore"/>
<CustomAction Id="RustLaunchApplication" BinaryRef="RustDll" DllEntry="LaunchApplication" Impersonate="yes" Execute="immediate"
Return="ignore"/>
<InstallUISequence>
<Custom Action="RustEarlyBootstrap" Before="AppSearch" Condition="REMOVE=&quot;&quot;"/>
</InstallUISequence>
<InstallExecuteSequence>
<Custom Action="RemoveAppDirectory" Before="RemoveFolders"
Condition="(REMOVE=&quot;ALL&quot;) AND (NOT UPGRADINGPRODUCTCODE)"/>
<Custom Action="RemoveTempDirectory" Before="InstallFinalize"
Condition="(REMOVE=&quot;ALL&quot;) AND (NOT UPGRADINGPRODUCTCODE)"/>
<Custom Action="RustBootstrap" Before="InstallInitialize" Condition="(REMOVE=&quot;&quot;)"/>
<Custom Action="SetRustCleanupData" Before="RustCleanup" Condition="(REMOVE=&quot;ALL&quot;)"/>
<Custom Action="RustCleanup" Before="RemoveFolders" Condition="(REMOVE=&quot;ALL&quot;)"/>
</InstallExecuteSequence>
</Package>
@@ -155,49 +152,6 @@
<Property Id="DefaultUIFont" Value="WixUI_Font_Normal"/>
<Dialog Id="InstallPrerequisitesDlg" Width="370" Height="270" Title="!(loc.VerifyReadyDlg_Title)">
<Control Id="InstallTitle" Type="Text" X="15" Y="15" Width="300" Height="15" Transparent="yes"
NoPrefix="yes" Text="{\WixUI_Font_Title}!(loc.InstallPrerequisitesDlgInstallTitle)"/>
<Control Id="InstallText" Type="Text" X="25" Y="70" Width="320" Height="80"
Text="!(loc.InstallPrerequisitesDlgInstallText) [MISSING_DEPENDENCIES]"/>
<Control Id="InstallProgressBar" Type="ProgressBar" X="25" Y="180" Width="320" Height="16"
Property="MISSING_DEPENDENCIES_PROGRESS"/>
<!-- MISSING_DEPENDENCIES_STARTED, MISSING_DEPENDENCIES_COMPLETE, MISSING_DEPENDENCIES_PROGRESS, MISSING_DEPENDENCIES -->
<Control Id="Continue" Type="PushButton" ElevationShield="yes" X="212" Y="243" Width="80" Height="17"
Hidden="yes" Disabled="yes" Text="!(loc.InstallPrerequisitesDlgContinue)"
ShowCondition="MISSING_DEPENDENCIES_COMPLETE" EnableCondition="MISSING_DEPENDENCIES_COMPLETE">
<Publish Event="EndDialog" Value="Return" Condition="OutOfDiskSpace &lt;&gt; 1"/>
<Publish Event="SpawnDialog" Value="OutOfRbDiskDlg"
Condition="OutOfDiskSpace = 1 AND OutOfNoRbDiskSpace = 0 AND (PROMPTROLLBACKCOST=&quot;P&quot; OR NOT PROMPTROLLBACKCOST)"/>
<Publish Event="EndDialog" Value="Return"
Condition="OutOfDiskSpace = 1 AND OutOfNoRbDiskSpace = 0 AND PROMPTROLLBACKCOST=&quot;D&quot;"/>
<Publish Event="EnableRollback" Value="False"
Condition="OutOfDiskSpace = 1 AND OutOfNoRbDiskSpace = 0 AND PROMPTROLLBACKCOST=&quot;D&quot;"/>
<Publish Event="SpawnDialog" Value="OutOfDiskDlg"
Condition="(OutOfDiskSpace = 1 AND OutOfNoRbDiskSpace = 1) OR (OutOfDiskSpace = 1 AND PROMPTROLLBACKCOST=&quot;F&quot;)"/>
</Control>
<Control Id="Install" Type="PushButton" ElevationShield="yes" X="212" Y="243" Width="80" Height="17"
Default="yes" Hidden="yes" Disabled="yes" Text="!(loc.VerifyReadyDlgInstall)"
ShowCondition="NOT MISSING_DEPENDENCIES_COMPLETE"
EnableCondition="NOT MISSING_DEPENDENCIES_STARTED">
</Control>
<Control Id="Cancel" Type="PushButton" X="304" Y="243" Width="56" Height="17" Cancel="yes"
Text="!(loc.WixUICancel)">
<Publish Event="SpawnDialog" Value="CancelDlg"/>
</Control>
<Control Id="Back" Type="PushButton" X="156" Y="243" Width="56" Height="17" Text="!(loc.WixUIBack)"/>
<Control Id="BannerBitmap" Type="Bitmap" X="0" Y="0" Width="370" Height="44" TabSkip="no"
Text="!(loc.VerifyReadyDlgBannerBitmap)"/>
<Control Id="BannerLine" Type="Line" X="0" Y="44" Width="373" Height="0"/>
<Control Id="BottomLine" Type="Line" X="0" Y="234" Width="373" Height="0"/>
</Dialog>
<DialogRef Id="BrowseDlg"/>
<DialogRef Id="DiskCostDlg"/>
<DialogRef Id="ErrorDlg"/>
@@ -209,22 +163,39 @@
<DialogRef Id="ResumeDlg"/>
<DialogRef Id="UserExit"/>
<Publish Dialog="BrowseDlg" Control="OK" Event="SpawnDialog" Value="InvalidDirDlg" Order="4"
Condition="NOT WIXUI_DONTVALIDATEPATH AND WIXUI_INSTALLDIR_VALID&lt;&gt;&quot;1&quot;"/>
Condition="NOT WIXUI_DONTVALIDATEPATH AND WIXUI_INSTALLDIR_VALID&lt;&gt;1"/>
<Publish Dialog="ExitDialog" Control="Finish" Event="EndDialog" Value="Return" Order="999"/>
<Publish Dialog="WelcomeDlg" Control="Next" Event="NewDialog" Value="{{WelcomeNextPage}}"
Condition="NOT Installed"/>
<Publish Dialog="WelcomeDlg" Control="Next" Event="NewDialog" Value="VerifyReadyDlg"
Condition="Installed AND PATCH"/>
{{#if HasLicense}}
<Publish Dialog="WelcomeDlg" Control="Next" Event="NewDialog" Value="LicenseAgreementDlg" Condition="NOT Installed"/>
{{else}}
{{#if InstallLocationEither}}
<Publish Dialog="WelcomeDlg" Control="Next" Event="NewDialog" Value="InstallScopeDlg" Condition="NOT Installed"/>
{{else}}
<Publish Dialog="WelcomeDlg" Control="Next" Event="NewDialog" Value="VerifyReadyDlg" Condition="NOT Installed"/>
{{/if}}
{{/if}}
<Publish Dialog="WelcomeDlg" Control="Next" Event="NewDialog" Value="VerifyReadyDlg" Condition="Installed AND PATCH"/>
{{#if HasLicense}}
<Publish Dialog="LicenseAgreementDlg" Control="Back" Event="NewDialog" Value="WelcomeDlg"/>
<Publish Dialog="LicenseAgreementDlg" Control="Next" Event="NewDialog" Value="{{LicenseNextPage}}"
Condition="LicenseAccepted = &quot;1&quot;"/>
{{#if InstallLocationEither}}
<Publish Dialog="LicenseAgreementDlg" Control="Next" Event="NewDialog" Value="InstallScopeDlg"
Condition="LicenseAccepted = &quot;1&quot;"/>
{{else}}
<Publish Dialog="LicenseAgreementDlg" Control="Next" Event="NewDialog" Value="VerifyReadyDlg"
Condition="LicenseAccepted = &quot;1&quot;"/>
{{/if}}
{{/if}}
{{#if InstallLocationEither}}
<Publish Dialog="InstallScopeDlg" Control="Back" Event="NewDialog" Value="{{InstallScopePrevPage}}"/>
{{#if HasLicense}}
<Publish Dialog="InstallScopeDlg" Control="Back" Event="NewDialog" Value="LicenseAgreementDlg"/>
{{else}}
<Publish Dialog="InstallScopeDlg" Control="Back" Event="NewDialog" Value="WelcomeDlg"/>
{{/if}}
<Publish Dialog="InstallScopeDlg" Control="Next" Property="WixAppFolder" Value="WixPerUserFolder"
Order="1" Condition="!(wix.WixUISupportPerUser) AND NOT Privileged"/>
<Publish Dialog="InstallScopeDlg" Control="Next" Property="ALLUSERS" Value="{}" Order="2"
@@ -232,15 +203,14 @@
<Publish Dialog="InstallScopeDlg" Control="Next" Property="ALLUSERS" Value="1" Order="3"
Condition="WixAppFolder = &quot;WixPerMachineFolder&quot;"/>
<Publish Dialog="InstallScopeDlg" Control="Next" Property="INSTALLFOLDER"
Value="[LocalAppDataFolder][ApplicationFolderName]" Order=" 4"
Value="[LocalAppDataFolder][ApplicationFolderName]" Order="4"
Condition="WixAppFolder = &quot;WixPerUserFolder&quot;"/>
<Publish Dialog="InstallScopeDlg" Control="Next" Property="INSTALLFOLDER"
Value="{{ProgramFilesFolderName}}[ApplicationFolderName]" Order="5"
Condition="WixAppFolder = &quot;WixPerMachineFolder&quot;"/>
<Publish Dialog="InstallScopeDlg" Control="Next" Event="SetTargetPath" Value="INSTALLFOLDER" Order="6"/>
<Publish Dialog="InstallScopeDlg" Control="Next" Event="NewDialog" Value="VerifyReadyDlg" Order="7"/>
<Publish Dialog="InstallScopeDlg" Control="Next" Event="DoAction" Value="FindRelatedProducts"
Order="8"/>
<Publish Dialog="InstallScopeDlg" Control="Next" Event="DoAction" Value="FindRelatedProducts" Order="8"/>
{{/if}}
{{#if InstallLocationCurrentUserOnly}}
@@ -260,19 +230,25 @@
<Publish Dialog="WelcomeDlg" Control="Next" Event="SetTargetPath" Value="INSTALLFOLDER" Order="3"/>
{{/if}}
<Publish Dialog="VerifyReadyDlg" Control="Back" Event="NewDialog" Value="{{VerifyReadyPrevPage}}" Order="1"
Condition="NOT Installed"/>
{{#if InstallLocationEither}}
<Publish Dialog="VerifyReadyDlg" Control="Back" Event="NewDialog" Value="InstallScopeDlg" Order="1"
Condition="NOT Installed"/>
{{else}}
{{#if HasLicense}}
<Publish Dialog="VerifyReadyDlg" Control="Back" Event="NewDialog" Value="LicenseAgreementDlg" Order="1"
Condition="NOT Installed"/>
{{else}}
<Publish Dialog="VerifyReadyDlg" Control="Back" Event="NewDialog" Value="WelcomeDlg" Order="1"
Condition="NOT Installed"/>
{{/if}}
{{/if}}
<Publish Dialog="VerifyReadyDlg" Control="Back" Event="NewDialog" Value="MaintenanceTypeDlg" Order="2"
Condition="Installed AND NOT PATCH"/>
<Publish Dialog="VerifyReadyDlg" Control="Back" Event="NewDialog" Value="WelcomeDlg" Order="2"
Condition="Installed AND PATCH"/>
<!-- <Publish Dialog="VerifyReadyDlg" Control="Install" Event="DoAction" Value="RustCheckMissing" Order="1"/>-->
<!-- <Publish Dialog="VerifyReadyDlg" Control="Install" Event="NewDialog" Value="InstallPrerequisitesDlg"-->
<!-- Order="1"/>-->
<Publish Dialog="MaintenanceWelcomeDlg" Control="Next" Event="NewDialog" Value="MaintenanceTypeDlg"/>
<Publish Dialog="MaintenanceTypeDlg" Control="RepairButton" Event="NewDialog" Value="VerifyReadyDlg"/>
<Publish Dialog="MaintenanceTypeDlg" Control="RemoveButton" Event="NewDialog" Value="VerifyReadyDlg"/>
<Publish Dialog="MaintenanceTypeDlg" Control="Back" Event="NewDialog" Value="MaintenanceWelcomeDlg"/>

25
src/wix-dll/Cargo.toml Normal file
View File

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

101
src/wix-dll/src/lib.rs Normal file
View File

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

164
src/wix-dll/src/msi.rs Normal file
View File

@@ -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<S: AsRef<str>>(h_install: MSIHANDLE, name: S) -> Option<String> {
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<S1: AsRef<str>, S2: AsRef<str>>(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<S1: AsRef<str>>(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<S1: AsRef<str>>(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<S: AsRef<str>>(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<S: AsRef<str>>(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<S: AsRef<str>>(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<S: AsRef<str>>(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<S: AsRef<str>>(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);
// }