diff --git a/src/Squirrel.Csq/Commands/BaseCommand.cs b/src/Squirrel.Csq/Commands/BaseCommand.cs index 2452539d..b510a305 100644 --- a/src/Squirrel.Csq/Commands/BaseCommand.cs +++ b/src/Squirrel.Csq/Commands/BaseCommand.cs @@ -8,7 +8,6 @@ public class BaseCommand : CliCommand protected CliOption ReleaseDirectoryOption { get; private set; } - //protected static IFullLogger Log = SquirrelLocator.CurrentMutable.GetService().GetLogger(typeof(BaseCommand)); private Dictionary> _setters = new(); protected BaseCommand(string name, string description) @@ -29,7 +28,7 @@ public class BaseCommand : CliCommand protected virtual CliOption AddOption(Action setValue, params string[] aliases) { - return AddOption(setValue, new CliOption(aliases)); + return AddOption(setValue, new CliOption(aliases.OrderBy(a => a.Length).First(), aliases)); } protected virtual CliOption AddOption(Action setValue, CliOption 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; } } \ No newline at end of file diff --git a/src/Squirrel.Csq/Commands/GitHubCommands.cs b/src/Squirrel.Csq/Commands/GitHubCommands.cs index 046e5fd9..f9458416 100644 --- a/src/Squirrel.Csq/Commands/GitHubCommands.cs +++ b/src/Squirrel.Csq/Commands/GitHubCommands.cs @@ -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; } diff --git a/src/Squirrel.Csq/Compatibility/oldProgram.cs b/src/Squirrel.Csq/Compatibility/oldProgram.cs index aa9597c9..e2eae054 100644 --- a/src/Squirrel.Csq/Compatibility/oldProgram.cs +++ b/src/Squirrel.Csq/Compatibility/oldProgram.cs @@ -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 CsqVersion { get; } - = new CliOption("--csq-version"); - private static CliOption CsqSolutionPath { get; } - = new CliOption(new[] { "--csq-sln", "--csq-solution" }).ExistingOnly(); - private static CliOption Verbose { get; } - = new CliOption("--verbose"); +// private static CliOption CsqVersion { get; } +// = new CliOption("--csq-version"); +// private static CliOption CsqSolutionPath { get; } +// = new CliOption(new[] { "--csq-sln", "--csq-solution" }).ExistingOnly(); +// private static CliOption Verbose { get; } +// = new CliOption("--verbose"); - static Task Main(string[] args) - { +// static Task 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[] 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[] 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 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 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 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 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 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 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 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 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; - } - } - } - } - } -} \ No newline at end of file +// if (currentDepth < maxDepth) { +// foreach (var dir in Directory.EnumerateDirectories(rootPath)) { +// foreach (var file in EnumerateFilesUntilSpecificDepth(dir, searchPattern, maxDepth, currentDepth + 1)) { +// yield return file; +// } +// } +// } +// } +// } +//} \ No newline at end of file diff --git a/src/Squirrel.Csq/Program.cs b/src/Squirrel.Csq/Program.cs index c89c0358..a53a7a19 100644 --- a/src/Squirrel.Csq/Program.cs +++ b/src/Squirrel.Csq/Program.cs @@ -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 Main(string[] args) + public static CliOption TargetRuntime { get; } + = new CliOption("runtime", "-r", "--runtime", "The target runtime to build packages for.") + .SetArgumentHelpName("RID") + .MustBeSupportedRid() + .SetRequired(); + + public static CliOption VerboseOption { get; } + = new CliOption("--verbose", "Print diagnostic messages."); + + public static Task 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>(); + 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(CliCommand parent, T command, Action 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(CliCommand parent, T command, Func 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; } } diff --git a/src/Squirrel.Csq/Squirrel.Csq.csproj b/src/Squirrel.Csq/Squirrel.Csq.csproj index 4fc0be52..f9f98038 100644 --- a/src/Squirrel.Csq/Squirrel.Csq.csproj +++ b/src/Squirrel.Csq/Squirrel.Csq.csproj @@ -29,4 +29,10 @@ + + + + + + diff --git a/src/Squirrel.Deployment/GitHubRepository.cs b/src/Squirrel.Deployment/GitHubRepository.cs index 63e7ac81..22cf8f91 100644 --- a/src/Squirrel.Deployment/GitHubRepository.cs +++ b/src/Squirrel.Deployment/GitHubRepository.cs @@ -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); diff --git a/src/Squirrel.Deployment/S3Repository.cs b/src/Squirrel.Deployment/S3Repository.cs index 29ed4862..c78c51e3 100644 --- a/src/Squirrel.Deployment/S3Repository.cs +++ b/src/Squirrel.Deployment/S3Repository.cs @@ -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().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 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 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 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 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); + } } \ No newline at end of file diff --git a/src/Squirrel.Deployment/SimpleWebRepository.cs b/src/Squirrel.Deployment/SimpleWebRepository.cs index 367dd1cc..9e855ab1 100644 --- a/src/Squirrel.Deployment/SimpleWebRepository.cs +++ b/src/Squirrel.Deployment/SimpleWebRepository.cs @@ -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 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 retryAsync(int count, Func> 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 block) - { - await retryAsync(count, async () => { await block(); return false; }); + foreach (var releaseToDownload in releasesToDownload) { + await retryAsync(3, () => downloadRelease(releaseToDownload.LocalPath, releaseToDownload.RemoteUrl)); } } + + async Task 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 retryAsync(int count, Func> 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 block) + { + await retryAsync(count, async () => { await block(); return false; }); + } } diff --git a/src/Squirrel.Deployment/Squirrel.Deployment.csproj b/src/Squirrel.Deployment/Squirrel.Deployment.csproj index 73c8132d..5583b6b1 100644 --- a/src/Squirrel.Deployment/Squirrel.Deployment.csproj +++ b/src/Squirrel.Deployment/Squirrel.Deployment.csproj @@ -3,24 +3,18 @@ net6.0 enable + $(NoWarn);CA2007;CS8002 - - - - - - - - - - - - - - + + + + + + + diff --git a/src/Squirrel.Packaging.OSX/AppInfo.cs b/src/Squirrel.Packaging.OSX/AppInfo.cs index 435f1ea2..73eef9dd 100644 --- a/src/Squirrel.Packaging.OSX/AppInfo.cs +++ b/src/Squirrel.Packaging.OSX/AppInfo.cs @@ -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; } - } } diff --git a/src/Squirrel.Packaging.OSX/Commands.cs b/src/Squirrel.Packaging.OSX/Commands.cs deleted file mode 100644 index 0f35ad08..00000000 --- a/src/Squirrel.Packaging.OSX/Commands.cs +++ /dev/null @@ -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().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(); - - 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 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."); - } - } -} \ No newline at end of file diff --git a/src/Squirrel.Packaging.OSX/HelperExe.cs b/src/Squirrel.Packaging.OSX/HelperExe.cs index 183ccdf0..87323937 100644 --- a/src/Squirrel.Packaging.OSX/HelperExe.cs +++ b/src/Squirrel.Packaging.OSX/HelperExe.cs @@ -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 { + "-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 { + "--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 { + "--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> 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, $"{SecurityElement.Escape(appTitle)}"); + + // disable local system installation (install to home dir) + distXml.Insert(2, ""); + + // 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 { - "-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 { - "--assess", - "-vvvv", - filePath - }; + File.WriteAllLines(distributionPath, distXml); - Log.Info($"Verifying signature/notarization for code using spctl..."); - Console.WriteLine(InvokeAndThrowIfNonZero("spctl", args2, null)); + List 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 { - "--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> 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 { + "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(ntresultjson.StdOutput); + if (ntresult?.status != "Accepted" || ntresultjson.ExitCode != 0) { + if (ntresult?.id != null) { + var logargs = new List { + "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, $"{SecurityElement.Escape(appTitle)}"); - - // disable local system installation (install to home dir) - distXml.Insert(2, ""); - - // 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 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 { - "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(ntresultjson.StdOutput); - if (ntresult?.status != "Accepted" || ntresultjson.ExitCode != 0) { - if (ntresult?.id != null) { - var logargs = new List { - "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 { - "-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 { + "-c", + "-k", + "--rsrc", + "--keepParent", + "--sequesterRsrc", + folder, + outputZip + }; + + Log.Info($"Creating ditto bundle '{outputZip}'"); + InvokeAndThrowIfNonZero("ditto", args, null); } } \ No newline at end of file diff --git a/src/Squirrel.Packaging.OSX/OsxCommands.cs b/src/Squirrel.Packaging.OSX/OsxCommands.cs new file mode 100644 index 00000000..24e20652 --- /dev/null +++ b/src/Squirrel.Packaging.OSX/OsxCommands.cs @@ -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(); + + 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 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."); + } +} \ No newline at end of file diff --git a/src/Squirrel.Packaging.OSX/PListParser.cs b/src/Squirrel.Packaging.OSX/PListParser.cs index 7cd24442..910df369 100644 --- a/src/Squirrel.Packaging.OSX/PListParser.cs +++ b/src/Squirrel.Packaging.OSX/PListParser.cs @@ -116,7 +116,7 @@ using System.Text.RegularExpressions; using System.Xml; // ASCIIPropertyListParser.cs -namespace Squirrel.PropertyList +namespace Squirrel.Packaging.OSX { /// /// @@ -900,7 +900,7 @@ namespace Squirrel.PropertyList } // BinaryPropertyListParser.cs -namespace Squirrel.PropertyList +namespace Squirrel.Packaging.OSX { /// /// @@ -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 { /// /// A BinaryPropertyListWriter is a helper class for writing out binary property list files. @@ -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 { /// Represents an Array. /// @author Daniel Dreibrodt @@ -2273,7 +2273,7 @@ namespace Squirrel.PropertyList } // NSArray.IList.cs -namespace Squirrel.PropertyList +namespace Squirrel.Packaging.OSX { partial class NSArray : IList { @@ -2331,7 +2331,7 @@ namespace Squirrel.PropertyList } // NSData.cs -namespace Squirrel.PropertyList +namespace Squirrel.Packaging.OSX { /// NSData objects are wrappers for byte buffers /// @author Daniel Dreibrodt @@ -2488,7 +2488,7 @@ namespace Squirrel.PropertyList } // NSDate.cs -namespace Squirrel.PropertyList +namespace Squirrel.Packaging.OSX { /// Represents a date /// @author Daniel Dreibrodt @@ -2638,7 +2638,7 @@ namespace Squirrel.PropertyList } // NSDictionary.cs -namespace Squirrel.PropertyList +namespace Squirrel.Packaging.OSX { /// /// @@ -3164,7 +3164,7 @@ namespace Squirrel.PropertyList } // NSNumber.cs -namespace Squirrel.PropertyList +namespace Squirrel.Packaging.OSX { /// A number whose value is either an integer, a real number or bool. /// @author Daniel Dreibrodt @@ -3682,7 +3682,7 @@ namespace Squirrel.PropertyList } // NSObject.cs -namespace Squirrel.PropertyList +namespace Squirrel.Packaging.OSX { /// /// Abstract interface for any object contained in a property list. @@ -4099,7 +4099,7 @@ namespace Squirrel.PropertyList } // NSSet.cs -namespace Squirrel.PropertyList +namespace Squirrel.Packaging.OSX { /// /// A set is an interface to an unordered collection of objects. @@ -4472,7 +4472,7 @@ namespace Squirrel.PropertyList } // NSString.cs -namespace Squirrel.PropertyList +namespace Squirrel.Packaging.OSX { /// A NSString contains a string. /// @author Daniel Dreibrodt @@ -4717,7 +4717,7 @@ namespace Squirrel.PropertyList } // PropertyListException.cs -namespace Squirrel.PropertyList +namespace Squirrel.Packaging.OSX { /// The exception that is thrown when an property list file could not be processed correctly. [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 { /// /// 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 { /// /// 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 { /// An UID. Only found in binary property lists that are keyed archives. /// @author Daniel Dreibrodt @@ -5341,7 +5341,7 @@ namespace Squirrel.PropertyList } // XmlPropertyListParser.cs -namespace Squirrel.PropertyList +namespace Squirrel.Packaging.OSX { /// Parses XML property lists. /// @author Daniel Dreibrodt diff --git a/src/Squirrel.Packaging.OSX/PlistWriter.cs b/src/Squirrel.Packaging.OSX/PlistWriter.cs index 57c0a525..781a6c34 100644 --- a/src/Squirrel.Packaging.OSX/PlistWriter.cs +++ b/src/Squirrel.Packaging.OSX/PlistWriter.cs @@ -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( + ""); + 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( - ""); - 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 + //} } \ No newline at end of file diff --git a/src/Squirrel.Packaging.OSX/Properties/AssemblyInfo.cs b/src/Squirrel.Packaging.OSX/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..4a61d179 --- /dev/null +++ b/src/Squirrel.Packaging.OSX/Properties/AssemblyInfo.cs @@ -0,0 +1 @@ +[assembly: System.Runtime.Versioning.SupportedOSPlatform("osx")] \ No newline at end of file diff --git a/src/Squirrel.Packaging.OSX/Squirrel.Packaging.OSX.csproj b/src/Squirrel.Packaging.OSX/Squirrel.Packaging.OSX.csproj index d215c71f..a0085eb2 100644 --- a/src/Squirrel.Packaging.OSX/Squirrel.Packaging.OSX.csproj +++ b/src/Squirrel.Packaging.OSX/Squirrel.Packaging.OSX.csproj @@ -1,8 +1,13 @@ - + net6.0 enable + $(NoWarn);CA2007;CS8002 + + + + diff --git a/src/Squirrel.Packaging.OSX/StructureBuilder.cs b/src/Squirrel.Packaging.OSX/StructureBuilder.cs index 14bfcab8..1112ec85 100644 --- a/src/Squirrel.Packaging.OSX/StructureBuilder.cs +++ b/src/Squirrel.Packaging.OSX/StructureBuilder.cs @@ -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); } } diff --git a/src/Squirrel.Packaging.Windows/AuthenticodeTools.cs b/src/Squirrel.Packaging.Windows/AuthenticodeTools.cs index 3df3900c..aab8bb32 100644 --- a/src/Squirrel.Packaging.Windows/AuthenticodeTools.cs +++ b/src/Squirrel.Packaging.Windows/AuthenticodeTools.cs @@ -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); diff --git a/src/Squirrel.Packaging.Windows/Commands.cs b/src/Squirrel.Packaging.Windows/Commands.cs deleted file mode 100644 index 9ae78513..00000000 --- a/src/Squirrel.Packaging.Windows/Commands.cs +++ /dev/null @@ -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().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(); - - var releaseFilePath = Path.Combine(targetDir, "RELEASES"); - var previousReleases = new List(); - 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((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 { - { "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); - } - } - } -} \ No newline at end of file diff --git a/src/Squirrel.Packaging.Windows/CopStache.cs b/src/Squirrel.Packaging.Windows/CopStache.cs index 4647474d..864f01d0 100644 --- a/src/Squirrel.Packaging.Windows/CopStache.cs +++ b/src/Squirrel.Packaging.Windows/CopStache.cs @@ -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 identifiers) { - public static string Render(string template, Dictionary 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(); } } diff --git a/src/Squirrel.Packaging.Windows/DotnetUtil.cs b/src/Squirrel.Packaging.Windows/DotnetUtil.cs index 06ad13d8..75478c0f 100644 --- a/src/Squirrel.Packaging.Windows/DotnetUtil.cs +++ b/src/Squirrel.Packaging.Windows/DotnetUtil.cs @@ -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 \ No newline at end of file diff --git a/src/Squirrel.Packaging.Windows/HelperExe.cs b/src/Squirrel.Packaging.Windows/HelperExe.cs index 0832a04a..0d686b3d 100644 --- a/src/Squirrel.Packaging.Windows/HelperExe.cs +++ b/src/Squirrel.Packaging.Windows/HelperExe.cs @@ -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 pendingSign = new Queue(); + 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 pendingSign = new Queue(); + + 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 args = new List(); - 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 args = new List(); + 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 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 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 args = new List() { - 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 args = new List() { + 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)); + } } \ No newline at end of file diff --git a/src/Squirrel.Packaging/HostModel/AppHost/AppHostExceptions.cs b/src/Squirrel.Packaging.Windows/HostModel/AppHost/AppHostExceptions.cs similarity index 100% rename from src/Squirrel.Packaging/HostModel/AppHost/AppHostExceptions.cs rename to src/Squirrel.Packaging.Windows/HostModel/AppHost/AppHostExceptions.cs diff --git a/src/Squirrel.Packaging/HostModel/AppHost/BinaryUtils.cs b/src/Squirrel.Packaging.Windows/HostModel/AppHost/BinaryUtils.cs similarity index 100% rename from src/Squirrel.Packaging/HostModel/AppHost/BinaryUtils.cs rename to src/Squirrel.Packaging.Windows/HostModel/AppHost/BinaryUtils.cs diff --git a/src/Squirrel.Packaging/HostModel/AppHost/ElfUtils.cs b/src/Squirrel.Packaging.Windows/HostModel/AppHost/ElfUtils.cs similarity index 100% rename from src/Squirrel.Packaging/HostModel/AppHost/ElfUtils.cs rename to src/Squirrel.Packaging.Windows/HostModel/AppHost/ElfUtils.cs diff --git a/src/Squirrel.Packaging/HostModel/AppHost/HResultException.cs b/src/Squirrel.Packaging.Windows/HostModel/AppHost/HResultException.cs similarity index 100% rename from src/Squirrel.Packaging/HostModel/AppHost/HResultException.cs rename to src/Squirrel.Packaging.Windows/HostModel/AppHost/HResultException.cs diff --git a/src/Squirrel.Packaging/HostModel/AppHost/HostWriter.cs b/src/Squirrel.Packaging.Windows/HostModel/AppHost/HostWriter.cs similarity index 100% rename from src/Squirrel.Packaging/HostModel/AppHost/HostWriter.cs rename to src/Squirrel.Packaging.Windows/HostModel/AppHost/HostWriter.cs diff --git a/src/Squirrel.Packaging/HostModel/AppHost/MachOFormatError.cs b/src/Squirrel.Packaging.Windows/HostModel/AppHost/MachOFormatError.cs similarity index 100% rename from src/Squirrel.Packaging/HostModel/AppHost/MachOFormatError.cs rename to src/Squirrel.Packaging.Windows/HostModel/AppHost/MachOFormatError.cs diff --git a/src/Squirrel.Packaging/HostModel/AppHost/MachOUtils.cs b/src/Squirrel.Packaging.Windows/HostModel/AppHost/MachOUtils.cs similarity index 100% rename from src/Squirrel.Packaging/HostModel/AppHost/MachOUtils.cs rename to src/Squirrel.Packaging.Windows/HostModel/AppHost/MachOUtils.cs diff --git a/src/Squirrel.Packaging/HostModel/AppHost/PEUtils.cs b/src/Squirrel.Packaging.Windows/HostModel/AppHost/PEUtils.cs similarity index 100% rename from src/Squirrel.Packaging/HostModel/AppHost/PEUtils.cs rename to src/Squirrel.Packaging.Windows/HostModel/AppHost/PEUtils.cs diff --git a/src/Squirrel.Packaging/HostModel/AppHost/PlaceHolderNotFoundInAppHostException.cs b/src/Squirrel.Packaging.Windows/HostModel/AppHost/PlaceHolderNotFoundInAppHostException.cs similarity index 100% rename from src/Squirrel.Packaging/HostModel/AppHost/PlaceHolderNotFoundInAppHostException.cs rename to src/Squirrel.Packaging.Windows/HostModel/AppHost/PlaceHolderNotFoundInAppHostException.cs diff --git a/src/Squirrel.Packaging/HostModel/AppHost/RetryUtil.cs b/src/Squirrel.Packaging.Windows/HostModel/AppHost/RetryUtil.cs similarity index 100% rename from src/Squirrel.Packaging/HostModel/AppHost/RetryUtil.cs rename to src/Squirrel.Packaging.Windows/HostModel/AppHost/RetryUtil.cs diff --git a/src/Squirrel.Packaging/HostModel/Bundle/BundleOptions.cs b/src/Squirrel.Packaging.Windows/HostModel/Bundle/BundleOptions.cs similarity index 100% rename from src/Squirrel.Packaging/HostModel/Bundle/BundleOptions.cs rename to src/Squirrel.Packaging.Windows/HostModel/Bundle/BundleOptions.cs diff --git a/src/Squirrel.Packaging/HostModel/Bundle/Bundler.cs b/src/Squirrel.Packaging.Windows/HostModel/Bundle/Bundler.cs similarity index 100% rename from src/Squirrel.Packaging/HostModel/Bundle/Bundler.cs rename to src/Squirrel.Packaging.Windows/HostModel/Bundle/Bundler.cs diff --git a/src/Squirrel.Packaging/HostModel/Bundle/FileEntry.cs b/src/Squirrel.Packaging.Windows/HostModel/Bundle/FileEntry.cs similarity index 100% rename from src/Squirrel.Packaging/HostModel/Bundle/FileEntry.cs rename to src/Squirrel.Packaging.Windows/HostModel/Bundle/FileEntry.cs diff --git a/src/Squirrel.Packaging/HostModel/Bundle/FileSpec.cs b/src/Squirrel.Packaging.Windows/HostModel/Bundle/FileSpec.cs similarity index 100% rename from src/Squirrel.Packaging/HostModel/Bundle/FileSpec.cs rename to src/Squirrel.Packaging.Windows/HostModel/Bundle/FileSpec.cs diff --git a/src/Squirrel.Packaging/HostModel/Bundle/FileType.cs b/src/Squirrel.Packaging.Windows/HostModel/Bundle/FileType.cs similarity index 100% rename from src/Squirrel.Packaging/HostModel/Bundle/FileType.cs rename to src/Squirrel.Packaging.Windows/HostModel/Bundle/FileType.cs diff --git a/src/Squirrel.Packaging/HostModel/Bundle/Manifest.cs b/src/Squirrel.Packaging.Windows/HostModel/Bundle/Manifest.cs similarity index 100% rename from src/Squirrel.Packaging/HostModel/Bundle/Manifest.cs rename to src/Squirrel.Packaging.Windows/HostModel/Bundle/Manifest.cs diff --git a/src/Squirrel.Packaging/HostModel/Bundle/TargetInfo.cs b/src/Squirrel.Packaging.Windows/HostModel/Bundle/TargetInfo.cs similarity index 100% rename from src/Squirrel.Packaging/HostModel/Bundle/TargetInfo.cs rename to src/Squirrel.Packaging.Windows/HostModel/Bundle/TargetInfo.cs diff --git a/src/Squirrel.Packaging/HostModel/Bundle/Trace.cs b/src/Squirrel.Packaging.Windows/HostModel/Bundle/Trace.cs similarity index 100% rename from src/Squirrel.Packaging/HostModel/Bundle/Trace.cs rename to src/Squirrel.Packaging.Windows/HostModel/Bundle/Trace.cs diff --git a/src/Squirrel.Packaging/HostModel/HostModelUtils.cs b/src/Squirrel.Packaging.Windows/HostModel/HostModelUtils.cs similarity index 100% rename from src/Squirrel.Packaging/HostModel/HostModelUtils.cs rename to src/Squirrel.Packaging.Windows/HostModel/HostModelUtils.cs diff --git a/src/Squirrel.Packaging/HostModel/README.md b/src/Squirrel.Packaging.Windows/HostModel/README.md similarity index 100% rename from src/Squirrel.Packaging/HostModel/README.md rename to src/Squirrel.Packaging.Windows/HostModel/README.md diff --git a/src/Squirrel.Packaging/HostModel/ResourceUpdater.Squirrel.cs b/src/Squirrel.Packaging.Windows/HostModel/ResourceUpdater.Squirrel.cs similarity index 100% rename from src/Squirrel.Packaging/HostModel/ResourceUpdater.Squirrel.cs rename to src/Squirrel.Packaging.Windows/HostModel/ResourceUpdater.Squirrel.cs diff --git a/src/Squirrel.Packaging/HostModel/ResourceUpdater.cs b/src/Squirrel.Packaging.Windows/HostModel/ResourceUpdater.cs similarity index 100% rename from src/Squirrel.Packaging/HostModel/ResourceUpdater.cs rename to src/Squirrel.Packaging.Windows/HostModel/ResourceUpdater.cs diff --git a/src/Squirrel.Packaging.Windows/SetupBundle.cs b/src/Squirrel.Packaging.Windows/SetupBundle.cs index c949627e..be5f0d7e 100644 --- a/src/Squirrel.Packaging.Windows/SetupBundle.cs +++ b/src/Squirrel.Packaging.Windows/SetupBundle.cs @@ -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; } } \ No newline at end of file diff --git a/src/Squirrel.Packaging.Windows/Squirrel.Packaging.Windows.csproj b/src/Squirrel.Packaging.Windows/Squirrel.Packaging.Windows.csproj index d215c71f..d1edfefe 100644 --- a/src/Squirrel.Packaging.Windows/Squirrel.Packaging.Windows.csproj +++ b/src/Squirrel.Packaging.Windows/Squirrel.Packaging.Windows.csproj @@ -3,6 +3,17 @@ net6.0 enable + true + $(NoWarn);CA2007;CS8002 + + + + + + + + + diff --git a/src/Squirrel.Packaging.Windows/WindowsCommands.cs b/src/Squirrel.Packaging.Windows/WindowsCommands.cs new file mode 100644 index 00000000..bd6eee86 --- /dev/null +++ b/src/Squirrel.Packaging.Windows/WindowsCommands.cs @@ -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(); + + var releaseFilePath = Path.Combine(targetDir, "RELEASES"); + var previousReleases = new List(); + 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); + } + } +} \ No newline at end of file diff --git a/src/Squirrel.Packaging/DeltaPackageBuilder.cs b/src/Squirrel.Packaging/DeltaPackageBuilder.cs index 37f6a1cc..487dd056 100644 --- a/src/Squirrel.Packaging/DeltaPackageBuilder.cs +++ b/src/Squirrel.Packaging/DeltaPackageBuilder.cs @@ -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 a1, ReadOnlySpan 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 a1, ReadOnlySpan 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); } } diff --git a/src/Squirrel.Packaging/HelperFile.cs b/src/Squirrel.Packaging/HelperFile.cs index 78635c29..a572376c 100644 --- a/src/Squirrel.Packaging/HelperFile.cs +++ b/src/Squirrel.Packaging/HelperFile.cs @@ -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 _searchPaths = new List(); + protected readonly ILogger Log; + + static HelperFile() { - private static List _searchPaths = new List(); - protected static IFullLogger Log = SquirrelLocator.CurrentMutable.GetService().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 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 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 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 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); } } diff --git a/src/Squirrel.Packaging/MarkdownSharp.cs b/src/Squirrel.Packaging/MarkdownSharp.cs index 75899dd8..7c7d782f 100644 --- a/src/Squirrel.Packaging/MarkdownSharp.cs +++ b/src/Squirrel.Packaging/MarkdownSharp.cs @@ -85,415 +85,413 @@ software, even if advised of the possibility of such damage. #endregion -using System; -using System.Collections.Generic; using System.Text; using System.Text.RegularExpressions; -namespace Squirrel.MarkdownSharp +namespace Squirrel.Packaging; + +public class MarkdownOptions { - internal class MarkdownOptions + /// + /// when true, (most) bare plain URLs are auto-hyperlinked + /// WARNING: this is a significant deviation from the markdown spec + /// + public bool AutoHyperlink { get; set; } + /// + /// when true, RETURN becomes a literal newline + /// WARNING: this is a significant deviation from the markdown spec + /// + public bool AutoNewlines { get; set; } + /// + /// use ">" for HTML output, or " />" for XHTML output + /// + public string EmptyElementSuffix { get; set; } + /// + /// when true, problematic URL characters like [, ], (, and so forth will be encoded + /// WARNING: this is a significant deviation from the markdown spec + /// + public bool EncodeProblemUrlCharacters { get; set; } + /// + /// when false, email addresses will never be auto-linked + /// WARNING: this is a significant deviation from the markdown spec + /// + public bool LinkEmails { get; set; } + /// + /// when true, bold and italic require non-word characters on either side + /// WARNING: this is a significant deviation from the markdown spec + /// + public bool StrictBoldItalic { get; set; } +} + +/// +/// Markdown is a text-to-HTML conversion tool for web writers. +/// Markdown allows you to write using an easy-to-read, easy-to-write plain text format, +/// then convert it to structurally valid XHTML (or HTML). +/// +public class Markdown +{ + private const string _version = "1.13"; + + #region Constructors and Options + + /// + /// Create a new Markdown instance using default options + /// + public Markdown() { - /// - /// when true, (most) bare plain URLs are auto-hyperlinked - /// WARNING: this is a significant deviation from the markdown spec - /// - public bool AutoHyperlink { get; set; } - /// - /// when true, RETURN becomes a literal newline - /// WARNING: this is a significant deviation from the markdown spec - /// - public bool AutoNewlines { get; set; } - /// - /// use ">" for HTML output, or " />" for XHTML output - /// - public string EmptyElementSuffix { get; set; } - /// - /// when true, problematic URL characters like [, ], (, and so forth will be encoded - /// WARNING: this is a significant deviation from the markdown spec - /// - public bool EncodeProblemUrlCharacters { get; set; } - /// - /// when false, email addresses will never be auto-linked - /// WARNING: this is a significant deviation from the markdown spec - /// - public bool LinkEmails { get; set; } - /// - /// when true, bold and italic require non-word characters on either side - /// WARNING: this is a significant deviation from the markdown spec - /// - public bool StrictBoldItalic { get; set; } } /// - /// Markdown is a text-to-HTML conversion tool for web writers. - /// Markdown allows you to write using an easy-to-read, easy-to-write plain text format, - /// then convert it to structurally valid XHTML (or HTML). + /// Create a new Markdown instance and set the options from the MarkdownOptions object. /// - internal class Markdown + public Markdown(MarkdownOptions options) { - private const string _version = "1.13"; + _autoHyperlink = options.AutoHyperlink; + _autoNewlines = options.AutoNewlines; + _emptyElementSuffix = options.EmptyElementSuffix; + _encodeProblemUrlCharacters = options.EncodeProblemUrlCharacters; + _linkEmails = options.LinkEmails; + _strictBoldItalic = options.StrictBoldItalic; + } - #region Constructors and Options - /// - /// Create a new Markdown instance using default options - /// - public Markdown() + /// + /// use ">" for HTML output, or " />" for XHTML output + /// + public string EmptyElementSuffix + { + get { return _emptyElementSuffix; } + set { _emptyElementSuffix = value; } + } + private string _emptyElementSuffix = " />"; + + /// + /// when false, email addresses will never be auto-linked + /// WARNING: this is a significant deviation from the markdown spec + /// + public bool LinkEmails + { + get { return _linkEmails; } + set { _linkEmails = value; } + } + private bool _linkEmails = true; + + /// + /// when true, bold and italic require non-word characters on either side + /// WARNING: this is a significant deviation from the markdown spec + /// + public bool StrictBoldItalic + { + get { return _strictBoldItalic; } + set { _strictBoldItalic = value; } + } + private bool _strictBoldItalic = false; + + /// + /// when true, RETURN becomes a literal newline + /// WARNING: this is a significant deviation from the markdown spec + /// + public bool AutoNewLines + { + get { return _autoNewlines; } + set { _autoNewlines = value; } + } + private bool _autoNewlines = false; + + /// + /// when true, (most) bare plain URLs are auto-hyperlinked + /// WARNING: this is a significant deviation from the markdown spec + /// + public bool AutoHyperlink + { + get { return _autoHyperlink; } + set { _autoHyperlink = value; } + } + private bool _autoHyperlink = false; + + /// + /// when true, problematic URL characters like [, ], (, and so forth will be encoded + /// WARNING: this is a significant deviation from the markdown spec + /// + public bool EncodeProblemUrlCharacters + { + get { return _encodeProblemUrlCharacters; } + set { _encodeProblemUrlCharacters = value; } + } + private bool _encodeProblemUrlCharacters = false; + + #endregion + + private enum TokenType { Text, Tag } + + private struct Token + { + public Token(TokenType type, string value) { + this.Type = type; + this.Value = value; + } + public TokenType Type; + public string Value; + } + + /// + /// maximum nested depth of [] and () supported by the transform; implementation detail + /// + private const int _nestDepth = 6; + + /// + /// Tabs are automatically converted to spaces as part of the transform + /// this constant determines how "wide" those tabs become in spaces + /// + private const int _tabWidth = 4; + + private const string _markerUL = @"[*+-]"; + private const string _markerOL = @"\d+[.]"; + + private static readonly Dictionary _escapeTable; + private static readonly Dictionary _invertedEscapeTable; + private static readonly Dictionary _backslashEscapeTable; + + private readonly Dictionary _urls = new Dictionary(); + private readonly Dictionary _titles = new Dictionary(); + private readonly Dictionary _htmlBlocks = new Dictionary(); + + private int _listLevel; + private static string AutoLinkPreventionMarker = "\x1AP"; // temporarily replaces "://" where auto-linking shouldn't happen; + + /// + /// In the static constuctor we'll initialize what stays the same across all transforms. + /// + static Markdown() + { + // Table of hash values for escaped characters: + _escapeTable = new Dictionary(); + _invertedEscapeTable = new Dictionary(); + // Table of hash value for backslash escaped characters: + _backslashEscapeTable = new Dictionary(); + + string backslashPattern = ""; + + foreach (char c in @"\`*_{}[]()>#+-.!/") + { + string key = c.ToString(); + string hash = GetHashKey(key, isHtmlBlock: false); + _escapeTable.Add(key, hash); + _invertedEscapeTable.Add(hash, key); + _backslashEscapeTable.Add(@"\" + key, hash); + backslashPattern += Regex.Escape(@"\" + key) + "|"; } - /// - /// Create a new Markdown instance and set the options from the MarkdownOptions object. - /// - public Markdown(MarkdownOptions options) + _backslashEscapes = new Regex(backslashPattern.Substring(0, backslashPattern.Length - 1), RegexOptions.Compiled); + } + + /// + /// current version of MarkdownSharp; + /// see http://code.google.com/p/markdownsharp/ for the latest code or to contribute + /// + public string Version + { + get { return _version; } + } + + /// + /// Transforms the provided Markdown-formatted text to HTML; + /// see http://en.wikipedia.org/wiki/Markdown + /// + /// + /// The order in which other subs are called here is + /// essential. Link and image substitutions need to happen before + /// EscapeSpecialChars(), so that any *'s or _'s in the a + /// and img tags get encoded. + /// + public string Transform(string text) + { + if (String.IsNullOrEmpty(text)) return ""; + + Setup(); + + text = Normalize(text); + + text = HashHTMLBlocks(text); + text = StripLinkDefinitions(text); + text = RunBlockGamut(text); + text = Unescape(text); + + Cleanup(); + + return text + "\n"; + } + + + /// + /// Perform transformations that form block-level tags like paragraphs, headers, and list items. + /// + private string RunBlockGamut(string text, bool unhash = true) + { + text = DoHeaders(text); + text = DoHorizontalRules(text); + text = DoLists(text); + text = DoCodeBlocks(text); + text = DoBlockQuotes(text); + + // We already ran HashHTMLBlocks() before, in Markdown(), but that + // was to escape raw HTML in the original Markdown source. This time, + // we're escaping the markup we've just created, so that we don't wrap + //

