From 124ee4d227c1428824abf49f46992b8a68ae84d5 Mon Sep 17 00:00:00 2001 From: Caelan Sayler Date: Sun, 12 Dec 2021 09:46:51 +0000 Subject: [PATCH] Revert 50103ee8 for now until I can come up with a better way to refactor the tests --- src/Squirrel/DeltaPackage.cs | 158 ++++++++++++++++- src/{SquirrelCli => Squirrel}/HelperExe.cs | 2 +- src/Squirrel/ReleasePackage.cs | 155 +++++++++++++++++ src/Squirrel/UpdateManager.ApplyReleases.cs | 2 +- src/SquirrelCli/DeltaPackageBuilder.cs | 163 ------------------ src/SquirrelCli/Options.cs | 1 + src/SquirrelCli/Program.cs | 2 +- src/SquirrelCli/ReleasePackageBuilder.cs | 181 -------------------- 8 files changed, 315 insertions(+), 349 deletions(-) rename src/{SquirrelCli => Squirrel}/HelperExe.cs (99%) delete mode 100644 src/SquirrelCli/DeltaPackageBuilder.cs delete mode 100644 src/SquirrelCli/ReleasePackageBuilder.cs diff --git a/src/Squirrel/DeltaPackage.cs b/src/Squirrel/DeltaPackage.cs index 4d7f34ae..c9b5ccde 100644 --- a/src/Squirrel/DeltaPackage.cs +++ b/src/Squirrel/DeltaPackage.cs @@ -17,14 +17,80 @@ using SharpCompress.Compressors.Deflate; namespace Squirrel { - public class DeltaPackage : IEnableLogger + public interface IDeltaPackageBuilder + { + ReleasePackage CreateDeltaPackage(ReleasePackage basePackage, ReleasePackage newPackage, string outputFile); + ReleasePackage ApplyDeltaPackage(ReleasePackage basePackage, ReleasePackage deltaPackage, string outputFile); + } + + public class DeltaPackageBuilder : IEnableLogger, IDeltaPackageBuilder { readonly string localAppDirectory; - public DeltaPackage(string localAppDataOverride = null) + public DeltaPackageBuilder(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); + + 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); + } + + ReleasePackage.addDeltaFilesToContentTypes(tempInfo.FullName); + HelperExe.CreateZipFromDirectory(outputFile, tempInfo.FullName).Wait(); + } + + return new ReleasePackage(outputFile); + } + public ReleasePackage ApplyDeltaPackage(ReleasePackage basePackage, ReleasePackage deltaPackage, string outputFile) { return ApplyDeltaPackage(basePackage, deltaPackage, outputFile, x => { }); @@ -110,6 +176,76 @@ namespace Squirrel return new ReleasePackage(outputFile); } + void createDeltaForSingleFile(FileInfo targetFile, DirectoryInfo workingDirectory, Dictionary 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); @@ -176,5 +312,23 @@ 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; + } } } diff --git a/src/SquirrelCli/HelperExe.cs b/src/Squirrel/HelperExe.cs similarity index 99% rename from src/SquirrelCli/HelperExe.cs rename to src/Squirrel/HelperExe.cs index 2d4a22e6..2aa7e96a 100644 --- a/src/SquirrelCli/HelperExe.cs +++ b/src/Squirrel/HelperExe.cs @@ -10,7 +10,7 @@ using System.Threading.Tasks; using Squirrel; using Squirrel.SimpleSplat; -namespace SquirrelCli +namespace Squirrel { internal static class HelperExe { diff --git a/src/Squirrel/ReleasePackage.cs b/src/Squirrel/ReleasePackage.cs index 3cb94fe0..cdef9729 100644 --- a/src/Squirrel/ReleasePackage.cs +++ b/src/Squirrel/ReleasePackage.cs @@ -23,6 +23,8 @@ namespace Squirrel string InputPackageFile { get; } string ReleasePackageFile { get; } string SuggestedReleaseFileName { get; } + + string CreateReleasePackage(string outputFile, Func releaseNotesProcessor = null, Action contentsPostProcessHook = null); } public class ReleasePackage : IEnableLogger, IReleasePackage @@ -48,6 +50,106 @@ namespace Squirrel public SemanticVersion Version { get { return InputPackageFile.ToSemanticVersion(); } } + public string CreateReleasePackage(string outputFile, Func releaseNotesProcessor = null, Action 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); + } + } + }); + } + public static Task ExtractZipForInstall(string zipFilePath, string outFolder, string rootPackageFolder) { return ExtractZipForInstall(zipFilePath, outFolder, rootPackageFolder, x => { }); @@ -109,6 +211,59 @@ namespace Squirrel progress(100); }); } + + void renderReleaseNotesMarkdown(string specPath, Func releaseNotesProcessor) + { + var doc = new XmlDocument(); + doc.Load(specPath); + + // XXX: This code looks full tart + var metadata = doc.DocumentElement.ChildNodes + .OfType() + .First(x => x.Name.ToLowerInvariant() == "metadata"); + + var releaseNotes = metadata.ChildNodes + .OfType() + .FirstOrDefault(x => x.Name.ToLowerInvariant() == "releasenotes"); + + if (releaseNotes == null) { + this.Log().Info("No release notes found in {0}", specPath); + return; + } + + releaseNotes.InnerText = String.Format("", + 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().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 diff --git a/src/Squirrel/UpdateManager.ApplyReleases.cs b/src/Squirrel/UpdateManager.ApplyReleases.cs index 659a6d74..7547f9e3 100644 --- a/src/Squirrel/UpdateManager.ApplyReleases.cs +++ b/src/Squirrel/UpdateManager.ApplyReleases.cs @@ -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 DeltaPackage(Directory.GetParent(this.rootAppDirectory).FullName); + var deltaBuilder = new DeltaPackageBuilder(Directory.GetParent(this.rootAppDirectory).FullName); return deltaBuilder.ApplyDeltaPackage(basePkg, deltaPkg, Regex.Replace(deltaPkg.InputPackageFile, @"-delta.nupkg$", ".nupkg", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant), diff --git a/src/SquirrelCli/DeltaPackageBuilder.cs b/src/SquirrelCli/DeltaPackageBuilder.cs deleted file mode 100644 index 1ffe9675..00000000 --- a/src/SquirrelCli/DeltaPackageBuilder.cs +++ /dev/null @@ -1,163 +0,0 @@ -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 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; - } - } -} diff --git a/src/SquirrelCli/Options.cs b/src/SquirrelCli/Options.cs index 7c8a0bca..a066a950 100644 --- a/src/SquirrelCli/Options.cs +++ b/src/SquirrelCli/Options.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; +using Squirrel; using Squirrel.Lib; namespace SquirrelCli diff --git a/src/SquirrelCli/Program.cs b/src/SquirrelCli/Program.cs index 98842e9d..e74bc26c 100644 --- a/src/SquirrelCli/Program.cs +++ b/src/SquirrelCli/Program.cs @@ -157,7 +157,7 @@ namespace SquirrelCli foreach (var file in toProcess) { Log.Info("Creating release package: " + file.FullName); - var rp = new ReleasePackageBuilder(file.FullName); + var rp = new ReleasePackage(file.FullName); rp.CreateReleasePackage(Path.Combine(di.FullName, rp.SuggestedReleaseFileName), contentsPostProcessHook: pkgPath => { // create stub executable for all exe's in this package (except Squirrel!) diff --git a/src/SquirrelCli/ReleasePackageBuilder.cs b/src/SquirrelCli/ReleasePackageBuilder.cs deleted file mode 100644 index a8e45cbb..00000000 --- a/src/SquirrelCli/ReleasePackageBuilder.cs +++ /dev/null @@ -1,181 +0,0 @@ -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 releaseNotesProcessor = null, Action 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 releaseNotesProcessor) - { - var doc = new XmlDocument(); - doc.Load(specPath); - - // XXX: This code looks full tart - var metadata = doc.DocumentElement.ChildNodes - .OfType() - .First(x => x.Name.ToLowerInvariant() == "metadata"); - - var releaseNotes = metadata.ChildNodes - .OfType() - .FirstOrDefault(x => x.Name.ToLowerInvariant() == "releasenotes"); - - if (releaseNotes == null) { - this.Log().Info("No release notes found in {0}", specPath); - return; - } - - releaseNotes.InnerText = String.Format("", - 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().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); - } - } - } -}