Refactor wxs into a dedicated handlebar template

This commit is contained in:
Caelan Sayler
2025-05-26 10:13:00 +01:00
parent 88c1312c5d
commit da3f8e4afd
8 changed files with 603 additions and 391 deletions

View File

@@ -1,16 +1,12 @@
using System.Diagnostics; using System.Runtime.Versioning;
using System.Globalization;
using System.Runtime.Versioning;
using System.Text;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Xml;
using Markdig; using Markdig;
using MarkdigExtensions.RtfRenderer; using MarkdigExtensions.RtfRenderer;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using NuGet.Versioning;
using Velopack.Core; using Velopack.Core;
using Velopack.Core.Abstractions; using Velopack.Core.Abstractions;
using Velopack.NuGet; using Velopack.NuGet;
using Velopack.Packaging.Windows.Msi;
using Velopack.Util; using Velopack.Util;
using Velopack.Windows; using Velopack.Windows;
@@ -95,23 +91,16 @@ public class WindowsPackCommandRunner : PackageBuilder<WindowsPackOptions>
protected string GetShortcutLocations() protected string GetShortcutLocations()
{ {
if (String.IsNullOrWhiteSpace(Options.Shortcuts)) if (String.IsNullOrWhiteSpace(Options.Shortcuts))
return null; return "None";
try { var flags = GetShortcuts();
var shortcuts = GetShortcuts(); var names = Enum.GetValues(typeof(ShortcutLocation))
.Cast<ShortcutLocation>()
.Where(f => f != ShortcutLocation.None && flags.HasFlag(f))
.Select(f => f.ToString())
.ToList();
if (shortcuts.Count == 0) return names.Count > 0 ? string.Join(",", names) : "None";
return null;
var shortcutString = string.Join(",", shortcuts.Select(x => x.ToString()));
Log.Debug($"Shortcut Locations: {shortcutString}");
return shortcutString;
} catch (Exception ex) {
throw new UserInfoException(
$"Invalid shortcut locations '{Options.Shortcuts}'. " +
$"Valid values for comma delimited list are: {string.Join(", ", Enum.GetNames(typeof(ShortcutLocation)))}." +
$"Error was {ex.Message}");
}
} }
protected string GetRuntimeDependencies() protected string GetRuntimeDependencies()
@@ -183,20 +172,11 @@ public class WindowsPackCommandRunner : PackageBuilder<WindowsPackOptions>
return String.Join(",", validated); return String.Join(",", validated);
} }
protected override Task CreateSetupPackage(Action<int> progress, string releasePkg, string packDir, string targetSetupExe, Func<string, VelopackAssetType, string> createAsset) protected override Task CreateSetupPackage(Action<int> progress, string releasePkg, string packDir, string targetSetupExe,
Func<string, VelopackAssetType, string> createAsset)
{ {
void setupExeProgress(int x) var setupExeProgress = Options.BuildMsi ? CoreUtil.CreateProgressDelegate(progress, 0, 50) : progress;
{ var msiProgress = CoreUtil.CreateProgressDelegate(progress, 50, 100);
if (Options.BuildMsi) {
progress(x / 2);
} else {
progress(x);
}
}
void msiProgress(int value)
{
progress(50 + value / 2);
}
var bundledZip = new ZipPackage(releasePkg); var bundledZip = new ZipPackage(releasePkg);
IoUtil.Retry(() => File.Copy(HelperFile.SetupPath, targetSetupExe, true)); IoUtil.Retry(() => File.Copy(HelperFile.SetupPath, targetSetupExe, true));
@@ -216,7 +196,7 @@ public class WindowsPackCommandRunner : PackageBuilder<WindowsPackOptions>
setupExeProgress(50); setupExeProgress(50);
Log.Debug("Signing Setup bundle"); Log.Debug("Signing Setup bundle");
SignFilesImpl(CoreUtil.CreateProgressDelegate(setupExeProgress, 50, 100), targetSetupExe); SignFilesImpl(CoreUtil.CreateProgressDelegate(setupExeProgress, 50, 100), targetSetupExe);
Log.Debug($"Setup bundle created '{Path.GetFileName(targetSetupExe)}'."); Log.Info($"Setup bundle created '{Path.GetFileName(targetSetupExe)}'.");
setupExeProgress(100); setupExeProgress(100);
if (Options.BuildMsi && VelopackRuntimeInfo.IsWindows) { if (Options.BuildMsi && VelopackRuntimeInfo.IsWindows) {
@@ -346,67 +326,8 @@ public class WindowsPackCommandRunner : PackageBuilder<WindowsPackOptions>
} }
[SupportedOSPlatform("windows")] [SupportedOSPlatform("windows")]
private void CompileWixTemplateToMsi(Action<int> progress, private void CompileWixTemplateToMsi(Action<int> progress, DirectoryInfo portableDirectory, string msiFilePath)
DirectoryInfo portableDirectory, string msiFilePath)
{ {
bool packageAs64Bit =
Options.TargetRuntime.Architecture is not RuntimeCpu.x86;
Log.Info($"Compiling msi installer in {(packageAs64Bit ? "64-bit" : "32-bit")} mode");
var outputDirectory = portableDirectory.Parent.CreateSubdirectory("msi");
var culture = CultureInfo.GetCultureInfo("en-US").TextInfo.ANSICodePage;
// WiX Identifiers may contain ASCII characters A-Z, a-z, digits, underscores (_), or
// periods(.). Every identifier must begin with either a letter or an underscore.
var wixId = Regex.Replace(Options.PackId, @"[^\w\.]", "_");
if (char.GetUnicodeCategory(wixId[0]) == UnicodeCategory.DecimalDigitNumber)
wixId = "_" + wixId;
var msiVersion = Options.MsiVersionOverride;
if (string.IsNullOrWhiteSpace(msiVersion)) {
var parsedVersion = SemanticVersion.Parse(Options.PackVersion);
msiVersion = $"{parsedVersion.Major}.{parsedVersion.Minor}.{parsedVersion.Patch}.0";
}
static string SanitizeDirectoryString(string name)
=> string.Join("_", name.Split(Path.GetInvalidPathChars()));
static string FormatXmlMessage(string message)
{
if (string.IsNullOrWhiteSpace(message))
return "";
StringBuilder sb = new();
XmlWriterSettings settings = new() {
ConformanceLevel = ConformanceLevel.Fragment,
NewLineHandling = NewLineHandling.None,
};
using XmlWriter writer = XmlWriter.Create(sb, settings);
writer.WriteString(message);
writer.Flush();
var rv = sb.ToString();
rv = rv.Replace("\r", "&#10;").Replace("\n", "&#13;");
return rv;
}
static string GetFileContent(string filePath)
{
if (string.IsNullOrWhiteSpace(filePath))
return "";
string fileContents = File.ReadAllText(filePath, Encoding.UTF8);
return fileContents;
}
static string RenderMarkdownAsPlainText(string markdown)
{
if (string.IsNullOrWhiteSpace(markdown))
return "";
return Markdown.ToPlainText(markdown);
}
string licenseFile = null;
string GetLicenseRtfFile() string GetLicenseRtfFile()
{ {
string license = Options.InstLicenseRtf; string license = Options.InstLicenseRtf;
@@ -416,8 +337,7 @@ public class WindowsPackCommandRunner : PackageBuilder<WindowsPackOptions>
license = Options.InstLicense; license = Options.InstLicense;
if (!string.IsNullOrWhiteSpace(license)) { if (!string.IsNullOrWhiteSpace(license)) {
var licenseFile = Path.Combine(portableDirectory.Parent!.FullName, "license.rtf");
licenseFile = Path.Combine(outputDirectory.FullName, wixId + "_license.rtf");
using var writer = new StreamWriter(licenseFile); using var writer = new StreamWriter(licenseFile);
var renderer = new RtfRenderer(writer); var renderer = new RtfRenderer(writer);
renderer.StartDocument(); renderer.StartDocument();
@@ -429,287 +349,9 @@ public class WindowsPackCommandRunner : PackageBuilder<WindowsPackOptions>
return null; return null;
} }
var shortcuts = GetShortcuts().ToHashSet(); var licenseRtfPath = GetLicenseRtfFile();
string title = GetEffectiveTitle(); var templateData = MsiBuilder.ConvertOptionsToTemplateData(portableDirectory, GetShortcuts(), licenseRtfPath, GetRuntimeDependencies(), Options);
string authors = GetEffectiveAuthors(); MsiBuilder.CompileWixMsi(Log, templateData, progress, msiFilePath);
string stub = GetPortableStubFileName();
string conclusionMessage = FormatXmlMessage(RenderMarkdownAsPlainText(GetFileContent(Options.InstConclusion)));
string license = GetLicenseRtfFile();
bool hasLicense = !string.IsNullOrWhiteSpace(license);
bool showLocationDialog = Options.InstLocation == InstallLocation.Either;
string bannerImage = string.IsNullOrWhiteSpace(Options.MsiBanner) ? HelperFile.WixAssetsTopBanner : Options.MsiBanner;
string dialogImage = string.IsNullOrWhiteSpace(Options.MsiLogo) ? HelperFile.WixAssetsDialogBackground : Options.MsiLogo;
string wixPackage = $$"""
<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs" xmlns:ui="http://wixtoolset.org/schemas/v4/wxs/ui">
<Package Name="{{title}}"
Manufacturer="{{authors}}"
Version="{{msiVersion}}"
Codepage="{{culture}}"
Language="1033"
Scope="perUserOrMachine"
UpgradeCode="{{GuidUtil.CreateGuidFromHash($"{Options.PackId}:UpgradeCode")}}"
>
<Media Id="1" Cabinet="app.cab" EmbedCab="yes" />
<StandardDirectory Id="TARGETDIR">
<Directory Id="INSTALLFOLDER" Name="{{SanitizeDirectoryString(title)}}" ComponentGuidGenerationSeed="{{GuidUtil.CreateGuidFromHash($"{Options.PackId}:INSTALLFOLDER")}}">
<Directory Name="current" />
<Directory Id="PACKAGES_DIR" Name="packages" />
</Directory>
</StandardDirectory>
{{(shortcuts.Contains(ShortcutLocation.Desktop) ? $"""
<StandardDirectory Id="DesktopFolder">
<Component Id="ApplicationDesktopShortcut">
<Shortcut Id="ApplicationDesktopShortcut"
Name="{title}"
Description="Desktop shortcut for {title}"
Target="[INSTALLFOLDER]{stub}"
WorkingDirectory="INSTALLFOLDER"/>
<RemoveFolder Id="CleanUpDesktopShortcut" Directory="INSTALLFOLDER" On="uninstall"/>
<RegistryValue Root="HKCU" Key="Software\{SanitizeDirectoryString(authors)}\{Options.PackId}.DesktopShortcut" Name="installed" Type="integer" Value="1" KeyPath="yes"/>
</Component>
</StandardDirectory>
""" : "")}}
{{(shortcuts.Contains(ShortcutLocation.StartMenu) ? $"""
<StandardDirectory Id="StartMenuFolder">
<Component Id="ApplicationStartMenuShortcut">
<Shortcut Id="ApplicationStartMenuShortcut"
Name="{title}"
Description="Start Menu shortcut for {title}"
Target="[INSTALLFOLDER]{stub}"
WorkingDirectory="INSTALLFOLDER"/>
<RemoveFolder Id="CleanUpStartMenuShortcut" Directory="INSTALLFOLDER" On="uninstall"/>
<RegistryValue Root="HKCU" Key="Software\{SanitizeDirectoryString(authors)}\{Options.PackId}.StartMenuShortcut" Name="installed" Type="integer" Value="1" KeyPath="yes"/>
</Component>
</StandardDirectory>
""" : "")}}
{{(!string.IsNullOrWhiteSpace(Options.Icon) ? $"""
<Icon Id="appicon" SourceFile="{Options.Icon}"/>
<Property Id="ARPPRODUCTICON" Value="appicon" />
""" : "")}}
{{(hasLicense ? $"""
<WixVariable
Id="WixUILicenseRtf"
Value="{license}"
/>
""" : "")}}
<WixVariable
Id="WixUIBannerBmp"
Value="{{bannerImage}}"
/>
<WixVariable
Id="WixUIDialogBmp"
Value="{{dialogImage}}"
/>
<!-- Message on last screen after install -->
{{(!string.IsNullOrWhiteSpace(conclusionMessage) ? $"""
<Property Id="WIXUI_EXITDIALOGOPTIONALTEXT" Value="{conclusionMessage}" />
""" : "")}}
<!-- Default checked state of launch app check box to true -->
<Property Id="WIXUI_EXITDIALOGOPTIONALCHECKBOX" Value="1" />
<!-- Check box for launching -->
<Property
Id="WIXUI_EXITDIALOGOPTIONALCHECKBOXTEXT"
Value="Launch {{title}}"
/>
<Property Id="WixAppFolder" Value="WixPerMachineFolder" />
<Property Id="ApplicationFolderName" Value="{{SanitizeDirectoryString(Options.PackId)}}" />
{{(Options.InstLocation == InstallLocation.Either ? """
<WixVariable Id="override WixUISupportPerUser" Value="1" />
<WixVariable Id="override WixUISupportPerMachine" Value="1" />
""" : "")}}
<UI>
<ui:WixUI
Id="WixUI_Velopack"
InstallDirectory="INSTALLFOLDER"
/>
<Publish Dialog="ExitDialog"
Control="Finish"
Event="DoAction"
Value="LaunchApplication"
Condition="WIXUI_EXITDIALOGOPTIONALCHECKBOX = 1 and NOT Installed" />
</UI>
<Files Include="{{portableDirectory.FullName}}\**" />
<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_{{Options.PackId}}&quot;" Execute="deferred" Return="ignore" />
<CustomAction Id="LaunchApplication" Directory="INSTALLFOLDER" Impersonate="yes" ExeCommand="&quot;[INSTALLFOLDER]{{stub}}&quot;" Execute="immediate" Return="ignore" />
<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)" />
</InstallExecuteSequence>
</Package>
<!-- https://github.com/wixtoolset/wix/blob/v5.0.2/src/ext/UI/wixlib/WixUI_Advanced.wxs -->
<?foreach WIXUIARCH in X86;X64;A64 ?>
<Fragment>
<UI Id="WixUI_Velopack_$(WIXUIARCH)">
{{(hasLicense ? $"""
<Publish Dialog="LicenseAgreementDlg" Control="Print" Event="DoAction" Value="WixUIPrintEula_$(WIXUIARCH)" />
""" : "")}}
<Publish Dialog="BrowseDlg" Control="OK" Event="DoAction" Value="WixUIValidatePath_$(WIXUIARCH)" Order="3" Condition="NOT WIXUI_DONTVALIDATEPATH" />
{{(showLocationDialog ? """
<Publish Dialog="InstallScopeDlg" Control="Next" Event="DoAction" Value="WixUIValidatePath_$(WIXUIARCH)" Order="7" Condition="NOT WIXUI_DONTVALIDATEPATH" />
""" : "")}}
</UI>
<UIRef Id="WixUI_Velopack" />
</Fragment>
<?endforeach?>
<Fragment>
<PropertyRef Id="ApplicationFolderName" />
<UI Id="file WixUI_Velopack">
<TextStyle Id="WixUI_Font_Normal" FaceName="Tahoma" Size="8" />
<TextStyle Id="WixUI_Font_Bigger" FaceName="Tahoma" Size="12" />
<TextStyle Id="WixUI_Font_Title" FaceName="Tahoma" Size="9" Bold="yes" />
<Property Id="DefaultUIFont" Value="WixUI_Font_Normal" />
<DialogRef Id="BrowseDlg" />
<DialogRef Id="DiskCostDlg" />
<DialogRef Id="ErrorDlg" />
<DialogRef Id="FatalError" />
<DialogRef Id="FilesInUse" />
<DialogRef Id="MsiRMFilesInUse" />
<DialogRef Id="PrepareDlg" />
<DialogRef Id="ProgressDlg" />
<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;" />
<Publish Dialog="ExitDialog" Control="Finish" Event="EndDialog" Value="Return" Order="999" />
{{(hasLicense ? """
<Publish Dialog="WelcomeDlg" Control="Next" Event="NewDialog" Value="LicenseAgreementDlg" Condition="NOT Installed" />
""" : showLocationDialog ?
"""
<Publish Dialog="WelcomeDlg" Control="Next" Event="NewDialog" Value="InstallScopeDlg" Condition="NOT Installed" />
""" : """
<Publish Dialog="WelcomeDlg" Control="Next" Event="NewDialog" Value="VerifyReadyDlg" Condition="NOT Installed" />
"""
)}}
<Publish Dialog="WelcomeDlg" Control="Next" Event="NewDialog" Value="VerifyReadyDlg" Condition="Installed AND PATCH" />
{{(hasLicense ? $"""
<Publish Dialog="LicenseAgreementDlg" Control="Back" Event="NewDialog" Value="WelcomeDlg" />
<Publish Dialog="LicenseAgreementDlg" Control="Next" Event="NewDialog" Value="{(showLocationDialog ? "InstallScopeDlg" : "VerifyReadyDlg")}" Condition="LicenseAccepted = &quot;1&quot;" />
""" : "")}}
{{Options.InstLocation switch {
InstallLocation.Either => $$"""
<Publish Dialog="InstallScopeDlg" Control="Back" Event="NewDialog" Value="{{(hasLicense ? "LicenseAgreementDlg" : "WelcomeDlg")}}" />
<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" Condition="WixAppFolder = &quot;WixPerUserFolder&quot;" />
<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" Condition="WixAppFolder = &quot;WixPerUserFolder&quot;" />
<Publish Dialog="InstallScopeDlg" Control="Next" Property="INSTALLFOLDER" Value="[{{(packageAs64Bit ? "ProgramFiles64Folder" : "ProgramFilesFolder")}}][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" />
""",
InstallLocation.PerUser => """
<Publish Dialog="WelcomeDlg" Control="Next" Property="ALLUSERS" Value="{}" Order="1" Condition="WixAppFolder = &quot;WixPerUserFolder&quot;" />
<Publish Dialog="WelcomeDlg" Control="Next" Property="INSTALLFOLDER" Value="[LocalAppDataFolder][ApplicationFolderName]" Order=" 2" />
<Publish Dialog="WelcomeDlg" Control="Next" Event="SetTargetPath" Value="INSTALLFOLDER" Order="3" />
""",
InstallLocation.PerMachine => $$"""
<Publish Dialog="WelcomeDlg" Control="Next" Property="ALLUSERS" Value="1" Order="1" Condition="WixAppFolder = &quot;WixPerMachineFolder&quot;" />
<Publish Dialog="WelcomeDlg" Control="Next" Property="INSTALLFOLDER" Value="[{{(packageAs64Bit ? "ProgramFiles64Folder" : "ProgramFilesFolder")}}][ApplicationFolderName]" Order="5" Condition="WixAppFolder = &quot;WixPerMachineFolder&quot;" />
<Publish Dialog="WelcomeDlg" Control="Next" Event="SetTargetPath" Value="INSTALLFOLDER" Order="3" />
""",
_ => ""
}}}
<Publish Dialog="VerifyReadyDlg" Control="Back" Event="NewDialog" Value="{{(showLocationDialog ? "InstallScopeDlg" : hasLicense ? "LicenseAgreementDlg" : "WelcomeDlg")}}" Order="1" Condition="NOT Installed" />
<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="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" />
<Property Id="ARPNOMODIFY" Value="1" />
</UI>
<UIRef Id="WixUI_Common" />
</Fragment>
</Wix>
""";
string welcomeMessage = FormatXmlMessage(RenderMarkdownAsPlainText(GetFileContent(Options.InstWelcome)));
string readmeMessage = FormatXmlMessage(RenderMarkdownAsPlainText(GetFileContent(Options.InstReadme)));
string localizedStrings = $"""
<WixLocalization Culture="en-US" Codepage="1252" xmlns="http://wixtoolset.org/schemas/v4/wxl">
{(!string.IsNullOrWhiteSpace(welcomeMessage) ? $"""
<!-- Message on first welcome dialog; covers both initial install and update -->
<String
Id="WelcomeDlgDescription"
Value="{welcomeMessage}"
/>
<String
Id="WelcomeUpdateDlgDescriptionUpdate"
Value="{welcomeMessage}"
/>
""" : "")}
{(!string.IsNullOrWhiteSpace(readmeMessage) ? $"""
<!-- Message on the completion dialog (last screen after install) -->
<String
Id="VerifyReadyDlgInstallText"
Value="{readmeMessage}"
/>
""" : "")}
</WixLocalization>
""";
var wxs = Path.Combine(outputDirectory.FullName, wixId + ".wxs");
var localization = Path.Combine(outputDirectory.FullName, wixId + "_en-US.wxs");
try {
File.WriteAllText(wxs, wixPackage, Encoding.UTF8);
File.WriteAllText(localization, localizedStrings, Encoding.UTF8);
progress(30);
Log.Info("Compiling WiX Template");
List<string> wixExtensions = [HelperFile.WixUiExtPath];
//When localization is supported in Velopack, we will need to add -culture here:
//https://docs.firegiant.com/wix/tools/wixext/wixui/
var buildCommand = $"\"{HelperFile.WixPath}\" build -platform {(packageAs64Bit ? "x64" : "x86")} -outputType Package -pdbType none {string.Join(" ", wixExtensions.Select(x => $"-ext \"{x}\""))} -loc \"{localization}\" -out \"{msiFilePath}\" \"{wxs}\"";
_ = Exe.RunHostedCommand(buildCommand);
progress(90);
} finally {
IoUtil.DeleteFileOrDirectoryHard(wxs, throwOnFailure: false);
IoUtil.DeleteFileOrDirectoryHard(localization, throwOnFailure: false);
if (licenseFile is not null) {
IoUtil.DeleteFileOrDirectoryHard(licenseFile, throwOnFailure: false);
}
}
progress(100);
} }
protected override string[] GetMainExeSearchPaths(string packDirectory, string mainExeName) protected override string[] GetMainExeSearchPaths(string packDirectory, string mainExeName)
@@ -722,14 +364,24 @@ public class WindowsPackCommandRunner : PackageBuilder<WindowsPackOptions>
private string GetPortableStubFileName() => (Options.PackTitle ?? Options.PackId) + ".exe"; private string GetPortableStubFileName() => (Options.PackTitle ?? Options.PackId) + ".exe";
private IReadOnlyList<ShortcutLocation> GetShortcuts() => [.. Options.Shortcuts.Split([',', ';'], StringSplitOptions.RemoveEmptyEntries) private ShortcutLocation GetShortcuts()
.Select(x => x.Trim()) {
.Select(x => { var items = Options.Shortcuts
if (Enum.TryParse(x, true, out ShortcutLocation location)) { .Split([',', ';'], StringSplitOptions.RemoveEmptyEntries)
return location; .Select(x => x.Trim());
}
return ShortcutLocation.None; ShortcutLocation result = ShortcutLocation.None;
})
.Where(x => x != ShortcutLocation.None) foreach (var item in items) {
]; if (Enum.TryParse<ShortcutLocation>(item, true, out var loc)) {
result |= loc;
} else {
throw new UserInfoException(
$"Invalid shortcut locations '{Options.Shortcuts}'. " +
$"Valid values for comma delimited list are: {string.Join(", ", Enum.GetNames(typeof(ShortcutLocation)))}.");
}
}
return result;
}
} }

