Re-introduce WIX/MSI support in the new setup architecture

This commit is contained in:
Caelan Sayler
2021-12-20 12:36:17 +00:00
parent d06238fa93
commit d713596bd8
11 changed files with 217 additions and 63 deletions

View File

@@ -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'"

View File

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

View File

@@ -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);

View File

@@ -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<string> _searchPaths = new List<string>();
private static IFullLogger Log = SquirrelLocator.CurrentMutable.GetService<ILogManager>().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<string>();
var dirs = (new[] { AppContext.BaseDirectory, Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location) })
.Concat(additionalDirs ?? Enumerable.Empty<string>()).Select(Path.GetFullPath);
.Concat(additionalDirs ?? Enumerable.Empty<string>())
.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 };

View File

@@ -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.
/// </summary>
public static Task<(int ExitCode, string StdOutput)> InvokeProcessAsync(string fileName, IEnumerable<string> args, CancellationToken ct)
public static Task<(int ExitCode, string StdOutput)> InvokeProcessAsync(string fileName, IEnumerable<string> 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);
}
}

View File

@@ -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<string, string> 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();
}
}
}

View File

@@ -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,22 +46,35 @@ 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));
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}'.");
}
}
internal class PackOptions : ReleasifyOptions
@@ -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);
}
}

View File

@@ -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<string, string> {
{ "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(

View File

@@ -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) {

View File

@@ -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);

View File

@@ -13,13 +13,13 @@
<Directory Id="TARGETDIR" Name="SourceDir">
<Directory Id="{{ProgramFilesFolder}}">
<Directory Id="APPLICATIONROOTDIRECTORY" Name="{{Title}} Deployment" />
<Directory Id="APPLICATIONROOTDIRECTORY" Name="{{Title}} Deployment Tool" />
</Directory>
</Directory>
<DirectoryRef Id="APPLICATIONROOTDIRECTORY">
<Component Id="{{Id}}.exe" Guid="{{IdAsGuid2}}" Win64="{{Win64YesNo}}">
<File Id="{{Id}}.exe" Name="{{Id}}DeploymentTool.exe" Source="./Setup.exe" KeyPath="yes"/>
<File Id="{{Id}}.exe" Name="{{Id}}DeploymentTool.exe" Source="./{{SetupName}}.exe" KeyPath="yes"/>
</Component>
</DirectoryRef>