tags around block-level tags. + text = HashHTMLBlocks(text); + + text = FormParagraphs(text, unhash: unhash); + + return text; + } + + + ///

+ /// Perform transformations that occur *within* block-level tags like paragraphs, headers, and list items. + /// + private string RunSpanGamut(string text) + { + text = DoCodeSpans(text); + text = EscapeSpecialCharsWithinTagAttributes(text); + text = EscapeBackslashes(text); + + // Images must come first, because ![foo][f] looks like an anchor. + text = DoImages(text); + text = DoAnchors(text); + + // Must come after DoAnchors(), because you can use < and > + // delimiters in inline links like [this](). + text = DoAutoLinks(text); + + text = text.Replace(AutoLinkPreventionMarker, "://"); + + text = EncodeAmpsAndAngles(text); + text = DoItalicsAndBold(text); + text = DoHardBreaks(text); + + return text; + } + + private static Regex _newlinesLeadingTrailing = new Regex(@"^\n+|\n+\z", RegexOptions.Compiled); + private static Regex _newlinesMultiple = new Regex(@"\n{2,}", RegexOptions.Compiled); + private static Regex _leadingWhitespace = new Regex(@"^[ ]*", RegexOptions.Compiled); + + private static Regex _htmlBlockHash = new Regex("\x1AH\\d+H", RegexOptions.Compiled); + + /// + /// splits on two or more newlines, to form "paragraphs"; + /// each paragraph is then unhashed (if it is a hash and unhashing isn't turned off) or wrapped in HTML p tag + /// + private string FormParagraphs(string text, bool unhash = true) + { + // split on two or more newlines + string[] grafs = _newlinesMultiple.Split(_newlinesLeadingTrailing.Replace(text, "")); + + for (int i = 0; i < grafs.Length; i++) { - _autoHyperlink = options.AutoHyperlink; - _autoNewlines = options.AutoNewlines; - _emptyElementSuffix = options.EmptyElementSuffix; - _encodeProblemUrlCharacters = options.EncodeProblemUrlCharacters; - _linkEmails = options.LinkEmails; - _strictBoldItalic = options.StrictBoldItalic; - } - - - /// - /// use ">" for HTML output, or " />" for XHTML output - /// - public string EmptyElementSuffix - { - get { return _emptyElementSuffix; } - set { _emptyElementSuffix = value; } - } - private string _emptyElementSuffix = " />"; - - /// - /// when false, email addresses will never be auto-linked - /// WARNING: this is a significant deviation from the markdown spec - /// - public bool LinkEmails - { - get { return _linkEmails; } - set { _linkEmails = value; } - } - private bool _linkEmails = true; - - /// - /// when true, bold and italic require non-word characters on either side - /// WARNING: this is a significant deviation from the markdown spec - /// - public bool StrictBoldItalic - { - get { return _strictBoldItalic; } - set { _strictBoldItalic = value; } - } - private bool _strictBoldItalic = false; - - /// - /// when true, RETURN becomes a literal newline - /// WARNING: this is a significant deviation from the markdown spec - /// - public bool AutoNewLines - { - get { return _autoNewlines; } - set { _autoNewlines = value; } - } - private bool _autoNewlines = false; - - /// - /// when true, (most) bare plain URLs are auto-hyperlinked - /// WARNING: this is a significant deviation from the markdown spec - /// - public bool AutoHyperlink - { - get { return _autoHyperlink; } - set { _autoHyperlink = value; } - } - private bool _autoHyperlink = false; - - /// - /// when true, problematic URL characters like [, ], (, and so forth will be encoded - /// WARNING: this is a significant deviation from the markdown spec - /// - public bool EncodeProblemUrlCharacters - { - get { return _encodeProblemUrlCharacters; } - set { _encodeProblemUrlCharacters = value; } - } - private bool _encodeProblemUrlCharacters = false; - - #endregion - - private enum TokenType { Text, Tag } - - private struct Token - { - public Token(TokenType type, string value) + if (grafs[i].StartsWith("\x1AH")) { - this.Type = type; - this.Value = value; - } - public TokenType Type; - public string Value; - } - - /// - /// maximum nested depth of [] and () supported by the transform; implementation detail - /// - private const int _nestDepth = 6; - - /// - /// Tabs are automatically converted to spaces as part of the transform - /// this constant determines how "wide" those tabs become in spaces - /// - private const int _tabWidth = 4; - - private const string _markerUL = @"[*+-]"; - private const string _markerOL = @"\d+[.]"; - - private static readonly Dictionary _escapeTable; - private static readonly Dictionary _invertedEscapeTable; - private static readonly Dictionary _backslashEscapeTable; - - private readonly Dictionary _urls = new Dictionary(); - private readonly Dictionary _titles = new Dictionary(); - private readonly Dictionary _htmlBlocks = new Dictionary(); - - private int _listLevel; - private static string AutoLinkPreventionMarker = "\x1AP"; // temporarily replaces "://" where auto-linking shouldn't happen; - - /// - /// In the static constuctor we'll initialize what stays the same across all transforms. - /// - static Markdown() - { - // Table of hash values for escaped characters: - _escapeTable = new Dictionary(); - _invertedEscapeTable = new Dictionary(); - // Table of hash value for backslash escaped characters: - _backslashEscapeTable = new Dictionary(); - - string backslashPattern = ""; - - foreach (char c in @"\`*_{}[]()>#+-.!/") - { - string key = c.ToString(); - string hash = GetHashKey(key, isHtmlBlock: false); - _escapeTable.Add(key, hash); - _invertedEscapeTable.Add(hash, key); - _backslashEscapeTable.Add(@"\" + key, hash); - backslashPattern += Regex.Escape(@"\" + key) + "|"; - } - - _backslashEscapes = new Regex(backslashPattern.Substring(0, backslashPattern.Length - 1), RegexOptions.Compiled); - } - - /// - /// current version of MarkdownSharp; - /// see http://code.google.com/p/markdownsharp/ for the latest code or to contribute - /// - public string Version - { - get { return _version; } - } - - /// - /// Transforms the provided Markdown-formatted text to HTML; - /// see http://en.wikipedia.org/wiki/Markdown - /// - /// - /// The order in which other subs are called here is - /// essential. Link and image substitutions need to happen before - /// EscapeSpecialChars(), so that any *'s or _'s in the a - /// and img tags get encoded. - /// - public string Transform(string text) - { - if (String.IsNullOrEmpty(text)) return ""; - - Setup(); - - text = Normalize(text); - - text = HashHTMLBlocks(text); - text = StripLinkDefinitions(text); - text = RunBlockGamut(text); - text = Unescape(text); - - Cleanup(); - - return text + "\n"; - } - - - /// - /// Perform transformations that form block-level tags like paragraphs, headers, and list items. - /// - private string RunBlockGamut(string text, bool unhash = true) - { - text = DoHeaders(text); - text = DoHorizontalRules(text); - text = DoLists(text); - text = DoCodeBlocks(text); - text = DoBlockQuotes(text); - - // We already ran HashHTMLBlocks() before, in Markdown(), but that - // was to escape raw HTML in the original Markdown source. This time, - // we're escaping the markup we've just created, so that we don't wrap - //

