Refactor the rest of the command classes

This commit is contained in:
Caelan Sayler
2023-12-15 14:54:15 +00:00
parent c169323bd5
commit 8bfaccdbe8
13 changed files with 469 additions and 442 deletions

View File

@@ -4,10 +4,6 @@ public class WindowsReleasifyCommand : WindowsSigningCommand
{
public string Package { get; set; }
public string BaseUrl { get; private set; }
public string DebugSetupExe { get; private set; }
public bool NoDelta { get; private set; }
public string Runtimes { get; private set; }
@@ -16,9 +12,7 @@ public class WindowsReleasifyCommand : WindowsSigningCommand
public string Icon { get; private set; }
public string[] SquirrelAwareExecutableNames { get; private set; }
public string AppIcon { get; private set; }
public string EntryExecutableName { get; private set; }
public WindowsReleasifyCommand()
: this("releasify", "Take an existing nuget package and convert it into a Squirrel release.")
@@ -38,19 +32,6 @@ public class WindowsReleasifyCommand : WindowsSigningCommand
protected WindowsReleasifyCommand(string name, string description)
: base(name, description)
{
AddOption<Uri>((v) => BaseUrl = v.ToAbsoluteOrNull(), "-b", "--baseUrl")
.SetDescription("Provides a base URL to prefix the RELEASES file packages with.")
.SetHidden()
.MustBeValidHttpUri();
AddOption<FileInfo>((v) => DebugSetupExe = v.ToFullNameOrNull(), "--debugSetupExe")
.SetDescription("Uses the Setup.exe at this {PATH} to create the bundle, and then replaces it with the bundle. " +
"Used for locally debugging Setup.exe with a real bundle attached.")
.SetArgumentHelpName("PATH")
.SetHidden()
.AcceptExistingOnly()
.RequiresExtension(".exe");
AddOption<bool>((v) => NoDelta = v, "--noDelta")
.SetDescription("Skip the generation of delta packages.");
@@ -70,14 +51,9 @@ public class WindowsReleasifyCommand : WindowsSigningCommand
.AcceptExistingOnly()
.RequiresExtension(".ico");
AddOption<string[]>((v) => SquirrelAwareExecutableNames = v ?? new string[0], "-e", "--mainExe")
.SetDescription("Name of one or more SquirrelAware executables.")
.SetArgumentHelpName("NAME");
AddOption<FileInfo>((v) => AppIcon = v.ToFullNameOrNull(), "--appIcon")
.SetDescription("Path to .ico for 'Apps and Features' list.")
.SetArgumentHelpName("PATH")
.AcceptExistingOnly()
.RequiresExtension(".ico");
AddOption<string>((v) => EntryExecutableName = v, "-e", "--mainExe")
.SetDescription("The file name of the main/entry executable.")
.SetArgumentHelpName("NAME")
.SetRequired();
}
}

View File