View File

@@ -0,0 +1,128 @@
using System.Globalization;
using System.Reflection;
using System.Runtime.Versioning;
using System.Text;
using System.Text.RegularExpressions;
using HandlebarsDotNet;
using Microsoft.Extensions.Logging;
using NuGet.Versioning;
using Velopack.Core;
using Velopack.Packaging.Windows.Commands;
using Velopack.Util;
using Velopack.Windows;
namespace Velopack.Packaging.Windows.Msi;
public static class MsiBuilder
{
public static (string mainTemplate, string enLocale) GenerateWixTemplate(MsiTemplateData data)
{
if (data == null)
throw new ArgumentNullException(nameof(data));
var templateContent = GetResourceContent("MsiTemplate.hbs");
var localeContent = GetResourceContent("MsiLocale_en_US.hbs");
var template = Handlebars.Compile(templateContent);
var locale = Handlebars.Compile(localeContent);
return (template(data), locale(data));
}
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
// periods(.). Every identifier must begin with either a letter or an underscore.
var wixId = Regex.Replace(options.PackId, @"[^\w\.]", "_");
if (char.GetUnicodeCategory(wixId[0]) == UnicodeCategory.DecimalDigitNumber)
wixId = "_" + wixId;
var msiVersion = options.MsiVersionOverride;
if (string.IsNullOrWhiteSpace(msiVersion)) {
var parsedVersion = SemanticVersion.Parse(options.PackVersion);
msiVersion = $"{parsedVersion.Major}.{parsedVersion.Minor}.{parsedVersion.Patch}.0";
}
string welcomeMessage = MsiUtil.FormatXmlMessage(MsiUtil.RenderMarkdownAsPlainText(MsiUtil.GetFileContent(options.InstWelcome)));
string readmeMessage = MsiUtil.FormatXmlMessage(MsiUtil.RenderMarkdownAsPlainText(MsiUtil.GetFileContent(options.InstReadme)));
string conclusionMessage = MsiUtil.FormatXmlMessage(MsiUtil.RenderMarkdownAsPlainText(MsiUtil.GetFileContent(options.InstConclusion)));
return new MsiTemplateData() {
WixId = wixId,
AppId = options.PackId,
AppPublisher = options.PackAuthors ?? options.PackId,
AppTitle = options.PackTitle ?? options.PackId,
AppMsiVersion = msiVersion,
SourceDirectoryPath = portableDir.FullName,
Is64Bit = options.TargetRuntime.Architecture is not RuntimeCpu.x86,
CultureLCID = CultureInfo.GetCultureInfo("en-US").TextInfo.ANSICodePage,
InstallForAllUsers = options.InstLocation.HasFlag(InstallLocation.PerMachine),
InstallForCurrentUser = options.InstLocation.HasFlag(InstallLocation.PerUser),
UpgradeCodeGuid = GuidUtil.CreateGuidFromHash($"{options.PackId}:UpgradeCode").ToString(),
ComponentGenerationSeedGuid = GuidUtil.CreateGuidFromHash($"{options.PackId}:INSTALLFOLDER").ToString(),
IconPath = options.Icon,
StubFileName = (options.PackTitle ?? options.PackId) + ".exe",
DesktopShortcut = shortcuts.HasFlag(ShortcutLocation.Desktop),
StartMenuShortcut = shortcuts.HasFlag(ShortcutLocation.StartMenu),
RustNativeModulePath = HelperFile.GetWixNativeModulePath(options.TargetRuntime),
SideBannerImagePath = options.MsiBanner ?? HelperFile.WixAssetsDialogBackground,
TopBannerImagePath = options.MsiLogo ?? HelperFile.WixAssetsTopBanner,
RuntimeDependencies = runtimeDeps,
ConclusionMessage = conclusionMessage,
ReadmeMessage = readmeMessage,
WelcomeMessage = welcomeMessage,
LicenseRtfFilePath = licenseRtfPath,
};
}
[SupportedOSPlatform("windows")]
public static void CompileWixMsi(ILogger Log, MsiTemplateData data, Action<int> progress, string outputFilePath)
{
bool as64Bit = data.Is64Bit;
Log.Info($"Configuring WiX in {(as64Bit ? "64-bit" : "32-bit")} mode");
var _1 = TempUtil.GetTempDirectory(out var outputDir);
var wixId = data.WixId;
var wxsPath = Path.Combine(outputDir, wixId + ".wxs");
var localizationPath = Path.Combine(outputDir, wixId + "_en-US.wxs");
var (wxsContent, localizationContent) = GenerateWixTemplate(data);
File.WriteAllText(wxsPath, wxsContent, Encoding.UTF8);
File.WriteAllText(localizationPath, localizationContent, Encoding.UTF8);
progress(30);
Log.Info("Compiling WiX Template");
List<string> wixExtensions = [HelperFile.WixUiExtPath];
//When localization is supported in Velopack, we will need to add -culture here:
//https://docs.firegiant.com/wix/tools/wixext/wixui/
var buildCommand =
$"\"{HelperFile.WixPath}\" build -platform {(as64Bit ? "x64" : "x86")} -outputType Package " +
$"-pdbType none {string.Join(" ", wixExtensions.Select(x => $"-ext \"{x}\""))} -loc \"{localizationPath}\" -out \"{outputFilePath}\" \"{wxsPath}\"";
_ = Exe.RunHostedCommand(buildCommand);
progress(100);
}
private static string GetResourceContent(string resourceName)
{
var assy = Assembly.GetExecutingAssembly();
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)}");
using var stream = assy.GetManifestResourceStream(resourceNameFull);
if (stream == null)
throw new InvalidOperationException($"Resource '{resourceName}' not found in assembly.");
using var reader = new StreamReader(stream);
return reader.ReadToEnd();
}
}