tags around block-level tags. - text = HashHTMLBlocks(text); - - text = FormParagraphs(text, unhash: unhash); - - return text; - } - - - ///

- /// Perform transformations that occur *within* block-level tags like paragraphs, headers, and list items. - /// - private string RunSpanGamut(string text) - { - text = DoCodeSpans(text); - text = EscapeSpecialCharsWithinTagAttributes(text); - text = EscapeBackslashes(text); - - // Images must come first, because ![foo][f] looks like an anchor. - text = DoImages(text); - text = DoAnchors(text); - - // Must come after DoAnchors(), because you can use < and > - // delimiters in inline links like [this](). - text = DoAutoLinks(text); - - text = text.Replace(AutoLinkPreventionMarker, "://"); - - text = EncodeAmpsAndAngles(text); - text = DoItalicsAndBold(text); - text = DoHardBreaks(text); - - return text; - } - - private static Regex _newlinesLeadingTrailing = new Regex(@"^\n+|\n+\z", RegexOptions.Compiled); - private static Regex _newlinesMultiple = new Regex(@"\n{2,}", RegexOptions.Compiled); - private static Regex _leadingWhitespace = new Regex(@"^[ ]*", RegexOptions.Compiled); - - private static Regex _htmlBlockHash = new Regex("\x1AH\\d+H", RegexOptions.Compiled); - - /// - /// splits on two or more newlines, to form "paragraphs"; - /// each paragraph is then unhashed (if it is a hash and unhashing isn't turned off) or wrapped in HTML p tag - /// - private string FormParagraphs(string text, bool unhash = true) - { - // split on two or more newlines - string[] grafs = _newlinesMultiple.Split(_newlinesLeadingTrailing.Replace(text, "")); - - for (int i = 0; i < grafs.Length; i++) - { - if (grafs[i].StartsWith("\x1AH")) + // unhashify HTML blocks + if (unhash) { - // unhashify HTML blocks - if (unhash) + int sanityCheck = 50; // just for safety, guard against an infinite loop + bool keepGoing = true; // as long as replacements where made, keep going + while (keepGoing && sanityCheck > 0) { - int sanityCheck = 50; // just for safety, guard against an infinite loop - bool keepGoing = true; // as long as replacements where made, keep going - while (keepGoing && sanityCheck > 0) + keepGoing = false; + grafs[i] = _htmlBlockHash.Replace(grafs[i], match => { - keepGoing = false; - grafs[i] = _htmlBlockHash.Replace(grafs[i], match => - { - keepGoing = true; - return _htmlBlocks[match.Value]; - }); - sanityCheck--; - } - /* if (keepGoing) - { - // Logging of an infinite loop goes here. - // If such a thing should happen, please open a new issue on http://code.google.com/p/markdownsharp/ - // with the input that caused it. - }*/ + keepGoing = true; + return _htmlBlocks[match.Value]; + }); + sanityCheck--; } - } - else - { - // do span level processing inside the block, then wrap result in

