Remove package creation code from SquirrelLib; refactor helper exe code into class

This commit is contained in:
Caelan Sayler
2021-12-11 12:31:43 +00:00
parent 7f1c33ce20
commit 50103ee88e
9 changed files with 614 additions and 518 deletions

View File

@@ -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;
}
}
}

View File

@@ -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

View File

@@ -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),

View File

@@ -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)]

View 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;
}
}
}

View 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;
}
}
}
}

View File

@@ -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()

View File

@@ -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}");
}

View 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);
}
}
}
}