Fixing build errors

This commit is contained in:
Caelan Sayler
2023-12-14 15:49:22 +00:00
parent e4c0aa2200
commit 123f91ea1d
64 changed files with 3948 additions and 3839 deletions

View File

@@ -8,7 +8,6 @@ public class BaseCommand : CliCommand
protected CliOption<DirectoryInfo> ReleaseDirectoryOption { get; private set; }
//protected static IFullLogger Log = SquirrelLocator.CurrentMutable.GetService<ILogManager>().GetLogger(typeof(BaseCommand));
private Dictionary<CliOption, Action<ParseResult>> _setters = new();
protected BaseCommand(string name, string description)
@@ -29,7 +28,7 @@ public class BaseCommand : CliCommand
protected virtual CliOption<T> AddOption<T>(Action<T> setValue, params string[] aliases)
{
return AddOption(setValue, new CliOption<T>(aliases));
return AddOption(setValue, new CliOption<T>(aliases.OrderBy(a => a.Length).First(), aliases));
}
protected virtual CliOption<T> AddOption<T>(Action<T> setValue, CliOption<T> opt)
@@ -42,7 +41,7 @@ public class BaseCommand : CliCommand
public virtual void SetProperties(ParseResult context)
{
foreach (var kvp in _setters) {
if (context.Errors.Any(e => e.SymbolResult?.Symbol?.Equals(kvp.Key) == true)) {
if (context.Errors.Any(e => e.SymbolResult?.Tokens?.Any(t => t.Equals(kvp.Key)) == true)) {
continue; // skip setting values for options with errors
}
kvp.Value(context);
@@ -55,15 +54,4 @@ public class BaseCommand : CliCommand
SetProperties(x);
return x;
}
}
public interface INugetPackCommand
{
string PackId { get; }
string PackVersion { get; }
string PackDirectory { get; }
string PackAuthors { get; }
string PackTitle { get; }
bool IncludePdb { get; }
string ReleaseNotes { get; }
}

View File

@@ -1,6 +1,9 @@
namespace Squirrel.Csq.Commands;
using Microsoft.Extensions.Logging;
using Squirrel.Deployment;
public class GitHubBaseCommand : BaseCommand
namespace Squirrel.Csq.Commands;
public abstract class GitHubBaseCommand : BaseCommand
{
public string RepoUrl { get; private set; }

View File

@@ -1,307 +1,306 @@
#if false
using System;
using System.Collections.Generic;
using System.CommandLine;
using System.CommandLine.Builder;
using System.CommandLine.Invocation;
using System.CommandLine.Parsing;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Xml;
using System.Xml.Linq;
using Microsoft.Build.Construction;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using Squirrel.CommandLine;
//using System;
//using System.Collections.Generic;
//using System.CommandLine;
//using System.CommandLine.Builder;
//using System.CommandLine.Invocation;
//using System.CommandLine.Parsing;
//using System.Diagnostics;
//using System.IO;
//using System.Linq;
//using System.Threading;
//using System.Threading.Tasks;
//using System.Xml;
//using System.Xml.Linq;
//using Microsoft.Build.Construction;
//using Microsoft.Extensions.Configuration;
//using Microsoft.Extensions.Hosting;
//using Squirrel.CommandLine;
namespace Squirrel.Tool
{
class Program
{
const string CLOWD_PACKAGE_NAME = "Clowd.Squirrel";
//namespace Squirrel.Tool
//{
// class Program
// {
// const string CLOWD_PACKAGE_NAME = "Clowd.Squirrel";
private static ConsoleLogger _logger;
// private static ConsoleLogger _logger;
private static CliOption<string> CsqVersion { get; }
= new CliOption<string>("--csq-version");
private static CliOption<FileSystemInfo> CsqSolutionPath { get; }
= new CliOption<FileSystemInfo>(new[] { "--csq-sln", "--csq-solution" }).ExistingOnly();
private static CliOption<bool> Verbose { get; }
= new CliOption<bool>("--verbose");
// private static CliOption<string> CsqVersion { get; }
// = new CliOption<string>("--csq-version");
// private static CliOption<FileSystemInfo> CsqSolutionPath { get; }
// = new CliOption<FileSystemInfo>(new[] { "--csq-sln", "--csq-solution" }).ExistingOnly();
// private static CliOption<bool> Verbose { get; }
// = new CliOption<bool>("--verbose");
static Task<int> Main(string[] args)
{
// static Task<int> Main(string[] args)
// {
HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
builder.Environment.ContentRootPath = Directory.GetCurrentDirectory();
builder.Configuration.AddJsonFile("hostsettings.json", optional: true);
builder.Configuration.AddEnvironmentVariables(prefix: "PREFIX_");
builder.Configuration.AddCommandLine(args);
_logger = ConsoleLogger.RegisterLogger();
// HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
// builder.Environment.ContentRootPath = Directory.GetCurrentDirectory();
// builder.Configuration.AddJsonFile("hostsettings.json", optional: true);
// builder.Configuration.AddEnvironmentVariables(prefix: "PREFIX_");
// builder.Configuration.AddCommandLine(args);
// _logger = ConsoleLogger.RegisterLogger();
System.CommandLine.Hosting.HostingExtensions.
// System.CommandLine.Hosting.HostingExtensions.
CliRootCommand rootCommand = new CliRootCommand() {
CsqVersion,
CsqSolutionPath,
Verbose
};
rootCommand.TreatUnmatchedTokensAsErrors = false;
// CliRootCommand rootCommand = new CliRootCommand() {
// CsqVersion,
// CsqSolutionPath,
// Verbose
// };
// rootCommand.TreatUnmatchedTokensAsErrors = false;
rootCommand.SetHandler(MainInner);
// rootCommand.SetHandler(MainInner);
ParseResult parseResult = rootCommand.Parse(inargs);
// ParseResult parseResult = rootCommand.Parse(inargs);
CommandLineBuilder builder = new CommandLineBuilder(rootCommand);
// CommandLineBuilder builder = new CommandLineBuilder(rootCommand);
if (parseResult.Directives.Contains("local")) {
builder.UseDefaults();
} else {
builder
.UseParseErrorReporting()
.UseExceptionHandler()
.CancelOnProcessTermination();
}
// if (parseResult.Directives.Contains("local")) {
// builder.UseDefaults();
// } else {
// builder
// .UseParseErrorReporting()
// .UseExceptionHandler()
// .CancelOnProcessTermination();
// }
return builder.Build().InvokeAsync(inargs);
}
// return builder.Build().InvokeAsync(inargs);
// }
static async Task MainInner(InvocationContext context)
{
bool verbose = context.ParseResult.GetValueForOption(Verbose);
FileSystemInfo explicitSolutionPath = context.ParseResult.GetValueForOption(CsqSolutionPath);
string explicitSquirrelVersion = context.ParseResult.GetValueForOption(CsqVersion);
// static async Task MainInner(InvocationContext context)
// {
// bool verbose = context.ParseResult.GetValueForOption(Verbose);
// FileSystemInfo explicitSolutionPath = context.ParseResult.GetValueForOption(CsqSolutionPath);
// string explicitSquirrelVersion = context.ParseResult.GetValueForOption(CsqVersion);
// we want to forward the --verbose argument to Squirrel, too.
var verboseArgs = verbose ? new string[] { "--verbose" } : Array.Empty<string>();
string[] restArgs = context.ParseResult.UnmatchedTokens
.Concat(verboseArgs)
.ToArray();
// // we want to forward the --verbose argument to Squirrel, too.
// var verboseArgs = verbose ? new string[] { "--verbose" } : Array.Empty<string>();
// string[] restArgs = context.ParseResult.UnmatchedTokens
// .Concat(verboseArgs)
// .ToArray();
if (verbose) {
_logger.Level = LogLevel.Debug;
}
// if (verbose) {
// _logger.Level = LogLevel.Debug;
// }
context.Console.WriteLine($"Squirrel Locator 'csq' {SquirrelRuntimeInfo.SquirrelDisplayVersion}");
_logger.Write($"Entry EXE: {SquirrelRuntimeInfo.EntryExePath}", LogLevel.Debug);
CancellationToken cancellationToken = context.GetCancellationToken();
// context.Console.WriteLine($"Squirrel Locator 'csq' {SquirrelRuntimeInfo.SquirrelDisplayVersion}");
// _logger.Write($"Entry EXE: {SquirrelRuntimeInfo.EntryExePath}", LogLevel.Debug);
// CancellationToken cancellationToken = context.GetCancellationToken();
await CheckForUpdates(cancellationToken).ConfigureAwait(false);
// await CheckForUpdates(cancellationToken).ConfigureAwait(false);
#if DEBUG && false
var devcsproj = Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "src", "Squirrel.CommandLine", "Squirrel.CommandLine.csproj");
var devargs = new[] { "run", "--no-build", "-v", "q", "--project", devcsproj, "--" }.Concat(restArgs).ToArray();
context.ExitCode = RunProcess("dotnet", devargs);
return;
#endif
//#if DEBUG && false
// var devcsproj = Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "src", "Squirrel.CommandLine", "Squirrel.CommandLine.csproj");
// var devargs = new[] { "run", "--no-build", "-v", "q", "--project", devcsproj, "--" }.Concat(restArgs).ToArray();
// context.ExitCode = RunProcess("dotnet", devargs);
// return;
//#endif
var solutionDir = FindSolutionDirectory(explicitSolutionPath?.FullName);
var nugetPackagesDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".nuget", "packages");
var cacheDir = Path.GetFullPath(solutionDir is null ? ".squirrel" : Path.Combine(solutionDir, ".squirrel"));
// var solutionDir = FindSolutionDirectory(explicitSolutionPath?.FullName);
// var nugetPackagesDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".nuget", "packages");
// var cacheDir = Path.GetFullPath(solutionDir is null ? ".squirrel" : Path.Combine(solutionDir, ".squirrel"));
Dictionary<string, string> packageSearchPaths = new();
packageSearchPaths.Add("nuget user profile cache", Path.Combine(nugetPackagesDir, CLOWD_PACKAGE_NAME.ToLower(), "{0}", "tools"));
if (solutionDir != null)
packageSearchPaths.Add("visual studio packages cache", Path.Combine(solutionDir, "packages", CLOWD_PACKAGE_NAME + ".{0}", "tools"));
packageSearchPaths.Add("squirrel cache", Path.Combine(cacheDir, "{0}", "tools"));
// Dictionary<string, string> packageSearchPaths = new();
// packageSearchPaths.Add("nuget user profile cache", Path.Combine(nugetPackagesDir, CLOWD_PACKAGE_NAME.ToLower(), "{0}", "tools"));
// if (solutionDir != null)
// packageSearchPaths.Add("visual studio packages cache", Path.Combine(solutionDir, "packages", CLOWD_PACKAGE_NAME + ".{0}", "tools"));
// packageSearchPaths.Add("squirrel cache", Path.Combine(cacheDir, "{0}", "tools"));
async Task<int> runSquirrel(string version, CancellationToken cancellationToken)
{
foreach (var kvp in packageSearchPaths) {
var path = String.Format(kvp.Value, version);
if (Directory.Exists(path)) {
_logger.Write($"Running {CLOWD_PACKAGE_NAME} {version} from {kvp.Key}", LogLevel.Info);
return RunCsqFromPath(path, restArgs);
}
}
// async Task<int> runSquirrel(string version, CancellationToken cancellationToken)
// {
// foreach (var kvp in packageSearchPaths) {
// var path = String.Format(kvp.Value, version);
// if (Directory.Exists(path)) {
// _logger.Write($"Running {CLOWD_PACKAGE_NAME} {version} from {kvp.Key}", LogLevel.Info);
// return RunCsqFromPath(path, restArgs);
// }
// }
// we did not find it locally on first pass, search for the package online
var dl = new NugetDownloader(_logger);
var package = await dl.GetPackageMetadata(CLOWD_PACKAGE_NAME, version, cancellationToken).ConfigureAwait(false);
// // we did not find it locally on first pass, search for the package online
// var dl = new NugetDownloader(_logger);
// var package = await dl.GetPackageMetadata(CLOWD_PACKAGE_NAME, version, cancellationToken).ConfigureAwait(false);
// search one more time now that we've potentially resolved the nuget version
foreach (var kvp in packageSearchPaths) {
var path = String.Format(kvp.Value, package.Identity.Version);
if (Directory.Exists(path)) {
_logger.Write($"Running {CLOWD_PACKAGE_NAME} {package.Identity.Version} from {kvp.Key}", LogLevel.Info);
return RunCsqFromPath(path, restArgs);
}
}
// // search one more time now that we've potentially resolved the nuget version
// foreach (var kvp in packageSearchPaths) {
// var path = String.Format(kvp.Value, package.Identity.Version);
// if (Directory.Exists(path)) {
// _logger.Write($"Running {CLOWD_PACKAGE_NAME} {package.Identity.Version} from {kvp.Key}", LogLevel.Info);
// return RunCsqFromPath(path, restArgs);
// }
// }
// let's try to download it from NuGet.org
var versionDir = Path.Combine(cacheDir, package.Identity.Version.ToString());
Directory.CreateDirectory(cacheDir);
Directory.CreateDirectory(versionDir);
// // let's try to download it from NuGet.org
// var versionDir = Path.Combine(cacheDir, package.Identity.Version.ToString());
// Directory.CreateDirectory(cacheDir);
// Directory.CreateDirectory(versionDir);
_logger.Write($"Downloading {package.Identity} from NuGet.", LogLevel.Info);
// _logger.Write($"Downloading {package.Identity} from NuGet.", LogLevel.Info);
var filePath = Path.Combine(versionDir, package.Identity + ".nupkg");
using (var fs = File.Create(filePath)) {
await dl.DownloadPackageToStream(package, fs, cancellationToken).ConfigureAwait(false);
}
// var filePath = Path.Combine(versionDir, package.Identity + ".nupkg");
// using (var fs = File.Create(filePath)) {
// await dl.DownloadPackageToStream(package, fs, cancellationToken).ConfigureAwait(false);
// }
EasyZip.ExtractZipToDirectory(filePath, versionDir);
// EasyZip.ExtractZipToDirectory(filePath, versionDir);
var toolsPath = Path.Combine(versionDir, "tools");
return RunCsqFromPath(toolsPath, restArgs);
}
// var toolsPath = Path.Combine(versionDir, "tools");
// return RunCsqFromPath(toolsPath, restArgs);
// }
if (explicitSquirrelVersion != null) {
context.ExitCode = await runSquirrel(explicitSquirrelVersion, cancellationToken).ConfigureAwait(false);
return;
}
// if (explicitSquirrelVersion != null) {
// context.ExitCode = await runSquirrel(explicitSquirrelVersion, cancellationToken).ConfigureAwait(false);
// return;
// }
if (solutionDir is null) {
throw new Exception($"Could not find '.sln'. Specify solution with '{CsqSolutionPath.Aliases.First()}=', or specify version of squirrel to use with '{CsqVersion.Aliases.First()}='.");
}
// if (solutionDir is null) {
// throw new Exception($"Could not find '.sln'. Specify solution with '{CsqSolutionPath.Aliases.First()}=', or specify version of squirrel to use with '{CsqVersion.Aliases.First()}='.");
// }
_logger.Write("Solution dir found at: " + solutionDir, LogLevel.Debug);
// _logger.Write("Solution dir found at: " + solutionDir, LogLevel.Debug);
// TODO actually read the SLN file rather than just searching for all .csproj files
var dependencies = GetPackageVersionsFromDir(solutionDir, CLOWD_PACKAGE_NAME).Distinct().ToArray();
// // TODO actually read the SLN file rather than just searching for all .csproj files
// var dependencies = GetPackageVersionsFromDir(solutionDir, CLOWD_PACKAGE_NAME).Distinct().ToArray();
if (dependencies.Length == 0) {
throw new Exception($"{CLOWD_PACKAGE_NAME} nuget package was not found installed in solution.");
}
// if (dependencies.Length == 0) {
// throw new Exception($"{CLOWD_PACKAGE_NAME} nuget package was not found installed in solution.");
// }
if (dependencies.Length > 1) {
throw new Exception($"Found multiple versions of {CLOWD_PACKAGE_NAME} installed in solution ({string.Join(", ", dependencies)}). " +
$"Please consolidate the following to a single version, or specify the version to use with '{CsqVersion.Aliases.First()}='");
}
// if (dependencies.Length > 1) {
// throw new Exception($"Found multiple versions of {CLOWD_PACKAGE_NAME} installed in solution ({string.Join(", ", dependencies)}). " +
// $"Please consolidate the following to a single version, or specify the version to use with '{CsqVersion.Aliases.First()}='");
// }
var targetVersion = dependencies.Single();
// var targetVersion = dependencies.Single();
context.ExitCode = await runSquirrel(targetVersion, cancellationToken).ConfigureAwait(false);
}
// context.ExitCode = await runSquirrel(targetVersion, cancellationToken).ConfigureAwait(false);
// }
static async Task CheckForUpdates(CancellationToken cancellationToken)
{
try {
var myVer = SquirrelRuntimeInfo.SquirrelNugetVersion;
var dl = new NugetDownloader(new NullNugetLogger());
var package = await dl.GetPackageMetadata("csq", (myVer.IsPrerelease || myVer.HasMetadata) ? "pre" : "latest", cancellationToken).ConfigureAwait(false);
if (package.Identity.Version > myVer)
_logger.Write($"There is a new version of csq available ({package.Identity.Version})", LogLevel.Warn);
} catch { }
}
// static async Task CheckForUpdates(CancellationToken cancellationToken)
// {
// try {
// var myVer = SquirrelRuntimeInfo.SquirrelNugetVersion;
// var dl = new NugetDownloader(new NullNugetLogger());
// var package = await dl.GetPackageMetadata("csq", (myVer.IsPrerelease || myVer.HasMetadata) ? "pre" : "latest", cancellationToken).ConfigureAwait(false);
// if (package.Identity.Version > myVer)
// _logger.Write($"There is a new version of csq available ({package.Identity.Version})", LogLevel.Warn);
// } catch { }
// }
static string FindSolutionDirectory(string slnArgument)
{
if (!String.IsNullOrWhiteSpace(slnArgument)) {
if (File.Exists(slnArgument) && slnArgument.EndsWith(".sln", StringComparison.InvariantCultureIgnoreCase)) {
// we were given a sln file as argument
return Path.GetDirectoryName(Path.GetFullPath(slnArgument));
}
// static string FindSolutionDirectory(string slnArgument)
// {
// if (!String.IsNullOrWhiteSpace(slnArgument)) {
// if (File.Exists(slnArgument) && slnArgument.EndsWith(".sln", StringComparison.InvariantCultureIgnoreCase)) {
// // we were given a sln file as argument
// return Path.GetDirectoryName(Path.GetFullPath(slnArgument));
// }
if (Directory.Exists(slnArgument) && Directory.EnumerateFiles(slnArgument, "*.sln").Any()) {
return Path.GetFullPath(slnArgument);
}
}
// if (Directory.Exists(slnArgument) && Directory.EnumerateFiles(slnArgument, "*.sln").Any()) {
// return Path.GetFullPath(slnArgument);
// }
// }
// try to find the solution directory from cwd
var cwd = Environment.CurrentDirectory;
var slnSearchDirs = new string[] {
cwd,
Path.Combine(cwd, ".."),
Path.Combine(cwd, "..", ".."),
};
// // try to find the solution directory from cwd
// var cwd = Environment.CurrentDirectory;
// var slnSearchDirs = new string[] {
// cwd,
// Path.Combine(cwd, ".."),
// Path.Combine(cwd, "..", ".."),
// };
return slnSearchDirs.FirstOrDefault(d => Directory.EnumerateFiles(d, "*.sln").Any());
}
// return slnSearchDirs.FirstOrDefault(d => Directory.EnumerateFiles(d, "*.sln").Any());
// }
static int RunCsqFromPath(string toolRootPath, string[] args)
{
// > v3.0.170
if (File.Exists(Path.Combine(toolRootPath, "Squirrel.CommandLine.runtimeconfig.json"))) {
var cliPath = Path.Combine(toolRootPath, "Squirrel.CommandLine.dll");
var dnargs = new[] { cliPath }.Concat(args).ToArray();
_logger.Write("running dotnet " + String.Join(" ", dnargs), LogLevel.Debug);
return RunProcess("dotnet", dnargs);
}
// static int RunCsqFromPath(string toolRootPath, string[] args)
// {
// // > v3.0.170
// if (File.Exists(Path.Combine(toolRootPath, "Squirrel.CommandLine.runtimeconfig.json"))) {
// var cliPath = Path.Combine(toolRootPath, "Squirrel.CommandLine.dll");
// var dnargs = new[] { cliPath }.Concat(args).ToArray();
// _logger.Write("running dotnet " + String.Join(" ", dnargs), LogLevel.Debug);
// return RunProcess("dotnet", dnargs);
// }
// v3.0 - v3.0.170
var toolDllPath = Path.Combine(toolRootPath, "csq.dll");
if (File.Exists(toolDllPath)) {
var dnargs = new[] { toolDllPath, "--csq-embedded" }.Concat(args).ToArray();
_logger.Write("running dotnet " + String.Join(" ", dnargs), LogLevel.Debug);
return RunProcess("dotnet", dnargs);
}
// // v3.0 - v3.0.170
// var toolDllPath = Path.Combine(toolRootPath, "csq.dll");
// if (File.Exists(toolDllPath)) {
// var dnargs = new[] { toolDllPath, "--csq-embedded" }.Concat(args).ToArray();
// _logger.Write("running dotnet " + String.Join(" ", dnargs), LogLevel.Debug);
// return RunProcess("dotnet", dnargs);
// }
// < v3.0
var toolExePath = Path.Combine(toolRootPath, "Squirrel.exe");
if (File.Exists(toolExePath)) {
if (!SquirrelRuntimeInfo.IsWindows)
throw new NotSupportedException(
$"Squirrel at '{toolRootPath}' does not support this operating system. Please update the package version to >= 3.0");
_logger.Write("running " + toolExePath + " " + String.Join(" ", args), LogLevel.Debug);
return RunProcess(toolExePath, args);
}
// // < v3.0
// var toolExePath = Path.Combine(toolRootPath, "Squirrel.exe");
// if (File.Exists(toolExePath)) {
// if (!SquirrelRuntimeInfo.IsWindows)
// throw new NotSupportedException(
// $"Squirrel at '{toolRootPath}' does not support this operating system. Please update the package version to >= 3.0");
// _logger.Write("running " + toolExePath + " " + String.Join(" ", args), LogLevel.Debug);
// return RunProcess(toolExePath, args);
// }
throw new Exception("Unable to locate Squirrel at: " + toolRootPath);
}
// throw new Exception("Unable to locate Squirrel at: " + toolRootPath);
// }
static int RunProcess(string path, string[] args)
{
var p = Process.Start(path, args);
p.WaitForExit();
return p.ExitCode;
}
// static int RunProcess(string path, string[] args)
// {
// var p = Process.Start(path, args);
// p.WaitForExit();
// return p.ExitCode;
// }
static IEnumerable<string> GetPackageVersionsFromDir(string rootDir, string packageName)
{
// old-style framework packages.config
foreach (var packagesFile in EnumerateFilesUntilSpecificDepth(rootDir, "packages.config", 3)) {
using var xmlStream = File.OpenRead(packagesFile);
using var xmlReader = new XmlTextReader(xmlStream);
var xdoc = XDocument.Load(xmlReader);
// static IEnumerable<string> GetPackageVersionsFromDir(string rootDir, string packageName)
// {
// // old-style framework packages.config
// foreach (var packagesFile in EnumerateFilesUntilSpecificDepth(rootDir, "packages.config", 3)) {
// using var xmlStream = File.OpenRead(packagesFile);
// using var xmlReader = new XmlTextReader(xmlStream);
// var xdoc = XDocument.Load(xmlReader);
var sqel = xdoc.Root?.Elements().FirstOrDefault(e => e.Attribute("id")?.Value == packageName);
var ver = sqel?.Attribute("version");
if (ver == null) continue;
// var sqel = xdoc.Root?.Elements().FirstOrDefault(e => e.Attribute("id")?.Value == packageName);
// var ver = sqel?.Attribute("version");
// if (ver == null) continue;
_logger.Write($"{packageName} {ver.Value} referenced in {packagesFile}", LogLevel.Debug);
// _logger.Write($"{packageName} {ver.Value} referenced in {packagesFile}", LogLevel.Debug);
if (ver.Value.Contains('*'))
throw new Exception(
$"Wildcard versions are not supported in packages.config. Remove wildcard or upgrade csproj format to use PackageReference.");
// if (ver.Value.Contains('*'))
// throw new Exception(
// $"Wildcard versions are not supported in packages.config. Remove wildcard or upgrade csproj format to use PackageReference.");
yield return ver.Value;
}
// yield return ver.Value;
// }
// new-style csproj PackageReference
foreach (var projFile in EnumerateFilesUntilSpecificDepth(rootDir, "*.csproj", 3)) {
var proj = ProjectRootElement.Open(projFile);
if (proj == null) continue;
// // new-style csproj PackageReference
// foreach (var projFile in EnumerateFilesUntilSpecificDepth(rootDir, "*.csproj", 3)) {
// var proj = ProjectRootElement.Open(projFile);
// if (proj == null) continue;
ProjectItemElement item = proj.Items.FirstOrDefault(i => i.ItemType == "PackageReference" && i.Include == packageName);
if (item == null) continue;
// ProjectItemElement item = proj.Items.FirstOrDefault(i => i.ItemType == "PackageReference" && i.Include == packageName);
// if (item == null) continue;
var version = item.Children.FirstOrDefault(x => x.ElementName == "Version") as ProjectMetadataElement;
if (version?.Value == null) continue;
// var version = item.Children.FirstOrDefault(x => x.ElementName == "Version") as ProjectMetadataElement;
// if (version?.Value == null) continue;
_logger.Write($"{packageName} {version.Value} referenced in {projFile}", LogLevel.Debug);
// _logger.Write($"{packageName} {version.Value} referenced in {projFile}", LogLevel.Debug);
yield return version.Value;
}
}
// yield return version.Value;
// }
// }
static IEnumerable<string> EnumerateFilesUntilSpecificDepth(string rootPath, string searchPattern, int maxDepth, int currentDepth = 0)
{
var files = Directory.EnumerateFiles(rootPath, searchPattern, SearchOption.TopDirectoryOnly);
foreach (var f in files) {
yield return f;
}
// static IEnumerable<string> EnumerateFilesUntilSpecificDepth(string rootPath, string searchPattern, int maxDepth, int currentDepth = 0)
// {
// var files = Directory.EnumerateFiles(rootPath, searchPattern, SearchOption.TopDirectoryOnly);
// foreach (var f in files) {
// yield return f;
// }
if (currentDepth < maxDepth) {
foreach (var dir in Directory.EnumerateDirectories(rootPath)) {
foreach (var file in EnumerateFilesUntilSpecificDepth(dir, searchPattern, maxDepth, currentDepth + 1)) {
yield return file;
}
}
}
}
}
}
// if (currentDepth < maxDepth) {
// foreach (var dir in Directory.EnumerateDirectories(rootPath)) {
// foreach (var file in EnumerateFilesUntilSpecificDepth(dir, searchPattern, maxDepth, currentDepth + 1)) {
// yield return file;
// }
// }
// }
// }
// }
//}

View File