tags - grafs[i] = _leadingWhitespace.Replace(RunSpanGamut(grafs[i]), "

") + "

"; + /* if (keepGoing) + { + // Logging of an infinite loop goes here. + // If such a thing should happen, please open a new issue on http://code.google.com/p/markdownsharp/ + // with the input that caused it. + }*/ } } - - return string.Join("\n\n", grafs); + else + { + // do span level processing inside the block, then wrap result in

tags + grafs[i] = _leadingWhitespace.Replace(RunSpanGamut(grafs[i]), "

") + "

"; + } } + return string.Join("\n\n", grafs); + } - private void Setup() - { - // Clear the global hashes. If we don't clear these, you get conflicts - // from other articles when generating a page which contains more than - // one article (e.g. an index page that shows the N most recent - // articles): - _urls.Clear(); - _titles.Clear(); - _htmlBlocks.Clear(); - _listLevel = 0; - } - private void Cleanup() - { - Setup(); - } + private void Setup() + { + // Clear the global hashes. If we don't clear these, you get conflicts + // from other articles when generating a page which contains more than + // one article (e.g. an index page that shows the N most recent + // articles): + _urls.Clear(); + _titles.Clear(); + _htmlBlocks.Clear(); + _listLevel = 0; + } - private static string _nestedBracketsPattern; + private void Cleanup() + { + Setup(); + } - /// - /// Reusable pattern to match balanced [brackets]. See Friedl's - /// "Mastering Regular Expressions", 2nd Ed., pp. 328-331. - /// - private static string GetNestedBracketsPattern() - { - // in other words [this] and [this[also]] and [this[also[too]]] - // up to _nestDepth - if (_nestedBracketsPattern == null) - _nestedBracketsPattern = - RepeatString(@" + private static string _nestedBracketsPattern; + + /// + /// Reusable pattern to match balanced [brackets]. See Friedl's + /// "Mastering Regular Expressions", 2nd Ed., pp. 328-331. + /// + private static string GetNestedBracketsPattern() + { + // in other words [this] and [this[also]] and [this[also[too]]] + // up to _nestDepth + if (_nestedBracketsPattern == null) + _nestedBracketsPattern = + RepeatString(@" (?> # Atomic matching [^\[\]]+ # Anything other than brackets | \[ ", _nestDepth) + RepeatString( - @" \] + @" \] )*" - , _nestDepth); - return _nestedBracketsPattern; - } + , _nestDepth); + return _nestedBracketsPattern; + } - private static string _nestedParensPattern; + private static string _nestedParensPattern; - /// - /// Reusable pattern to match balanced (parens). See Friedl's - /// "Mastering Regular Expressions", 2nd Ed., pp. 328-331. - /// - private static string GetNestedParensPattern() - { - // in other words (this) and (this(also)) and (this(also(too))) - // up to _nestDepth - if (_nestedParensPattern == null) - _nestedParensPattern = - RepeatString(@" + /// + /// Reusable pattern to match balanced (parens). See Friedl's + /// "Mastering Regular Expressions", 2nd Ed., pp. 328-331. + /// + private static string GetNestedParensPattern() + { + // in other words (this) and (this(also)) and (this(also(too))) + // up to _nestDepth + if (_nestedParensPattern == null) + _nestedParensPattern = + RepeatString(@" (?> # Atomic matching [^()\s]+ # Anything other than parens or whitespace | \( ", _nestDepth) + RepeatString( - @" \) + @" \) )*" - , _nestDepth); - return _nestedParensPattern; - } + , _nestDepth); + return _nestedParensPattern; + } - private static Regex _linkDef = new Regex(string.Format(@" + private static Regex _linkDef = new Regex(string.Format(@" ^[ ]{{0,{0}}}\[([^\[\]]+)\]: # id = $1 [ ]* \n? # maybe *one* newline @@ -511,56 +509,56 @@ namespace Squirrel.MarkdownSharp )? # title is optional (?:\n+|\Z)", _tabWidth - 1), RegexOptions.Multiline | RegexOptions.IgnorePatternWhitespace | RegexOptions.Compiled); - /// - /// Strips link definitions from text, stores the URLs and titles in hash references. - /// - /// - /// ^[id]: url "optional title" - /// - private string StripLinkDefinitions(string text) - { - return _linkDef.Replace(text, new MatchEvaluator(LinkEvaluator)); - } + /// + /// Strips link definitions from text, stores the URLs and titles in hash references. + /// + /// + /// ^[id]: url "optional title" + /// + private string StripLinkDefinitions(string text) + { + return _linkDef.Replace(text, new MatchEvaluator(LinkEvaluator)); + } - private string LinkEvaluator(Match match) - { - string linkID = match.Groups[1].Value.ToLowerInvariant(); - _urls[linkID] = EncodeAmpsAndAngles(match.Groups[2].Value); + private string LinkEvaluator(Match match) + { + string linkID = match.Groups[1].Value.ToLowerInvariant(); + _urls[linkID] = EncodeAmpsAndAngles(match.Groups[2].Value); - if (match.Groups[3] != null && match.Groups[3].Length > 0) - _titles[linkID] = match.Groups[3].Value.Replace("\"", """); + if (match.Groups[3] != null && match.Groups[3].Length > 0) + _titles[linkID] = match.Groups[3].Value.Replace("\"", """); - return ""; - } + return ""; + } - // compiling this monster regex results in worse performance. trust me. - private static Regex _blocksHtml = new Regex(GetBlockPattern(), RegexOptions.Multiline | RegexOptions.IgnorePatternWhitespace); + // compiling this monster regex results in worse performance. trust me. + private static Regex _blocksHtml = new Regex(GetBlockPattern(), RegexOptions.Multiline | RegexOptions.IgnorePatternWhitespace); - /// - /// derived pretty much verbatim from PHP Markdown - /// - private static string GetBlockPattern() - { + /// + /// derived pretty much verbatim from PHP Markdown + /// + private static string GetBlockPattern() + { - // Hashify HTML blocks: - // We only want to do this for block-level HTML tags, such as headers, - // lists, and tables. That's because we still want to wrap

s around - // "paragraphs" that are wrapped in non-block-level tags, such as anchors, - // phrase emphasis, and spans. The list of tags we're looking for is - // hard-coded: - // - // * List "a" is made of tags which can be both inline or block-level. - // These will be treated block-level when the start tag is alone on - // its line, otherwise they're not matched here and will be taken as - // inline later. - // * List "b" is made of tags which are always block-level; - // - string blockTagsA = "ins|del"; - string blockTagsB = "p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|address|script|noscript|form|fieldset|iframe|math"; + // Hashify HTML blocks: + // We only want to do this for block-level HTML tags, such as headers, + // lists, and tables. That's because we still want to wrap

s around + // "paragraphs" that are wrapped in non-block-level tags, such as anchors, + // phrase emphasis, and spans. The list of tags we're looking for is + // hard-coded: + // + // * List "a" is made of tags which can be both inline or block-level. + // These will be treated block-level when the start tag is alone on + // its line, otherwise they're not matched here and will be taken as + // inline later. + // * List "b" is made of tags which are always block-level; + // + string blockTagsA = "ins|del"; + string blockTagsB = "p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|address|script|noscript|form|fieldset|iframe|math"; - // Regular expression for the content of a block tag. - string attr = @" + // Regular expression for the content of a block tag. + string attr = @" (?>    # optional tag attributes \s   # starts with whitespace (?> @@ -575,7 +573,7 @@ namespace Squirrel.MarkdownSharp )? "; - string content = RepeatString(@" + string content = RepeatString(@" (?> [^<]+   # content without tag | @@ -585,8 +583,8 @@ namespace Squirrel.MarkdownSharp /> | >", _nestDepth) + // end of opening tag - ".*?" + // last level nested tag content - RepeatString(@" + ".*?" + // last level nested tag content + RepeatString(@"  # closing nested tag ) |    @@ -594,20 +592,20 @@ namespace Squirrel.MarkdownSharp ) )*", _nestDepth); - string content2 = content.Replace(@"\2", @"\3"); + string content2 = content.Replace(@"\2", @"\3"); - // First, look for nested blocks, e.g.: - // 

- //  
- //   tags for inner block must be indented. - //  
- // 
- // - // The outermost tags must start at the left margin for this to match, and - // the inner nested divs must be indented. - // We need to do this before the next, more liberal match, because the next - // match will start at the first `
` and stop at the first `
`. - string pattern = @" + // First, look for nested blocks, e.g.: + // 
+ //  
+ //   tags for inner block must be indented. + //  
+ // 
+ // + // The outermost tags must start at the left margin for this to match, and + // the inner nested divs must be indented. + // We need to do this before the next, more liberal match, because the next + // match will start at the first `
` and stop at the first `
`. + string pattern = @" (?> (?> (?<=\n) # Starting at the beginning of a line @@ -669,81 +667,81 @@ namespace Squirrel.MarkdownSharp ) )"; - pattern = pattern.Replace("$less_than_tab", (_tabWidth - 1).ToString()); - pattern = pattern.Replace("$block_tags_b_re", blockTagsB); - pattern = pattern.Replace("$block_tags_a_re", blockTagsA); - pattern = pattern.Replace("$attr", attr); - pattern = pattern.Replace("$content2", content2); - pattern = pattern.Replace("$content", content); + pattern = pattern.Replace("$less_than_tab", (_tabWidth - 1).ToString()); + pattern = pattern.Replace("$block_tags_b_re", blockTagsB); + pattern = pattern.Replace("$block_tags_a_re", blockTagsA); + pattern = pattern.Replace("$attr", attr); + pattern = pattern.Replace("$content2", content2); + pattern = pattern.Replace("$content", content); - return pattern; - } + return pattern; + } - /// - /// replaces any block-level HTML blocks with hash entries - /// - private string HashHTMLBlocks(string text) - { - return _blocksHtml.Replace(text, new MatchEvaluator(HtmlEvaluator)); - } + /// + /// replaces any block-level HTML blocks with hash entries + /// + private string HashHTMLBlocks(string text) + { + return _blocksHtml.Replace(text, new MatchEvaluator(HtmlEvaluator)); + } - private string HtmlEvaluator(Match match) - { - string text = match.Groups[1].Value; - string key = GetHashKey(text, isHtmlBlock: true); - _htmlBlocks[key] = text; + private string HtmlEvaluator(Match match) + { + string text = match.Groups[1].Value; + string key = GetHashKey(text, isHtmlBlock: true); + _htmlBlocks[key] = text; - return string.Concat("\n\n", key, "\n\n"); - } + return string.Concat("\n\n", key, "\n\n"); + } - private static string GetHashKey(string s, bool isHtmlBlock) - { - var delim = isHtmlBlock ? 'H' : 'E'; - return "\x1A" + delim + Math.Abs(s.GetHashCode()).ToString() + delim; - } + private static string GetHashKey(string s, bool isHtmlBlock) + { + var delim = isHtmlBlock ? 'H' : 'E'; + return "\x1A" + delim + Math.Abs(s.GetHashCode()).ToString() + delim; + } - private static Regex _htmlTokens = new Regex(@" + private static Regex _htmlTokens = new Regex(@" ()| # match (<\?.*?\?>)| # match " + - RepeatString(@" + RepeatString(@" (<[A-Za-z\/!$](?:[^<>]|", _nestDepth) + RepeatString(@")*>)", _nestDepth) + - " # match and ", - RegexOptions.Multiline | RegexOptions.Singleline | RegexOptions.ExplicitCapture | RegexOptions.IgnorePatternWhitespace | RegexOptions.Compiled); + " # match and ", + RegexOptions.Multiline | RegexOptions.Singleline | RegexOptions.ExplicitCapture | RegexOptions.IgnorePatternWhitespace | RegexOptions.Compiled); - /// - /// returns an array of HTML tokens comprising the input string. Each token is - /// either a tag (possibly with nested, tags contained therein, such - /// as <a href="<MTFoo>">, or a run of text between tags. Each element of the - /// array is a two-element array; the first is either 'tag' or 'text'; the second is - /// the actual value. - /// - private List TokenizeHTML(string text) + /// + /// returns an array of HTML tokens comprising the input string. Each token is + /// either a tag (possibly with nested, tags contained therein, such + /// as <a href="<MTFoo>">, or a run of text between tags. Each element of the + /// array is a two-element array; the first is either 'tag' or 'text'; the second is + /// the actual value. + /// + private List TokenizeHTML(string text) + { + int pos = 0; + int tagStart = 0; + var tokens = new List(); + + // this regex is derived from the _tokenize() subroutine in Brad Choate's MTRegex plugin. + // http://www.bradchoate.com/past/mtregex.php + foreach (Match m in _htmlTokens.Matches(text)) { - int pos = 0; - int tagStart = 0; - var tokens = new List(); + tagStart = m.Index; - // this regex is derived from the _tokenize() subroutine in Brad Choate's MTRegex plugin. - // http://www.bradchoate.com/past/mtregex.php - foreach (Match m in _htmlTokens.Matches(text)) - { - tagStart = m.Index; + if (pos < tagStart) + tokens.Add(new Token(TokenType.Text, text.Substring(pos, tagStart - pos))); - if (pos < tagStart) - tokens.Add(new Token(TokenType.Text, text.Substring(pos, tagStart - pos))); - - tokens.Add(new Token(TokenType.Tag, m.Value)); - pos = tagStart + m.Length; - } - - if (pos < text.Length) - tokens.Add(new Token(TokenType.Text, text.Substring(pos, text.Length - pos))); - - return tokens; + tokens.Add(new Token(TokenType.Tag, m.Value)); + pos = tagStart + m.Length; } + if (pos < text.Length) + tokens.Add(new Token(TokenType.Text, text.Substring(pos, text.Length - pos))); - private static Regex _anchorRef = new Regex(string.Format(@" + return tokens; + } + + + private static Regex _anchorRef = new Regex(string.Format(@" ( # wrap whole match in $1 \[ ({0}) # link text = $2 @@ -757,7 +755,7 @@ namespace Squirrel.MarkdownSharp \] )", GetNestedBracketsPattern()), RegexOptions.Singleline | RegexOptions.IgnorePatternWhitespace | RegexOptions.Compiled); - private static Regex _anchorInline = new Regex(string.Format(@" + private static Regex _anchorInline = new Regex(string.Format(@" ( # wrap whole match in $1 \[ ({0}) # link text = $2 @@ -774,136 +772,136 @@ namespace Squirrel.MarkdownSharp )? # title is optional \) )", GetNestedBracketsPattern(), GetNestedParensPattern()), - RegexOptions.Singleline | RegexOptions.IgnorePatternWhitespace | RegexOptions.Compiled); + RegexOptions.Singleline | RegexOptions.IgnorePatternWhitespace | RegexOptions.Compiled); - private static Regex _anchorRefShortcut = new Regex(@" + private static Regex _anchorRefShortcut = new Regex(@" ( # wrap whole match in $1 \[ ([^\[\]]+) # link text = $2; can't contain [ or ] \] )", RegexOptions.Singleline | RegexOptions.IgnorePatternWhitespace | RegexOptions.Compiled); - /// - /// Turn Markdown link shortcuts into HTML anchor tags - /// - /// - /// [link text](url "title") - /// [link text][id] - /// [id] - /// - private string DoAnchors(string text) + /// + /// Turn Markdown link shortcuts into HTML anchor tags + /// + /// + /// [link text](url "title") + /// [link text][id] + /// [id] + /// + private string DoAnchors(string text) + { + // First, handle reference-style links: [link text] [id] + text = _anchorRef.Replace(text, new MatchEvaluator(AnchorRefEvaluator)); + + // Next, inline-style links: [link text](url "optional title") or [link text](url "optional title") + text = _anchorInline.Replace(text, new MatchEvaluator(AnchorInlineEvaluator)); + + // Last, handle reference-style shortcuts: [link text] + // These must come last in case you've also got [link test][1] + // or [link test](/foo) + text = _anchorRefShortcut.Replace(text, new MatchEvaluator(AnchorRefShortcutEvaluator)); + return text; + } + + private string SaveFromAutoLinking(string s) + { + return s.Replace("://", AutoLinkPreventionMarker); + } + + private string AnchorRefEvaluator(Match match) + { + string wholeMatch = match.Groups[1].Value; + string linkText = SaveFromAutoLinking(match.Groups[2].Value); + string linkID = match.Groups[3].Value.ToLowerInvariant(); + + string result; + + // for shortcut links like [this][]. + if (linkID == "") + linkID = linkText.ToLowerInvariant(); + + if (_urls.ContainsKey(linkID)) { - // First, handle reference-style links: [link text] [id] - text = _anchorRef.Replace(text, new MatchEvaluator(AnchorRefEvaluator)); - - // Next, inline-style links: [link text](url "optional title") or [link text](url "optional title") - text = _anchorInline.Replace(text, new MatchEvaluator(AnchorInlineEvaluator)); - - // Last, handle reference-style shortcuts: [link text] - // These must come last in case you've also got [link test][1] - // or [link test](/foo) - text = _anchorRefShortcut.Replace(text, new MatchEvaluator(AnchorRefShortcutEvaluator)); - return text; - } - - private string SaveFromAutoLinking(string s) - { - return s.Replace("://", AutoLinkPreventionMarker); - } - - private string AnchorRefEvaluator(Match match) - { - string wholeMatch = match.Groups[1].Value; - string linkText = SaveFromAutoLinking(match.Groups[2].Value); - string linkID = match.Groups[3].Value.ToLowerInvariant(); - - string result; - - // for shortcut links like [this][]. - if (linkID == "") - linkID = linkText.ToLowerInvariant(); - - if (_urls.ContainsKey(linkID)) - { - string url = _urls[linkID]; - - url = EncodeProblemUrlChars(url); - url = EscapeBoldItalic(url); - result = ""; - } - else - result = wholeMatch; - - return result; - } - - private string AnchorRefShortcutEvaluator(Match match) - { - string wholeMatch = match.Groups[1].Value; - string linkText = SaveFromAutoLinking(match.Groups[2].Value); - string linkID = Regex.Replace(linkText.ToLowerInvariant(), @"[ ]*\n[ ]*", " "); // lower case and remove newlines / extra spaces - - string result; - - if (_urls.ContainsKey(linkID)) - { - string url = _urls[linkID]; - - url = EncodeProblemUrlChars(url); - url = EscapeBoldItalic(url); - result = ""; - } - else - result = wholeMatch; - - return result; - } - - - private string AnchorInlineEvaluator(Match match) - { - string linkText = SaveFromAutoLinking(match.Groups[2].Value); - string url = match.Groups[3].Value; - string title = match.Groups[6].Value; - string result; + string url = _urls[linkID]; url = EncodeProblemUrlChars(url); url = EscapeBoldItalic(url); - if (url.StartsWith("<") && url.EndsWith(">")) - url = url.Substring(1, url.Length - 2); // remove <>'s surrounding URL, if present + result = "{0}", linkText); - return result; + result += ">" + linkText + ""; + } + else + result = wholeMatch; + + return result; + } + + private string AnchorRefShortcutEvaluator(Match match) + { + string wholeMatch = match.Groups[1].Value; + string linkText = SaveFromAutoLinking(match.Groups[2].Value); + string linkID = Regex.Replace(linkText.ToLowerInvariant(), @"[ ]*\n[ ]*", " "); // lower case and remove newlines / extra spaces + + string result; + + if (_urls.ContainsKey(linkID)) + { + string url = _urls[linkID]; + + url = EncodeProblemUrlChars(url); + url = EscapeBoldItalic(url); + result = ""; + } + else + result = wholeMatch; + + return result; + } + + + private string AnchorInlineEvaluator(Match match) + { + string linkText = SaveFromAutoLinking(match.Groups[2].Value); + string url = match.Groups[3].Value; + string title = match.Groups[6].Value; + string result; + + url = EncodeProblemUrlChars(url); + url = EscapeBoldItalic(url); + if (url.StartsWith("<") && url.EndsWith(">")) + url = url.Substring(1, url.Length - 2); // remove <>'s surrounding URL, if present + + result = string.Format("{0}", linkText); + return result; + } + + private static Regex _imagesRef = new Regex(@" ( # wrap whole match in $1 !\[ (.*?) # alt text = $2 @@ -918,7 +916,7 @@ namespace Squirrel.MarkdownSharp )", RegexOptions.IgnorePatternWhitespace | RegexOptions.Singleline | RegexOptions.Compiled); - private static Regex _imagesInline = new Regex(String.Format(@" + private static Regex _imagesInline = new Regex(String.Format(@" ( # wrap whole match in $1 !\[ (.*?) # alt text = $2 @@ -936,148 +934,148 @@ namespace Squirrel.MarkdownSharp )? # title is optional \) )", GetNestedParensPattern()), - RegexOptions.IgnorePatternWhitespace | RegexOptions.Singleline | RegexOptions.Compiled); + RegexOptions.IgnorePatternWhitespace | RegexOptions.Singleline | RegexOptions.Compiled); - /// - /// Turn Markdown image shortcuts into HTML img tags. - /// - /// - /// ![alt text][id] - /// ![alt text](url "optional title") - /// - private string DoImages(string text) + /// + /// Turn Markdown image shortcuts into HTML img tags. + /// + /// + /// ![alt text][id] + /// ![alt text](url "optional title") + /// + private string DoImages(string text) + { + // First, handle reference-style labeled images: ![alt text][id] + text = _imagesRef.Replace(text, new MatchEvaluator(ImageReferenceEvaluator)); + + // Next, handle inline images: ![alt text](url "optional title") + // Don't forget: encode * and _ + text = _imagesInline.Replace(text, new MatchEvaluator(ImageInlineEvaluator)); + + return text; + } + + // This prevents the creation of horribly broken HTML when some syntax ambiguities + // collide. It likely still doesn't do what the user meant, but at least we're not + // outputting garbage. + private string EscapeImageAltText(string s) + { + s = EscapeBoldItalic(s); + s = Regex.Replace(s, @"[\[\]()]", m => _escapeTable[m.ToString()]); + return s; + } + + private string ImageReferenceEvaluator(Match match) + { + string wholeMatch = match.Groups[1].Value; + string altText = match.Groups[2].Value; + string linkID = match.Groups[3].Value.ToLowerInvariant(); + + // for shortcut links like ![this][]. + if (linkID == "") + linkID = altText.ToLowerInvariant(); + + if (_urls.ContainsKey(linkID)) { - // First, handle reference-style labeled images: ![alt text][id] - text = _imagesRef.Replace(text, new MatchEvaluator(ImageReferenceEvaluator)); + string url = _urls[linkID]; + string title = null; - // Next, handle inline images: ![alt text](url "optional title") - // Don't forget: encode * and _ - text = _imagesInline.Replace(text, new MatchEvaluator(ImageInlineEvaluator)); + if (_titles.ContainsKey(linkID)) + title = _titles[linkID]; - return text; + return ImageTag(url, altText, title); } - - // This prevents the creation of horribly broken HTML when some syntax ambiguities - // collide. It likely still doesn't do what the user meant, but at least we're not - // outputting garbage. - private string EscapeImageAltText(string s) + else { - s = EscapeBoldItalic(s); - s = Regex.Replace(s, @"[\[\]()]", m => _escapeTable[m.ToString()]); - return s; + // If there's no such link ID, leave intact: + return wholeMatch; } + } - private string ImageReferenceEvaluator(Match match) + private string ImageInlineEvaluator(Match match) + { + string alt = match.Groups[2].Value; + string url = match.Groups[3].Value; + string title = match.Groups[6].Value; + + if (url.StartsWith("<") && url.EndsWith(">")) + url = url.Substring(1, url.Length - 2); // Remove <>'s surrounding URL, if present + + return ImageTag(url, alt, title); + } + + private string ImageTag(string url, string altText, string title) + { + altText = EscapeImageAltText(AttributeEncode(altText)); + url = EncodeProblemUrlChars(url); + url = EscapeBoldItalic(url); + var result = string.Format("\"{1}\"",")) - url = url.Substring(1, url.Length - 2); // Remove <>'s surrounding URL, if present - - return ImageTag(url, alt, title); - } - - private string ImageTag(string url, string altText, string title) - { - altText = EscapeImageAltText(AttributeEncode(altText)); - url = EncodeProblemUrlChars(url); - url = EscapeBoldItalic(url); - var result = string.Format("\"{1}\"", - /// Turn Markdown headers into HTML header tags - ///
- /// - /// Header 1 - /// ======== - /// - /// Header 2 - /// -------- - /// - /// # Header 1 - /// ## Header 2 - /// ## Header 2 with closing hashes ## - /// ... - /// ###### Header 6 - /// - private string DoHeaders(string text) - { - text = _headerSetext.Replace(text, new MatchEvaluator(SetextHeaderEvaluator)); - text = _headerAtx.Replace(text, new MatchEvaluator(AtxHeaderEvaluator)); - return text; - } + /// + /// Turn Markdown headers into HTML header tags + /// + /// + /// Header 1 + /// ======== + /// + /// Header 2 + /// -------- + /// + /// # Header 1 + /// ## Header 2 + /// ## Header 2 with closing hashes ## + /// ... + /// ###### Header 6 + /// + private string DoHeaders(string text) + { + text = _headerSetext.Replace(text, new MatchEvaluator(SetextHeaderEvaluator)); + text = _headerAtx.Replace(text, new MatchEvaluator(AtxHeaderEvaluator)); + return text; + } - private string SetextHeaderEvaluator(Match match) - { - string header = match.Groups[1].Value; - int level = match.Groups[2].Value.StartsWith("=") ? 1 : 2; - return string.Format("{0}\n\n", RunSpanGamut(header), level); - } + private string SetextHeaderEvaluator(Match match) + { + string header = match.Groups[1].Value; + int level = match.Groups[2].Value.StartsWith("=") ? 1 : 2; + return string.Format("{0}\n\n", RunSpanGamut(header), level); + } - private string AtxHeaderEvaluator(Match match) - { - string header = match.Groups[2].Value; - int level = match.Groups[1].Value.Length; - return string.Format("{0}\n\n", RunSpanGamut(header), level); - } + private string AtxHeaderEvaluator(Match match) + { + string header = match.Groups[2].Value; + int level = match.Groups[1].Value.Length; + return string.Format("{0}\n\n", RunSpanGamut(header), level); + } - private static Regex _horizontalRules = new Regex(@" + private static Regex _horizontalRules = new Regex(@" ^[ ]{0,3} # Leading space ([-*_]) # $1: First marker (?> # Repeated marker group @@ -1088,21 +1086,21 @@ namespace Squirrel.MarkdownSharp $ # End of line. ", RegexOptions.Multiline | RegexOptions.IgnorePatternWhitespace | RegexOptions.Compiled); - /// - /// Turn Markdown horizontal rules into HTML hr tags - /// - /// - /// *** - /// * * * - /// --- - /// - - - - /// - private string DoHorizontalRules(string text) - { - return _horizontalRules.Replace(text, " + /// Turn Markdown horizontal rules into HTML hr tags + ///
+ /// + /// *** + /// * * * + /// --- + /// - - - + /// + private string DoHorizontalRules(string text) + { + return _horizontalRules.Replace(text, " - /// Turn Markdown lists into HTML ul and ol and li tags - ///
- private string DoLists(string text, bool isInsideParagraphlessListItem = false) - { - // We use a different prefix before nested lists than top-level lists. - // See extended comment in _ProcessListItems(). - if (_listLevel > 0) - text = _listNested.Replace(text, GetListEvaluator(isInsideParagraphlessListItem)); - else - text = _listTopLevel.Replace(text, GetListEvaluator(false)); + /// + /// Turn Markdown lists into HTML ul and ol and li tags + /// + private string DoLists(string text, bool isInsideParagraphlessListItem = false) + { + // We use a different prefix before nested lists than top-level lists. + // See extended comment in _ProcessListItems(). + if (_listLevel > 0) + text = _listNested.Replace(text, GetListEvaluator(isInsideParagraphlessListItem)); + else + text = _listTopLevel.Replace(text, GetListEvaluator(false)); - return text; - } + return text; + } - private MatchEvaluator GetListEvaluator(bool isInsideParagraphlessListItem = false) - { - return new MatchEvaluator(match => - { - string list = match.Groups[1].Value; - string listType = Regex.IsMatch(match.Groups[3].Value, _markerUL) ? "ul" : "ol"; - string result; + private MatchEvaluator GetListEvaluator(bool isInsideParagraphlessListItem = false) + { + return new MatchEvaluator(match => + { + string list = match.Groups[1].Value; + string listType = Regex.IsMatch(match.Groups[3].Value, _markerUL) ? "ul" : "ol"; + string result; - result = ProcessListItems(list, listType == "ul" ? _markerUL : _markerOL, isInsideParagraphlessListItem); + result = ProcessListItems(list, listType == "ul" ? _markerUL : _markerOL, isInsideParagraphlessListItem); - result = string.Format("<{0}>\n{1}\n", listType, result); - return result; - }); - } + result = string.Format("<{0}>\n{1}\n", listType, result); + return result; + }); + } - /// - /// Process the contents of a single ordered or unordered list, splitting it - /// into individual list items. - /// - private string ProcessListItems(string list, string marker, bool isInsideParagraphlessListItem = false) - { - // The listLevel global keeps track of when we're inside a list. - // Each time we enter a list, we increment it; when we leave a list, - // we decrement. If it's zero, we're not in a list anymore. + /// + /// Process the contents of a single ordered or unordered list, splitting it + /// into individual list items. + /// + private string ProcessListItems(string list, string marker, bool isInsideParagraphlessListItem = false) + { + // The listLevel global keeps track of when we're inside a list. + // Each time we enter a list, we increment it; when we leave a list, + // we decrement. If it's zero, we're not in a list anymore. - // We do this because when we're not inside a list, we want to treat - // something like this: + // We do this because when we're not inside a list, we want to treat + // something like this: - // I recommend upgrading to version - // 8. Oops, now this line is treated - // as a sub-list. + // I recommend upgrading to version + // 8. Oops, now this line is treated + // as a sub-list. - // As a single paragraph, despite the fact that the second line starts - // with a digit-period-space sequence. + // As a single paragraph, despite the fact that the second line starts + // with a digit-period-space sequence. - // Whereas when we're inside a list (or sub-list), that line will be - // treated as the start of a sub-list. What a kludge, huh? This is - // an aspect of Markdown's syntax that's hard to parse perfectly - // without resorting to mind-reading. Perhaps the solution is to - // change the syntax rules such that sub-lists must start with a - // starting cardinal number; e.g. "1." or "a.". + // Whereas when we're inside a list (or sub-list), that line will be + // treated as the start of a sub-list. What a kludge, huh? This is + // an aspect of Markdown's syntax that's hard to parse perfectly + // without resorting to mind-reading. Perhaps the solution is to + // change the syntax rules such that sub-lists must start with a + // starting cardinal number; e.g. "1." or "a.". - _listLevel++; + _listLevel++; - // Trim trailing blank lines: - list = Regex.Replace(list, @"\n{2,}\z", "\n"); + // Trim trailing blank lines: + list = Regex.Replace(list, @"\n{2,}\z", "\n"); - string pattern = string.Format( - @"(^[ ]*) # leading whitespace = $1 + string pattern = string.Format( + @"(^[ ]*) # leading whitespace = $1 ({0}) [ ]+ # list marker = $2 ((?s:.+?) # list item text = $3 (\n+)) (?= (\z | \1 ({0}) [ ]+))", marker); - bool lastItemHadADoubleNewline = false; + bool lastItemHadADoubleNewline = false; - // has to be a closure, so subsequent invocations can share the bool - MatchEvaluator ListItemEvaluator = (Match match) => + // has to be a closure, so subsequent invocations can share the bool + MatchEvaluator ListItemEvaluator = (Match match) => + { + string item = match.Groups[3].Value; + + bool endsWithDoubleNewline = item.EndsWith("\n\n"); + bool containsDoubleNewline = endsWithDoubleNewline || item.Contains("\n\n"); + + if (containsDoubleNewline || lastItemHadADoubleNewline) + // we could correct any bad indentation here.. + item = RunBlockGamut(Outdent(item) + "\n", unhash: false); + else { - string item = match.Groups[3].Value; + // recursion for sub-lists + item = DoLists(Outdent(item), isInsideParagraphlessListItem: true); + item = item.TrimEnd('\n'); + if (!isInsideParagraphlessListItem) // only the outer-most item should run this, otherwise it's run multiple times for the inner ones + item = RunSpanGamut(item); + } + lastItemHadADoubleNewline = endsWithDoubleNewline; + return string.Format("
  • {0}
  • \n", item); + }; - bool endsWithDoubleNewline = item.EndsWith("\n\n"); - bool containsDoubleNewline = endsWithDoubleNewline || item.Contains("\n\n"); + list = Regex.Replace(list, pattern, new MatchEvaluator(ListItemEvaluator), + RegexOptions.IgnorePatternWhitespace | RegexOptions.Multiline); + _listLevel--; + return list; + } - if (containsDoubleNewline || lastItemHadADoubleNewline) - // we could correct any bad indentation here.. - item = RunBlockGamut(Outdent(item) + "\n", unhash: false); - else - { - // recursion for sub-lists - item = DoLists(Outdent(item), isInsideParagraphlessListItem: true); - item = item.TrimEnd('\n'); - if (!isInsideParagraphlessListItem) // only the outer-most item should run this, otherwise it's run multiple times for the inner ones - item = RunSpanGamut(item); - } - lastItemHadADoubleNewline = endsWithDoubleNewline; - return string.Format("
  • {0}
  • \n", item); - }; - - list = Regex.Replace(list, pattern, new MatchEvaluator(ListItemEvaluator), - RegexOptions.IgnorePatternWhitespace | RegexOptions.Multiline); - _listLevel--; - return list; - } - - private static Regex _codeBlock = new Regex(string.Format(@" + private static Regex _codeBlock = new Regex(string.Format(@" (?:\n\n|\A\n?) ( # $1 = the code block -- one or more lines, starting with a space (?: @@ -1237,28 +1235,28 @@ namespace Squirrel.MarkdownSharp )+ ) ((?=^[ ]{{0,{0}}}[^ \t\n])|\Z) # Lookahead for non-space at line-start, or end of doc", - _tabWidth), RegexOptions.Multiline | RegexOptions.IgnorePatternWhitespace | RegexOptions.Compiled); + _tabWidth), RegexOptions.Multiline | RegexOptions.IgnorePatternWhitespace | RegexOptions.Compiled); - /// - /// /// Turn Markdown 4-space indented code into HTML pre code blocks - /// - private string DoCodeBlocks(string text) - { - text = _codeBlock.Replace(text, new MatchEvaluator(CodeBlockEvaluator)); - return text; - } + /// + /// /// Turn Markdown 4-space indented code into HTML pre code blocks + /// + private string DoCodeBlocks(string text) + { + text = _codeBlock.Replace(text, new MatchEvaluator(CodeBlockEvaluator)); + return text; + } - private string CodeBlockEvaluator(Match match) - { - string codeBlock = match.Groups[1].Value; + private string CodeBlockEvaluator(Match match) + { + string codeBlock = match.Groups[1].Value; - codeBlock = EncodeCode(Outdent(codeBlock)); - codeBlock = _newlinesLeadingTrailing.Replace(codeBlock, ""); + codeBlock = EncodeCode(Outdent(codeBlock)); + codeBlock = _newlinesLeadingTrailing.Replace(codeBlock, ""); - return string.Concat("\n\n
    ", codeBlock, "\n
    \n\n"); - } + return string.Concat("\n\n
    ", codeBlock, "\n
    \n\n"); + } - private static Regex _codeSpan = new Regex(@" + private static Regex _codeSpan = new Regex(@" (? - /// Turn Markdown `code spans` into HTML code tags - ///
    - private string DoCodeSpans(string text) + /// + /// Turn Markdown `code spans` into HTML code tags + /// + private string DoCodeSpans(string text) + { + // * You can use multiple backticks as the delimiters if you want to + // include literal backticks in the code span. So, this input: + // + // Just type ``foo `bar` baz`` at the prompt. + // + // Will translate to: + // + //

    Just type foo `bar` baz at the prompt.

    + // + // There's no arbitrary limit to the number of backticks you + // can use as delimters. If you need three consecutive backticks + // in your code, use four for delimiters, etc. + // + // * You can use spaces to get literal backticks at the edges: + // + // ... type `` `bar` `` ... + // + // Turns to: + // + // ... type `bar` ... + // + + return _codeSpan.Replace(text, new MatchEvaluator(CodeSpanEvaluator)); + } + + private string CodeSpanEvaluator(Match match) + { + string span = match.Groups[2].Value; + span = Regex.Replace(span, @"^[ ]*", ""); // leading whitespace + span = Regex.Replace(span, @"[ ]*$", ""); // trailing whitespace + span = EncodeCode(span); + span = SaveFromAutoLinking(span); // to prevent auto-linking. Not necessary in code *blocks*, but in code spans. + + return string.Concat("", span, ""); + } + + + private static Regex _bold = new Regex(@"(\*\*|__) (?=\S) (.+?[*_]*) (?<=\S) \1", + RegexOptions.IgnorePatternWhitespace | RegexOptions.Singleline | RegexOptions.Compiled); + private static Regex _strictBold = new Regex(@"(^|[\W_])(?:(?!\1)|(?=^))(\*|_)\2(?=\S)(.*?\S)\2\2(?!\2)(?=[\W_]|$)", + RegexOptions.Singleline | RegexOptions.Compiled); + + private static Regex _italic = new Regex(@"(\*|_) (?=\S) (.+?) (?<=\S) \1", + RegexOptions.IgnorePatternWhitespace | RegexOptions.Singleline | RegexOptions.Compiled); + private static Regex _strictItalic = new Regex(@"(^|[\W_])(?:(?!\1)|(?=^))(\*|_)(?=\S)((?:(?!\2).)*?\S)\2(?!\2)(?=[\W_]|$)", + RegexOptions.Singleline | RegexOptions.Compiled); + + /// + /// Turn Markdown *italics* and **bold** into HTML strong and em tags + /// + private string DoItalicsAndBold(string text) + { + + // must go first, then + if (_strictBoldItalic) { - // * You can use multiple backticks as the delimiters if you want to - // include literal backticks in the code span. So, this input: - // - // Just type ``foo `bar` baz`` at the prompt. - // - // Will translate to: - // - //

    Just type foo `bar` baz at the prompt.

    - // - // There's no arbitrary limit to the number of backticks you - // can use as delimters. If you need three consecutive backticks - // in your code, use four for delimiters, etc. - // - // * You can use spaces to get literal backticks at the edges: - // - // ... type `` `bar` `` ... - // - // Turns to: - // - // ... type `bar` ... - // - - return _codeSpan.Replace(text, new MatchEvaluator(CodeSpanEvaluator)); + text = _strictBold.Replace(text, "$1$3"); + text = _strictItalic.Replace(text, "$1$3"); } - - private string CodeSpanEvaluator(Match match) + else { - string span = match.Groups[2].Value; - span = Regex.Replace(span, @"^[ ]*", ""); // leading whitespace - span = Regex.Replace(span, @"[ ]*$", ""); // trailing whitespace - span = EncodeCode(span); - span = SaveFromAutoLinking(span); // to prevent auto-linking. Not necessary in code *blocks*, but in code spans. - - return string.Concat("", span, ""); + text = _bold.Replace(text, "$2"); + text = _italic.Replace(text, "$2"); } + return text; + } + /// + /// Turn markdown line breaks (two space at end of line) into HTML break tags + /// + private string DoHardBreaks(string text) + { + if (_autoNewlines) + text = Regex.Replace(text, @"\n", string.Format(" - /// Turn Markdown *italics* and **bold** into HTML strong and em tags - ///
    - private string DoItalicsAndBold(string text) - { - - // must go first, then - if (_strictBoldItalic) - { - text = _strictBold.Replace(text, "$1$3"); - text = _strictItalic.Replace(text, "$1$3"); - } - else - { - text = _bold.Replace(text, "$2"); - text = _italic.Replace(text, "$2"); - } - return text; - } - - /// - /// Turn markdown line breaks (two space at end of line) into HTML break tags - /// - private string DoHardBreaks(string text) - { - if (_autoNewlines) - text = Regex.Replace(text, @"\n", string.Format("[ ]? # '>' at the start of a line @@ -1361,118 +1359,118 @@ namespace Squirrel.MarkdownSharp )+ )", RegexOptions.IgnorePatternWhitespace | RegexOptions.Multiline | RegexOptions.Compiled); - /// - /// Turn Markdown > quoted blocks into HTML blockquote blocks - /// - private string DoBlockQuotes(string text) + /// + /// Turn Markdown > quoted blocks into HTML blockquote blocks + /// + private string DoBlockQuotes(string text) + { + return _blockquote.Replace(text, new MatchEvaluator(BlockQuoteEvaluator)); + } + + private string BlockQuoteEvaluator(Match match) + { + string bq = match.Groups[1].Value; + + bq = Regex.Replace(bq, @"^[ ]*>[ ]?", "", RegexOptions.Multiline); // trim one level of quoting + bq = Regex.Replace(bq, @"^[ ]+$", "", RegexOptions.Multiline); // trim whitespace-only lines + bq = RunBlockGamut(bq); // recurse + + bq = Regex.Replace(bq, @"^", " ", RegexOptions.Multiline); + + // These leading spaces screw with
     content, so we need to fix that:
    +        bq = Regex.Replace(bq, @"(\s*
    .+?
    )", new MatchEvaluator(BlockQuoteEvaluator2), RegexOptions.IgnorePatternWhitespace | RegexOptions.Singleline); + + bq = string.Format("
    \n{0}\n
    ", bq); + string key = GetHashKey(bq, isHtmlBlock: true); + _htmlBlocks[key] = bq; + + return "\n\n" + key + "\n\n"; + } + + private string BlockQuoteEvaluator2(Match match) + { + return Regex.Replace(match.Groups[1].Value, @"^ ", "", RegexOptions.Multiline); + } + + private const string _charInsideUrl = @"[-A-Z0-9+&@#/%?=~_|\[\]\(\)!:,\.;" + "\x1a]"; + private const string _charEndingUrl = "[-A-Z0-9+&@#/%=~_|\\[\\])]"; + + private static Regex _autolinkBare = new Regex(@"(<|="")?\b(https?|ftp)(://" + _charInsideUrl + "*" + _charEndingUrl + ")(?=$|\\W)", + RegexOptions.IgnoreCase | RegexOptions.Compiled); + + private static Regex _endCharRegex = new Regex(_charEndingUrl, RegexOptions.IgnoreCase | RegexOptions.Compiled); + + private static string handleTrailingParens(Match match) + { + // The first group is essentially a negative lookbehind -- if there's a < or a =", we don't touch this. + // We're not using a *real* lookbehind, because of links with in links, like + // With a real lookbehind, the full link would never be matched, and thus the http://www.google.com *would* be matched. + // With the simulated lookbehind, the full link *is* matched (just not handled, because of this early return), causing + // the google link to not be matched again. + if (match.Groups[1].Success) + return match.Value; + + var protocol = match.Groups[2].Value; + var link = match.Groups[3].Value; + if (!link.EndsWith(")")) + return "<" + protocol + link + ">"; + var level = 0; + foreach (Match c in Regex.Matches(link, "[()]")) { - return _blockquote.Replace(text, new MatchEvaluator(BlockQuoteEvaluator)); - } - - private string BlockQuoteEvaluator(Match match) - { - string bq = match.Groups[1].Value; - - bq = Regex.Replace(bq, @"^[ ]*>[ ]?", "", RegexOptions.Multiline); // trim one level of quoting - bq = Regex.Replace(bq, @"^[ ]+$", "", RegexOptions.Multiline); // trim whitespace-only lines - bq = RunBlockGamut(bq); // recurse - - bq = Regex.Replace(bq, @"^", " ", RegexOptions.Multiline); - - // These leading spaces screw with
     content, so we need to fix that:
    -            bq = Regex.Replace(bq, @"(\s*
    .+?
    )", new MatchEvaluator(BlockQuoteEvaluator2), RegexOptions.IgnorePatternWhitespace | RegexOptions.Singleline); - - bq = string.Format("
    \n{0}\n
    ", bq); - string key = GetHashKey(bq, isHtmlBlock: true); - _htmlBlocks[key] = bq; - - return "\n\n" + key + "\n\n"; - } - - private string BlockQuoteEvaluator2(Match match) - { - return Regex.Replace(match.Groups[1].Value, @"^ ", "", RegexOptions.Multiline); - } - - private const string _charInsideUrl = @"[-A-Z0-9+&@#/%?=~_|\[\]\(\)!:,\.;" + "\x1a]"; - private const string _charEndingUrl = "[-A-Z0-9+&@#/%=~_|\\[\\])]"; - - private static Regex _autolinkBare = new Regex(@"(<|="")?\b(https?|ftp)(://" + _charInsideUrl + "*" + _charEndingUrl + ")(?=$|\\W)", - RegexOptions.IgnoreCase | RegexOptions.Compiled); - - private static Regex _endCharRegex = new Regex(_charEndingUrl, RegexOptions.IgnoreCase | RegexOptions.Compiled); - - private static string handleTrailingParens(Match match) - { - // The first group is essentially a negative lookbehind -- if there's a < or a =", we don't touch this. - // We're not using a *real* lookbehind, because of links with in links, like
    - // With a real lookbehind, the full link would never be matched, and thus the http://www.google.com *would* be matched. - // With the simulated lookbehind, the full link *is* matched (just not handled, because of this early return), causing - // the google link to not be matched again. - if (match.Groups[1].Success) - return match.Value; - - var protocol = match.Groups[2].Value; - var link = match.Groups[3].Value; - if (!link.EndsWith(")")) - return "<" + protocol + link + ">"; - var level = 0; - foreach (Match c in Regex.Matches(link, "[()]")) + if (c.Value == "(") { - if (c.Value == "(") - { - if (level <= 0) - level = 1; - else - level++; - } + if (level <= 0) + level = 1; else - { - level--; - } + level++; } - var tail = ""; - if (level < 0) + else { - link = Regex.Replace(link, @"\){1," + (-level) + "}$", m => { tail = m.Value; return ""; }); + level--; } - if (tail.Length > 0) + } + var tail = ""; + if (level < 0) + { + link = Regex.Replace(link, @"\){1," + (-level) + "}$", m => { tail = m.Value; return ""; }); + } + if (tail.Length > 0) + { + var lastChar = link[link.Length - 1]; + if (!_endCharRegex.IsMatch(lastChar.ToString())) { - var lastChar = link[link.Length - 1]; - if (!_endCharRegex.IsMatch(lastChar.ToString())) - { - tail = lastChar + tail; - link = link.Substring(0, link.Length - 1); - } + tail = lastChar + tail; + link = link.Substring(0, link.Length - 1); } - return "<" + protocol + link + ">" + tail; + } + return "<" + protocol + link + ">" + tail; + } + + /// + /// Turn angle-delimited URLs into HTML anchor tags + /// + /// + /// <http://www.example.com> + /// + private string DoAutoLinks(string text) + { + + if (_autoHyperlink) + { + // fixup arbitrary URLs by adding Markdown < > so they get linked as well + // note that at this point, all other URL in the text are already hyperlinked as + // *except* for the case + text = _autolinkBare.Replace(text, handleTrailingParens); } - /// - /// Turn angle-delimited URLs into HTML anchor tags - /// - /// - /// <http://www.example.com> - /// - private string DoAutoLinks(string text) + // Hyperlinks: + text = Regex.Replace(text, "<((https?|ftp):[^'\">\\s]+)>", new MatchEvaluator(HyperlinkEvaluator)); + + if (_linkEmails) { - - if (_autoHyperlink) - { - // fixup arbitrary URLs by adding Markdown < > so they get linked as well - // note that at this point, all other URL in the text are already hyperlinked as - // *except* for the case - text = _autolinkBare.Replace(text, handleTrailingParens); - } - - // Hyperlinks: - text = Regex.Replace(text, "<((https?|ftp):[^'\">\\s]+)>", new MatchEvaluator(HyperlinkEvaluator)); - - if (_linkEmails) - { - // Email addresses: - string pattern = - @"< + // Email addresses: + string pattern = + @"< (?:mailto:)? ( [-.\w]+ @@ -1480,299 +1478,298 @@ namespace Squirrel.MarkdownSharp [-a-z0-9]+(\.[-a-z0-9]+)*\.[a-z]+ ) >"; - text = Regex.Replace(text, pattern, new MatchEvaluator(EmailEvaluator), RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace); + text = Regex.Replace(text, pattern, new MatchEvaluator(EmailEvaluator), RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace); + } + + return text; + } + + private string HyperlinkEvaluator(Match match) + { + string link = match.Groups[1].Value; + return string.Format("{1}", EscapeBoldItalic(EncodeProblemUrlChars(link)), link); + } + + private string EmailEvaluator(Match match) + { + string email = Unescape(match.Groups[1].Value); + + // + // Input: an email address, e.g. "foo@example.com" + // + // Output: the email address as a mailto link, with each character + // of the address encoded as either a decimal or hex entity, in + // the hopes of foiling most address harvesting spam bots. E.g.: + // + // foo + // @example.com + // + // Based by a filter by Matthew Wickline, posted to the BBEdit-Talk + // mailing list: + // + email = "mailto:" + email; + + // leave ':' alone (to spot mailto: later) + email = EncodeEmailAddress(email); + + email = string.Format("{0}", email); + + // strip the mailto: from the visible part + email = Regex.Replace(email, "\">.+?:", "\">"); + return email; + } + + + private static Regex _outDent = new Regex(@"^[ ]{1," + _tabWidth + @"}", RegexOptions.Multiline | RegexOptions.Compiled); + + /// + /// Remove one level of line-leading spaces + /// + private string Outdent(string block) + { + return _outDent.Replace(block, ""); + } + + + #region Encoding and Normalization + + + /// + /// encodes email address randomly + /// roughly 10% raw, 45% hex, 45% dec + /// note that @ is always encoded and : never is + /// + private string EncodeEmailAddress(string addr) + { + var sb = new StringBuilder(addr.Length * 5); + var rand = new Random(); + int r; + foreach (char c in addr) + { + r = rand.Next(1, 100); + if ((r > 90 || c == ':') && c != '@') + sb.Append(c); // m + else if (r < 45) + sb.AppendFormat("&#x{0:x};", (int)c); // m + else + sb.AppendFormat("&#{0};", (int)c); // m + } + return sb.ToString(); + } + + private static Regex _codeEncoder = new Regex(@"&|<|>|\\|\*|_|\{|\}|\[|\]", RegexOptions.Compiled); + + /// + /// Encode/escape certain Markdown characters inside code blocks and spans where they are literals + /// + private string EncodeCode(string code) + { + return _codeEncoder.Replace(code, EncodeCodeEvaluator); + } + private string EncodeCodeEvaluator(Match match) + { + switch (match.Value) + { + // Encode all ampersands; HTML entities are not + // entities within a Markdown code span. + case "&": + return "&"; + // Do the angle bracket song and dance + case "<": + return "<"; + case ">": + return ">"; + // escape characters that are magic in Markdown + default: + return _escapeTable[match.Value]; + } + } + + + private static Regex _amps = new Regex(@"&(?!((#[0-9]+)|(#[xX][a-fA-F0-9]+)|([a-zA-Z][a-zA-Z0-9]*));)", RegexOptions.ExplicitCapture | RegexOptions.Compiled); + private static Regex _angles = new Regex(@"<(?![A-Za-z/?\$!])", RegexOptions.ExplicitCapture | RegexOptions.Compiled); + + /// + /// Encode any ampersands (that aren't part of an HTML entity) and left or right angle brackets + /// + private string EncodeAmpsAndAngles(string s) + { + s = _amps.Replace(s, "&"); + s = _angles.Replace(s, "<"); + return s; + } + + private static Regex _backslashEscapes; + + /// + /// Encodes any escaped characters such as \`, \*, \[ etc + /// + private string EscapeBackslashes(string s) + { + return _backslashEscapes.Replace(s, new MatchEvaluator(EscapeBackslashesEvaluator)); + } + private string EscapeBackslashesEvaluator(Match match) + { + return _backslashEscapeTable[match.Value]; + } + + private static Regex _unescapes = new Regex("\x1A" + "E\\d+E", RegexOptions.Compiled); + + /// + /// swap back in all the special characters we've hidden + /// + private string Unescape(string s) + { + return _unescapes.Replace(s, new MatchEvaluator(UnescapeEvaluator)); + } + private string UnescapeEvaluator(Match match) + { + return _invertedEscapeTable[match.Value]; + } + + + /// + /// escapes Bold [ * ] and Italic [ _ ] characters + /// + private string EscapeBoldItalic(string s) + { + s = s.Replace("*", _escapeTable["*"]); + s = s.Replace("_", _escapeTable["_"]); + return s; + } + + private static string AttributeEncode(string s) + { + return s.Replace(">", ">").Replace("<", "<").Replace("\"", """); + } + + private static readonly char[] _problemUrlChars = @"""'*()[]$:".ToCharArray(); + + /// + /// hex-encodes some unusual "problem" chars in URLs to avoid URL detection problems + /// + private string EncodeProblemUrlChars(string url) + { + if (!_encodeProblemUrlCharacters) return url; + + var sb = new StringBuilder(url.Length); + bool encode; + char c; + + for (int i = 0; i < url.Length; i++) + { + c = url[i]; + encode = Array.IndexOf(_problemUrlChars, c) != -1; + if (encode && c == ':' && i < url.Length - 1) + encode = !(url[i + 1] == '/') && !(url[i + 1] >= '0' && url[i + 1] <= '9'); + + if (encode) + sb.Append("%" + String.Format("{0:x}", (byte)c)); + else + sb.Append(c); + } + + return sb.ToString(); + } + + + /// + /// Within tags -- meaning between < and > -- encode [\ ` * _] so they + /// don't conflict with their use in Markdown for code, italics and strong. + /// We're replacing each such character with its corresponding hash + /// value; this is likely overkill, but it should prevent us from colliding + /// with the escape values by accident. + /// + private string EscapeSpecialCharsWithinTagAttributes(string text) + { + var tokens = TokenizeHTML(text); + + // now, rebuild text from the tokens + var sb = new StringBuilder(text.Length); + + foreach (var token in tokens) + { + string value = token.Value; + + if (token.Type == TokenType.Tag) + { + value = value.Replace(@"\", _escapeTable[@"\"]); + + if (_autoHyperlink && value.StartsWith("(?=.)", _escapeTable[@"`"]); + value = EscapeBoldItalic(value); } - return text; + sb.Append(value); } - private string HyperlinkEvaluator(Match match) + return sb.ToString(); + } + + /// + /// convert all tabs to _tabWidth spaces; + /// standardizes line endings from DOS (CR LF) or Mac (CR) to UNIX (LF); + /// makes sure text ends with a couple of newlines; + /// removes any blank lines (only spaces) in the text + /// + private string Normalize(string text) + { + var output = new StringBuilder(text.Length); + var line = new StringBuilder(); + bool valid = false; + + for (int i = 0; i < text.Length; i++) { - string link = match.Groups[1].Value; - return string.Format("{1}", EscapeBoldItalic(EncodeProblemUrlChars(link)), link); - } - - private string EmailEvaluator(Match match) - { - string email = Unescape(match.Groups[1].Value); - - // - // Input: an email address, e.g. "foo@example.com" - // - // Output: the email address as a mailto link, with each character - // of the address encoded as either a decimal or hex entity, in - // the hopes of foiling most address harvesting spam bots. E.g.: - // - // foo - // @example.com - // - // Based by a filter by Matthew Wickline, posted to the BBEdit-Talk - // mailing list: - // - email = "mailto:" + email; - - // leave ':' alone (to spot mailto: later) - email = EncodeEmailAddress(email); - - email = string.Format("{0}", email); - - // strip the mailto: from the visible part - email = Regex.Replace(email, "\">.+?:", "\">"); - return email; - } - - - private static Regex _outDent = new Regex(@"^[ ]{1," + _tabWidth + @"}", RegexOptions.Multiline | RegexOptions.Compiled); - - /// - /// Remove one level of line-leading spaces - /// - private string Outdent(string block) - { - return _outDent.Replace(block, ""); - } - - - #region Encoding and Normalization - - - /// - /// encodes email address randomly - /// roughly 10% raw, 45% hex, 45% dec - /// note that @ is always encoded and : never is - /// - private string EncodeEmailAddress(string addr) - { - var sb = new StringBuilder(addr.Length * 5); - var rand = new Random(); - int r; - foreach (char c in addr) + switch (text[i]) { - r = rand.Next(1, 100); - if ((r > 90 || c == ':') && c != '@') - sb.Append(c); // m - else if (r < 45) - sb.AppendFormat("&#x{0:x};", (int)c); // m - else - sb.AppendFormat("&#{0};", (int)c); // m - } - return sb.ToString(); - } - - private static Regex _codeEncoder = new Regex(@"&|<|>|\\|\*|_|\{|\}|\[|\]", RegexOptions.Compiled); - - /// - /// Encode/escape certain Markdown characters inside code blocks and spans where they are literals - /// - private string EncodeCode(string code) - { - return _codeEncoder.Replace(code, EncodeCodeEvaluator); - } - private string EncodeCodeEvaluator(Match match) - { - switch (match.Value) - { - // Encode all ampersands; HTML entities are not - // entities within a Markdown code span. - case "&": - return "&"; - // Do the angle bracket song and dance - case "<": - return "<"; - case ">": - return ">"; - // escape characters that are magic in Markdown - default: - return _escapeTable[match.Value]; - } - } - - - private static Regex _amps = new Regex(@"&(?!((#[0-9]+)|(#[xX][a-fA-F0-9]+)|([a-zA-Z][a-zA-Z0-9]*));)", RegexOptions.ExplicitCapture | RegexOptions.Compiled); - private static Regex _angles = new Regex(@"<(?![A-Za-z/?\$!])", RegexOptions.ExplicitCapture | RegexOptions.Compiled); - - /// - /// Encode any ampersands (that aren't part of an HTML entity) and left or right angle brackets - /// - private string EncodeAmpsAndAngles(string s) - { - s = _amps.Replace(s, "&"); - s = _angles.Replace(s, "<"); - return s; - } - - private static Regex _backslashEscapes; - - /// - /// Encodes any escaped characters such as \`, \*, \[ etc - /// - private string EscapeBackslashes(string s) - { - return _backslashEscapes.Replace(s, new MatchEvaluator(EscapeBackslashesEvaluator)); - } - private string EscapeBackslashesEvaluator(Match match) - { - return _backslashEscapeTable[match.Value]; - } - - private static Regex _unescapes = new Regex("\x1A" + "E\\d+E", RegexOptions.Compiled); - - /// - /// swap back in all the special characters we've hidden - /// - private string Unescape(string s) - { - return _unescapes.Replace(s, new MatchEvaluator(UnescapeEvaluator)); - } - private string UnescapeEvaluator(Match match) - { - return _invertedEscapeTable[match.Value]; - } - - - /// - /// escapes Bold [ * ] and Italic [ _ ] characters - /// - private string EscapeBoldItalic(string s) - { - s = s.Replace("*", _escapeTable["*"]); - s = s.Replace("_", _escapeTable["_"]); - return s; - } - - private static string AttributeEncode(string s) - { - return s.Replace(">", ">").Replace("<", "<").Replace("\"", """); - } - - private static readonly char[] _problemUrlChars = @"""'*()[]$:".ToCharArray(); - - /// - /// hex-encodes some unusual "problem" chars in URLs to avoid URL detection problems - /// - private string EncodeProblemUrlChars(string url) - { - if (!_encodeProblemUrlCharacters) return url; - - var sb = new StringBuilder(url.Length); - bool encode; - char c; - - for (int i = 0; i < url.Length; i++) - { - c = url[i]; - encode = Array.IndexOf(_problemUrlChars, c) != -1; - if (encode && c == ':' && i < url.Length - 1) - encode = !(url[i + 1] == '/') && !(url[i + 1] >= '0' && url[i + 1] <= '9'); - - if (encode) - sb.Append("%" + String.Format("{0:x}", (byte)c)); - else - sb.Append(c); - } - - return sb.ToString(); - } - - - /// - /// Within tags -- meaning between < and > -- encode [\ ` * _] so they - /// don't conflict with their use in Markdown for code, italics and strong. - /// We're replacing each such character with its corresponding hash - /// value; this is likely overkill, but it should prevent us from colliding - /// with the escape values by accident. - /// - private string EscapeSpecialCharsWithinTagAttributes(string text) - { - var tokens = TokenizeHTML(text); - - // now, rebuild text from the tokens - var sb = new StringBuilder(text.Length); - - foreach (var token in tokens) - { - string value = token.Value; - - if (token.Type == TokenType.Tag) - { - value = value.Replace(@"\", _escapeTable[@"\"]); - - if (_autoHyperlink && value.StartsWith("(?=.)", _escapeTable[@"`"]); - value = EscapeBoldItalic(value); - } - - sb.Append(value); - } - - return sb.ToString(); - } - - /// - /// convert all tabs to _tabWidth spaces; - /// standardizes line endings from DOS (CR LF) or Mac (CR) to UNIX (LF); - /// makes sure text ends with a couple of newlines; - /// removes any blank lines (only spaces) in the text - /// - private string Normalize(string text) - { - var output = new StringBuilder(text.Length); - var line = new StringBuilder(); - bool valid = false; - - for (int i = 0; i < text.Length; i++) - { - switch (text[i]) - { - case '\n': + case '\n': + if (valid) output.Append(line); + output.Append('\n'); + line.Length = 0; valid = false; + break; + case '\r': + if ((i < text.Length - 1) && (text[i + 1] != '\n')) + { if (valid) output.Append(line); output.Append('\n'); line.Length = 0; valid = false; - break; - case '\r': - if ((i < text.Length - 1) && (text[i + 1] != '\n')) - { - if (valid) output.Append(line); - output.Append('\n'); - line.Length = 0; valid = false; - } - break; - case '\t': - int width = (_tabWidth - line.Length % _tabWidth); - for (int k = 0; k < width; k++) - line.Append(' '); - break; - case '\x1A': - break; - default: - if (!valid && text[i] != ' ') valid = true; - line.Append(text[i]); - break; - } + } + break; + case '\t': + int width = (_tabWidth - line.Length % _tabWidth); + for (int k = 0; k < width; k++) + line.Append(' '); + break; + case '\x1A': + break; + default: + if (!valid && text[i] != ' ') valid = true; + line.Append(text[i]); + break; } - - if (valid) output.Append(line); - output.Append('\n'); - - // add two newlines to the end before return - return output.Append("\n\n").ToString(); } - #endregion - - /// - /// this is to emulate what's evailable in PHP - /// - private static string RepeatString(string text, int count) - { - var sb = new StringBuilder(text.Length * count); - for (int i = 0; i < count; i++) - sb.Append(text); - return sb.ToString(); - } + if (valid) output.Append(line); + output.Append('\n'); + // add two newlines to the end before return + return output.Append("\n\n").ToString(); } + + #endregion + + /// + /// this is to emulate what's evailable in PHP + /// + private static string RepeatString(string text, int count) + { + var sb = new StringBuilder(text.Length * count); + for (int i = 0; i < count; i++) + sb.Append(text); + return sb.ToString(); + } + } \ No newline at end of file diff --git a/src/Squirrel.Packaging/NugetConsole.cs b/src/Squirrel.Packaging/NugetConsole.cs index be99ae1b..c53fc4fa 100644 --- a/src/Squirrel.Packaging/NugetConsole.cs +++ b/src/Squirrel.Packaging/NugetConsole.cs @@ -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 - : $"{SecurityElement.Escape(File.ReadAllText(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 + : $"{SecurityElement.Escape(File.ReadAllText(releaseNotes))}"; + + string nuspec = $@" @@ -37,119 +49,57 @@ namespace Squirrel.CommandLine ".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(), - Arguments = Enumerable.Empty(), - 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(), + Arguments = Enumerable.Empty(), + 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."); } } \ No newline at end of file diff --git a/src/Squirrel.Packaging/NugetLoggingWrapper.cs b/src/Squirrel.Packaging/NugetLoggingWrapper.cs new file mode 100644 index 00000000..5e734389 --- /dev/null +++ b/src/Squirrel.Packaging/NugetLoggingWrapper.cs @@ -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); + } +} diff --git a/src/Squirrel.Packaging/ReleasePackageBuilder.cs b/src/Squirrel.Packaging/ReleasePackageBuilder.cs index 3b8ff1f6..3c8b8de9 100644 --- a/src/Squirrel.Packaging/ReleasePackageBuilder.cs +++ b/src/Squirrel.Packaging/ReleasePackageBuilder.cs @@ -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 _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(() => 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 releaseNotesProcessor = null, Action contentsPostProcessHook = null) { - private Lazy _package; + return CreateReleasePackage((i, p) => { + contentsPostProcessHook?.Invoke(i, p); + return outputFile; + }, releaseNotesProcessor); + } - public ReleasePackageBuilder(string inputPackageFile, bool isReleasePackage = false) - { - InputPackageFile = inputPackageFile; - _package = new Lazy(() => new ZipPackage(inputPackageFile)); + public string CreateReleasePackage(Func contentsPostProcessHook, Func 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 releaseNotesProcessor = null, Action 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 contentsPostProcessHook, Func 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); + /// + /// Given a list of releases and a specified release package, returns the release package + /// directly previous to the specified version. + /// + public static ReleasePackageBuilder GetPreviousRelease(ILogger logger, IEnumerable 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 releaseNotesProcessor) + { + var doc = new XmlDocument(); + doc.Load(specPath); - var outputFile = contentsPostProcessHook.Invoke(tempPath, package); + var metadata = doc.DocumentElement.ChildNodes + .OfType() + .First(x => x.Name.ToLowerInvariant() == "metadata"); - EasyZip.CreateZipFromDirectory(outputFile, tempPath); + var releaseNotes = metadata.ChildNodes + .OfType() + .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("", + 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().FirstOrDefault(x => x.Name.ToLowerInvariant() == "dependencies"); + if (dependenciesNode != null) { + metadata.RemoveChild(dependenciesNode); } - /// - /// Given a list of releases and a specified release package, returns the release package - /// directly previous to the specified version. - /// - internal static ReleasePackageBuilder GetPreviousRelease(IEnumerable 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 releaseNotesProcessor) - { - var doc = new XmlDocument(); - doc.Load(specPath); - - var metadata = doc.DocumentElement.ChildNodes - .OfType() - .First(x => x.Name.ToLowerInvariant() == "metadata"); - - var releaseNotes = metadata.ChildNodes - .OfType() - .FirstOrDefault(x => x.Name.ToLowerInvariant() == "releasenotes"); - - if (releaseNotes == null || String.IsNullOrWhiteSpace(releaseNotes.InnerText)) { - this.Log().Info("No release notes found in {0}", specPath); - return; - } - - var releaseNotesHtml = doc.CreateElement("releaseNotesHtml"); - releaseNotesHtml.InnerText = String.Format("", - 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().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); } } } diff --git a/src/Squirrel.Packaging/Squirrel.Packaging.csproj b/src/Squirrel.Packaging/Squirrel.Packaging.csproj index ce863c84..cd0a55e4 100644 --- a/src/Squirrel.Packaging/Squirrel.Packaging.csproj +++ b/src/Squirrel.Packaging/Squirrel.Packaging.csproj @@ -2,9 +2,7 @@ net6.0 - Exe - false - true + enable $(NoWarn);CA2007;CS8002 @@ -13,17 +11,8 @@ - - - - - - - - - diff --git a/src/Squirrel/Internal/EnumerableExtensions.cs b/src/Squirrel/Internal/EnumerableExtensions.cs index 2a251437..803b6b9b 100644 --- a/src/Squirrel/Internal/EnumerableExtensions.cs +++ b/src/Squirrel/Internal/EnumerableExtensions.cs @@ -5,8 +5,14 @@ using System.Collections.Generic; namespace Squirrel { + /// + /// Useful enumerable extensions used by Squirrel + /// internal static class EnumerableExtensions { + /// + /// Turn a single value into an IEnumerable of that value. + /// public static IEnumerable Return(T value) { yield return value; diff --git a/src/Squirrel/LoggerExtensions.cs b/src/Squirrel/Internal/LoggerExtensions.cs similarity index 60% rename from src/Squirrel/LoggerExtensions.cs rename to src/Squirrel/Internal/LoggerExtensions.cs index 43e57950..7ab1c53a 100644 --- a/src/Squirrel/LoggerExtensions.cs +++ b/src/Squirrel/Internal/LoggerExtensions.cs @@ -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); - } } } \ No newline at end of file diff --git a/src/Squirrel/Lib/SimpleJson.cs b/src/Squirrel/Internal/SimpleJson.cs similarity index 100% rename from src/Squirrel/Lib/SimpleJson.cs rename to src/Squirrel/Internal/SimpleJson.cs diff --git a/src/Squirrel/NuGet/NuspecManifest.cs b/src/Squirrel/NuGet/NuspecManifest.cs index ff2a0ab1..cd0dc0fa 100644 --- a/src/Squirrel/NuGet/NuspecManifest.cs +++ b/src/Squirrel/NuGet/NuspecManifest.cs @@ -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 RuntimeDependencies { get; } } - internal class NuspecManifest : IPackage + public class NuspecManifest : IPackage { public string ProductName => Title ?? Id; public string ProductDescription => Description ?? Summary ?? Title ?? Id; diff --git a/src/Squirrel/NuGet/PackageDependency.cs b/src/Squirrel/NuGet/PackageDependency.cs index 04fc1b63..bb38f40e 100644 --- a/src/Squirrel/NuGet/PackageDependency.cs +++ b/src/Squirrel/NuGet/PackageDependency.cs @@ -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 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 _dependencies; @@ -85,7 +86,7 @@ namespace Squirrel.NuGet } } - internal class FrameworkAssemblyReference : IFrameworkTargetable + public class FrameworkAssemblyReference : IFrameworkTargetable { public FrameworkAssemblyReference(string assemblyName) : this(assemblyName, Enumerable.Empty()) diff --git a/src/Squirrel/NuGet/ZipPackage.cs b/src/Squirrel/NuGet/ZipPackage.cs index 075c64c5..2774c8bb 100644 --- a/src/Squirrel/NuGet/ZipPackage.cs +++ b/src/Squirrel/NuGet/ZipPackage.cs @@ -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 Frameworks { get; } IEnumerable Files { get; } } - internal class ZipPackage : NuspecManifest, IZipPackage + public class ZipPackage : NuspecManifest, IZipPackage { public IEnumerable Frameworks { get; private set; } = Enumerable.Empty(); public IEnumerable Files { get; private set; } = Enumerable.Empty(); diff --git a/src/Squirrel/NuGet/ZipPackageFile.cs b/src/Squirrel/NuGet/ZipPackageFile.cs index a035bf78..b8a74fbe 100644 --- a/src/Squirrel/NuGet/ZipPackageFile.cs +++ b/src/Squirrel/NuGet/ZipPackageFile.cs @@ -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 + public class ZipPackageFile : IPackageFile, IEquatable { public Uri Key { get; } public string EffectivePath { get; } diff --git a/src/Squirrel/Properties/AssemblyInfo.cs b/src/Squirrel/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..f31d406c --- /dev/null +++ b/src/Squirrel/Properties/AssemblyInfo.cs @@ -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"; +} \ No newline at end of file diff --git a/src/Squirrel/Lib/RID.cs b/src/Squirrel/RID.cs similarity index 100% rename from src/Squirrel/Lib/RID.cs rename to src/Squirrel/RID.cs