@@ -1,7 +1,8 @@
using System.Runtime.Versioning;
using Squirrel.Csq.Commands;
using Squirrel.Deployment;
using Squirrel.Packaging.OSX;
using Squirrel.Packaging.OSX.Commands;
using Squirrel.Packaging.Windows.Commands;
namespace Squirrel.Csq.Compat;
@@ -17,7 +18,7 @@ public class EmbeddedRunner : ICommandRunner
[SupportedOSPlatform("osx")]
public Task ExecuteBundleOsx(OsxBundleCommand command)
{
var options = new BundleOsxOptions {
var options = new OsxBundleOptions {
BundleId = command.BundleId,
PackAuthors = command.PackAuthors,
EntryExecutableName = command.EntryExecutableName,
@@ -28,7 +29,78 @@ public class EmbeddedRunner : ICommandRunner
PackVersion = command.PackVersion,
ReleaseDir = command.GetReleaseDirectory(),
};
new OsxCommands(_logger).Bundle(options);
new OsxBundleCommandRunner(_logger).Bundle(options);
return Task.CompletedTask;
}
[SupportedOSPlatform("osx")]
public Task ExecuteReleasifyOsx(OsxReleasifyCommand command)
{
var options = new OsxReleasifyOptions {
ReleaseDir = command.GetReleaseDirectory(),
BundleDirectory = command.BundleDirectory,
IncludePdb = command.IncludePdb,
NoDelta = command.NoDelta,
NoPackage = command.NoPackage,
NotaryProfile = command.NotaryProfile,
PackageConclusion = command.PackageConclusion,
PackageLicense = command.PackageLicense,
PackageReadme = command.PackageReadme,
PackageWelcome = command.PackageWelcome,
ReleaseNotes = command.ReleaseNotes,
SigningAppIdentity = command.SigningAppIdentity,
SigningEntitlements = command.SigningEntitlements,
SigningInstallIdentity = command.SigningInstallIdentity,
TargetRuntime = command.TargetRuntime,
};
new OsxReleasifyCommandRunner(_logger).Releasify(options);
return Task.CompletedTask;
}
public Task ExecutePackWindows(WindowsPackCommand command)
{
var options = new WindowsPackOptions {
TargetRuntime = command.TargetRuntime,
ReleaseDir = command.GetReleaseDirectory(),
Package = command.Package,
Icon = command.Icon,
NoDelta = command.NoDelta,
IncludePdb = command.IncludePdb,
SignParameters = command.SignParameters,
EntryExecutableName = command.EntryExecutableName,
PackAuthors = command.PackAuthors,
PackDirectory = command.PackDirectory,
PackId = command.PackId,
PackTitle = command.PackTitle,
PackVersion = command.PackVersion,
ReleaseNotes = command.ReleaseNotes,
Runtimes = command.Runtimes,
SignParallel = command.SignParallel,
SignSkipDll = command.SignSkipDll,
SignTemplate = command.SignTemplate,
SplashImage = command.SplashImage,
};
new WindowsPackCommandRunner(_logger).Pack(options);
return Task.CompletedTask;
}
public Task ExecuteReleasifyWindows(WindowsReleasifyCommand command)
{
var options = new WindowsReleasifyOptions {
TargetRuntime = command.TargetRuntime,
ReleaseDir = command.GetReleaseDirectory(),
Package = command.Package,
Icon = command.Icon,
NoDelta = command.NoDelta,
SignParameters = command.SignParameters,
EntryExecutableName = command.EntryExecutableName,
Runtimes = command.Runtimes,
SignParallel = command.SignParallel,
SignSkipDll = command.SignSkipDll,
SignTemplate = command.SignTemplate,
SplashImage = command.SplashImage,
};
new WindowsReleasifyCommandRunner(_logger).Releasify(options);
return Task.CompletedTask;
}
@@ -64,22 +136,6 @@ public class EmbeddedRunner : ICommandRunner
return new SimpleWebRepository(_logger).DownloadRecentPackages(options);
}
public Task ExecutePackWindows(WindowsPackCommand command)
{
throw new NotImplementedException();
}
[SupportedOSPlatform("osx")]
public Task ExecuteReleasifyOsx(OsxReleasifyCommand command)
{
throw new NotImplementedException();
}
public Task ExecuteReleasifyWindows(WindowsReleasifyCommand command)
{
throw new NotImplementedException();
}
public Task ExecuteS3Download(S3DownloadCommand command)
{
var options = new S3Options {

View File

@@ -25,11 +25,9 @@ public class V2CompatRunner : ICommandRunner
var options = new PackOptions {
releaseDir = command.GetReleaseDirectory().FullName,
package = command.Package,
baseUrl = command.BaseUrl,
framework = command.Runtimes,
splashImage = command.SplashImage,
icon = command.Icon,
appIcon = command.AppIcon,
noDelta = command.NoDelta,
allowUnaware = false,
signParams = command.SignParameters,
@@ -58,11 +56,9 @@ public class V2CompatRunner : ICommandRunner
var options = new ReleasifyOptions {
releaseDir = command.GetReleaseDirectory().FullName,
package = command.Package,
baseUrl = command.BaseUrl,
framework = command.Runtimes,
splashImage = command.SplashImage,
icon = command.Icon,
appIcon = command.AppIcon,
noDelta = command.NoDelta,
allowUnaware = false,
signParams = command.SignParameters,

View File

@@ -0,0 +1,75 @@
using System.Text;
using System.Text.RegularExpressions;
using Microsoft.Extensions.Logging;
using NuGet.Versioning;
namespace Squirrel.Packaging.OSX.Commands;
public class OsxBundleCommandRunner
{
private readonly ILogger _logger;
public OsxBundleCommandRunner(ILogger logger)
{
_logger = logger;
}
public void Bundle(OsxBundleOptions 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;
_logger.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),
};
_logger.Info("Creating '.app' directory structure");
var builder = new StructureBuilder(packId, releaseDir.FullName);
if (Directory.Exists(builder.AppDirectory)) {
_logger.Warn(builder.AppDirectory + " already exists, deleting...");
Utility.DeleteFileOrDirectoryHard(builder.AppDirectory);
}
builder.Build();
_logger.Info("Writing Info.plist");
var plist = new PlistWriter(_logger, info, builder.ContentsDirectory);
plist.Write();
_logger.Info("Copying resources into new '.app' bundle");
File.Copy(icon, Path.Combine(builder.ResourcesDirectory, Path.GetFileName(icon)));
_logger.Info("Copying application files into new '.app' bundle");
Utility.CopyFiles(new DirectoryInfo(packDirectory), new DirectoryInfo(builder.MacosDirectory));
_logger.Info("Bundle created successfully: " + builder.AppDirectory);
}
}

View File

@@ -0,0 +1,22 @@
namespace Squirrel.Packaging.OSX.Commands;
public class OsxBundleOptions
{
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; }
}

View File

@@ -0,0 +1,148 @@
using System.Text;
using System.Text.RegularExpressions;
using Microsoft.Extensions.Logging;
using NuGet.Versioning;
namespace Squirrel.Packaging.OSX.Commands;
public class OsxReleasifyCommandRunner
{
private readonly ILogger _logger;
public OsxReleasifyCommandRunner(ILogger logger)
{
_logger = logger;
}
public void Releasify(OsxReleasifyOptions options)
{
var releaseDir = options.ReleaseDir;
var appBundlePath = options.BundleDirectory;
_logger.Info("Creating Squirrel application from app bundle at: " + appBundlePath);
_logger.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)");
var 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}'");
_logger.Info($"Package valid: '{packId}', Name: '{packTitle}', Version: {packVersion}");
_logger.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(_logger);
// 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 {
_logger.Warn("Package will not be signed or notarized. Requires the --signAppIdentity and --notaryProfile options.");
}
// create a portable zip package from signed/notarized bundle
_logger.Info("Creating final application artifact (zip)");
helper.CreateDittoZip(appBundlePath, zipPath);
// create release / delta from notarized .app
_logger.Info("Creating Squirrel Release");
using var _ = Utility.GetTempDirectory(out var tmp);
var nuget = new NugetConsole(_logger);
var nupkgPath = nuget.CreatePackageFromNuspecPath(tmp, appBundlePath, nuspecPath);
var releaseFilePath = Path.Combine(releaseDir.FullName, "RELEASES");
var releases = new Dictionary<string, ReleaseEntry>();
ReleaseEntry.BuildReleasesFile(releaseDir.FullName);
foreach (var rel in ReleaseEntry.ParseReleaseFile(File.ReadAllText(releaseFilePath, Encoding.UTF8))) {
releases[rel.Filename] = rel;
}
var rp = new ReleasePackageBuilder(_logger, nupkgPath);
var suggestedName = ReleasePackageBuilder.GetSuggestedFileName(packId, packVersion, options.TargetRuntime.StringWithNoVersion);
var newPkgPath = rp.CreateReleasePackage((i, pkg) => Path.Combine(releaseDir.FullName, suggestedName));
_logger.Info("Creating Delta Packages");
var prev = ReleasePackageBuilder.GetPreviousRelease(_logger, releases.Values, rp, releaseDir.FullName, options.TargetRuntime);
if (prev != null && !options.NoDelta) {
var deltaBuilder = new DeltaPackageBuilder(_logger);
var deltaFile = rp.ReleasePackageFile.Replace("-full", "-delta");
var dp = deltaBuilder.CreateDeltaPackage(prev, rp, deltaFile);
var deltaEntry = ReleaseEntry.GenerateFromFile(deltaFile);
releases[deltaEntry.Filename] = deltaEntry;
}
var fullEntry = ReleaseEntry.GenerateFromFile(newPkgPath);
releases[fullEntry.Filename] = fullEntry;
ReleaseEntry.WriteReleaseFile(releases.Values, releaseFilePath);
// create installer package, sign and notarize
if (!options.NoPackage) {
if (SquirrelRuntimeInfo.IsOSX) {
var pkgPath = Path.Combine(releaseDir.FullName, $"{packId}-{options.TargetRuntime.StringWithNoVersion}.pkg");
Dictionary<string, string> pkgContent = new() {
{"welcome", options.PackageWelcome },
{"license", options.PackageLicense },
{"readme", options.PackageReadme },
{"conclusion", options.PackageConclusion },
};
helper.CreateInstallerPkg(appBundlePath, packTitle, pkgContent, pkgPath, options.SigningInstallIdentity);
if (!string.IsNullOrEmpty(options.SigningInstallIdentity) && !string.IsNullOrEmpty(options.NotaryProfile)) {
helper.Notarize(pkgPath, options.NotaryProfile);
helper.Staple(pkgPath);
helper.SpctlAssessInstaller(pkgPath);
} else {
_logger.Warn("Package installer (.pkg) will not be Notarized. " +
"This is supported with the --signInstallIdentity and --notaryProfile arguments.");
}
} else {
_logger.Warn("Package installer (.pkg) will not be created - this is only supported on OSX.");
}
}
_logger.Info("Done.");
}
}

