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:
Kevin Bost
2025-01-26 21:52:58 -08:00
committed by Caelan
parent 55fbc17450
commit e9871e1656
29 changed files with 375 additions and 56 deletions

View File

@@ -15,4 +15,10 @@
<PackageReference Include="System.Formats.Asn1" Version="9.0.1" />
</ItemGroup>
<ItemGroup>
<Reference Include="Microsoft.Deployment.WindowsInstaller">
<HintPath>..\..\vendor\wix\Microsoft.Deployment.WindowsInstaller.dll</HintPath>
</Reference>
</ItemGroup>
</Project>

View File

@@ -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<WindowsPackTests>();
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<WindowsPackTests>();
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<WindowsPackTests>();
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");