mirror of
https://github.com/velopack/velopack.git
synced 2025-10-25 15:19:22 +00:00
Re-introduce WIX/MSI support in the new setup architecture
This commit is contained in:
25
build.ps1
25
build.ps1
@@ -19,23 +19,22 @@ foreach ($Folder in $Folders) {
|
|||||||
&"$MSBuildPath" /verbosity:minimal /restore /p:Configuration=Release
|
&"$MSBuildPath" /verbosity:minimal /restore /p:Configuration=Release
|
||||||
|
|
||||||
# Build single-exe packaged projects
|
# 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\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 over all files we need
|
||||||
Copy-Item "$In\Win32\Setup.exe" -Destination "$Out"
|
Copy-Item -Path "$PSScriptRoot\vendor\7zip\*" -Destination "$BinOut" -Recurse
|
||||||
# Copy-Item "$In\Win32\Setup.pdb" -Destination "$Out"
|
Copy-Item -Path "$PSScriptRoot\vendor\wix\*" -Destination "$BinOut" -Recurse
|
||||||
Copy-Item "$In\Win32\StubExecutable.exe" -Destination "$Out"
|
Copy-Item "$In\Win32\Setup.exe" -Destination "$BinOut"
|
||||||
# Copy-Item "$In\Win32\WriteZipToSetup.exe" -Destination "$Out"
|
Copy-Item "$In\Win32\StubExecutable.exe" -Destination "$BinOut"
|
||||||
# Copy-Item "$In\Win32\WriteZipToSetup.pdb" -Destination "$Out"
|
Copy-Item "$PSScriptRoot\vendor\nuget.exe" -Destination "$BinOut"
|
||||||
|
Copy-Item "$PSScriptRoot\vendor\rcedit.exe" -Destination "$BinOut"
|
||||||
Copy-Item -Path "$PSScriptRoot\vendor\7zip\*" -Destination "$Out" -Recurse
|
Copy-Item "$PSScriptRoot\vendor\signtool.exe" -Destination "$BinOut"
|
||||||
# Copy-Item -Path "$PSScriptRoot\vendor\wix\*" -Destination "$Out" -Recurse
|
Copy-Item "$PSScriptRoot\vendor\singlefilehost.exe" -Destination "$BinOut"
|
||||||
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"
|
|
||||||
|
|
||||||
Remove-Item "$Out\*.pdb"
|
Remove-Item "$Out\*.pdb"
|
||||||
|
Remove-Item "$BinOut\*.pdb"
|
||||||
|
|
||||||
Write-Output "Successfully copied files to './build/publish'"
|
Write-Output "Successfully copied files to './build/publish'"
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ void unzipSingleFile(BYTE* zipBuf, DWORD cZipBuf, wstring fileLocation, std::fun
|
|||||||
}
|
}
|
||||||
|
|
||||||
// https://stackoverflow.com/a/17387176/184746
|
// https://stackoverflow.com/a/17387176/184746
|
||||||
void ThrowLastWin32Error()
|
void throwLastWin32Error()
|
||||||
{
|
{
|
||||||
DWORD errorMessageID = ::GetLastError();
|
DWORD errorMessageID = ::GetLastError();
|
||||||
if (errorMessageID == 0) {
|
if (errorMessageID == 0) {
|
||||||
@@ -110,7 +110,7 @@ void wexec(const wchar_t* cmd)
|
|||||||
|
|
||||||
PROCESS_INFORMATION pi = { 0 };
|
PROCESS_INFORMATION pi = { 0 };
|
||||||
if (!CreateProcess(NULL, szCmdline, NULL, NULL, false, 0, NULL, NULL, &si, &pi)) {
|
if (!CreateProcess(NULL, szCmdline, NULL, NULL, false, 0, NULL, NULL, &si, &pi)) {
|
||||||
ThrowLastWin32Error();
|
throwLastWin32Error();
|
||||||
}
|
}
|
||||||
|
|
||||||
WaitForSingleObject(pi.hProcess, INFINITE);
|
WaitForSingleObject(pi.hProcess, INFINITE);
|
||||||
@@ -143,18 +143,19 @@ int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, PWSTR pCmdLine
|
|||||||
});
|
});
|
||||||
unzipSingleFile(zipBuf, cZipBuf, updaterPath, endsWithSquirrel);
|
unzipSingleFile(zipBuf, cZipBuf, updaterPath, endsWithSquirrel);
|
||||||
|
|
||||||
// run installer
|
// run installer and forward our command line arguments
|
||||||
wstring cmd = L"\"" + updaterPath + L"\" --setup \"" + myPath + L"\"";
|
wstring cmd = L"\"" + updaterPath + L"\" --setup \"" + myPath + L"\" " + pCmdLine;
|
||||||
wexec(cmd.c_str());
|
wexec(cmd.c_str());
|
||||||
}
|
}
|
||||||
catch (std::exception ex) {
|
catch (std::exception ex) {
|
||||||
string msg = ex.what();
|
string msg = ex.what();
|
||||||
// just use the _A function because std does not have a wexception
|
// 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 (...) {
|
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());
|
DeleteFile(updaterPath.c_str());
|
||||||
}
|
}
|
||||||
@@ -6,7 +6,8 @@ namespace Squirrel.Lib
|
|||||||
{
|
{
|
||||||
internal class BundledSetupInfo
|
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[] RequiredFrameworks { get; set; } = new string[0];
|
||||||
public string BundledPackageName { get; set; }
|
public string BundledPackageName { get; set; }
|
||||||
public byte[] BundledPackageBytes { get; set; }
|
public byte[] BundledPackageBytes { get; set; }
|
||||||
@@ -20,7 +21,8 @@ namespace Squirrel.Lib
|
|||||||
{
|
{
|
||||||
var bundle = new BundledSetupInfo();
|
var bundle = new BundledSetupInfo();
|
||||||
using var reader = new ResourceReader(exePath);
|
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.SplashImageBytes = ReadBytes(reader, 202);
|
||||||
bundle.RequiredFrameworks = ReadString(reader, 203)?.Split(',') ?? new string[0];
|
bundle.RequiredFrameworks = ReadString(reader, 203)?.Split(',') ?? new string[0];
|
||||||
bundle.BundledPackageName = ReadString(reader, 204);
|
bundle.BundledPackageName = ReadString(reader, 204);
|
||||||
@@ -45,7 +47,8 @@ namespace Squirrel.Lib
|
|||||||
public void WriteToFile(string exePath)
|
public void WriteToFile(string exePath)
|
||||||
{
|
{
|
||||||
using var writer = new ResourceUpdater(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, 202, SplashImageBytes);
|
||||||
WriteValue(writer, 203, String.Join(",", RequiredFrameworks ?? new string[0]));
|
WriteValue(writer, 203, String.Join(",", RequiredFrameworks ?? new string[0]));
|
||||||
WriteValue(writer, 204, BundledPackageName);
|
WriteValue(writer, 204, BundledPackageName);
|
||||||
|
|||||||
@@ -19,12 +19,15 @@ namespace Squirrel
|
|||||||
public static string UpdatePath => FindHelperExecutable("Update.exe", _searchPaths);
|
public static string UpdatePath => FindHelperExecutable("Update.exe", _searchPaths);
|
||||||
public static string StubExecutablePath => FindHelperExecutable("StubExecutable.exe", _searchPaths);
|
public static string StubExecutablePath => FindHelperExecutable("StubExecutable.exe", _searchPaths);
|
||||||
public static string SingleFileHostPath => FindHelperExecutable("singlefilehost.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 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 NugetPath => FindHelperExecutable("NuGet.exe", _searchPaths);
|
||||||
private static string RceditPath => FindHelperExecutable("rcedit.exe", _searchPaths);
|
private static string RceditPath => FindHelperExecutable("rcedit.exe", _searchPaths);
|
||||||
private static string SevenZipPath => FindHelperExecutable("7z.exe", _searchPaths);
|
private static string SevenZipPath => FindHelperExecutable("7z.exe", _searchPaths);
|
||||||
private static string SignToolPath => FindHelperExecutable("signtool.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 List<string> _searchPaths = new List<string>();
|
||||||
private static IFullLogger Log = SquirrelLocator.CurrentMutable.GetService<ILogManager>().GetLogger(typeof(HelperExe));
|
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"));
|
||||||
AddSearchPath(Path.Combine(AssemblyRuntimeInfo.BaseDirectory, "..", "..", "..", "vendor", "7zip"));
|
AddSearchPath(Path.Combine(AssemblyRuntimeInfo.BaseDirectory, "..", "..", "..", "vendor", "7zip"));
|
||||||
AddSearchPath(Path.Combine(AssemblyRuntimeInfo.BaseDirectory, "..", "..", "..", "vendor", "wix"));
|
AddSearchPath(Path.Combine(AssemblyRuntimeInfo.BaseDirectory, "..", "..", "..", "vendor", "wix"));
|
||||||
|
#else
|
||||||
|
AddSearchPath(Path.Combine(AssemblyRuntimeInfo.BaseDirectory, "bin"));
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -59,7 +64,9 @@ namespace Squirrel
|
|||||||
|
|
||||||
additionalDirs = additionalDirs ?? Enumerable.Empty<string>();
|
additionalDirs = additionalDirs ?? Enumerable.Empty<string>();
|
||||||
var dirs = (new[] { AppContext.BaseDirectory, Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location) })
|
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 exe = @".\" + toFind;
|
||||||
var result = dirs
|
var result = dirs
|
||||||
@@ -72,6 +79,41 @@ namespace Squirrel
|
|||||||
return result ?? exe;
|
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)
|
public static async Task SetExeIcon(string exePath, string iconPath)
|
||||||
{
|
{
|
||||||
var args = new[] { Path.GetFullPath(exePath), "--set-icon", iconPath };
|
var args = new[] { Path.GetFullPath(exePath), "--set-icon", iconPath };
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
using System.Diagnostics.Contracts;
|
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.
|
/// 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.
|
/// It also will automatically execute wine if trying to run an exe while not on windows.
|
||||||
/// </summary>
|
/// </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)) {
|
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 {
|
} else {
|
||||||
return InvokeProcessUnsafeAsync(CreateProcessStartInfo(fileName, ArgsToCommandLine(args)), ct);
|
return InvokeProcessUnsafeAsync(CreateProcessStartInfo(fileName, ArgsToCommandLine(args), workingDirectory), ct);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
29
src/SquirrelCli/CopStache.cs
Normal file
29
src/SquirrelCli/CopStache.cs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -27,9 +27,9 @@ namespace SquirrelCli
|
|||||||
public string updateIcon { get; private set; }
|
public string updateIcon { get; private set; }
|
||||||
public string appIcon { get; private set; }
|
public string appIcon { get; private set; }
|
||||||
public string setupIcon { get; private set; }
|
public string setupIcon { get; private set; }
|
||||||
public string setupName { get; private set; } = "Setup";
|
|
||||||
public bool noDelta { get; private set; }
|
public bool noDelta { get; private set; }
|
||||||
public bool allowUnaware { get; private set; }
|
public bool allowUnaware { get; private set; }
|
||||||
|
public string msi { get; private set; }
|
||||||
|
|
||||||
public ReleasifyOptions()
|
public ReleasifyOptions()
|
||||||
{
|
{
|
||||||
@@ -46,21 +46,34 @@ namespace SquirrelCli
|
|||||||
Add("updateIcon=", "ICO file that will be used for Update.exe", v => updateIcon = v);
|
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("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("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("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("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("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()
|
public override void Validate()
|
||||||
|
{
|
||||||
|
ValidateInternal(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual void ValidateInternal(bool checkPackage)
|
||||||
{
|
{
|
||||||
IsValidFile(nameof(appIcon), ".ico");
|
IsValidFile(nameof(appIcon), ".ico");
|
||||||
IsValidFile(nameof(setupIcon), ".ico");
|
IsValidFile(nameof(setupIcon), ".ico");
|
||||||
IsValidFile(nameof(updateIcon), ".ico");
|
IsValidFile(nameof(updateIcon), ".ico");
|
||||||
IsValidFile(nameof(splashImage));
|
IsValidFile(nameof(splashImage));
|
||||||
IsValidUrl(nameof(baseUrl));
|
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()
|
public override void Validate()
|
||||||
{
|
{
|
||||||
IsRequired(nameof(packName), nameof(packVersion), nameof(packAuthors), nameof(packDirectory));
|
IsRequired(nameof(packName), nameof(packVersion), nameof(packAuthors), nameof(packDirectory));
|
||||||
IsValidFile(nameof(setupIcon), ".ico");
|
base.ValidateInternal(false);
|
||||||
IsValidFile(nameof(splashImage));
|
|
||||||
IsValidUrl(nameof(baseUrl));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -265,7 +265,7 @@ namespace SquirrelCli
|
|||||||
ReleaseEntry.WriteReleaseFile(releaseEntries, releaseFilePath);
|
ReleaseEntry.WriteReleaseFile(releaseEntries, releaseFilePath);
|
||||||
|
|
||||||
var bundledzp = new ZipPackage(package);
|
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);
|
File.Copy(HelperExe.SetupPath, targetSetupExe, true);
|
||||||
Utility.Retry(() => HelperExe.SetPEVersionBlockFromPackageInfo(targetSetupExe, bundledzp, setupIcon).Wait());
|
Utility.Retry(() => HelperExe.SetPEVersionBlockFromPackageInfo(targetSetupExe, bundledzp, setupIcon).Wait());
|
||||||
|
|
||||||
@@ -274,7 +274,8 @@ namespace SquirrelCli
|
|||||||
|
|
||||||
Log.Info($"Creating Setup bundle");
|
Log.Info($"Creating Setup bundle");
|
||||||
var infosave = new BundledSetupInfo() {
|
var infosave = new BundledSetupInfo() {
|
||||||
AppName = bundledzp.Title ?? bundledzp.Id,
|
AppId = bundledzp.Id,
|
||||||
|
AppFriendlyName = bundledzp.Title ?? bundledzp.Id,
|
||||||
BundledPackageBytes = File.ReadAllBytes(newestReleasePath),
|
BundledPackageBytes = File.ReadAllBytes(newestReleasePath),
|
||||||
BundledPackageName = Path.GetFileName(newestReleasePath),
|
BundledPackageName = Path.GetFileName(newestReleasePath),
|
||||||
RequiredFrameworks = requiredFrameworks.ToArray(),
|
RequiredFrameworks = requiredFrameworks.ToArray(),
|
||||||
@@ -287,23 +288,57 @@ namespace SquirrelCli
|
|||||||
|
|
||||||
HelperExe.SignPEFile(targetSetupExe, signingOpts).Wait();
|
HelperExe.SignPEFile(targetSetupExe, signingOpts).Wait();
|
||||||
|
|
||||||
//setupPath = @"C:\Source\Clowd\clowd-windows\modules\Clowd.Squirrel\build\Debug\Win32\Setup.exe";
|
if (!String.IsNullOrEmpty(options.msi)) {
|
||||||
//string onlineSource = "https://caesay.com/clowd-updates";
|
bool x64 = options.msi.Equals("x64");
|
||||||
//byte[] offlineSourcesZip = new byte[0];
|
createMsiPackage(targetSetupExe, new ZipPackage(package), x64).Wait();
|
||||||
|
HelperExe.SignPEFile(targetSetupExe.Replace(".exe", ".msi"), signingOpts).Wait();
|
||||||
//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();
|
|
||||||
// }
|
|
||||||
//}
|
|
||||||
|
|
||||||
Log.Info("Done");
|
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)
|
static async Task createExecutableStubForExe(string exeToCopy)
|
||||||
{
|
{
|
||||||
var target = Path.Combine(
|
var target = Path.Combine(
|
||||||
|
|||||||
@@ -48,8 +48,10 @@ namespace Squirrel.Update
|
|||||||
|
|
||||||
// NB: Trying to delete the app directory while we have Setup.log
|
// NB: Trying to delete the app directory while we have Setup.log
|
||||||
// open will actually crash the uninstaller
|
// open will actually crash the uninstaller
|
||||||
bool logToTemp = opt.updateAction == UpdateAction.Uninstall
|
bool logToTemp =
|
||||||
|| opt.updateAction == UpdateAction.Setup;
|
opt.updateAction == UpdateAction.Uninstall ||
|
||||||
|
opt.updateAction == UpdateAction.Setup ||
|
||||||
|
opt.updateAction == UpdateAction.Install;
|
||||||
|
|
||||||
var logger = new SetupLogLogger(logToTemp, opt.updateAction) { Level = LogLevel.Info };
|
var logger = new SetupLogLogger(logToTemp, opt.updateAction) { Level = LogLevel.Info };
|
||||||
SquirrelLocator.CurrentMutable.Register(() => logger, typeof(SimpleSplat.ILogger));
|
SquirrelLocator.CurrentMutable.Register(() => logger, typeof(SimpleSplat.ILogger));
|
||||||
@@ -79,7 +81,7 @@ namespace Squirrel.Update
|
|||||||
|
|
||||||
switch (opt.updateAction) {
|
switch (opt.updateAction) {
|
||||||
case UpdateAction.Setup:
|
case UpdateAction.Setup:
|
||||||
Setup(opt.target, opt.silentInstall).Wait();
|
Setup(opt.target, opt.silentInstall, opt.checkInstall).Wait();
|
||||||
break;
|
break;
|
||||||
case UpdateAction.Install:
|
case UpdateAction.Install:
|
||||||
var progressSource = new ProgressSource();
|
var progressSource = new ProgressSource();
|
||||||
@@ -115,11 +117,42 @@ namespace Squirrel.Update
|
|||||||
return 0;
|
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}'.");
|
Log.Info($"Extracting bundled app data from '{setupPath}'.");
|
||||||
var info = BundledSetupInfo.ReadFromFile(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);
|
using var _t = Utility.WithTempDirectory(out var tempFolder);
|
||||||
|
|
||||||
// show splash screen
|
// show splash screen
|
||||||
@@ -142,9 +175,9 @@ namespace Squirrel.Update
|
|||||||
if (missingFrameworks.Any()) {
|
if (missingFrameworks.Any()) {
|
||||||
Log.Info($"The following components are missing: " + String.Join(", ", missingFrameworks.Select(m => m.Id)));
|
Log.Info($"The following components are missing: " + String.Join(", ", missingFrameworks.Select(m => m.Id)));
|
||||||
string message = missingFrameworks.Length > 1
|
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?"
|
$"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 running in attended mode, ask the user if they want to continue. otherwise, proceed
|
||||||
if (splash != null) {
|
if (splash != null) {
|
||||||
|
|||||||
@@ -13,16 +13,16 @@ namespace Squirrel.Update
|
|||||||
internal class StartupOption
|
internal class StartupOption
|
||||||
{
|
{
|
||||||
private readonly OptionSet optionSet;
|
private readonly OptionSet optionSet;
|
||||||
internal bool silentInstall { get; private set; } = false;
|
|
||||||
internal UpdateAction updateAction { get; private set; } = default(UpdateAction);
|
internal UpdateAction updateAction { get; private set; } = default(UpdateAction);
|
||||||
internal string target { get; private set; } = default(string);
|
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 processStart { get; private set; } = default(string);
|
||||||
internal string processStartArgs { get; private set; } = default(string);
|
internal string processStartArgs { get; private set; } = default(string);
|
||||||
internal string icon { get; private set; } = default(string);
|
internal string icon { get; private set; } = default(string);
|
||||||
internal string shortcutArgs { get; private set; } = default(string);
|
internal string shortcutArgs { get; private set; } = default(string);
|
||||||
internal bool shouldWait { get; private set; } = false;
|
internal bool shouldWait { get; private set; } = false;
|
||||||
internal bool onlyUpdateShortcuts { 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)
|
public StartupOption(string[] args)
|
||||||
{
|
{
|
||||||
@@ -40,8 +40,7 @@ namespace Squirrel.Update
|
|||||||
$"Usage: {exeName} command [OPTS]",
|
$"Usage: {exeName} command [OPTS]",
|
||||||
"",
|
"",
|
||||||
"Commands:",
|
"Commands:",
|
||||||
{ "install=", "Install the app whose package is in the specified directory or url", v => { updateAction = UpdateAction.Install; target = v; } },
|
{ "uninstall", "Uninstall the app in the same directory as Update.exe", v => updateAction = UpdateAction.Uninstall},
|
||||||
{ "uninstall", "Uninstall the app the same dir 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; } },
|
{ "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; } },
|
{ "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; } },
|
{ "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:",
|
"Options:",
|
||||||
{ "h|?|help", "Display Help and exit", _ => {} },
|
{ "h|?|help", "Display Help and exit", _ => {} },
|
||||||
{ "i=|icon", "Path to an ICO file that will be used for icon shortcuts", v => icon = v},
|
{ "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},
|
{ "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},
|
{ "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},
|
{ "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},
|
{ "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},
|
{ "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 },
|
{ "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);
|
opts.Parse(args);
|
||||||
|
|||||||
4
vendor/wix/template.wxs
vendored
4
vendor/wix/template.wxs
vendored
@@ -13,13 +13,13 @@
|
|||||||
|
|
||||||
<Directory Id="TARGETDIR" Name="SourceDir">
|
<Directory Id="TARGETDIR" Name="SourceDir">
|
||||||
<Directory Id="{{ProgramFilesFolder}}">
|
<Directory Id="{{ProgramFilesFolder}}">
|
||||||
<Directory Id="APPLICATIONROOTDIRECTORY" Name="{{Title}} Deployment" />
|
<Directory Id="APPLICATIONROOTDIRECTORY" Name="{{Title}} Deployment Tool" />
|
||||||
</Directory>
|
</Directory>
|
||||||
</Directory>
|
</Directory>
|
||||||
|
|
||||||
<DirectoryRef Id="APPLICATIONROOTDIRECTORY">
|
<DirectoryRef Id="APPLICATIONROOTDIRECTORY">
|
||||||
<Component Id="{{Id}}.exe" Guid="{{IdAsGuid2}}" Win64="{{Win64YesNo}}">
|
<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>
|
</Component>
|
||||||
</DirectoryRef>
|
</DirectoryRef>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user