Refactor helper/exe, Fix zstd bugs: don't use update binary on osx and windowLog

This commit is contained in:
Caelan Sayler
2024-01-14 11:27:38 +00:00
parent 293c6ab693
commit d3a045f578
31 changed files with 608 additions and 501 deletions

View File

@@ -11,17 +11,33 @@ pub fn patch(old_file: &PathBuf, patch_file: &PathBuf, output_file: &PathBuf) ->
}
let dict = fs::read(old_file)?;
info!("Loading Dictionary (Size: {})", dict.len());
let patch = fs::OpenOptions::new().read(true).open(patch_file)?;
let patch_reader = io::BufReader::new(patch);
let mut output = fs::OpenOptions::new().write(true).create(true).open(output_file)?;
let mut decoder = zstd::Decoder::with_dictionary(patch_reader, &dict)?;
info!("Dictionary Size: {}", dict.len());
info!("Decoder loaded. Beginning patch...");
let window_log = fio_highbit64(dict.len() as u64) + 1;
if window_log >= 27 {
info!("Large File detected. Overriding windowLog to {}", window_log);
decoder.window_log_max(window_log)?;
}
info!("Decoder loaded. Beginning patch...");
let mut output = fs::OpenOptions::new().write(true).create(true).open(output_file)?;
io::copy(&mut decoder, &mut output)?;
info!("Patch applied successfully.");
Ok(())
}
fn fio_highbit64(v: u64) -> u32 {
let mut count: u32 = 0;
let mut v = v;
v >>= 1;
while v > 0 {
v >>= 1;
count += 1;
}
return count;
}

View File

@@ -0,0 +1,21 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Versioning;
using System.Text;
using System.Threading.Tasks;
namespace Velopack.Packaging.Unix
{
public class AppImageTool
{
[SupportedOSPlatform("linux")]
public static void CreateLinuxAppImage(string appDir, string outputFile)
{
var tool = HelperFile.AppImageToolX64;
Chmod.ChmodFileAsExecutable(tool);
Exe.InvokeAndThrowIfNonZero(tool, new[] { appDir, outputFile }, null);
Chmod.ChmodFileAsExecutable(outputFile);
}
}
}

View File

@@ -27,7 +27,7 @@ public class Chmod
else if (VelopackRuntimeInfo.IsLinux) chmod = nix_chmod;
else return; // no-op on windows, all .exe files can be executed.
var filePermissionOctal = Convert.ToInt32("777", 8);
var filePermissionOctal = Convert.ToInt32("755", 8);
const int EINTR = 4;
int chmodReturnCode;

View File

