diff --git a/src/lib-csharp/VelopackAsset.cs b/src/lib-csharp/VelopackAsset.cs
index 4e4deaec..291a9969 100644
--- a/src/lib-csharp/VelopackAsset.cs
+++ b/src/lib-csharp/VelopackAsset.cs
@@ -20,6 +20,8 @@ namespace Velopack
Portable = 3,
/// An application installer archive.
Installer = 4,
+ /// A Windows Installer package (.msi) for the application.
+ Msi = 5,
}
///
diff --git a/src/vpk/Velopack.Build/PackTask.cs b/src/vpk/Velopack.Build/PackTask.cs
index 27c6c974..76bb0f0d 100644
--- a/src/vpk/Velopack.Build/PackTask.cs
+++ b/src/vpk/Velopack.Build/PackTask.cs
@@ -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 ExecuteAsync(CancellationToken cancellationToken)
{
//System.Diagnostics.Debugger.Launch();
diff --git a/src/vpk/Velopack.Build/TaskOptionsMapper.cs b/src/vpk/Velopack.Build/TaskOptionsMapper.cs
index e4ccb446..e3164e5a 100644
--- a/src/vpk/Velopack.Build/TaskOptionsMapper.cs
+++ b/src/vpk/Velopack.Build/TaskOptionsMapper.cs
@@ -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;
diff --git a/src/vpk/Velopack.Core/DefaultName.cs b/src/vpk/Velopack.Core/DefaultName.cs
index 1440a40b..88ba36a4 100644
--- a/src/vpk/Velopack.Core/DefaultName.cs
+++ b/src/vpk/Velopack.Core/DefaultName.cs
@@ -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;
diff --git a/src/vpk/Velopack.Packaging.Unix/Commands/LinuxPackOptions.cs b/src/vpk/Velopack.Packaging.Unix/Commands/LinuxPackOptions.cs
index 19c0fa45..f05a56a5 100644
--- a/src/vpk/Velopack.Packaging.Unix/Commands/LinuxPackOptions.cs
+++ b/src/vpk/Velopack.Packaging.Unix/Commands/LinuxPackOptions.cs
@@ -37,4 +37,5 @@ public class LinuxPackOptions : IPackOptions
public string Categories { get; set; }
public string Compression { get; set; }
+ public bool BuildMsi => false;
}
diff --git a/src/vpk/Velopack.Packaging.Unix/Commands/OsxPackOptions.cs b/src/vpk/Velopack.Packaging.Unix/Commands/OsxPackOptions.cs
index 9db24541..8bc7d081 100644
--- a/src/vpk/Velopack.Packaging.Unix/Commands/OsxPackOptions.cs
+++ b/src/vpk/Velopack.Packaging.Unix/Commands/OsxPackOptions.cs
@@ -35,4 +35,5 @@ public class OsxPackOptions : OsxBundleOptions, IPackOptions
public string Channel { get; set; }
public string Exclude { get; set; }
+ public bool BuildMsi => false;
}
diff --git a/src/vpk/Velopack.Packaging.Windows/Commands/WindowsPackCommandRunner.cs b/src/vpk/Velopack.Packaging.Windows/Commands/WindowsPackCommandRunner.cs
index b4770fa4..86ed2163 100644
--- a/src/vpk/Velopack.Packaging.Windows/Commands/WindowsPackCommandRunner.cs
+++ b/src/vpk/Velopack.Packaging.Windows/Commands/WindowsPackCommandRunner.cs
@@ -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
.Select(x => x.FullName)
.ToArray();
- SignFilesImpl(Options, progress, filesToSign);
+ SignFilesImpl(progress, filesToSign);
return Task.CompletedTask;
}
@@ -198,7 +202,7 @@ public class WindowsPackCommandRunner : PackageBuilder
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
return dict;
}
+ protected override Task CreateMsiPackage(Action 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
}
}
- private void SignFilesImpl(WindowsSigningOptions options, Action progress, params string[] filePaths)
+ private void SignFilesImpl(Action 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
// return dlibPath;
}
+ [SupportedOSPlatform("windows")]
+ private void CompileWixTemplateToMsi(Action 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(@"\{\{(?[^\}]+)\}\}", 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 [
diff --git a/src/vpk/Velopack.Packaging.Windows/Commands/WindowsPackOptions.cs b/src/vpk/Velopack.Packaging.Windows/Commands/WindowsPackOptions.cs
index d70e6a4a..21b574d0 100644
--- a/src/vpk/Velopack.Packaging.Windows/Commands/WindowsPackOptions.cs
+++ b/src/vpk/Velopack.Packaging.Windows/Commands/WindowsPackOptions.cs
@@ -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; }
}
diff --git a/src/vpk/Velopack.Packaging.Windows/Commands/WindowsReleasifyOptions.cs b/src/vpk/Velopack.Packaging.Windows/Commands/WindowsReleasifyOptions.cs
index 7eecd93b..747953a1 100644
--- a/src/vpk/Velopack.Packaging.Windows/Commands/WindowsReleasifyOptions.cs
+++ b/src/vpk/Velopack.Packaging.Windows/Commands/WindowsReleasifyOptions.cs
@@ -1,6 +1,4 @@
-using Velopack.Core;
-
-namespace Velopack.Packaging.Windows.Commands;
+namespace Velopack.Packaging.Windows.Commands;
public class WindowsReleasifyOptions : WindowsSigningOptions
{
diff --git a/src/vpk/Velopack.Packaging.Windows/ResourceEdit.cs b/src/vpk/Velopack.Packaging.Windows/ResourceEdit.cs
index 9f5eda44..a72968f4 100644
--- a/src/vpk/Velopack.Packaging.Windows/ResourceEdit.cs
+++ b/src/vpk/Velopack.Packaging.Windows/ResourceEdit.cs
@@ -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()
diff --git a/src/vpk/Velopack.Packaging/Abstractions/IPackOptions.cs b/src/vpk/Velopack.Packaging/Abstractions/IPackOptions.cs
index 9b2fb687..9d37a5e7 100644
--- a/src/vpk/Velopack.Packaging/Abstractions/IPackOptions.cs
+++ b/src/vpk/Velopack.Packaging/Abstractions/IPackOptions.cs
@@ -9,4 +9,5 @@ public interface IPackOptions : INugetPackCommand, IPlatformOptions
string Exclude { get; set; }
bool NoPortable { get; set; }
bool NoInst { get; set; }
+ bool BuildMsi { get; }
}
diff --git a/src/vpk/Velopack.Packaging/Exe.cs b/src/vpk/Velopack.Packaging/Exe.cs
index b83fa78b..9db84590 100644
--- a/src/vpk/Velopack.Packaging/Exe.cs
+++ b/src/vpk/Velopack.Packaging/Exe.cs
@@ -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)
diff --git a/src/vpk/Velopack.Packaging/HelperFile.cs b/src/vpk/Velopack.Packaging/HelperFile.cs
index 159bfdec..b3e5af29 100644
--- a/src/vpk/Velopack.Packaging/HelperFile.cs
+++ b/src/vpk/Velopack.Packaging/HelperFile.cs
@@ -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;
}
diff --git a/src/vpk/Velopack.Packaging/PackageBuilder.cs b/src/vpk/Velopack.Packaging/PackageBuilder.cs
index 2992efeb..864b3a03 100644
--- a/src/vpk/Velopack.Packaging/PackageBuilder.cs
+++ b/src/vpk/Velopack.Packaging/PackageBuilder.cs
@@ -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 : ICommand
});
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 : ICommand
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 : ICommand
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 : ICommand
{packId}
- {packTitle ?? packId}
- {packTitle ?? packId}
+ {packTitle}
+ {packTitle}
{packAuthors ?? packId}
{packVersion}
{Options.Channel}
@@ -277,6 +287,11 @@ public abstract class PackageBuilder : ICommand
return Task.CompletedTask;
}
+ protected virtual Task CreateMsiPackage(Action progress, string setupExePath, string msiPath)
+ {
+ return Task.CompletedTask;
+ }
+
protected virtual async Task CreateReleasePackage(Action progress, string packDir, string outputPath)
{
var stagingDir = TempDir.CreateSubdirectory("CreateReleasePackage");
@@ -395,4 +410,8 @@ public abstract class PackageBuilder : ICommand
""";
File.WriteAllText(Path.Combine(relsDir, ".rels"), rels);
}
+
+ protected string GetEffectiveTitle() => Options.PackTitle ?? Options.PackId;
+
+ protected string GetEffectiveAuthors() => Options.PackAuthors ?? Options.PackId;
}
\ No newline at end of file
diff --git a/src/vpk/Velopack.Vpk/Commands/Packaging/WindowsPackCommand.cs b/src/vpk/Velopack.Vpk/Commands/Packaging/WindowsPackCommand.cs
index 16c3438c..b803f60d 100644
--- a/src/vpk/Velopack.Vpk/Commands/Packaging/WindowsPackCommand.cs
+++ b/src/vpk/Velopack.Vpk/Commands/Packaging/WindowsPackCommand.cs
@@ -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((v) => BuildMsi = v, "--msi")
+ .SetDescription("Compile a .msi machine-wide deployment tool.")
+ .SetHidden()
+ .SetArgumentHelpName("BITNESS");
+
+ AddOption((v) => MsiVersionOverride = v, "--msiVersion")
+ .SetDescription("Override the product version for the generated msi.")
+ .SetArgumentHelpName("VERSION")
+ .SetHidden()
+ .MustBeValidMsiVersion();
}
}
}
\ No newline at end of file
diff --git a/test/Velopack.Packaging.Tests/Velopack.Packaging.Tests.csproj b/test/Velopack.Packaging.Tests/Velopack.Packaging.Tests.csproj
index 3b773a3d..dca6bdfc 100644
--- a/test/Velopack.Packaging.Tests/Velopack.Packaging.Tests.csproj
+++ b/test/Velopack.Packaging.Tests/Velopack.Packaging.Tests.csproj
@@ -15,4 +15,10 @@
+
+
+ ..\..\vendor\wix\Microsoft.Deployment.WindowsInstaller.dll
+
+
+
diff --git a/test/Velopack.Packaging.Tests/WindowsPackTests.cs b/test/Velopack.Packaging.Tests/WindowsPackTests.cs
index 4c7b95dc..f47bcc86 100644
--- a/test/Velopack.Packaging.Tests/WindowsPackTests.cs
+++ b/test/Velopack.Packaging.Tests/WindowsPackTests.cs
@@ -2,17 +2,18 @@
using System.Globalization;
using System.Runtime.Versioning;
using System.Xml.Linq;
+using Microsoft.Deployment.WindowsInstaller;
using Microsoft.Win32;
using NuGet.Packaging;
using Velopack.Compression;
using Velopack.Core;
using Velopack.Packaging.Commands;
-using Velopack.Packaging.Exceptions;
using Velopack.Packaging.Windows.Commands;
using Velopack.Util;
using Velopack.Vpk;
using Velopack.Vpk.Logging;
using Velopack.Windows;
+using static Azure.Core.HttpHeader;
namespace Velopack.Packaging.Tests;
@@ -26,7 +27,7 @@ public class WindowsPackTests
_output = output;
}
- private WindowsPackCommandRunner GetPackRunner(ILogger logger)
+ private static WindowsPackCommandRunner GetPackRunner(ILogger logger)
{
var console = new BasicConsole(logger, new VelopackDefaults(false));
return new WindowsPackCommandRunner(logger, console);
@@ -199,6 +200,7 @@ public class WindowsPackTests
TargetRuntime = RID.Parse("win-x64"),
PackDirectory = tmpOutput,
Shortcuts = "Desktop,StartMenuRoot",
+ NoPortable = true
};
var runner = GetPackRunner(logger);
@@ -207,7 +209,7 @@ public class WindowsPackTests
var setupPath1 = Path.Combine(tmpReleaseDir, $"{id}-win-Setup.exe");
Assert.True(File.Exists(setupPath1));
- RunNoCoverage(setupPath1, new[] { "--silent", "--installto", tmpInstallDir }, Environment.CurrentDirectory, logger);
+ RunNoCoverage(setupPath1, ["--silent", "--installto", tmpInstallDir], Environment.CurrentDirectory, logger);
var updatePath = Path.Combine(tmpInstallDir, "Update.exe");
Assert.True(File.Exists(updatePath));
@@ -244,17 +246,16 @@ public class WindowsPackTests
var date = DateTime.Now.ToString("yyyyMMdd", CultureInfo.InvariantCulture);
Assert.Equal(date, installDate.Trim('\0'));
- var uninstOutput = RunNoCoverage(updatePath, new string[] { "--silent", "--uninstall" }, Environment.CurrentDirectory, logger);
+ var uninstOutput = RunNoCoverage(updatePath, ["--silent", "--uninstall"], Environment.CurrentDirectory, logger);
Assert.EndsWith(Environment.NewLine + "Y", uninstOutput); // this checks that the self-delete succeeded
Assert.False(File.Exists(startLnk));
Assert.False(File.Exists(desktopLnk));
Assert.False(File.Exists(appPath));
- using (var key2 = RegistryKey.OpenBaseKey(RegistryHive.CurrentUser, RegistryView.Default)
- .OpenSubKey(uninstallRegSubKey + "\\" + id, RegistryKeyPermissionCheck.ReadSubTree)) {
- Assert.Null(key2);
- }
+ using var key2 = RegistryKey.OpenBaseKey(RegistryHive.CurrentUser, RegistryView.Default)
+ .OpenSubKey(uninstallRegSubKey + "\\" + id, RegistryKeyPermissionCheck.ReadSubTree);
+ Assert.Null(key2);
}
[SkippableFact]
@@ -274,7 +275,7 @@ public class WindowsPackTests
var setupPath1 = Path.Combine(releaseDir, $"{id}-win-Setup.exe");
RunNoCoverage(
setupPath1,
- new string[] { "--silent", "--installto", installDir },
+ ["--silent", "--installto", installDir],
Environment.GetFolderPath(Environment.SpecialFolder.Desktop),
logger);
@@ -287,19 +288,19 @@ public class WindowsPackTests
var mvTo = Path.Combine(installDir, "packages", fileName);
File.Copy(mvFrom, mvTo);
- RunCoveredDotnet(appPath, new string[] { "--autoupdate" }, installDir, logger, exitCode: null);
+ RunCoveredDotnet(appPath, ["--autoupdate"], installDir, logger, exitCode: null);
Thread.Sleep(3000); // update.exe runs in separate process
- var chk1version = RunCoveredDotnet(appPath, new string[] { "version" }, installDir, logger);
+ var chk1version = RunCoveredDotnet(appPath, ["version"], installDir, logger);
Assert.EndsWith(Environment.NewLine + "2.0.0", chk1version);
}
[SkippableFact]
public void TestPackGeneratesValidDelta()
{
- using var _1 = TempUtil.GetTempDirectory(out var releaseDir);
Skip.IfNot(VelopackRuntimeInfo.IsWindows);
+ using var _1 = TempUtil.GetTempDirectory(out var releaseDir);
using var logger = _output.BuildLoggerFor();
string id = "SquirrelDeltaTest";
PackTestApp(id, "1.0.0", "version 1 test", releaseDir, logger);
@@ -338,7 +339,7 @@ public class WindowsPackTests
new DeltaPatchOptions {
BasePackage = Path.Combine(releaseDir, $"{id}-1.0.0-full.nupkg"),
OutputFile = output,
- PatchFiles = new[] { new FileInfo(deltaPath) },
+ PatchFiles = [new FileInfo(deltaPath)],
}).GetAwaiterResult();
// are the packages the same?
@@ -437,7 +438,7 @@ public class WindowsPackTests
var setupPath1 = Path.Combine(releaseDir, $"{id}-win-Setup.exe");
RunNoCoverage(
setupPath1,
- new string[] { "--silent", "--installto", installDir },
+ ["--silent", "--installto", installDir],
Environment.GetFolderPath(Environment.SpecialFolder.Desktop),
logger);
@@ -451,11 +452,11 @@ public class WindowsPackTests
logger.Info("TEST: v1 installed");
// check app output
- var chk1test = RunCoveredDotnet(appPath, new string[] { "test" }, installDir, logger);
+ var chk1test = RunCoveredDotnet(appPath, ["test"], installDir, logger);
Assert.EndsWith(Environment.NewLine + "version 1 test", chk1test);
- var chk1version = RunCoveredDotnet(appPath, new string[] { "version" }, installDir, logger);
+ var chk1version = RunCoveredDotnet(appPath, ["version"], installDir, logger);
Assert.EndsWith(Environment.NewLine + "1.0.0", chk1version);
- var chk1check = RunCoveredDotnet(appPath, new string[] { "check", releaseDir }, installDir, logger);
+ var chk1check = RunCoveredDotnet(appPath, ["check", releaseDir], installDir, logger);
Assert.EndsWith(Environment.NewLine + "no updates", chk1check);
logger.Info("TEST: v1 output verified");
@@ -463,7 +464,7 @@ public class WindowsPackTests
PackTestApp(id, "2.0.0", "version 2 test", releaseDir, logger);
// check can find v2 update
- var chk2check = RunCoveredDotnet(appPath, new string[] { "check", releaseDir }, installDir, logger);
+ var chk2check = RunCoveredDotnet(appPath, ["check", releaseDir], installDir, logger);
Assert.EndsWith(Environment.NewLine + "update: 2.0.0", chk2check);
logger.Info("TEST: found v2 update");
@@ -476,17 +477,17 @@ public class WindowsPackTests
// perform full update, check that we get v3
// apply should fail if there's not an update downloaded
- RunCoveredDotnet(appPath, new string[] { "apply", releaseDir }, installDir, logger, exitCode: -1);
- RunCoveredDotnet(appPath, new string[] { "download", releaseDir }, installDir, logger);
- RunCoveredDotnet(appPath, new string[] { "apply", releaseDir }, installDir, logger, exitCode: null);
+ RunCoveredDotnet(appPath, ["apply", releaseDir], installDir, logger, exitCode: -1);
+ RunCoveredDotnet(appPath, ["download", releaseDir], installDir, logger);
+ RunCoveredDotnet(appPath, ["apply", releaseDir], installDir, logger, exitCode: null);
logger.Info("TEST: v3 applied");
// check app output
- var chk3test = RunCoveredDotnet(appPath, new string[] { "test" }, installDir, logger);
+ var chk3test = RunCoveredDotnet(appPath, ["test"], installDir, logger);
Assert.EndsWith(Environment.NewLine + "version 3 test", chk3test);
- var chk3version = RunCoveredDotnet(appPath, new string[] { "version" }, installDir, logger);
+ var chk3version = RunCoveredDotnet(appPath, ["version"], installDir, logger);
Assert.EndsWith(Environment.NewLine + "3.0.0", chk3version);
- var ch3check2 = RunCoveredDotnet(appPath, new string[] { "check", releaseDir }, installDir, logger);
+ var ch3check2 = RunCoveredDotnet(appPath, ["check", releaseDir], installDir, logger);
Assert.EndsWith(Environment.NewLine + "no updates", ch3check2);
logger.Info("TEST: v3 output verified");
@@ -504,7 +505,7 @@ public class WindowsPackTests
// uninstall
var updatePath = Path.Combine(installDir, "Update.exe");
- RunNoCoverage(updatePath, new string[] { "--silent", "--uninstall" }, Environment.CurrentDirectory, logger);
+ RunNoCoverage(updatePath, ["--silent", "--uninstall"], Environment.CurrentDirectory, logger);
logger.Info("TEST: uninstalled / complete");
}
@@ -543,8 +544,8 @@ public class WindowsPackTests
using var _1 = TempUtil.GetTempDirectory(out var releaseDir);
PackTestApp("LegacyTestApp", "2.0.0", "hello!", releaseDir, logger);
- RunNoCoverage(appExe, new string[] { "download", releaseDir }, currentDir, logger, exitCode: 0);
- RunNoCoverage(appExe, new string[] { "apply", releaseDir }, currentDir, logger, exitCode: null);
+ RunNoCoverage(appExe, ["download", releaseDir], currentDir, logger, exitCode: 0);
+ RunNoCoverage(appExe, ["apply", releaseDir], currentDir, logger, exitCode: null);
logger.Info("TEST: " + DateTime.Now.ToLongTimeString());
@@ -555,7 +556,7 @@ public class WindowsPackTests
logger.Info("TEST: " + DateTime.Now.ToLongTimeString());
if (origDirName != "current") {
- Assert.True(!Directory.Exists(currentDir));
+ Assert.False(Directory.Exists(currentDir));
currentDir = Path.Combine(rootDir, "current");
}
@@ -569,10 +570,90 @@ public class WindowsPackTests
// this is the file written by TestApp when it's detected the squirrel restart. if this is here, everything went smoothly.
Assert.True(File.Exists(Path.Combine(rootDir, "restarted")));
- var chk3version = RunNoCoverage(appExe, new string[] { "version" }, currentDir, logger);
+ var chk3version = RunNoCoverage(appExe, ["version"], currentDir, logger);
Assert.EndsWith(Environment.NewLine + "2.0.0", chk3version);
}
+ [SkippableFact]
+ public async Task TestPackGeneratesMsi()
+ {
+ Skip.IfNot(VelopackRuntimeInfo.IsWindows);
+
+ using var logger = _output.BuildLoggerFor();
+
+ using var _1 = TempUtil.GetTempDirectory(out var tmpOutput);
+ using var _2 = TempUtil.GetTempDirectory(out var tmpReleaseDir);
+
+ var exe = "testapp.exe";
+ var pdb = Path.ChangeExtension(exe, ".pdb");
+ var id = "Test.Squirrel-App";
+ var version = "1.2.3";
+
+ PathHelper.CopyRustAssetTo(exe, tmpOutput);
+ PathHelper.CopyRustAssetTo(pdb, tmpOutput);
+
+ var options = new WindowsPackOptions {
+ EntryExecutableName = exe,
+ ReleaseDir = new DirectoryInfo(tmpReleaseDir),
+ PackId = id,
+ PackVersion = version,
+ TargetRuntime = RID.Parse("win-x64"),
+ PackDirectory = tmpOutput,
+ Shortcuts = "Desktop,StartMenuRoot",
+ BuildMsi = true
+ };
+
+ var runner = GetPackRunner(logger);
+ await runner.Run(options);
+
+ string msiPath = Path.Combine(tmpReleaseDir, $"{id}-win-DeploymentTool.msi");
+ Assert.True(File.Exists(msiPath));
+ using Database db = new Database(msiPath);
+ var msiVersion = db.ExecuteScalar("SELECT `Value` FROM `Property` WHERE `Property` = 'ProductVersion'") as string;
+ Assert.Equal("1.2.3.0", msiVersion);
+ }
+
+ [SkippableFact]
+ public async Task TestPackGeneratesMsiWithSpecifiedVersion()
+ {
+ Skip.IfNot(VelopackRuntimeInfo.IsWindows);
+
+ using var logger = _output.BuildLoggerFor();
+
+ using var _1 = TempUtil.GetTempDirectory(out var tmpOutput);
+ using var _2 = TempUtil.GetTempDirectory(out var tmpReleaseDir);
+
+ var exe = "testapp.exe";
+ var pdb = Path.ChangeExtension(exe, ".pdb");
+ var id = "Test.Squirrel-App";
+ var version = "1.0.0";
+
+ PathHelper.CopyRustAssetTo(exe, tmpOutput);
+ PathHelper.CopyRustAssetTo(pdb, tmpOutput);
+
+ var options = new WindowsPackOptions {
+ EntryExecutableName = exe,
+ ReleaseDir = new DirectoryInfo(tmpReleaseDir),
+ PackId = id,
+ PackVersion = version,
+ TargetRuntime = RID.Parse("win-x64"),
+ PackDirectory = tmpOutput,
+ Shortcuts = "Desktop,StartMenuRoot",
+ BuildMsi = true,
+ MsiVersionOverride = "4.5.6.1"
+ };
+
+ var runner = GetPackRunner(logger);
+ await runner.Run(options);
+
+ string msiPath = Path.Combine(tmpReleaseDir, $"{id}-win-DeploymentTool.msi");
+ Assert.True(File.Exists(msiPath));
+
+ using Database db = new Database(msiPath);
+ var msiVersion = db.ExecuteScalar("SELECT `Value` FROM `Property` WHERE `Property` = 'ProductVersion'") as string;
+ Assert.Equal("4.5.6.1", msiVersion);
+ }
+
private static string ReadFileWithRetry(string path, ILogger logger)
{
return IoUtil.Retry(
@@ -610,7 +691,7 @@ public class WindowsPackTests
// return RunImpl(psi, logger, exitCode);
//}
- private string RunImpl(ProcessStartInfo psi, ILogger logger, int? exitCode = 0)
+ private static string RunImpl(ProcessStartInfo psi, ILogger logger, int? exitCode = 0)
{
//logger.Info($"TEST: Running {psi.FileName} {psi.ArgumentList.Aggregate((a, b) => $"{a} {b}")}");
//using var p = Process.Start(psi);
@@ -679,7 +760,7 @@ public class WindowsPackTests
}
}
- private string RunCoveredDotnet(string exe, string[] args, string workingDir, ILogger logger, int? exitCode = 0)
+ private static string RunCoveredDotnet(string exe, string[] args, string workingDir, ILogger logger, int? exitCode = 0)
{
var outputfile = PathHelper.GetTestRootPath($"coverage.rundotnet.{RandomString(8)}.xml");
@@ -703,7 +784,7 @@ public class WindowsPackTests
return RunImpl(psi, logger, exitCode);
}
- private static Random _random = new Random();
+ private static readonly Random _random = Random.Shared;
private static string RandomString(int length)
{
@@ -727,7 +808,7 @@ public class WindowsPackTests
return RunImpl(psi, logger, exitCode);
}
- private void PackTestApp(string id, string version, string testString, string releaseDir, ILogger logger, bool addNewFile = false)
+ private static void PackTestApp(string id, string version, string testString, string releaseDir, ILogger logger, bool addNewFile = false)
{
var projDir = PathHelper.GetTestRootPath("TestApp");
var testStringFile = Path.Combine(projDir, "Const.cs");
diff --git a/vendor/wix/Microsoft.Deployment.Resources.dll b/vendor/wix/Microsoft.Deployment.Resources.dll
new file mode 100644
index 00000000..1ce0ed6d
Binary files /dev/null and b/vendor/wix/Microsoft.Deployment.Resources.dll differ
diff --git a/vendor/wix/Microsoft.Deployment.WindowsInstaller.dll b/vendor/wix/Microsoft.Deployment.WindowsInstaller.dll
new file mode 100644
index 00000000..742169d2
Binary files /dev/null and b/vendor/wix/Microsoft.Deployment.WindowsInstaller.dll differ
diff --git a/vendor/wix/WixNetFxExtension.dll b/vendor/wix/WixNetFxExtension.dll
new file mode 100644
index 00000000..8d8b60e9
Binary files /dev/null and b/vendor/wix/WixNetFxExtension.dll differ
diff --git a/vendor/wix/candle.exe b/vendor/wix/candle.exe
new file mode 100644
index 00000000..61be7aae
Binary files /dev/null and b/vendor/wix/candle.exe differ
diff --git a/vendor/wix/candle.exe.config b/vendor/wix/candle.exe.config
new file mode 100644
index 00000000..91db2350
--- /dev/null
+++ b/vendor/wix/candle.exe.config
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/vendor/wix/darice.cub b/vendor/wix/darice.cub
new file mode 100644
index 00000000..dab45677
Binary files /dev/null and b/vendor/wix/darice.cub differ
diff --git a/vendor/wix/light.exe b/vendor/wix/light.exe
new file mode 100644
index 00000000..5874d3a0
Binary files /dev/null and b/vendor/wix/light.exe differ
diff --git a/vendor/wix/light.exe.config b/vendor/wix/light.exe.config
new file mode 100644
index 00000000..91db2350
--- /dev/null
+++ b/vendor/wix/light.exe.config
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/vendor/wix/template.wxs b/vendor/wix/template.wxs
new file mode 100644
index 00000000..0a7440b1
--- /dev/null
+++ b/vendor/wix/template.wxs
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/vendor/wix/wconsole.dll b/vendor/wix/wconsole.dll
new file mode 100644
index 00000000..b5cc7a94
Binary files /dev/null and b/vendor/wix/wconsole.dll differ
diff --git a/vendor/wix/winterop.dll b/vendor/wix/winterop.dll
new file mode 100644
index 00000000..f0897060
Binary files /dev/null and b/vendor/wix/winterop.dll differ
diff --git a/vendor/wix/wix.dll b/vendor/wix/wix.dll
new file mode 100644
index 00000000..606abfdb
Binary files /dev/null and b/vendor/wix/wix.dll differ