mirror of
https://github.com/velopack/velopack.git
synced 2025-10-25 15:19:22 +00:00
Re-implemented msi installer based on Clowd.Squirrel
This re-implements the msi installer that was present inside of Clowd.Squirrel. This is hidden behind some hidden options.
This commit is contained in:
@@ -20,6 +20,8 @@ namespace Velopack
|
||||
Portable = 3,
|
||||
/// <summary> An application installer archive. </summary>
|
||||
Installer = 4,
|
||||
/// <summary> A Windows Installer package (.msi) for the application.</summary>
|
||||
Msi = 5,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -94,6 +94,10 @@ public class PackTask : MSBuildAsyncTask
|
||||
|
||||
public string? Compression { get; set; }
|
||||
|
||||
public bool BuildMsi { get; set; }
|
||||
|
||||
public string? MsiVersionOverride { get; set; }
|
||||
|
||||
protected override async Task<bool> ExecuteAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
//System.Diagnostics.Debugger.Launch();
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using Riok.Mapperly.Abstractions;
|
||||
using Velopack.Core;
|
||||
using Velopack.Packaging;
|
||||
using Velopack.Packaging.Unix.Commands;
|
||||
using Velopack.Packaging.Windows.Commands;
|
||||
|
||||
@@ -40,6 +40,16 @@ public static class DefaultName
|
||||
throw new PlatformNotSupportedException("Platform not supported.");
|
||||
}
|
||||
|
||||
|
||||
public static string GetSuggestedMsiName(string id, string channel, RuntimeOs os)
|
||||
{
|
||||
var suffix = GetUniqueAssetSuffix(channel);
|
||||
if (os == RuntimeOs.Windows)
|
||||
return $"{id}{suffix}-DeploymentTool.msi";
|
||||
else
|
||||
throw new PlatformNotSupportedException("Platform not supported.");
|
||||
}
|
||||
|
||||
private static string GetUniqueAssetSuffix(string channel)
|
||||
{
|
||||
return "-" + channel;
|
||||
|
||||
@@ -37,4 +37,5 @@ public class LinuxPackOptions : IPackOptions
|
||||
public string Categories { get; set; }
|
||||
|
||||
public string Compression { get; set; }
|
||||
public bool BuildMsi => false;
|
||||
}
|
||||
|
||||
@@ -35,4 +35,5 @@ public class OsxPackOptions : OsxBundleOptions, IPackOptions
|
||||
public string Channel { get; set; }
|
||||
|
||||
public string Exclude { get; set; }
|
||||
public bool BuildMsi => false;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
using System.Runtime.Versioning;
|
||||
using System.Globalization;
|
||||
using System.Runtime.Versioning;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using NuGet.Versioning;
|
||||
using Velopack.Compression;
|
||||
using Velopack.Core;
|
||||
using Velopack.Core.Abstractions;
|
||||
@@ -25,7 +29,7 @@ public class WindowsPackCommandRunner : PackageBuilder<WindowsPackOptions>
|
||||
.Select(x => x.FullName)
|
||||
.ToArray();
|
||||
|
||||
SignFilesImpl(Options, progress, filesToSign);
|
||||
SignFilesImpl(progress, filesToSign);
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
@@ -198,7 +202,7 @@ public class WindowsPackCommandRunner : PackageBuilder<WindowsPackOptions>
|
||||
SetupBundle.CreatePackageBundle(targetSetupExe, releasePkg);
|
||||
progress(50);
|
||||
Log.Debug("Signing Setup bundle");
|
||||
SignFilesImpl(Options, CoreUtil.CreateProgressDelegate(progress, 50, 100), targetSetupExe);
|
||||
SignFilesImpl(CoreUtil.CreateProgressDelegate(progress, 50, 100), targetSetupExe);
|
||||
Log.Debug($"Setup bundle created '{Path.GetFileName(targetSetupExe)}'.");
|
||||
progress(100);
|
||||
|
||||
@@ -237,6 +241,14 @@ public class WindowsPackCommandRunner : PackageBuilder<WindowsPackOptions>
|
||||
return dict;
|
||||
}
|
||||
|
||||
protected override Task CreateMsiPackage(Action<int> progress, string setupExePath, string msiPath)
|
||||
{
|
||||
if (VelopackRuntimeInfo.IsWindows) {
|
||||
CompileWixTemplateToMsi(progress, setupExePath, msiPath);
|
||||
}
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private void CreateExecutableStubForExe(string exeToCopy, string targetStubPath)
|
||||
{
|
||||
if (!File.Exists(exeToCopy)) {
|
||||
@@ -253,12 +265,12 @@ public class WindowsPackCommandRunner : PackageBuilder<WindowsPackOptions>
|
||||
}
|
||||
}
|
||||
|
||||
private void SignFilesImpl(WindowsSigningOptions options, Action<int> progress, params string[] filePaths)
|
||||
private void SignFilesImpl(Action<int> progress, params string[] filePaths)
|
||||
{
|
||||
var signParams = options.SignParameters;
|
||||
var signTemplate = options.SignTemplate;
|
||||
var signParallel = options.SignParallel;
|
||||
var trustedSignMetadataPath = options.AzureTrustedSignFile;
|
||||
var signParams = Options.SignParameters;
|
||||
var signTemplate = Options.SignTemplate;
|
||||
var signParallel = Options.SignParallel;
|
||||
var trustedSignMetadataPath = Options.AzureTrustedSignFile;
|
||||
var helper = new CodeSign(Log);
|
||||
|
||||
if (string.IsNullOrEmpty(signParams) && string.IsNullOrEmpty(signTemplate) && string.IsNullOrEmpty(trustedSignMetadataPath)) {
|
||||
@@ -317,6 +329,83 @@ public class WindowsPackCommandRunner : PackageBuilder<WindowsPackOptions>
|
||||
// return dlibPath;
|
||||
}
|
||||
|
||||
[SupportedOSPlatform("windows")]
|
||||
private void CompileWixTemplateToMsi(Action<int> progress,
|
||||
string setupExePath, string msiFilePath)
|
||||
{
|
||||
bool packageAs64Bit =
|
||||
Options.TargetRuntime.Architecture is RuntimeCpu.x64 or RuntimeCpu.arm64;
|
||||
|
||||
Log.Info($"Compiling machine-wide msi deployment tool in {(packageAs64Bit ? "64-bit" : "32-bit")} mode");
|
||||
|
||||
var outputDirectory = Path.GetDirectoryName(setupExePath);
|
||||
var setupName = Path.GetFileNameWithoutExtension(setupExePath);
|
||||
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;
|
||||
|
||||
Regex stacheRegex = new(@"\{\{(?<key>[^\}]+)\}\}", RegexOptions.Compiled);
|
||||
|
||||
var wxsFile = Path.Combine(outputDirectory, wixId + ".wxs");
|
||||
var objFile = Path.Combine(outputDirectory, wixId + ".wixobj");
|
||||
|
||||
|
||||
var msiVersion = Options.MsiVersionOverride;
|
||||
if (string.IsNullOrWhiteSpace(msiVersion)) {
|
||||
var parsedVersion = SemanticVersion.Parse(Options.PackVersion);
|
||||
msiVersion = $"{parsedVersion.Major}.{parsedVersion.Minor}.{parsedVersion.Patch}.0";
|
||||
}
|
||||
|
||||
try {
|
||||
// apply dictionary to wsx template
|
||||
var templateText = File.ReadAllText(HelperFile.WixTemplatePath);
|
||||
|
||||
var templateResult = stacheRegex.Replace(templateText, match => {
|
||||
string key = match.Groups["key"].Value;
|
||||
return key switch {
|
||||
"Id" => wixId,
|
||||
"Title" => GetEffectiveTitle(),
|
||||
"Author" => GetEffectiveAuthors(),
|
||||
"Version" => msiVersion,
|
||||
"Summary" => GetEffectiveTitle(),
|
||||
"Codepage" => $"{culture}",
|
||||
"Platform" => packageAs64Bit ? "x64" : "x86",
|
||||
"ProgramFilesFolder" => packageAs64Bit ? "ProgramFiles64Folder" : "ProgramFilesFolder",
|
||||
"Win64YesNo" => packageAs64Bit ? "yes" : "no",
|
||||
"SetupName" => setupName,
|
||||
_ when key.StartsWith("IdAsGuid") => GuidUtil.CreateGuidFromHash($"{Options.PackId}:{key.Substring(8)}").ToString(),
|
||||
_ => match.Value,
|
||||
};
|
||||
});
|
||||
|
||||
File.WriteAllText(wxsFile, templateResult, Encoding.UTF8);
|
||||
|
||||
// Candle reprocesses and compiles WiX source files into object files (.wixobj).
|
||||
Log.Info("Compiling WiX Template (candle.exe)");
|
||||
var candleCommand = $"{HelperFile.WixCandlePath} -nologo -ext WixNetFxExtension -out \"{objFile}\" \"{wxsFile}\"";
|
||||
_ = Exe.RunHostedCommand(candleCommand);
|
||||
|
||||
progress(45);
|
||||
|
||||
// Light links and binds one or more .wixobj files and creates a Windows Installer database (.msi or .msm).
|
||||
Log.Info("Linking WiX Template (light.exe)");
|
||||
var lightCommand = $"{HelperFile.WixLightPath} -ext WixNetFxExtension -spdb -sval -out \"{msiFilePath}\" \"{objFile}\"";
|
||||
_ = Exe.RunHostedCommand(lightCommand);
|
||||
|
||||
progress(90);
|
||||
|
||||
} finally {
|
||||
IoUtil.DeleteFileOrDirectoryHard(wxsFile, throwOnFailure: false);
|
||||
IoUtil.DeleteFileOrDirectoryHard(objFile, throwOnFailure: false);
|
||||
}
|
||||
progress(100);
|
||||
|
||||
}
|
||||
|
||||
protected override string[] GetMainExeSearchPaths(string packDirectory, string mainExeName)
|
||||
{
|
||||
return [
|
||||
|
||||
@@ -23,4 +23,8 @@ public class WindowsPackOptions : WindowsReleasifyOptions, INugetPackCommand, IP
|
||||
public bool NoInst { get; set; }
|
||||
|
||||
public string Shortcuts { get; set; }
|
||||
|
||||
public bool BuildMsi { get; set; }
|
||||
|
||||
public string MsiVersionOverride { get; set; }
|
||||
}
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
using Velopack.Core;
|
||||
|
||||
namespace Velopack.Packaging.Windows.Commands;
|
||||
namespace Velopack.Packaging.Windows.Commands;
|
||||
|
||||
public class WindowsReleasifyOptions : WindowsSigningOptions
|
||||
{
|
||||
|
||||
@@ -152,7 +152,7 @@ public class ResourceEdit
|
||||
|
||||
var file = PEFile.FromBytes(File.ReadAllBytes(otherExeFile));
|
||||
var image = PEImage.FromFile(file);
|
||||
_resources = image.Resources;
|
||||
_resources = image.Resources ?? new ResourceDirectory((uint) 0);
|
||||
}
|
||||
|
||||
public void Commit()
|
||||
|
||||
@@ -9,4 +9,5 @@ public interface IPackOptions : INugetPackCommand, IPlatformOptions
|
||||
string Exclude { get; set; }
|
||||
bool NoPortable { get; set; }
|
||||
bool NoInst { get; set; }
|
||||
bool BuildMsi { get; }
|
||||
}
|
||||
|
||||
@@ -69,7 +69,7 @@ public static class Exe
|
||||
var stdout = IoUtil.Retry(() => File.ReadAllText(outputFile).Trim(), 10, 1000);
|
||||
var result = (process.ExitCode, stdout, "", command);
|
||||
ProcessFailedException.ThrowIfNonZero(result);
|
||||
return result.Item2;
|
||||
return stdout;
|
||||
}
|
||||
|
||||
public static void RunHostedCommandNoWait(string command, string workDir = null)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Runtime.Versioning;
|
||||
using System.Text;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
#if !DEBUG
|
||||
@@ -70,6 +71,13 @@ public static class HelperFile
|
||||
|
||||
public static string StubExecutablePath => FindHelperFile("stub.exe");
|
||||
|
||||
[SupportedOSPlatform("windows")]
|
||||
public static string WixTemplatePath => FindHelperFile("wix\\template.wxs");
|
||||
[SupportedOSPlatform("windows")]
|
||||
public static string WixCandlePath => FindHelperFile("wix\\candle.exe");
|
||||
[SupportedOSPlatform("windows")]
|
||||
public static string WixLightPath => FindHelperFile("wix\\light.exe");
|
||||
|
||||
[SupportedOSPlatform("windows")]
|
||||
public static string SignToolPath => FindHelperFile("signing\\signtool.exe");
|
||||
|
||||
@@ -135,8 +143,14 @@ public static class HelperFile
|
||||
files = files.Where(predicate);
|
||||
|
||||
var result = files.FirstOrDefault();
|
||||
if (result == null && throwWhenNotFound)
|
||||
throw new Exception($"HelperFile could not find '{toFind}'.");
|
||||
if (result == null && throwWhenNotFound) {
|
||||
StringBuilder msg = new();
|
||||
msg.AppendLine($"HelperFile could not find '{toFind}'.");
|
||||
msg.AppendLine("Search paths:");
|
||||
foreach (var path in _searchPaths)
|
||||
msg.AppendLine($" {Path.GetFullPath(path)}");
|
||||
throw new Exception(msg.ToString());
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Security;
|
||||
using System.Security;
|
||||
using System.Text.RegularExpressions;
|
||||
using Markdig;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@@ -149,12 +148,13 @@ public abstract class PackageBuilder<T> : ICommand<T>
|
||||
});
|
||||
|
||||
Task setupTask = null;
|
||||
string setupExePath = null;
|
||||
if (!Options.NoInst && TargetOs != RuntimeOs.Linux) {
|
||||
setupTask = ctx.RunTask(
|
||||
"Building setup package",
|
||||
async (progress) => {
|
||||
var suggestedName = DefaultName.GetSuggestedSetupName(packId, channel, TargetOs);
|
||||
var path = assetCache.MakeAssetPath(suggestedName, VelopackAssetType.Installer);
|
||||
var path = setupExePath = assetCache.MakeAssetPath(suggestedName, VelopackAssetType.Installer);
|
||||
await CreateSetupPackage(progress, releasePath, packDirectory, path);
|
||||
});
|
||||
}
|
||||
@@ -177,6 +177,16 @@ public abstract class PackageBuilder<T> : ICommand<T>
|
||||
if (TargetOs != RuntimeOs.Linux && portableTask != null) await portableTask;
|
||||
if (setupTask != null) await setupTask;
|
||||
|
||||
if (!Options.NoInst && Options.BuildMsi && TargetOs == RuntimeOs.Windows) {
|
||||
await ctx.RunTask(
|
||||
"Building MSI package",
|
||||
async (progress) => {
|
||||
var msiName = DefaultName.GetSuggestedMsiName(packId, channel, TargetOs);
|
||||
var msiPath = assetCache.MakeAssetPath(msiName, VelopackAssetType.Msi);
|
||||
await CreateMsiPackage(progress, setupExePath, msiPath);
|
||||
});
|
||||
}
|
||||
|
||||
await ctx.RunTask(
|
||||
"Post-process steps",
|
||||
(progress) => {
|
||||
@@ -196,8 +206,8 @@ public abstract class PackageBuilder<T> : ICommand<T>
|
||||
protected virtual string GenerateNuspecContent()
|
||||
{
|
||||
var packId = Options.PackId;
|
||||
var packTitle = Options.PackTitle ?? Options.PackId;
|
||||
var packAuthors = Options.PackAuthors ?? Options.PackId;
|
||||
var packTitle = GetEffectiveTitle();
|
||||
var packAuthors = GetEffectiveAuthors();
|
||||
var packVersion = Options.PackVersion;
|
||||
var releaseNotes = Options.ReleaseNotes;
|
||||
var rid = Options.TargetRuntime;
|
||||
@@ -240,8 +250,8 @@ public abstract class PackageBuilder<T> : ICommand<T>
|
||||
<package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd">
|
||||
<metadata>
|
||||
<id>{packId}</id>
|
||||
<title>{packTitle ?? packId}</title>
|
||||
<description>{packTitle ?? packId}</description>
|
||||
<title>{packTitle}</title>
|
||||
<description>{packTitle}</description>
|
||||
<authors>{packAuthors ?? packId}</authors>
|
||||
<version>{packVersion}</version>
|
||||
<channel>{Options.Channel}</channel>
|
||||
@@ -277,6 +287,11 @@ public abstract class PackageBuilder<T> : ICommand<T>
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
protected virtual Task CreateMsiPackage(Action<int> progress, string setupExePath, string msiPath)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
protected virtual async Task CreateReleasePackage(Action<int> progress, string packDir, string outputPath)
|
||||
{
|
||||
var stagingDir = TempDir.CreateSubdirectory("CreateReleasePackage");
|
||||
@@ -395,4 +410,8 @@ public abstract class PackageBuilder<T> : ICommand<T>
|
||||
""";
|
||||
File.WriteAllText(Path.Combine(relsDir, ".rels"), rels);
|
||||
}
|
||||
|
||||
protected string GetEffectiveTitle() => Options.PackTitle ?? Options.PackId;
|
||||
|
||||
protected string GetEffectiveAuthors() => Options.PackAuthors ?? Options.PackId;
|
||||
}
|
||||
@@ -20,6 +20,10 @@ public class WindowsPackCommand : PackCommand
|
||||
|
||||
public string Shortcuts { get; private set; }
|
||||
|
||||
public bool BuildMsi { get; private set; }
|
||||
|
||||
public string MsiVersionOverride { get; private set; }
|
||||
|
||||
public WindowsPackCommand()
|
||||
: base("pack", "Creates a release from a folder containing application files.", RuntimeOs.Windows)
|
||||
{
|
||||
@@ -69,6 +73,17 @@ public class WindowsPackCommand : PackCommand
|
||||
.SetArgumentHelpName("PATH");
|
||||
|
||||
this.AreMutuallyExclusive(signTemplate, signParams, azTrustedSign);
|
||||
|
||||
AddOption<bool>((v) => BuildMsi = v, "--msi")
|
||||
.SetDescription("Compile a .msi machine-wide deployment tool.")
|
||||
.SetHidden()
|
||||
.SetArgumentHelpName("BITNESS");
|
||||
|
||||
AddOption<string>((v) => MsiVersionOverride = v, "--msiVersion")
|
||||
.SetDescription("Override the product version for the generated msi.")
|
||||
.SetArgumentHelpName("VERSION")
|
||||
.SetHidden()
|
||||
.MustBeValidMsiVersion();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user