@@ -60,11 +60,10 @@ Categories=Development;
// app icon
File.Copy(Options.Icon, Path.Combine(dir.FullName, Options.PackId + Path.GetExtension(Options.Icon)), true);
var helper = new HelperExe(Log);
// velopack required files
File.WriteAllText(Path.Combine(bin.FullName, "sq.version"), nuspecText);
File.Copy(helper.UpdateNixPath, Path.Combine(bin.FullName, "UpdateNix"), true);
File.Copy(HelperFile.GetUpdatePath(), Path.Combine(bin.FullName, "UpdateNix"), true);
progress(100);
return Task.FromResult(dir.FullName);
}
@@ -72,8 +71,7 @@ Categories=Development;
protected override Task CreatePortablePackage(Action<int> progress, string packDir, string outputPath)
{
progress(-1);
var helper = new HelperExe(Log);
helper.CreateLinuxAppImage(packDir, outputPath);
AppImageTool.CreateLinuxAppImage(packDir, outputPath);
PortablePackagePath = outputPath;
progress(100);
return Task.CompletedTask;

View File

@@ -31,10 +31,9 @@ public class OsxPackCommandRunner : PackageBuilder<OsxPackOptions>
Utility.DeleteFileOrDirectoryHard(appBundlePath);
}
var helper = new HelperExe(Log);
var macosdir = structure.MacosDirectory;
File.WriteAllText(Path.Combine(macosdir, "sq.version"), nuspecText);
File.Copy(helper.UpdateMacPath, Path.Combine(macosdir, "UpdateMac"), true);
File.Copy(HelperFile.GetUpdatePath(), Path.Combine(macosdir, "UpdateMac"), true);
foreach (var f in Directory.GetFiles(macosdir, "*", SearchOption.AllDirectories)) {
if (MachO.IsMachOImage(f)) {
@@ -49,12 +48,12 @@ public class OsxPackCommandRunner : PackageBuilder<OsxPackOptions>
protected override Task CodeSign(Action<int> progress, string packDir)
{
var helper = new HelperExe(Log);
var helper = new OsxBuildTools(Log);
// code signing all mach-o binaries
if (!string.IsNullOrEmpty(Options.SigningAppIdentity) && !string.IsNullOrEmpty(Options.NotaryProfile)) {
progress(-1); // indeterminate
var zipPath = Path.Combine(TempDir.FullName, "notarize.zip");
helper.CodeSign(Options.SigningAppIdentity, Options.SigningEntitlements, packDir);
helper.CodeSign(Options.SigningAppIdentity, Options.SigningEntitlements, packDir);
helper.CreateDittoZip(packDir, zipPath);
helper.Notarize(zipPath, Options.NotaryProfile);
helper.Staple(packDir);
@@ -64,7 +63,7 @@ public class OsxPackCommandRunner : PackageBuilder<OsxPackOptions>
} else if (!string.IsNullOrEmpty(Options.SigningAppIdentity)) {
progress(-1); // indeterminate
Log.Warn("Package will be signed, but [underline]not notarized[/]. Missing the --notaryProfile option.");
helper.CodeSign(Options.SigningAppIdentity, Options.SigningEntitlements, packDir);
helper.CodeSign(Options.SigningAppIdentity, Options.SigningEntitlements, packDir);
progress(100);
} else {
Log.Warn("Package will not be signed or notarized. Missing the --signAppIdentity and --notaryProfile options.");
@@ -76,7 +75,7 @@ public class OsxPackCommandRunner : PackageBuilder<OsxPackOptions>
{
// create installer package, sign and notarize
if (!Options.NoPackage) {
var helper = new HelperExe(Log);
var helper = new OsxBuildTools(Log);
Dictionary<string, string> pkgContent = new() {
{"welcome", Options.PackageWelcome },
{"license", Options.PackageLicense },
@@ -108,7 +107,7 @@ public class OsxPackCommandRunner : PackageBuilder<OsxPackOptions>
protected override Task CreatePortablePackage(Action<int> progress, string packDir, string outputPath)
{
progress(-1); // indeterminate
var helper = new HelperExe(Log);
var helper = new OsxBuildTools(Log);
helper.CreateDittoZip(packDir, outputPath);
progress(100);
return Task.CompletedTask;

View File

@@ -8,32 +8,22 @@ using Newtonsoft.Json;
namespace Velopack.Packaging.Unix;
public class HelperExe : HelperFile
[SupportedOSPlatform("osx")]
public class OsxBuildTools
{
public HelperExe(ILogger logger) : base(logger)
public ILogger Log { get; }
public OsxBuildTools(ILogger logger)
{
Log = logger;
}
public string VelopackEntitlements => FindHelperFile("Velopack.entitlements");
protected string AppImageTool => FindHelperFile("appimagetool-x86_64.AppImage");
[SupportedOSPlatform("linux")]
public void CreateLinuxAppImage(string appDir, string outputFile)
{
var tool = AppImageTool;
Chmod.ChmodFileAsExecutable(tool);
InvokeAndThrowIfNonZero(tool, new[] { appDir, outputFile }, null);
Chmod.ChmodFileAsExecutable(outputFile);
}
[SupportedOSPlatform("osx")]
public void CodeSign(string identity, string entitlements, string filePath)
{
if (String.IsNullOrEmpty(entitlements)) {
Log.Info("No entitlements specified, using default: " +
"https://docs.microsoft.com/en-us/dotnet/core/install/macos-notarization-issues");
entitlements = VelopackEntitlements;
entitlements = HelperFile.VelopackEntitlements;
}
if (!File.Exists(entitlements)) {
@@ -53,12 +43,11 @@ public class HelperExe : HelperFile
Log.Info($"Beginning codesign for package...");
Log.Info(InvokeAndThrowIfNonZero("codesign", args, null));
Log.Info(Exe.InvokeAndThrowIfNonZero("codesign", args, null));
Log.Info("codesign completed successfully");
}
[SupportedOSPlatform("osx")]
public void SpctlAssessCode(string filePath)
{
var args2 = new List<string> {
@@ -68,10 +57,9 @@ public class HelperExe : HelperFile
};
Log.Info($"Verifying signature/notarization for code using spctl...");
Log.Info(InvokeAndThrowIfNonZero("spctl", args2, null));
Log.Info(Exe.InvokeAndThrowIfNonZero("spctl", args2, null));
}
[SupportedOSPlatform("osx")]
public void SpctlAssessInstaller(string filePath)
{
var args2 = new List<string> {
@@ -82,10 +70,9 @@ public class HelperExe : HelperFile
};
Log.Info($"Verifying signature/notarization for installer package using spctl...");
Log.Info(InvokeAndThrowIfNonZero("spctl", args2, null));
Log.Info(Exe.InvokeAndThrowIfNonZero("spctl", args2, null));
}
[SupportedOSPlatform("osx")]
public void CreateInstallerPkg(string appBundlePath, string appTitle, string appId, IEnumerable<KeyValuePair<string, string>> extraContent,
string pkgOutputPath, string signIdentity, Action<int> progress)
{
@@ -121,8 +108,8 @@ exit 0
// generate non-relocatable component pkg. this will be included into a product archive
var pkgPlistPath = Path.Combine(tmp, "tmp.plist");
InvokeAndThrowIfNonZero("pkgbuild", new[] { "--analyze", "--root", tmpPayload1, pkgPlistPath }, null);
InvokeAndThrowIfNonZero("plutil", new[] { "-replace", "BundleIsRelocatable", "-bool", "NO", pkgPlistPath }, null);
Exe.InvokeAndThrowIfNonZero("pkgbuild", new[] { "--analyze", "--root", tmpPayload1, pkgPlistPath }, null);
Exe.InvokeAndThrowIfNonZero("plutil", new[] { "-replace", "BundleIsRelocatable", "-bool", "NO", pkgPlistPath }, null);
progress(50);
var pkg1Path = Path.Combine(tmpPayload2, "1.pkg");
@@ -134,12 +121,12 @@ exit 0
pkg1Path,
};
InvokeAndThrowIfNonZero("pkgbuild", args1, null);
Exe.InvokeAndThrowIfNonZero("pkgbuild", args1, null);
progress(70);
// create final product package that contains app component
var distributionPath = Path.Combine(tmp, "distribution.xml");
InvokeAndThrowIfNonZero("productbuild", new[] { "--synthesize", "--package", pkg1Path, distributionPath }, null);
Exe.InvokeAndThrowIfNonZero("productbuild", new[] { "--synthesize", "--package", pkg1Path, distributionPath }, null);
progress(80);
// https://developer.apple.com/library/archive/documentation/DeveloperTools/Reference/DistributionDefinitionRef/Chapters/Distribution_XML_Ref.html
@@ -175,13 +162,12 @@ exit 0
Log.Warn("No Installer signing identity provided. The '.pkg' will not be signed.");
}
InvokeAndThrowIfNonZero("productbuild", args2, null);
Exe.InvokeAndThrowIfNonZero("productbuild", args2, null);
progress(100);
Log.Info("Installer created successfully");
}
[SupportedOSPlatform("osx")]
public void Notarize(string filePath, string keychainProfileName)
{
Log.Info($"Preparing to Notarize. This will upload to Apple and usually takes minutes, [underline]but could take hours.[/]");
@@ -195,7 +181,7 @@ exit 0
filePath
};
var ntresultjson = InvokeProcess("xcrun", args, null);
var ntresultjson = Exe.InvokeProcess("xcrun", args, null);
Log.Info(ntresultjson.StdOutput);
// try to catch any notarization errors. if we have a submission id, retrieve notary logs.
@@ -210,7 +196,7 @@ exit 0
"--keychain-profile", keychainProfileName,
};
var result = InvokeProcess("xcrun", logargs, null);
var result = Exe.InvokeProcess("xcrun", logargs, null);
Log.Warn(result.StdOutput);
}
@@ -223,11 +209,10 @@ exit 0
Log.Info("Notarization completed successfully");
}
[SupportedOSPlatform("osx")]
public void Staple(string filePath)
{
Log.Debug($"Stapling Notarization to '{filePath}'");
Log.Info(InvokeAndThrowIfNonZero("xcrun", new[] { "stapler", "staple", filePath }, null));
Log.Info(Exe.InvokeAndThrowIfNonZero("xcrun", new[] { "stapler", "staple", filePath }, null));
}
private class NotaryToolResult
@@ -237,7 +222,6 @@ exit 0
public string status { get; set; }
}
[SupportedOSPlatform("osx")]
public void CreateDittoZip(string folder, string outputZip)
{
if (File.Exists(outputZip)) File.Delete(outputZip);
@@ -253,6 +237,6 @@ exit 0
};
Log.Debug($"Creating ditto bundle '{outputZip}'");
Log.Debug(InvokeAndThrowIfNonZero("ditto", args, null));
Log.Debug(Exe.InvokeAndThrowIfNonZero("ditto", args, null));
}
}

View File

@@ -0,0 +1,145 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
namespace Velopack.Packaging.Windows
{
[SupportedOSPlatform("windows")]
public class CodeSign
{
public ILogger Log { get; }
public CodeSign(ILogger logger)
{
Log = logger;
}
private bool CheckIsAlreadySigned(string filePath)
{
if (String.IsNullOrWhiteSpace(filePath)) return true;
if (!File.Exists(filePath)) {
Log.Warn($"Cannot sign '{filePath}', file does not exist.");
return true;
}
try {
if (AuthenticodeTools.IsTrusted(filePath)) {
Log.Debug($"'{filePath}' is already signed, skipping...");
return true;
}
} catch (Exception ex) {
Log.Error(ex, "Failed to determine signing status for " + filePath);
}
return false;
}
public void SignPEFilesWithSignTool(string rootDir, string[] filePaths, string signArguments, int parallelism, Action<int> progress)
{
Queue<string> pendingSign = new Queue<string>();
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.");
}
}
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 = CommandLineToArgvW(signArguments);
do {
List<string> args = new List<string>();
args.Add("sign");
args.AddRange(baseSignArgs);
for (int i = Math.Min(pendingSign.Count, parallelism); i > 0; i--) {
args.Add(pendingSign.Dequeue());
}
var result = Exe.InvokeProcess(HelperFile.SignToolPath, args, rootDir);
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);
}
int processed = totalToSign - pendingSign.Count;
Log.Debug($"Signed {processed}/{totalToSign} successfully.\r\n" + result.StdOutput);
progress((int) ((double) processed / totalToSign * 100));
} while (pendingSign.Count > 0);
}
public void SignPEFileWithTemplate(string filePath, string signTemplate)
{
if (VelopackRuntimeInfo.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 result = Exe.InvokeProcess(command, null, null);
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("Sign successful: " + result.StdOutput);
}
private const string WIN_KERNEL32 = "kernel32.dll";
private const string WIN_SHELL32 = "shell32.dll";
[DllImport(WIN_KERNEL32, EntryPoint = "LocalFree", SetLastError = true)]
private static extern IntPtr _LocalFree(IntPtr hMem);
[DllImport(WIN_SHELL32, EntryPoint = "CommandLineToArgvW", CharSet = CharSet.Unicode)]
private static extern IntPtr _CommandLineToArgvW([MarshalAs(UnmanagedType.LPWStr)] string cmdLine, out int numArgs);
protected static string[] CommandLineToArgvW(string cmdLine)
{
IntPtr argv = IntPtr.Zero;
try {
argv = _CommandLineToArgvW(cmdLine, out var numArgs);
if (argv == IntPtr.Zero) {
throw new Win32Exception();
}
var result = new string[numArgs];
for (int i = 0; i < numArgs; i++) {
IntPtr currArg = Marshal.ReadIntPtr(argv, i * Marshal.SizeOf(typeof(IntPtr)));
result[i] = Marshal.PtrToStringUni(currArg);
}
return result;
} finally {
_LocalFree(argv);
}
}
}
}

