mirror of
https://github.com/velopack/velopack.git
synced 2025-10-25 15:19:22 +00:00
Fixing build errors
This commit is contained in:
@@ -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; }
|
||||
}
|
||||
@@ -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; }
|
||||
|
||||
|
||||
@@ -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;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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; });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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; }
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
262
src/Squirrel.Packaging.OSX/OsxCommands.cs
Normal file
262
src/Squirrel.Packaging.OSX/OsxCommands.cs
Normal 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.");
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
//}
|
||||
}
|
||||
1
src/Squirrel.Packaging.OSX/Properties/AssemblyInfo.cs
Normal file
1
src/Squirrel.Packaging.OSX/Properties/AssemblyInfo.cs
Normal file
@@ -0,0 +1 @@
|
||||
[assembly: System.Runtime.Versioning.SupportedOSPlatform("osx")]
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
283
src/Squirrel.Packaging.Windows/WindowsCommands.cs
Normal file
283
src/Squirrel.Packaging.Windows/WindowsCommands.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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.");
|
||||
}
|
||||
}
|
||||
86
src/Squirrel.Packaging/NugetLoggingWrapper.cs
Normal file
86
src/Squirrel.Packaging/NugetLoggingWrapper.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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>())
|
||||
|
||||
@@ -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>();
|
||||
|
||||
@@ -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; }
|
||||
|
||||
16
src/Squirrel/Properties/AssemblyInfo.cs
Normal file
16
src/Squirrel/Properties/AssemblyInfo.cs
Normal 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";
|
||||
}
|
||||
Reference in New Issue
Block a user