View File

@@ -0,0 +1,34 @@
namespace Squirrel.Packaging.OSX.Commands;
public class OsxReleasifyOptions
{
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; }
}

View File

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

View File

@@ -0,0 +1,28 @@
using System.Drawing;
using System.Text;
using Microsoft.Extensions.Logging;
using Squirrel.NuGet;
using FileMode = System.IO.FileMode;
namespace Squirrel.Packaging.Windows.Commands;
public class WindowsPackCommandRunner
{
private readonly ILogger _logger;
public WindowsPackCommandRunner(ILogger logger)
{
_logger = logger;
}
public void Pack(WindowsPackOptions options)
{
using (Utility.GetTempDirectory(out var tmp)) {
var nupkgPath = new NugetConsole(_logger).CreatePackageFromOptions(tmp, options);
options.Package = nupkgPath;
var runner = new WindowsReleasifyCommandRunner(_logger);
runner.Releasify(options);
}
}
}

View File

@@ -0,0 +1,18 @@
namespace Squirrel.Packaging.Windows.Commands;
public class WindowsPackOptions : WindowsReleasifyOptions, 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; }
}

View File

@@ -4,87 +4,24 @@ using Microsoft.Extensions.Logging;
using Squirrel.NuGet;
using FileMode = System.IO.FileMode;
namespace Squirrel.Packaging.Windows;
namespace Squirrel.Packaging.Windows.Commands;
public class SigningOptions
public class WindowsReleasifyCommandRunner
{
public string SignParameters { get; set; }
private readonly ILogger _logger;
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)
public WindowsReleasifyCommandRunner(ILogger logger)
{
Log = logger;
_logger = 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)
public void Releasify(WindowsReleasifyOptions 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;
var setupIcon = options.Icon;
// normalize and validate that the provided frameworks are supported
var requiredFrameworks = options.Runtimes
@@ -97,14 +34,14 @@ public class WindowsCommands
using var ud = Utility.GetTempDirectory(out var tempDir);
// update icon for Update.exe if requested
var helper = new HelperExe(Log);
var helper = new HelperExe(_logger);
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).");
_logger.Warn("Unable to set icon for Update.exe (only supported on windows).");
}
// copy input package to target output directory
@@ -123,9 +60,9 @@ public class WindowsCommands
}
foreach (var file in toProcess) {
Log.Info("Creating release for package: " + file.FullName);
_logger.Info("Creating release for package: " + file.FullName);
var rp = new ReleasePackageBuilder(Log, file.FullName);
var rp = new ReleasePackageBuilder(_logger, file.FullName);
rp.CreateReleasePackage(contentsPostProcessHook: (pkgPath, zpkg) => {
var nuspecPath = Directory.GetFiles(pkgPath, "*.nuspec", SearchOption.TopDirectoryOnly)
.ContextualSingle("package", "*.nuspec", "top level directory");
@@ -140,7 +77,7 @@ public class WindowsCommands
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}'."));
.ForEach(f => _logger.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"))) {
@@ -149,7 +86,7 @@ public class WindowsCommands
"Please publish your application to a folder without ClickOnce.");
}
ZipPackage.SetMetadata(nuspecPath, requiredFrameworks.Select(r => r.Id), options.TargetRuntime);
NuspecManifest.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
@@ -164,33 +101,6 @@ public class WindowsCommands
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)));
@@ -200,9 +110,9 @@ public class WindowsCommands
processed.Add(rp.ReleasePackageFile);
var prev = ReleasePackageBuilder.GetPreviousRelease(Log, previousReleases, rp, targetDir, options.TargetRuntime);
var prev = ReleasePackageBuilder.GetPreviousRelease(_logger, previousReleases, rp, targetDir, options.TargetRuntime);
if (prev != null && generateDeltas) {
var deltaBuilder = new DeltaPackageBuilder(Log);
var deltaBuilder = new DeltaPackageBuilder(_logger);
var deltaOutputPath = rp.ReleasePackageFile.Replace("-full", "-delta");
var dp = deltaBuilder.CreateDeltaPackage(prev, rp, deltaOutputPath);
processed.Insert(0, dp.InputPackageFile);
@@ -214,7 +124,7 @@ public class WindowsCommands
}
var newReleaseEntries = processed
.Select(packageFilename => ReleaseEntry.GenerateFromFile(packageFilename, baseUrl))
.Select(packageFilename => ReleaseEntry.GenerateFromFile(packageFilename))
.ToList();
var distinctPreviousReleases = previousReleases
.Where(x => !newReleaseEntries.Select(e => e.Version).Contains(x.Version));
@@ -224,48 +134,42 @@ public class WindowsCommands
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);
File.Copy(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)");
_logger.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 newestFullRelease = releaseEntries.MaxBy(x => x.Version).Where(x => !x.IsDelta).First();
var newestReleasePath = Path.Combine(targetDir, newestFullRelease.Filename);
Log.Info($"Creating Setup bundle");
_logger.Info($"Creating Setup bundle");
var bundleOffset = SetupBundle.CreatePackageBundle(targetSetupExe, newestReleasePath);
Log.Info("Signing Setup bundle");
_logger.Info("Signing Setup bundle");
signFiles(options, targetDir, targetSetupExe);
Log.Info("Bundle package offset is " + bundleOffset);
_logger.Info("Bundle package offset is " + bundleOffset);
Log.Info($"Setup bundle created at '{targetSetupExe}'.");
_logger.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");
_logger.Info("Done");
}
private void signFiles(SigningOptions options, string rootDir, params string[] filePaths)
private void signFiles(WindowsSigningOptions options, string rootDir, params string[] filePaths)
{
var signParams = options.SignParameters;
var signTemplate = options.SignTemplate;
var signParallel = options.SignParallel;
var helper = new HelperExe(Log);
var helper = new HelperExe(_logger);
if (String.IsNullOrEmpty(signParams) && String.IsNullOrEmpty(signTemplate)) {
Log.Debug($"No signing paramaters provided, {filePaths.Length} file(s) will not be signed.");
if (string.IsNullOrEmpty(signParams) && string.IsNullOrEmpty(signTemplate)) {
_logger.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");
if (!string.IsNullOrEmpty(signTemplate)) {
_logger.Info($"Preparing to sign {filePaths.Length} files with custom signing template");
foreach (var f in filePaths) {
helper.SignPEFileWithTemplate(f, signTemplate);
}
@@ -275,8 +179,8 @@ public class WindowsCommands
// 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}");
if (!string.IsNullOrEmpty(signParams)) {
_logger.Info($"Preparing to sign {filePaths.Length} files with embedded signtool.exe with parallelism of {signParallel}");
helper.SignPEFilesWithSignTool(rootDir, filePaths, signParams, signParallel);
}
}

View File

@@ -0,0 +1,20 @@
namespace Squirrel.Packaging.Windows.Commands;
public class WindowsReleasifyOptions : WindowsSigningOptions
{
public DirectoryInfo ReleaseDir { get; set; }
public RID TargetRuntime { get; set; }
public string Package { get; set; }
public bool NoDelta { get; set; }
public string Runtimes { get; set; }
public string SplashImage { get; set; }
public string Icon { get; set; }
public string EntryExecutableName { get; set; }
}

View File

@@ -0,0 +1,12 @@
namespace Squirrel.Packaging.Windows.Commands;
public class WindowsSigningOptions
{
public string SignParameters { get; set; }
public bool SignSkipDll { get; set; }
public int SignParallel { get; set; }
public string SignTemplate { get; set; }
}