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