View File

@@ -1,4 +1,5 @@
using System.Diagnostics;
using System.Runtime.Versioning;
using System.Threading.Channels;
using Microsoft.Extensions.Logging;
using NuGet.Versioning;
@@ -9,6 +10,7 @@ using Velopack.Windows;
namespace Velopack.Packaging.Windows.Commands;
[SupportedOSPlatform("windows")]
public class WindowsPackCommandRunner : PackageBuilder<WindowsPackOptions>
{
public WindowsPackCommandRunner(ILogger logger)
@@ -42,13 +44,12 @@ public class WindowsPackCommandRunner : PackageBuilder<WindowsPackOptions>
File.WriteAllText(Path.Combine(dir.FullName, "sq.version"), nuspecText);
packDir = dir.FullName;
var helper = new HelperExe(Log);
var updatePath = Path.Combine(TempDir.FullName, "Update.exe");
File.Copy(helper.UpdatePath, updatePath, true);
File.Copy(HelperFile.GetUpdatePath(), updatePath, true);
// update icon for Update.exe if requested
if (Options.Icon != null && VelopackRuntimeInfo.IsWindows) {
helper.SetExeIcon(updatePath, Options.Icon);
Rcedit.SetExeIcon(updatePath, Options.Icon);
} else if (Options.Icon != null) {
Log.Warn("Unable to set icon for Update.exe (only supported on windows).");
}
@@ -80,12 +81,11 @@ public class WindowsPackCommandRunner : PackageBuilder<WindowsPackOptions>
protected override Task CreateSetupPackage(Action<int> progress, string releasePkg, string packDir, string targetSetupExe)
{
var helper = new HelperExe(Log);
var bundledzp = new ZipPackage(releasePkg);
File.Copy(helper.SetupPath, targetSetupExe, true);
Utility.Retry(() => File.Copy(HelperFile.SetupPath, targetSetupExe, true));
progress(10);
if (VelopackRuntimeInfo.IsWindows) {
helper.SetPEVersionBlockFromPackageInfo(targetSetupExe, bundledzp, Options.Icon);
Rcedit.SetPEVersionBlockFromPackageInfo(targetSetupExe, bundledzp, Options.Icon);
} else {
Log.Warn("Unable to set Setup.exe icon (only supported on windows)");
}
@@ -129,9 +129,7 @@ public class WindowsPackCommandRunner : PackageBuilder<WindowsPackOptions>
{
try {
var target = Path.Combine(targetDir, Path.GetFileName(exeToCopy));
var helper = new HelperExe(Log);
Utility.Retry(() => File.Copy(helper.StubExecutablePath, target, true));
Utility.Retry(() => File.Copy(HelperFile.StubExecutablePath, target, true));
Utility.Retry(() => {
if (VelopackRuntimeInfo.IsWindows) {
using var writer = new Microsoft.NET.HostModel.ResourceUpdater(target, true);
@@ -151,7 +149,7 @@ public class WindowsPackCommandRunner : PackageBuilder<WindowsPackOptions>
var signParams = options.SignParameters;
var signTemplate = options.SignTemplate;
var signParallel = options.SignParallel;
var helper = new HelperExe(Log);
var helper = new CodeSign(Log);
if (string.IsNullOrEmpty(signParams) && string.IsNullOrEmpty(signTemplate)) {
Log.Warn($"No signing paramaters provided, {filePaths.Length} file(s) will not be signed.");

View File

@@ -1,180 +0,0 @@
using System.ComponentModel;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using System.Text.RegularExpressions;
using Microsoft.Extensions.Logging;
namespace Velopack.Packaging.Windows;
public class HelperExe : HelperFile
{
public HelperExe(ILogger logger) : base(logger)
{
}
public string SetupPath => FindHelperFile("Setup.exe");
public string StubExecutablePath => FindHelperFile("stub.exe");
private string SignToolPath => FindHelperFile("signtool.exe");
private string RceditPath => FindHelperFile("rcedit.exe");
[SupportedOSPlatform("windows")]
private bool CheckIsAlreadySigned(string filePath)
{
if (String.IsNullOrWhiteSpace(filePath)) return true;
if (!File.Exists(filePath)) {
Log.Warn($"Cannot sign '{filePath}', file does not exist.");
return true;
}
try {
if (AuthenticodeTools.IsTrusted(filePath)) {
Log.Debug($"'{filePath}' is already signed, skipping...");
return true;
}
} catch (Exception ex) {
Log.Error(ex, "Failed to determine signing status for " + filePath);
}
return false;
}
[SupportedOSPlatform("windows")]
public void SignPEFilesWithSignTool(string rootDir, string[] filePaths, string signArguments, int parallelism, Action<int> progress)
{
Queue<string> pendingSign = new Queue<string>();
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.");
}
}
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 = CommandLineToArgvW(signArguments);
do {
List<string> args = new List<string>();
args.Add("sign");
args.AddRange(baseSignArgs);
for (int i = Math.Min(pendingSign.Count, parallelism); i > 0; i--) {
args.Add(pendingSign.Dequeue());
}
var result = InvokeProcess(SignToolPath, args, rootDir);
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);
}
int processed = totalToSign - pendingSign.Count;
Log.Debug($"Signed {processed}/{totalToSign} successfully.\r\n" + result.StdOutput);
progress((int) ((double) processed / totalToSign * 100));
} while (pendingSign.Count > 0);
}
public void SignPEFileWithTemplate(string filePath, string signTemplate)
{
if (VelopackRuntimeInfo.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 result = InvokeProcess(command, null, null);
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("Sign successful: " + result.StdOutput);
}
[SupportedOSPlatform("windows")]
public void SetExeIcon(string exePath, string iconPath)
{
Log.Debug("Updating PE icon for: " + exePath);
var args = new[] { Path.GetFullPath(exePath), "--set-icon", iconPath };
Utility.Retry(() => InvokeAndThrowIfNonZero(RceditPath, args, null));
}
[SupportedOSPlatform("windows")]
public void SetPEVersionBlockFromPackageInfo(string exePath, NuGet.IPackage package, string iconPath = null)
{
Log.Debug("Updating StringTable resources for: " + exePath);
var realExePath = Path.GetFullPath(exePath);
List<string> args = new List<string>() {
realExePath,
"--set-version-string", "CompanyName", package.ProductCompany,
"--set-version-string", "LegalCopyright", package.ProductCopyright,
"--set-version-string", "FileDescription", package.ProductDescription,
"--set-version-string", "ProductName", package.ProductName,
"--set-file-version", package.Version.ToString(),
"--set-product-version", package.Version.ToString(),
};
if (iconPath != null) {
args.Add("--set-icon");
args.Add(Path.GetFullPath(iconPath));
}
Utility.Retry(() => InvokeAndThrowIfNonZero(RceditPath, args, null));
}
private const string WIN_KERNEL32 = "kernel32.dll";
private const string WIN_SHELL32 = "shell32.dll";
[SupportedOSPlatform("windows")]
[DllImport(WIN_KERNEL32, EntryPoint = "LocalFree", SetLastError = true)]
private static extern IntPtr _LocalFree(IntPtr hMem);
[SupportedOSPlatform("windows")]
[DllImport(WIN_SHELL32, EntryPoint = "CommandLineToArgvW", CharSet = CharSet.Unicode)]
private static extern IntPtr _CommandLineToArgvW([MarshalAs(UnmanagedType.LPWStr)] string cmdLine, out int numArgs);
[SupportedOSPlatform("windows")]
protected static string[] CommandLineToArgvW(string cmdLine)
{
IntPtr argv = IntPtr.Zero;
try {
argv = _CommandLineToArgvW(cmdLine, out var numArgs);
if (argv == IntPtr.Zero) {
throw new Win32Exception();
}
var result = new string[numArgs];
for (int i = 0; i < numArgs; i++) {
IntPtr currArg = Marshal.ReadIntPtr(argv, i * Marshal.SizeOf(typeof(IntPtr)));
result[i] = Marshal.PtrToStringUni(currArg);
}
return result;
} finally {
_LocalFree(argv);
}
}
}

View File

@@ -0,0 +1,42 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Versioning;
using System.Text;
using System.Threading.Tasks;
namespace Velopack.Packaging.Windows
{
[SupportedOSPlatform("windows")]
public class Rcedit
{
public static void SetExeIcon(string exePath, string iconPath)
{
var args = new[] { Path.GetFullPath(exePath), "--set-icon", iconPath };
Utility.Retry(() => Exe.InvokeAndThrowIfNonZero(HelperFile.RceditPath, args, null));
}
[SupportedOSPlatform("windows")]
public static void SetPEVersionBlockFromPackageInfo(string exePath, NuGet.IPackage package, string iconPath = null)
{
var realExePath = Path.GetFullPath(exePath);
List<string> args = new List<string>() {
realExePath,
"--set-version-string", "CompanyName", package.ProductCompany,
"--set-version-string", "LegalCopyright", package.ProductCopyright,
"--set-version-string", "FileDescription", package.ProductDescription,
"--set-version-string", "ProductName", package.ProductName,
"--set-file-version", package.Version.ToString(),
"--set-product-version", package.Version.ToString(),
};
if (iconPath != null) {
args.Add("--set-icon");
args.Add(Path.GetFullPath(iconPath));
}
Utility.Retry(() => Exe.InvokeAndThrowIfNonZero(HelperFile.RceditPath, args, null));
}
}
}

View File

@@ -1,5 +1,6 @@
using Microsoft.Extensions.Logging;
using Velopack.Compression;
using Velopack.Packaging.Exceptions;
namespace Velopack.Packaging.Commands
{
@@ -26,10 +27,8 @@ namespace Velopack.Packaging.Commands
var tmp = Utility.GetDefaultTempBaseDirectory();
using var _1 = Utility.GetTempDirectory(out var workDir);
var helper = new HelperFile(_logger);
var updateExe = helper.GetUpdatePath();
var delta = new DeltaPackage(_logger, tmp, updateExe);
var delta = new DeltaEmbedded(HelperFile.GetZstdPath(), _logger, tmp);
EasyZip.ExtractZipToDirectory(_logger, options.BasePackage, workDir);
await Progress.ExecuteAsync(_logger, async (ctx) => {

View File

@@ -0,0 +1,35 @@
using Microsoft.Extensions.Logging;
using Velopack.Compression;
namespace Velopack.Packaging
{
public class DeltaEmbedded
{
private readonly DeltaImpl _delta;
public DeltaEmbedded(string zstdPath, ILogger logger, string baseTmpDir)
{
_delta = new DeltaImpl(zstdPath, logger, baseTmpDir);
}
public void ApplyDeltaPackageFast(string workingPath, string deltaPackageZip, Action<int> progress = null)
{
_delta.ApplyDeltaPackageFast(workingPath, deltaPackageZip, progress);
}
private class DeltaImpl : DeltaPackage
{
private readonly Zstd _zstd;
public DeltaImpl(string zstdPath, ILogger logger, string baseTmpDir) : base(logger, baseTmpDir)
{
_zstd = new Zstd(zstdPath);
}
protected override void ApplyZstdPatch(string baseFile, string patchFile, string outputFile)
{
_zstd.ApplyPatch(baseFile, patchFile, outputFile);
}
}
}
}

View File

@@ -0,0 +1,8 @@
namespace Velopack.Packaging;
public enum DeltaMode
{
None,
BestSpeed,
BestSize,
}

View File

@@ -4,6 +4,7 @@ using System.Runtime.Intrinsics.Arm;
using System.Text;
using Microsoft.Extensions.Logging;
using Velopack.Compression;
using Velopack.Packaging.Exceptions;
namespace Velopack.Packaging;
@@ -32,16 +33,12 @@ public class DeltaPackageBuilder
if (newPackage == null) throw new ArgumentNullException(nameof(newPackage));
if (String.IsNullOrEmpty(outputFile) || File.Exists(outputFile)) throw new ArgumentException("The output file is null or already exists", nameof(outputFile));
bool isZstdAvailable = true;
var helper = new HelperFile(_logger);
if (!VelopackRuntimeInfo.IsWindows) {
try {
helper.AssertSystemBinaryExists("zstd");
} catch (Exception ex) {
_logger.Error(ex.Message);
_logger.Warn("Falling back to legacy bsdiff delta format. This will be a lot slower and more prone to breaking.");
isZstdAvailable = false;
}
Zstd zstd = null;
try {
zstd = new Zstd(HelperFile.GetZstdPath());
} catch (Exception ex) {
_logger.Error(ex.Message);
_logger.Warn("Zstd not available. Falling back to legacy bsdiff delta format. This will be a lot slower and more prone to breaking.");
}
if (basePackage.Version >= newPackage.Version) {
@@ -125,7 +122,7 @@ public class DeltaPackageBuilder
// 3. changed, write a delta in new
if (useZstd) {
var diffOut = targetFile.FullName + ".zsdiff";
helper.CreateZstdPatch(oldFilePath, targetFile.FullName, diffOut, mode);
zstd.CreatePatch(oldFilePath, targetFile.FullName, diffOut, mode);
} else {
var oldData = File.ReadAllBytes(oldFilePath);
var newData = File.ReadAllBytes(targetFile.FullName);
@@ -156,7 +153,7 @@ public class DeltaPackageBuilder
try {
Parallel.ForEach(newLibFiles, new ParallelOptions() { MaxDegreeOfParallelism = numParallel }, (f) => {
// we try to use zstd first, if it fails we'll try bsdiff
if (isZstdAvailable) {
if (zstd != null) {
try {
createDeltaForSingleFile(f, tempInfo, true);
return; // success, so return from this function
@@ -167,7 +164,7 @@ public class DeltaPackageBuilder
// if we're here, either zstd is not available or it failed
try {
createDeltaForSingleFile(f, tempInfo, false);
if (isZstdAvailable) {
if (zstd != null) {
_logger.Info($"Successfully created fallback bsdiff for file '{f.FullName}'.");
}
} catch (Exception ex) {

View File

@@ -1,6 +1,6 @@
using System.Diagnostics.CodeAnalysis;
namespace Velopack.Packaging;
namespace Velopack.Packaging.Exceptions;
[ExcludeFromCodeCoverage]
public class ProcessFailedException : Exception

View File

@@ -6,7 +6,7 @@ using System.Runtime.Serialization;
using System.Text;
using System.Threading.Tasks;
namespace Velopack.Packaging
namespace Velopack.Packaging.Exceptions
{
/// <summary>
/// Denotes that an error has occurred for which a stack trace should not be printed.

View File

@@ -5,7 +5,7 @@ using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Velopack.Packaging
namespace Velopack.Packaging.Exceptions
{
[ExcludeFromCodeCoverage]
public class VelopackAppVerificationException : UserInfoException

View File

@@ -0,0 +1,79 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Velopack.Packaging.Exceptions;
namespace Velopack.Packaging
{
public static class Exe
{
public static void AssertSystemBinaryExists(string binaryName)
{
try {
if (VelopackRuntimeInfo.IsWindows) {
var output = InvokeAndThrowIfNonZero("where", new[] { binaryName }, null);
if (String.IsNullOrWhiteSpace(output) || !File.Exists(output))
throw new ProcessFailedException("", "");
} else if (VelopackRuntimeInfo.IsOSX) {
InvokeAndThrowIfNonZero("command", new[] { "-v", binaryName }, null);
} else if (VelopackRuntimeInfo.IsLinux) {
InvokeAndThrowIfNonZero("which", new[] { binaryName }, null);
} else {
throw new PlatformNotSupportedException();
}
} catch (ProcessFailedException) {
throw new Exception($"Could not find '{binaryName}' on the system, ensure it is installed and on the PATH.");
}
}
public static string InvokeAndThrowIfNonZero(string exePath, IEnumerable<string> args, string workingDir)
{
var result = InvokeProcess(exePath, args, workingDir);
ProcessFailedException.ThrowIfNonZero(result);
return result.StdOutput;
}
public static (int ExitCode, string StdOutput) InvokeProcess(ProcessStartInfo psi, CancellationToken ct)
{
var pi = Process.Start(psi);
while (!ct.IsCancellationRequested) {
if (pi.WaitForExit(500)) break;
}
if (ct.IsCancellationRequested && !pi.HasExited) {
pi.Kill();
ct.ThrowIfCancellationRequested();
}
string output = pi.StandardOutput.ReadToEnd();
string error = pi.StandardError.ReadToEnd();
var all = (output ?? "") + Environment.NewLine + (error ?? "");
return (pi.ExitCode, all.Trim());
}
public static (int ExitCode, string StdOutput, string Command) InvokeProcess(string fileName, IEnumerable<string> args, string workingDirectory, CancellationToken ct = default)
{
var psi = CreateProcessStartInfo(fileName, workingDirectory);
psi.AppendArgumentListSafe(args, out var argString);
var p = InvokeProcess(psi, ct);
return (p.ExitCode, p.StdOutput, $"{fileName} {argString}");
}
public static ProcessStartInfo CreateProcessStartInfo(string fileName, string workingDirectory)
{
var psi = new ProcessStartInfo(fileName);
psi.UseShellExecute = false;
psi.WindowStyle = ProcessWindowStyle.Hidden;
psi.ErrorDialog = false;
psi.CreateNoWindow = true;
psi.RedirectStandardOutput = true;
psi.RedirectStandardError = true;
psi.WorkingDirectory = workingDirectory ?? Environment.CurrentDirectory;
return psi;
}
}
}

View File

@@ -1,63 +1,72 @@
using System.ComponentModel;
using System.Diagnostics;
using System.Reflection;
using Microsoft.Extensions.Logging;
using System.Reflection;
using System.Runtime.Versioning;
namespace Velopack.Packaging;
public enum DeltaMode
public static class HelperFile
{
None,
BestSpeed,
BestSize,
}
public class HelperFile
{
public string UpdatePath => FindHelperFile("Update.exe");
#if DEBUG
public string UpdateMacPath => FindHelperFile("update", MachO.IsMachOImage);
public string UpdateNixPath => FindHelperFile("update");
#else
public string UpdateMacPath => FindHelperFile("UpdateMac", MachO.IsMachOImage);
public string UpdateNixPath => FindHelperFile("UpdateNix");
#endif
public string GetUpdatePath(RuntimeOs? os = null)
public static string GetUpdateExeName(RuntimeOs? os = null)
{
var _os = os ?? VelopackRuntimeInfo.SystemOs;
switch (_os) {
case RuntimeOs.Windows:
return UpdatePath;
return FindHelperFile("Update.exe");
#if DEBUG
case RuntimeOs.Linux:
return UpdateNixPath;
return FindHelperFile("update");
case RuntimeOs.OSX:
return UpdateMacPath;
return FindHelperFile("update");
#else
case RuntimeOs.Linux:
return FindHelperFile("UpdateNix");
case RuntimeOs.OSX:
return FindHelperFile("UpdateMac");
#endif
default:
throw new PlatformNotSupportedException("Update binary is not available for this platform.");
}
}
public static string GetUpdatePath(RuntimeOs? os = null) => FindHelperFile(GetUpdateExeName(os));
public static string GetZstdPath()
{
if (VelopackRuntimeInfo.IsWindows)
return FindHelperFile("zstd.exe");
Exe.AssertSystemBinaryExists("zstd");
return "zstd";
}
[SupportedOSPlatform("macos")]
public static string VelopackEntitlements => FindHelperFile("Velopack.entitlements");
[SupportedOSPlatform("linux")]
public static string AppImageToolX64 => FindHelperFile("appimagetool-x86_64.AppImage");
[SupportedOSPlatform("windows")]
public static string SetupPath => FindHelperFile("Setup.exe");
[SupportedOSPlatform("windows")]
public static string StubExecutablePath => FindHelperFile("stub.exe");
[SupportedOSPlatform("windows")]
public static string SignToolPath => FindHelperFile("signtool.exe");
[SupportedOSPlatform("windows")]
public static string RceditPath => FindHelperFile("rcedit.exe");
private static List<string> _searchPaths = new List<string>();
protected readonly ILogger Log;
static HelperFile()
{
#if !DEBUG
#if DEBUG
AddSearchPath(AppContext.BaseDirectory);
AddSearchPath(AppContext.BaseDirectory, "..", "..", "..", "src", "Rust", "target", "debug");
#else
AddSearchPath(AppContext.BaseDirectory, "..", "..", "..", "vendor");
#endif
}
public HelperFile(ILogger logger)
{
Log = logger;
}
public static string FindTestFile(string toFind) => FindHelperFile(toFind, throwWhenNotFound: true);
public static void AddSearchPath(params string[] pathParts)
{
AddSearchPath(Path.Combine(pathParts));
@@ -69,89 +78,15 @@ public class HelperFile
_searchPaths.Insert(0, path);
}
public void CreateZstdPatch(string oldFile, string newFile, string outputFile, DeltaMode mode)
public static string FindHelperFile(string toFind, Func<string, bool> predicate = null, bool throwWhenNotFound = true)
{
if (mode == DeltaMode.None)
throw new ArgumentException("DeltaMode.None is not supported.", nameof(mode));
List<string> args = new() {
"--patch-from", oldFile,
newFile,
"-o", outputFile,
"--force",
};
if (mode == DeltaMode.BestSize) {
args.Add("-19");
args.Add("--single-thread");
args.Add("--zstd");
args.Add("targetLength=4096");
args.Add("--zstd");
args.Add("chainLog=30");
}
string zstdPath;
if (VelopackRuntimeInfo.IsWindows) {
zstdPath = FindHelperFile("zstd.exe");
} else {
zstdPath = "zstd";
AssertSystemBinaryExists(zstdPath);
}
InvokeAndThrowIfNonZero(zstdPath, args, null);
}
public void AssertSystemBinaryExists(string binaryName)
{
try {
if (VelopackRuntimeInfo.IsWindows) {
var output = InvokeAndThrowIfNonZero("where", new[] { binaryName }, null);
if (String.IsNullOrWhiteSpace(output) || !File.Exists(output))
throw new ProcessFailedException("", "");
} else if (VelopackRuntimeInfo.IsOSX) {
InvokeAndThrowIfNonZero("command", new[] { "-v", binaryName }, null);
} else if (VelopackRuntimeInfo.IsLinux) {
InvokeAndThrowIfNonZero("which", new[] { binaryName }, null);
} else {
throw new PlatformNotSupportedException();
}
} catch (ProcessFailedException) {
throw new Exception($"Could not find '{binaryName}' on the system, ensure it is installed and on the PATH.");
}
}
// protected static string FindAny(params string[] names)
// {
// var findCommand = SquirrelRuntimeInfo.IsWindows ? "where" : "which";
//
// // first search the usual places
// foreach (var n in names) {
// var helper = FindHelperFile(n, throwWhenNotFound: false);
// if (helper != null)
// return helper;
// }
//
// // then see if there is something on the path
// foreach (var n in names) {
// var result = ProcessUtil.InvokeProcess(findCommand, new[] { n }, null, CancellationToken.None);
// if (result.ExitCode == 0) {
// return n;
// }
// }
//
// throw new Exception($"Could not find any of {String.Join(", ", names)}.");
// }
protected static string FindHelperFile(string toFind, Func<string, bool> predicate = null, bool throwWhenNotFound = true)
{
var baseDirs = new[] {
AppContext.BaseDirectory,
Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location),
Environment.CurrentDirectory,
};
//var baseDirs = new[] {
// AppContext.BaseDirectory,
// Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location),
// Environment.CurrentDirectory,
//};
var files = _searchPaths
.Concat(baseDirs)
.Where(d => !String.IsNullOrEmpty(d))
.Distinct()
.Select(d => Path.Combine(d, toFind))
@@ -167,51 +102,4 @@ public class HelperFile
return result;
}
protected static string InvokeAndThrowIfNonZero(string exePath, IEnumerable<string> args, string workingDir)
{
var result = InvokeProcess(exePath, args, workingDir);
ProcessFailedException.ThrowIfNonZero(result);
return result.StdOutput;
}
protected static (int ExitCode, string StdOutput) InvokeProcess(ProcessStartInfo psi, CancellationToken ct)
{
var pi = Process.Start(psi);
while (!ct.IsCancellationRequested) {
if (pi.WaitForExit(500)) break;
}
if (ct.IsCancellationRequested && !pi.HasExited) {
pi.Kill();
ct.ThrowIfCancellationRequested();
}
string output = pi.StandardOutput.ReadToEnd();
string error = pi.StandardError.ReadToEnd();
var all = (output ?? "") + Environment.NewLine + (error ?? "");
return (pi.ExitCode, all.Trim());
}
protected static (int ExitCode, string StdOutput, string Command) InvokeProcess(string fileName, IEnumerable<string> args, string workingDirectory, CancellationToken ct = default)
{
var psi = CreateProcessStartInfo(fileName, workingDirectory);
psi.AppendArgumentListSafe(args, out var argString);
var p = InvokeProcess(psi, ct);
return (p.ExitCode, p.StdOutput, $"{fileName} {argString}");
}
protected static ProcessStartInfo CreateProcessStartInfo(string fileName, string workingDirectory)
{
var psi = new ProcessStartInfo(fileName);
psi.UseShellExecute = false;
psi.WindowStyle = ProcessWindowStyle.Hidden;
psi.ErrorDialog = false;
psi.CreateNoWindow = true;
psi.RedirectStandardOutput = true;
psi.RedirectStandardError = true;
psi.WorkingDirectory = workingDirectory ?? Environment.CurrentDirectory;
return psi;
}
}

View File

@@ -0,0 +1,11 @@
namespace Velopack.Packaging
{
public interface IPackOptions : INugetPackCommand
{
RID TargetRuntime { get; }
DirectoryInfo ReleaseDir { get; }
string Channel { get; }
DeltaMode DeltaMode { get; }
string EntryExecutableName { get; }
}
}

View File

@@ -6,18 +6,10 @@ using Microsoft.Extensions.Logging;
using NuGet.Versioning;
using Spectre.Console;
using Velopack.Compression;
using Velopack.Packaging.Exceptions;
namespace Velopack.Packaging
{
public interface IPackOptions : INugetPackCommand
{
RID TargetRuntime { get; }
DirectoryInfo ReleaseDir { get; }
string Channel { get; }
DeltaMode DeltaMode { get; }
string EntryExecutableName { get; }
}
public abstract class PackageBuilder<T> : ICommand<T>
where T : class, IPackOptions
{

View File

@@ -1,5 +1,6 @@
using Microsoft.Extensions.Logging;
using NuGet.Versioning;
using Velopack.Packaging.Exceptions;
using Velopack.Sources;
namespace Velopack.Packaging

View File

@@ -0,0 +1,70 @@
using Velopack.Packaging.Exceptions;
namespace Velopack.Packaging;
public class Zstd
{
private readonly string _zstdExe;
public Zstd(string zstdExe)
{
_zstdExe = zstdExe;
}
public void CreatePatch(string oldFile, string newFile, string outputFile, DeltaMode mode)
{
if (mode == DeltaMode.None)
throw new ArgumentException("DeltaMode.None is not supported.", nameof(mode));
List<string> args = new() {
"--patch-from", oldFile,
"-o", outputFile,
"--force",
"--single-thread",
newFile,
};
var windowLog = FIO_highbit64(new FileInfo(oldFile).Length) + 1;
if (windowLog >= 27) {
args.Add($"--long={windowLog}");
}
if (windowLog > 30) {
throw new UserInfoException($"The file '{Path.GetFileName(oldFile)}' is too large for delta compression. You can disable delta generation using '--delta none'.");
}
if (mode == DeltaMode.BestSize) {
args.Add("-19");
args.Add("--zstd=targetLength=4096");
args.Add("--zstd=chainLog=30");
}
Exe.InvokeAndThrowIfNonZero(_zstdExe, args, null);
}
public void ApplyPatch(string baseFile, string patchFile, string outputFile)
{
List<string> args = new() {
"--decompress",
"--patch-from", baseFile,
"-o", outputFile,
"--force",
patchFile,
};
var windowLog = FIO_highbit64(new FileInfo(baseFile).Length) + 1;
if (windowLog >= 27) {
args.Add($"--long={windowLog}");
}
Exe.InvokeAndThrowIfNonZero(_zstdExe, args, null);
}
private int FIO_highbit64(long v)
{
int count = 0;
v >>= 1;
while (v > 0) { v >>= 1; count++; }
return count;
}
}

View File

@@ -1,5 +1,5 @@
using Microsoft.Extensions.Configuration;
using Velopack.Packaging;
using Velopack.Packaging.Exceptions;
using Velopack.Vpk.Commands;
using Velopack.Vpk.Updates;

View File

@@ -4,6 +4,7 @@ using Microsoft.Extensions.Hosting;
using Serilog;
using Serilog.Events;
using Spectre.Console;
using Velopack.Packaging;
using Velopack.Vpk.Commands;
using Velopack.Vpk.Compat;
using Velopack.Vpk.Logging;

View File

@@ -23,27 +23,12 @@
<None Include="..\..\README_NUGET.md" Pack="true" PackagePath="\" />
</ItemGroup>
<ItemGroup Condition="'$(Configuration)' == 'Debug'">
<None Include="..\Rust\target\debug\update" CopyToOutputDirectory="Always" Condition="$([MSBuild]::IsOSPlatform('OSX')) or $([MSBuild]::IsOSPlatform('Linux'))" />
<None Include="..\Rust\target\debug\update.exe" CopyToOutputDirectory="Always" Condition="$([MSBuild]::IsOSPlatform('Windows'))" />
<None Include="..\Rust\target\debug\setup.exe" CopyToOutputDirectory="Always" Condition="$([MSBuild]::IsOSPlatform('Windows'))" />
<None Include="..\Rust\target\debug\stub.exe" CopyToOutputDirectory="Always" Condition="$([MSBuild]::IsOSPlatform('Windows'))" />
<None Include="..\..\vendor\appimagetool-x86_64.AppImage" CopyToOutputDirectory="Always" />
<None Include="..\..\vendor\rcedit.exe" CopyToOutputDirectory="Always" />
<None Include="..\..\vendor\zstd.exe" CopyToOutputDirectory="Always" />
<None Include="..\..\vendor\signtool.exe" CopyToOutputDirectory="Always" />
<None Include="..\..\Velopack.entitlements" CopyToOutputDirectory="Always" />
</ItemGroup>
<ItemGroup Condition="'$(PackRustAssets)' == 'true'">
<None Include="..\Rust\target\release\UpdateMac" Pack="true" PackagePath="vendor" />
<None Include="..\Rust\target\release\UpdateNix" Pack="true" PackagePath="vendor" />
<None Include="..\Rust\target\release\update.exe" Pack="true" PackagePath="vendor" />
<None Include="..\Rust\target\release\setup.exe" Pack="true" PackagePath="vendor" />
<None Include="..\Rust\target\release\stub.exe" Pack="true" PackagePath="vendor" />
</ItemGroup>
<ItemGroup>
<None Include="..\..\vendor\appimagetool-x86_64.AppImage" Pack="true" PackagePath="vendor" />
<None Include="..\..\vendor\rcedit.exe" Pack="true" PackagePath="vendor" />
<None Include="..\..\vendor\zstd.exe" Pack="true" PackagePath="vendor" />

View File

@@ -1,30 +1,24 @@
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
using System;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using Microsoft.Extensions.Logging;
// https://dev.to/emrahsungu/how-to-compare-two-files-using-net-really-really-fast-2pd9
// https://github.com/SnowflakePowered/vcdiff
namespace Velopack.Compression
{
internal class DeltaPackage
internal abstract class DeltaPackage
{
private readonly ILogger _log;
private readonly string _updatePath;
private readonly string _baseTempDir;
protected readonly ILogger Log;
protected readonly string BaseTempDir;
private static Regex DIFF_SUFFIX = new Regex(@"\.(bs|zs)?diff$", RegexOptions.Compiled | RegexOptions.IgnoreCase);
public DeltaPackage(ILogger logger, string baseTmpDir, string updateExePath)
public DeltaPackage(ILogger logger, string baseTmpDir)
{
_log = logger;
_baseTempDir = baseTmpDir;
_updatePath = updateExePath;
Log = logger;
BaseTempDir = baseTmpDir;
}
public void ApplyDeltaPackageFast(string workingPath, string deltaPackageZip, Action<int> progress = null)
@@ -33,10 +27,10 @@ namespace Velopack.Compression
if (deltaPackageZip is null) throw new ArgumentNullException(nameof(deltaPackageZip));
_log.Info($"Applying delta package from {deltaPackageZip} to delta staging directory.");
Log.Info($"Applying delta package from {deltaPackageZip} to delta staging directory.");
using var _1 = Utility.GetTempDirectory(out var deltaPath, _baseTempDir);
EasyZip.ExtractZipToDirectory(_log, deltaPackageZip, deltaPath);
using var _1 = Utility.GetTempDirectory(out var deltaPath, BaseTempDir);
EasyZip.ExtractZipToDirectory(Log, deltaPackageZip, deltaPath);
progress(10);
var pathsVisited = new List<string>();
@@ -68,7 +62,7 @@ namespace Velopack.Compression
.Select(x => x.FullName.Replace(workingPath + Path.DirectorySeparatorChar, "").ToLowerInvariant())
.Where(x => x.StartsWith("lib", StringComparison.InvariantCultureIgnoreCase) && !pathsVisited.Contains(x))
.ForEach(x => {
_log.Trace($"{x} was in old package but not in new one, deleting");
Log.Trace($"{x} was in old package but not in new one, deleting");
File.Delete(Path.Combine(workingPath, x));
});
@@ -79,7 +73,7 @@ namespace Velopack.Compression
deltaPathRelativePaths
.Where(x => !x.StartsWith("lib", StringComparison.InvariantCultureIgnoreCase))
.ForEach(x => {
_log.Trace($"Writing metadata file: {x}");
Log.Trace($"Writing metadata file: {x}");
File.Copy(Path.Combine(deltaPath, x), Path.Combine(workingPath, x), true);
});
@@ -89,50 +83,42 @@ namespace Velopack.Compression
.Where(x => !x.StartsWith("lib", StringComparison.InvariantCultureIgnoreCase)
&& !deltaPathRelativePaths.Contains(x, StringComparer.InvariantCultureIgnoreCase))
.ForEach(x => {
_log.Trace($"Deleting removed metadata file: {x}");
Log.Trace($"Deleting removed metadata file: {x}");
File.Delete(Path.Combine(workingPath, x));
});
progress(100);
}
protected abstract void ApplyZstdPatch(string baseFile, string patchFile, string outputFile);
void applyDiffToFile(string deltaPath, string relativeFilePath, string workingDirectory)
{
var inputFile = Path.Combine(deltaPath, relativeFilePath);
var finalTarget = Path.Combine(workingDirectory, DIFF_SUFFIX.Replace(relativeFilePath, ""));
using var _d = Utility.GetTempFileName(out var tempTargetFile, _baseTempDir);
using var _d = Utility.GetTempFileName(out var tempTargetFile, BaseTempDir);
// NB: Zero-length diffs indicate the file hasn't actually changed
if (new FileInfo(inputFile).Length == 0) {
_log.Trace($"{relativeFilePath} exists unchanged, skipping");
Log.Trace($"{relativeFilePath} exists unchanged, skipping");
return;
}
if (relativeFilePath.EndsWith(".zsdiff", StringComparison.InvariantCultureIgnoreCase)) {
var psi = new ProcessStartInfo(_updatePath);
psi.AppendArgumentListSafe(new string[] { "patch", "--nocolor", "--old", finalTarget, "--patch", inputFile, "--output", tempTargetFile }, out var _);
psi.CreateNoWindow = true;
_log.Trace($"Applying zstd diff to {relativeFilePath}");
var p = psi.StartRedirectOutputToILogger(_log, LogLevel.Debug);
if (!p.WaitForExit(60_000)) {
p.Kill();
throw new TimeoutException("zstd patch process timed out (60s).");
}
if (p.ExitCode != 0) {
throw new Exception($"zstd patch process failed with exit code {p.ExitCode}.");
}
Log.Trace($"Applying zstd diff to {relativeFilePath}");
ApplyZstdPatch(finalTarget, inputFile, tempTargetFile);
verifyPatchedFile(relativeFilePath, inputFile, tempTargetFile);
} else if (relativeFilePath.EndsWith(".bsdiff", StringComparison.InvariantCultureIgnoreCase)) {
using (var of = File.OpenWrite(tempTargetFile))
using (var inf = File.OpenRead(finalTarget)) {
_log.Trace($"Applying bsdiff to {relativeFilePath}");
Log.Trace($"Applying bsdiff to {relativeFilePath}");
BinaryPatchUtility.Apply(inf, () => File.OpenRead(inputFile), of);
}
verifyPatchedFile(relativeFilePath, inputFile, tempTargetFile);
} else if (relativeFilePath.EndsWith(".diff", StringComparison.InvariantCultureIgnoreCase)) {
_log.Trace($"Applying msdiff to {relativeFilePath}");
Log.Trace($"Applying msdiff to {relativeFilePath}");
if (VelopackRuntimeInfo.IsWindows) {
MsDeltaCompression.ApplyDelta(inputFile, finalTarget, tempTargetFile);
@@ -144,7 +130,7 @@ namespace Velopack.Compression
} else {
using (var of = File.OpenWrite(tempTargetFile))
using (var inf = File.OpenRead(inputFile)) {
_log.Trace($"Adding new file: {relativeFilePath}");
Log.Trace($"Adding new file: {relativeFilePath}");
inf.CopyTo(of);
}
}
@@ -164,12 +150,12 @@ namespace Velopack.Compression
var actualReleaseEntry = ReleaseEntry.GenerateFromFile(tempTargetFile);
if (expectedReleaseEntry.Filesize != actualReleaseEntry.Filesize) {
_log.Error($"Patched file {relativeFilePath} has incorrect size, expected {expectedReleaseEntry.Filesize}, got {actualReleaseEntry.Filesize}");
Log.Error($"Patched file {relativeFilePath} has incorrect size, expected {expectedReleaseEntry.Filesize}, got {actualReleaseEntry.Filesize}");
throw new ChecksumFailedException(relativeFilePath);
}
if (expectedReleaseEntry.SHA1 != actualReleaseEntry.SHA1) {
_log.Error($"Patched file {relativeFilePath} has incorrect SHA1, expected {expectedReleaseEntry.SHA1}, got {actualReleaseEntry.SHA1}");
Log.Error($"Patched file {relativeFilePath} has incorrect SHA1, expected {expectedReleaseEntry.SHA1}, got {actualReleaseEntry.SHA1}");
throw new ChecksumFailedException(relativeFilePath);
}
}

View File

@@ -0,0 +1,31 @@
using System;
using System.Diagnostics;
using Microsoft.Extensions.Logging;
namespace Velopack.Compression
{
internal class DeltaUpdateExe : DeltaPackage
{
private readonly string _updateExePath;
public DeltaUpdateExe(ILogger logger, string baseTmpDir, string updateExePath) : base(logger, baseTmpDir)
{
_updateExePath = updateExePath;
}
protected override void ApplyZstdPatch(string baseFile, string patchFile, string outputFile)
{
var psi = new ProcessStartInfo(_updateExePath);
psi.AppendArgumentListSafe(new string[] { "patch", "--nocolor", "--old", baseFile, "--patch", patchFile, "--output", outputFile }, out var _);
psi.CreateNoWindow = true;
var p = psi.StartRedirectOutputToILogger(Log, LogLevel.Debug);
if (!p.WaitForExit(30_000)) {
p.Kill();
throw new TimeoutException("zstd patch process timed out (30s).");
}
if (p.ExitCode != 0) {
throw new Exception($"zstd patch process failed with exit code {p.ExitCode}.");
}
}
}
}

View File

@@ -391,7 +391,7 @@ namespace Velopack
// applying deltas accounts for 50%-100% of progress
double progressStepSize = 100d / releasesToDownload.Length;
var builder = new DeltaPackage(Log, Locator.AppTempDir, Locator.UpdateExePath);
var builder = new DeltaUpdateExe(Log, Locator.AppTempDir, Locator.UpdateExePath);
for (var i = 0; i < releasesToDownload.Length; i++) {
var rel = releasesToDownload[i];
double baseProgress = i * progressStepSize;

View File

@@ -7,6 +7,7 @@ using Microsoft.Win32;
using NuGet.Packaging;
using Velopack.Compression;
using Velopack.Packaging.Commands;
using Velopack.Packaging.Exceptions;
using Velopack.Packaging.Windows.Commands;
using Velopack.Windows;
@@ -38,8 +39,8 @@ public class WindowsPackTests
var id = "Test.Squirrel-App";
var version = "1.0.0";
File.Copy(HelperFile.FindTestFile(exe), Path.Combine(tmpOutput, exe));
File.Copy(HelperFile.FindTestFile(pdb), Path.Combine(tmpOutput, pdb));
PathHelper.CopyRustAssetTo(exe, tmpOutput);
PathHelper.CopyRustAssetTo(pdb, tmpOutput);
var options = new WindowsPackOptions {
EntryExecutableName = exe,
@@ -103,8 +104,8 @@ public class WindowsPackTests
var id = "Test.Squirrel-App";
var version = "1.0.0";
File.Copy(HelperFile.FindTestFile(exe), Path.Combine(tmpOutput, exe));
File.Copy(HelperFile.FindTestFile(pdb), Path.Combine(tmpOutput, pdb));
PathHelper.CopyRustAssetTo(exe, tmpOutput);
PathHelper.CopyRustAssetTo(pdb, tmpOutput);
var options = new WindowsPackOptions {
EntryExecutableName = exe,
@@ -169,8 +170,8 @@ public class WindowsPackTests
var id = "Test.Squirrel-App";
var version = "1.0.0";
File.Copy(HelperFile.FindTestFile(exe), Path.Combine(tmpOutput, exe));
File.Copy(HelperFile.FindTestFile(pdb), Path.Combine(tmpOutput, pdb));
PathHelper.CopyRustAssetTo(exe, tmpOutput);
PathHelper.CopyRustAssetTo(pdb, tmpOutput);
var options = new WindowsPackOptions {
EntryExecutableName = exe,
@@ -202,8 +203,8 @@ public class WindowsPackTests
var id = "Test.Squirrel-App";
var version = "1.0.0";
File.Copy(HelperFile.FindTestFile(exe), Path.Combine(tmpOutput, exe));
File.Copy(HelperFile.FindTestFile(pdb), Path.Combine(tmpOutput, pdb));
PathHelper.CopyRustAssetTo(exe, tmpOutput);
PathHelper.CopyRustAssetTo(pdb, tmpOutput);
var options = new WindowsPackOptions {
EntryExecutableName = exe,
@@ -240,8 +241,8 @@ public class WindowsPackTests
var id = "Test.Squirrel-App";
var version = "1.0.0";
File.Copy(HelperFile.FindTestFile(exe), Path.Combine(tmpOutput, exe));
File.Copy(HelperFile.FindTestFile(pdb), Path.Combine(tmpOutput, pdb));
PathHelper.CopyRustAssetTo(exe, tmpOutput);
PathHelper.CopyRustAssetTo(pdb, tmpOutput);
var options = new WindowsPackOptions {
EntryExecutableName = exe,
@@ -272,8 +273,8 @@ public class WindowsPackTests
var id = "Test.Squirrel-App";
var version = "1.0.0";
File.Copy(HelperFile.FindTestFile(exe), Path.Combine(tmpOutput, exe));
File.Copy(HelperFile.FindTestFile(pdb), Path.Combine(tmpOutput, pdb));
PathHelper.CopyRustAssetTo(exe, tmpOutput);
PathHelper.CopyRustAssetTo(pdb, tmpOutput);
var options = new WindowsPackOptions {
EntryExecutableName = exe,