From d713596bd891a69c238d3cdd613ef7cff4314726 Mon Sep 17 00:00:00 2001 From: Caelan Sayler Date: Mon, 20 Dec 2021 12:36:17 +0000 Subject: [PATCH] Re-introduce WIX/MSI support in the new setup architecture --- build.ps1 | 25 +++++---- src/Setup/Setup.cpp | 13 ++--- src/Squirrel/Internal/BundledSetupInfo.cs | 9 ++-- src/Squirrel/Internal/HelperExe.cs | 44 ++++++++++++++- src/Squirrel/Internal/Utility.cs | 8 +-- src/SquirrelCli/CopStache.cs | 29 ++++++++++ src/SquirrelCli/Options.cs | 25 ++++++--- src/SquirrelCli/Program.cs | 65 +++++++++++++++++------ src/Update/Program.cs | 45 +++++++++++++--- src/Update/StartupOption.cs | 13 ++--- vendor/wix/template.wxs | 4 +- 11 files changed, 217 insertions(+), 63 deletions(-) create mode 100644 src/SquirrelCli/CopStache.cs diff --git a/build.ps1 b/build.ps1 index 63c3c1a1..1520d907 100644 --- a/build.ps1 +++ b/build.ps1 @@ -19,23 +19,22 @@ foreach ($Folder in $Folders) { &"$MSBuildPath" /verbosity:minimal /restore /p:Configuration=Release # Build single-exe packaged projects -dotnet publish -v minimal -c Release -r win-x86 --self-contained=true "$PSScriptRoot\src\Update\Update.csproj" -o "$Out" +# New-Item -Path "$Out" -Name "win-x86" -ItemType "directory" +$BinOut = $Out dotnet publish -v minimal -c Release -r win-x86 --self-contained=true "$PSScriptRoot\src\SquirrelCli\SquirrelCli.csproj" -o "$Out" +dotnet publish -v minimal -c Release -r win-x86 --self-contained=true "$PSScriptRoot\src\Update\Update.csproj" -o "$BinOut" # Copy over all files we need -Copy-Item "$In\Win32\Setup.exe" -Destination "$Out" -# Copy-Item "$In\Win32\Setup.pdb" -Destination "$Out" -Copy-Item "$In\Win32\StubExecutable.exe" -Destination "$Out" -# Copy-Item "$In\Win32\WriteZipToSetup.exe" -Destination "$Out" -# Copy-Item "$In\Win32\WriteZipToSetup.pdb" -Destination "$Out" - -Copy-Item -Path "$PSScriptRoot\vendor\7zip\*" -Destination "$Out" -Recurse -# Copy-Item -Path "$PSScriptRoot\vendor\wix\*" -Destination "$Out" -Recurse -Copy-Item "$PSScriptRoot\vendor\nuget.exe" -Destination "$Out" -Copy-Item "$PSScriptRoot\vendor\rcedit.exe" -Destination "$Out" -Copy-Item "$PSScriptRoot\vendor\signtool.exe" -Destination "$Out" -Copy-Item "$PSScriptRoot\vendor\singlefilehost.exe" -Destination "$Out" +Copy-Item -Path "$PSScriptRoot\vendor\7zip\*" -Destination "$BinOut" -Recurse +Copy-Item -Path "$PSScriptRoot\vendor\wix\*" -Destination "$BinOut" -Recurse +Copy-Item "$In\Win32\Setup.exe" -Destination "$BinOut" +Copy-Item "$In\Win32\StubExecutable.exe" -Destination "$BinOut" +Copy-Item "$PSScriptRoot\vendor\nuget.exe" -Destination "$BinOut" +Copy-Item "$PSScriptRoot\vendor\rcedit.exe" -Destination "$BinOut" +Copy-Item "$PSScriptRoot\vendor\signtool.exe" -Destination "$BinOut" +Copy-Item "$PSScriptRoot\vendor\singlefilehost.exe" -Destination "$BinOut" Remove-Item "$Out\*.pdb" +Remove-Item "$BinOut\*.pdb" Write-Output "Successfully copied files to './build/publish'" diff --git a/src/Setup/Setup.cpp b/src/Setup/Setup.cpp index 55622d28..4437d2f3 100644 --- a/src/Setup/Setup.cpp +++ b/src/Setup/Setup.cpp @@ -84,7 +84,7 @@ void unzipSingleFile(BYTE* zipBuf, DWORD cZipBuf, wstring fileLocation, std::fun } // https://stackoverflow.com/a/17387176/184746 -void ThrowLastWin32Error() +void throwLastWin32Error() { DWORD errorMessageID = ::GetLastError(); if (errorMessageID == 0) { @@ -110,7 +110,7 @@ void wexec(const wchar_t* cmd) PROCESS_INFORMATION pi = { 0 }; if (!CreateProcess(NULL, szCmdline, NULL, NULL, false, 0, NULL, NULL, &si, &pi)) { - ThrowLastWin32Error(); + throwLastWin32Error(); } WaitForSingleObject(pi.hProcess, INFINITE); @@ -143,18 +143,19 @@ int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, PWSTR pCmdLine }); unzipSingleFile(zipBuf, cZipBuf, updaterPath, endsWithSquirrel); - // run installer - wstring cmd = L"\"" + updaterPath + L"\" --setup \"" + myPath + L"\""; + // run installer and forward our command line arguments + wstring cmd = L"\"" + updaterPath + L"\" --setup \"" + myPath + L"\" " + pCmdLine; wexec(cmd.c_str()); } catch (std::exception ex) { string msg = ex.what(); // just use the _A function because std does not have a wexception - MessageBoxA(0, string("An error occurred preventing setup from starting: " + msg + ". Please contact the application author.").c_str(), "Setup Error", MB_OK | MB_ICONERROR); + MessageBoxA(0, string("An error occurred while running setup: " + msg + ". Please contact the application author.").c_str(), "Setup Error", MB_OK | MB_ICONERROR); } catch (...) { - MessageBox(0, L"An unknown error occurred while starting setup. Please contact the application author.", L"Setup Error", MB_OK | MB_ICONERROR); + MessageBox(0, L"An unknown error occurred while running setup. Please contact the application author.", L"Setup Error", MB_OK | MB_ICONERROR); } + // clean-up after ourselves DeleteFile(updaterPath.c_str()); } \ No newline at end of file diff --git a/src/Squirrel/Internal/BundledSetupInfo.cs b/src/Squirrel/Internal/BundledSetupInfo.cs index 43c2449c..9bf8d856 100644 --- a/src/Squirrel/Internal/BundledSetupInfo.cs +++ b/src/Squirrel/Internal/BundledSetupInfo.cs @@ -6,7 +6,8 @@ namespace Squirrel.Lib { internal class BundledSetupInfo { - public string AppName { get; set; } = "This Application"; + public string AppId { get; set; } + public string AppFriendlyName { get; set; } = "This Application"; public string[] RequiredFrameworks { get; set; } = new string[0]; public string BundledPackageName { get; set; } public byte[] BundledPackageBytes { get; set; } @@ -20,7 +21,8 @@ namespace Squirrel.Lib { var bundle = new BundledSetupInfo(); using var reader = new ResourceReader(exePath); - bundle.AppName = ReadString(reader, 201); + bundle.AppId = ReadString(reader, 200); + bundle.AppFriendlyName = ReadString(reader, 201); bundle.SplashImageBytes = ReadBytes(reader, 202); bundle.RequiredFrameworks = ReadString(reader, 203)?.Split(',') ?? new string[0]; bundle.BundledPackageName = ReadString(reader, 204); @@ -45,7 +47,8 @@ namespace Squirrel.Lib public void WriteToFile(string exePath) { using var writer = new ResourceUpdater(exePath); - WriteValue(writer, 201, AppName); + WriteValue(writer, 200, AppId); + WriteValue(writer, 201, AppFriendlyName); WriteValue(writer, 202, SplashImageBytes); WriteValue(writer, 203, String.Join(",", RequiredFrameworks ?? new string[0])); WriteValue(writer, 204, BundledPackageName); diff --git a/src/Squirrel/Internal/HelperExe.cs b/src/Squirrel/Internal/HelperExe.cs index 5a41ea9c..a5d54833 100644 --- a/src/Squirrel/Internal/HelperExe.cs +++ b/src/Squirrel/Internal/HelperExe.cs @@ -19,12 +19,15 @@ namespace Squirrel public static string UpdatePath => FindHelperExecutable("Update.exe", _searchPaths); public static string StubExecutablePath => FindHelperExecutable("StubExecutable.exe", _searchPaths); public static string SingleFileHostPath => FindHelperExecutable("singlefilehost.exe", _searchPaths); + public static string WixTemplatePath => FindHelperExecutable("template.wxs", _searchPaths); // private so we don't expose paths to internal tools. these should be exposed as a helper function private static string NugetPath => FindHelperExecutable("NuGet.exe", _searchPaths); private static string RceditPath => FindHelperExecutable("rcedit.exe", _searchPaths); private static string SevenZipPath => FindHelperExecutable("7z.exe", _searchPaths); private static string SignToolPath => FindHelperExecutable("signtool.exe", _searchPaths); + private static string WixCandlePath => FindHelperExecutable("candle.exe", _searchPaths); + private static string WixLightPath => FindHelperExecutable("light.exe", _searchPaths); private static List _searchPaths = new List(); private static IFullLogger Log = SquirrelLocator.CurrentMutable.GetService().GetLogger(typeof(HelperExe)); @@ -42,6 +45,8 @@ namespace Squirrel AddSearchPath(Path.Combine(AssemblyRuntimeInfo.BaseDirectory, "..", "..", "..", "vendor")); AddSearchPath(Path.Combine(AssemblyRuntimeInfo.BaseDirectory, "..", "..", "..", "vendor", "7zip")); AddSearchPath(Path.Combine(AssemblyRuntimeInfo.BaseDirectory, "..", "..", "..", "vendor", "wix")); +#else + AddSearchPath(Path.Combine(AssemblyRuntimeInfo.BaseDirectory, "bin")); #endif } } @@ -59,7 +64,9 @@ namespace Squirrel additionalDirs = additionalDirs ?? Enumerable.Empty(); var dirs = (new[] { AppContext.BaseDirectory, Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location) }) - .Concat(additionalDirs ?? Enumerable.Empty()).Select(Path.GetFullPath); + .Concat(additionalDirs ?? Enumerable.Empty()) + .Where(d => !String.IsNullOrEmpty(d)) + .Select(Path.GetFullPath); var exe = @".\" + toFind; var result = dirs @@ -72,6 +79,41 @@ namespace Squirrel return result ?? exe; } + public static async Task CompileWixTemplateToMsi(string wxsTarget, string outputFile) + { + var workingDir = Path.GetDirectoryName(wxsTarget); + var targetName = Path.GetFileNameWithoutExtension(wxsTarget); + var objFile = Path.Combine(workingDir, targetName + ".wixobj"); + + try { + // Candle reprocesses and compiles WiX source files into object files (.wixobj). + var candleParams = new string[] { "-nologo", "-ext", "WixNetFxExtension", "-out", objFile, wxsTarget }; + var processResult = await Utility.InvokeProcessAsync(WixCandlePath, candleParams, CancellationToken.None, workingDir); + + if (processResult.Item1 != 0) { + var msg = String.Format( + "Failed to compile WiX template, command invoked was: '{0} {1}'\n\nOutput was:\n{2}", + "candle.exe", candleParams, processResult.Item2); + + throw new Exception(msg); + } + + // Light links and binds one or more .wixobj files and creates a Windows Installer database (.msi or .msm). + var lightParams = new string[] { "-ext", "WixNetFxExtension", "-spdb", "-sval", "-out", outputFile, objFile }; + processResult = await Utility.InvokeProcessAsync(WixLightPath, lightParams, CancellationToken.None, workingDir); + + if (processResult.Item1 != 0) { + var msg = String.Format( + "Failed to link WiX template, command invoked was: '{0} {1}'\n\nOutput was:\n{2}", + "light.exe", lightParams, processResult.Item2); + + throw new Exception(msg); + } + } finally { + Utility.DeleteFileHarder(objFile); + } + } + public static async Task SetExeIcon(string exePath, string iconPath) { var args = new[] { Path.GetFullPath(exePath), "--set-icon", iconPath }; diff --git a/src/Squirrel/Internal/Utility.cs b/src/Squirrel/Internal/Utility.cs index 75186c4d..c64cf099 100644 --- a/src/Squirrel/Internal/Utility.cs +++ b/src/Squirrel/Internal/Utility.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics.Contracts; @@ -224,12 +224,12 @@ namespace Squirrel /// This function will escape command line arguments such that CommandLineToArgvW is guarenteed to produce the same output as the 'args' parameter. /// It also will automatically execute wine if trying to run an exe while not on windows. /// - public static Task<(int ExitCode, string StdOutput)> InvokeProcessAsync(string fileName, IEnumerable args, CancellationToken ct) + public static Task<(int ExitCode, string StdOutput)> InvokeProcessAsync(string fileName, IEnumerable args, CancellationToken ct, string workingDirectory = "") { if (Environment.OSVersion.Platform != PlatformID.Win32NT && fileName.EndsWith(".exe", StringComparison.OrdinalIgnoreCase)) { - return InvokeProcessUnsafeAsync(CreateProcessStartInfo("wine", ArgsToCommandLine(new string[] { fileName }.Concat(args))), ct); + return InvokeProcessUnsafeAsync(CreateProcessStartInfo("wine", ArgsToCommandLine(new string[] { fileName }.Concat(args)), workingDirectory), ct); } else { - return InvokeProcessUnsafeAsync(CreateProcessStartInfo(fileName, ArgsToCommandLine(args)), ct); + return InvokeProcessUnsafeAsync(CreateProcessStartInfo(fileName, ArgsToCommandLine(args), workingDirectory), ct); } } diff --git a/src/SquirrelCli/CopStache.cs b/src/SquirrelCli/CopStache.cs new file mode 100644 index 00000000..d6a6366d --- /dev/null +++ b/src/SquirrelCli/CopStache.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security; +using System.Text; +using System.Threading.Tasks; + +namespace SquirrelCli +{ + internal static class CopStache + { + public static string Render(string template, Dictionary identifiers) + { + var buf = new StringBuilder(); + + foreach (var line in template.Split('\n')) { + identifiers["RandomGuid"] = (Guid.NewGuid()).ToString(); + + foreach (var key in identifiers.Keys) { + buf.Replace("{{" + key + "}}", SecurityElement.Escape(identifiers[key])); + } + + buf.AppendLine(line); + } + + return buf.ToString(); + } + } +} diff --git a/src/SquirrelCli/Options.cs b/src/SquirrelCli/Options.cs index d9afb19d..b410e6c0 100644 --- a/src/SquirrelCli/Options.cs +++ b/src/SquirrelCli/Options.cs @@ -27,9 +27,9 @@ namespace SquirrelCli public string updateIcon { get; private set; } public string appIcon { get; private set; } public string setupIcon { get; private set; } - public string setupName { get; private set; } = "Setup"; public bool noDelta { get; private set; } public bool allowUnaware { get; private set; } + public string msi { get; private set; } public ReleasifyOptions() { @@ -46,21 +46,34 @@ namespace SquirrelCli Add("updateIcon=", "ICO file that will be used for Update.exe", v => updateIcon = v); Add("appIcon=", "ICO file that will be used in the 'Apps and Features' list.", v => appIcon = v); Add("setupIcon=", "ICO file that will be used for Setup.exe", v => setupIcon = v); - Add("setupName=", "The name of the app installer exe without the extension (default: 'Setup')", v => setupName = v); Add("splashImage=", "Image to be displayed during installation (can be jpg, png, gif, etc)", v => splashImage = v); Add("noDelta", "Skip the generation of delta packages to save time", v => noDelta = true); Add("addSearchPath=", "Add additional search directories when looking for helper exe's such as Setup.exe, Update.exe, etc", v => HelperExe.AddSearchPath(v)); + Add("msi=", "Will generate a .msi machine-wide deployment tool. If installed on a machine, this msi will silently install the releasified " + + "app each time a user signs in if it is not already installed. This value must be either 'x86' 'x64'.", v => msi = v.ToLower()); } public override void Validate() + { + ValidateInternal(true); + } + + protected virtual void ValidateInternal(bool checkPackage) { IsValidFile(nameof(appIcon), ".ico"); IsValidFile(nameof(setupIcon), ".ico"); IsValidFile(nameof(updateIcon), ".ico"); IsValidFile(nameof(splashImage)); IsValidUrl(nameof(baseUrl)); - IsRequired(nameof(package)); - IsValidFile(nameof(package), ".nupkg"); + + if (checkPackage) { + IsRequired(nameof(package)); + IsValidFile(nameof(package), ".nupkg"); + } + + if (!String.IsNullOrEmpty(msi)) + if (!msi.Equals("x86") && !msi.Equals("x64")) + throw new OptionValidationException($"Argument 'msi': File must be either 'x86' or 'x64'. Actual value was '{msi}'."); } } @@ -88,9 +101,7 @@ namespace SquirrelCli public override void Validate() { IsRequired(nameof(packName), nameof(packVersion), nameof(packAuthors), nameof(packDirectory)); - IsValidFile(nameof(setupIcon), ".ico"); - IsValidFile(nameof(splashImage)); - IsValidUrl(nameof(baseUrl)); + base.ValidateInternal(false); } } diff --git a/src/SquirrelCli/Program.cs b/src/SquirrelCli/Program.cs index a92ce371..6b810a9e 100644 --- a/src/SquirrelCli/Program.cs +++ b/src/SquirrelCli/Program.cs @@ -265,7 +265,7 @@ namespace SquirrelCli ReleaseEntry.WriteReleaseFile(releaseEntries, releaseFilePath); var bundledzp = new ZipPackage(package); - var targetSetupExe = Path.Combine(di.FullName, options.setupName + ".exe"); + var targetSetupExe = Path.Combine(di.FullName, $"{bundledzp.Id}Setup.exe"); File.Copy(HelperExe.SetupPath, targetSetupExe, true); Utility.Retry(() => HelperExe.SetPEVersionBlockFromPackageInfo(targetSetupExe, bundledzp, setupIcon).Wait()); @@ -274,7 +274,8 @@ namespace SquirrelCli Log.Info($"Creating Setup bundle"); var infosave = new BundledSetupInfo() { - AppName = bundledzp.Title ?? bundledzp.Id, + AppId = bundledzp.Id, + AppFriendlyName = bundledzp.Title ?? bundledzp.Id, BundledPackageBytes = File.ReadAllBytes(newestReleasePath), BundledPackageName = Path.GetFileName(newestReleasePath), RequiredFrameworks = requiredFrameworks.ToArray(), @@ -287,23 +288,57 @@ namespace SquirrelCli HelperExe.SignPEFile(targetSetupExe, signingOpts).Wait(); - //setupPath = @"C:\Source\Clowd\clowd-windows\modules\Clowd.Squirrel\build\Debug\Win32\Setup.exe"; - //string onlineSource = "https://caesay.com/clowd-updates"; - //byte[] offlineSourcesZip = new byte[0]; - - //SetupResourceWriter.WriteZipToSetup(targetSetupExe, newestReleasePath, frameworkVersion, backgroundGif); - - //if (generateMsi) { - // createMsiPackage(targetSetupExe, new ZipPackage(package), packageAs64Bit).Wait(); - - // if (signingOpts != null) { - // signPEFile(targetSetupExe.Replace(".exe", ".msi"), signingOpts).Wait(); - // } - //} + if (!String.IsNullOrEmpty(options.msi)) { + bool x64 = options.msi.Equals("x64"); + createMsiPackage(targetSetupExe, new ZipPackage(package), x64).Wait(); + HelperExe.SignPEFile(targetSetupExe.Replace(".exe", ".msi"), signingOpts).Wait(); + } Log.Info("Done"); } + static async Task createMsiPackage(string setupExe, IPackage package, bool packageAs64Bit) + { + Log.Info($"Compiling machine-wide msi deployment tool in {(packageAs64Bit ? "64-bit" : "32-bit")} mode"); + + var setupExeDir = Path.GetDirectoryName(setupExe); + var setupName = Path.GetFileNameWithoutExtension(setupExe); + var company = String.Join(",", package.Authors); + var culture = CultureInfo.GetCultureInfo(package.Language ?? "").TextInfo.ANSICodePage; + + var templateText = File.ReadAllText(HelperExe.WixTemplatePath); + var templateData = new Dictionary { + { "Id", package.Id }, + { "Title", package.Title }, + { "Author", company }, + { "Version", Regex.Replace(package.Version.ToString(), @"-.*$", "") }, + { "Summary", package.Summary ?? package.Description ?? package.Id }, + { "Codepage", $"{culture}" }, + { "Platform", packageAs64Bit ? "x64" : "x86" }, + { "ProgramFilesFolder", packageAs64Bit ? "ProgramFiles64Folder" : "ProgramFilesFolder" }, + { "Win64YesNo", packageAs64Bit ? "yes" : "no" }, + { "SetupName", setupName } + }; + + // NB: We need some GUIDs that are based on the package ID, but unique (i.e. + // "Unique but consistent"). + for (int i = 1; i <= 10; i++) { + templateData[String.Format("IdAsGuid{0}", i)] = Utility.CreateGuidFromHash(String.Format("{0}:{1}", package.Id, i)).ToString(); + } + + var templateResult = CopStache.Render(templateText, templateData); + + var wxsTarget = Path.Combine(setupExeDir, setupName + ".wxs"); + File.WriteAllText(wxsTarget, templateResult, Encoding.UTF8); + + try { + var msiTarget = Path.Combine(setupExeDir, setupName + "_DeploymentTool.msi"); + await HelperExe.CompileWixTemplateToMsi(wxsTarget, msiTarget); + } finally { + File.Delete(wxsTarget); + } + } + static async Task createExecutableStubForExe(string exeToCopy) { var target = Path.Combine( diff --git a/src/Update/Program.cs b/src/Update/Program.cs index a811847b..1e62e69d 100644 --- a/src/Update/Program.cs +++ b/src/Update/Program.cs @@ -48,8 +48,10 @@ namespace Squirrel.Update // NB: Trying to delete the app directory while we have Setup.log // open will actually crash the uninstaller - bool logToTemp = opt.updateAction == UpdateAction.Uninstall - || opt.updateAction == UpdateAction.Setup; + bool logToTemp = + opt.updateAction == UpdateAction.Uninstall || + opt.updateAction == UpdateAction.Setup || + opt.updateAction == UpdateAction.Install; var logger = new SetupLogLogger(logToTemp, opt.updateAction) { Level = LogLevel.Info }; SquirrelLocator.CurrentMutable.Register(() => logger, typeof(SimpleSplat.ILogger)); @@ -79,7 +81,7 @@ namespace Squirrel.Update switch (opt.updateAction) { case UpdateAction.Setup: - Setup(opt.target, opt.silentInstall).Wait(); + Setup(opt.target, opt.silentInstall, opt.checkInstall).Wait(); break; case UpdateAction.Install: var progressSource = new ProgressSource(); @@ -115,11 +117,42 @@ namespace Squirrel.Update return 0; } - static async Task Setup(string setupPath, bool silentInstall) + static async Task Setup(string setupPath, bool silentInstall, bool checkInstall) { Log.Info($"Extracting bundled app data from '{setupPath}'."); var info = BundledSetupInfo.ReadFromFile(setupPath); + if (checkInstall) { + // CS: migrated from MachineInstaller.cpp + // here we are being run by the MSI deployment tool. We should check if the app + // is installed. if it is, we exit. If not, we install it silently. The goal is + // to ensure that all users of a system will always have this app installed. + Log.Info($"Has --checkInstall argument, verifying machine-wide installation"); + + // NB: Users often get into the sitch where they install the MSI, then try to + // install the standalone package on top of that. In previous versions we tried + // to detect if the app was properly installed, but now we're taking the much + // more conservative approach, that if the package dir exists in any way, we're + // bailing out + + // C:\Users\Username\AppData\Local\$pkgName + var localadinstall = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), info.AppId); + if (Directory.Exists(localadinstall)) { + Log.Info($"App install detected at '{localadinstall}', exiting..."); + return; + } + + // C:\ProgramData\$pkgName\$username + var programdatainstall = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), info.AppId, Environment.UserName); + if (Directory.Exists(programdatainstall)) { + Log.Info($"App install detected at '{programdatainstall}', exiting..."); + return; + } + + Log.Info("Installation for this user was not found. Beginning silent setup..."); + silentInstall = true; + } + using var _t = Utility.WithTempDirectory(out var tempFolder); // show splash screen @@ -142,9 +175,9 @@ namespace Squirrel.Update if (missingFrameworks.Any()) { Log.Info($"The following components are missing: " + String.Join(", ", missingFrameworks.Select(m => m.Id))); string message = missingFrameworks.Length > 1 - ? $"{info.AppName} is missing the following system components: {String.Join(", ", missingFrameworks.Select(s => s.DisplayName))}. " + + ? $"{info.AppFriendlyName} is missing the following system components: {String.Join(", ", missingFrameworks.Select(s => s.DisplayName))}. " + $"Would you like to install these now?" - : $"{info.AppName} requires {missingFrameworks.First().DisplayName} to continue, would you like to install it now?"; + : $"{info.AppFriendlyName} requires {missingFrameworks.First().DisplayName} to continue, would you like to install it now?"; // if running in attended mode, ask the user if they want to continue. otherwise, proceed if (splash != null) { diff --git a/src/Update/StartupOption.cs b/src/Update/StartupOption.cs index 64b31345..9714d2a0 100644 --- a/src/Update/StartupOption.cs +++ b/src/Update/StartupOption.cs @@ -13,16 +13,16 @@ namespace Squirrel.Update internal class StartupOption { private readonly OptionSet optionSet; - internal bool silentInstall { get; private set; } = false; internal UpdateAction updateAction { get; private set; } = default(UpdateAction); internal string target { get; private set; } = default(string); - //internal string baseUrl { get; private set; } = default(string); internal string processStart { get; private set; } = default(string); internal string processStartArgs { get; private set; } = default(string); internal string icon { get; private set; } = default(string); internal string shortcutArgs { get; private set; } = default(string); internal bool shouldWait { get; private set; } = false; internal bool onlyUpdateShortcuts { get; private set; } = false; + internal bool checkInstall { get; private set; } = false; + internal bool silentInstall { get; private set; } = false; public StartupOption(string[] args) { @@ -40,8 +40,7 @@ namespace Squirrel.Update $"Usage: {exeName} command [OPTS]", "", "Commands:", - { "install=", "Install the app whose package is in the specified directory or url", v => { updateAction = UpdateAction.Install; target = v; } }, - { "uninstall", "Uninstall the app the same dir as Update.exe", v => updateAction = UpdateAction.Uninstall}, + { "uninstall", "Uninstall the app in the same directory as Update.exe", v => updateAction = UpdateAction.Uninstall}, { "download=", "Download the releases specified by the URL and write new results to stdout as JSON", v => { updateAction = UpdateAction.Download; target = v; } }, { "checkForUpdate=", "Check for one available update and writes new results to stdout as JSON", v => { updateAction = UpdateAction.CheckForUpdate; target = v; } }, { "update=", "Update the application to the latest remote version specified by URL", v => { updateAction = UpdateAction.Update; target = v; } }, @@ -52,15 +51,17 @@ namespace Squirrel.Update "Options:", { "h|?|help", "Display Help and exit", _ => {} }, { "i=|icon", "Path to an ICO file that will be used for icon shortcuts", v => icon = v}, - { "s|silent", "Silent install", _ => silentInstall = true}, { "l=|shortcut-locations=", "Comma-separated string of shortcut locations, e.g. 'Desktop,StartMenu'", v => shortcutArgs = v}, { "updateOnly", "Update shortcuts that already exist, rather than creating new ones", _ => onlyUpdateShortcuts = true}, - // hidden arguments + // hidden arguments, used internally by Squirrel and should not be used by Squirrel applications themselves + { "install=", "Install the app whose package is in the specified directory or url", v => { updateAction = UpdateAction.Install; target = v; }, true }, + { "s|silent", "Silent install", _ => silentInstall = true, true}, { "processStart=", "Start an executable in the latest version of the app package", v => { updateAction = UpdateAction.ProcessStart; processStart = v; }, true}, { "processStartAndWait=", "Start an executable in the latest version of the app package", v => { updateAction = UpdateAction.ProcessStart; processStart = v; shouldWait = true; }, true}, { "a=|process-start-args=", "Arguments that will be used when starting executable", v => processStartArgs = v, true}, { "setup=", "Does an initial install with the help of the Setup.exe resources", v => { updateAction = UpdateAction.Setup; target = v; }, true }, + { "checkInstall", "Will install the app silently if it is not currently installed. Used for machine-wide deployments", v => { checkInstall = true; }, true }, }; opts.Parse(args); diff --git a/vendor/wix/template.wxs b/vendor/wix/template.wxs index 22d6d69c..c6991f68 100644 --- a/vendor/wix/template.wxs +++ b/vendor/wix/template.wxs @@ -13,13 +13,13 @@ - + - +