diff --git a/README.md b/README.md index c415e42b..975af115 100644 --- a/README.md +++ b/README.md @@ -16,10 +16,10 @@ Velopack is an installation and auto-update framework for cross-platform applica ## Features -- 😍 **Zero config** – Velopack takes your compiler output and generates an installer, updates, delta packages, and self-updating portable package in just one command. -- 🎯 **Cross platform** – Velopack supports building packages for **Windows**, **OSX**, and **Linux**, so you can use one solution for every target. +- 😍 **Zero config** - Velopack takes your compiler output and generates an installer, updates, delta packages, and self-updating portable package in just one command. +- 🎯 **Cross platform** - Velopack supports building packages for **Windows**, **OSX**, and **Linux**, so you can use one solution for every target. - 🚀 **Automatic migrations** - If you are coming from other popular frameworks (eg. [Squirrel](https://github.com/Squirrel/Squirrel.Windows)), Velopack can automatically migrate your application. -- ⚡️ **Lightning fast** – Velopack is written in Rust for native performance. Delta packages mean your user only downloads what's changed between versions. +- ⚡️ **Lightning fast** - Velopack is written in Rust for native performance. Delta packages mean your user only downloads what's changed between versions. - 📔 **Language agnostic** - With support for C#, C++, JS, Rust and more. Use a familiar API for updates no matter what language your project is. https://github.com/velopack/velopack/assets/1287295/0ff1bea7-15ed-42ae-8bdd-9519f1033432 diff --git a/src/vpk/Velopack.Packaging.Windows/Commands/WindowsPackCommandRunner.cs b/src/vpk/Velopack.Packaging.Windows/Commands/WindowsPackCommandRunner.cs index cf6ce904..ec0618fe 100644 --- a/src/vpk/Velopack.Packaging.Windows/Commands/WindowsPackCommandRunner.cs +++ b/src/vpk/Velopack.Packaging.Windows/Commands/WindowsPackCommandRunner.cs @@ -1,7 +1,5 @@ using System.Runtime.Versioning; using System.Text.RegularExpressions; -using Markdig; -using MarkdigExtensions.RtfRenderer; using Microsoft.Extensions.Logging; using Velopack.Core; using Velopack.Core.Abstractions; @@ -340,32 +338,9 @@ public class WindowsPackCommandRunner : PackageBuilder [SupportedOSPlatform("windows")] private void CompileWixTemplateToMsi(Action progress, DirectoryInfo portableDirectory, string msiFilePath) { - string GetLicenseRtfFile() - { - string license = Options.InstLicenseRtf; - if (!string.IsNullOrWhiteSpace(license)) { - return license; - } - - license = Options.InstLicense; - if (!string.IsNullOrWhiteSpace(license)) { - var licenseFile = Path.Combine(portableDirectory.Parent!.FullName, "license.rtf"); - using var writer = new StreamWriter(licenseFile); - var renderer = new RtfRenderer(writer); - renderer.StartDocument(); - _ = Markdown.Convert(File.ReadAllText(license), renderer); - renderer.CloseDocument(); - return licenseFile; - } - - return null; - } - - var licenseRtfPath = GetLicenseRtfFile(); var templateData = MsiBuilder.ConvertOptionsToTemplateData( portableDirectory, GetShortcuts(), - licenseRtfPath, GetRuntimeDependencies(), Options); MsiBuilder.CompileWixMsi(Log, templateData, progress, msiFilePath); diff --git a/src/vpk/Velopack.Packaging.Windows/Commands/WindowsPackOptions.cs b/src/vpk/Velopack.Packaging.Windows/Commands/WindowsPackOptions.cs index e9e1bc75..41d13412 100644 --- a/src/vpk/Velopack.Packaging.Windows/Commands/WindowsPackOptions.cs +++ b/src/vpk/Velopack.Packaging.Windows/Commands/WindowsPackOptions.cs @@ -29,7 +29,6 @@ public class WindowsPackOptions : WindowsReleasifyOptions, INugetPackCommand, IP public string InstReadme { get; set; } public string InstLicense { get; set; } - public string InstLicenseRtf { get; set; } public string InstConclusion { get; set; } diff --git a/src/vpk/Velopack.Packaging.Windows/Msi/MsiBuilder.cs b/src/vpk/Velopack.Packaging.Windows/Msi/MsiBuilder.cs index dd67966a..fc82a8ee 100644 --- a/src/vpk/Velopack.Packaging.Windows/Msi/MsiBuilder.cs +++ b/src/vpk/Velopack.Packaging.Windows/Msi/MsiBuilder.cs @@ -3,10 +3,13 @@ using System.Reflection; using System.Runtime.Versioning; using System.Text; using System.Text.RegularExpressions; +using System.Xml; using HandlebarsDotNet; +using Markdig; using Microsoft.Extensions.Logging; using NuGet.Versioning; using Velopack.Core; +using Velopack.Packaging.Rtf; using Velopack.Packaging.Windows.Commands; using Velopack.Util; using Velopack.Windows; @@ -29,9 +32,81 @@ public static class MsiBuilder return (template(data), locale(data)); } - public static MsiTemplateData ConvertOptionsToTemplateData(DirectoryInfo portableDir, ShortcutLocation shortcuts, string licenseRtfPath, - string runtimeDeps, - WindowsPackOptions options) + private static string GetPlainTextMessage(string filePath) + { + if (string.IsNullOrWhiteSpace(filePath)) + return ""; + + if (!File.Exists(filePath)) + throw new FileNotFoundException("File not found", filePath); + + var extension = Path.GetExtension(filePath); + var content = File.ReadAllText(filePath, Encoding.UTF8); + + // if extension is .md render it to plain text + if (extension.Equals(".md", StringComparison.OrdinalIgnoreCase)) { + content = Markdown.ToPlainText(content); + } else if (extension.Equals(".txt", StringComparison.OrdinalIgnoreCase)) { + // do nothing but it's valid + } else { + throw new ArgumentException("Installer plain-text messages must be .md or .txt", nameof(filePath)); + } + + return FormatXmlMessage(content); + } + + private static string GetLicenseRtfPath(string licensePath, DirectoryInfo tempDir) + { + if (string.IsNullOrWhiteSpace(licensePath)) + return ""; + + if (!File.Exists(licensePath)) + throw new FileNotFoundException("File not found", licensePath); + + var extension = Path.GetExtension(licensePath); + var content = File.ReadAllText(licensePath, Encoding.UTF8); + + // if extension is .md, render it to rtf + if (extension.Equals(".md", StringComparison.OrdinalIgnoreCase) + || extension.Equals(".txt", StringComparison.OrdinalIgnoreCase)) { + licensePath = Path.Combine(tempDir.FullName, "rendered_license.rtf"); + using var writer = new StreamWriter(licensePath); + var renderer = new RtfRenderer(writer); + renderer.WriteRtfStart(); + _ = Markdown.Convert(content, renderer); + renderer.WriteRtfEnd(); + } else if (extension.Equals(".rtf", StringComparison.OrdinalIgnoreCase)) { + // do nothing but it's valid + } else { + throw new ArgumentException("Installer license must be .txt, .md, or .rtf", nameof(licensePath)); + } + + return licensePath; + } + + 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 MsiTemplateData ConvertOptionsToTemplateData(DirectoryInfo portableDir, ShortcutLocation shortcuts, + 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. @@ -45,10 +120,6 @@ public static class MsiBuilder 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, @@ -59,7 +130,6 @@ public static class MsiBuilder SourceDirectoryPath = portableDir.FullName, Is64Bit = options.TargetRuntime.Architecture is not RuntimeCpu.x86 and not RuntimeCpu.Unknown, IsArm64 = options.TargetRuntime.Architecture is RuntimeCpu.arm64, - 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(), @@ -73,10 +143,10 @@ public static class MsiBuilder SideBannerImagePath = options.MsiBanner ?? HelperFile.WixAssetsDialogBackground, TopBannerImagePath = options.MsiLogo ?? HelperFile.WixAssetsTopBanner, RuntimeDependencies = runtimeDeps, - ConclusionMessage = conclusionMessage, - ReadmeMessage = readmeMessage, - WelcomeMessage = welcomeMessage, - LicenseRtfFilePath = licenseRtfPath, + ConclusionMessage = GetPlainTextMessage(options.InstConclusion), + ReadmeMessage = GetPlainTextMessage(options.InstReadme), + WelcomeMessage = GetPlainTextMessage(options.InstWelcome), + LicenseRtfFilePath = GetLicenseRtfPath(options.InstLicense, portableDir.Parent), }; } diff --git a/src/vpk/Velopack.Packaging.Windows/Msi/MsiTemplateData.cs b/src/vpk/Velopack.Packaging.Windows/Msi/MsiTemplateData.cs index 8630e507..5cd64df8 100644 --- a/src/vpk/Velopack.Packaging.Windows/Msi/MsiTemplateData.cs +++ b/src/vpk/Velopack.Packaging.Windows/Msi/MsiTemplateData.cs @@ -7,7 +7,6 @@ public class MsiTemplateData public string RustNativeModulePath; public bool Is64Bit; public bool IsArm64; - public int CultureLCID; public string UpgradeCodeGuid; public string ComponentGenerationSeedGuid; @@ -17,9 +16,9 @@ public class MsiTemplateData public string AppId; public string AppTitle; - public string AppTitleSanitized => MsiUtil.SanitizeDirectoryString(AppTitle); + public string AppTitleSanitized => MsiBuilder.SanitizeDirectoryString(AppTitle); public string AppPublisher; - public string AppPublisherSanitized => MsiUtil.SanitizeDirectoryString(AppPublisher); + public string AppPublisherSanitized => MsiBuilder.SanitizeDirectoryString(AppPublisher); public string AppMsiVersion; public string AppVersion; diff --git a/src/vpk/Velopack.Packaging.Windows/Msi/MsiUtil.cs b/src/vpk/Velopack.Packaging.Windows/Msi/MsiUtil.cs deleted file mode 100644 index 45909741..00000000 --- a/src/vpk/Velopack.Packaging.Windows/Msi/MsiUtil.cs +++ /dev/null @@ -1,56 +0,0 @@ -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 index 82f01100..b2225486 100644 --- a/src/vpk/Velopack.Packaging.Windows/Msi/Templates/MsiLocale_en_US.hbs +++ b/src/vpk/Velopack.Packaging.Windows/Msi/Templates/MsiLocale_en_US.hbs @@ -1,11 +1,6 @@ - - - - + + {{#if HasWelcomeMessage}} diff --git a/src/vpk/Velopack.Packaging.Windows/Msi/Templates/MsiTemplate.hbs b/src/vpk/Velopack.Packaging.Windows/Msi/Templates/MsiTemplate.hbs index 54d7542b..0258dccd 100644 --- a/src/vpk/Velopack.Packaging.Windows/Msi/Templates/MsiTemplate.hbs +++ b/src/vpk/Velopack.Packaging.Windows/Msi/Templates/MsiTemplate.hbs @@ -3,7 +3,6 @@ @@ -91,7 +90,7 @@ - + diff --git a/src/vpk/Velopack.Packaging.Windows/Velopack.Packaging.Windows.csproj b/src/vpk/Velopack.Packaging.Windows/Velopack.Packaging.Windows.csproj index 8e6e0396..070cbd5f 100644 --- a/src/vpk/Velopack.Packaging.Windows/Velopack.Packaging.Windows.csproj +++ b/src/vpk/Velopack.Packaging.Windows/Velopack.Packaging.Windows.csproj @@ -15,7 +15,6 @@ - diff --git a/src/vpk/Velopack.Vpk/Commands/Packaging/WindowsPackCommand.cs b/src/vpk/Velopack.Vpk/Commands/Packaging/WindowsPackCommand.cs index c9d40034..4ca6a1da 100644 --- a/src/vpk/Velopack.Vpk/Commands/Packaging/WindowsPackCommand.cs +++ b/src/vpk/Velopack.Vpk/Commands/Packaging/WindowsPackCommand.cs @@ -61,19 +61,19 @@ public class WindowsPackCommand : PackCommand .SetHidden(); var signTemplate = AddOption((v) => SignTemplate = v, "--signTemplate") - .SetDescription("Use a custom signing command. {{file}} will be substituted.") - .SetArgumentHelpName("COMMAND"); + .SetDescription("Use a custom signing command. {{file}} will be substituted.") + .SetArgumentHelpName("COMMAND"); AddOption((v) => SignExclude = v, "--signExclude") .SetDescription("A regex which excludes matched files from signing.") .SetHidden(); AddOption((v) => SignParallel = v, "--signParallel") - .SetDescription("The number of files to sign in each signing command.") - .SetArgumentHelpName("NUM") - .MustBeBetween(1, 1000) - .SetHidden() - .SetDefault(10); + .SetDescription("The number of files to sign in each signing command.") + .SetArgumentHelpName("NUM") + .MustBeBetween(1, 1000) + .SetHidden() + .SetDefault(10); AddOption((v) => Shortcuts = v, "--shortcuts") .SetDescription("List of locations to install shortcuts to during setup.") @@ -93,36 +93,26 @@ public class WindowsPackCommand : PackCommand AddOption((v) => BuildMsi = v, "--msi") .SetDescription("Compile a .msi machine-wide bootstrap package."); - + AddOption(v => MsiVersionOverride = v, "--msiVersion") .SetDescription("Override the product version for the generated msi.") .SetArgumentHelpName("VERSION") .MustBeValidMsiVersion(); AddOption(v => InstWelcome = v.ToFullNameOrNull(), "--instWelcome") - .SetDescription("Set the installer package welcome content. Most formatting is not supported.") - .RequiresExtension(".md") + .SetDescription("Set the plain-text installer package welcome content.") .SetArgumentHelpName("PATH"); AddOption(v => InstLicense = v.ToFullNameOrNull(), "--instLicense") - .SetDescription("Set the installer package license content. This will be rendered as RTF content for the MSI. Formatting is done using https://github.com/uniederer/MarkdigExtensions.RtfRenderer.") - .RequiresExtension(".md") + .SetDescription("Set the installer package license content. Can be either RTF or Markdown.") .SetArgumentHelpName("PATH"); - AddOption(v => InstLicenseRtf = v.ToFullNameOrNull(), "--instLicenseRtf") - .SetDescription("Set the installer package license RTF content.") - .RequiresExtension(".rtf") - .SetArgumentHelpName("PATH") - .SetHidden(); - AddOption(v => InstReadme = v.ToFullNameOrNull(), "--instReadme") - .SetDescription("Set the installer package readme content. Most formatting is not supported.") - .RequiresExtension(".md") + .SetDescription("Set the plain-text installer package readme content.") .SetArgumentHelpName("PATH"); AddOption(v => InstConclusion = v.ToFullNameOrNull(), "--instConclusion") - .SetDescription("Set the installer package conclusion content. Most formatting is not supported.") - .RequiresExtension(".md") + .SetDescription("Set the plain-text installer package conclusion content.") .SetArgumentHelpName("PATH"); AddOption(v => InstLocation = v, "--instLocation")