From da3f8e4afd64c581f7a94052ef8dd79d0161af22 Mon Sep 17 00:00:00 2001 From: Caelan Sayler Date: Mon, 26 May 2025 10:13:00 +0100 Subject: [PATCH] Refactor wxs into a dedicated handlebar template --- .../Commands/WindowsPackCommandRunner.cs | 428 ++---------------- .../Msi/MsiBuilder.cs | 128 ++++++ .../Msi/MsiTemplateData.cs | 60 +++ .../Velopack.Packaging.Windows/Msi/MsiUtil.cs | 56 +++ .../Msi/Templates/MsiLocale_en_US.hbs | 23 + .../Msi/Templates/MsiTemplate.hbs | 285 ++++++++++++ .../Velopack.Packaging.Windows.csproj | 6 + src/vpk/Velopack.Packaging/InstallLocation.cs | 8 +- 8 files changed, 603 insertions(+), 391 deletions(-) create mode 100644 src/vpk/Velopack.Packaging.Windows/Msi/MsiBuilder.cs create mode 100644 src/vpk/Velopack.Packaging.Windows/Msi/MsiTemplateData.cs create mode 100644 src/vpk/Velopack.Packaging.Windows/Msi/MsiUtil.cs create mode 100644 src/vpk/Velopack.Packaging.Windows/Msi/Templates/MsiLocale_en_US.hbs create mode 100644 src/vpk/Velopack.Packaging.Windows/Msi/Templates/MsiTemplate.hbs diff --git a/src/vpk/Velopack.Packaging.Windows/Commands/WindowsPackCommandRunner.cs b/src/vpk/Velopack.Packaging.Windows/Commands/WindowsPackCommandRunner.cs index 03270a44..d439d171 100644 --- a/src/vpk/Velopack.Packaging.Windows/Commands/WindowsPackCommandRunner.cs +++ b/src/vpk/Velopack.Packaging.Windows/Commands/WindowsPackCommandRunner.cs @@ -1,16 +1,12 @@ -using System.Diagnostics; -using System.Globalization; -using System.Runtime.Versioning; -using System.Text; +using System.Runtime.Versioning; using System.Text.RegularExpressions; -using System.Xml; using Markdig; using MarkdigExtensions.RtfRenderer; using Microsoft.Extensions.Logging; -using NuGet.Versioning; using Velopack.Core; using Velopack.Core.Abstractions; using Velopack.NuGet; +using Velopack.Packaging.Windows.Msi; using Velopack.Util; using Velopack.Windows; @@ -95,23 +91,16 @@ public class WindowsPackCommandRunner : PackageBuilder protected string GetShortcutLocations() { if (String.IsNullOrWhiteSpace(Options.Shortcuts)) - return null; + return "None"; - try { - var shortcuts = GetShortcuts(); + var flags = GetShortcuts(); + var names = Enum.GetValues(typeof(ShortcutLocation)) + .Cast() + .Where(f => f != ShortcutLocation.None && flags.HasFlag(f)) + .Select(f => f.ToString()) + .ToList(); - if (shortcuts.Count == 0) - 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}"); - } + return names.Count > 0 ? string.Join(",", names) : "None"; } protected string GetRuntimeDependencies() @@ -183,20 +172,11 @@ public class WindowsPackCommandRunner : PackageBuilder return String.Join(",", validated); } - protected override Task CreateSetupPackage(Action progress, string releasePkg, string packDir, string targetSetupExe, Func createAsset) + protected override Task CreateSetupPackage(Action progress, string releasePkg, string packDir, string targetSetupExe, + Func createAsset) { - void setupExeProgress(int x) - { - if (Options.BuildMsi) { - progress(x / 2); - } else { - progress(x); - } - } - void msiProgress(int value) - { - progress(50 + value / 2); - } + var setupExeProgress = Options.BuildMsi ? CoreUtil.CreateProgressDelegate(progress, 0, 50) : progress; + var msiProgress = CoreUtil.CreateProgressDelegate(progress, 50, 100); var bundledZip = new ZipPackage(releasePkg); IoUtil.Retry(() => File.Copy(HelperFile.SetupPath, targetSetupExe, true)); @@ -216,7 +196,7 @@ public class WindowsPackCommandRunner : PackageBuilder setupExeProgress(50); Log.Debug("Signing Setup bundle"); 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); if (Options.BuildMsi && VelopackRuntimeInfo.IsWindows) { @@ -346,67 +326,8 @@ public class WindowsPackCommandRunner : PackageBuilder } [SupportedOSPlatform("windows")] - private void CompileWixTemplateToMsi(Action progress, - DirectoryInfo portableDirectory, string msiFilePath) + private void CompileWixTemplateToMsi(Action progress, 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", " ").Replace("\n", " "); - 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 license = Options.InstLicenseRtf; @@ -416,8 +337,7 @@ public class WindowsPackCommandRunner : PackageBuilder license = Options.InstLicense; if (!string.IsNullOrWhiteSpace(license)) { - - licenseFile = Path.Combine(outputDirectory.FullName, wixId + "_license.rtf"); + var licenseFile = Path.Combine(portableDirectory.Parent!.FullName, "license.rtf"); using var writer = new StreamWriter(licenseFile); var renderer = new RtfRenderer(writer); renderer.StartDocument(); @@ -429,287 +349,9 @@ public class WindowsPackCommandRunner : PackageBuilder return null; } - var shortcuts = GetShortcuts().ToHashSet(); - string title = GetEffectiveTitle(); - string authors = GetEffectiveAuthors(); - 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 = $$""" - - - - - - - - - - {{(shortcuts.Contains(ShortcutLocation.Desktop) ? $""" - - - - - - - - """ : "")}} - {{(shortcuts.Contains(ShortcutLocation.StartMenu) ? $""" - - - - - - - - """ : "")}} - - {{(!string.IsNullOrWhiteSpace(Options.Icon) ? $""" - - - """ : "")}} - - {{(hasLicense ? $""" - - """ : "")}} - - - - - - - {{(!string.IsNullOrWhiteSpace(conclusionMessage) ? $""" - - """ : "")}} - - - - - - - - - - - {{(Options.InstLocation == InstallLocation.Either ? """ - - - """ : "")}} - - - - - - - - - - - - - - - - - - - - - - - - {{(hasLicense ? $""" - - """ : "")}} - - {{(showLocationDialog ? """ - - """ : "")}} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - {{(hasLicense ? """ - - """ : showLocationDialog ? - """ - - """ : """ - - """ - )}} - - - {{(hasLicense ? $""" - - - """ : "")}} - - {{Options.InstLocation switch { - InstallLocation.Either => $$""" - - - - - - - - - - """, - InstallLocation.PerUser => """ - - - - """, - InstallLocation.PerMachine => $$""" - - - - """, - _ => "" - }}} - - - - - - - - - - - - - - - - - - - """; - - string welcomeMessage = FormatXmlMessage(RenderMarkdownAsPlainText(GetFileContent(Options.InstWelcome))); - string readmeMessage = FormatXmlMessage(RenderMarkdownAsPlainText(GetFileContent(Options.InstReadme))); - - string localizedStrings = $""" - - {(!string.IsNullOrWhiteSpace(welcomeMessage) ? $""" - - - - """ : "")} - - {(!string.IsNullOrWhiteSpace(readmeMessage) ? $""" - - - """ : "")} - - """; - - - 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 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); + var licenseRtfPath = GetLicenseRtfFile(); + var templateData = MsiBuilder.ConvertOptionsToTemplateData(portableDirectory, GetShortcuts(), licenseRtfPath, GetRuntimeDependencies(), Options); + MsiBuilder.CompileWixMsi(Log, templateData, progress, msiFilePath); } protected override string[] GetMainExeSearchPaths(string packDirectory, string mainExeName) @@ -722,14 +364,24 @@ public class WindowsPackCommandRunner : PackageBuilder private string GetPortableStubFileName() => (Options.PackTitle ?? Options.PackId) + ".exe"; - private IReadOnlyList GetShortcuts() => [.. Options.Shortcuts.Split([',', ';'], StringSplitOptions.RemoveEmptyEntries) - .Select(x => x.Trim()) - .Select(x => { - if (Enum.TryParse(x, true, out ShortcutLocation location)) { - return location; - } - return ShortcutLocation.None; - }) - .Where(x => x != ShortcutLocation.None) - ]; + private ShortcutLocation GetShortcuts() + { + var items = Options.Shortcuts + .Split([',', ';'], StringSplitOptions.RemoveEmptyEntries) + .Select(x => x.Trim()); + + ShortcutLocation result = ShortcutLocation.None; + + foreach (var item in items) { + if (Enum.TryParse(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; + } } \ No newline at end of file diff --git a/src/vpk/Velopack.Packaging.Windows/Msi/MsiBuilder.cs b/src/vpk/Velopack.Packaging.Windows/Msi/MsiBuilder.cs new file mode 100644 index 00000000..5a3e11bb --- /dev/null +++ b/src/vpk/Velopack.Packaging.Windows/Msi/MsiBuilder.cs @@ -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 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 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(); + } +} \ No newline at end of file diff --git a/src/vpk/Velopack.Packaging.Windows/Msi/MsiTemplateData.cs b/src/vpk/Velopack.Packaging.Windows/Msi/MsiTemplateData.cs new file mode 100644 index 00000000..8a63985c --- /dev/null +++ b/src/vpk/Velopack.Packaging.Windows/Msi/MsiTemplateData.cs @@ -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; +} \ No newline at end of file diff --git a/src/vpk/Velopack.Packaging.Windows/Msi/MsiUtil.cs b/src/vpk/Velopack.Packaging.Windows/Msi/MsiUtil.cs new file mode 100644 index 00000000..45909741 --- /dev/null +++ b/src/vpk/Velopack.Packaging.Windows/Msi/MsiUtil.cs @@ -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", " ").Replace("\n", " "); + 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(); + } +} \ No newline at end of file diff --git a/src/vpk/Velopack.Packaging.Windows/Msi/Templates/MsiLocale_en_US.hbs b/src/vpk/Velopack.Packaging.Windows/Msi/Templates/MsiLocale_en_US.hbs new file mode 100644 index 00000000..82f01100 --- /dev/null +++ b/src/vpk/Velopack.Packaging.Windows/Msi/Templates/MsiLocale_en_US.hbs @@ -0,0 +1,23 @@ + + + + + + + + {{#if HasWelcomeMessage}} + + + {{/if}} + + + {{#if HasReadmeMessage}} + + {{/if}} + \ 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 new file mode 100644 index 00000000..a77c631f --- /dev/null +++ b/src/vpk/Velopack.Packaging.Windows/Msi/Templates/MsiTemplate.hbs @@ -0,0 +1,285 @@ + + + + + + + + + + + + + {{#if DesktopShortcut}} + + + + + + + + {{/if}} + + {{#if StartMenuShortcut}} + + + + + + + + {{/if}} + + {{#if HasIcon}} + + + {{/if}} + + {{#if HasLicense}} + + {{/if}} + + {{#if HasTopBannerImage}} + + {{/if}} + + {{#if HasSideBannerImage}} + + {{/if}} + + + {{#if HasConclusionMessage}} + + {{/if}} + + + + + + + + + + + {{#if InstallLocationEither}} + + + {{/if}} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {{#if HasLicense}} + + {{/if}} + + + + {{#if InstallLocationEither}} + + {{/if}} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {{#if HasLicense}} + + + {{/if}} + + {{#if InstallLocationEither}} + + + + + + + + + + {{/if}} + + {{#if InstallLocationCurrentUserOnly}} + + + + {{/if}} + + {{#if InstallLocationAllUsersOnly}} + + + + {{/if}} + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/vpk/Velopack.Packaging.Windows/Velopack.Packaging.Windows.csproj b/src/vpk/Velopack.Packaging.Windows/Velopack.Packaging.Windows.csproj index 81a9e984..8e6e0396 100644 --- a/src/vpk/Velopack.Packaging.Windows/Velopack.Packaging.Windows.csproj +++ b/src/vpk/Velopack.Packaging.Windows/Velopack.Packaging.Windows.csproj @@ -17,6 +17,12 @@ + + + + + + diff --git a/src/vpk/Velopack.Packaging/InstallLocation.cs b/src/vpk/Velopack.Packaging/InstallLocation.cs index d16ef139..440e18f5 100644 --- a/src/vpk/Velopack.Packaging/InstallLocation.cs +++ b/src/vpk/Velopack.Packaging/InstallLocation.cs @@ -1,8 +1,10 @@ namespace Velopack.Packaging; +[Flags] public enum InstallLocation { - Either, - PerUser, - PerMachine, + None = 0, + PerUser = 1 << 0, + PerMachine = 1 << 1, + Either = PerUser | PerMachine, } \ No newline at end of file