diff --git a/src/Squirrel.CommandLine/ValidatedOptionSet.cs b/src/Squirrel.CommandLine/ValidatedOptionSet.cs index 3762f478f..f31d035c4 100644 --- a/src/Squirrel.CommandLine/ValidatedOptionSet.cs +++ b/src/Squirrel.CommandLine/ValidatedOptionSet.cs @@ -105,12 +105,13 @@ protected virtual void IsValidUrl(string propertyName) throw new OptionValidationException(propertyName, "Must start with http or https and be a valid URI."); } - protected virtual int ParseIntArg(string propertyName, string propertyValue) + protected virtual int ParseIntArg(string propertyName, string v, int min = Int32.MinValue, int max = Int32.MaxValue) { - if (int.TryParse(propertyValue, out var value)) - return value; + if (!int.TryParse(v, out var i) || i < min || i > max) { + throw new OptionValidationException(propertyName, $"Must be an integer between {min} and {max}."); + } - throw new OptionValidationException(propertyName, "Must be a valid integer."); + return i; } public abstract void Validate(); diff --git a/src/Squirrel.CommandLine/Windows/Commands.cs b/src/Squirrel.CommandLine/Windows/Commands.cs index 9cf904664..d8996940f 100644 --- a/src/Squirrel.CommandLine/Windows/Commands.cs +++ b/src/Squirrel.CommandLine/Windows/Commands.cs @@ -75,9 +75,6 @@ static void Releasify(ReleasifyOptions options) if (!DotnetUtil.IsSingleFileBundle(updatePath)) throw new InvalidOperationException("Update.exe is corrupt. Broken Squirrel install?"); - // Sign Update.exe so that virus scanners don't think we're pulling one over on them - options.SignPEFile(updatePath); - // copy input package to target output directory File.Copy(package, Path.Combine(targetDir.FullName, Path.GetFileName(package)), true); @@ -174,20 +171,25 @@ RuntimeCpu parseMachine(PeNet.Header.Pe.MachineType machine) var exesToCreateStubFor = new DirectoryInfo(pkgPath).GetAllFilesRecursively() .Where(x => x.Name.EndsWith(".exe", StringComparison.InvariantCultureIgnoreCase)) .Where(x => !x.Name.Equals("squirrel.exe", StringComparison.InvariantCultureIgnoreCase)) + .Where(x => !x.Name.Equals("createdump.exe", StringComparison.InvariantCultureIgnoreCase)) .Where(x => Utility.IsFileTopLevelInPackage(x.FullName, pkgPath)) .ToArray(); // materialize the IEnumerable so we never end up creating stubs for stubs Log.Info($"Creating {exesToCreateStubFor.Length} stub executables"); exesToCreateStubFor.ForEach(x => createExecutableStubForExe(x.FullName)); - // sign all exe's in this package - new DirectoryInfo(pkgPath).GetAllFilesRecursively() - .Where(x => Utility.FileIsLikelyPEImage(x.Name)) - .ForEachAsync(x => options.SignPEFile(x.FullName)) - .Wait(); - // copy Update.exe into package, so it can also be updated in both full/delta packages + // and do it before signing so that Update.exe will also be signed. It is renamed to + // 'Squirrel.exe' only because Squirrel.Windows expects it to be called this. File.Copy(updatePath, Path.Combine(libDir, "Squirrel.exe"), true); + + // sign all exe's in this package + var filesToSign = new DirectoryInfo(libDir).GetAllFilesRecursively() + .Where(x => options.signSkipDll ? Utility.PathPartEndsWith(x.Name, ".exe") : Utility.FileIsLikelyPEImage(x.Name)) + .Select(x => x.FullName) + .ToArray(); + + options.SignFiles(libDir, filesToSign); // copy app icon to 'lib/fx/app.ico' var iconTarget = Path.Combine(libDir, "app.ico"); @@ -261,8 +263,9 @@ RuntimeCpu parseMachine(PeNet.Header.Pe.MachineType machine) Log.Info($"Creating Setup bundle"); var bundleOffset = SetupBundle.CreatePackageBundle(targetSetupExe, newestReleasePath); Log.Info("Bundle package offset is " + bundleOffset); - options.SignPEFile(targetSetupExe); + List setupFilesToSign = new() { targetSetupExe }; + Log.Info($"Setup bundle created at '{targetSetupExe}'."); // this option is used for debugging a local Setup.exe @@ -275,11 +278,13 @@ RuntimeCpu parseMachine(PeNet.Header.Pe.MachineType machine) if (SquirrelRuntimeInfo.IsWindows) { bool x64 = options.msi.Equals("x64"); var msiPath = createMsiPackage(targetSetupExe, bundledzp, x64); - options.SignPEFile(msiPath); + setupFilesToSign.Add(msiPath); } else { Log.Warn("Unable to create MSI (only supported on windows)."); } } + + options.SignFiles(targetDir.FullName, setupFilesToSign.ToArray()); Log.Info("Done"); } diff --git a/src/Squirrel.CommandLine/Windows/HelperExe.cs b/src/Squirrel.CommandLine/Windows/HelperExe.cs index 520482a3b..0832a04a1 100644 --- a/src/Squirrel.CommandLine/Windows/HelperExe.cs +++ b/src/Squirrel.CommandLine/Windows/HelperExe.cs @@ -14,8 +14,10 @@ namespace Squirrel.CommandLine.Windows internal class HelperExe : HelperFile { public static string SetupPath => FindHelperFile("Setup.exe"); + public static string UpdatePath => FindHelperFile("Update.exe", p => Microsoft.NET.HostModel.AppHost.HostWriter.IsBundle(p, out var _)); + public static string StubExecutablePath => FindHelperFile("StubExecutable.exe"); // private so we don't expose paths to internal tools. these should be exposed as a helper function @@ -48,46 +50,73 @@ private static bool CheckIsAlreadySigned(string filePath) } [SupportedOSPlatform("windows")] - public static void SignPEFilesWithSignTool(string filePath, string signArguments) + public static void SignPEFilesWithSignTool(string rootDir, string[] filePaths, string signArguments, int parallelism) { - if (CheckIsAlreadySigned(filePath)) return; - - List args = new List(); - args.Add("sign"); - args.AddRange(PlatformUtil.CommandLineToArgvW(signArguments)); - args.Add(filePath); + Queue pendingSign = new Queue(); + + foreach (var f in filePaths) { + if (!CheckIsAlreadySigned(f)) { + // try to find the path relative to rootDir + if (String.IsNullOrEmpty(rootDir)) { + pendingSign.Enqueue(f); + } else { + var partialPath = Utility.NormalizePath(f).Substring(Utility.NormalizePath(rootDir).Length).Trim('/', '\\'); + pendingSign.Enqueue(partialPath); + } + } else { + Log.Debug($"'{f}' is already signed, and will not be signed again."); + } + } - var result = PlatformUtil.InvokeProcess(SignToolPath, args, null, CancellationToken.None); - if (result.ExitCode != 0) { - var cmdWithPasswordHidden = new Regex(@"\/p\s+?[^\s]+").Replace(result.Command, "/p ********"); - throw new Exception( - $"Command failed:\n{cmdWithPasswordHidden}\n\n" + - $"Output was:\n" + result.StdOutput); - } else { - Log.Info("Sign successful: " + result.StdOutput); + if (filePaths.Length != pendingSign.Count) { + var diff = filePaths.Length - pendingSign.Count; + Log.Info($"{pendingSign.Count} files will be signed, {diff} will be skipped because they are already signed."); } + + var totalToSign = pendingSign.Count; + var baseSignArgs = PlatformUtil.CommandLineToArgvW(signArguments); + + do { + List args = new List(); + args.Add("sign"); + args.AddRange(baseSignArgs); + for (int i = Math.Min(pendingSign.Count, parallelism); i > 0; i--) { + args.Add(pendingSign.Dequeue()); + } + + var result = PlatformUtil.InvokeProcess(SignToolPath, args, rootDir, CancellationToken.None); + if (result.ExitCode != 0) { + var cmdWithPasswordHidden = new Regex(@"\/p\s+?[^\s]+").Replace(result.Command, "/p ********"); + Log.Debug($"Signing command failed: {cmdWithPasswordHidden}"); + throw new Exception( + $"Signing command failed. Specify --verbose argument to print signing command.\n\n" + + $"Output was:\n" + result.StdOutput); + } + + Log.Info($"Signed {totalToSign - pendingSign.Count}/{totalToSign} successfully.\r\n" + result.StdOutput); + + } while (pendingSign.Count > 0); } - [SupportedOSPlatform("windows")] - public static void SignPEFilesWithTemplate(string filePath, string signTemplate) + public static void SignPEFileWithTemplate(string filePath, string signTemplate) { - if (CheckIsAlreadySigned(filePath)) return; + if (SquirrelRuntimeInfo.IsWindows && CheckIsAlreadySigned(filePath)) { + Log.Debug($"'{filePath}' is already signed, and will not be signed again."); + return; + } var command = signTemplate.Replace("\"{{file}}\"", "{{file}}").Replace("{{file}}", $"\"{filePath}\""); - var args = PlatformUtil.CommandLineToArgvW(command); - if (args.Length < 2) - throw new OptionValidationException("Invalid signing template"); - - var result = PlatformUtil.InvokeProcess(args[0], args.Skip(1), null, CancellationToken.None); + var result = PlatformUtil.InvokeProcess(command, null, null, CancellationToken.None); if (result.ExitCode != 0) { var cmdWithPasswordHidden = new Regex(@"\/p\s+?[^\s]+").Replace(result.Command, "/p ********"); + Log.Debug($"Signing command failed: {cmdWithPasswordHidden}"); throw new Exception( - $"Command failed:\n{cmdWithPasswordHidden}\n\n" + + $"Signing command failed. Specify --verbose argument to print signing command.\n\n" + $"Output was:\n" + result.StdOutput); - } else { - Log.Info("Sign successful: " + result.StdOutput); } + + Log.Info("Sign successful: " + result.StdOutput); } [SupportedOSPlatform("windows")] @@ -151,4 +180,4 @@ public static void SetPEVersionBlockFromPackageInfo(string exePath, NuGet.IPacka Utility.Retry(() => InvokeAndThrowIfNonZero(RceditPath, args, null)); } } -} +} \ No newline at end of file diff --git a/src/Squirrel.CommandLine/Windows/Options.cs b/src/Squirrel.CommandLine/Windows/Options.cs index fd956c4ef..b3e3c7fc9 100644 --- a/src/Squirrel.CommandLine/Windows/Options.cs +++ b/src/Squirrel.CommandLine/Windows/Options.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Diagnostics; using System.Runtime.Versioning; @@ -12,27 +12,43 @@ internal class SigningOptions : BaseOptions { public string signParams { get; private set; } public string signTemplate { get; private set; } + public bool signSkipDll { get; private set; } + public int signParallel { get; private set; } = 10; public SigningOptions() { if (SquirrelRuntimeInfo.IsWindows) { - Add("n=|signParams=", "Sign files via SignTool.exe using these {PARAMETERS}", + Add("n=|signParams=", "Sign files via signtool.exe using these {PARAMETERS}", v => signParams = v); - Add("signTemplate=", "Use a custom signing {COMMAND}. '{{{{file}}}}' will be replaced by the path of the file to sign.", - v => signTemplate = v); + Add("signSkipDll", "Only signs EXE files, and skips signing DLL files.", v => signSkipDll = true); + Add("signParallel=", "The number of files to sign in each call to signtool.exe", + v => signParallel = ParseIntArg(nameof(signParallel), v, 1, 1000)); } + + Add("signTemplate=", "Use a custom signing {COMMAND}. '{{{{file}}}}' will be replaced by the path of the file to sign.", v => signTemplate = v); } - public void SignPEFile(string filePath) + public void SignFiles(string rootDir, params string[] filePaths) { + if (String.IsNullOrEmpty(signParams) && String.IsNullOrEmpty(signTemplate)) { + Log.Debug($"No signing paramaters provided, {filePaths.Length} file(s) will not be signed."); + return; + } + + if (!String.IsNullOrEmpty(signTemplate)) { + Log.Info($"Preparing to sign {filePaths.Length} files with custom signing template"); + foreach (var f in filePaths) { + HelperExe.SignPEFileWithTemplate(f, signTemplate); + } + return; + } + + // signtool.exe does not work if we're not on windows. if (!SquirrelRuntimeInfo.IsWindows) return; - + if (!String.IsNullOrEmpty(signParams)) { - HelperExe.SignPEFilesWithSignTool(filePath, signParams); - } else if (!String.IsNullOrEmpty(signTemplate)) { - HelperExe.SignPEFilesWithTemplate(filePath, signTemplate); - } else { - Log.Debug($"No signing paramaters, file will not be signed: '{filePath}'."); + Log.Info($"Preparing to sign {filePaths.Length} files with embedded signtool.exe with parallelism of {signParallel}"); + HelperExe.SignPEFilesWithSignTool(rootDir, filePaths, signParams, signParallel); } } @@ -43,7 +59,8 @@ public override void Validate() } if (!String.IsNullOrEmpty(signTemplate) && !signTemplate.Contains("{{file}}")) { - throw new OptionValidationException($"Argument 'signTemplate': Must contain '{{{{file}}}}' in template string (replaced with the file to sign). Current value is '{signTemplate}'"); + throw new OptionValidationException( + $"Argument 'signTemplate': Must contain '{{{{file}}}}' in template string (replaced with the file to sign). Current value is '{signTemplate}'"); } } } @@ -70,7 +87,7 @@ public ReleasifyOptions() Add("addSearchPath=", "Add additional search directories when looking for helper exe's such as Setup.exe, Update.exe, etc", HelperExe.AddSearchPath, true); Add("debugSetupExe=", "Uses the Setup.exe at this {PATH} to create the bundle, and then replaces it with the bundle. " + - "Used for locally debugging Setup.exe with a real bundle attached.", v => debugSetupExe = v, true); + "Used for locally debugging Setup.exe with a real bundle attached.", v => debugSetupExe = v, true); // public arguments InsertAt(1, "p=|package=", "{PATH} to a '.nupkg' package to releasify", v => package = v); @@ -128,7 +145,10 @@ public PackOptions() // hidden arguments Add("packName=", "The name of the package to create", - v => { packId = v; Log.Warn("--packName is deprecated. Use --packId instead."); }, true); + v => { + packId = v; + Log.Warn("--packName is deprecated. Use --packId instead."); + }, true); Add("packDirectory=", "", v => packDirectory = v, true); // public arguments, with indexes so they appear before ReleasifyOptions @@ -151,4 +171,4 @@ public override void Validate() base.ValidateInternal(false); } } -} +} \ No newline at end of file diff --git a/src/Squirrel/Internal/PlatformUtil.cs b/src/Squirrel/Internal/PlatformUtil.cs index b79b59e3f..486a441d4 100644 --- a/src/Squirrel/Internal/PlatformUtil.cs +++ b/src/Squirrel/Internal/PlatformUtil.cs @@ -435,14 +435,16 @@ private static (ProcessStartInfo StartInfo, string CommandDisplayString) CreateP { var psi = CreateProcessStartInfo(fileName, workingDirectory); - string displayArgs; + string displayArgs = ""; + if (args != null) { #if NET5_0_OR_GREATER - foreach (var a in args) psi.ArgumentList.Add(a); - displayArgs = $"['{String.Join("', '", args)}']"; + foreach (var a in args) psi.ArgumentList.Add(a); + displayArgs = $"['{String.Join("', '", args)}']"; #else - psi.Arguments = displayArgs = SquirrelRuntimeInfo.IsWindows ? ArgsToCommandLine(args) : ArgsToCommandLineUnix(args); + psi.Arguments = displayArgs = SquirrelRuntimeInfo.IsWindows ? ArgsToCommandLine(args) : ArgsToCommandLineUnix(args); #endif + } return (psi, fileName + " " + displayArgs); }