mirror of
https://github.com/velopack/velopack.git
synced 2025-10-25 15:19:22 +00:00
Remove package creation code from SquirrelLib; refactor helper exe code into class
This commit is contained in:
@@ -17,80 +17,14 @@ using SharpCompress.Compressors.Deflate;
|
||||
|
||||
namespace Squirrel
|
||||
{
|
||||
public interface IDeltaPackageBuilder
|
||||
{
|
||||
ReleasePackage CreateDeltaPackage(ReleasePackage basePackage, ReleasePackage newPackage, string outputFile);
|
||||
ReleasePackage ApplyDeltaPackage(ReleasePackage basePackage, ReleasePackage deltaPackage, string outputFile);
|
||||
}
|
||||
|
||||
public class DeltaPackageBuilder : IEnableLogger, IDeltaPackageBuilder
|
||||
public class DeltaPackage : IEnableLogger
|
||||
{
|
||||
readonly string localAppDirectory;
|
||||
public DeltaPackageBuilder(string localAppDataOverride = null)
|
||||
public DeltaPackage(string localAppDataOverride = null)
|
||||
{
|
||||
this.localAppDirectory = localAppDataOverride;
|
||||
}
|
||||
|
||||
public ReleasePackage CreateDeltaPackage(ReleasePackage basePackage, ReleasePackage newPackage, string outputFile)
|
||||
{
|
||||
Contract.Requires(basePackage != null);
|
||||
Contract.Requires(!String.IsNullOrEmpty(outputFile) && !File.Exists(outputFile));
|
||||
|
||||
if (basePackage.Version > newPackage.Version) {
|
||||
var message = String.Format(
|
||||
"You cannot create a delta package based on version {0} as it is a later version than {1}",
|
||||
basePackage.Version,
|
||||
newPackage.Version);
|
||||
throw new InvalidOperationException(message);
|
||||
}
|
||||
|
||||
if (basePackage.ReleasePackageFile == null) {
|
||||
throw new ArgumentException("The base package's release file is null", "basePackage");
|
||||
}
|
||||
|
||||
if (!File.Exists(basePackage.ReleasePackageFile)) {
|
||||
throw new FileNotFoundException("The base package release does not exist", basePackage.ReleasePackageFile);
|
||||
}
|
||||
|
||||
if (!File.Exists(newPackage.ReleasePackageFile)) {
|
||||
throw new FileNotFoundException("The new package release does not exist", newPackage.ReleasePackageFile);
|
||||
}
|
||||
|
||||
string baseTempPath = null;
|
||||
string tempPath = null;
|
||||
|
||||
using (Utility.WithTempDirectory(out baseTempPath, null))
|
||||
using (Utility.WithTempDirectory(out tempPath, null)) {
|
||||
var baseTempInfo = new DirectoryInfo(baseTempPath);
|
||||
var tempInfo = new DirectoryInfo(tempPath);
|
||||
|
||||
this.Log().Info("Extracting {0} and {1} into {2}",
|
||||
basePackage.ReleasePackageFile, newPackage.ReleasePackageFile, tempPath);
|
||||
|
||||
Utility.ExtractZipToDirectory(basePackage.ReleasePackageFile, baseTempInfo.FullName).Wait();
|
||||
Utility.ExtractZipToDirectory(newPackage.ReleasePackageFile, tempInfo.FullName).Wait();
|
||||
|
||||
// Collect a list of relative paths under 'lib' and map them
|
||||
// to their full name. We'll use this later to determine in
|
||||
// the new version of the package whether the file exists or
|
||||
// not.
|
||||
var baseLibFiles = baseTempInfo.GetAllFilesRecursively()
|
||||
.Where(x => x.FullName.ToLowerInvariant().Contains("lib" + Path.DirectorySeparatorChar))
|
||||
.ToDictionary(k => k.FullName.Replace(baseTempInfo.FullName, ""), v => v.FullName);
|
||||
|
||||
var newLibDir = tempInfo.GetDirectories().First(x => x.Name.ToLowerInvariant() == "lib");
|
||||
|
||||
foreach (var libFile in newLibDir.GetAllFilesRecursively()) {
|
||||
createDeltaForSingleFile(libFile, tempInfo, baseLibFiles);
|
||||
}
|
||||
|
||||
ReleasePackage.addDeltaFilesToContentTypes(tempInfo.FullName);
|
||||
Utility.CreateZipFromDirectory(outputFile, tempInfo.FullName).Wait();
|
||||
}
|
||||
|
||||
return new ReleasePackage(outputFile);
|
||||
}
|
||||
|
||||
public ReleasePackage ApplyDeltaPackage(ReleasePackage basePackage, ReleasePackage deltaPackage, string outputFile)
|
||||
{
|
||||
return ApplyDeltaPackage(basePackage, deltaPackage, outputFile, x => { });
|
||||
@@ -176,76 +110,6 @@ namespace Squirrel
|
||||
return new ReleasePackage(outputFile);
|
||||
}
|
||||
|
||||
void createDeltaForSingleFile(FileInfo targetFile, DirectoryInfo workingDirectory, Dictionary<string, string> baseFileListing)
|
||||
{
|
||||
// NB: There are three cases here that we'll handle:
|
||||
//
|
||||
// 1. Exists only in new => leave it alone, we'll use it directly.
|
||||
// 2. Exists in both old and new => write a dummy file so we know
|
||||
// to keep it.
|
||||
// 3. Exists in old but changed in new => create a delta file
|
||||
//
|
||||
// The fourth case of "Exists only in old => delete it in new"
|
||||
// is handled when we apply the delta package
|
||||
var relativePath = targetFile.FullName.Replace(workingDirectory.FullName, "");
|
||||
|
||||
if (!baseFileListing.ContainsKey(relativePath)) {
|
||||
this.Log().Info("{0} not found in base package, marking as new", relativePath);
|
||||
return;
|
||||
}
|
||||
|
||||
var oldData = File.ReadAllBytes(baseFileListing[relativePath]);
|
||||
var newData = File.ReadAllBytes(targetFile.FullName);
|
||||
|
||||
if (bytesAreIdentical(oldData, newData)) {
|
||||
this.Log().Info("{0} hasn't changed, writing dummy file", relativePath);
|
||||
|
||||
File.Create(targetFile.FullName + ".diff").Dispose();
|
||||
File.Create(targetFile.FullName + ".shasum").Dispose();
|
||||
targetFile.Delete();
|
||||
return;
|
||||
}
|
||||
|
||||
this.Log().Info("Delta patching {0} => {1}", baseFileListing[relativePath], targetFile.FullName);
|
||||
var msDelta = new MsDeltaCompression();
|
||||
|
||||
if (targetFile.Extension.Equals(".exe", StringComparison.OrdinalIgnoreCase) ||
|
||||
targetFile.Extension.Equals(".dll", StringComparison.OrdinalIgnoreCase) ||
|
||||
targetFile.Extension.Equals(".node", StringComparison.OrdinalIgnoreCase)) {
|
||||
try {
|
||||
msDelta.CreateDelta(baseFileListing[relativePath], targetFile.FullName, targetFile.FullName + ".diff");
|
||||
goto exit;
|
||||
} catch (Exception) {
|
||||
this.Log().Warn("We couldn't create a delta for {0}, attempting to create bsdiff", targetFile.Name);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
using (FileStream of = File.Create(targetFile.FullName + ".bsdiff")) {
|
||||
BinaryPatchUtility.Create(oldData, newData, of);
|
||||
|
||||
// NB: Create a dummy corrupt .diff file so that older
|
||||
// versions which don't understand bsdiff will fail out
|
||||
// until they get upgraded, instead of seeing the missing
|
||||
// file and just removing it.
|
||||
File.WriteAllText(targetFile.FullName + ".diff", "1");
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
this.Log().WarnException(String.Format("We really couldn't create a delta for {0}", targetFile.Name), ex);
|
||||
|
||||
Utility.DeleteFileHarder(targetFile.FullName + ".bsdiff", true);
|
||||
Utility.DeleteFileHarder(targetFile.FullName + ".diff", true);
|
||||
return;
|
||||
}
|
||||
|
||||
exit:
|
||||
|
||||
var rl = ReleaseEntry.GenerateFromFile(new MemoryStream(newData), targetFile.Name + ".shasum");
|
||||
File.WriteAllText(targetFile.FullName + ".shasum", rl.EntryAsString, Encoding.UTF8);
|
||||
targetFile.Delete();
|
||||
}
|
||||
|
||||
|
||||
void applyDiffToFile(string deltaPath, string relativeFilePath, string workingDirectory)
|
||||
{
|
||||
var inputFile = Path.Combine(deltaPath, relativeFilePath);
|
||||
@@ -312,23 +176,5 @@ namespace Squirrel
|
||||
throw new ChecksumFailedException() { Filename = relativeFilePath };
|
||||
}
|
||||
}
|
||||
|
||||
bool bytesAreIdentical(byte[] oldData, byte[] newData)
|
||||
{
|
||||
if (oldData == null || newData == null) {
|
||||
return oldData == newData;
|
||||
}
|
||||
if (oldData.LongLength != newData.LongLength) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (long i = 0; i < newData.LongLength; i++) {
|
||||
if (oldData[i] != newData[i]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.Design;
|
||||
using System.Diagnostics.Contracts;
|
||||
@@ -23,8 +23,6 @@ namespace Squirrel
|
||||
string InputPackageFile { get; }
|
||||
string ReleasePackageFile { get; }
|
||||
string SuggestedReleaseFileName { get; }
|
||||
|
||||
string CreateReleasePackage(string outputFile, Func<string, string> releaseNotesProcessor = null, Action<string> contentsPostProcessHook = null);
|
||||
}
|
||||
|
||||
public class ReleasePackage : IEnableLogger, IReleasePackage
|
||||
@@ -50,106 +48,6 @@ namespace Squirrel
|
||||
|
||||
public SemanticVersion Version { get { return InputPackageFile.ToSemanticVersion(); } }
|
||||
|
||||
public string CreateReleasePackage(string outputFile, Func<string, string> releaseNotesProcessor = null, Action<string> contentsPostProcessHook = null)
|
||||
{
|
||||
Contract.Requires(!String.IsNullOrEmpty(outputFile));
|
||||
releaseNotesProcessor = releaseNotesProcessor ?? (x => (new Markdown()).Transform(x));
|
||||
|
||||
if (ReleasePackageFile != null) {
|
||||
return ReleasePackageFile;
|
||||
}
|
||||
|
||||
var package = new ZipPackage(InputPackageFile);
|
||||
|
||||
var dontcare = default(SemanticVersion);
|
||||
|
||||
// NB: Our test fixtures use packages that aren't SemVer compliant,
|
||||
// we don't really care that they aren't valid
|
||||
if (!ModeDetector.InUnitTestRunner() && !SemanticVersion.TryParseStrict(package.Version.ToString(), out dontcare)) {
|
||||
throw new Exception(
|
||||
String.Format(
|
||||
"Your package version is currently {0}, which is *not* SemVer-compatible, change this to be a SemVer version number",
|
||||
package.Version.ToString()));
|
||||
}
|
||||
|
||||
// we can tell from here what platform(s) the package targets
|
||||
// but given this is a simple package we only
|
||||
// ever expect one entry here (crash hard otherwise)
|
||||
var frameworks = package.GetSupportedFrameworks();
|
||||
if (frameworks.Count() > 1) {
|
||||
var platforms = frameworks
|
||||
.Aggregate(new StringBuilder(), (sb, f) => sb.Append(f.ToString() + "; "));
|
||||
|
||||
throw new InvalidOperationException(String.Format(
|
||||
"The input package file {0} targets multiple platforms - {1} - and cannot be transformed into a release package.", InputPackageFile, platforms));
|
||||
|
||||
} else if (!frameworks.Any()) {
|
||||
throw new InvalidOperationException(String.Format(
|
||||
"The input package file {0} targets no platform and cannot be transformed into a release package.", InputPackageFile));
|
||||
}
|
||||
|
||||
// CS - docs say we don't support dependencies. I can't think of any reason allowing this is useful.
|
||||
if (package.DependencySets.Any()) {
|
||||
throw new InvalidOperationException(String.Format(
|
||||
"The input package file {0} must have no dependencies.", InputPackageFile));
|
||||
}
|
||||
|
||||
var targetFramework = frameworks.Single();
|
||||
|
||||
this.Log().Info("Creating release package: {0} => {1}", InputPackageFile, outputFile);
|
||||
|
||||
string tempPath = null;
|
||||
|
||||
using (Utility.WithTempDirectory(out tempPath, null)) {
|
||||
var tempDir = new DirectoryInfo(tempPath);
|
||||
|
||||
extractZipWithEscaping(InputPackageFile, tempPath).Wait();
|
||||
|
||||
var specPath = tempDir.GetFiles("*.nuspec").First().FullName;
|
||||
|
||||
this.Log().Info("Removing unnecessary data");
|
||||
removeDependenciesFromPackageSpec(specPath);
|
||||
|
||||
if (releaseNotesProcessor != null) {
|
||||
renderReleaseNotesMarkdown(specPath, releaseNotesProcessor);
|
||||
}
|
||||
|
||||
addDeltaFilesToContentTypes(tempDir.FullName);
|
||||
|
||||
contentsPostProcessHook?.Invoke(tempPath);
|
||||
|
||||
Utility.CreateZipFromDirectory(outputFile, tempPath).Wait();
|
||||
|
||||
ReleasePackageFile = outputFile;
|
||||
return ReleasePackageFile;
|
||||
}
|
||||
}
|
||||
|
||||
static Task extractZipWithEscaping(string zipFilePath, string outFolder)
|
||||
{
|
||||
return Task.Run(() => {
|
||||
using (var za = ZipArchive.Open(zipFilePath))
|
||||
using (var reader = za.ExtractAllEntries()) {
|
||||
while (reader.MoveToNextEntry()) {
|
||||
var parts = reader.Entry.Key.Split('\\', '/').Select(x => Uri.UnescapeDataString(x));
|
||||
var decoded = String.Join(Path.DirectorySeparatorChar.ToString(), parts);
|
||||
|
||||
var fullTargetFile = Path.Combine(outFolder, decoded);
|
||||
var fullTargetDir = Path.GetDirectoryName(fullTargetFile);
|
||||
Directory.CreateDirectory(fullTargetDir);
|
||||
|
||||
Utility.Retry(() => {
|
||||
if (reader.Entry.IsDirectory) {
|
||||
Directory.CreateDirectory(Path.Combine(outFolder, decoded));
|
||||
} else {
|
||||
reader.WriteEntryToFile(Path.Combine(outFolder, decoded));
|
||||
}
|
||||
}, 5);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static Task ExtractZipForInstall(string zipFilePath, string outFolder, string rootPackageFolder)
|
||||
{
|
||||
return ExtractZipForInstall(zipFilePath, outFolder, rootPackageFolder, x => { });
|
||||
@@ -211,59 +109,6 @@ namespace Squirrel
|
||||
progress(100);
|
||||
});
|
||||
}
|
||||
|
||||
void renderReleaseNotesMarkdown(string specPath, Func<string, string> releaseNotesProcessor)
|
||||
{
|
||||
var doc = new XmlDocument();
|
||||
doc.Load(specPath);
|
||||
|
||||
// XXX: This code looks full tart
|
||||
var metadata = doc.DocumentElement.ChildNodes
|
||||
.OfType<XmlElement>()
|
||||
.First(x => x.Name.ToLowerInvariant() == "metadata");
|
||||
|
||||
var releaseNotes = metadata.ChildNodes
|
||||
.OfType<XmlElement>()
|
||||
.FirstOrDefault(x => x.Name.ToLowerInvariant() == "releasenotes");
|
||||
|
||||
if (releaseNotes == null) {
|
||||
this.Log().Info("No release notes found in {0}", specPath);
|
||||
return;
|
||||
}
|
||||
|
||||
releaseNotes.InnerText = String.Format("<![CDATA[\n" + "{0}\n" + "]]>",
|
||||
releaseNotesProcessor(releaseNotes.InnerText));
|
||||
|
||||
doc.Save(specPath);
|
||||
}
|
||||
|
||||
void removeDependenciesFromPackageSpec(string specPath)
|
||||
{
|
||||
var xdoc = new XmlDocument();
|
||||
xdoc.Load(specPath);
|
||||
|
||||
var metadata = xdoc.DocumentElement.FirstChild;
|
||||
var dependenciesNode = metadata.ChildNodes.OfType<XmlElement>().FirstOrDefault(x => x.Name.ToLowerInvariant() == "dependencies");
|
||||
if (dependenciesNode != null) {
|
||||
metadata.RemoveChild(dependenciesNode);
|
||||
}
|
||||
|
||||
xdoc.Save(specPath);
|
||||
}
|
||||
|
||||
static internal void addDeltaFilesToContentTypes(string rootDirectory)
|
||||
{
|
||||
var doc = new XmlDocument();
|
||||
var path = Path.Combine(rootDirectory, "[Content_Types].xml");
|
||||
doc.Load(path);
|
||||
|
||||
ContentType.Merge(doc);
|
||||
ContentType.Clean(doc);
|
||||
|
||||
using (var sw = new StreamWriter(path, false, Encoding.UTF8)) {
|
||||
doc.Save(sw);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class ChecksumFailedException : Exception
|
||||
|
||||
@@ -358,7 +358,7 @@ namespace Squirrel
|
||||
var basePkg = new ReleasePackage(Path.Combine(rootAppDirectory, "packages", currentVersion.Filename));
|
||||
var deltaPkg = new ReleasePackage(Path.Combine(rootAppDirectory, "packages", releasesToApply.First().Filename));
|
||||
|
||||
var deltaBuilder = new DeltaPackageBuilder(Directory.GetParent(this.rootAppDirectory).FullName);
|
||||
var deltaBuilder = new DeltaPackage(Directory.GetParent(this.rootAppDirectory).FullName);
|
||||
|
||||
return deltaBuilder.ApplyDeltaPackage(basePkg, deltaPkg,
|
||||
Regex.Replace(deltaPkg.InputPackageFile, @"-delta.nupkg$", ".nupkg", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant),
|
||||
|
||||
@@ -20,7 +20,7 @@ using Squirrel.NuGet;
|
||||
|
||||
namespace Squirrel
|
||||
{
|
||||
static class Utility
|
||||
internal static class Utility
|
||||
{
|
||||
public static string RemoveByteOrderMarkerIfPresent(string content)
|
||||
{
|
||||
@@ -233,7 +233,7 @@ namespace Squirrel
|
||||
}
|
||||
}
|
||||
|
||||
private static ProcessStartInfo CreateProcessStartInfo(string fileName, string arguments, string workingDirectory = "")
|
||||
public static ProcessStartInfo CreateProcessStartInfo(string fileName, string arguments, string workingDirectory = "")
|
||||
{
|
||||
var psi = new ProcessStartInfo(fileName, arguments);
|
||||
psi.UseShellExecute = false;
|
||||
@@ -246,7 +246,7 @@ namespace Squirrel
|
||||
return psi;
|
||||
}
|
||||
|
||||
private static async Task<(int ExitCode, string StdOutput)> InvokeProcessUnsafeAsync(ProcessStartInfo psi, CancellationToken ct)
|
||||
public static async Task<(int ExitCode, string StdOutput)> InvokeProcessUnsafeAsync(ProcessStartInfo psi, CancellationToken ct)
|
||||
{
|
||||
var pi = Process.Start(psi);
|
||||
await Task.Run(() => {
|
||||
@@ -405,88 +405,6 @@ namespace Squirrel
|
||||
}
|
||||
}
|
||||
|
||||
public static string FindHelperExecutable(string toFind, IEnumerable<string> additionalDirs = null, bool throwWhenNotFound = false)
|
||||
{
|
||||
if (File.Exists(toFind))
|
||||
return Path.GetFullPath(toFind);
|
||||
|
||||
additionalDirs = additionalDirs ?? Enumerable.Empty<string>();
|
||||
var dirs = (new[] { AppContext.BaseDirectory, Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location) })
|
||||
.Concat(additionalDirs ?? Enumerable.Empty<string>()).Select(Path.GetFullPath);
|
||||
|
||||
var exe = @".\" + toFind;
|
||||
var result = dirs
|
||||
.Select(x => Path.Combine(x, toFind))
|
||||
.FirstOrDefault(x => File.Exists(x));
|
||||
|
||||
if (result == null && throwWhenNotFound)
|
||||
throw new Exception($"Could not find helper '{exe}'.");
|
||||
|
||||
return result ?? exe;
|
||||
}
|
||||
|
||||
static string find7Zip()
|
||||
{
|
||||
if (ModeDetector.InUnitTestRunner()) {
|
||||
var vendorDir = Path.Combine(
|
||||
Path.GetDirectoryName(Assembly.GetExecutingAssembly().CodeBase.Replace("file:///", "")),
|
||||
"..", "..", "..", "..",
|
||||
"vendor", "7zip"
|
||||
);
|
||||
return FindHelperExecutable("7z.exe", new[] { vendorDir });
|
||||
} else {
|
||||
return FindHelperExecutable("7z.exe");
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task ExtractZipToDirectory(string zipFilePath, string outFolder)
|
||||
{
|
||||
var sevenZip = find7Zip();
|
||||
|
||||
try {
|
||||
var cmd = sevenZip;
|
||||
var args = String.Format("x \"{0}\" -tzip -mmt on -aoa -y -o\"{1}\" *", zipFilePath, outFolder);
|
||||
|
||||
// TODO this should probably fall back to SharpCompress if not on windows
|
||||
if (Environment.OSVersion.Platform != PlatformID.Win32NT) {
|
||||
cmd = "wine";
|
||||
args = sevenZip + " " + args;
|
||||
}
|
||||
|
||||
var psi = CreateProcessStartInfo(cmd, args);
|
||||
|
||||
var result = await Utility.InvokeProcessUnsafeAsync(psi, CancellationToken.None);
|
||||
if (result.ExitCode != 0) throw new Exception(result.StdOutput);
|
||||
} catch (Exception ex) {
|
||||
Log().Error($"Failed to extract file {zipFilePath} to {outFolder}\n{ex.Message}");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task CreateZipFromDirectory(string zipFilePath, string inFolder)
|
||||
{
|
||||
var sevenZip = find7Zip();
|
||||
|
||||
try {
|
||||
var cmd = sevenZip;
|
||||
var args = String.Format("a \"{0}\" -tzip -aoa -y -mmt on *", zipFilePath);
|
||||
|
||||
// TODO this should probably fall back to SharpCompress if not on windows
|
||||
if (Environment.OSVersion.Platform != PlatformID.Win32NT) {
|
||||
cmd = "wine";
|
||||
args = sevenZip + " " + args;
|
||||
}
|
||||
|
||||
var psi = CreateProcessStartInfo(cmd, args, inFolder);
|
||||
|
||||
var result = await Utility.InvokeProcessUnsafeAsync(psi, CancellationToken.None);
|
||||
if (result.ExitCode != 0) throw new Exception(result.StdOutput);
|
||||
} catch (Exception ex) {
|
||||
Log().Error($"Failed to extract file {zipFilePath} to {inFolder}\n{ex.Message}");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public static string AppDirForRelease(string rootAppDirectory, ReleaseEntry entry)
|
||||
{
|
||||
return Path.Combine(rootAppDirectory, "app-" + entry.Version.ToString());
|
||||
@@ -790,8 +708,7 @@ namespace Squirrel
|
||||
static IFullLogger logger;
|
||||
static IFullLogger Log()
|
||||
{
|
||||
return logger ??
|
||||
(logger = SquirrelLocator.CurrentMutable.GetService<ILogManager>().GetLogger(typeof(Utility)));
|
||||
return logger ?? (logger = SquirrelLocator.CurrentMutable.GetService<ILogManager>().GetLogger(typeof(Utility)));
|
||||
}
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
|
||||
|
||||
163
src/SquirrelCli/DeltaPackageBuilder.cs
Normal file
163
src/SquirrelCli/DeltaPackageBuilder.cs
Normal file
@@ -0,0 +1,163 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.Contracts;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Squirrel;
|
||||
using Squirrel.Bsdiff;
|
||||
using Squirrel.SimpleSplat;
|
||||
|
||||
namespace SquirrelCli
|
||||
{
|
||||
internal class DeltaPackageBuilder : IEnableLogger
|
||||
{
|
||||
public ReleasePackage CreateDeltaPackage(ReleasePackage basePackage, ReleasePackage newPackage, string outputFile)
|
||||
{
|
||||
Contract.Requires(basePackage != null);
|
||||
Contract.Requires(!String.IsNullOrEmpty(outputFile) && !File.Exists(outputFile));
|
||||
|
||||
if (basePackage.Version > newPackage.Version) {
|
||||
var message = String.Format(
|
||||
"You cannot create a delta package based on version {0} as it is a later version than {1}",
|
||||
basePackage.Version,
|
||||
newPackage.Version);
|
||||
throw new InvalidOperationException(message);
|
||||
}
|
||||
|
||||
if (basePackage.ReleasePackageFile == null) {
|
||||
throw new ArgumentException("The base package's release file is null", "basePackage");
|
||||
}
|
||||
|
||||
if (!File.Exists(basePackage.ReleasePackageFile)) {
|
||||
throw new FileNotFoundException("The base package release does not exist", basePackage.ReleasePackageFile);
|
||||
}
|
||||
|
||||
if (!File.Exists(newPackage.ReleasePackageFile)) {
|
||||
throw new FileNotFoundException("The new package release does not exist", newPackage.ReleasePackageFile);
|
||||
}
|
||||
|
||||
string baseTempPath = null;
|
||||
string tempPath = null;
|
||||
|
||||
using (Utility.WithTempDirectory(out baseTempPath, null))
|
||||
using (Utility.WithTempDirectory(out tempPath, null)) {
|
||||
var baseTempInfo = new DirectoryInfo(baseTempPath);
|
||||
var tempInfo = new DirectoryInfo(tempPath);
|
||||
|
||||
this.Log().Info("Extracting {0} and {1} into {2}",
|
||||
basePackage.ReleasePackageFile, newPackage.ReleasePackageFile, tempPath);
|
||||
|
||||
HelperExe.ExtractZipToDirectory(basePackage.ReleasePackageFile, baseTempInfo.FullName).Wait();
|
||||
HelperExe.ExtractZipToDirectory(newPackage.ReleasePackageFile, tempInfo.FullName).Wait();
|
||||
|
||||
// Collect a list of relative paths under 'lib' and map them
|
||||
// to their full name. We'll use this later to determine in
|
||||
// the new version of the package whether the file exists or
|
||||
// not.
|
||||
var baseLibFiles = baseTempInfo.GetAllFilesRecursively()
|
||||
.Where(x => x.FullName.ToLowerInvariant().Contains("lib" + Path.DirectorySeparatorChar))
|
||||
.ToDictionary(k => k.FullName.Replace(baseTempInfo.FullName, ""), v => v.FullName);
|
||||
|
||||
var newLibDir = tempInfo.GetDirectories().First(x => x.Name.ToLowerInvariant() == "lib");
|
||||
|
||||
foreach (var libFile in newLibDir.GetAllFilesRecursively()) {
|
||||
createDeltaForSingleFile(libFile, tempInfo, baseLibFiles);
|
||||
}
|
||||
|
||||
ReleasePackageBuilder.addDeltaFilesToContentTypes(tempInfo.FullName);
|
||||
HelperExe.CreateZipFromDirectory(outputFile, tempInfo.FullName).Wait();
|
||||
}
|
||||
|
||||
return new ReleasePackage(outputFile);
|
||||
}
|
||||
|
||||
void createDeltaForSingleFile(FileInfo targetFile, DirectoryInfo workingDirectory, Dictionary<string, string> baseFileListing)
|
||||
{
|
||||
// NB: There are three cases here that we'll handle:
|
||||
//
|
||||
// 1. Exists only in new => leave it alone, we'll use it directly.
|
||||
// 2. Exists in both old and new => write a dummy file so we know
|
||||
// to keep it.
|
||||
// 3. Exists in old but changed in new => create a delta file
|
||||
//
|
||||
// The fourth case of "Exists only in old => delete it in new"
|
||||
// is handled when we apply the delta package
|
||||
var relativePath = targetFile.FullName.Replace(workingDirectory.FullName, "");
|
||||
|
||||
if (!baseFileListing.ContainsKey(relativePath)) {
|
||||
this.Log().Info("{0} not found in base package, marking as new", relativePath);
|
||||
return;
|
||||
}
|
||||
|
||||
var oldData = File.ReadAllBytes(baseFileListing[relativePath]);
|
||||
var newData = File.ReadAllBytes(targetFile.FullName);
|
||||
|
||||
if (bytesAreIdentical(oldData, newData)) {
|
||||
this.Log().Info("{0} hasn't changed, writing dummy file", relativePath);
|
||||
|
||||
File.Create(targetFile.FullName + ".diff").Dispose();
|
||||
File.Create(targetFile.FullName + ".shasum").Dispose();
|
||||
targetFile.Delete();
|
||||
return;
|
||||
}
|
||||
|
||||
this.Log().Info("Delta patching {0} => {1}", baseFileListing[relativePath], targetFile.FullName);
|
||||
var msDelta = new MsDeltaCompression();
|
||||
|
||||
if (targetFile.Extension.Equals(".exe", StringComparison.OrdinalIgnoreCase) ||
|
||||
targetFile.Extension.Equals(".dll", StringComparison.OrdinalIgnoreCase) ||
|
||||
targetFile.Extension.Equals(".node", StringComparison.OrdinalIgnoreCase)) {
|
||||
try {
|
||||
msDelta.CreateDelta(baseFileListing[relativePath], targetFile.FullName, targetFile.FullName + ".diff");
|
||||
goto exit;
|
||||
} catch (Exception) {
|
||||
this.Log().Warn("We couldn't create a delta for {0}, attempting to create bsdiff", targetFile.Name);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
using (FileStream of = File.Create(targetFile.FullName + ".bsdiff")) {
|
||||
BinaryPatchUtility.Create(oldData, newData, of);
|
||||
|
||||
// NB: Create a dummy corrupt .diff file so that older
|
||||
// versions which don't understand bsdiff will fail out
|
||||
// until they get upgraded, instead of seeing the missing
|
||||
// file and just removing it.
|
||||
File.WriteAllText(targetFile.FullName + ".diff", "1");
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
this.Log().WarnException(String.Format("We really couldn't create a delta for {0}", targetFile.Name), ex);
|
||||
|
||||
Utility.DeleteFileHarder(targetFile.FullName + ".bsdiff", true);
|
||||
Utility.DeleteFileHarder(targetFile.FullName + ".diff", true);
|
||||
return;
|
||||
}
|
||||
|
||||
exit:
|
||||
|
||||
var rl = ReleaseEntry.GenerateFromFile(new MemoryStream(newData), targetFile.Name + ".shasum");
|
||||
File.WriteAllText(targetFile.FullName + ".shasum", rl.EntryAsString, Encoding.UTF8);
|
||||
targetFile.Delete();
|
||||
}
|
||||
|
||||
static bool bytesAreIdentical(byte[] oldData, byte[] newData)
|
||||
{
|
||||
if (oldData == null || newData == null) {
|
||||
return oldData == newData;
|
||||
}
|
||||
if (oldData.LongLength != newData.LongLength) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (long i = 0; i < newData.LongLength; i++) {
|
||||
if (oldData[i] != newData[i]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
237
src/SquirrelCli/HelperExe.cs
Normal file
237
src/SquirrelCli/HelperExe.cs
Normal file
@@ -0,0 +1,237 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Squirrel;
|
||||
using Squirrel.SimpleSplat;
|
||||
|
||||
namespace SquirrelCli
|
||||
{
|
||||
internal static class HelperExe
|
||||
{
|
||||
public static string SetupPath => FindHelperExecutable("Setup.exe", _searchPaths);
|
||||
public static string UpdatePath => FindHelperExecutable("Update.exe", _searchPaths);
|
||||
public static string StubExecutablePath => FindHelperExecutable("StubExecutable.exe", _searchPaths);
|
||||
|
||||
// private so we don't expose paths to internal tools. these should be exposed as a helper function
|
||||
private static string NugetPath => FindHelperExecutable("NuGet.exe", _searchPaths);
|
||||
private static string RceditPath => FindHelperExecutable("rcedit.exe", _searchPaths);
|
||||
private static string SevenZipPath => FindHelperExecutable("7z.exe", _searchPaths);
|
||||
private static string SignToolPath => FindHelperExecutable("signtool.exe", _searchPaths);
|
||||
private static string SetupZipBuilderPath => FindHelperExecutable("WriteZipToSetup.exe", _searchPaths);
|
||||
|
||||
private static List<string> _searchPaths = new List<string>();
|
||||
private static IFullLogger Log = SquirrelLocator.CurrentMutable.GetService<ILogManager>().GetLogger(typeof(HelperExe));
|
||||
|
||||
static HelperExe()
|
||||
{
|
||||
if (ModeDetector.InUnitTestRunner()) {
|
||||
var baseDir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().CodeBase.Replace("file:///", ""));
|
||||
AddSearchPath(Path.Combine(baseDir, "..", "..", "..", "..", "vendor"));
|
||||
AddSearchPath(Path.Combine(baseDir, "..", "..", "..", "..", "vendor", "7zip"));
|
||||
AddSearchPath(Path.Combine(baseDir, "..", "..", "..", "..", "vendor", "wix"));
|
||||
} else {
|
||||
#if DEBUG
|
||||
AddSearchPath(Path.Combine(AssemblyRuntimeInfo.BaseDirectory, "..", "..", "..", "build", "publish"));
|
||||
AddSearchPath(Path.Combine(AssemblyRuntimeInfo.BaseDirectory, "..", "..", "..", "vendor"));
|
||||
AddSearchPath(Path.Combine(AssemblyRuntimeInfo.BaseDirectory, "..", "..", "..", "vendor", "7zip"));
|
||||
AddSearchPath(Path.Combine(AssemblyRuntimeInfo.BaseDirectory, "..", "..", "..", "vendor", "wix"));
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
public static void AddSearchPath(string path)
|
||||
{
|
||||
if (Directory.Exists(path))
|
||||
_searchPaths.Insert(0, path);
|
||||
}
|
||||
|
||||
private static string FindHelperExecutable(string toFind, IEnumerable<string> additionalDirs = null, bool throwWhenNotFound = true)
|
||||
{
|
||||
if (File.Exists(toFind))
|
||||
return Path.GetFullPath(toFind);
|
||||
|
||||
additionalDirs = additionalDirs ?? Enumerable.Empty<string>();
|
||||
var dirs = (new[] { AppContext.BaseDirectory, Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location) })
|
||||
.Concat(additionalDirs ?? Enumerable.Empty<string>()).Select(Path.GetFullPath);
|
||||
|
||||
var exe = @".\" + toFind;
|
||||
var result = dirs
|
||||
.Select(x => Path.Combine(x, toFind))
|
||||
.FirstOrDefault(x => File.Exists(x));
|
||||
|
||||
if (result == null && throwWhenNotFound)
|
||||
throw new Exception($"Could not find helper '{exe}'. If not in the default location, add additional search paths using command arguments.");
|
||||
|
||||
return result ?? exe;
|
||||
}
|
||||
|
||||
public static async Task SetExeIcon(string exePath, string iconPath)
|
||||
{
|
||||
var args = new[] { Path.GetFullPath(exePath), "--set-icon", iconPath };
|
||||
var processResult = await Utility.InvokeProcessAsync(RceditPath, args, CancellationToken.None);
|
||||
|
||||
if (processResult.ExitCode != 0) {
|
||||
var msg = String.Format(
|
||||
"Failed to modify resources, command invoked was: '{0} {1}'\n\nOutput was:\n{2}",
|
||||
RceditPath, args, processResult.StdOutput);
|
||||
|
||||
throw new Exception(msg);
|
||||
} else {
|
||||
Console.WriteLine(processResult.StdOutput);
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task SetPEVersionBlockFromPackageInfo(string exePath, Squirrel.NuGet.IPackage package, string iconPath = null)
|
||||
{
|
||||
var realExePath = Path.GetFullPath(exePath);
|
||||
var company = String.Join(",", package.Authors);
|
||||
|
||||
List<string> args = new List<string>() {
|
||||
realExePath,
|
||||
"--set-version-string", "CompanyName", company,
|
||||
"--set-version-string", "LegalCopyright", package.Copyright ?? "Copyright © " + DateTime.Now.Year.ToString() + " " + company,
|
||||
"--set-version-string", "FileDescription", package.Summary ?? package.Description ?? "Installer for " + package.Id,
|
||||
"--set-version-string", "ProductName", package.Description ?? package.Summary ?? package.Id,
|
||||
"--set-file-version", package.Version.ToString(),
|
||||
"--set-product-version", package.Version.ToString(),
|
||||
};
|
||||
|
||||
if (iconPath != null) {
|
||||
args.Add("--set-icon");
|
||||
args.Add(Path.GetFullPath(iconPath));
|
||||
}
|
||||
|
||||
var processResult = await Utility.InvokeProcessAsync(RceditPath, args, CancellationToken.None);
|
||||
|
||||
if (processResult.ExitCode != 0) {
|
||||
var msg = String.Format(
|
||||
"Failed to modify resources, command invoked was: '{0} {1}'\n\nOutput was:\n{2}",
|
||||
RceditPath, args, processResult.StdOutput);
|
||||
|
||||
throw new Exception(msg);
|
||||
} else {
|
||||
Console.WriteLine(processResult.StdOutput);
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task SignPEFile(string exePath, string signingOpts)
|
||||
{
|
||||
var psi = Utility.CreateProcessStartInfo(SignToolPath, $"sign {signingOpts} \"{exePath}\"");
|
||||
var processResult = await Utility.InvokeProcessUnsafeAsync(psi, CancellationToken.None);
|
||||
|
||||
if (processResult.ExitCode != 0) {
|
||||
var optsWithPasswordHidden = new Regex(@"/p\s+\w+").Replace(signingOpts, "/p ********");
|
||||
var msg = String.Format("Failed to sign, command invoked was: '{0} sign {1} {2}'\r\n{3}",
|
||||
SignToolPath, optsWithPasswordHidden, exePath, processResult.StdOutput);
|
||||
throw new Exception(msg);
|
||||
} else {
|
||||
Log.Info("Sign successful: " + processResult.StdOutput);
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task ValidateFrameworkVersion(string frameworkVersion)
|
||||
{
|
||||
if (String.IsNullOrWhiteSpace(frameworkVersion)) {
|
||||
return;
|
||||
}
|
||||
|
||||
var chkFrameworkResult = await Utility.InvokeProcessAsync(SetupPath, new string[] { "--checkFramework ", frameworkVersion }, CancellationToken.None);
|
||||
if (chkFrameworkResult.ExitCode != 0) {
|
||||
throw new ArgumentException($"Unsupported FrameworkVersion: '{frameworkVersion}'. {chkFrameworkResult.StdOutput}");
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task CopyResourcesToTargetStubExe(string targetStubExe, string copyResourcesFromExe)
|
||||
{
|
||||
var processResult = await Utility.InvokeProcessAsync(
|
||||
SetupZipBuilderPath,
|
||||
new string[] { "--copy-stub-resources", copyResourcesFromExe, targetStubExe },
|
||||
CancellationToken.None);
|
||||
|
||||
if (processResult.ExitCode != 0) {
|
||||
throw new Exception("Unable to copy resources to stub exe: " + processResult.StdOutput);
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task BundleZipIntoTargetSetupExe(string targetSetupExe, string zipPath, string frameworkVersion, string backgroundGif)
|
||||
{
|
||||
List<string> arguments = new List<string>() {
|
||||
targetSetupExe,
|
||||
zipPath
|
||||
};
|
||||
if (!String.IsNullOrWhiteSpace(frameworkVersion)) {
|
||||
arguments.Add("--set-required-framework");
|
||||
arguments.Add(frameworkVersion);
|
||||
}
|
||||
if (!String.IsNullOrWhiteSpace(backgroundGif)) {
|
||||
arguments.Add("--set-splash");
|
||||
arguments.Add(Path.GetFullPath(backgroundGif));
|
||||
}
|
||||
|
||||
var result = await Utility.InvokeProcessAsync(SetupZipBuilderPath, arguments, CancellationToken.None);
|
||||
if (result.ExitCode != 0)
|
||||
throw new Exception("Failed to write Zip to Setup.exe!\n\n" + result.StdOutput);
|
||||
}
|
||||
|
||||
public static async Task NugetPack(string nuspecPath, string baseDirectory, string outputDirectory)
|
||||
{
|
||||
var args = new string[] { "pack", nuspecPath, "-BasePath", baseDirectory, "-OutputDirectory", outputDirectory };
|
||||
|
||||
Log.Info($"Packing '{baseDirectory}' into nupkg.");
|
||||
var res = await Utility.InvokeProcessAsync(NugetPath, args, CancellationToken.None);
|
||||
|
||||
if (res.ExitCode != 0)
|
||||
throw new Exception($"Failed nuget pack (exit {res.ExitCode}): \r\n " + res.StdOutput);
|
||||
}
|
||||
|
||||
public static async Task ExtractZipToDirectory(string zipFilePath, string outFolder)
|
||||
{
|
||||
try {
|
||||
var cmd = SevenZipPath;
|
||||
var args = String.Format("x \"{0}\" -tzip -mmt on -aoa -y -o\"{1}\" *", zipFilePath, outFolder);
|
||||
|
||||
// TODO this should probably fall back to SharpCompress if not on windows
|
||||
if (Environment.OSVersion.Platform != PlatformID.Win32NT) {
|
||||
cmd = "wine";
|
||||
args = SevenZipPath + " " + args;
|
||||
}
|
||||
|
||||
var psi = Utility.CreateProcessStartInfo(cmd, args);
|
||||
|
||||
var result = await Utility.InvokeProcessUnsafeAsync(psi, CancellationToken.None);
|
||||
if (result.ExitCode != 0) throw new Exception(result.StdOutput);
|
||||
} catch (Exception ex) {
|
||||
Log.Error($"Failed to extract file {zipFilePath} to {outFolder}\n{ex.Message}");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task CreateZipFromDirectory(string zipFilePath, string inFolder)
|
||||
{
|
||||
try {
|
||||
var cmd = SevenZipPath;
|
||||
var args = String.Format("a \"{0}\" -tzip -aoa -y -mmt on *", zipFilePath);
|
||||
|
||||
// TODO this should probably fall back to SharpCompress if not on windows
|
||||
if (Environment.OSVersion.Platform != PlatformID.Win32NT) {
|
||||
cmd = "wine";
|
||||
args = SevenZipPath + " " + args;
|
||||
}
|
||||
|
||||
var psi = Utility.CreateProcessStartInfo(cmd, args, inFolder);
|
||||
|
||||
var result = await Utility.InvokeProcessUnsafeAsync(psi, CancellationToken.None);
|
||||
if (result.ExitCode != 0) throw new Exception(result.StdOutput);
|
||||
} catch (Exception ex) {
|
||||
Log.Error($"Failed to extract file {zipFilePath} to {inFolder}\n{ex.Message}");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -37,6 +37,7 @@ namespace SquirrelCli
|
||||
Add("f=|framework=", "Set the required .NET framework version, e.g. net461", v => framework = v);
|
||||
Add("no-delta", "Don't generate delta packages to save time", v => noDelta = true);
|
||||
Add("b=|baseUrl=", "Provides a base URL to prefix the RELEASES file packages with", v => baseUrl = v, true);
|
||||
Add("addSearchPath=", "Add additional search directories when looking for helper exe's such as Setup.exe, Update.exe, etc", v => HelperExe.AddSearchPath(v));
|
||||
}
|
||||
|
||||
public override void Validate()
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
@@ -76,13 +76,6 @@ namespace SquirrelCli
|
||||
|
||||
static IFullLogger Log => SquirrelLocator.Current.GetService<ILogManager>().GetLogger(typeof(Program));
|
||||
|
||||
static string[] VendorDirs => new string[] {
|
||||
Path.Combine(AssemblyRuntimeInfo.BaseDirectory, "..", "..", "..", "vendor")
|
||||
};
|
||||
static string BootstrapperPath = Utility.FindHelperExecutable("Setup.exe", throwWhenNotFound: true);
|
||||
static string UpdatePath = Utility.FindHelperExecutable("Update.exe", throwWhenNotFound: true);
|
||||
static string NugetPath = Utility.FindHelperExecutable("NuGet.exe", VendorDirs, throwWhenNotFound: true);
|
||||
|
||||
static void Pack(PackOptions options)
|
||||
{
|
||||
using (Utility.WithTempDirectory(out var tmpDir)) {
|
||||
@@ -104,13 +97,7 @@ namespace SquirrelCli
|
||||
var nuspecPath = Path.Combine(tmpDir, options.packName + ".nuspec");
|
||||
File.WriteAllText(nuspecPath, nuspec);
|
||||
|
||||
var args = new string[] { "pack", nuspecPath, "-BasePath", options.packDirectory, "-OutputDirectory", tmpDir };
|
||||
|
||||
Log.Info($"Packing '{options.packDirectory}' into nupkg.");
|
||||
var res = Utility.InvokeProcessAsync(NugetPath, args, CancellationToken.None).Result;
|
||||
|
||||
if (res.ExitCode != 0)
|
||||
throw new Exception($"Failed nuget pack (exit {res.ExitCode}): \r\n " + res.StdOutput);
|
||||
HelperExe.NugetPack(nuspecPath, options.packDirectory, tmpDir).Wait();
|
||||
|
||||
var nupkgPath = Directory.EnumerateFiles(tmpDir).Where(f => f.EndsWith(".nupkg")).FirstOrDefault();
|
||||
if (nupkgPath == null)
|
||||
@@ -136,12 +123,11 @@ namespace SquirrelCli
|
||||
var backgroundGif = options.splashImage;
|
||||
var setupIcon = options.setupIcon;
|
||||
|
||||
var updatePath = HelperExe.UpdatePath;
|
||||
|
||||
// validate that the provided "frameworkVersion" is supported by Setup.exe
|
||||
if (!String.IsNullOrWhiteSpace(frameworkVersion)) {
|
||||
var chkFrameworkResult = Utility.InvokeProcessAsync(BootstrapperPath, new string[] { "--checkFramework ", frameworkVersion }, CancellationToken.None).Result;
|
||||
if (chkFrameworkResult.ExitCode != 0) {
|
||||
throw new ArgumentException($"Unsupported FrameworkVersion: '{frameworkVersion}'. {chkFrameworkResult.StdOutput}");
|
||||
}
|
||||
HelperExe.ValidateFrameworkVersion(frameworkVersion).Wait();
|
||||
}
|
||||
|
||||
// copy input package to target output directory
|
||||
@@ -165,7 +151,7 @@ namespace SquirrelCli
|
||||
foreach (var file in toProcess) {
|
||||
Log.Info("Creating release package: " + file.FullName);
|
||||
|
||||
var rp = new ReleasePackage(file.FullName);
|
||||
var rp = new ReleasePackageBuilder(file.FullName);
|
||||
rp.CreateReleasePackage(Path.Combine(di.FullName, rp.SuggestedReleaseFileName), contentsPostProcessHook: pkgPath => {
|
||||
|
||||
// create stub executable for all exe's in this package (except Squirrel!)
|
||||
@@ -185,7 +171,7 @@ namespace SquirrelCli
|
||||
.Where(d => re.IsMatch(d))
|
||||
.OrderBy(d => d.Length)
|
||||
.FirstOrDefault();
|
||||
File.Copy(UpdatePath, Path.Combine(libDir, "Squirrel.exe"));
|
||||
File.Copy(updatePath, Path.Combine(libDir, "Squirrel.exe"));
|
||||
|
||||
// sign all exe's in this package
|
||||
if (signingOpts == null) return;
|
||||
@@ -198,7 +184,7 @@ namespace SquirrelCli
|
||||
}
|
||||
|
||||
Log.Info("About to sign {0}", x.FullName);
|
||||
await signPEFile(x.FullName, signingOpts);
|
||||
await HelperExe.SignPEFile(x.FullName, signingOpts);
|
||||
}, 1)
|
||||
.Wait();
|
||||
});
|
||||
@@ -207,8 +193,7 @@ namespace SquirrelCli
|
||||
|
||||
var prev = ReleaseEntry.GetPreviousRelease(previousReleases, rp, targetDir);
|
||||
if (prev != null && generateDeltas) {
|
||||
var deltaBuilder = new DeltaPackageBuilder(null);
|
||||
|
||||
var deltaBuilder = new DeltaPackageBuilder();
|
||||
var dp = deltaBuilder.CreateDeltaPackage(prev, rp,
|
||||
Path.Combine(di.FullName, rp.SuggestedReleaseFileName.Replace("full", "delta")));
|
||||
processed.Insert(0, dp.InputPackageFile);
|
||||
@@ -229,27 +214,11 @@ namespace SquirrelCli
|
||||
var targetSetupExe = Path.Combine(di.FullName, "Setup.exe");
|
||||
var newestFullRelease = Squirrel.EnumerableExtensions.MaxBy(releaseEntries, x => x.Version).Where(x => !x.IsDelta).First();
|
||||
|
||||
File.Copy(BootstrapperPath, targetSetupExe, true);
|
||||
var zipPath = createSetupEmbeddedZip(Path.Combine(di.FullName, newestFullRelease.Filename), di.FullName, signingOpts, setupIcon).Result;
|
||||
|
||||
var writeZipToSetup = Utility.FindHelperExecutable("WriteZipToSetup.exe");
|
||||
File.Copy(HelperExe.SetupPath, targetSetupExe, true);
|
||||
var zipPath = createSetupEmbeddedZip(Path.Combine(di.FullName, newestFullRelease.Filename), di.FullName, signingOpts, setupIcon, updatePath).Result;
|
||||
|
||||
try {
|
||||
List<string> arguments = new List<string>() {
|
||||
targetSetupExe,
|
||||
zipPath
|
||||
};
|
||||
if (!String.IsNullOrWhiteSpace(frameworkVersion)) {
|
||||
arguments.Add("--set-required-framework");
|
||||
arguments.Add(frameworkVersion);
|
||||
}
|
||||
if (!String.IsNullOrWhiteSpace(backgroundGif)) {
|
||||
arguments.Add("--set-splash");
|
||||
arguments.Add(Path.GetFullPath(backgroundGif));
|
||||
}
|
||||
|
||||
var result = Utility.InvokeProcessAsync(writeZipToSetup, arguments, CancellationToken.None).Result;
|
||||
if (result.ExitCode != 0) throw new Exception("Failed to write Zip to Setup.exe!\n\n" + result.StdOutput);
|
||||
HelperExe.BundleZipIntoTargetSetupExe(targetSetupExe, zipPath, frameworkVersion, backgroundGif).Wait();
|
||||
} catch (Exception ex) {
|
||||
Log.ErrorException("Failed to update Setup.exe with new Zip file", ex);
|
||||
throw;
|
||||
@@ -258,10 +227,10 @@ namespace SquirrelCli
|
||||
}
|
||||
|
||||
Utility.Retry(() =>
|
||||
setPEVersionInfoAndIcon(targetSetupExe, new ZipPackage(package), setupIcon).Wait());
|
||||
HelperExe.SetPEVersionBlockFromPackageInfo(targetSetupExe, new ZipPackage(package), setupIcon).Wait());
|
||||
|
||||
if (signingOpts != null) {
|
||||
signPEFile(targetSetupExe, signingOpts).Wait();
|
||||
HelperExe.SignPEFile(targetSetupExe, signingOpts).Wait();
|
||||
}
|
||||
|
||||
//if (generateMsi) {
|
||||
@@ -273,14 +242,14 @@ namespace SquirrelCli
|
||||
//}
|
||||
}
|
||||
|
||||
static async Task<string> createSetupEmbeddedZip(string fullPackage, string releasesDir, string signingOpts, string setupIcon)
|
||||
static async Task<string> createSetupEmbeddedZip(string fullPackage, string releasesDir, string signingOpts, string setupIcon, string updatePath)
|
||||
{
|
||||
string tempPath;
|
||||
|
||||
Log.Info("Building embedded zip file for Setup.exe");
|
||||
using (Utility.WithTempDirectory(out tempPath, null)) {
|
||||
Log.ErrorIfThrows(() => {
|
||||
File.Copy(UpdatePath, Path.Combine(tempPath, "Update.exe"));
|
||||
File.Copy(updatePath, Path.Combine(tempPath, "Update.exe"));
|
||||
File.Copy(fullPackage, Path.Combine(tempPath, Path.GetFileName(fullPackage)));
|
||||
}, "Failed to write package files to temp dir: " + tempPath);
|
||||
|
||||
@@ -305,7 +274,7 @@ namespace SquirrelCli
|
||||
.Where(x => x.Name.EndsWith(".exe", StringComparison.InvariantCultureIgnoreCase))
|
||||
.Select(x => x.FullName);
|
||||
|
||||
await files.ForEachAsync(x => signPEFile(x, signingOpts));
|
||||
await files.ForEachAsync(x => HelperExe.SignPEFile(x, signingOpts));
|
||||
}
|
||||
|
||||
Log.ErrorIfThrows(() =>
|
||||
@@ -316,31 +285,6 @@ namespace SquirrelCli
|
||||
}
|
||||
}
|
||||
|
||||
static async Task signPEFile(string exePath, string signingOpts)
|
||||
{
|
||||
// Try to find SignTool.exe
|
||||
var exe = @".\signtool.exe";
|
||||
if (!File.Exists(exe)) {
|
||||
exe = Path.Combine(AssemblyRuntimeInfo.BaseDirectory, "signtool.exe");
|
||||
|
||||
// Run down PATH and hope for the best
|
||||
if (!File.Exists(exe)) exe = "signtool.exe";
|
||||
}
|
||||
|
||||
var processResult = await Utility.InvokeProcessAsync(exe,
|
||||
new string[] { "sign", signingOpts, exePath },
|
||||
CancellationToken.None);
|
||||
|
||||
if (processResult.ExitCode != 0) {
|
||||
var optsWithPasswordHidden = new Regex(@"/p\s+\w+").Replace(signingOpts, "/p ********");
|
||||
var msg = String.Format("Failed to sign, command invoked was: '{0} sign {1} {2}'",
|
||||
exe, optsWithPasswordHidden, exePath);
|
||||
|
||||
throw new Exception(msg);
|
||||
} else {
|
||||
Console.WriteLine(processResult.StdOutput);
|
||||
}
|
||||
}
|
||||
static bool isPEFileSigned(string path)
|
||||
{
|
||||
try {
|
||||
@@ -351,54 +295,14 @@ namespace SquirrelCli
|
||||
}
|
||||
}
|
||||
|
||||
static async Task createExecutableStubForExe(string fullName)
|
||||
static async Task createExecutableStubForExe(string exeToCopy)
|
||||
{
|
||||
var exe = Utility.FindHelperExecutable(@"StubExecutable.exe");
|
||||
|
||||
var target = Path.Combine(
|
||||
Path.GetDirectoryName(fullName),
|
||||
Path.GetFileNameWithoutExtension(fullName) + "_ExecutionStub.exe");
|
||||
Path.GetDirectoryName(exeToCopy),
|
||||
Path.GetFileNameWithoutExtension(exeToCopy) + "_ExecutionStub.exe");
|
||||
|
||||
await Utility.CopyToAsync(exe, target);
|
||||
|
||||
await Utility.InvokeProcessAsync(
|
||||
Utility.FindHelperExecutable("WriteZipToSetup.exe"),
|
||||
new string[] { "--copy-stub-resources", fullName, target },
|
||||
CancellationToken.None);
|
||||
}
|
||||
|
||||
static async Task setPEVersionInfoAndIcon(string exePath, IPackage package, string iconPath = null)
|
||||
{
|
||||
var realExePath = Path.GetFullPath(exePath);
|
||||
var company = String.Join(",", package.Authors);
|
||||
|
||||
List<string> args = new List<string>() {
|
||||
realExePath,
|
||||
"--set-version-string", "CompanyName", company,
|
||||
"--set-version-string", "LegalCopyright", package.Copyright ?? "Copyright © " + DateTime.Now.Year.ToString() + " " + company,
|
||||
"--set-version-string", "FileDescription", package.Summary ?? package.Description ?? "Installer for " + package.Id,
|
||||
"--set-version-string", "ProductName", package.Description ?? package.Summary ?? package.Id,
|
||||
"--set-file-version", package.Version.ToString(),
|
||||
"--set-product-version", package.Version.ToString(),
|
||||
};
|
||||
|
||||
if (iconPath != null) {
|
||||
args.Add("--set-icon");
|
||||
args.Add(Path.GetFullPath(iconPath));
|
||||
}
|
||||
|
||||
string exe = Utility.FindHelperExecutable("rcedit.exe");
|
||||
var processResult = await Utility.InvokeProcessAsync(exe, args, CancellationToken.None);
|
||||
|
||||
if (processResult.ExitCode != 0) {
|
||||
var msg = String.Format(
|
||||
"Failed to modify resources, command invoked was: '{0} {1}'\n\nOutput was:\n{2}",
|
||||
exe, args, processResult.StdOutput);
|
||||
|
||||
throw new Exception(msg);
|
||||
} else {
|
||||
Console.WriteLine(processResult.StdOutput);
|
||||
}
|
||||
await Utility.CopyToAsync(HelperExe.StubExecutablePath, target);
|
||||
await HelperExe.CopyResourcesToTargetStubExe(exeToCopy, target);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -416,8 +320,10 @@ namespace SquirrelCli
|
||||
string lvl = logLevel.ToString().Substring(0, 4).ToUpper();
|
||||
if (logLevel == LogLevel.Error || logLevel == LogLevel.Fatal) {
|
||||
Utility.ConsoleWriteWithColor($"[{lvl}] {message}\r\n", ConsoleColor.Red);
|
||||
Console.WriteLine();
|
||||
} else if (logLevel == LogLevel.Warn) {
|
||||
Utility.ConsoleWriteWithColor($"[{lvl}] {message}\r\n", ConsoleColor.Yellow);
|
||||
Console.WriteLine();
|
||||
} else {
|
||||
Console.WriteLine($"[{lvl}] {message}");
|
||||
}
|
||||
|
||||
181
src/SquirrelCli/ReleasePackageBuilder.cs
Normal file
181
src/SquirrelCli/ReleasePackageBuilder.cs
Normal file
@@ -0,0 +1,181 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.Design;
|
||||
using System.Diagnostics.Contracts;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.Versioning;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Xml;
|
||||
using Squirrel.MarkdownSharp;
|
||||
using Squirrel.NuGet;
|
||||
using Squirrel.SimpleSplat;
|
||||
using System.Threading.Tasks;
|
||||
using SharpCompress.Archives.Zip;
|
||||
using SharpCompress.Readers;
|
||||
using Squirrel;
|
||||
|
||||
namespace SquirrelCli
|
||||
{
|
||||
internal class ReleasePackageBuilder : ReleasePackage
|
||||
{
|
||||
public ReleasePackageBuilder(string inputPackageFile, bool isReleasePackage = false) : base(inputPackageFile, isReleasePackage)
|
||||
{
|
||||
}
|
||||
|
||||
public string CreateReleasePackage(string outputFile, Func<string, string> releaseNotesProcessor = null, Action<string> contentsPostProcessHook = null)
|
||||
{
|
||||
Contract.Requires(!String.IsNullOrEmpty(outputFile));
|
||||
releaseNotesProcessor = releaseNotesProcessor ?? (x => (new Markdown()).Transform(x));
|
||||
|
||||
if (ReleasePackageFile != null) {
|
||||
return ReleasePackageFile;
|
||||
}
|
||||
|
||||
var package = new ZipPackage(InputPackageFile);
|
||||
|
||||
var dontcare = default(SemanticVersion);
|
||||
|
||||
// NB: Our test fixtures use packages that aren't SemVer compliant,
|
||||
// we don't really care that they aren't valid
|
||||
if (!ModeDetector.InUnitTestRunner() && !SemanticVersion.TryParseStrict(package.Version.ToString(), out dontcare)) {
|
||||
throw new Exception(
|
||||
String.Format(
|
||||
"Your package version is currently {0}, which is *not* SemVer-compatible, change this to be a SemVer version number",
|
||||
package.Version.ToString()));
|
||||
}
|
||||
|
||||
// we can tell from here what platform(s) the package targets
|
||||
// but given this is a simple package we only
|
||||
// ever expect one entry here (crash hard otherwise)
|
||||
var frameworks = package.GetSupportedFrameworks();
|
||||
if (frameworks.Count() > 1) {
|
||||
var platforms = frameworks
|
||||
.Aggregate(new StringBuilder(), (sb, f) => sb.Append(f.ToString() + "; "));
|
||||
|
||||
throw new InvalidOperationException(String.Format(
|
||||
"The input package file {0} targets multiple platforms - {1} - and cannot be transformed into a release package.", InputPackageFile, platforms));
|
||||
|
||||
} else if (!frameworks.Any()) {
|
||||
throw new InvalidOperationException(String.Format(
|
||||
"The input package file {0} targets no platform and cannot be transformed into a release package.", InputPackageFile));
|
||||
}
|
||||
|
||||
// CS - docs say we don't support dependencies. I can't think of any reason allowing this is useful.
|
||||
if (package.DependencySets.Any()) {
|
||||
throw new InvalidOperationException(String.Format(
|
||||
"The input package file {0} must have no dependencies.", InputPackageFile));
|
||||
}
|
||||
|
||||
var targetFramework = frameworks.Single();
|
||||
|
||||
this.Log().Info("Creating release package: {0} => {1}", InputPackageFile, outputFile);
|
||||
|
||||
string tempPath = null;
|
||||
|
||||
using (Utility.WithTempDirectory(out tempPath, null)) {
|
||||
var tempDir = new DirectoryInfo(tempPath);
|
||||
|
||||
extractZipWithEscaping(InputPackageFile, tempPath).Wait();
|
||||
|
||||
var specPath = tempDir.GetFiles("*.nuspec").First().FullName;
|
||||
|
||||
this.Log().Info("Removing unnecessary data");
|
||||
removeDependenciesFromPackageSpec(specPath);
|
||||
|
||||
if (releaseNotesProcessor != null) {
|
||||
renderReleaseNotesMarkdown(specPath, releaseNotesProcessor);
|
||||
}
|
||||
|
||||
addDeltaFilesToContentTypes(tempDir.FullName);
|
||||
|
||||
contentsPostProcessHook?.Invoke(tempPath);
|
||||
|
||||
HelperExe.CreateZipFromDirectory(outputFile, tempPath).Wait();
|
||||
|
||||
ReleasePackageFile = outputFile;
|
||||
return ReleasePackageFile;
|
||||
}
|
||||
}
|
||||
|
||||
static Task extractZipWithEscaping(string zipFilePath, string outFolder)
|
||||
{
|
||||
return Task.Run(() => {
|
||||
using (var za = ZipArchive.Open(zipFilePath))
|
||||
using (var reader = za.ExtractAllEntries()) {
|
||||
while (reader.MoveToNextEntry()) {
|
||||
var parts = reader.Entry.Key.Split('\\', '/').Select(x => Uri.UnescapeDataString(x));
|
||||
var decoded = String.Join(Path.DirectorySeparatorChar.ToString(), parts);
|
||||
|
||||
var fullTargetFile = Path.Combine(outFolder, decoded);
|
||||
var fullTargetDir = Path.GetDirectoryName(fullTargetFile);
|
||||
Directory.CreateDirectory(fullTargetDir);
|
||||
|
||||
Utility.Retry(() => {
|
||||
if (reader.Entry.IsDirectory) {
|
||||
Directory.CreateDirectory(Path.Combine(outFolder, decoded));
|
||||
} else {
|
||||
reader.WriteEntryToFile(Path.Combine(outFolder, decoded));
|
||||
}
|
||||
}, 5);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void renderReleaseNotesMarkdown(string specPath, Func<string, string> releaseNotesProcessor)
|
||||
{
|
||||
var doc = new XmlDocument();
|
||||
doc.Load(specPath);
|
||||
|
||||
// XXX: This code looks full tart
|
||||
var metadata = doc.DocumentElement.ChildNodes
|
||||
.OfType<XmlElement>()
|
||||
.First(x => x.Name.ToLowerInvariant() == "metadata");
|
||||
|
||||
var releaseNotes = metadata.ChildNodes
|
||||
.OfType<XmlElement>()
|
||||
.FirstOrDefault(x => x.Name.ToLowerInvariant() == "releasenotes");
|
||||
|
||||
if (releaseNotes == null) {
|
||||
this.Log().Info("No release notes found in {0}", specPath);
|
||||
return;
|
||||
}
|
||||
|
||||
releaseNotes.InnerText = String.Format("<![CDATA[\n" + "{0}\n" + "]]>",
|
||||
releaseNotesProcessor(releaseNotes.InnerText));
|
||||
|
||||
doc.Save(specPath);
|
||||
}
|
||||
|
||||
void removeDependenciesFromPackageSpec(string specPath)
|
||||
{
|
||||
var xdoc = new XmlDocument();
|
||||
xdoc.Load(specPath);
|
||||
|
||||
var metadata = xdoc.DocumentElement.FirstChild;
|
||||
var dependenciesNode = metadata.ChildNodes.OfType<XmlElement>().FirstOrDefault(x => x.Name.ToLowerInvariant() == "dependencies");
|
||||
if (dependenciesNode != null) {
|
||||
metadata.RemoveChild(dependenciesNode);
|
||||
}
|
||||
|
||||
xdoc.Save(specPath);
|
||||
}
|
||||
|
||||
static internal void addDeltaFilesToContentTypes(string rootDirectory)
|
||||
{
|
||||
var doc = new XmlDocument();
|
||||
var path = Path.Combine(rootDirectory, "[Content_Types].xml");
|
||||
doc.Load(path);
|
||||
|
||||
ContentType.Merge(doc);
|
||||
ContentType.Clean(doc);
|
||||
|
||||
using (var sw = new StreamWriter(path, false, Encoding.UTF8)) {
|
||||
doc.Save(sw);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user