mirror of
https://github.com/velopack/velopack.git
synced 2025-10-25 15:19:22 +00:00
Sort out some msi unicode issues
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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; }
|
||||
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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}}
|
||||
|
||||
@@ -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}}"/>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -61,19 +61,19 @@ public class WindowsPackCommand : PackCommand
|
||||
.SetHidden();
|
||||
|
||||
var signTemplate = AddOption<string>((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<string>((v) => SignExclude = v, "--signExclude")
|
||||
.SetDescription("A regex which excludes matched files from signing.")
|
||||
.SetHidden();
|
||||
|
||||
AddOption<int>((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<string>((v) => Shortcuts = v, "--shortcuts")
|
||||
.SetDescription("List of locations to install shortcuts to during setup.")
|
||||
@@ -93,36 +93,26 @@ public class WindowsPackCommand : PackCommand
|
||||
|
||||
AddOption<bool>((v) => BuildMsi = v, "--msi")
|
||||
.SetDescription("Compile a .msi machine-wide bootstrap package.");
|
||||
|
||||
|
||||
AddOption<string>(v => MsiVersionOverride = v, "--msiVersion")
|
||||
.SetDescription("Override the product version for the generated msi.")
|
||||
.SetArgumentHelpName("VERSION")
|
||||
.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")
|
||||
|
||||
Reference in New Issue
Block a user