@@ -1,80 +1,117 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Hosting;
using Serilog.Events;
using Serilog;
using CliFx;
using Microsoft.Extensions.Configuration;
using Squirrel.Csq.Commands;
using Squirrel.Deployment;
using Microsoft.Extensions.DependencyInjection;
namespace Squirrel.Csq;
public class Program
{
public static async Task<int> Main(string[] args)
public static CliOption<string> TargetRuntime { get; }
= new CliOption<string>("runtime", "-r", "--runtime", "The target runtime to build packages for.")
.SetArgumentHelpName("RID")
.MustBeSupportedRid()
.SetRequired();
public static CliOption<bool> VerboseOption { get; }
= new CliOption<bool>("--verbose", "Print diagnostic messages.");
public static Task<int> Main(string[] args)
{
CliRootCommand platformRootCommand = new CliRootCommand() {
TargetRuntime,
VerboseOption,
};
platformRootCommand.TreatUnmatchedTokensAsErrors = false;
ParseResult parseResult = platformRootCommand.Parse(args);
var runtime = RID.Parse(parseResult.GetValue(TargetRuntime) ?? SquirrelRuntimeInfo.SystemOs.GetOsShortName());
bool verbose = parseResult.GetValue(VerboseOption);
var builder = Host.CreateEmptyApplicationBuilder(new HostApplicationBuilderSettings {
Args = args,
ApplicationName = "My App",
ApplicationName = "Clowd.Squirrel",
EnvironmentName = "Production",
ContentRootPath = Environment.CurrentDirectory,
Configuration = new ConfigurationManager(),
});
var minLevel = verbose ? LogEventLevel.Debug : LogEventLevel.Information;
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Is(minLevel)
.MinimumLevel.Override("Microsoft", LogEventLevel.Warning)
.MinimumLevel.Override("System", LogEventLevel.Warning)
.WriteTo.Console()
.CreateLogger();
builder.Logging.AddSerilog();
var host = builder.Build();
host.Services.
var logger = host.Services.GetRequiredService<Microsoft.Extensions.Logging.ILogger<Program>>();
CliRootCommand rootCommand = new CliRootCommand($"Squirrel {SquirrelRuntimeInfo.SquirrelDisplayVersion} for creating and distributing Squirrel releases.");
rootCommand.Options.Add(TargetRuntime);
rootCommand.Options.Add(VerboseOption);
return await new CliApplicationBuilder()
.AddCommandsFromThisAssembly()
.Build()
.RunAsync()
.ConfigureAwait(false);
switch (runtime.BaseRID) {
case RuntimeOs.Windows:
if (!SquirrelRuntimeInfo.IsWindows)
logger.Warn("Cross-compiling will cause some commands and options of Squirrel to be unavailable.");
Add(rootCommand, new PackWindowsCommand(), Windows.Commands.Pack);
Add(rootCommand, new ReleasifyWindowsCommand(), Windows.Commands.Releasify);
break;
case RuntimeOs.OSX:
if (!SquirrelRuntimeInfo.IsOSX)
throw new InvalidOperationException("Cannot create OSX packages on non-OSX platforms.");
Add(rootCommand, new BundleOsxCommand(), OSX.Commands.Bundle);
Add(rootCommand, new ReleasifyOsxCommand(), OSX.Commands.Releasify);
break;
default:
throw new NotSupportedException("Unsupported OS platform: " + runtime.BaseRID.GetOsLongName());
}
CliCommand downloadCommand = new CliCommand("download", "Download's the latest release from a remote update source.");
Add(downloadCommand, new HttpDownloadCommand(), options => SimpleWebRepository.DownloadRecentPackages(options));
Add(downloadCommand, new S3DownloadCommand(), options => S3Repository.DownloadRecentPackages(options));
Add(downloadCommand, new GitHubDownloadCommand(), options => GitHubRepository.DownloadRecentPackages(options));
rootCommand.Add(downloadCommand);
var uploadCommand = new CliCommand("upload", "Upload local package(s) to a remote update source.");
Add(uploadCommand, new S3UploadCommand(), options => S3Repository.UploadMissingPackages(options));
Add(uploadCommand, new GitHubUploadCommand(), options => GitHubRepository.UploadMissingPackages(options));
rootCommand.Add(uploadCommand);
var cli = new CliConfiguration(rootCommand);
return cli.InvokeAsync(args);
}
private static Parser CreateParser()
private static CliCommand Add<T>(CliCommand parent, T command, Action<T> execute)
where T : BaseCommand
{
//var rootCommand = new RootCommand("Bicep registry module tool")
// .AddSubcommand(new ValidateCommand())
// .AddSubcommand(new GenerateCommand());
var parser = new CliConfiguration(null)
.UseHost(Host.CreateDefaultBuilder, ConfigureHost)
.UseDefaults()
.UseVerboseOption()
.Build();
// Have to use parser.Invoke instead of rootCommand.Invoke due to the
// System.CommandLine bug: https://github.com/dotnet/command-line-api/issues/1691.
rootCommand.Handler = CommandHandler.Create(() => parser.Invoke("-h"));
return parser;
command.SetAction((ctx) => {
command.SetProperties(ctx);
command.TargetRuntime = RID.Parse(ctx.GetValue(TargetRuntime));
execute(command);
});
parent.Subcommands.Add(command);
return command;
}
private static IHostBuilder CreateBuilder(string[] args)
private static CliCommand Add<T>(CliCommand parent, T command, Func<T, Task> execute)
where T : BaseCommand
{
}
private static void ConfigureHost(IHostBuilder builder)
{
builder.UseSerilog((context, logging) => logging
.MinimumLevel.Is(GetMinimumLogEventLevel(context))
.MinimumLevel.Override("Microsoft", LogEventLevel.Warning)
.MinimumLevel.Override("System", LogEventLevel.Warning)
.WriteTo.Console())
.UseCommandHandlers();
}
private static LogEventLevel GetMinimumLogEventLevel(HostBuilderContext context)
{
var verboseSpecified =
context.Properties.TryGetValue(typeof(InvocationContext), out var value) &&
value is InvocationContext invocationContext &&
invocationContext.ParseResult.FindResultFor(GlobalOptions.Verbose) is not null;
return verboseSpecified ? LogEventLevel.Debug : LogEventLevel.Fatal;
command.SetAction((ctx, token) => {
command.SetProperties(ctx);
command.TargetRuntime = RID.Parse(ctx.GetValue(TargetRuntime));
return execute(command);
});
parent.Subcommands.Add(command);
return command;
}
}

View File

@@ -29,4 +29,10 @@
<PackageReference Include="System.CommandLine" Version="2.0.0-beta4.23407.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Squirrel.Deployment\Squirrel.Deployment.csproj" />
<ProjectReference Include="..\Squirrel.Packaging.OSX\Squirrel.Packaging.OSX.csproj" />
<ProjectReference Include="..\Squirrel.Packaging.Windows\Squirrel.Packaging.Windows.csproj" />
</ItemGroup>
</Project>

View File