View File

@@ -0,0 +1,60 @@
namespace Velopack.Packaging.Windows.Msi;
public class MsiTemplateData
{
public string WixId;
public string SourceDirectoryPath;
public string RustNativeModulePath;
public bool Is64Bit;
public int CultureLCID;
public string UpgradeCodeGuid;
public string ComponentGenerationSeedGuid;
public string ProgramFilesFolderName => Is64Bit
? "[ProgramFiles64Folder]"
: "[ProgramFilesFolder]";
public string AppId;
public string AppTitle;
public string AppTitleSanitized => MsiUtil.SanitizeDirectoryString(AppTitle);
public string AppPublisher;
public string AppPublisherSanitized => MsiUtil.SanitizeDirectoryString(AppPublisher);
public string AppMsiVersion;
public string StubFileName;
public string RuntimeDependencies;
public bool DesktopShortcut;
public bool StartMenuShortcut;
public bool InstallLocationEither => InstallForAllUsers && InstallForCurrentUser;
public bool InstallLocationAllUsersOnly => InstallForAllUsers && !InstallForCurrentUser;
public bool InstallLocationCurrentUserOnly => !InstallForAllUsers && InstallForCurrentUser;
public bool InstallForAllUsers;
public bool InstallForCurrentUser;
public bool HasIcon => !string.IsNullOrWhiteSpace(IconPath) && File.Exists(IconPath);
public string IconPath;
public bool HasLicense => !string.IsNullOrWhiteSpace(LicenseRtfFilePath);
public string LicenseRtfFilePath;
public bool HasConclusionMessage => !string.IsNullOrWhiteSpace(ConclusionMessage);
public string ConclusionMessage;
public bool HasWelcomeMessage => !string.IsNullOrWhiteSpace(WelcomeMessage);
public string WelcomeMessage;
public bool HasReadmeMessage => !string.IsNullOrWhiteSpace(ReadmeMessage);
public string ReadmeMessage;
public bool HasTopBannerImage => !string.IsNullOrWhiteSpace(TopBannerImagePath) && File.Exists(TopBannerImagePath);
public string TopBannerImagePath;
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

@@ -0,0 +1,56 @@
using System.Text;
using System.Xml;
using Markdig;
using MarkdigExtensions.RtfRenderer;
namespace Velopack.Packaging.Windows.Msi;
public static class MsiUtil
{
public static string SanitizeDirectoryString(string name)
=> string.Join("_", name.Split(Path.GetInvalidPathChars()));
public static string FormatXmlMessage(string message)
{
if (string.IsNullOrWhiteSpace(message))
return "";
StringBuilder sb = new();
XmlWriterSettings settings = new() {
ConformanceLevel = ConformanceLevel.Fragment,
NewLineHandling = NewLineHandling.None,
};
using XmlWriter writer = XmlWriter.Create(sb, settings);
writer.WriteString(message);
writer.Flush();
var rv = sb.ToString();
rv = rv.Replace("\r", "&#10;").Replace("\n", "&#13;");
return rv;
}
public static string GetFileContent(string filePath)
{
if (string.IsNullOrWhiteSpace(filePath))
return "";
string fileContents = File.ReadAllText(filePath, Encoding.UTF8);
return fileContents;
}
public static string RenderMarkdownAsPlainText(string markdown)
{
if (string.IsNullOrWhiteSpace(markdown))
return "";
return Markdown.ToPlainText(markdown);
}
public static string RenderMarkdownAsRtf(string markdown)
{
var builder = new StringBuilder();
using var writer = new StringWriter(builder);
var renderer = new RtfRenderer(writer);
renderer.StartDocument();
_ = Markdown.Convert(markdown, renderer);
renderer.CloseDocument();
return builder.ToString();
}
}

View File

@@ -0,0 +1,23 @@
<!--suppress CheckEmptyScriptTag -->
<WixLocalization Culture="en-US" Codepage="1252" xmlns="http://wixtoolset.org/schemas/v4/wxl">
<String Id="InstallPrerequisitesDlgInstallTitle"
Value="Missing System Requirements"/>
<String Id="InstallPrerequisitesDlgInstallText"
Value="There are several prerequisites which need to be installed before setup can continue: "/>
<String Id="InstallPrerequisitesDlgContinue"
Value="Continue"/>
<!-- Message on first welcome dialog; covers both initial install and update -->
{{#if HasWelcomeMessage}}
<String Id="WelcomeDlgDescription"
Value="{welcomeMessage}"/>
<String Id="WelcomeUpdateDlgDescriptionUpdate"
Value="{welcomeMessage}"/>
{{/if}}
<!-- Message on the completion dialog (last screen after install) -->
{{#if HasReadmeMessage}}
<String Id="VerifyReadyDlgInstallText"
Value="{readmeMessage}"/>
{{/if}}
</WixLocalization>

View File

@@ -0,0 +1,285 @@
<!--suppress CheckEmptyScriptTag -->
<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs" xmlns:ui="http://wixtoolset.org/schemas/v4/wxs/ui">
<Package Name="{{AppTitle}}"
Manufacturer="{{AppPublisher}}"
Version="{{AppMsiVersion}}"
Codepage="{{CultureLCID}}"
Language="1033"
Scope="perUserOrMachine"
UpgradeCode="{{UpgradeCodeGuid}}">
<Media Id="1" Cabinet="app.cab" EmbedCab="yes"/>
<StandardDirectory Id="TARGETDIR">
<Directory Id="INSTALLFOLDER" Name="{{AppTitleSanitized}}"
ComponentGuidGenerationSeed="{{ComponentGenerationSeedGuid}}">
<Directory Name="current"/>
<Directory Id="PACKAGES_DIR" Name="packages"/>
</Directory>
</StandardDirectory>
{{#if DesktopShortcut}}
<StandardDirectory Id="DesktopFolder">
<Component Id="ApplicationDesktopShortcut">
<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"
Name="installed" Type="integer" Value="1" KeyPath="yes"/>
</Component>
</StandardDirectory>
{{/if}}
{{#if StartMenuShortcut}}
<StandardDirectory Id="StartMenuFolder">
<Component Id="ApplicationStartMenuShortcut">
<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"
Name="installed" Type="integer" Value="1" KeyPath="yes"/>
</Component>
</StandardDirectory>
{{/if}}
{{#if HasIcon}}
<Icon Id="appicon" SourceFile="{{IconPath}}"/>
<Property Id="ARPPRODUCTICON" Value="appicon"/>
{{/if}}
{{#if HasLicense}}
<WixVariable Id="WixUILicenseRtf" Value="{{LicenseRtfFilePath}}"/>
{{/if}}
{{#if HasTopBannerImage}}
<WixVariable Id="WixUIBannerBmp" Value="{{TopBannerImagePath}}"/>
{{/if}}
{{#if HasSideBannerImage}}
<WixVariable Id="WixUIDialogBmp" Value="{{SideBannerImagePath}}"/>
{{/if}}
<!-- Message on last screen after install -->
{{#if HasConclusionMessage}}
<Property Id="WIXUI_EXITDIALOGOPTIONALTEXT" Value="{{ConclusionMessage}}"/>
{{/if}}
<!-- Default checked state of launch app check box to true -->
<Property Id="WIXUI_EXITDIALOGOPTIONALCHECKBOX" Value="1"/>
<!-- Check box for launching -->
<Property Id="WIXUI_EXITDIALOGOPTIONALCHECKBOXTEXT" Value="Launch {{AppTitle}}"/>
<Property Id="WixAppFolder" Value="WixPerMachineFolder"/>
<Property Id="ApplicationFolderName" Value="{{AppId}}"/>
{{#if InstallLocationEither}}
<WixVariable Id="override WixUISupportPerUser" Value="1"/>
<WixVariable Id="override WixUISupportPerMachine" Value="1"/>
{{/if}}
<UI>
<ui:WixUI Id="WixUI_Velopack"
InstallDirectory="INSTALLFOLDER"/>
<Publish Dialog="ExitDialog"
Control="Finish"
Event="DoAction"
Value="LaunchApplication"
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"/>
<!-- Add our custom Rust module for custom actions -->
<Binary Id="RustDll" SourceFile="{{RustNativeModulePath}}"/>
<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"
Return="check"/>
<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;)"/>
</InstallExecuteSequence>
</Package>
<!-- https://github.com/wixtoolset/wix/blob/v5.0.2/src/ext/UI/wixlib/WixUI_Advanced.wxs -->
<?foreach WIXUIARCH in X86;X64;A64 ?>
<Fragment>
<UI Id="WixUI_Velopack_$(WIXUIARCH)">
{{#if HasLicense}}
<Publish Dialog="LicenseAgreementDlg" Control="Print" Event="DoAction"
Value="WixUIPrintEula_$(WIXUIARCH)"/>
{{/if}}
<Publish Dialog="BrowseDlg" Control="OK" Event="DoAction" Value="WixUIValidatePath_$(WIXUIARCH)" Order="3"
Condition="NOT WIXUI_DONTVALIDATEPATH"/>
{{#if InstallLocationEither}}
<Publish Dialog="InstallScopeDlg" Control="Next" Event="DoAction" Value="WixUIValidatePath_$(WIXUIARCH)"
Order="7" Condition="NOT WIXUI_DONTVALIDATEPATH"/>
{{/if}}
</UI>
<UIRef Id="WixUI_Velopack"/>
</Fragment>
<?endforeach?>
<Fragment>
<PropertyRef Id="ApplicationFolderName"/>
<UI Id="file WixUI_Velopack">
<TextStyle Id="WixUI_Font_Normal" FaceName="Segoe UI" Size="8"/>
<TextStyle Id="WixUI_Font_Bigger" FaceName="Segoe UI" Size="12"/>
<TextStyle Id="WixUI_Font_Title" FaceName="Segoe UI" Size="9" Bold="yes"/>
<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"/>
<DialogRef Id="FatalError"/>
<DialogRef Id="FilesInUse"/>
<DialogRef Id="MsiRMFilesInUse"/>
<DialogRef Id="PrepareDlg"/>
<DialogRef Id="ProgressDlg"/>
<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;"/>
<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="LicenseAgreementDlg" Control="Back" Event="NewDialog" Value="WelcomeDlg"/>
<Publish Dialog="LicenseAgreementDlg" Control="Next" Event="NewDialog" Value="{{LicenseNextPage}}"
Condition="LicenseAccepted = &quot;1&quot;"/>
{{/if}}
{{#if InstallLocationEither}}
<Publish Dialog="InstallScopeDlg" Control="Back" Event="NewDialog" Value="{{InstallScopePrevPage}}"/>
<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"
Condition="WixAppFolder = &quot;WixPerUserFolder&quot;"/>
<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"
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"/>
{{/if}}
{{#if InstallLocationCurrentUserOnly}}
<Publish Dialog="WelcomeDlg" Control="Next" Property="ALLUSERS" Value="{}" Order="1"
Condition="WixAppFolder = &quot;WixPerUserFolder&quot;"/>
<Publish Dialog="WelcomeDlg" Control="Next" Property="INSTALLFOLDER"
Value="[LocalAppDataFolder][ApplicationFolderName]" Order=" 2"/>
<Publish Dialog="WelcomeDlg" Control="Next" Event="SetTargetPath" Value="INSTALLFOLDER" Order="3"/>
{{/if}}
{{#if InstallLocationAllUsersOnly}}
<Publish Dialog="WelcomeDlg" Control="Next" Property="ALLUSERS" Value="1" Order="1"
Condition="WixAppFolder = &quot;WixPerMachineFolder&quot;"/>
<Publish Dialog="WelcomeDlg" Control="Next" Property="INSTALLFOLDER"
Value="{{ProgramFilesFolderName}}[ApplicationFolderName]" Order="5"
Condition="WixAppFolder = &quot;WixPerMachineFolder&quot;"/>
<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"/>
<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"/>
<Property Id="ARPNOMODIFY" Value="1"/>
</UI>
<UIRef Id="WixUI_Common"/>
</Fragment>
</Wix>

View File

@@ -17,6 +17,12 @@
<PackageReference Include="AsmResolver.PE.Win32Resources" Version="5.5.1" /> <PackageReference Include="AsmResolver.PE.Win32Resources" Version="5.5.1" />
<PackageReference Include="MarkdigExtensions.RtfRenderer" Version="1.1.0" /> <PackageReference Include="MarkdigExtensions.RtfRenderer" Version="1.1.0" />
<PackageReference Include="Microsoft.Security.Extensions" Version="1.4.0" /> <PackageReference Include="Microsoft.Security.Extensions" Version="1.4.0" />
<PackageReference Include="Handlebars.Net" Version="2.1.6" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Msi\Templates\MsiLocale_en_US.hbs" />
<EmbeddedResource Include="Msi\Templates\MsiTemplate.hbs" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@@ -1,8 +1,10 @@
namespace Velopack.Packaging; namespace Velopack.Packaging;
[Flags]
public enum InstallLocation public enum InstallLocation
{ {
Either, None = 0,
PerUser, PerUser = 1 << 0,
PerMachine, PerMachine = 1 << 1,
Either = PerUser | PerMachine,
} }