Sort out some msi unicode issues

This commit is contained in:
Caelan Sayler
2025-05-27 21:03:04 +01:00
parent 76c551fda7
commit 10b2dfc63f
10 changed files with 102 additions and 132 deletions

View File

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

View File

@@ -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<WindowsPackOptions>
[SupportedOSPlatform("windows")]
private void CompileWixTemplateToMsi(Action<int> 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);

View File

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

View File

@@ -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", "&#10;").Replace("\n", "&#13;");
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),
};
}

View File

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

View File

@@ -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", "&#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

@@ -1,11 +1,6 @@
<!--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"/>
<WixLocalization Culture="en-US" Language="1033" Codepage="65001" SummaryInformationCodepage="0" xmlns="http://wixtoolset.org/schemas/v4/wxl">
<String Id="ConclusionLaunchCheckboxText" Value="Launch"/>
<!-- Message on first welcome dialog; covers both initial install and update -->
{{#if HasWelcomeMessage}}

View File

@@ -3,7 +3,6 @@
<Package Name="{{AppTitle}}"
Manufacturer="{{AppPublisher}}"
Version="{{AppMsiVersion}}"
Codepage="{{CultureLCID}}"
Language="1033"
Scope="perUserOrMachine"
UpgradeCode="{{UpgradeCodeGuid}}">
@@ -91,7 +90,7 @@
<Property Id="WIXUI_EXITDIALOGOPTIONALCHECKBOX" Value="1"/>
<!-- Check box for launching -->
<Property Id="WIXUI_EXITDIALOGOPTIONALCHECKBOXTEXT" Value="Launch {{AppTitle}}"/>
<Property Id="WIXUI_EXITDIALOGOPTIONALCHECKBOXTEXT" Value="!(loc.ConclusionLaunchCheckboxText) {{AppTitle}}"/>
<Property Id="WixAppFolder" Value="WixPerMachineFolder"/>
<Property Id="ApplicationFolderName" Value="{{AppId}}"/>

View File

@@ -15,7 +15,6 @@
<ItemGroup>
<PackageReference Include="AsmResolver.DotNet" Version="5.5.1" />
<PackageReference Include="AsmResolver.PE.Win32Resources" Version="5.5.1" />
<PackageReference Include="MarkdigExtensions.RtfRenderer" Version="1.1.0" />
<PackageReference Include="Microsoft.Security.Extensions" Version="1.4.0" />
<PackageReference Include="Handlebars.Net" Version="2.1.6" />
</ItemGroup>

View File

@@ -100,29 +100,19 @@ public class WindowsPackCommand : PackCommand
.MustBeValidMsiVersion();
AddOption<FileInfo>(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<FileInfo>(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<FileInfo>(v => InstLicenseRtf = v.ToFullNameOrNull(), "--instLicenseRtf")
.SetDescription("Set the installer package license RTF content.")
.RequiresExtension(".rtf")
.SetArgumentHelpName("PATH")
.SetHidden();
AddOption<FileInfo>(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<FileInfo>(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<InstallLocation>(v => InstLocation = v, "--instLocation")