@@ -1,9 +1,30 @@
using Microsoft.Extensions.Logging;
using Octokit;
using Squirrel.Extensions;
using Squirrel.Sources;
namespace Squirrel.CommandLine.Sync;
namespace Squirrel.Deployment;
public class GitHubOptions
{
public DirectoryInfo ReleaseDir { get; set; }
public string RepoUrl { get; set; }
public string Token { get; set; }
}
public class GitHubDownloadOptions : GitHubOptions
{
public bool Pre { get; set; }
}
public class GitHubUploadOptions : GitHubOptions
{
public bool Publish { get; set; }
public string ReleaseName { get; set; }
}
public class GitHubRepository
{
@@ -14,13 +35,14 @@ public class GitHubRepository
_log = logger;
}
public async Task DownloadRecentPackages(DirectoryInfo releaseDirectoryInfo, string repoUrl, string token, bool asPrerelease)
public async Task DownloadRecentPackages(GitHubDownloadOptions options)
{
if (String.IsNullOrWhiteSpace(token))
var releaseDirectoryInfo = options.ReleaseDir;
if (String.IsNullOrWhiteSpace(options.Token))
_log.Warn("No GitHub access token provided. Unauthenticated requests will be limited to 60 per hour.");
_log.Info("Fetching RELEASES...");
var source = new GithubSource(repoUrl, token, asPrerelease);
var source = new GithubSource(options.RepoUrl, options.Token, options.Pre);
var latestReleaseEntries = await source.GetReleaseFeed();
if (latestReleaseEntries == null || latestReleaseEntries.Length == 0) {
@@ -54,12 +76,12 @@ public class GitHubRepository
_log.Info("Done.");
}
public static async Task UploadMissingPackages(GitHubUploadCommand options)
public async Task UploadMissingPackages(GitHubUploadOptions options)
{
if (String.IsNullOrWhiteSpace(options.Token))
throw new InvalidOperationException("Must provide access token to create a GitHub release.");
var releaseDirectoryInfo = options.GetReleaseDirectory();
var releaseDirectoryInfo = options.ReleaseDir;
var repoUri = new Uri(options.RepoUrl);
var repoParts = repoUri.AbsolutePath.Trim('/').Split('/');
@@ -145,7 +167,7 @@ public class GitHubRepository
_log.Info("Release URL: " + release.HtmlUrl);
}
private static async Task UploadFileAsAsset(GitHubClient client, Release release, string filePath)
private async Task UploadFileAsAsset(GitHubClient client, Release release, string filePath)
{
_log.Info($"Uploading asset '{Path.GetFileName(filePath)}'");
using var stream = File.OpenRead(filePath);

View File

@@ -1,312 +1,332 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Net;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using Amazon;
using Amazon.S3;
using Amazon.S3.Model;
using Squirrel.CommandLine.Commands;
using Squirrel.SimpleSplat;
using Microsoft.Extensions.Logging;
namespace Squirrel.CommandLine.Sync
namespace Squirrel.Deployment;
public class S3Options
{
internal static class S3Repository
public DirectoryInfo ReleaseDir { get; set; }
public string KeyId { get; set; }
public string Secret { get; set; }
public string Region { get; set; }
public string Endpoint { get; set; }
public string Bucket { get; set; }
public string PathPrefix { get; set; }
}
public class S3UploadOptions : S3Options
{
public bool Overwrite { get; set; }
public int KeepMaxReleases { get; set; }
}
public class S3Repository
{
private readonly ILogger Log;
public S3Repository(ILogger logger)
{
private readonly static IFullLogger Log = SquirrelLocator.Current.GetService<ILogManager>().GetLogger(typeof(S3Repository));
Log = logger;
}
private static AmazonS3Client GetS3Client(S3BaseCommand options)
{
if (options.Region != null) {
var r = RegionEndpoint.GetBySystemName(options.Region);
return new AmazonS3Client(options.KeyId, options.Secret, r);
} else if (options.Endpoint != null) {
var config = new AmazonS3Config() { ServiceURL = options.Endpoint };
return new AmazonS3Client(options.KeyId, options.Secret, config);
} else {
throw new InvalidOperationException("Missing endpoint");
}
}
private static string GetPrefix(S3BaseCommand options)
{
var prefix = options.PathPrefix?.Replace('\\', '/') ?? "";
if (!String.IsNullOrWhiteSpace(prefix) && !prefix.EndsWith("/")) prefix += "/";
return prefix;
}
public static async Task DownloadRecentPackages(S3DownloadCommand options)
{
var _client = GetS3Client(options);
var _prefix = GetPrefix(options);
var releasesDir = options.GetReleaseDirectory();
var releasesPath = Path.Combine(releasesDir.FullName, "RELEASES");
Log.Info($"Downloading latest release to '{releasesDir.FullName}' from S3 bucket '{options.Bucket}'"
+ (String.IsNullOrWhiteSpace(_prefix) ? "" : " with prefix '" + _prefix + "'"));
try {
Log.Info("Downloading RELEASES");
using (var obj = await _client.GetObjectAsync(options.Bucket, _prefix + "RELEASES"))
await obj.WriteResponseStreamToFileAsync(releasesPath, false, CancellationToken.None);
} catch (AmazonS3Exception ex) when (ex.StatusCode == HttpStatusCode.NotFound) {
Log.Warn("RELEASES file not found. No releases to download.");
return;
}
var releasesToDownload = ReleaseEntry.ParseReleaseFile(File.ReadAllText(releasesPath))
.Where(x => !x.IsDelta)
.OrderByDescending(x => x.Version)
.Take(1)
.Select(x => new {
LocalPath = Path.Combine(releasesDir.FullName, x.Filename),
Filename = x.Filename,
});
foreach (var releaseToDownload in releasesToDownload) {
Log.Info("Downloading " + releaseToDownload.Filename);
using (var pkgobj = await _client.GetObjectAsync(options.Bucket, _prefix + releaseToDownload.Filename))
await pkgobj.WriteResponseStreamToFileAsync(releaseToDownload.LocalPath, false, CancellationToken.None);
}
}
public static async Task UploadMissingPackages(S3UploadCommand options)
{
var _client = GetS3Client(options);
var _prefix = GetPrefix(options);
var releasesDir = options.GetReleaseDirectory();
Log.Info($"Uploading releases from '{releasesDir.FullName}' to S3 bucket '{options.Bucket}'"
+ (String.IsNullOrWhiteSpace(_prefix) ? "" : " with prefix '" + _prefix + "'"));
// locate files to upload
var files = releasesDir.GetFiles("*", SearchOption.TopDirectoryOnly);
var msiFile = files.Where(f => f.FullName.EndsWith(".msi", StringComparison.InvariantCultureIgnoreCase)).SingleOrDefault();
var setupFile = files.Where(f => f.FullName.EndsWith("Setup.exe", StringComparison.InvariantCultureIgnoreCase))
.ContextualSingle("release directory", "Setup.exe file");
var releasesFile = files.Where(f => f.Name.Equals("RELEASES", StringComparison.InvariantCultureIgnoreCase))
.ContextualSingle("release directory", "RELEASES file");
var nupkgFiles = files.Where(f => f.FullName.EndsWith(".nupkg", StringComparison.InvariantCultureIgnoreCase)).ToArray();
// we will merge the remote RELEASES file with the local one
string remoteReleasesContent = null;
try {
Log.Info("Downloading remote RELEASES file");
using (var obj = await _client.GetObjectAsync(options.Bucket, _prefix + "RELEASES"))
using (var sr = new StreamReader(obj.ResponseStream, Encoding.UTF8, true))
remoteReleasesContent = await sr.ReadToEndAsync();
Log.Info("Merging remote and local RELEASES files");
} catch (AmazonS3Exception ex) when (ex.StatusCode == HttpStatusCode.NotFound) {
Log.Warn("No remote RELEASES found.");
}
var localReleases = ReleaseEntry.ParseReleaseFile(File.ReadAllText(releasesFile.FullName));
var remoteReleases = ReleaseEntry.ParseReleaseFile(remoteReleasesContent);
// apply retention policy. count '-full' versions only, then also remove corresponding delta packages
var releaseEntries = localReleases
.Concat(remoteReleases)
.DistinctBy(r => r.Filename) // will preserve the local entries because they appear first
.OrderBy(k => k.Version)
.ThenBy(k => !k.IsDelta)
.ToArray();
if (releaseEntries.Length == 0) {
Log.Warn("No releases found.");
return;
}
if (!releaseEntries.All(f => f.PackageName == releaseEntries.First().PackageName)) {
throw new Exception("There are mix-matched package Id's in local/remote RELEASES file. " +
"Please fix the release files manually so there is only one consistent package Id present.");
}
var fullCount = releaseEntries.Where(r => !r.IsDelta).Count();
if (options.KeepMaxReleases > 0 && fullCount > options.KeepMaxReleases) {
Log.Info($"Retention Policy: {fullCount - options.KeepMaxReleases} releases will be removed from RELEASES file.");
var fullReleases = releaseEntries
.OrderByDescending(k => k.Version)
.Where(k => !k.IsDelta)
.Take(options.KeepMaxReleases)
.ToArray();
var deltaReleases = releaseEntries
.Where(k => k.IsDelta)
.Where(k => fullReleases.Any(f => f.Version == k.Version))
.Where(k => k.Version != fullReleases.Last().Version) // ignore delta packages for the oldest full package
.ToArray();
Log.Info($"Total number of packages in remote after retention: {fullReleases.Length} full, {deltaReleases.Length} delta.");
fullCount = fullReleases.Length;
ReleaseEntry.WriteReleaseFile(fullReleases.Concat(deltaReleases), releasesFile.FullName);
} else {
Log.Info($"There are currently {fullCount} full releases in RELEASES file.");
ReleaseEntry.WriteReleaseFile(releaseEntries, releasesFile.FullName);
}
async Task UploadFile(FileInfo f, bool overwriteRemote)
{
string key = _prefix + f.Name;
string deleteOldVersionId = null;
// try to detect an existing remote file of the same name
try {
var metadata = await _client.GetObjectMetadataAsync(options.Bucket, key);
var md5 = GetFileMD5Checksum(f.FullName);
var stored = metadata?.ETag?.Trim().Trim('"');
if (stored != null) {
if (stored.Equals(md5, StringComparison.InvariantCultureIgnoreCase)) {
Log.Info($"Upload file '{f.Name}' skipped (already exists in remote)");
return;
} else if (overwriteRemote) {
Log.Info($"File '{f.Name}' exists in remote, replacing...");
deleteOldVersionId = metadata.VersionId;
} else {
Log.Warn($"File '{f.Name}' exists in remote and checksum does not match local file. Use 'overwrite' argument to replace remote file.");
return;
}
}
} catch {
// don't care if this check fails. worst case, we end up re-uploading a file that
// already exists. storage providers should prefer the newer file of the same name.
}
var req = new PutObjectRequest {
BucketName = options.Bucket,
FilePath = f.FullName,
Key = key,
};
await RetryAsync(() => _client.PutObjectAsync(req), "Uploading " + f.Name);
if (deleteOldVersionId != null) {
await RetryAsync(() => _client.DeleteObjectAsync(options.Bucket, key, deleteOldVersionId),
"Removing old version of " + f.Name,
throwIfFail: false);
}
}
// we need to upload things in a certain order. If we upload 'RELEASES' first, for example, a client
// might try to request a nupkg that does not yet exist.
// upload nupkg's first
foreach (var f in nupkgFiles) {
if (!releaseEntries.Any(r => r.Filename.Equals(f.Name, StringComparison.InvariantCultureIgnoreCase))) {
Log.Warn($"Upload file '{f.Name}' skipped (not in RELEASES file)");
continue;
}
await UploadFile(f, options.Overwrite);
}
// next upload setup files
await UploadFile(setupFile, true);
if (msiFile != null) await UploadFile(msiFile, true);
// upload RELEASES
await UploadFile(releasesFile, true);
// ignore dead package cleanup if there is no retention policy
if (options.KeepMaxReleases > 0) {
// remove any dead packages (not in RELEASES) as they are undiscoverable anyway
Log.Info("Searching for remote dead packages (not in RELEASES file)");
var objects = await ListBucketContentsAsync(_client, options.Bucket, _prefix).ToArrayAsync();
var deadObjectQuery =
from o in objects
let key = o.Key
where key.EndsWith(".nupkg", StringComparison.InvariantCultureIgnoreCase)
where key.StartsWith(_prefix, StringComparison.InvariantCultureIgnoreCase)
let fileName = key.Substring(_prefix.Length)
where !fileName.Contains('/') // filters out objects in folders if _prefix is empty
where !releaseEntries.Any(r => r.Filename.Equals(fileName, StringComparison.InvariantCultureIgnoreCase))
orderby o.LastModified ascending
select new { key, fileName, versionId = o.VersionId };
var deadObj = deadObjectQuery.ToArray();
Log.Info($"Found {deadObj.Length} dead packages.");
foreach (var s3obj in deadObj) {
var req = new DeleteObjectRequest { BucketName = options.Bucket, Key = s3obj.key, VersionId = s3obj.versionId };
await RetryAsync(() => _client.DeleteObjectAsync(req), "Deleting dead package: " + s3obj, throwIfFail: false);
}
}
Log.Info("Done");
var endpointHost = options.Endpoint ?? RegionEndpoint.GetBySystemName(options.Region).GetEndpointForService("s3").Hostname;
if (Regex.IsMatch(endpointHost, @"^https?:\/\/", RegexOptions.IgnoreCase)) {
endpointHost = new Uri(endpointHost, UriKind.Absolute).Host;
}
var baseurl = $"https://{options.Bucket}.{endpointHost}/{_prefix}";
Log.Info($"Bucket URL: {baseurl}");
Log.Info($"Setup URL: {baseurl}{setupFile.Name}");
}
private static async IAsyncEnumerable<S3ObjectVersion> ListBucketContentsAsync(IAmazonS3 client, string bucketName, string prefix)
{
var request = new ListVersionsRequest {
BucketName = bucketName,
MaxKeys = 100,
Prefix = prefix,
};
ListVersionsResponse response;
do {
response = await client.ListVersionsAsync(request);
foreach (var obj in response.Versions) {
yield return obj;
}
// If the response is truncated, set the request ContinuationToken
// from the NextContinuationToken property of the response.
request.KeyMarker = response.NextKeyMarker;
request.VersionIdMarker = response.NextVersionIdMarker;
} while (response.IsTruncated);
}
private static async Task RetryAsync(Func<Task> block, string message, bool throwIfFail = true, bool showMessageFirst = true)
{
int ctry = 0;
while (true) {
try {
if (showMessageFirst || ctry > 0)
Log.Info((ctry > 0 ? $"(retry {ctry}) " : "") + message);
await block().ConfigureAwait(false);
return;
} catch (Exception ex) {
if (ctry++ > 2) {
if (throwIfFail) {
throw;
} else {
Log.Error("Error: " + ex.Message + ", will not try again.");
return;
}
}
Log.Error($"Error: {ex.Message}, retrying in 1 second.");
await Task.Delay(1000).ConfigureAwait(false);
}
}
}
private static string GetFileMD5Checksum(string filePath)
{
var sha = System.Security.Cryptography.MD5.Create();
byte[] checksum;
using (var fs = File.OpenRead(filePath))
checksum = sha.ComputeHash(fs);
return BitConverter.ToString(checksum).Replace("-", String.Empty);
private static AmazonS3Client GetS3Client(S3Options options)
{
if (options.Region != null) {
var r = RegionEndpoint.GetBySystemName(options.Region);
return new AmazonS3Client(options.KeyId, options.Secret, r);
} else if (options.Endpoint != null) {
var config = new AmazonS3Config() { ServiceURL = options.Endpoint };
return new AmazonS3Client(options.KeyId, options.Secret, config);
} else {
throw new InvalidOperationException("Missing endpoint");
}
}
private static string GetPrefix(S3Options options)
{
var prefix = options.PathPrefix?.Replace('\\', '/') ?? "";
if (!String.IsNullOrWhiteSpace(prefix) && !prefix.EndsWith("/")) prefix += "/";
return prefix;
}
public async Task DownloadRecentPackages(S3Options options)
{
var _client = GetS3Client(options);
var _prefix = GetPrefix(options);
var releasesDir = options.ReleaseDir;
var releasesPath = Path.Combine(releasesDir.FullName, "RELEASES");
Log.Info($"Downloading latest release to '{releasesDir.FullName}' from S3 bucket '{options.Bucket}'"
+ (String.IsNullOrWhiteSpace(_prefix) ? "" : " with prefix '" + _prefix + "'"));
try {
Log.Info("Downloading RELEASES");
using (var obj = await _client.GetObjectAsync(options.Bucket, _prefix + "RELEASES"))
await obj.WriteResponseStreamToFileAsync(releasesPath, false, CancellationToken.None);
} catch (AmazonS3Exception ex) when (ex.StatusCode == HttpStatusCode.NotFound) {
Log.Warn("RELEASES file not found. No releases to download.");
return;
}
var releasesToDownload = ReleaseEntry.ParseReleaseFile(File.ReadAllText(releasesPath))
.Where(x => !x.IsDelta)
.OrderByDescending(x => x.Version)
.Take(1)
.Select(x => new {
LocalPath = Path.Combine(releasesDir.FullName, x.Filename),
Filename = x.Filename,
});
foreach (var releaseToDownload in releasesToDownload) {
Log.Info("Downloading " + releaseToDownload.Filename);
using (var pkgobj = await _client.GetObjectAsync(options.Bucket, _prefix + releaseToDownload.Filename))
await pkgobj.WriteResponseStreamToFileAsync(releaseToDownload.LocalPath, false, CancellationToken.None);
}
}
public async Task UploadMissingPackages(S3UploadOptions options)
{
var _client = GetS3Client(options);
var _prefix = GetPrefix(options);
var releasesDir = options.ReleaseDir;
Log.Info($"Uploading releases from '{releasesDir.FullName}' to S3 bucket '{options.Bucket}'"
+ (String.IsNullOrWhiteSpace(_prefix) ? "" : " with prefix '" + _prefix + "'"));
// locate files to upload
var files = releasesDir.GetFiles("*", SearchOption.TopDirectoryOnly);
var msiFile = files.Where(f => f.FullName.EndsWith(".msi", StringComparison.InvariantCultureIgnoreCase)).SingleOrDefault();
var setupFile = files.Where(f => f.FullName.EndsWith("Setup.exe", StringComparison.InvariantCultureIgnoreCase))
.ContextualSingle("release directory", "Setup.exe file");
var releasesFile = files.Where(f => f.Name.Equals("RELEASES", StringComparison.InvariantCultureIgnoreCase))
.ContextualSingle("release directory", "RELEASES file");
var nupkgFiles = files.Where(f => f.FullName.EndsWith(".nupkg", StringComparison.InvariantCultureIgnoreCase)).ToArray();
// we will merge the remote RELEASES file with the local one
string remoteReleasesContent = null;
try {
Log.Info("Downloading remote RELEASES file");
using (var obj = await _client.GetObjectAsync(options.Bucket, _prefix + "RELEASES"))
using (var sr = new StreamReader(obj.ResponseStream, Encoding.UTF8, true))
remoteReleasesContent = await sr.ReadToEndAsync();
Log.Info("Merging remote and local RELEASES files");
} catch (AmazonS3Exception ex) when (ex.StatusCode == HttpStatusCode.NotFound) {
Log.Warn("No remote RELEASES found.");
}
var localReleases = ReleaseEntry.ParseReleaseFile(File.ReadAllText(releasesFile.FullName));
var remoteReleases = ReleaseEntry.ParseReleaseFile(remoteReleasesContent);
// apply retention policy. count '-full' versions only, then also remove corresponding delta packages
var releaseEntries = localReleases
.Concat(remoteReleases)
.DistinctBy(r => r.Filename) // will preserve the local entries because they appear first
.OrderBy(k => k.Version)
.ThenBy(k => !k.IsDelta)
.ToArray();
if (releaseEntries.Length == 0) {
Log.Warn("No releases found.");
return;
}
if (!releaseEntries.All(f => f.PackageName == releaseEntries.First().PackageName)) {
throw new Exception("There are mix-matched package Id's in local/remote RELEASES file. " +
"Please fix the release files manually so there is only one consistent package Id present.");
}
var fullCount = releaseEntries.Where(r => !r.IsDelta).Count();
if (options.KeepMaxReleases > 0 && fullCount > options.KeepMaxReleases) {
Log.Info($"Retention Policy: {fullCount - options.KeepMaxReleases} releases will be removed from RELEASES file.");
var fullReleases = releaseEntries
.OrderByDescending(k => k.Version)
.Where(k => !k.IsDelta)
.Take(options.KeepMaxReleases)
.ToArray();
var deltaReleases = releaseEntries
.Where(k => k.IsDelta)
.Where(k => fullReleases.Any(f => f.Version == k.Version))
.Where(k => k.Version != fullReleases.Last().Version) // ignore delta packages for the oldest full package
.ToArray();
Log.Info($"Total number of packages in remote after retention: {fullReleases.Length} full, {deltaReleases.Length} delta.");
fullCount = fullReleases.Length;
ReleaseEntry.WriteReleaseFile(fullReleases.Concat(deltaReleases), releasesFile.FullName);
} else {
Log.Info($"There are currently {fullCount} full releases in RELEASES file.");
ReleaseEntry.WriteReleaseFile(releaseEntries, releasesFile.FullName);
}
async Task UploadFile(FileInfo f, bool overwriteRemote)
{
string key = _prefix + f.Name;
string deleteOldVersionId = null;
// try to detect an existing remote file of the same name
try {
var metadata = await _client.GetObjectMetadataAsync(options.Bucket, key);
var md5 = GetFileMD5Checksum(f.FullName);
var stored = metadata?.ETag?.Trim().Trim('"');
if (stored != null) {
if (stored.Equals(md5, StringComparison.InvariantCultureIgnoreCase)) {
Log.Info($"Upload file '{f.Name}' skipped (already exists in remote)");
return;
} else if (overwriteRemote) {
Log.Info($"File '{f.Name}' exists in remote, replacing...");
deleteOldVersionId = metadata.VersionId;
} else {
Log.Warn($"File '{f.Name}' exists in remote and checksum does not match local file. Use 'overwrite' argument to replace remote file.");
return;
}
}
} catch {
// don't care if this check fails. worst case, we end up re-uploading a file that
// already exists. storage providers should prefer the newer file of the same name.
}
var req = new PutObjectRequest {
BucketName = options.Bucket,
FilePath = f.FullName,
Key = key,
};
await RetryAsync(() => _client.PutObjectAsync(req), "Uploading " + f.Name);
if (deleteOldVersionId != null) {
await RetryAsync(() => _client.DeleteObjectAsync(options.Bucket, key, deleteOldVersionId),
"Removing old version of " + f.Name,
throwIfFail: false);
}
}
// we need to upload things in a certain order. If we upload 'RELEASES' first, for example, a client
// might try to request a nupkg that does not yet exist.
// upload nupkg's first
foreach (var f in nupkgFiles) {
if (!releaseEntries.Any(r => r.Filename.Equals(f.Name, StringComparison.InvariantCultureIgnoreCase))) {
Log.Warn($"Upload file '{f.Name}' skipped (not in RELEASES file)");
continue;
}
await UploadFile(f, options.Overwrite);
}
// next upload setup files
await UploadFile(setupFile, true);
if (msiFile != null) await UploadFile(msiFile, true);
// upload RELEASES
await UploadFile(releasesFile, true);
// ignore dead package cleanup if there is no retention policy
if (options.KeepMaxReleases > 0) {
// remove any dead packages (not in RELEASES) as they are undiscoverable anyway
Log.Info("Searching for remote dead packages (not in RELEASES file)");
var objects = await ListBucketContentsAsync(_client, options.Bucket, _prefix).ToArrayAsync();
var deadObjectQuery =
from o in objects
let key = o.Key
where key.EndsWith(".nupkg", StringComparison.InvariantCultureIgnoreCase)
where key.StartsWith(_prefix, StringComparison.InvariantCultureIgnoreCase)
let fileName = key.Substring(_prefix.Length)
where !fileName.Contains('/') // filters out objects in folders if _prefix is empty
where !releaseEntries.Any(r => r.Filename.Equals(fileName, StringComparison.InvariantCultureIgnoreCase))
orderby o.LastModified ascending
select new { key, fileName, versionId = o.VersionId };
var deadObj = deadObjectQuery.ToArray();
Log.Info($"Found {deadObj.Length} dead packages.");
foreach (var s3obj in deadObj) {
var req = new DeleteObjectRequest { BucketName = options.Bucket, Key = s3obj.key, VersionId = s3obj.versionId };
await RetryAsync(() => _client.DeleteObjectAsync(req), "Deleting dead package: " + s3obj, throwIfFail: false);
}
}
Log.Info("Done");
var endpointHost = options.Endpoint ?? RegionEndpoint.GetBySystemName(options.Region).GetEndpointForService("s3").Hostname;
if (Regex.IsMatch(endpointHost, @"^https?:\/\/", RegexOptions.IgnoreCase)) {
endpointHost = new Uri(endpointHost, UriKind.Absolute).Host;
}
var baseurl = $"https://{options.Bucket}.{endpointHost}/{_prefix}";
Log.Info($"Bucket URL: {baseurl}");
Log.Info($"Setup URL: {baseurl}{setupFile.Name}");
}
private static async IAsyncEnumerable<S3ObjectVersion> ListBucketContentsAsync(IAmazonS3 client, string bucketName, string prefix)
{
var request = new ListVersionsRequest {
BucketName = bucketName,
MaxKeys = 100,
Prefix = prefix,
};
ListVersionsResponse response;
do {
response = await client.ListVersionsAsync(request);
foreach (var obj in response.Versions) {
yield return obj;
}
// If the response is truncated, set the request ContinuationToken
// from the NextContinuationToken property of the response.
request.KeyMarker = response.NextKeyMarker;
request.VersionIdMarker = response.NextVersionIdMarker;
} while (response.IsTruncated);
}
private async Task RetryAsync(Func<Task> block, string message, bool throwIfFail = true, bool showMessageFirst = true)
{
int ctry = 0;
while (true) {
try {
if (showMessageFirst || ctry > 0)
Log.Info((ctry > 0 ? $"(retry {ctry}) " : "") + message);
await block().ConfigureAwait(false);
return;
} catch (Exception ex) {
if (ctry++ > 2) {
if (throwIfFail) {
throw;
} else {
Log.Error("Error: " + ex.Message + ", will not try again.");
return;
}
}
Log.Error($"Error: {ex.Message}, retrying in 1 second.");
await Task.Delay(1000).ConfigureAwait(false);
}
}
}
private static string GetFileMD5Checksum(string filePath)
{
var sha = System.Security.Cryptography.MD5.Create();
byte[] checksum;
using (var fs = File.OpenRead(filePath))
checksum = sha.ComputeHash(fs);
return BitConverter.ToString(checksum).Replace("-", String.Empty);
}
}

View File

@@ -1,85 +1,85 @@
using System;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Reflection;
using System.Threading.Tasks;
using Squirrel.CommandLine.Commands;
using System.Reflection;
using Microsoft.Extensions.Logging;
namespace Squirrel.CommandLine.Sync
namespace Squirrel.Deployment;
public class HttpDownloadOptions
{
internal class SimpleWebRepository
public DirectoryInfo ReleaseDir { get; set; }
public string Url { get; set; }
}
public class SimpleWebRepository
{
private readonly ILogger _logger;
public SimpleWebRepository(ILogger logger)
{
private readonly HttpDownloadCommand options;
_logger = logger;
}
public SimpleWebRepository(HttpDownloadCommand options)
{
this.options = options;
}
public async Task DownloadRecentPackages(HttpDownloadOptions options)
{
var uri = new Uri(options.Url);
var releasesDir = options.ReleaseDir;
var releasesUri = Utility.AppendPathToUri(uri, "RELEASES");
var releasesIndex = await retryAsync(3, () => downloadReleasesIndex(releasesUri));
public static async Task DownloadRecentPackages(HttpDownloadCommand options)
{
var uri = new Uri(options.Url);
var releasesDir = options.GetReleaseDirectory();
var releasesUri = Utility.AppendPathToUri(uri, "RELEASES");
var releasesIndex = await retryAsync(3, () => downloadReleasesIndex(releasesUri));
File.WriteAllText(Path.Combine(releasesDir.FullName, "RELEASES"), releasesIndex);
File.WriteAllText(Path.Combine(releasesDir.FullName, "RELEASES"), releasesIndex);
var releasesToDownload = ReleaseEntry.ParseReleaseFile(releasesIndex)
.Where(x => !x.IsDelta)
.OrderByDescending(x => x.Version)
.Take(1)
.Select(x => new {
LocalPath = Path.Combine(releasesDir.FullName, x.Filename),
RemoteUrl = new Uri(Utility.EnsureTrailingSlash(uri), x.BaseUrl + x.Filename + x.Query)
});
var releasesToDownload = ReleaseEntry.ParseReleaseFile(releasesIndex)
.Where(x => !x.IsDelta)
.OrderByDescending(x => x.Version)
.Take(1)
.Select(x => new {
LocalPath = Path.Combine(releasesDir.FullName, x.Filename),
RemoteUrl = new Uri(Utility.EnsureTrailingSlash(uri), x.BaseUrl + x.Filename + x.Query)
});
foreach (var releaseToDownload in releasesToDownload) {
await retryAsync(3, () => downloadRelease(releaseToDownload.LocalPath, releaseToDownload.RemoteUrl));
}
}
static async Task<string> downloadReleasesIndex(Uri uri)
{
Console.WriteLine("Trying to download RELEASES index from {0}", uri);
var userAgent = new System.Net.Http.Headers.ProductInfoHeaderValue("Squirrel", Assembly.GetExecutingAssembly().GetName().Version.ToString());
using (HttpClient client = new HttpClient()) {
client.DefaultRequestHeaders.UserAgent.Add(userAgent);
return await client.GetStringAsync(uri);
}
}
static async Task downloadRelease(string localPath, Uri remoteUrl)
{
if (File.Exists(localPath)) {
File.Delete(localPath);
}
Console.WriteLine("Downloading release from {0}", remoteUrl);
var wc = Utility.CreateDefaultDownloader();
await wc.DownloadFile(remoteUrl.ToString(), localPath, null);
}
static async Task<T> retryAsync<T>(int count, Func<Task<T>> block)
{
int retryCount = count;
retry:
try {
return await block();
} catch (Exception) {
retryCount--;
if (retryCount >= 0) goto retry;
throw;
}
}
static async Task retryAsync(int count, Func<Task> block)
{
await retryAsync(count, async () => { await block(); return false; });
foreach (var releaseToDownload in releasesToDownload) {
await retryAsync(3, () => downloadRelease(releaseToDownload.LocalPath, releaseToDownload.RemoteUrl));
}
}
async Task<string> downloadReleasesIndex(Uri uri)
{
_logger.Info($"Trying to download RELEASES index from {uri}");
var userAgent = new System.Net.Http.Headers.ProductInfoHeaderValue("Squirrel", Assembly.GetExecutingAssembly().GetName().Version.ToString());
using (HttpClient client = new HttpClient()) {
client.DefaultRequestHeaders.UserAgent.Add(userAgent);
return await client.GetStringAsync(uri);
}
}
async Task downloadRelease(string localPath, Uri remoteUrl)
{
if (File.Exists(localPath)) {
File.Delete(localPath);
}
_logger.Info($"Downloading release from {remoteUrl}");
var wc = Utility.CreateDefaultDownloader();
await wc.DownloadFile(remoteUrl.ToString(), localPath, null);
}
static async Task<T> retryAsync<T>(int count, Func<Task<T>> block)
{
int retryCount = count;
retry:
try {
return await block();
} catch (Exception) {
retryCount--;
if (retryCount >= 0) goto retry;
throw;
}
}
static async Task retryAsync(int count, Func<Task> block)
{
await retryAsync(count, async () => { await block(); return false; });
}
}

View File

@@ -3,24 +3,18 @@
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<NoWarn>$(NoWarn);CA2007;CS8002</NoWarn>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="AWSSDK.S3" Version="3.7.205.22" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="Octokit" Version="9.0.0" />
<PackageReference Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
<PackageReference Include="System.IO" Version="4.3.0" />
<PackageReference Include="System.Net.Http" Version="4.3.4" />
<PackageReference Include="System.Security.Cryptography.Algorithms" Version="4.3.1" />
<PackageReference Include="System.Linq.Async" Version="6.0.1" />
<PackageReference Include="NuGet.Commands" Version="6.7.0" />
<PackageReference Include="System.Drawing.Common" Version="7.0.0" />
<PackageReference Include="PeNet" Version="4.0.2" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Squirrel\Squirrel.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="AWSSDK.S3" Version="3.7.205.22" />
<PackageReference Include="Octokit" Version="9.0.0" />
<PackageReference Include="System.Linq.Async" Version="6.0.1" />
<PackageReference Include="NuGet.Commands" Version="6.7.0" />
</ItemGroup>
</Project>

View File

@@ -1,34 +1,33 @@
namespace Squirrel.CommandLine.OSX
namespace Squirrel.Packaging.OSX;
internal class AppInfo
{
internal class AppInfo
{
public string SQPackId { get; set; }
public string SQPackId { get; set; }
public string SQPackAuthors { get; set; }
public string SQPackAuthors { get; set; }
public string CFBundleName { get; set; }
public string CFBundleName { get; set; }
public string CFBundleDisplayName { get; set; }
public string CFBundleDisplayName { get; set; }
public string CFBundleIdentifier { get; set; }
public string CFBundleIdentifier { get; set; }
public string CFBundleVersion { get; set; }
public string CFBundleVersion { get; set; }
public string CFBundlePackageType { get; set; }
public string CFBundlePackageType { get; set; }
public string CFBundleSignature { get; set; }
public string CFBundleSignature { get; set; }
public string CFBundleExecutable { get; set; }
public string CFBundleExecutable { get; set; }
public string CFBundleIconFile { get; set; }
public string CFBundleIconFile { get; set; }
public string CFBundleShortVersionString { get; set; }
public string CFBundleShortVersionString { get; set; }
public string NSPrincipalClass { get; set; }
public string NSPrincipalClass { get; set; }
public bool NSHighResolutionCapable { get; set; }
public bool NSHighResolutionCapable { get; set; }
public bool? NSRequiresAquaSystemAppearance { get; private set; }
public bool? NSRequiresAquaSystemAppearance { get; private set; }
}
}

View File

@@ -1,216 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Text.RegularExpressions;
using NuGet.Versioning;
using Squirrel.CommandLine.Commands;
using Squirrel.PropertyList;
using Squirrel.SimpleSplat;
namespace Squirrel.CommandLine.OSX
{
class Commands
{
static IFullLogger Log => SquirrelLocator.Current.GetService<ILogManager>().GetLogger(typeof(Commands));
public static void Bundle(BundleOsxCommand options)
{
var icon = options.Icon;
var packId = options.PackId;
var packDirectory = options.PackDirectory;
var packVersion = options.PackVersion;
var exeName = options.EntryExecutableName;
var packAuthors = options.PackAuthors;
var packTitle = options.PackTitle;
var releaseDir = options.GetReleaseDirectory();
Log.Info("Generating new '.app' bundle from a directory of application files.");
var mainExePath = Path.Combine(packDirectory, exeName);
if (!File.Exists(mainExePath))// || !PlatformUtil.IsMachOImage(mainExePath))
throw new ArgumentException($"--exeName '{mainExePath}' does not exist or is not a mach-o executable.");
var appleId = $"com.{packAuthors ?? packId}.{packId}";
var escapedAppleId = Regex.Replace(appleId, @"[^\w\.]", "_");
var appleSafeVersion = NuGetVersion.Parse(packVersion).Version.ToString();
var info = new AppInfo {
SQPackId = packId,
SQPackAuthors = packAuthors,
CFBundleName = packTitle ?? packId,
//CFBundleDisplayName = packTitle ?? packId,
CFBundleExecutable = exeName,
CFBundleIdentifier = options.BundleId ?? escapedAppleId,
CFBundlePackageType = "APPL",
CFBundleShortVersionString = appleSafeVersion,
CFBundleVersion = packVersion,
CFBundleSignature = "????",
NSPrincipalClass = "NSApplication",
NSHighResolutionCapable = true,
CFBundleIconFile = Path.GetFileName(icon),
};
Log.Info("Creating '.app' directory structure");
var builder = new StructureBuilder(packId, releaseDir.FullName);
if (Directory.Exists(builder.AppDirectory)) {
Log.Warn(builder.AppDirectory + " already exists, deleting...");
Utility.DeleteFileOrDirectoryHard(builder.AppDirectory);
}
builder.Build();
Log.Info("Writing Info.plist");
var plist = new PlistWriter(info, builder.ContentsDirectory);
plist.Write();
Log.Info("Copying resources into new '.app' bundle");
File.Copy(icon, Path.Combine(builder.ResourcesDirectory, Path.GetFileName(icon)));
Log.Info("Copying application files into new '.app' bundle");
Utility.CopyFiles(new DirectoryInfo(packDirectory), new DirectoryInfo(builder.MacosDirectory));
Log.Info("Bundle created successfully: " + builder.AppDirectory);
}
public static void Releasify(ReleasifyOsxCommand options)
{
var releaseDir = options.GetReleaseDirectory();
var appBundlePath = options.BundleDirectory;
Log.Info("Creating Squirrel application from app bundle at: " + appBundlePath);
Log.Info("Parsing app Info.plist");
var contentsDir = Path.Combine(appBundlePath, "Contents");
if (!Directory.Exists(contentsDir))
throw new Exception("Invalid bundle structure (missing Contents dir)");
var plistPath = Path.Combine(contentsDir, "Info.plist");
if (!File.Exists(plistPath))
throw new Exception("Invalid bundle structure (missing Info.plist)");
NSDictionary rootDict = (NSDictionary) PropertyListParser.Parse(plistPath);
var packId = rootDict.ObjectForKey(nameof(AppInfo.SQPackId))?.ToString();
if (String.IsNullOrWhiteSpace(packId))
packId = rootDict.ObjectForKey(nameof(AppInfo.CFBundleIdentifier))?.ToString();
var packAuthors = rootDict.ObjectForKey(nameof(AppInfo.SQPackAuthors))?.ToString();
if (String.IsNullOrWhiteSpace(packAuthors))
packAuthors = packId;
var packTitle = rootDict.ObjectForKey(nameof(AppInfo.CFBundleName))?.ToString();
var packVersion = rootDict.ObjectForKey(nameof(AppInfo.CFBundleVersion))?.ToString();
if (String.IsNullOrWhiteSpace(packId))
throw new InvalidOperationException($"Invalid CFBundleIdentifier in Info.plist: '{packId}'");
if (String.IsNullOrWhiteSpace(packTitle))
throw new InvalidOperationException($"Invalid CFBundleName in Info.plist: '{packTitle}'");
if (String.IsNullOrWhiteSpace(packVersion) || !NuGetVersion.TryParse(packVersion, out var _))
throw new InvalidOperationException($"Invalid CFBundleVersion in Info.plist: '{packVersion}'");
Log.Info($"Package valid: '{packId}', Name: '{packTitle}', Version: {packVersion}");
Log.Info("Adding Squirrel resources to bundle.");
var nuspecText = NugetConsole.CreateNuspec(
packId, packTitle, packAuthors, packVersion, options.ReleaseNotes, options.IncludePdb);
var nuspecPath = Path.Combine(contentsDir, Utility.SpecVersionFileName);
// nuspec and UpdateMac need to be in contents dir or this package can't update
File.WriteAllText(nuspecPath, nuspecText);
File.Copy(HelperExe.UpdateMacPath, Path.Combine(contentsDir, "UpdateMac"), true);
var zipPath = Path.Combine(releaseDir.FullName, $"{packId}-{options.TargetRuntime.StringWithNoVersion}.zip");
if (File.Exists(zipPath)) File.Delete(zipPath);
// code signing all mach-o binaries
if (SquirrelRuntimeInfo.IsOSX && !String.IsNullOrEmpty(options.SigningAppIdentity) && !String.IsNullOrEmpty(options.NotaryProfile)) {
HelperExe.CodeSign(options.SigningAppIdentity, options.SigningEntitlements, appBundlePath);
HelperExe.CreateDittoZip(appBundlePath, zipPath);
HelperExe.Notarize(zipPath, options.NotaryProfile);
HelperExe.Staple(appBundlePath);
HelperExe.SpctlAssessCode(appBundlePath);
File.Delete(zipPath);
} else if (SquirrelRuntimeInfo.IsOSX && !String.IsNullOrEmpty(options.SigningAppIdentity)) {
HelperExe.CodeSign(options.SigningAppIdentity, options.SigningEntitlements, appBundlePath);
Log.Warn("Package was signed but will not be notarized or verified. Must supply the --notaryProfile option.");
} else if (SquirrelRuntimeInfo.IsOSX) {
Log.Warn("Package will not be signed or notarized. Requires the --signAppIdentity and --notaryProfile options.");
} else {
Log.Warn("Package will not be signed or notarized. Only supported on OSX.");
}
// create a portable zip package from signed/notarized bundle
Log.Info("Creating final application artifact (zip)");
if (SquirrelRuntimeInfo.IsOSX) {
HelperExe.CreateDittoZip(appBundlePath, zipPath);
} else {
Log.Warn("Could not create executable zip with ditto. Only supported on OSX.");
EasyZip.CreateZipFromDirectory(zipPath, appBundlePath, nestDirectory: true);
}
// create release / delta from notarized .app
Log.Info("Creating Squirrel Release");
using var _ = Utility.GetTempDirectory(out var tmp);
var nupkgPath = NugetConsole.CreatePackageFromNuspecPath(tmp, appBundlePath, nuspecPath);
var releaseFilePath = Path.Combine(releaseDir.FullName, "RELEASES");
var releases = new Dictionary<string, ReleaseEntry>();
ReleaseEntry.BuildReleasesFile(releaseDir.FullName);
foreach (var rel in ReleaseEntry.ParseReleaseFile(File.ReadAllText(releaseFilePath, Encoding.UTF8))) {
releases[rel.Filename] = rel;
}
var rp = new ReleasePackageBuilder(nupkgPath);
var suggestedName = ReleasePackageBuilder.GetSuggestedFileName(packId, packVersion, options.TargetRuntime.StringWithNoVersion);
var newPkgPath = rp.CreateReleasePackage((i, pkg) => Path.Combine(releaseDir.FullName, suggestedName));
Log.Info("Creating Delta Packages");
var prev = ReleasePackageBuilder.GetPreviousRelease(releases.Values, rp, releaseDir.FullName, options.TargetRuntime);
if (prev != null && !options.NoDelta) {
var deltaBuilder = new DeltaPackageBuilder();
var deltaFile = rp.ReleasePackageFile.Replace("-full", "-delta");
var dp = deltaBuilder.CreateDeltaPackage(prev, rp, deltaFile);
var deltaEntry = ReleaseEntry.GenerateFromFile(deltaFile);
releases[deltaEntry.Filename] = deltaEntry;
}
var fullEntry = ReleaseEntry.GenerateFromFile(newPkgPath);
releases[fullEntry.Filename] = fullEntry;
ReleaseEntry.WriteReleaseFile(releases.Values, releaseFilePath);
// create installer package, sign and notarize
if (!options.NoPackage) {
if (SquirrelRuntimeInfo.IsOSX) {
var pkgPath = Path.Combine(releaseDir.FullName, $"{packId}-{options.TargetRuntime.StringWithNoVersion}.pkg");
Dictionary<string, string> pkgContent = new() {
{"welcome", options.PackageWelcome },
{"license", options.PackageLicense },
{"readme", options.PackageReadme },
{"conclusion", options.PackageConclusion },
};
HelperExe.CreateInstallerPkg(appBundlePath, packTitle, pkgContent, pkgPath, options.SigningInstallIdentity);
if (!String.IsNullOrEmpty(options.SigningInstallIdentity) && !String.IsNullOrEmpty(options.NotaryProfile)) {
HelperExe.Notarize(pkgPath, options.NotaryProfile);
HelperExe.Staple(pkgPath);
HelperExe.SpctlAssessInstaller(pkgPath);
} else {
Log.Warn("Package installer (.pkg) will not be Notarized. " +
"This is supported with the --signInstallIdentity and --notaryProfile arguments.");
}
} else {
Log.Warn("Package installer (.pkg) will not be created - this is only supported on OSX.");
}
}
Log.Info("Done.");
}
}
}

View File

@@ -1,237 +1,235 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.Versioning;
using System.Runtime.Versioning;
using System.Security;
using System.Threading;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
namespace Squirrel.CommandLine.OSX
namespace Squirrel.Packaging.OSX;
public class HelperExe : HelperFile
{
internal class HelperExe : HelperFile
public HelperExe(ILogger logger) : base(logger)
{
public static string UpdateMacPath
=> FindHelperFile("UpdateMac", p => Microsoft.NET.HostModel.AppHost.HostWriter.IsBundle(p, out var _));
}
public static string SquirrelEntitlements => FindHelperFile("Squirrel.entitlements");
public string UpdateMacPath => FindHelperFile("UpdateMac");
[SupportedOSPlatform("osx")]
public static void CodeSign(string identity, string entitlements, string filePath)
{
if (String.IsNullOrEmpty(entitlements)) {
Log.Info("No codesign entitlements provided, using default dotnet entitlements: " +
"https://docs.microsoft.com/en-us/dotnet/core/install/macos-notarization-issues");
entitlements = SquirrelEntitlements;
public string SquirrelEntitlements => FindHelperFile("Squirrel.entitlements");
[SupportedOSPlatform("osx")]
public void CodeSign(string identity, string entitlements, string filePath)
{
if (String.IsNullOrEmpty(entitlements)) {
Log.Info("No codesign entitlements provided, using default dotnet entitlements: " +
"https://docs.microsoft.com/en-us/dotnet/core/install/macos-notarization-issues");
entitlements = SquirrelEntitlements;
}
if (!File.Exists(entitlements)) {
throw new Exception("Could not find entitlements file at: " + entitlements);
}
var args = new List<string> {
"-s", identity,
"-f",
"-v",
"--deep",
"--timestamp",
"--options", "runtime",
"--entitlements", entitlements,
filePath
};
Log.Info($"Beginning codesign for package...");
Console.WriteLine(InvokeAndThrowIfNonZero("codesign", args, null));
Log.Info("codesign completed successfully");
}
[SupportedOSPlatform("osx")]
public void SpctlAssessCode(string filePath)
{
var args2 = new List<string> {
"--assess",
"-vvvv",
filePath
};
Log.Info($"Verifying signature/notarization for code using spctl...");
Console.WriteLine(InvokeAndThrowIfNonZero("spctl", args2, null));
}
[SupportedOSPlatform("osx")]
public void SpctlAssessInstaller(string filePath)
{
var args2 = new List<string> {
"--assess",
"-vvv",
"-t", "install",
filePath
};
Log.Info($"Verifying signature/notarization for installer package using spctl...");
Console.WriteLine(InvokeAndThrowIfNonZero("spctl", args2, null));
}
[SupportedOSPlatform("osx")]
public void CreateInstallerPkg(string appBundlePath, string appTitle, IEnumerable<KeyValuePair<string, string>> extraContent,
string pkgOutputPath, string signIdentity)
{
// https://matthew-brett.github.io/docosx/flat_packages.html
Log.Info($"Creating installer '.pkg' for app at '{appBundlePath}'");
if (File.Exists(pkgOutputPath)) File.Delete(pkgOutputPath);
using var _1 = Utility.GetTempDirectory(out var tmp);
using var _2 = Utility.GetTempDirectory(out var tmpPayload1);
using var _3 = Utility.GetTempDirectory(out var tmpPayload2);
using var _4 = Utility.GetTempDirectory(out var tmpScripts);
using var _5 = Utility.GetTempDirectory(out var tmpResources);
// copy .app to tmp folder
var bundleName = Path.GetFileName(appBundlePath);
var tmpBundlePath = Path.Combine(tmpPayload1, bundleName);
Utility.CopyFiles(new DirectoryInfo(appBundlePath), new DirectoryInfo(tmpBundlePath));
// create postinstall scripts to open app after install
// https://stackoverflow.com/questions/35619036/open-app-after-installation-from-pkg-file-in-mac
var postinstall = Path.Combine(tmpScripts, "postinstall");
File.WriteAllText(postinstall, $"#!/bin/sh\nsudo -u \"$USER\" open \"$2/{bundleName}/\"\nexit 0");
PlatformUtil.ChmodFileAsExecutable(postinstall);
// generate non-relocatable component pkg. this will be included into a product archive
var pkgPlistPath = Path.Combine(tmp, "tmp.plist");
InvokeAndThrowIfNonZero("pkgbuild", new[] { "--analyze", "--root", tmpPayload1, pkgPlistPath }, null);
InvokeAndThrowIfNonZero("plutil", new[] { "-replace", "BundleIsRelocatable", "-bool", "NO", pkgPlistPath }, null);
var pkg1Path = Path.Combine(tmpPayload2, "1.pkg");
string[] args1 = {
"--root", tmpPayload1,
"--component-plist", pkgPlistPath,
"--scripts", tmpScripts,
"--install-location", "/Applications",
pkg1Path,
};
InvokeAndThrowIfNonZero("pkgbuild", args1, null);
// create final product package that contains app component
var distributionPath = Path.Combine(tmp, "distribution.xml");
InvokeAndThrowIfNonZero("productbuild", new[] { "--synthesize", "--package", pkg1Path, distributionPath }, null);
// https://developer.apple.com/library/archive/documentation/DeveloperTools/Reference/DistributionDefinitionRef/Chapters/Distribution_XML_Ref.html
var distXml = File.ReadAllLines(distributionPath).ToList();
distXml.Insert(2, $"<title>{SecurityElement.Escape(appTitle)}</title>");
// disable local system installation (install to home dir)
distXml.Insert(2, "<domains enable_anywhere=\"false\" enable_currentUserHome=\"true\" enable_localSystem=\"false\" />");
// add extra landing content (eg. license, readme)
foreach (var kvp in extraContent) {
if (!String.IsNullOrEmpty(kvp.Value) && File.Exists(kvp.Value)) {
var fileName = Path.GetFileName(kvp.Value);
File.Copy(kvp.Value, Path.Combine(tmpResources, fileName));
distXml.Insert(2, $"<{kvp.Key} file=\"{fileName}\" />");
}
if (!File.Exists(entitlements)) {
throw new Exception("Could not find entitlements file at: " + entitlements);
}
var args = new List<string> {
"-s", identity,
"-f",
"-v",
"--deep",
"--timestamp",
"--options", "runtime",
"--entitlements", entitlements,
filePath
};
Log.Info($"Beginning codesign for package...");
Console.WriteLine(InvokeAndThrowIfNonZero("codesign", args, null));
Log.Info("codesign completed successfully");
}
[SupportedOSPlatform("osx")]
public static void SpctlAssessCode(string filePath)
{
var args2 = new List<string> {
"--assess",
"-vvvv",
filePath
};
File.WriteAllLines(distributionPath, distXml);
Log.Info($"Verifying signature/notarization for code using spctl...");
Console.WriteLine(InvokeAndThrowIfNonZero("spctl", args2, null));
List<string> args2 = new() {
"--distribution", distributionPath,
"--package-path", tmpPayload2,
"--resources", tmpResources,
pkgOutputPath
};
if (!String.IsNullOrEmpty(signIdentity)) {
args2.Add("--sign");
args2.Add(signIdentity);
} else {
Log.Warn("No Installer signing identity provided. The '.pkg' will not be signed.");
}
[SupportedOSPlatform("osx")]
public static void SpctlAssessInstaller(string filePath)
{
var args2 = new List<string> {
"--assess",
"-vvv",
"-t", "install",
filePath
};
InvokeAndThrowIfNonZero("productbuild", args2, null);
Log.Info($"Verifying signature/notarization for installer package using spctl...");
Console.WriteLine(InvokeAndThrowIfNonZero("spctl", args2, null));
}
Log.Info("Installer created successfully");
}
[SupportedOSPlatform("osx")]
public static void CreateInstallerPkg(string appBundlePath, string appTitle, IEnumerable<KeyValuePair<string, string>> extraContent,
string pkgOutputPath, string signIdentity)
{
// https://matthew-brett.github.io/docosx/flat_packages.html
[SupportedOSPlatform("osx")]
public void Notarize(string filePath, string keychainProfileName)
{
Log.Info($"Preparing to Notarize '{filePath}'. This will upload to Apple and usually takes minutes, but could take hours.");
Log.Info($"Creating installer '.pkg' for app at '{appBundlePath}'");
var args = new List<string> {
"notarytool",
"submit",
"-f", "json",
"--keychain-profile", keychainProfileName,
"--wait",
filePath
};
if (File.Exists(pkgOutputPath)) File.Delete(pkgOutputPath);
var ntresultjson = PlatformUtil.InvokeProcess("xcrun", args, null, CancellationToken.None);
Log.Info(ntresultjson.StdOutput);
using var _1 = Utility.GetTempDirectory(out var tmp);
using var _2 = Utility.GetTempDirectory(out var tmpPayload1);
using var _3 = Utility.GetTempDirectory(out var tmpPayload2);
using var _4 = Utility.GetTempDirectory(out var tmpScripts);
using var _5 = Utility.GetTempDirectory(out var tmpResources);
// try to catch any notarization errors. if we have a submission id, retrieve notary logs.
try {
var ntresult = JsonConvert.DeserializeObject<NotaryToolResult>(ntresultjson.StdOutput);
if (ntresult?.status != "Accepted" || ntresultjson.ExitCode != 0) {
if (ntresult?.id != null) {
var logargs = new List<string> {
"notarytool",
"log",
ntresult?.id,
"--keychain-profile", keychainProfileName,
};
// copy .app to tmp folder
var bundleName = Path.GetFileName(appBundlePath);
var tmpBundlePath = Path.Combine(tmpPayload1, bundleName);
Utility.CopyFiles(new DirectoryInfo(appBundlePath), new DirectoryInfo(tmpBundlePath));
// create postinstall scripts to open app after install
// https://stackoverflow.com/questions/35619036/open-app-after-installation-from-pkg-file-in-mac
var postinstall = Path.Combine(tmpScripts, "postinstall");
File.WriteAllText(postinstall, $"#!/bin/sh\nsudo -u \"$USER\" open \"$2/{bundleName}/\"\nexit 0");
PlatformUtil.ChmodFileAsExecutable(postinstall);
// generate non-relocatable component pkg. this will be included into a product archive
var pkgPlistPath = Path.Combine(tmp, "tmp.plist");
InvokeAndThrowIfNonZero("pkgbuild", new[] { "--analyze", "--root", tmpPayload1, pkgPlistPath }, null);
InvokeAndThrowIfNonZero("plutil", new[] { "-replace", "BundleIsRelocatable", "-bool", "NO", pkgPlistPath }, null);
var pkg1Path = Path.Combine(tmpPayload2, "1.pkg");
string[] args1 = {
"--root", tmpPayload1,
"--component-plist", pkgPlistPath,
"--scripts", tmpScripts,
"--install-location", "/Applications",
pkg1Path,
};
InvokeAndThrowIfNonZero("pkgbuild", args1, null);
// create final product package that contains app component
var distributionPath = Path.Combine(tmp, "distribution.xml");
InvokeAndThrowIfNonZero("productbuild", new[] { "--synthesize", "--package", pkg1Path, distributionPath }, null);
// https://developer.apple.com/library/archive/documentation/DeveloperTools/Reference/DistributionDefinitionRef/Chapters/Distribution_XML_Ref.html
var distXml = File.ReadAllLines(distributionPath).ToList();
distXml.Insert(2, $"<title>{SecurityElement.Escape(appTitle)}</title>");
// disable local system installation (install to home dir)
distXml.Insert(2, "<domains enable_anywhere=\"false\" enable_currentUserHome=\"true\" enable_localSystem=\"false\" />");
// add extra landing content (eg. license, readme)
foreach (var kvp in extraContent) {
if (!String.IsNullOrEmpty(kvp.Value) && File.Exists(kvp.Value)) {
var fileName = Path.GetFileName(kvp.Value);
File.Copy(kvp.Value, Path.Combine(tmpResources, fileName));
distXml.Insert(2, $"<{kvp.Key} file=\"{fileName}\" />");
var result = PlatformUtil.InvokeProcess("xcrun", logargs, null, CancellationToken.None);
Log.Warn(result.StdOutput);
}
}
File.WriteAllLines(distributionPath, distXml);
List<string> args2 = new() {
"--distribution", distributionPath,
"--package-path", tmpPayload2,
"--resources", tmpResources,
pkgOutputPath
};
if (!String.IsNullOrEmpty(signIdentity)) {
args2.Add("--sign");
args2.Add(signIdentity);
} else {
Log.Warn("No Installer signing identity provided. The '.pkg' will not be signed.");
}
InvokeAndThrowIfNonZero("productbuild", args2, null);
Log.Info("Installer created successfully");
}
[SupportedOSPlatform("osx")]
public static void Notarize(string filePath, string keychainProfileName)
{
Log.Info($"Preparing to Notarize '{filePath}'. This will upload to Apple and usually takes minutes, but could take hours.");
var args = new List<string> {
"notarytool",
"submit",
"-f", "json",
"--keychain-profile", keychainProfileName,
"--wait",
filePath
};
var ntresultjson = PlatformUtil.InvokeProcess("xcrun", args, null, CancellationToken.None);
Log.Info(ntresultjson.StdOutput);
// try to catch any notarization errors. if we have a submission id, retrieve notary logs.
try {
var ntresult = JsonConvert.DeserializeObject<NotaryToolResult>(ntresultjson.StdOutput);
if (ntresult?.status != "Accepted" || ntresultjson.ExitCode != 0) {
if (ntresult?.id != null) {
var logargs = new List<string> {
"notarytool",
"log",
ntresult?.id,
"--keychain-profile", keychainProfileName,
};
var result = PlatformUtil.InvokeProcess("xcrun", logargs, null, CancellationToken.None);
Log.Warn(result.StdOutput);
}
throw new Exception("Notarization failed: " + ntresultjson.StdOutput);
}
} catch (JsonReaderException) {
throw new Exception("Notarization failed: " + ntresultjson.StdOutput);
}
Log.Info("Notarization completed successfully");
} catch (JsonReaderException) {
throw new Exception("Notarization failed: " + ntresultjson.StdOutput);
}
[SupportedOSPlatform("osx")]
public static void Staple(string filePath)
{
Log.Info($"Stapling Notarization to '{filePath}'");
Console.WriteLine(InvokeAndThrowIfNonZero("xcrun", new[] { "stapler", "staple", filePath }, null));
}
Log.Info("Notarization completed successfully");
}
private class NotaryToolResult
{
public string id { get; set; }
public string message { get; set; }
public string status { get; set; }
}
[SupportedOSPlatform("osx")]
public void Staple(string filePath)
{
Log.Info($"Stapling Notarization to '{filePath}'");
Console.WriteLine(InvokeAndThrowIfNonZero("xcrun", new[] { "stapler", "staple", filePath }, null));
}
[SupportedOSPlatform("osx")]
public static void CreateDittoZip(string folder, string outputZip)
{
if (File.Exists(outputZip)) File.Delete(outputZip);
private class NotaryToolResult
{
public string id { get; set; }
public string message { get; set; }
public string status { get; set; }
}
var args = new List<string> {
"-c",
"-k",
"--rsrc",
"--keepParent",
"--sequesterRsrc",
folder,
outputZip
};
[SupportedOSPlatform("osx")]
public void CreateDittoZip(string folder, string outputZip)
{
if (File.Exists(outputZip)) File.Delete(outputZip);
Log.Info($"Creating ditto bundle '{outputZip}'");
InvokeAndThrowIfNonZero("ditto", args, null);
}
var args = new List<string> {
"-c",
"-k",
"--rsrc",
"--keepParent",
"--sequesterRsrc",
folder,
outputZip
};
Log.Info($"Creating ditto bundle '{outputZip}'");
InvokeAndThrowIfNonZero("ditto", args, null);
}
}

View File

@@ -0,0 +1,262 @@
using System.Text;
using System.Text.RegularExpressions;
using Microsoft.Extensions.Logging;
using NuGet.Versioning;
namespace Squirrel.Packaging.OSX;
public class BundleOsxOptions
{
public DirectoryInfo ReleaseDir { get; set; }
public string PackId { get; set; }
public string PackVersion { get; set; }
public string PackDirectory { get; set; }
public string PackAuthors { get; set; }
public string PackTitle { get; set; }
public string EntryExecutableName { get; set; }
public string Icon { get; set; }
public string BundleId { get; set; }
}
public class ReleasifyOsxOptions
{
public DirectoryInfo ReleaseDir { get; set; }
public RID TargetRuntime { get; set; }
public string BundleDirectory { get; set; }
public bool IncludePdb { get; set; }
public string ReleaseNotes { get; set; }
public bool NoDelta { get; set; }
public bool NoPackage { get; set; }
public string PackageWelcome { get; set; }
public string PackageReadme { get; set; }
public string PackageLicense { get; set; }
public string PackageConclusion { get; set; }
public string SigningAppIdentity { get; set; }
public string SigningInstallIdentity { get; set; }
public string SigningEntitlements { get; set; }
public string NotaryProfile { get; set; }
}
public class OsxCommands
{
public ILogger Log { get; }
public OsxCommands(ILogger logger)
{
Log = logger;
}
public void Bundle(BundleOsxOptions options)
{
var icon = options.Icon;
var packId = options.PackId;
var packDirectory = options.PackDirectory;
var packVersion = options.PackVersion;
var exeName = options.EntryExecutableName;
var packAuthors = options.PackAuthors;
var packTitle = options.PackTitle;
var releaseDir = options.ReleaseDir;
Log.Info("Generating new '.app' bundle from a directory of application files.");
var mainExePath = Path.Combine(packDirectory, exeName);
if (!File.Exists(mainExePath))// || !PlatformUtil.IsMachOImage(mainExePath))
throw new ArgumentException($"--exeName '{mainExePath}' does not exist or is not a mach-o executable.");
var appleId = $"com.{packAuthors ?? packId}.{packId}";
var escapedAppleId = Regex.Replace(appleId, @"[^\w\.]", "_");
var appleSafeVersion = NuGetVersion.Parse(packVersion).Version.ToString();
var info = new AppInfo {
SQPackId = packId,
SQPackAuthors = packAuthors,
CFBundleName = packTitle ?? packId,
//CFBundleDisplayName = packTitle ?? packId,
CFBundleExecutable = exeName,
CFBundleIdentifier = options.BundleId ?? escapedAppleId,
CFBundlePackageType = "APPL",
CFBundleShortVersionString = appleSafeVersion,
CFBundleVersion = packVersion,
CFBundleSignature = "????",
NSPrincipalClass = "NSApplication",
NSHighResolutionCapable = true,
CFBundleIconFile = Path.GetFileName(icon),
};
Log.Info("Creating '.app' directory structure");
var builder = new StructureBuilder(packId, releaseDir.FullName);
if (Directory.Exists(builder.AppDirectory)) {
Log.Warn(builder.AppDirectory + " already exists, deleting...");
Utility.DeleteFileOrDirectoryHard(builder.AppDirectory);
}
builder.Build();
Log.Info("Writing Info.plist");
var plist = new PlistWriter(Log, info, builder.ContentsDirectory);
plist.Write();
Log.Info("Copying resources into new '.app' bundle");
File.Copy(icon, Path.Combine(builder.ResourcesDirectory, Path.GetFileName(icon)));
Log.Info("Copying application files into new '.app' bundle");
Utility.CopyFiles(new DirectoryInfo(packDirectory), new DirectoryInfo(builder.MacosDirectory));
Log.Info("Bundle created successfully: " + builder.AppDirectory);
}
public void Releasify(ReleasifyOsxOptions options)
{
var releaseDir = options.ReleaseDir;
var appBundlePath = options.BundleDirectory;
Log.Info("Creating Squirrel application from app bundle at: " + appBundlePath);
Log.Info("Parsing app Info.plist");
var contentsDir = Path.Combine(appBundlePath, "Contents");
if (!Directory.Exists(contentsDir))
throw new Exception("Invalid bundle structure (missing Contents dir)");
var plistPath = Path.Combine(contentsDir, "Info.plist");
if (!File.Exists(plistPath))
throw new Exception("Invalid bundle structure (missing Info.plist)");
NSDictionary rootDict = (NSDictionary) PropertyListParser.Parse(plistPath);
var packId = rootDict.ObjectForKey(nameof(AppInfo.SQPackId))?.ToString();
if (String.IsNullOrWhiteSpace(packId))
packId = rootDict.ObjectForKey(nameof(AppInfo.CFBundleIdentifier))?.ToString();
var packAuthors = rootDict.ObjectForKey(nameof(AppInfo.SQPackAuthors))?.ToString();
if (String.IsNullOrWhiteSpace(packAuthors))
packAuthors = packId;
var packTitle = rootDict.ObjectForKey(nameof(AppInfo.CFBundleName))?.ToString();
var packVersion = rootDict.ObjectForKey(nameof(AppInfo.CFBundleVersion))?.ToString();
if (String.IsNullOrWhiteSpace(packId))
throw new InvalidOperationException($"Invalid CFBundleIdentifier in Info.plist: '{packId}'");
if (String.IsNullOrWhiteSpace(packTitle))
throw new InvalidOperationException($"Invalid CFBundleName in Info.plist: '{packTitle}'");
if (String.IsNullOrWhiteSpace(packVersion) || !NuGetVersion.TryParse(packVersion, out var _))
throw new InvalidOperationException($"Invalid CFBundleVersion in Info.plist: '{packVersion}'");
Log.Info($"Package valid: '{packId}', Name: '{packTitle}', Version: {packVersion}");
Log.Info("Adding Squirrel resources to bundle.");
var nuspecText = NugetConsole.CreateNuspec(
packId, packTitle, packAuthors, packVersion, options.ReleaseNotes, options.IncludePdb);
var nuspecPath = Path.Combine(contentsDir, Utility.SpecVersionFileName);
var helper = new HelperExe(Log);
// nuspec and UpdateMac need to be in contents dir or this package can't update
File.WriteAllText(nuspecPath, nuspecText);
File.Copy(helper.UpdateMacPath, Path.Combine(contentsDir, "UpdateMac"), true);
var zipPath = Path.Combine(releaseDir.FullName, $"{packId}-{options.TargetRuntime.StringWithNoVersion}.zip");
if (File.Exists(zipPath)) File.Delete(zipPath);
// code signing all mach-o binaries
if (!String.IsNullOrEmpty(options.SigningAppIdentity) && !String.IsNullOrEmpty(options.NotaryProfile)) {
helper.CodeSign(options.SigningAppIdentity, options.SigningEntitlements, appBundlePath);
helper.CreateDittoZip(appBundlePath, zipPath);
helper.Notarize(zipPath, options.NotaryProfile);
helper.Staple(appBundlePath);
helper.SpctlAssessCode(appBundlePath);
File.Delete(zipPath);
} else {
Log.Warn("Package will not be signed or notarized. Requires the --signAppIdentity and --notaryProfile options.");
}
// create a portable zip package from signed/notarized bundle
Log.Info("Creating final application artifact (zip)");
helper.CreateDittoZip(appBundlePath, zipPath);
// create release / delta from notarized .app
Log.Info("Creating Squirrel Release");
using var _ = Utility.GetTempDirectory(out var tmp);
var nuget = new NugetConsole(Log);
var nupkgPath = nuget.CreatePackageFromNuspecPath(tmp, appBundlePath, nuspecPath);
var releaseFilePath = Path.Combine(releaseDir.FullName, "RELEASES");
var releases = new Dictionary<string, ReleaseEntry>();
ReleaseEntry.BuildReleasesFile(releaseDir.FullName);
foreach (var rel in ReleaseEntry.ParseReleaseFile(File.ReadAllText(releaseFilePath, Encoding.UTF8))) {
releases[rel.Filename] = rel;
}
var rp = new ReleasePackageBuilder(Log, nupkgPath);
var suggestedName = ReleasePackageBuilder.GetSuggestedFileName(packId, packVersion, options.TargetRuntime.StringWithNoVersion);
var newPkgPath = rp.CreateReleasePackage((i, pkg) => Path.Combine(releaseDir.FullName, suggestedName));
Log.Info("Creating Delta Packages");
var prev = ReleasePackageBuilder.GetPreviousRelease(Log, releases.Values, rp, releaseDir.FullName, options.TargetRuntime);
if (prev != null && !options.NoDelta) {
var deltaBuilder = new DeltaPackageBuilder(Log);
var deltaFile = rp.ReleasePackageFile.Replace("-full", "-delta");
var dp = deltaBuilder.CreateDeltaPackage(prev, rp, deltaFile);
var deltaEntry = ReleaseEntry.GenerateFromFile(deltaFile);
releases[deltaEntry.Filename] = deltaEntry;
}
var fullEntry = ReleaseEntry.GenerateFromFile(newPkgPath);
releases[fullEntry.Filename] = fullEntry;
ReleaseEntry.WriteReleaseFile(releases.Values, releaseFilePath);
// create installer package, sign and notarize
if (!options.NoPackage) {
if (SquirrelRuntimeInfo.IsOSX) {
var pkgPath = Path.Combine(releaseDir.FullName, $"{packId}-{options.TargetRuntime.StringWithNoVersion}.pkg");
Dictionary<string, string> pkgContent = new() {
{"welcome", options.PackageWelcome },
{"license", options.PackageLicense },
{"readme", options.PackageReadme },
{"conclusion", options.PackageConclusion },
};
helper.CreateInstallerPkg(appBundlePath, packTitle, pkgContent, pkgPath, options.SigningInstallIdentity);
if (!String.IsNullOrEmpty(options.SigningInstallIdentity) && !String.IsNullOrEmpty(options.NotaryProfile)) {
helper.Notarize(pkgPath, options.NotaryProfile);
helper.Staple(pkgPath);
helper.SpctlAssessInstaller(pkgPath);
} else {
Log.Warn("Package installer (.pkg) will not be Notarized. " +
"This is supported with the --signInstallIdentity and --notaryProfile arguments.");
}
} else {
Log.Warn("Package installer (.pkg) will not be created - this is only supported on OSX.");
}
}
Log.Info("Done.");
}
}

View File

@@ -116,7 +116,7 @@ using System.Text.RegularExpressions;
using System.Xml;
// ASCIIPropertyListParser.cs
namespace Squirrel.PropertyList
namespace Squirrel.Packaging.OSX
{
/// <summary>
/// <para>
@@ -900,7 +900,7 @@ namespace Squirrel.PropertyList
}
// BinaryPropertyListParser.cs
namespace Squirrel.PropertyList
namespace Squirrel.Packaging.OSX
{
/// <summary>
/// <para>
@@ -1467,7 +1467,7 @@ namespace Squirrel.PropertyList
}
// BinaryPropertyListWriter.AddObjectEqualityComparer.cs
namespace Squirrel.PropertyList
namespace Squirrel.Packaging.OSX
{
internal partial class BinaryPropertyListWriter
{
@@ -1504,7 +1504,7 @@ namespace Squirrel.PropertyList
}
// BinaryPropertyListWriter.cs
namespace Squirrel.PropertyList
namespace Squirrel.Packaging.OSX
{
/// <summary>
/// <para>A BinaryPropertyListWriter is a helper class for writing out binary property list files.</para>
@@ -1899,7 +1899,7 @@ namespace Squirrel.PropertyList
}
// BinaryPropertyListWriter.GetObjectEqualityComparer.cs
namespace Squirrel.PropertyList
namespace Squirrel.Packaging.OSX
{
internal partial class BinaryPropertyListWriter
{
@@ -1938,7 +1938,7 @@ namespace Squirrel.PropertyList
}
// NSArray.cs
namespace Squirrel.PropertyList
namespace Squirrel.Packaging.OSX
{
/// <summary>Represents an Array.</summary>
/// @author Daniel Dreibrodt
@@ -2273,7 +2273,7 @@ namespace Squirrel.PropertyList
}
// NSArray.IList.cs
namespace Squirrel.PropertyList
namespace Squirrel.Packaging.OSX
{
partial class NSArray : IList<NSObject>
{
@@ -2331,7 +2331,7 @@ namespace Squirrel.PropertyList
}
// NSData.cs
namespace Squirrel.PropertyList
namespace Squirrel.Packaging.OSX
{
/// <summary>NSData objects are wrappers for byte buffers</summary>
/// @author Daniel Dreibrodt
@@ -2488,7 +2488,7 @@ namespace Squirrel.PropertyList
}
// NSDate.cs
namespace Squirrel.PropertyList
namespace Squirrel.Packaging.OSX
{
/// <summary>Represents a date</summary>
/// @author Daniel Dreibrodt
@@ -2638,7 +2638,7 @@ namespace Squirrel.PropertyList
}
// NSDictionary.cs
namespace Squirrel.PropertyList
namespace Squirrel.Packaging.OSX
{
/// <summary>
/// <para>
@@ -3164,7 +3164,7 @@ namespace Squirrel.PropertyList
}
// NSNumber.cs
namespace Squirrel.PropertyList
namespace Squirrel.Packaging.OSX
{
/// <summary>A number whose value is either an integer, a real number or bool.</summary>
/// @author Daniel Dreibrodt
@@ -3682,7 +3682,7 @@ namespace Squirrel.PropertyList
}
// NSObject.cs
namespace Squirrel.PropertyList
namespace Squirrel.Packaging.OSX
{
/// <summary>
/// <para>Abstract interface for any object contained in a property list.</para>
@@ -4099,7 +4099,7 @@ namespace Squirrel.PropertyList
}
// NSSet.cs
namespace Squirrel.PropertyList
namespace Squirrel.Packaging.OSX
{
/// <summary>
/// <para>A set is an interface to an unordered collection of objects.</para>
@@ -4472,7 +4472,7 @@ namespace Squirrel.PropertyList
}
// NSString.cs
namespace Squirrel.PropertyList
namespace Squirrel.Packaging.OSX
{
/// <summary>A NSString contains a string.</summary>
/// @author Daniel Dreibrodt
@@ -4717,7 +4717,7 @@ namespace Squirrel.PropertyList
}
// PropertyListException.cs
namespace Squirrel.PropertyList
namespace Squirrel.Packaging.OSX
{
/// <summary>The exception that is thrown when an property list file could not be processed correctly.</summary>
[Serializable]
@@ -4767,7 +4767,7 @@ namespace Squirrel.PropertyList
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
namespace Squirrel.PropertyList
namespace Squirrel.Packaging.OSX
{
/// <summary>
/// A PropertyListFormatException is thrown by the various property list format parsers when an error in the
@@ -4784,7 +4784,7 @@ namespace Squirrel.PropertyList
}
// PropertyListParser.cs
namespace Squirrel.PropertyList
namespace Squirrel.Packaging.OSX
{
/// <summary>
/// This class provides methods to parse property lists. It can handle files, input streams and byte arrays. All
@@ -5156,7 +5156,7 @@ namespace Squirrel.PropertyList
}
// UID.cs
namespace Squirrel.PropertyList
namespace Squirrel.Packaging.OSX
{
/// <summary>An UID. Only found in binary property lists that are keyed archives.</summary>
/// @author Daniel Dreibrodt
@@ -5341,7 +5341,7 @@ namespace Squirrel.PropertyList
}
// XmlPropertyListParser.cs
namespace Squirrel.PropertyList
namespace Squirrel.Packaging.OSX
{
/// <summary>Parses XML property lists.</summary>
/// @author Daniel Dreibrodt

View File

@@ -1,157 +1,156 @@
// https://raw.githubusercontent.com/egramtel/dotnet-bundle/master/DotNet.Bundle/PlistWriter.cs
using System;
using System.IO;
using System.Xml;
using Squirrel.SimpleSplat;
using Microsoft.Extensions.Logging;
namespace Squirrel.CommandLine.OSX
namespace Squirrel.Packaging.OSX;
internal class PlistWriter
{
internal class PlistWriter : IEnableLogger
private readonly ILogger _logger;
private readonly AppInfo _task;
private readonly string _outputDir;
private static readonly string[] ArrayTypeProperties = { "CFBundleURLSchemes" };
private const char Separator = ';';
public const string PlistFileName = "Info.plist";
public PlistWriter(ILogger logger, AppInfo task, string outputDir)
{
private readonly AppInfo _task;
private readonly string _outputDir;
_logger = logger;
_task = task;
_outputDir = outputDir;
}
private static readonly string[] ArrayTypeProperties = { "CFBundleURLSchemes" };
private const char Separator = ';';
public const string PlistFileName = "Info.plist";
public void Write()
{
var settings = new XmlWriterSettings {
Indent = true,
NewLineOnAttributes = false
};
public PlistWriter(AppInfo task, string outputDir)
{
_task = task;
_outputDir = outputDir;
}
var path = Path.Combine(_outputDir, PlistFileName);
public void Write()
{
var settings = new XmlWriterSettings {
Indent = true,
NewLineOnAttributes = false
};
_logger.Info($"Writing property list file: {path}");
using (var xmlWriter = XmlWriter.Create(path, settings)) {
xmlWriter.WriteStartDocument();
var path = Path.Combine(_outputDir, PlistFileName);
xmlWriter.WriteRaw(Environment.NewLine);
xmlWriter.WriteRaw(
"<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">");
xmlWriter.WriteRaw(Environment.NewLine);
this.Log().Info($"Writing property list file: {path}");
using (var xmlWriter = XmlWriter.Create(path, settings)) {
xmlWriter.WriteStartDocument();
xmlWriter.WriteStartElement("plist");
xmlWriter.WriteAttributeString("version", "1.0");
xmlWriter.WriteStartElement("dict");
xmlWriter.WriteRaw(Environment.NewLine);
xmlWriter.WriteRaw(
"<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">");
xmlWriter.WriteRaw(Environment.NewLine);
if (!String.IsNullOrEmpty(_task.SQPackId))
WriteProperty(xmlWriter, nameof(_task.SQPackId), _task.SQPackId);
xmlWriter.WriteStartElement("plist");
xmlWriter.WriteAttributeString("version", "1.0");
xmlWriter.WriteStartElement("dict");
if (!String.IsNullOrEmpty(_task.SQPackAuthors))
WriteProperty(xmlWriter, nameof(_task.SQPackAuthors), _task.SQPackAuthors);
if (!String.IsNullOrEmpty(_task.SQPackId))
WriteProperty(xmlWriter, nameof(_task.SQPackId), _task.SQPackId);
if (!String.IsNullOrEmpty(_task.CFBundleDisplayName))
WriteProperty(xmlWriter, nameof(_task.CFBundleDisplayName), _task.CFBundleDisplayName);
if (!String.IsNullOrEmpty(_task.SQPackAuthors))
WriteProperty(xmlWriter, nameof(_task.SQPackAuthors), _task.SQPackAuthors);
WriteProperty(xmlWriter, nameof(_task.CFBundleName), _task.CFBundleName);
WriteProperty(xmlWriter, nameof(_task.CFBundleIdentifier), _task.CFBundleIdentifier);
WriteProperty(xmlWriter, nameof(_task.CFBundleVersion), _task.CFBundleVersion);
WriteProperty(xmlWriter, nameof(_task.CFBundlePackageType), _task.CFBundlePackageType);
WriteProperty(xmlWriter, nameof(_task.CFBundleSignature), _task.CFBundleSignature);
WriteProperty(xmlWriter, nameof(_task.CFBundleExecutable), _task.CFBundleExecutable);
WriteProperty(xmlWriter, nameof(_task.CFBundleIconFile), Path.GetFileName(_task.CFBundleIconFile));
WriteProperty(xmlWriter, nameof(_task.CFBundleShortVersionString), _task.CFBundleShortVersionString);
WriteProperty(xmlWriter, nameof(_task.NSPrincipalClass), _task.NSPrincipalClass);
WriteProperty(xmlWriter, nameof(_task.NSHighResolutionCapable), _task.NSHighResolutionCapable);
if (!String.IsNullOrEmpty(_task.CFBundleDisplayName))
WriteProperty(xmlWriter, nameof(_task.CFBundleDisplayName), _task.CFBundleDisplayName);
WriteProperty(xmlWriter, nameof(_task.CFBundleName), _task.CFBundleName);
WriteProperty(xmlWriter, nameof(_task.CFBundleIdentifier), _task.CFBundleIdentifier);
WriteProperty(xmlWriter, nameof(_task.CFBundleVersion), _task.CFBundleVersion);
WriteProperty(xmlWriter, nameof(_task.CFBundlePackageType), _task.CFBundlePackageType);
WriteProperty(xmlWriter, nameof(_task.CFBundleSignature), _task.CFBundleSignature);
WriteProperty(xmlWriter, nameof(_task.CFBundleExecutable), _task.CFBundleExecutable);
WriteProperty(xmlWriter, nameof(_task.CFBundleIconFile), Path.GetFileName(_task.CFBundleIconFile));
WriteProperty(xmlWriter, nameof(_task.CFBundleShortVersionString), _task.CFBundleShortVersionString);
WriteProperty(xmlWriter, nameof(_task.NSPrincipalClass), _task.NSPrincipalClass);
WriteProperty(xmlWriter, nameof(_task.NSHighResolutionCapable), _task.NSHighResolutionCapable);
if (_task.NSRequiresAquaSystemAppearance.HasValue) {
WriteProperty(xmlWriter, nameof(_task.NSRequiresAquaSystemAppearance), _task.NSRequiresAquaSystemAppearance.Value);
}
//if (_task.CFBundleURLTypes.Length != 0) {
// WriteProperty(xmlWriter, nameof(_task.CFBundleURLTypes), _task.CFBundleURLTypes);
//}
xmlWriter.WriteEndElement();
xmlWriter.WriteEndElement();
if (_task.NSRequiresAquaSystemAppearance.HasValue) {
WriteProperty(xmlWriter, nameof(_task.NSRequiresAquaSystemAppearance), _task.NSRequiresAquaSystemAppearance.Value);
}
//if (_task.CFBundleURLTypes.Length != 0) {
// WriteProperty(xmlWriter, nameof(_task.CFBundleURLTypes), _task.CFBundleURLTypes);
//}
xmlWriter.WriteEndElement();
xmlWriter.WriteEndElement();
}
}
private void WriteProperty(XmlWriter xmlWriter, string name, string value)
{
if (!string.IsNullOrWhiteSpace(value)) {
xmlWriter.WriteStartElement("key");
xmlWriter.WriteString(name);
xmlWriter.WriteEndElement();
xmlWriter.WriteStartElement("string");
xmlWriter.WriteString(value);
xmlWriter.WriteEndElement();
}
}
private void WriteProperty(XmlWriter xmlWriter, string name, bool value)
{
private void WriteProperty(XmlWriter xmlWriter, string name, string value)
{
if (!string.IsNullOrWhiteSpace(value)) {
xmlWriter.WriteStartElement("key");
xmlWriter.WriteString(name);
xmlWriter.WriteEndElement();
if (value) {
xmlWriter.WriteStartElement("true");
} else {
xmlWriter.WriteStartElement("false");
xmlWriter.WriteStartElement("string");
xmlWriter.WriteString(value);
xmlWriter.WriteEndElement();
}
}
private void WriteProperty(XmlWriter xmlWriter, string name, bool value)
{
xmlWriter.WriteStartElement("key");
xmlWriter.WriteString(name);
xmlWriter.WriteEndElement();
if (value) {
xmlWriter.WriteStartElement("true");
} else {
xmlWriter.WriteStartElement("false");
}
xmlWriter.WriteEndElement();
}
private void WriteProperty(XmlWriter xmlWriter, string name, string[] values)
{
if (values.Length != 0) {
xmlWriter.WriteStartElement("key");
xmlWriter.WriteString(name);
xmlWriter.WriteEndElement();
xmlWriter.WriteStartElement("array");
foreach (var value in values) {
if (!string.IsNullOrEmpty(value)) {
xmlWriter.WriteStartElement("string");
xmlWriter.WriteString(value);
xmlWriter.WriteEndElement();
}
}
xmlWriter.WriteEndElement();
}
private void WriteProperty(XmlWriter xmlWriter, string name, string[] values)
{
if (values.Length != 0) {
xmlWriter.WriteStartElement("key");
xmlWriter.WriteString(name);
xmlWriter.WriteEndElement();
xmlWriter.WriteStartElement("array");
foreach (var value in values) {
if (!string.IsNullOrEmpty(value)) {
xmlWriter.WriteStartElement("string");
xmlWriter.WriteString(value);
xmlWriter.WriteEndElement();
}
}
xmlWriter.WriteEndElement();
}
}
//private void WriteProperty(XmlWriter xmlWriter, string name, ITaskItem[] values)
//{
// xmlWriter.WriteStartElement("key");
// xmlWriter.WriteString(name);
// xmlWriter.WriteEndElement();
// xmlWriter.WriteStartElement("array");
// foreach (var value in values) {
// xmlWriter.WriteStartElement("dict");
// var metadataDictionary = value.CloneCustomMetadata();
// foreach (DictionaryEntry entry in metadataDictionary) {
// var dictValue = entry.Value.ToString();
// var dictKey = entry.Key.ToString();
// if (dictValue.Contains(Separator.ToString()) || ArrayTypeProperties.Contains(dictKey)) //array
// {
// WriteProperty(xmlWriter, dictKey, dictValue.Split(Separator));
// } else {
// WriteProperty(xmlWriter, dictKey, dictValue);
// }
// }
// xmlWriter.WriteEndElement(); //End dict
// }
// xmlWriter.WriteEndElement(); //End outside array
//}
}
//private void WriteProperty(XmlWriter xmlWriter, string name, ITaskItem[] values)
//{
// xmlWriter.WriteStartElement("key");
// xmlWriter.WriteString(name);
// xmlWriter.WriteEndElement();
// xmlWriter.WriteStartElement("array");
// foreach (var value in values) {
// xmlWriter.WriteStartElement("dict");
// var metadataDictionary = value.CloneCustomMetadata();
// foreach (DictionaryEntry entry in metadataDictionary) {
// var dictValue = entry.Value.ToString();
// var dictKey = entry.Key.ToString();
// if (dictValue.Contains(Separator.ToString()) || ArrayTypeProperties.Contains(dictKey)) //array
// {
// WriteProperty(xmlWriter, dictKey, dictValue.Split(Separator));
// } else {
// WriteProperty(xmlWriter, dictKey, dictValue);
// }
// }
// xmlWriter.WriteEndElement(); //End dict
// }
// xmlWriter.WriteEndElement(); //End outside array
//}
}

View File

@@ -0,0 +1 @@
[assembly: System.Runtime.Versioning.SupportedOSPlatform("osx")]

View File

@@ -1,8 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<NoWarn>$(NoWarn);CA2007;CS8002</NoWarn>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Squirrel.Packaging\Squirrel.Packaging.csproj" />
</ItemGroup>
</Project>

View File

@@ -1,51 +1,46 @@
// https://github.com/egramtel/dotnet-bundle/blob/master/DotNet.Bundle/StructureBuilder.cs
using System;
using System.IO;
using Squirrel.SimpleSplat;
namespace Squirrel.Packaging.OSX;
namespace Squirrel.CommandLine.OSX
public class StructureBuilder
{
public class StructureBuilder : IEnableLogger
private readonly string _id;
private readonly string _outputDir;
private readonly string _appDir;
public StructureBuilder(string appDir)
{
private readonly string _id;
private readonly string _outputDir;
private readonly string _appDir;
_appDir = appDir;
}
public StructureBuilder(string appDir)
{
_appDir = appDir;
public StructureBuilder(string id, string outputDir)
{
_id = id;
_outputDir = outputDir;
}
public string AppDirectory => _appDir ?? Path.Combine(Path.Combine(_outputDir, _id + ".app"));
public string ContentsDirectory => Path.Combine(AppDirectory, "Contents");
public string MacosDirectory => Path.Combine(ContentsDirectory, "MacOS");
public string ResourcesDirectory => Path.Combine(ContentsDirectory, "Resources");
public void Build()
{
if (string.IsNullOrEmpty(_outputDir))
throw new NotSupportedException();
Directory.CreateDirectory(_outputDir);
if (Directory.Exists(AppDirectory)) {
Directory.Delete(AppDirectory, true);
}
public StructureBuilder(string id, string outputDir)
{
_id = id;
_outputDir = outputDir;
}
public string AppDirectory => _appDir ?? Path.Combine(Path.Combine(_outputDir, _id + ".app"));
public string ContentsDirectory => Path.Combine(AppDirectory, "Contents");
public string MacosDirectory => Path.Combine(ContentsDirectory, "MacOS");
public string ResourcesDirectory => Path.Combine(ContentsDirectory, "Resources");
public void Build()
{
if (string.IsNullOrEmpty(_outputDir))
throw new NotSupportedException();
Directory.CreateDirectory(_outputDir);
if (Directory.Exists(AppDirectory)) {
Directory.Delete(AppDirectory, true);
}
Directory.CreateDirectory(AppDirectory);
Directory.CreateDirectory(ContentsDirectory);
Directory.CreateDirectory(MacosDirectory);
Directory.CreateDirectory(ResourcesDirectory);
}
Directory.CreateDirectory(AppDirectory);
Directory.CreateDirectory(ContentsDirectory);
Directory.CreateDirectory(MacosDirectory);
Directory.CreateDirectory(ResourcesDirectory);
}
}

View File

@@ -2,10 +2,10 @@
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
namespace Squirrel.Lib
namespace Squirrel.Packaging.Windows
{
[SupportedOSPlatform("windows")]
internal static class AuthenticodeTools
public static class AuthenticodeTools
{
[DllImport("Wintrust.dll", PreserveSig = true, SetLastError = false)]
static extern uint WinVerifyTrust(IntPtr hWnd, IntPtr pgActionID, IntPtr pWinTrustData);

View File

@@ -1,356 +0,0 @@
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Runtime.Versioning;
using System.Text;
using System.Text.RegularExpressions;
using Squirrel.CommandLine.Commands;
using Squirrel.NuGet;
using Squirrel.SimpleSplat;
using FileMode = System.IO.FileMode;
namespace Squirrel.CommandLine.Windows
{
class Commands : IEnableLogger
{
static IFullLogger Log => SquirrelLocator.Current.GetService<ILogManager>().GetLogger(typeof(Commands));
public static void Pack(PackWindowsCommand options)
{
using (Utility.GetTempDirectory(out var tmp)) {
var nupkgPath = NugetConsole.CreatePackageFromOptions(tmp, options);
options.Package = nupkgPath;
Releasify(options);
}
}
public static void Releasify(ReleasifyWindowsCommand options)
{
var targetDir = options.ReleaseDirectory;
var package = options.Package;
var baseUrl = options.BaseUrl;
var generateDeltas = !options.NoDelta;
var backgroundGif = options.SplashImage;
var setupIcon = options.Icon ?? options.AppIcon;
// normalize and validate that the provided frameworks are supported
var requiredFrameworks = Runtimes.ParseDependencyString(options.Runtimes);
if (requiredFrameworks.Any())
Log.Info("Package dependencies (from '--framework' argument) resolved as: " + String.Join(", ", requiredFrameworks.Select(r => r.Id)));
using var ud = Utility.GetTempDirectory(out var tempDir);
// update icon for Update.exe if requested
var bundledUpdatePath = HelperExe.UpdatePath;
var updatePath = Path.Combine(tempDir, "Update.exe");
if (setupIcon != null && SquirrelRuntimeInfo.IsWindows) {
DotnetUtil.UpdateSingleFileBundleIcon(bundledUpdatePath, updatePath, setupIcon);
} else {
if (setupIcon != null) {
Log.Warn("Unable to set icon for Update.exe (only supported on windows).");
}
File.Copy(bundledUpdatePath, updatePath, true);
}
if (!DotnetUtil.IsSingleFileBundle(updatePath))
throw new InvalidOperationException("Update.exe is corrupt. Broken Squirrel install?");
// copy input package to target output directory
File.Copy(package, Path.Combine(targetDir, Path.GetFileName(package)), true);
var allNuGetFiles = Directory.EnumerateFiles(targetDir)
.Where(x => x.EndsWith(".nupkg", StringComparison.InvariantCultureIgnoreCase));
var toProcess = allNuGetFiles.Select(p => new FileInfo(p)).Where(x => !x.Name.Contains("-delta") && !x.Name.Contains("-full"));
var processed = new List<string>();
var releaseFilePath = Path.Combine(targetDir, "RELEASES");
var previousReleases = new List<ReleaseEntry>();
if (File.Exists(releaseFilePath)) {
previousReleases.AddRange(ReleaseEntry.ParseReleaseFile(File.ReadAllText(releaseFilePath, Encoding.UTF8)));
}
foreach (var file in toProcess) {
Log.Info("Creating release for package: " + file.FullName);
var rp = new ReleasePackageBuilder(file.FullName);
rp.CreateReleasePackage(contentsPostProcessHook: (pkgPath, zpkg) => {
var nuspecPath = Directory.GetFiles(pkgPath, "*.nuspec", SearchOption.TopDirectoryOnly)
.ContextualSingle("package", "*.nuspec", "top level directory");
var libDir = Directory.GetDirectories(Path.Combine(pkgPath, "lib"))
.ContextualSingle("package", "'lib' folder");
var spec = NuspecManifest.ParseFromFile(nuspecPath);
foreach (var exename in options.SquirrelAwareExecutableNames) {
var exepath = Path.GetFullPath(Path.Combine(libDir, exename));
if (!File.Exists(exepath)) {
throw new Exception($"Could not find main exe '{exename}' in package.");
}
File.WriteAllText(exepath + ".squirrel", "1");
}
var awareExes = SquirrelAwareExecutableDetector.GetAllSquirrelAwareApps(libDir);
// do not allow the creation of packages without a SquirrelAwareApp inside
if (!awareExes.Any()) {
throw new ArgumentException(
"There are no SquirrelAwareApps in the provided package. Please mark an exe " +
"as aware using the '-e' argument, or the assembly manifest.");
}
// warning if there are long paths (>200 char) in this package. 260 is max path
// but with the %localappdata% + user name + app name this can add up quickly.
// eg. 'C:\Users\SamanthaJones\AppData\Local\Application\app-1.0.1\' is 60 characters.
Directory.EnumerateFiles(libDir, "*", SearchOption.AllDirectories)
.Select(f => f.Substring(libDir.Length).Trim(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar))
.Where(f => f.Length >= 200)
.ForEach(f => Log.Warn($"File path in package exceeds 200 characters ({f.Length}) and may cause issues on Windows: '{f}'."));
// fail the release if this is a clickonce application
if (Directory.EnumerateFiles(libDir, "*.application").Any(f => File.ReadAllText(f).Contains("clickonce"))) {
throw new ArgumentException(
"Squirrel does not support building releases for ClickOnce applications. " +
"Please publish your application to a folder without ClickOnce.");
}
// parse the PE header of every squirrel aware app
var peparsed = awareExes.ToDictionary(path => path, path => new PeNet.PeFile(path));
// record architecture of squirrel aware binaries so setup can fast fail if unsupported
RuntimeCpu parseMachine(PeNet.Header.Pe.MachineType machine)
{
Utility.TryParseEnumU16<RuntimeCpu>((ushort) machine, out var cpu);
return cpu;
}
var peArch = from pe in peparsed
let machine = pe.Value?.ImageNtHeaders?.FileHeader?.Machine ?? 0
let arch = parseMachine(machine)
select new { Name = Path.GetFileName(pe.Key), Architecture = arch };
if (awareExes.Count > 0) {
Log.Info($"There are {awareExes.Count} SquirrelAwareApps. Binaries will be executed during install/update/uninstall hooks.");
foreach (var pe in peArch) {
Log.Info($" Detected SquirrelAwareApp '{pe.Name}' (arch: {pe.Architecture})");
}
} else {
Log.Warn("There are no SquirrelAwareApps. No hooks will be executed during install/update/uninstall. " +
"Shortcuts will be created for every binary in package.");
}
ZipPackage.SetMetadata(nuspecPath, requiredFrameworks.Select(r => r.Id), options.TargetRuntime);
// create stub executable for all exe's in this package (except Squirrel!)
var exesToCreateStubFor = new DirectoryInfo(pkgPath).GetAllFilesRecursively()
.Where(x => x.Name.EndsWith(".exe", StringComparison.InvariantCultureIgnoreCase))
.Where(x => !x.Name.Equals("squirrel.exe", StringComparison.InvariantCultureIgnoreCase))
.Where(x => !x.Name.Equals("createdump.exe", StringComparison.InvariantCultureIgnoreCase))
.Where(x => Utility.IsFileTopLevelInPackage(x.FullName, pkgPath))
.ToArray(); // materialize the IEnumerable so we never end up creating stubs for stubs
Log.Info($"Creating {exesToCreateStubFor.Length} stub executables");
exesToCreateStubFor.ForEach(x => createExecutableStubForExe(x.FullName));
// copy Update.exe into package, so it can also be updated in both full/delta packages
// and do it before signing so that Update.exe will also be signed. It is renamed to
// 'Squirrel.exe' only because Squirrel.Windows expects it to be called this.
File.Copy(updatePath, Path.Combine(libDir, "Squirrel.exe"), true);
// sign all exe's in this package
var filesToSign = new DirectoryInfo(libDir).GetAllFilesRecursively()
.Where(x => options.SignSkipDll ? Utility.PathPartEndsWith(x.Name, ".exe") : Utility.FileIsLikelyPEImage(x.Name))
.Select(x => x.FullName)
.ToArray();
signFiles(options, libDir, filesToSign);
// copy app icon to 'lib/fx/app.ico'
var iconTarget = Path.Combine(libDir, "app.ico");
if (options.AppIcon != null) {
// icon was specified on the command line
Log.Info("Using app icon from command line arguments");
File.Copy(options.AppIcon, iconTarget, true);
} else if (!File.Exists(iconTarget) && zpkg.IconUrl != null) {
// icon was provided in the nuspec. download it and possibly convert it from a different image format
Log.Info($"Downloading app icon from '{zpkg.IconUrl}'.");
var fd = Utility.CreateDefaultDownloader();
var imgBytes = fd.DownloadBytes(zpkg.IconUrl.ToString()).Result;
if (zpkg.IconUrl.AbsolutePath.EndsWith(".ico")) {
File.WriteAllBytes(iconTarget, imgBytes);
} else {
if (SquirrelRuntimeInfo.IsWindows) {
using var imgStream = new MemoryStream(imgBytes);
using var bmp = (Bitmap) Image.FromStream(imgStream);
using var ico = Icon.FromHandle(bmp.GetHicon());
using var fs = File.Open(iconTarget, FileMode.Create, FileAccess.Write);
ico.Save(fs);
} else {
Log.Warn($"App icon is currently {Path.GetExtension(zpkg.IconUrl.AbsolutePath)} and can not be automatically " +
$"converted to .ico (only supported on windows). Supply a .ico image instead.");
}
}
}
// copy other images to root (used by setup)
if (setupIcon != null) File.Copy(setupIcon, Path.Combine(pkgPath, "setup.ico"), true);
if (backgroundGif != null) File.Copy(backgroundGif, Path.Combine(pkgPath, "splashimage" + Path.GetExtension(backgroundGif)));
return Path.Combine(targetDir, ReleasePackageBuilder.GetSuggestedFileName(spec.Id, spec.Version.ToString(), options.TargetRuntime.StringWithNoVersion));
});
processed.Add(rp.ReleasePackageFile);
var prev = ReleasePackageBuilder.GetPreviousRelease(previousReleases, rp, targetDir, options.TargetRuntime);
if (prev != null && generateDeltas) {
var deltaBuilder = new DeltaPackageBuilder();
var deltaOutputPath = rp.ReleasePackageFile.Replace("-full", "-delta");
var dp = deltaBuilder.CreateDeltaPackage(prev, rp, deltaOutputPath);
processed.Insert(0, dp.InputPackageFile);
}
}
foreach (var file in toProcess) {
File.Delete(file.FullName);
}
var newReleaseEntries = processed
.Select(packageFilename => ReleaseEntry.GenerateFromFile(packageFilename, baseUrl))
.ToList();
var distinctPreviousReleases = previousReleases
.Where(x => !newReleaseEntries.Select(e => e.Version).Contains(x.Version));
var releaseEntries = distinctPreviousReleases.Concat(newReleaseEntries).ToList();
ReleaseEntry.WriteReleaseFile(releaseEntries, releaseFilePath);
var bundledzp = new ZipPackage(package);
var targetSetupExe = Path.Combine(targetDir, $"{bundledzp.Id}Setup-{options.TargetRuntime.StringWithNoVersion}.exe");
File.Copy(options.DebugSetupExe ?? HelperExe.SetupPath, targetSetupExe, true);
if (SquirrelRuntimeInfo.IsWindows) {
HelperExe.SetPEVersionBlockFromPackageInfo(targetSetupExe, bundledzp, setupIcon);
} else {
Log.Warn("Unable to set Setup.exe icon (only supported on windows)");
}
var newestFullRelease = Squirrel.EnumerableExtensions.MaxBy(releaseEntries, x => x.Version).Where(x => !x.IsDelta).First();
var newestReleasePath = Path.Combine(targetDir, newestFullRelease.Filename);
Log.Info($"Creating Setup bundle");
var bundleOffset = SetupBundle.CreatePackageBundle(targetSetupExe, newestReleasePath);
Log.Info("Signing Setup bundle");
signFiles(options, targetDir, targetSetupExe);
Log.Info("Bundle package offset is " + bundleOffset);
Log.Info($"Setup bundle created at '{targetSetupExe}'.");
// this option is used for debugging a local Setup.exe
if (options.DebugSetupExe != null) {
File.Copy(targetSetupExe, options.DebugSetupExe, true);
Log.Warn($"DEBUG OPTION: Setup bundle copied on top of '{options.DebugSetupExe}'. Recompile before creating a new bundle.");
}
if (options.BuildMsi) {
if (SquirrelRuntimeInfo.IsWindows) {
var msiPath = createMsiPackage(targetSetupExe, bundledzp, options.TargetRuntime.Architecture == RuntimeCpu.x64, options.MsiVersion);
Log.Info("Signing MSI package");
signFiles(options, targetDir, msiPath);
} else {
Log.Warn("Unable to create MSI (only supported on windows).");
}
}
Log.Info("Done");
}
private static void signFiles(SigningCommand options, string rootDir, params string[] filePaths)
{
var signParams = options.SignParameters;
var signTemplate = options.SignTemplate;
var signParallel = options.SignParallel;
if (String.IsNullOrEmpty(signParams) && String.IsNullOrEmpty(signTemplate)) {
Log.Debug($"No signing paramaters provided, {filePaths.Length} file(s) will not be signed.");
return;
}
if (!String.IsNullOrEmpty(signTemplate)) {
Log.Info($"Preparing to sign {filePaths.Length} files with custom signing template");
foreach (var f in filePaths) {
HelperExe.SignPEFileWithTemplate(f, signTemplate);
}
return;
}
// signtool.exe does not work if we're not on windows.
if (!SquirrelRuntimeInfo.IsWindows) return;
if (!String.IsNullOrEmpty(signParams)) {
Log.Info($"Preparing to sign {filePaths.Length} files with embedded signtool.exe with parallelism of {signParallel}");
HelperExe.SignPEFilesWithSignTool(rootDir, filePaths, signParams, signParallel);
}
}
[SupportedOSPlatform("windows")]
static string createMsiPackage(string setupExe, IPackage package, bool packageAs64Bit, string msiVersionOverride)
{
Log.Info($"Compiling machine-wide msi deployment tool in {(packageAs64Bit ? "64-bit" : "32-bit")} mode");
var setupExeDir = Path.GetDirectoryName(setupExe);
var setupName = Path.GetFileNameWithoutExtension(setupExe);
var culture = CultureInfo.GetCultureInfo(package.Language ?? "").TextInfo.ANSICodePage;
// WiX Identifiers may contain ASCII characters A-Z, a-z, digits, underscores (_), or
// periods(.). Every identifier must begin with either a letter or an underscore.
var wixId = Regex.Replace(package.Id, @"[^\w\.]", "_");
if (Char.GetUnicodeCategory(wixId[0]) == UnicodeCategory.DecimalDigitNumber)
wixId = "_" + wixId;
var templateData = new Dictionary<string, string> {
{ "Id", wixId },
{ "Title", package.ProductName },
{ "Author", package.ProductCompany },
{ "Version", msiVersionOverride ?? $"{package.Version.Major}.{package.Version.Minor}.{package.Version.Patch}.0" },
{ "Summary", package.ProductDescription },
{ "Codepage", $"{culture}" },
{ "Platform", packageAs64Bit ? "x64" : "x86" },
{ "ProgramFilesFolder", packageAs64Bit ? "ProgramFiles64Folder" : "ProgramFilesFolder" },
{ "Win64YesNo", packageAs64Bit ? "yes" : "no" },
{ "SetupName", setupName }
};
// NB: We need some GUIDs that are based on the package ID, but unique (i.e.
// "Unique but consistent").
for (int i = 1; i <= 10; i++) {
templateData[String.Format("IdAsGuid{0}", i)] = Utility.CreateGuidFromHash(String.Format("{0}:{1}", package.Id, i)).ToString();
}
return HelperExe.CompileWixTemplateToMsi(templateData, setupExeDir, setupName);
}
static void createExecutableStubForExe(string exeToCopy)
{
try {
var targetName = Path.GetFileNameWithoutExtension(exeToCopy) + "_ExecutionStub.exe";
var target = Path.Combine(Path.GetDirectoryName(exeToCopy), targetName);
Utility.Retry(() => File.Copy(HelperExe.StubExecutablePath, target, true));
Utility.Retry(() => {
if (SquirrelRuntimeInfo.IsWindows) {
using var writer = new Microsoft.NET.HostModel.ResourceUpdater(target, true);
writer.AddResourcesFromPEImage(exeToCopy);
writer.Update();
} else {
Log.Warn($"Cannot set resources/icon for {target} (only supported on windows).");
}
});
} catch (Exception ex) {
Log.ErrorException($"Error creating StubExecutable and copying resources for '{exeToCopy}'. This stub may or may not work properly.", ex);
}
}
}
}

View File

@@ -1,27 +1,24 @@
using System;
using System.Collections.Generic;
using System.Security;
using System.Security;
using System.Text;
namespace Squirrel.CommandLine.Windows
namespace Squirrel.Packaging.Windows;
internal static class CopStache
{
internal static class CopStache
public static string Render(string template, Dictionary<string, string> identifiers)
{
public static string Render(string template, Dictionary<string, string> identifiers)
{
var buf = new StringBuilder();
var buf = new StringBuilder();
foreach (var line in template.Split('\n')) {
identifiers["RandomGuid"] = (Guid.NewGuid()).ToString();
foreach (var line in template.Split('\n')) {
identifiers["RandomGuid"] = (Guid.NewGuid()).ToString();
foreach (var key in identifiers.Keys) {
buf.Replace("{{" + key + "}}", SecurityElement.Escape(identifiers[key]));
}
buf.AppendLine(line);
foreach (var key in identifiers.Keys) {
buf.Replace("{{" + key + "}}", SecurityElement.Escape(identifiers[key]));
}
return buf.ToString();
buf.AppendLine(line);
}
return buf.ToString();
}
}

View File

@@ -1,10 +1,11 @@
// Parts of this file have been used from
#if false
// Parts of this file have been used from
// https://github.com/icsharpcode/ILSpy/blob/f7460a041ea8fb8b0abf8527b97a5b890eb94eea/ICSharpCode.Decompiler/SingleFileBundle.cs
using Microsoft.NET.HostModel.AppHost;
using Microsoft.NET.HostModel.Bundle;
using NuGet.Versioning;
using Squirrel.SimpleSplat;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
@@ -304,3 +305,4 @@ namespace Squirrel.CommandLine.Windows
}
}
}
#endif

View File

@@ -1,113 +1,87 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.Versioning;
using System.Runtime.Versioning;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using Squirrel.Lib;
using Microsoft.Extensions.Logging;
namespace Squirrel.CommandLine.Windows
namespace Squirrel.Packaging.Windows;
public class HelperExe : HelperFile
{
internal class HelperExe : HelperFile
public HelperExe(ILogger logger) : base(logger)
{
public static string SetupPath => FindHelperFile("Setup.exe");
}
public static string UpdatePath
=> FindHelperFile("Update.exe", p => Microsoft.NET.HostModel.AppHost.HostWriter.IsBundle(p, out var _));
public static string SetupPath => FindHelperFile("Setup.exe");
public static string StubExecutablePath => FindHelperFile("StubExecutable.exe");
public static string UpdatePath => FindHelperFile("Update.exe");
// private so we don't expose paths to internal tools. these should be exposed as a helper function
private static string SignToolPath => FindHelperFile("signtool.exe");
private static string WixTemplatePath => FindHelperFile("template.wxs");
private static string RceditPath => FindHelperFile("rcedit.exe");
private static string WixCandlePath => FindHelperFile("candle.exe");
private static string WixLightPath => FindHelperFile("light.exe");
public static string StubExecutablePath => FindHelperFile("StubExecutable.exe");
[SupportedOSPlatform("windows")]
private static bool CheckIsAlreadySigned(string filePath)
{
if (String.IsNullOrWhiteSpace(filePath)) return true;
// private so we don't expose paths to internal tools. these should be exposed as a helper function
private static string SignToolPath => FindHelperFile("signtool.exe");
private static string WixTemplatePath => FindHelperFile("template.wxs");
private static string RceditPath => FindHelperFile("rcedit.exe");
private static string WixCandlePath => FindHelperFile("candle.exe");
private static string WixLightPath => FindHelperFile("light.exe");
if (!File.Exists(filePath)) {
Log.Warn($"Cannot sign '{filePath}', file does not exist.");
[SupportedOSPlatform("windows")]
private bool CheckIsAlreadySigned(string filePath)
{
if (String.IsNullOrWhiteSpace(filePath)) return true;
if (!File.Exists(filePath)) {
Log.Warn($"Cannot sign '{filePath}', file does not exist.");
return true;
}
try {
if (AuthenticodeTools.IsTrusted(filePath)) {
Log.Debug($"'{filePath}' is already signed, skipping...");
return true;
}
try {
if (AuthenticodeTools.IsTrusted(filePath)) {
Log.Debug("'{0}' is already signed, skipping...", filePath);
return true;
}
} catch (Exception ex) {
Log.ErrorException("Failed to determine signing status for " + filePath, ex);
}
return false;
} catch (Exception ex) {
Log.Error(ex, "Failed to determine signing status for " + filePath);
}
[SupportedOSPlatform("windows")]
public static void SignPEFilesWithSignTool(string rootDir, string[] filePaths, string signArguments, int parallelism)
{
Queue<string> pendingSign = new Queue<string>();
return false;
}
foreach (var f in filePaths) {
if (!CheckIsAlreadySigned(f)) {
// try to find the path relative to rootDir
if (String.IsNullOrEmpty(rootDir)) {
pendingSign.Enqueue(f);
} else {
var partialPath = Utility.NormalizePath(f).Substring(Utility.NormalizePath(rootDir).Length).Trim('/', '\\');
pendingSign.Enqueue(partialPath);
}
[SupportedOSPlatform("windows")]
public void SignPEFilesWithSignTool(string rootDir, string[] filePaths, string signArguments, int parallelism)
{
Queue<string> pendingSign = new Queue<string>();
foreach (var f in filePaths) {
if (!CheckIsAlreadySigned(f)) {
// try to find the path relative to rootDir
if (String.IsNullOrEmpty(rootDir)) {
pendingSign.Enqueue(f);
} else {
Log.Debug($"'{f}' is already signed, and will not be signed again.");
var partialPath = Utility.NormalizePath(f).Substring(Utility.NormalizePath(rootDir).Length).Trim('/', '\\');
pendingSign.Enqueue(partialPath);
}
} else {
Log.Debug($"'{f}' is already signed, and will not be signed again.");
}
if (filePaths.Length != pendingSign.Count) {
var diff = filePaths.Length - pendingSign.Count;
Log.Info($"{pendingSign.Count} files will be signed, {diff} will be skipped because they are already signed.");
}
var totalToSign = pendingSign.Count;
var baseSignArgs = PlatformUtil.CommandLineToArgvW(signArguments);
do {
List<string> args = new List<string>();
args.Add("sign");
args.AddRange(baseSignArgs);
for (int i = Math.Min(pendingSign.Count, parallelism); i > 0; i--) {
args.Add(pendingSign.Dequeue());
}
var result = PlatformUtil.InvokeProcess(SignToolPath, args, rootDir, CancellationToken.None);
if (result.ExitCode != 0) {
var cmdWithPasswordHidden = new Regex(@"\/p\s+?[^\s]+").Replace(result.Command, "/p ********");
Log.Debug($"Signing command failed: {cmdWithPasswordHidden}");
throw new Exception(
$"Signing command failed. Specify --verbose argument to print signing command.\n\n" +
$"Output was:\n" + result.StdOutput);
}
Log.Info($"Signed {totalToSign - pendingSign.Count}/{totalToSign} successfully.\r\n" + result.StdOutput);
} while (pendingSign.Count > 0);
}
public static void SignPEFileWithTemplate(string filePath, string signTemplate)
{
if (SquirrelRuntimeInfo.IsWindows && CheckIsAlreadySigned(filePath)) {
Log.Debug($"'{filePath}' is already signed, and will not be signed again.");
return;
if (filePaths.Length != pendingSign.Count) {
var diff = filePaths.Length - pendingSign.Count;
Log.Info($"{pendingSign.Count} files will be signed, {diff} will be skipped because they are already signed.");
}
var totalToSign = pendingSign.Count;
var baseSignArgs = PlatformUtil.CommandLineToArgvW(signArguments);
do {
List<string> args = new List<string>();
args.Add("sign");
args.AddRange(baseSignArgs);
for (int i = Math.Min(pendingSign.Count, parallelism); i > 0; i--) {
args.Add(pendingSign.Dequeue());
}
var command = signTemplate.Replace("\"{{file}}\"", "{{file}}").Replace("{{file}}", $"\"{filePath}\"");
var result = PlatformUtil.InvokeProcess(command, null, null, CancellationToken.None);
var result = PlatformUtil.InvokeProcess(SignToolPath, args, rootDir, CancellationToken.None);
if (result.ExitCode != 0) {
var cmdWithPasswordHidden = new Regex(@"\/p\s+?[^\s]+").Replace(result.Command, "/p ********");
Log.Debug($"Signing command failed: {cmdWithPasswordHidden}");
@@ -116,68 +90,90 @@ namespace Squirrel.CommandLine.Windows
$"Output was:\n" + result.StdOutput);
}
Log.Info("Sign successful: " + result.StdOutput);
Log.Info($"Signed {totalToSign - pendingSign.Count}/{totalToSign} successfully.\r\n" + result.StdOutput);
} while (pendingSign.Count > 0);
}
public void SignPEFileWithTemplate(string filePath, string signTemplate)
{
if (SquirrelRuntimeInfo.IsWindows && CheckIsAlreadySigned(filePath)) {
Log.Debug($"'{filePath}' is already signed, and will not be signed again.");
return;
}
[SupportedOSPlatform("windows")]
public static string CompileWixTemplateToMsi(Dictionary<string, string> templateData, string workingDir, string appId)
{
var wxsFile = Path.Combine(workingDir, appId + ".wxs");
var objFile = Path.Combine(workingDir, appId + ".wixobj");
var msiFile = Path.Combine(workingDir, appId + "_DeploymentTool.msi");
var command = signTemplate.Replace("\"{{file}}\"", "{{file}}").Replace("{{file}}", $"\"{filePath}\"");
try {
// apply dictionary to wsx template
var templateText = File.ReadAllText(WixTemplatePath);
var templateResult = CopStache.Render(templateText, templateData);
File.WriteAllText(wxsFile, templateResult, Encoding.UTF8);
// Candle reprocesses and compiles WiX source files into object files (.wixobj).
Log.Info("Compiling WiX Template (candle.exe)");
var candleParams = new string[] { "-nologo", "-ext", "WixNetFxExtension", "-out", objFile, wxsFile };
InvokeAndThrowIfNonZero(WixCandlePath, candleParams, workingDir);
// Light links and binds one or more .wixobj files and creates a Windows Installer database (.msi or .msm).
Log.Info("Linking WiX Template (light.exe)");
var lightParams = new string[] { "-ext", "WixNetFxExtension", "-spdb", "-sval", "-out", msiFile, objFile };
InvokeAndThrowIfNonZero(WixLightPath, lightParams, workingDir);
return msiFile;
} finally {
Utility.DeleteFileOrDirectoryHard(wxsFile, throwOnFailure: false);
Utility.DeleteFileOrDirectoryHard(objFile, throwOnFailure: false);
}
var result = PlatformUtil.InvokeProcess(command, null, null, CancellationToken.None);
if (result.ExitCode != 0) {
var cmdWithPasswordHidden = new Regex(@"\/p\s+?[^\s]+").Replace(result.Command, "/p ********");
Log.Debug($"Signing command failed: {cmdWithPasswordHidden}");
throw new Exception(
$"Signing command failed. Specify --verbose argument to print signing command.\n\n" +
$"Output was:\n" + result.StdOutput);
}
[SupportedOSPlatform("windows")]
public static void SetExeIcon(string exePath, string iconPath)
{
Log.Info("Updating PE icon for: " + exePath);
var args = new[] { Path.GetFullPath(exePath), "--set-icon", iconPath };
Utility.Retry(() => InvokeAndThrowIfNonZero(RceditPath, args, null));
}
Log.Info("Sign successful: " + result.StdOutput);
}
[SupportedOSPlatform("windows")]
public static void SetPEVersionBlockFromPackageInfo(string exePath, NuGet.IPackage package, string iconPath = null)
{
Log.Info("Updating StringTable resources for: " + exePath);
var realExePath = Path.GetFullPath(exePath);
[SupportedOSPlatform("windows")]
public string CompileWixTemplateToMsi(Dictionary<string, string> templateData, string workingDir, string appId)
{
var wxsFile = Path.Combine(workingDir, appId + ".wxs");
var objFile = Path.Combine(workingDir, appId + ".wixobj");
var msiFile = Path.Combine(workingDir, appId + "_DeploymentTool.msi");
List<string> args = new List<string>() {
realExePath,
"--set-version-string", "CompanyName", package.ProductCompany,
"--set-version-string", "LegalCopyright", package.ProductCopyright,
"--set-version-string", "FileDescription", package.ProductDescription,
"--set-version-string", "ProductName", package.ProductName,
"--set-file-version", package.Version.ToString(),
"--set-product-version", package.Version.ToString(),
};
try {
// apply dictionary to wsx template
var templateText = File.ReadAllText(WixTemplatePath);
var templateResult = CopStache.Render(templateText, templateData);
File.WriteAllText(wxsFile, templateResult, Encoding.UTF8);
if (iconPath != null) {
args.Add("--set-icon");
args.Add(Path.GetFullPath(iconPath));
}
// Candle reprocesses and compiles WiX source files into object files (.wixobj).
Log.Info("Compiling WiX Template (candle.exe)");
var candleParams = new string[] { "-nologo", "-ext", "WixNetFxExtension", "-out", objFile, wxsFile };
InvokeAndThrowIfNonZero(WixCandlePath, candleParams, workingDir);
Utility.Retry(() => InvokeAndThrowIfNonZero(RceditPath, args, null));
// Light links and binds one or more .wixobj files and creates a Windows Installer database (.msi or .msm).
Log.Info("Linking WiX Template (light.exe)");
var lightParams = new string[] { "-ext", "WixNetFxExtension", "-spdb", "-sval", "-out", msiFile, objFile };
InvokeAndThrowIfNonZero(WixLightPath, lightParams, workingDir);
return msiFile;
} finally {
Utility.DeleteFileOrDirectoryHard(wxsFile, throwOnFailure: false);
Utility.DeleteFileOrDirectoryHard(objFile, throwOnFailure: false);
}
}
[SupportedOSPlatform("windows")]
public void SetExeIcon(string exePath, string iconPath)
{
Log.Info("Updating PE icon for: " + exePath);
var args = new[] { Path.GetFullPath(exePath), "--set-icon", iconPath };
Utility.Retry(() => InvokeAndThrowIfNonZero(RceditPath, args, null));
}
[SupportedOSPlatform("windows")]
public void SetPEVersionBlockFromPackageInfo(string exePath, NuGet.IPackage package, string iconPath = null)
{
Log.Info("Updating StringTable resources for: " + exePath);
var realExePath = Path.GetFullPath(exePath);
List<string> args = new List<string>() {
realExePath,
"--set-version-string", "CompanyName", package.ProductCompany,
"--set-version-string", "LegalCopyright", package.ProductCopyright,
"--set-version-string", "FileDescription", package.ProductDescription,
"--set-version-string", "ProductName", package.ProductName,
"--set-file-version", package.Version.ToString(),
"--set-product-version", package.Version.ToString(),
};
if (iconPath != null) {
args.Add("--set-icon");
args.Add(Path.GetFullPath(iconPath));
}
Utility.Retry(() => InvokeAndThrowIfNonZero(RceditPath, args, null));
}
}

View File

@@ -1,93 +1,89 @@
using System;
using System.IO;
using System.IO.MemoryMappedFiles;
using System.Runtime.Versioning;
using System.IO.MemoryMappedFiles;
using Microsoft.NET.HostModel;
using Microsoft.NET.HostModel.AppHost;
namespace Squirrel.CommandLine.Windows
namespace Squirrel.Packaging.Windows;
public static class SetupBundle
{
public static class SetupBundle
public static bool IsBundle(string setupPath, out long bundleOffset, out long bundleLength)
{
public static bool IsBundle(string setupPath, out long bundleOffset, out long bundleLength)
byte[] bundleSignature = {
// 64 bytes represent the bundle signature: SHA-256 for "squirrel bundle"
0x94, 0xf0, 0xb1, 0x7b, 0x68, 0x93, 0xe0, 0x29,
0x37, 0xeb, 0x34, 0xef, 0x53, 0xaa, 0xe7, 0xd4,
0x2b, 0x54, 0xf5, 0x70, 0x7e, 0xf5, 0xd6, 0xf5,
0x78, 0x54, 0x98, 0x3e, 0x5e, 0x94, 0xed, 0x7d
};
long offset = 0;
long length = 0;
void FindBundleHeader()
{
byte[] bundleSignature = {
// 64 bytes represent the bundle signature: SHA-256 for "squirrel bundle"
0x94, 0xf0, 0xb1, 0x7b, 0x68, 0x93, 0xe0, 0x29,
0x37, 0xeb, 0x34, 0xef, 0x53, 0xaa, 0xe7, 0xd4,
0x2b, 0x54, 0xf5, 0x70, 0x7e, 0xf5, 0xd6, 0xf5,
0x78, 0x54, 0x98, 0x3e, 0x5e, 0x94, 0xed, 0x7d
};
long offset = 0;
long length = 0;
void FindBundleHeader()
{
using (var memoryMappedFile = MemoryMappedFile.CreateFromFile(setupPath, FileMode.Open, null, 0, MemoryMappedFileAccess.Read))
using (MemoryMappedViewAccessor accessor = memoryMappedFile.CreateViewAccessor(0, 0, MemoryMappedFileAccess.Read)) {
int position = BinaryUtils.SearchInFile(accessor, bundleSignature);
if (position == -1) {
throw new PlaceHolderNotFoundInAppHostException(bundleSignature);
}
offset = accessor.ReadInt64(position - 16);
length = accessor.ReadInt64(position - 8);
using (var memoryMappedFile = MemoryMappedFile.CreateFromFile(setupPath, FileMode.Open, null, 0, MemoryMappedFileAccess.Read))
using (MemoryMappedViewAccessor accessor = memoryMappedFile.CreateViewAccessor(0, 0, MemoryMappedFileAccess.Read)) {
int position = BinaryUtils.SearchInFile(accessor, bundleSignature);
if (position == -1) {
throw new PlaceHolderNotFoundInAppHostException(bundleSignature);
}
offset = accessor.ReadInt64(position - 16);
length = accessor.ReadInt64(position - 8);
}
Utility.Retry(FindBundleHeader);
bundleOffset = offset;
bundleLength = length;
return bundleOffset != 0 && bundleLength != 0;
}
public static long CreatePackageBundle(string setupPath, string packagePath)
{
long bundleOffset, bundleLength;
Stream pkgStream = null, setupStream = null;
Utility.Retry(FindBundleHeader);
try {
pkgStream = Utility.Retry(() => File.OpenRead(packagePath), retries: 10);
setupStream = Utility.Retry(() => File.Open(setupPath, FileMode.Append, FileAccess.Write), retries: 10);
bundleOffset = setupStream.Position;
bundleLength = pkgStream.Length;
pkgStream.CopyTo(setupStream);
} finally {
if (pkgStream != null) pkgStream.Dispose();
if (setupStream != null) setupStream.Dispose();
}
bundleOffset = offset;
bundleLength = length;
byte[] placeholder = {
// 8 bytes represent the package offset
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// 8 bytes represent the package length
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// 64 bytes represent the bundle signature: SHA-256 for "squirrel bundle"
0x94, 0xf0, 0xb1, 0x7b, 0x68, 0x93, 0xe0, 0x29,
0x37, 0xeb, 0x34, 0xef, 0x53, 0xaa, 0xe7, 0xd4,
0x2b, 0x54, 0xf5, 0x70, 0x7e, 0xf5, 0xd6, 0xf5,
0x78, 0x54, 0x98, 0x3e, 0x5e, 0x94, 0xed, 0x7d
};
return bundleOffset != 0 && bundleLength != 0;
}
var data = new byte[16];
Array.Copy(BitConverter.GetBytes(bundleOffset), data, 8);
Array.Copy(BitConverter.GetBytes(bundleLength), 0, data, 8, 8);
public static long CreatePackageBundle(string setupPath, string packagePath)
{
long bundleOffset, bundleLength;
Stream pkgStream = null, setupStream = null;
// replace the beginning of the placeholder with the bytes from 'data'
RetryUtil.RetryOnIOError(() =>
BinaryUtils.SearchAndReplace(setupPath, placeholder, data, pad0s: false));
// memory-mapped write does not updating last write time
RetryUtil.RetryOnIOError(() =>
File.SetLastWriteTimeUtc(setupPath, DateTime.UtcNow));
if (!IsBundle(setupPath, out var offset, out var length))
throw new InvalidOperationException("Internal logic error writing setup bundle.");
return bundleOffset;
try {
pkgStream = Utility.Retry(() => File.OpenRead(packagePath), retries: 10);
setupStream = Utility.Retry(() => File.Open(setupPath, FileMode.Append, FileAccess.Write), retries: 10);
bundleOffset = setupStream.Position;
bundleLength = pkgStream.Length;
pkgStream.CopyTo(setupStream);
} finally {
if (pkgStream != null) pkgStream.Dispose();
if (setupStream != null) setupStream.Dispose();
}
byte[] placeholder = {
// 8 bytes represent the package offset
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// 8 bytes represent the package length
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// 64 bytes represent the bundle signature: SHA-256 for "squirrel bundle"
0x94, 0xf0, 0xb1, 0x7b, 0x68, 0x93, 0xe0, 0x29,
0x37, 0xeb, 0x34, 0xef, 0x53, 0xaa, 0xe7, 0xd4,
0x2b, 0x54, 0xf5, 0x70, 0x7e, 0xf5, 0xd6, 0xf5,
0x78, 0x54, 0x98, 0x3e, 0x5e, 0x94, 0xed, 0x7d
};
var data = new byte[16];
Array.Copy(BitConverter.GetBytes(bundleOffset), data, 8);
Array.Copy(BitConverter.GetBytes(bundleLength), 0, data, 8, 8);
// replace the beginning of the placeholder with the bytes from 'data'
RetryUtil.RetryOnIOError(() =>
BinaryUtils.SearchAndReplace(setupPath, placeholder, data, pad0s: false));
// memory-mapped write does not updating last write time
RetryUtil.RetryOnIOError(() =>
File.SetLastWriteTimeUtc(setupPath, DateTime.UtcNow));
if (!IsBundle(setupPath, out var offset, out var length))
throw new InvalidOperationException("Internal logic error writing setup bundle.");
return bundleOffset;
}
}

View File

@@ -3,6 +3,17 @@
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<NoWarn>$(NoWarn);CA2007;CS8002</NoWarn>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Squirrel.Packaging\Squirrel.Packaging.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="PeNet" Version="4.0.3" />
<PackageReference Include="System.Drawing.Common" Version="8.0.0" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,283 @@
using System.Drawing;
using System.Text;
using Microsoft.Extensions.Logging;
using Squirrel.NuGet;
using FileMode = System.IO.FileMode;
namespace Squirrel.Packaging.Windows;
public class SigningOptions
{
public string SignParameters { get; set; }
public bool SignSkipDll { get; set; }
public int SignParallel { get; set; }
public string SignTemplate { get; set; }
}
public class ReleasifyWindowsOptions : SigningOptions
{
public DirectoryInfo ReleaseDir { get; set; }
public RID TargetRuntime { get; set; }
public string Package { get; set; }
public string BaseUrl { get; set; }
public string DebugSetupExe { get; set; }
public bool NoDelta { get; set; }
public string Runtimes { get; set; }
public string SplashImage { get; set; }
public string Icon { get; set; }
public string[] MainExe { get; set; }
public string AppIcon { get; set; }
}
public class PackWindowsOptions : ReleasifyWindowsOptions, INugetPackCommand
{
public string PackId { get; set; }
public string PackVersion { get; set; }
public string PackDirectory { get; set; }
public string PackAuthors { get; set; }
public string PackTitle { get; set; }
public bool IncludePdb { get; set; }
public string ReleaseNotes { get; set; }
}
public class WindowsCommands
{
private readonly ILogger Log;
public WindowsCommands(ILogger logger)
{
Log = logger;
}
public void Pack(PackWindowsOptions options)
{
using (Utility.GetTempDirectory(out var tmp)) {
var nupkgPath = new NugetConsole(Log).CreatePackageFromOptions(tmp, options);
options.Package = nupkgPath;
Releasify(options);
}
}
public void Releasify(ReleasifyWindowsOptions options)
{
var targetDir = options.ReleaseDir.FullName;
var package = options.Package;
var baseUrl = options.BaseUrl;
var generateDeltas = !options.NoDelta;
var backgroundGif = options.SplashImage;
var setupIcon = options.Icon ?? options.AppIcon;
// normalize and validate that the provided frameworks are supported
var requiredFrameworks = options.Runtimes
.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
.Select(Runtimes.GetRuntimeByName);
if (requiredFrameworks.Where(f => f == null).Any())
throw new ArgumentException("Invalid target frameworks string.");
using var ud = Utility.GetTempDirectory(out var tempDir);
// update icon for Update.exe if requested
var helper = new HelperExe(Log);
var updatePath = Path.Combine(tempDir, "Update.exe");
File.Copy(HelperExe.UpdatePath, updatePath, true);
if (setupIcon != null && SquirrelRuntimeInfo.IsWindows) {
helper.SetExeIcon(updatePath, setupIcon);
} else if (setupIcon != null) {
Log.Warn("Unable to set icon for Update.exe (only supported on windows).");
}
// copy input package to target output directory
File.Copy(package, Path.Combine(targetDir, Path.GetFileName(package)), true);
var allNuGetFiles = Directory.EnumerateFiles(targetDir)
.Where(x => x.EndsWith(".nupkg", StringComparison.InvariantCultureIgnoreCase));
var toProcess = allNuGetFiles.Select(p => new FileInfo(p)).Where(x => !x.Name.Contains("-delta") && !x.Name.Contains("-full"));
var processed = new List<string>();
var releaseFilePath = Path.Combine(targetDir, "RELEASES");
var previousReleases = new List<ReleaseEntry>();
if (File.Exists(releaseFilePath)) {
previousReleases.AddRange(ReleaseEntry.ParseReleaseFile(File.ReadAllText(releaseFilePath, Encoding.UTF8)));
}
foreach (var file in toProcess) {
Log.Info("Creating release for package: " + file.FullName);
var rp = new ReleasePackageBuilder(Log, file.FullName);
rp.CreateReleasePackage(contentsPostProcessHook: (pkgPath, zpkg) => {
var nuspecPath = Directory.GetFiles(pkgPath, "*.nuspec", SearchOption.TopDirectoryOnly)
.ContextualSingle("package", "*.nuspec", "top level directory");
var libDir = Directory.GetDirectories(Path.Combine(pkgPath, "lib"))
.ContextualSingle("package", "'lib' folder");
var spec = NuspecManifest.ParseFromFile(nuspecPath);
// warning if there are long paths (>200 char) in this package. 260 is max path
// but with the %localappdata% + user name + app name this can add up quickly.
// eg. 'C:\Users\SamanthaJones\AppData\Local\Application\app-1.0.1\' is 60 characters.
Directory.EnumerateFiles(libDir, "*", SearchOption.AllDirectories)
.Select(f => f.Substring(libDir.Length).Trim(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar))
.Where(f => f.Length >= 200)
.ForEach(f => Log.Warn($"File path in package exceeds 200 characters ({f.Length}) and may cause issues on Windows: '{f}'."));
// fail the release if this is a clickonce application
if (Directory.EnumerateFiles(libDir, "*.application").Any(f => File.ReadAllText(f).Contains("clickonce"))) {
throw new ArgumentException(
"Squirrel does not support building releases for ClickOnce applications. " +
"Please publish your application to a folder without ClickOnce.");
}
ZipPackage.SetMetadata(nuspecPath, requiredFrameworks.Select(r => r.Id), options.TargetRuntime);
// copy Update.exe into package, so it can also be updated in both full/delta packages
// and do it before signing so that Update.exe will also be signed. It is renamed to
// 'Squirrel.exe' only because Squirrel.Windows expects it to be called this.
File.Copy(updatePath, Path.Combine(libDir, "Squirrel.exe"), true);
// sign all exe's in this package
var filesToSign = new DirectoryInfo(libDir).GetAllFilesRecursively()
.Where(x => options.SignSkipDll ? Utility.PathPartEndsWith(x.Name, ".exe") : Utility.FileIsLikelyPEImage(x.Name))
.Select(x => x.FullName)
.ToArray();
signFiles(options, libDir, filesToSign);
// copy app icon to 'lib/fx/app.ico'
var iconTarget = Path.Combine(libDir, "app.ico");
if (options.AppIcon != null) {
// icon was specified on the command line
Log.Info("Using app icon from command line arguments");
File.Copy(options.AppIcon, iconTarget, true);
} else if (!File.Exists(iconTarget) && zpkg.IconUrl != null) {
// icon was provided in the nuspec. download it and possibly convert it from a different image format
Log.Info($"Downloading app icon from '{zpkg.IconUrl}'.");
var fd = Utility.CreateDefaultDownloader();
var imgBytes = fd.DownloadBytes(zpkg.IconUrl.ToString()).Result;
if (zpkg.IconUrl.AbsolutePath.EndsWith(".ico")) {
File.WriteAllBytes(iconTarget, imgBytes);
} else {
if (SquirrelRuntimeInfo.IsWindows) {
using var imgStream = new MemoryStream(imgBytes);
using var bmp = (Bitmap) Image.FromStream(imgStream);
using var ico = Icon.FromHandle(bmp.GetHicon());
using var fs = File.Open(iconTarget, FileMode.Create, FileAccess.Write);
ico.Save(fs);
} else {
Log.Warn($"App icon is currently {Path.GetExtension(zpkg.IconUrl.AbsolutePath)} and can not be automatically " +
$"converted to .ico (only supported on windows). Supply a .ico image instead.");
}
}
}
// copy other images to root (used by setup)
if (setupIcon != null) File.Copy(setupIcon, Path.Combine(pkgPath, "setup.ico"), true);
if (backgroundGif != null) File.Copy(backgroundGif, Path.Combine(pkgPath, "splashimage" + Path.GetExtension(backgroundGif)));
return Path.Combine(targetDir, ReleasePackageBuilder.GetSuggestedFileName(spec.Id, spec.Version.ToString(), options.TargetRuntime.StringWithNoVersion));
});
processed.Add(rp.ReleasePackageFile);
var prev = ReleasePackageBuilder.GetPreviousRelease(Log, previousReleases, rp, targetDir, options.TargetRuntime);
if (prev != null && generateDeltas) {
var deltaBuilder = new DeltaPackageBuilder(Log);
var deltaOutputPath = rp.ReleasePackageFile.Replace("-full", "-delta");
var dp = deltaBuilder.CreateDeltaPackage(prev, rp, deltaOutputPath);
processed.Insert(0, dp.InputPackageFile);
}
}
foreach (var file in toProcess) {
File.Delete(file.FullName);
}
var newReleaseEntries = processed
.Select(packageFilename => ReleaseEntry.GenerateFromFile(packageFilename, baseUrl))
.ToList();
var distinctPreviousReleases = previousReleases
.Where(x => !newReleaseEntries.Select(e => e.Version).Contains(x.Version));
var releaseEntries = distinctPreviousReleases.Concat(newReleaseEntries).ToList();
ReleaseEntry.WriteReleaseFile(releaseEntries, releaseFilePath);
var bundledzp = new ZipPackage(package);
var targetSetupExe = Path.Combine(targetDir, $"{bundledzp.Id}Setup-{options.TargetRuntime.StringWithNoVersion}.exe");
File.Copy(options.DebugSetupExe ?? HelperExe.SetupPath, targetSetupExe, true);
if (SquirrelRuntimeInfo.IsWindows) {
helper.SetPEVersionBlockFromPackageInfo(targetSetupExe, bundledzp, setupIcon);
} else {
Log.Warn("Unable to set Setup.exe icon (only supported on windows)");
}
var newestFullRelease = Squirrel.EnumerableExtensions.MaxBy(releaseEntries, x => x.Version).Where(x => !x.IsDelta).First();
var newestReleasePath = Path.Combine(targetDir, newestFullRelease.Filename);
Log.Info($"Creating Setup bundle");
var bundleOffset = SetupBundle.CreatePackageBundle(targetSetupExe, newestReleasePath);
Log.Info("Signing Setup bundle");
signFiles(options, targetDir, targetSetupExe);
Log.Info("Bundle package offset is " + bundleOffset);
Log.Info($"Setup bundle created at '{targetSetupExe}'.");
// this option is used for debugging a local Setup.exe
if (options.DebugSetupExe != null) {
File.Copy(targetSetupExe, options.DebugSetupExe, true);
Log.Warn($"DEBUG OPTION: Setup bundle copied on top of '{options.DebugSetupExe}'. Recompile before creating a new bundle.");
}
Log.Info("Done");
}
private void signFiles(SigningOptions options, string rootDir, params string[] filePaths)
{
var signParams = options.SignParameters;
var signTemplate = options.SignTemplate;
var signParallel = options.SignParallel;
var helper = new HelperExe(Log);
if (String.IsNullOrEmpty(signParams) && String.IsNullOrEmpty(signTemplate)) {
Log.Debug($"No signing paramaters provided, {filePaths.Length} file(s) will not be signed.");
return;
}
if (!String.IsNullOrEmpty(signTemplate)) {
Log.Info($"Preparing to sign {filePaths.Length} files with custom signing template");
foreach (var f in filePaths) {
helper.SignPEFileWithTemplate(f, signTemplate);
}
return;
}
// signtool.exe does not work if we're not on windows.
if (!SquirrelRuntimeInfo.IsWindows) return;
if (!String.IsNullOrEmpty(signParams)) {
Log.Info($"Preparing to sign {filePaths.Length} files with embedded signtool.exe with parallelism of {signParallel}");
helper.SignPEFilesWithSignTool(rootDir, filePaths, signParams, signParallel);
}
}
}

View File

@@ -1,177 +1,176 @@
using System;
using System.Diagnostics.Contracts;
using System.IO;
using System.Linq;
using System.Text;
using Squirrel.SimpleSplat;
using Squirrel.Bsdiff;
using System.Threading.Tasks;
using System.Threading;
using System.Text;
using Squirrel.Compression;
using Microsoft.Extensions.Logging;
namespace Squirrel.CommandLine
namespace Squirrel.Packaging;
public class DeltaPackageBuilder
{
internal class DeltaPackageBuilder : IEnableLogger
private readonly ILogger _logger;
public DeltaPackageBuilder(ILogger logger)
{
public ReleasePackageBuilder CreateDeltaPackage(ReleasePackageBuilder basePackage, ReleasePackageBuilder newPackage, string outputFile)
{
Contract.Requires(basePackage != null);
Contract.Requires(!String.IsNullOrEmpty(outputFile) && !File.Exists(outputFile));
_logger = logger;
}
if (basePackage.Version >= newPackage.Version) {
var message = String.Format(
"Cannot create a delta package based on version {0} as it is a later or equal to the base version {1}",
basePackage.Version,
newPackage.Version);
throw new InvalidOperationException(message);
}
public ReleasePackageBuilder CreateDeltaPackage(ReleasePackageBuilder basePackage, ReleasePackageBuilder newPackage, string outputFile)
{
if (basePackage == null) throw new ArgumentNullException(nameof(basePackage));
if (newPackage == null) throw new ArgumentNullException(nameof(newPackage));
if (String.IsNullOrEmpty(outputFile) || File.Exists(outputFile)) throw new ArgumentException("The output file is null or already exists", nameof(outputFile));
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);
}
using (Utility.GetTempDirectory(out var baseTempPath))
using (Utility.GetTempDirectory(out var tempPath)) {
var baseTempInfo = new DirectoryInfo(baseTempPath);
var tempInfo = new DirectoryInfo(tempPath);
// minThreads = 1, maxThreads = 8
int numParallel = Math.Min(Math.Max(Environment.ProcessorCount - 1, 1), 8);
this.Log().Info($"Creating delta for {basePackage.Version} -> {newPackage.Version} with {numParallel} parallel threads.");
this.Log().Debug("Extracting {0} and {1} into {2}",
Path.GetFileName(basePackage.ReleasePackageFile), Path.GetFileName(newPackage.ReleasePackageFile), tempPath);
EasyZip.ExtractZipToDirectory(basePackage.ReleasePackageFile, baseTempInfo.FullName);
EasyZip.ExtractZipToDirectory(newPackage.ReleasePackageFile, tempInfo.FullName);
// 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");
var newLibFiles = newLibDir.GetAllFilesRecursively().ToArray();
int fNew = 0, fSame = 0, fChanged = 0, fWarnings = 0;
bool bytesAreIdentical(ReadOnlySpan<byte> a1, ReadOnlySpan<byte> a2)
{
return a1.SequenceEqual(a2);
}
void createDeltaForSingleFile(FileInfo targetFile, DirectoryInfo workingDirectory)
{
// 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
try {
var relativePath = targetFile.FullName.Replace(workingDirectory.FullName, "");
// 1. new file, leave it alone
if (!baseLibFiles.ContainsKey(relativePath)) {
this.Log().Debug("{0} not found in base package, marking as new", relativePath);
fNew++;
return;
}
var oldFilePath = baseLibFiles[relativePath];
this.Log().Debug("Delta patching {0} => {1}", oldFilePath, targetFile.FullName);
var oldData = File.ReadAllBytes(oldFilePath);
var newData = File.ReadAllBytes(targetFile.FullName);
if (bytesAreIdentical(oldData, newData)) {
// 2. exists in both, keep it the same
this.Log().Debug("{0} hasn't changed, writing dummy file", relativePath);
File.Create(targetFile.FullName + ".bsdiff").Dispose();
File.Create(targetFile.FullName + ".shasum").Dispose();
fSame++;
} else {
// 3. changed, write a delta in new
using (FileStream of = File.Create(targetFile.FullName + ".bsdiff")) {
BinaryPatchUtility.Create(oldData, newData, of);
}
var rl = ReleaseEntry.GenerateFromFile(new MemoryStream(newData), targetFile.Name + ".shasum");
File.WriteAllText(targetFile.FullName + ".shasum", rl.EntryAsString, Encoding.UTF8);
fChanged++;
}
targetFile.Delete();
baseLibFiles.Remove(relativePath);
} catch (Exception ex) {
this.Log().DebugException(String.Format("Failed to create a delta for {0}", targetFile.Name), ex);
Utility.DeleteFileOrDirectoryHard(targetFile.FullName + ".bsdiff", throwOnFailure: false);
Utility.DeleteFileOrDirectoryHard(targetFile.FullName + ".diff", throwOnFailure: false);
Utility.DeleteFileOrDirectoryHard(targetFile.FullName + ".shasum", throwOnFailure: false);
fWarnings++;
throw;
}
}
void printProcessed(int cur, int? removed = null)
{
string rem = removed.HasValue ? removed.Value.ToString("D4") : "????";
this.Log().Info($"Processed {cur.ToString("D4")}/{newLibFiles.Length.ToString("D4")} files. " +
$"{fChanged.ToString("D4")} patched, {fSame.ToString("D4")} unchanged, {fNew.ToString("D4")} new, {rem} removed");
}
printProcessed(0);
var tResult = Task.Run(() => {
Parallel.ForEach(newLibFiles, new ParallelOptions() { MaxDegreeOfParallelism = numParallel }, (f) => {
Utility.Retry(() => createDeltaForSingleFile(f, tempInfo));
});
});
int prevCount = 0;
while (!tResult.IsCompleted) {
// sleep for 2 seconds (in 100ms intervals)
for (int i = 0; i < 20 && !tResult.IsCompleted; i++)
Thread.Sleep(100);
int processed = fNew + fChanged + fSame;
if (prevCount == processed) {
// if there has been no progress, do not print another message
continue;
}
if (processed < newLibFiles.Length)
printProcessed(processed);
prevCount = processed;
}
if (tResult.Exception != null)
throw new Exception("Unable to create delta package.", tResult.Exception);
printProcessed(newLibFiles.Length, baseLibFiles.Count);
ReleasePackageBuilder.addDeltaFilesToContentTypes(tempInfo.FullName);
EasyZip.CreateZipFromDirectory(outputFile, tempInfo.FullName);
this.Log().Info(
$"Successfully created delta package for {basePackage.Version} -> {newPackage.Version}" +
(fWarnings > 0 ? $" (with {fWarnings} retries)" : "") +
".");
}
return new ReleasePackageBuilder(outputFile);
if (basePackage.Version >= newPackage.Version) {
var message = String.Format(
"Cannot create a delta package based on version {0} as it is a later or equal to the base version {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);
}
using (Utility.GetTempDirectory(out var baseTempPath))
using (Utility.GetTempDirectory(out var tempPath)) {
var baseTempInfo = new DirectoryInfo(baseTempPath);
var tempInfo = new DirectoryInfo(tempPath);
// minThreads = 1, maxThreads = 8
int numParallel = Math.Min(Math.Max(Environment.ProcessorCount - 1, 1), 8);
_logger.Info($"Creating delta for {basePackage.Version} -> {newPackage.Version} with {numParallel} parallel threads.");
_logger.Debug($"Extracting {Path.GetFileName(basePackage.ReleasePackageFile)} and {Path.GetFileName(newPackage.ReleasePackageFile)} into {tempPath}");
EasyZip.ExtractZipToDirectory(_logger, basePackage.ReleasePackageFile, baseTempInfo.FullName);
EasyZip.ExtractZipToDirectory(_logger, newPackage.ReleasePackageFile, tempInfo.FullName);
// 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");
var newLibFiles = newLibDir.GetAllFilesRecursively().ToArray();
int fNew = 0, fSame = 0, fChanged = 0, fWarnings = 0;
bool bytesAreIdentical(ReadOnlySpan<byte> a1, ReadOnlySpan<byte> a2)
{
return a1.SequenceEqual(a2);
}
void createDeltaForSingleFile(FileInfo targetFile, DirectoryInfo workingDirectory)
{
// 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
try {
var relativePath = targetFile.FullName.Replace(workingDirectory.FullName, "");
// 1. new file, leave it alone
if (!baseLibFiles.ContainsKey(relativePath)) {
_logger.Debug($"{relativePath} not found in base package, marking as new");
fNew++;
return;
}
var oldFilePath = baseLibFiles[relativePath];
_logger.Debug($"Delta patching {oldFilePath} => {targetFile.FullName}");
var oldData = File.ReadAllBytes(oldFilePath);
var newData = File.ReadAllBytes(targetFile.FullName);
if (bytesAreIdentical(oldData, newData)) {
// 2. exists in both, keep it the same
_logger.Debug($"{relativePath} hasn't changed, writing dummy file");
File.Create(targetFile.FullName + ".bsdiff").Dispose();
File.Create(targetFile.FullName + ".shasum").Dispose();
fSame++;
} else {
// 3. changed, write a delta in new
using (FileStream of = File.Create(targetFile.FullName + ".bsdiff")) {
BinaryPatchUtility.Create(oldData, newData, of);
}
var rl = ReleaseEntry.GenerateFromFile(new MemoryStream(newData), targetFile.Name + ".shasum");
File.WriteAllText(targetFile.FullName + ".shasum", rl.EntryAsString, Encoding.UTF8);
fChanged++;
}
targetFile.Delete();
baseLibFiles.Remove(relativePath);
} catch (Exception ex) {
_logger.Debug(ex, String.Format("Failed to create a delta for {0}", targetFile.Name));
Utility.DeleteFileOrDirectoryHard(targetFile.FullName + ".bsdiff", throwOnFailure: false);
Utility.DeleteFileOrDirectoryHard(targetFile.FullName + ".diff", throwOnFailure: false);
Utility.DeleteFileOrDirectoryHard(targetFile.FullName + ".shasum", throwOnFailure: false);
fWarnings++;
throw;
}
}
void printProcessed(int cur, int? removed = null)
{
string rem = removed.HasValue ? removed.Value.ToString("D4") : "????";
_logger.Info($"Processed {cur.ToString("D4")}/{newLibFiles.Length.ToString("D4")} files. " +
$"{fChanged.ToString("D4")} patched, {fSame.ToString("D4")} unchanged, {fNew.ToString("D4")} new, {rem} removed");
}
printProcessed(0);
var tResult = Task.Run(() => {
Parallel.ForEach(newLibFiles, new ParallelOptions() { MaxDegreeOfParallelism = numParallel }, (f) => {
Utility.Retry(() => createDeltaForSingleFile(f, tempInfo));
});
});
int prevCount = 0;
while (!tResult.IsCompleted) {
// sleep for 2 seconds (in 100ms intervals)
for (int i = 0; i < 20 && !tResult.IsCompleted; i++)
Thread.Sleep(100);
int processed = fNew + fChanged + fSame;
if (prevCount == processed) {
// if there has been no progress, do not print another message
continue;
}
if (processed < newLibFiles.Length)
printProcessed(processed);
prevCount = processed;
}
if (tResult.Exception != null)
throw new Exception("Unable to create delta package.", tResult.Exception);
printProcessed(newLibFiles.Length, baseLibFiles.Count);
ReleasePackageBuilder.addDeltaFilesToContentTypes(tempInfo.FullName);
EasyZip.CreateZipFromDirectory(_logger, outputFile, tempInfo.FullName);
_logger.Info(
$"Successfully created delta package for {basePackage.Version} -> {newPackage.Version}" +
(fWarnings > 0 ? $" (with {fWarnings} retries)" : "") +
".");
}
return new ReleasePackageBuilder(_logger, outputFile);
}
}

View File

@@ -1,116 +1,112 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.Versioning;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Squirrel.SimpleSplat;
using System.Reflection;
using Microsoft.Extensions.Logging;
namespace Squirrel.CommandLine
namespace Squirrel.Packaging;
public class HelperFile
{
internal class HelperFile
private static List<string> _searchPaths = new List<string>();
protected readonly ILogger Log;
static HelperFile()
{
private static List<string> _searchPaths = new List<string>();
protected static IFullLogger Log = SquirrelLocator.CurrentMutable.GetService<ILogManager>().GetLogger(typeof(HelperFile));
static HelperFile()
{
AddSearchPath(SquirrelRuntimeInfo.BaseDirectory, "wix");
AddSearchPath(SquirrelRuntimeInfo.BaseDirectory, "wix");
#if DEBUG
AddSearchPath(SquirrelRuntimeInfo.BaseDirectory, "..", "..", "..", "build", "publish");
AddSearchPath(SquirrelRuntimeInfo.BaseDirectory, "..", "..", "..", "build", "Release", "squirrel", "tools");
AddSearchPath(SquirrelRuntimeInfo.BaseDirectory, "..", "..", "..", "vendor");
AddSearchPath(SquirrelRuntimeInfo.BaseDirectory, "..", "..", "..", "vendor", "wix");
AddSearchPath(SquirrelRuntimeInfo.BaseDirectory, "..", "..", "..", "build", "publish");
AddSearchPath(SquirrelRuntimeInfo.BaseDirectory, "..", "..", "..", "build", "Release", "squirrel", "tools");
AddSearchPath(SquirrelRuntimeInfo.BaseDirectory, "..", "..", "..", "vendor");
AddSearchPath(SquirrelRuntimeInfo.BaseDirectory, "..", "..", "..", "vendor", "wix");
#endif
}
public static void AddSearchPath(params string[] pathParts)
{
AddSearchPath(Path.Combine(pathParts));
}
public static void AddSearchPath(string path)
{
if (Directory.Exists(path))
_searchPaths.Insert(0, path);
}
// protected static string FindAny(params string[] names)
// {
// var findCommand = SquirrelRuntimeInfo.IsWindows ? "where" : "which";
//
// // first search the usual places
// foreach (var n in names) {
// var helper = FindHelperFile(n, throwWhenNotFound: false);
// if (helper != null)
// return helper;
// }
//
// // then see if there is something on the path
// foreach (var n in names) {
// var result = ProcessUtil.InvokeProcess(findCommand, new[] { n }, null, CancellationToken.None);
// if (result.ExitCode == 0) {
// return n;
// }
// }
//
// throw new Exception($"Could not find any of {String.Join(", ", names)}.");
// }
protected static string FindHelperFile(string toFind, Func<string, bool> predicate = null, bool throwWhenNotFound = true)
{
var baseDirs = new[] {
AppContext.BaseDirectory,
Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location),
Environment.CurrentDirectory,
};
var files = _searchPaths
.Concat(baseDirs)
.Where(d => !String.IsNullOrEmpty(d))
.Distinct()
.Select(d => Path.Combine(d, toFind))
.Where(d => File.Exists(d))
.Select(Path.GetFullPath);
if (predicate != null)
files = files.Where(predicate);
var result = files.FirstOrDefault();
if (result == null && throwWhenNotFound)
throw new Exception($"Could not find '{toFind}'.");
return result;
}
protected static string InvokeAndThrowIfNonZero(string exePath, IEnumerable<string> args, string workingDir)
{
var result = PlatformUtil.InvokeProcess(exePath, args, workingDir, CancellationToken.None);
ProcessFailedException.ThrowIfNonZero(result);
return result.StdOutput;
}
}
public class ProcessFailedException : Exception
public HelperFile(ILogger logger)
{
public string Command { get; }
public string StdOutput { get; }
Log = logger;
}
public ProcessFailedException(string command, string stdOutput)
: base($"Command failed:\n{command}\n\nOutput was:\n{stdOutput}")
{
Command = command;
StdOutput = stdOutput;
}
public static void AddSearchPath(params string[] pathParts)
{
AddSearchPath(Path.Combine(pathParts));
}
public static void ThrowIfNonZero((int ExitCode, string StdOutput, string Command) result)
{
if (result.ExitCode != 0)
throw new ProcessFailedException(result.Command, result.StdOutput);
}
public static void AddSearchPath(string path)
{
if (Directory.Exists(path))
_searchPaths.Insert(0, path);
}
// protected static string FindAny(params string[] names)
// {
// var findCommand = SquirrelRuntimeInfo.IsWindows ? "where" : "which";
//
// // first search the usual places
// foreach (var n in names) {
// var helper = FindHelperFile(n, throwWhenNotFound: false);
// if (helper != null)
// return helper;
// }
//
// // then see if there is something on the path
// foreach (var n in names) {
// var result = ProcessUtil.InvokeProcess(findCommand, new[] { n }, null, CancellationToken.None);
// if (result.ExitCode == 0) {
// return n;
// }
// }
//
// throw new Exception($"Could not find any of {String.Join(", ", names)}.");
// }
protected static string FindHelperFile(string toFind, Func<string, bool> predicate = null, bool throwWhenNotFound = true)
{
var baseDirs = new[] {
AppContext.BaseDirectory,
Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location),
Environment.CurrentDirectory,
};
var files = _searchPaths
.Concat(baseDirs)
.Where(d => !String.IsNullOrEmpty(d))
.Distinct()
.Select(d => Path.Combine(d, toFind))
.Where(d => File.Exists(d))
.Select(Path.GetFullPath);
if (predicate != null)
files = files.Where(predicate);
var result = files.FirstOrDefault();
if (result == null && throwWhenNotFound)
throw new Exception($"Could not find '{toFind}'.");
return result;
}
protected static string InvokeAndThrowIfNonZero(string exePath, IEnumerable<string> args, string workingDir)
{
var result = PlatformUtil.InvokeProcess(exePath, args, workingDir, CancellationToken.None);
ProcessFailedException.ThrowIfNonZero(result);
return result.StdOutput;
}
}
public class ProcessFailedException : Exception
{
public string Command { get; }
public string StdOutput { get; }
public ProcessFailedException(string command, string stdOutput)
: base($"Command failed:\n{command}\n\nOutput was:\n{stdOutput}")
{
Command = command;
StdOutput = stdOutput;
}
public static void ThrowIfNonZero((int ExitCode, string StdOutput, string Command) result)
{
if (result.ExitCode != 0)
throw new ProcessFailedException(result.Command, result.StdOutput);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,26 +1,38 @@
using System;
using System.IO;
using System.Linq;
using System.Security;
using System.Threading.Tasks;
using System.Security;
using Microsoft.Extensions.Logging;
using NuGet.Commands;
using Squirrel.CommandLine.Commands;
using Squirrel.SimpleSplat;
using NG = NuGet.Common;
namespace Squirrel.CommandLine
namespace Squirrel.Packaging;
public interface INugetPackCommand
{
internal class NugetConsole : NG.ILogger, IEnableLogger
{
public static string CreateNuspec(
string packId, string packTitle, string packAuthors,
string packVersion, string releaseNotes, bool includePdb)
{
var releaseNotesText = String.IsNullOrEmpty(releaseNotes)
? "" // no releaseNotes
: $"<releaseNotes>{SecurityElement.Escape(File.ReadAllText(releaseNotes))}</releaseNotes>";
string PackId { get; }
string PackVersion { get; }
string PackDirectory { get; }
string PackAuthors { get; }
string PackTitle { get; }
bool IncludePdb { get; }
string ReleaseNotes { get; }
}
string nuspec = $@"
public class NugetConsole
{
private readonly ILogger Log;
public NugetConsole(ILogger logger)
{
Log = logger;
}
public static string CreateNuspec(
string packId, string packTitle, string packAuthors,
string packVersion, string releaseNotes, bool includePdb)
{
var releaseNotesText = String.IsNullOrEmpty(releaseNotes)
? "" // no releaseNotes
: $"<releaseNotes>{SecurityElement.Escape(File.ReadAllText(releaseNotes))}</releaseNotes>";
string nuspec = $@"
<?xml version=""1.0"" encoding=""utf-8""?>
<package>
<metadata>
@@ -37,119 +49,57 @@ namespace Squirrel.CommandLine
</package>
".Trim();
return nuspec;
}
return nuspec;
}
public static string CreatePackageFromNuspecPath(string tempDir, string packDir, string nuspecPath)
{
var nup = Path.Combine(tempDir, "squirreltemp.nuspec");
File.Copy(nuspecPath, nup);
public string CreatePackageFromNuspecPath(string tempDir, string packDir, string nuspecPath)
{
var nup = Path.Combine(tempDir, "squirreltemp.nuspec");
File.Copy(nuspecPath, nup);
new NugetConsole().Pack(nup, packDir, tempDir);
Pack(nup, packDir, tempDir);
var nupkgPath = Directory.EnumerateFiles(tempDir).Where(f => f.EndsWith(".nupkg")).FirstOrDefault();
if (nupkgPath == null)
throw new Exception($"Failed to generate nupkg, unspecified error");
var nupkgPath = Directory.EnumerateFiles(tempDir).Where(f => f.EndsWith(".nupkg")).FirstOrDefault();
if (nupkgPath == null)
throw new Exception($"Failed to generate nupkg, unspecified error");
return nupkgPath;
}
return nupkgPath;
}
public static string CreatePackageFromOptions(string tempDir, INugetPackCommand command)
{
return CreatePackageFromMetadata(tempDir, command.PackDirectory, command.PackId, command.PackTitle,
command.PackAuthors, command.PackVersion, command.ReleaseNotes, command.IncludePdb);
}
public string CreatePackageFromOptions(string tempDir, INugetPackCommand command)
{
return CreatePackageFromMetadata(tempDir, command.PackDirectory, command.PackId, command.PackTitle,
command.PackAuthors, command.PackVersion, command.ReleaseNotes, command.IncludePdb);
}
public static string CreatePackageFromMetadata(
string tempDir, string packDir, string packId, string packTitle, string packAuthors,
string packVersion, string releaseNotes, bool includePdb)
{
string nuspec = CreateNuspec(packId, packTitle, packAuthors, packVersion, releaseNotes, includePdb);
var nuspecPath = Path.Combine(tempDir, packId + ".nuspec");
File.WriteAllText(nuspecPath, nuspec);
return CreatePackageFromNuspecPath(tempDir, packDir, nuspecPath);
}
public string CreatePackageFromMetadata(
string tempDir, string packDir, string packId, string packTitle, string packAuthors,
string packVersion, string releaseNotes, bool includePdb)
{
string nuspec = CreateNuspec(packId, packTitle, packAuthors, packVersion, releaseNotes, includePdb);
var nuspecPath = Path.Combine(tempDir, packId + ".nuspec");
File.WriteAllText(nuspecPath, nuspec);
return CreatePackageFromNuspecPath(tempDir, packDir, nuspecPath);
}
public void Pack(string nuspecPath, string baseDirectory, string outputDirectory)
{
this.Log().Info($"Starting to package '{nuspecPath}'");
var args = new PackArgs() {
Deterministic = true,
BasePath = baseDirectory,
OutputDirectory = outputDirectory,
Path = nuspecPath,
Exclude = Enumerable.Empty<string>(),
Arguments = Enumerable.Empty<string>(),
Logger = this,
ExcludeEmptyDirectories = true,
NoDefaultExcludes = true,
NoPackageAnalysis = true,
};
public void Pack(string nuspecPath, string baseDirectory, string outputDirectory)
{
Log.Info($"Starting to package '{nuspecPath}'");
var args = new PackArgs() {
Deterministic = true,
BasePath = baseDirectory,
OutputDirectory = outputDirectory,
Path = nuspecPath,
Exclude = Enumerable.Empty<string>(),
Arguments = Enumerable.Empty<string>(),
Logger = new NugetLoggingWrapper(Log),
ExcludeEmptyDirectories = true,
NoDefaultExcludes = true,
NoPackageAnalysis = true,
};
var c = new PackCommandRunner(args, null);
if (!c.RunPackageBuild())
throw new Exception("Error creating nuget package.");
}
#region NuGet.Common.ILogger
public void Log(NG.LogLevel level, string data)
{
this.Log().Info(data);
}
public void Log(NG.ILogMessage message)
{
this.Log().Info(message.Message);
}
public Task LogAsync(NG.LogLevel level, string data)
{
this.Log().Info(data);
return Task.CompletedTask;
}
public Task LogAsync(NG.ILogMessage message)
{
this.Log().Info(message.Message);
return Task.CompletedTask;
}
public void LogDebug(string data)
{
this.Log().Debug(data);
}
public void LogError(string data)
{
this.Log().Error(data);
}
public void LogInformation(string data)
{
this.Log().Info(data);
}
public void LogInformationSummary(string data)
{
this.Log().Info(data);
}
public void LogMinimal(string data)
{
this.Log().Info(data);
}
public void LogVerbose(string data)
{
this.Log().Debug(data);
}
public void LogWarning(string data)
{
this.Log().Warn(data);
}
#endregion NuGet.Common.ILogger
var c = new PackCommandRunner(args, null);
if (!c.RunPackageBuild())
throw new Exception("Error creating nuget package.");
}
}

View File

@@ -0,0 +1,86 @@
using Microsoft.Extensions.Logging;
using INugetLogger = NuGet.Common.ILogger;
using NugetLogLevel = NuGet.Common.LogLevel;
using INugetLogMessage = NuGet.Common.ILogMessage;
namespace Squirrel.Packaging;
public class NugetLoggingWrapper : INugetLogger
{
private readonly ILogger _logger;
public NugetLoggingWrapper(ILogger logger)
{
_logger = logger;
}
private LogLevel ToMsLevel(NugetLogLevel level)
{
return level switch {
NugetLogLevel.Debug => LogLevel.Debug,
NugetLogLevel.Error => LogLevel.Error,
NugetLogLevel.Information => LogLevel.Information,
NugetLogLevel.Minimal => LogLevel.Information,
NugetLogLevel.Verbose => LogLevel.Information,
NugetLogLevel.Warning => LogLevel.Warning,
_ => LogLevel.Information,
};
}
public void Log(NugetLogLevel level, string data)
{
_logger.Log(ToMsLevel(level), data);
}
public void Log(INugetLogMessage message)
{
_logger.Log(ToMsLevel(message.Level), message.Message);
}
public Task LogAsync(NugetLogLevel level, string data)
{
_logger.Log(ToMsLevel(level), data);
return Task.CompletedTask;
}
public Task LogAsync(INugetLogMessage message)
{
_logger.Log(ToMsLevel(message.Level), message.Message);
return Task.CompletedTask;
}
public void LogDebug(string data)
{
_logger.LogDebug(data);
}
public void LogError(string data)
{
_logger.LogError(data);
}
public void LogInformation(string data)
{
_logger.LogInformation(data);
}
public void LogInformationSummary(string data)
{
_logger.LogInformation(data);
}
public void LogMinimal(string data)
{
_logger.LogInformation(data);
}
public void LogVerbose(string data)
{
_logger.LogInformation(data);
}
public void LogWarning(string data)
{
_logger.LogWarning(data);
}
}

View File

@@ -1,221 +1,217 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.IO.Compression;
using System.Text;
using System.Threading.Tasks;
using System.Xml;
using Microsoft.Extensions.Logging;
using NuGet.Versioning;
using Squirrel.MarkdownSharp;
using Squirrel.Compression;
using Squirrel.NuGet;
using Squirrel.SimpleSplat;
namespace Squirrel.CommandLine
namespace Squirrel.Packaging;
public interface IReleasePackage
{
internal interface IReleasePackage
string InputPackageFile { get; }
string ReleasePackageFile { get; }
SemanticVersion Version { get; }
}
public class ReleasePackageBuilder : IReleasePackage
{
private Lazy<ZipPackage> _package;
private readonly ILogger _logger;
public ReleasePackageBuilder(ILogger logger, string inputPackageFile, bool isReleasePackage = false)
{
string InputPackageFile { get; }
string ReleasePackageFile { get; }
SemanticVersion Version { get; }
_logger = logger;
InputPackageFile = inputPackageFile;
_package = new Lazy<ZipPackage>(() => new ZipPackage(inputPackageFile));
if (isReleasePackage) {
ReleasePackageFile = inputPackageFile;
}
}
internal class ReleasePackageBuilder : IEnableLogger, IReleasePackage
public string InputPackageFile { get; protected set; }
public string ReleasePackageFile { get; protected set; }
public string Id => ReleaseEntry.ParseEntryFileName(InputPackageFile).PackageName;
public SemanticVersion Version => ReleaseEntry.ParseEntryFileName(InputPackageFile).Version;
public string CreateReleasePackage(string outputFile, Func<string, string> releaseNotesProcessor = null, Action<string, ZipPackage> contentsPostProcessHook = null)
{
private Lazy<ZipPackage> _package;
return CreateReleasePackage((i, p) => {
contentsPostProcessHook?.Invoke(i, p);
return outputFile;
}, releaseNotesProcessor);
}
public ReleasePackageBuilder(string inputPackageFile, bool isReleasePackage = false)
{
InputPackageFile = inputPackageFile;
_package = new Lazy<ZipPackage>(() => new ZipPackage(inputPackageFile));
public string CreateReleasePackage(Func<string, ZipPackage, string> contentsPostProcessHook, Func<string, string> releaseNotesProcessor = null)
{
releaseNotesProcessor = releaseNotesProcessor ?? (x => (new Markdown()).Transform(x));
if (isReleasePackage) {
ReleasePackageFile = inputPackageFile;
}
if (ReleasePackageFile != null) {
return ReleasePackageFile;
}
public string InputPackageFile { get; protected set; }
var package = _package.Value;
public string ReleasePackageFile { get; protected set; }
// just in-case our parsing is more-strict than nuget.exe and
// the 'releasify' command was used instead of 'pack'.
NugetUtil.ThrowIfInvalidNugetId(package.Id);
public string Id => ReleaseEntry.ParseEntryFileName(InputPackageFile).PackageName;
// 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.Frameworks;
if (frameworks.Count() > 1) {
var platforms = frameworks
.Aggregate(new StringBuilder(), (sb, f) => sb.Append(f.ToString() + "; "));
public SemanticVersion Version => ReleaseEntry.ParseEntryFileName(InputPackageFile).Version;
throw new InvalidOperationException(String.Format(
"The input package file {0} targets multiple platforms - {1} - and cannot be transformed into a release package.", InputPackageFile, platforms));
internal string CreateReleasePackage(string outputFile, Func<string, string> releaseNotesProcessor = null, Action<string, ZipPackage> contentsPostProcessHook = null)
{
return CreateReleasePackage((i, p) => {
contentsPostProcessHook?.Invoke(i, p);
return outputFile;
}, releaseNotesProcessor);
} 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));
}
internal string CreateReleasePackage(Func<string, ZipPackage, string> contentsPostProcessHook, Func<string, string> releaseNotesProcessor = null)
{
releaseNotesProcessor = releaseNotesProcessor ?? (x => (new Markdown()).Transform(x));
// 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));
}
if (ReleasePackageFile != null) {
return ReleasePackageFile;
_logger.Info($"Creating release from input package {InputPackageFile}");
using (Utility.GetTempDirectory(out var tempPath)) {
var tempDir = new DirectoryInfo(tempPath);
extractZipWithEscaping(InputPackageFile, tempPath).Wait();
var specPath = tempDir.GetFiles("*.nuspec").First().FullName;
_logger.Info("Removing unnecessary data");
removeDependenciesFromPackageSpec(specPath);
if (releaseNotesProcessor != null) {
renderReleaseNotesMarkdown(specPath, releaseNotesProcessor);
}
var package = _package.Value;
addDeltaFilesToContentTypes(tempDir.FullName);
// just in-case our parsing is more-strict than nuget.exe and
// the 'releasify' command was used instead of 'pack'.
NugetUtil.ThrowIfInvalidNugetId(package.Id);
var outputFile = contentsPostProcessHook.Invoke(tempPath, package);
// 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.Frameworks;
if (frameworks.Count() > 1) {
var platforms = frameworks
.Aggregate(new StringBuilder(), (sb, f) => sb.Append(f.ToString() + "; "));
EasyZip.CreateZipFromDirectory(_logger, outputFile, tempPath);
throw new InvalidOperationException(String.Format(
"The input package file {0} targets multiple platforms - {1} - and cannot be transformed into a release package.", InputPackageFile, platforms));
ReleasePackageFile = outputFile;
} 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));
}
_logger.Info($"Package created at {outputFile}");
return ReleasePackageFile;
}
}
// 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));
}
public static string GetSuggestedFileName(string id, string version, string runtime, bool delta = false)
{
var tail = delta ? "delta" : "full";
if (String.IsNullOrEmpty(runtime)) {
return String.Format("{0}-{1}-{2}.nupkg", id, version, tail);
} else {
return String.Format("{0}-{1}-{2}-{3}.nupkg", id, version, runtime, tail);
}
}
this.Log().Info("Creating release from input package {0}", InputPackageFile);
/// <summary>
/// Given a list of releases and a specified release package, returns the release package
/// directly previous to the specified version.
/// </summary>
public static ReleasePackageBuilder GetPreviousRelease(ILogger logger, IEnumerable<ReleaseEntry> releaseEntries, IReleasePackage package, string targetDir, RID compatibleRid)
{
if (releaseEntries == null || !releaseEntries.Any()) return null;
return Utility.FindCompatibleVersions(releaseEntries, compatibleRid)
.Where(x => x.IsDelta == false)
.Where(x => x.Version < package.Version)
.OrderByDescending(x => x.Version)
.Select(x => new ReleasePackageBuilder(logger, Path.Combine(targetDir, x.Filename), true))
.FirstOrDefault();
}
using (Utility.GetTempDirectory(out var tempPath)) {
var tempDir = new DirectoryInfo(tempPath);
static Task extractZipWithEscaping(string zipFilePath, string outFolder)
{
return Task.Run(() => {
using (var fs = File.OpenRead(zipFilePath))
using (var za = new ZipArchive(fs))
foreach (var entry in za.Entries) {
var parts = entry.FullName.Split('\\', '/').Select(x => Uri.UnescapeDataString(x));
var decoded = String.Join(Path.DirectorySeparatorChar.ToString(), parts);
extractZipWithEscaping(InputPackageFile, tempPath).Wait();
var fullTargetFile = Path.Combine(outFolder, decoded);
var fullTargetDir = Path.GetDirectoryName(fullTargetFile);
Directory.CreateDirectory(fullTargetDir);
var isDirectory = entry.IsDirectory();
var specPath = tempDir.GetFiles("*.nuspec").First().FullName;
this.Log().Info("Removing unnecessary data");
removeDependenciesFromPackageSpec(specPath);
if (releaseNotesProcessor != null) {
renderReleaseNotesMarkdown(specPath, releaseNotesProcessor);
Utility.Retry(() => {
if (isDirectory) {
Directory.CreateDirectory(fullTargetFile);
} else {
entry.ExtractToFile(fullTargetFile, true);
}
}, 5);
}
});
}
addDeltaFilesToContentTypes(tempDir.FullName);
void renderReleaseNotesMarkdown(string specPath, Func<string, string> releaseNotesProcessor)
{
var doc = new XmlDocument();
doc.Load(specPath);
var outputFile = contentsPostProcessHook.Invoke(tempPath, package);
var metadata = doc.DocumentElement.ChildNodes
.OfType<XmlElement>()
.First(x => x.Name.ToLowerInvariant() == "metadata");
EasyZip.CreateZipFromDirectory(outputFile, tempPath);
var releaseNotes = metadata.ChildNodes
.OfType<XmlElement>()
.FirstOrDefault(x => x.Name.ToLowerInvariant() == "releasenotes");
ReleasePackageFile = outputFile;
this.Log().Info("Package created at {0}", outputFile);
return ReleasePackageFile;
}
if (releaseNotes == null || String.IsNullOrWhiteSpace(releaseNotes.InnerText)) {
_logger.Info($"No release notes found in {specPath}");
return;
}
internal static string GetSuggestedFileName(string id, string version, string runtime, bool delta = false)
{
var tail = delta ? "delta" : "full";
if (String.IsNullOrEmpty(runtime)) {
return String.Format("{0}-{1}-{2}.nupkg", id, version, tail);
} else {
return String.Format("{0}-{1}-{2}-{3}.nupkg", id, version, runtime, tail);
}
var releaseNotesHtml = doc.CreateElement("releaseNotesHtml");
releaseNotesHtml.InnerText = String.Format("<![CDATA[\n" + "{0}\n" + "]]>",
releaseNotesProcessor(releaseNotes.InnerText));
metadata.AppendChild(releaseNotesHtml);
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);
}
/// <summary>
/// Given a list of releases and a specified release package, returns the release package
/// directly previous to the specified version.
/// </summary>
internal static ReleasePackageBuilder GetPreviousRelease(IEnumerable<ReleaseEntry> releaseEntries, IReleasePackage package, string targetDir, RID compatibleRid)
{
if (releaseEntries == null || !releaseEntries.Any()) return null;
return Utility.FindCompatibleVersions(releaseEntries, compatibleRid)
.Where(x => x.IsDelta == false)
.Where(x => x.Version < package.Version)
.OrderByDescending(x => x.Version)
.Select(x => new ReleasePackageBuilder(Path.Combine(targetDir, x.Filename), true))
.FirstOrDefault();
}
xdoc.Save(specPath);
}
static Task extractZipWithEscaping(string zipFilePath, string outFolder)
{
return Task.Run(() => {
using (var fs = File.OpenRead(zipFilePath))
using (var za = new ZipArchive(fs))
foreach (var entry in za.Entries) {
var parts = entry.FullName.Split('\\', '/').Select(x => Uri.UnescapeDataString(x));
var decoded = String.Join(Path.DirectorySeparatorChar.ToString(), parts);
static internal void addDeltaFilesToContentTypes(string rootDirectory)
{
var doc = new XmlDocument();
var path = Path.Combine(rootDirectory, ContentType.ContentTypeFileName);
doc.Load(path);
var fullTargetFile = Path.Combine(outFolder, decoded);
var fullTargetDir = Path.GetDirectoryName(fullTargetFile);
Directory.CreateDirectory(fullTargetDir);
var isDirectory = entry.IsDirectory();
ContentType.Merge(doc);
ContentType.Clean(doc);
Utility.Retry(() => {
if (isDirectory) {
Directory.CreateDirectory(fullTargetFile);
} else {
entry.ExtractToFile(fullTargetFile, true);
}
}, 5);
}
});
}
void renderReleaseNotesMarkdown(string specPath, Func<string, string> releaseNotesProcessor)
{
var doc = new XmlDocument();
doc.Load(specPath);
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 || String.IsNullOrWhiteSpace(releaseNotes.InnerText)) {
this.Log().Info("No release notes found in {0}", specPath);
return;
}
var releaseNotesHtml = doc.CreateElement("releaseNotesHtml");
releaseNotesHtml.InnerText = String.Format("<![CDATA[\n" + "{0}\n" + "]]>",
releaseNotesProcessor(releaseNotes.InnerText));
metadata.AppendChild(releaseNotesHtml);
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, ContentType.ContentTypeFileName);
doc.Load(path);
ContentType.Merge(doc);
ContentType.Clean(doc);
using (var sw = new StreamWriter(path, false, Encoding.UTF8)) {
doc.Save(sw);
}
using (var sw = new StreamWriter(path, false, Encoding.UTF8)) {
doc.Save(sw);
}
}
}

View File

@@ -2,9 +2,7 @@
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<OutputType>Exe</OutputType>
<UseAppHost>false</UseAppHost>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<ImplicitUsings>enable</ImplicitUsings>
<NoWarn>$(NoWarn);CA2007;CS8002</NoWarn>
</PropertyGroup>
@@ -13,17 +11,8 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="AWSSDK.S3" Version="3.7.205.22" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="Octokit" Version="9.0.0" />
<PackageReference Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
<PackageReference Include="System.IO" Version="4.3.0" />
<PackageReference Include="System.Net.Http" Version="4.3.4" />
<PackageReference Include="System.Security.Cryptography.Algorithms" Version="4.3.1" />
<PackageReference Include="System.Linq.Async" Version="6.0.1" />
<PackageReference Include="NuGet.Commands" Version="6.7.0" />
<PackageReference Include="System.Drawing.Common" Version="7.0.0" />
<PackageReference Include="PeNet" Version="4.0.2" />
</ItemGroup>
</Project>

View File

@@ -5,8 +5,14 @@ using System.Collections.Generic;
namespace Squirrel
{
/// <summary>
/// Useful enumerable extensions used by Squirrel
/// </summary>
internal static class EnumerableExtensions
{
/// <summary>
/// Turn a single value into an IEnumerable of that value.
/// </summary>
public static IEnumerable<T> Return<T>(T value)
{
yield return value;

View File

@@ -4,8 +4,52 @@ using Microsoft.Extensions.Logging;
namespace Squirrel
{
public static class LoggerExtensions
internal static class LoggerExtensions
{
public static void Trace(this ILogger logger, string message)
{
logger.LogTrace(message);
}
public static void Trace(this ILogger logger, Exception ex, string message)
{
logger.LogTrace(ex, message);
}
public static void Trace(this ILogger logger, Exception ex)
{
logger.LogTrace(ex, ex.Message);
}
public static void Debug(this ILogger logger, string message)
{
logger.LogDebug(message);
}
public static void Debug(this ILogger logger, Exception ex, string message)
{
logger.LogDebug(ex, message);
}
public static void Debug(this ILogger logger, Exception ex)
{
logger.LogDebug(ex, ex.Message);
}
public static void Info(this ILogger logger, string message)
{
logger.LogInformation(message);
}
public static void Info(this ILogger logger, Exception ex, string message)
{
logger.LogInformation(ex, message);
}
public static void Info(this ILogger logger, Exception ex)
{
logger.LogInformation(ex, ex.Message);
}
public static void Warn(this ILogger logger, string message)
{
logger.LogWarning(message);
@@ -21,11 +65,6 @@ namespace Squirrel
logger.LogWarning(ex, ex.Message);
}
public static void Info(this ILogger logger, string message)
{
logger.LogInformation(message);
}
public static void Error(this ILogger logger, string message)
{
logger.LogError(message);
@@ -40,15 +79,5 @@ namespace Squirrel
{
logger.LogError(ex, ex.Message);
}
public static void Debug(this ILogger logger, string message)
{
logger.LogDebug(message);
}
public static void Trace(this ILogger logger, string message)
{
logger.LogTrace(message);
}
}
}

View File

@@ -1,4 +1,5 @@
using System;
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
@@ -7,7 +8,7 @@ using NuGet.Versioning;
namespace Squirrel.NuGet
{
internal interface IPackage
public interface IPackage
{
string Id { get; }
string ProductName { get; }
@@ -26,7 +27,7 @@ namespace Squirrel.NuGet
IEnumerable<string> RuntimeDependencies { get; }
}
internal class NuspecManifest : IPackage
public class NuspecManifest : IPackage
{
public string ProductName => Title ?? Id;
public string ProductDescription => Description ?? Summary ?? Title ?? Id;

View File

@@ -1,4 +1,5 @@
using System;
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Globalization;
@@ -6,12 +7,12 @@ using System.Linq;
namespace Squirrel.NuGet
{
internal interface IFrameworkTargetable
public interface IFrameworkTargetable
{
IEnumerable<string> SupportedFrameworks { get; }
}
internal class PackageDependency
public class PackageDependency
{
public PackageDependency(string id)
: this(id, versionSpec: null)
@@ -47,7 +48,7 @@ namespace Squirrel.NuGet
}
}
internal class PackageDependencySet : IFrameworkTargetable
public class PackageDependencySet : IFrameworkTargetable
{
private readonly string _targetFramework;
private readonly ReadOnlyCollection<PackageDependency> _dependencies;
@@ -85,7 +86,7 @@ namespace Squirrel.NuGet
}
}
internal class FrameworkAssemblyReference : IFrameworkTargetable
public class FrameworkAssemblyReference : IFrameworkTargetable
{
public FrameworkAssemblyReference(string assemblyName)
: this(assemblyName, Enumerable.Empty<string>())

View File

@@ -1,4 +1,5 @@
using System;
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
@@ -10,13 +11,13 @@ using Microsoft.Extensions.Logging;
namespace Squirrel.NuGet
{
internal interface IZipPackage : IPackage
public interface IZipPackage : IPackage
{
IEnumerable<string> Frameworks { get; }
IEnumerable<ZipPackageFile> Files { get; }
}
internal class ZipPackage : NuspecManifest, IZipPackage
public class ZipPackage : NuspecManifest, IZipPackage
{
public IEnumerable<string> Frameworks { get; private set; } = Enumerable.Empty<string>();
public IEnumerable<ZipPackageFile> Files { get; private set; } = Enumerable.Empty<ZipPackageFile>();

View File

@@ -1,11 +1,12 @@
using System;
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
namespace Squirrel.NuGet
{
internal interface IPackageFile : IFrameworkTargetable
public interface IPackageFile : IFrameworkTargetable
{
Uri Key { get; }
string Path { get; }
@@ -16,7 +17,7 @@ namespace Squirrel.NuGet
//Stream GetEntryStream(Stream archiveStream);
}
internal class ZipPackageFile : IPackageFile, IEquatable<ZipPackageFile>
public class ZipPackageFile : IPackageFile, IEquatable<ZipPackageFile>
{
public Uri Key { get; }
public string EffectivePath { get; }

View File

@@ -0,0 +1,16 @@
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
[assembly: ComVisible(false)]
[assembly: InternalsVisibleTo("Squirrel.Tests, PublicKey=" + SNK.SHA1)]
[assembly: InternalsVisibleTo("Squirrel, PublicKey=" + SNK.SHA1)]
[assembly: InternalsVisibleTo("Squirrel.Deployment, PublicKey=" + SNK.SHA1)]
[assembly: InternalsVisibleTo("Squirrel.Packaging, PublicKey=" + SNK.SHA1)]
[assembly: InternalsVisibleTo("Squirrel.Packaging.Windows, PublicKey=" + SNK.SHA1)]
[assembly: InternalsVisibleTo("Squirrel.Packaging.OSX, PublicKey=" + SNK.SHA1)]
[assembly: InternalsVisibleTo("csq, PublicKey=" + SNK.SHA1)]
internal static class SNK
{
public const string SHA1 = "002400000480000094000000060200000024000052534131000400000100010061b199572531d267773d7783a077bc020aacb34a10d8c11407505a4a814284d4c953df3229ccf8f63d1a410a3395b7266e5e5cba8f1c0bc9ee10fc7ddafdae297431e2eef82eccd2ac8957bfc9119063f4a965d6ae3ccf53e1f4d8e9ce894a79ea1f681eb2067745c5253f6747cbc51eec640dd2edb4a67339b44f093e3ec5b0";
}