Skip to content
This repository has been archived by the owner on Jul 5, 2024. It is now read-only.

Commit

Permalink
WIP add code-signing and notarization for MacOS
Browse files Browse the repository at this point in the history
  • Loading branch information
caesay committed Jun 20, 2022
1 parent c5e5451 commit b6be1b3
Show file tree
Hide file tree
Showing 7 changed files with 230 additions and 83 deletions.
28 changes: 22 additions & 6 deletions src/Squirrel.CommandLine/HelperFile.cs
Original file line number Diff line number Diff line change
Expand Up @@ -100,14 +100,30 @@ public static void CompressLzma7z(string zipFilePath, string inFolder)
InvokeAndThrowIfNonZero(SevenZipPath, args, inFolder);
}

protected static void InvokeAndThrowIfNonZero(string exePath, IEnumerable<string> args, string workingDir)
protected static string InvokeAndThrowIfNonZero(string exePath, IEnumerable<string> args, string workingDir)
{
var result = PlatformUtil.InvokeProcess(exePath, args, workingDir, CancellationToken.None);
if (result.ExitCode != 0) {
throw new Exception(
$"Command failed:\n{result.Command}\n\n" +
$"Output was:\n" + result.StdOutput);
}
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);
}
}
}
94 changes: 51 additions & 43 deletions src/Squirrel.CommandLine/OSX/Commands.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
using System.Runtime.Versioning;
using System.Text;
using System.Text.RegularExpressions;
using Microsoft.NET.HostModel.AppHost;
using NuGet.Versioning;
using Squirrel.NuGet;
using Squirrel.PropertyList;
using Squirrel.SimpleSplat;
Expand All @@ -19,49 +21,11 @@ public static CommandSet GetCommands()
{
return new CommandSet {
"[ Package Authoring ]",
{ "bundle", "Convert a build directory into a OSX '.app' bundle", new BundleOptions(), Bundle },
{ "pack", "Create a Squirrel release from a '.app' bundle", new PackOptions(), Pack },
{ "pack", "Convert a build or '.app' dir into a Squirrel release", new PackOptions(), Pack },
};
}

private static void Pack(PackOptions options)
{
var releasesDir = options.GetReleaseDirectory();
using var _ = Utility.GetTempDirectory(out var tmp);

var manifest = Utility.ReadManifestFromVersionDir(options.package);
if (manifest == null)
throw new Exception("Package directory is not a valid Squirrel bundle. Execute 'bundle' command on this app first.");

var nupkgPath = NugetConsole.CreatePackageFromNuspecPath(tmp, options.package, manifest.FilePath);

var releaseFilePath = Path.Combine(releasesDir.FullName, "RELEASES");
var releases = new List<ReleaseEntry>();
if (File.Exists(releaseFilePath)) {
releases.AddRange(ReleaseEntry.ParseReleaseFile(File.ReadAllText(releaseFilePath, Encoding.UTF8)));
}

Log.Info("Creating Squirrel Release");
var rp = new ReleasePackageBuilder(nupkgPath);
var newPkgPath = rp.CreateReleasePackage(Path.Combine(releasesDir.FullName, rp.SuggestedReleaseFileName));

Log.Info("Creating Delta Packages");
var prev = ReleasePackageBuilder.GetPreviousRelease(releases, rp, releasesDir.FullName);
if (prev != null && !options.noDelta) {
var deltaBuilder = new DeltaPackageBuilder();
var deltaFile = Path.Combine(releasesDir.FullName, rp.SuggestedReleaseFileName.Replace("full", "delta"));
var dp = deltaBuilder.CreateDeltaPackage(prev, rp, deltaFile);
releases.Add(ReleaseEntry.GenerateFromFile(deltaFile));
}

releases.Add(ReleaseEntry.GenerateFromFile(newPkgPath));
ReleaseEntry.WriteReleaseFile(releases, releaseFilePath);
// EasyZip.CreateZipFromDirectory(Path.Combine(releasesDir.FullName, $"{rp.Id}.app.zip"), options.package, nestDirectory: true);

Log.Info("Done");
}

private static void Bundle(BundleOptions options)
{
var releaseDir = options.GetReleaseDirectory();
string appBundlePath;
Expand Down Expand Up @@ -105,13 +69,15 @@ private static void Bundle(BundleOptions options)
var appleId = $"com.{options.packAuthors ?? options.packId}.{options.packId}";
var escapedAppleId = Regex.Replace(appleId, @"[^\w\.]", "_");

var appleSafeVersion = NuGetVersion.Parse(options.packVersion).Version.ToString();

var info = new AppInfo {
CFBundleName = options.packTitle ?? options.packId,
CFBundleDisplayName = options.packTitle ?? options.packId,
CFBundleExecutable = options.mainExe,
CFBundleIdentifier = escapedAppleId,
CFBundlePackageType = "APPL",
CFBundleShortVersionString = options.packVersion,
CFBundleShortVersionString = appleSafeVersion,
CFBundleVersion = options.packVersion,
CFBundleSignature = "????",
NSPrincipalClass = "NSApplication",
Expand Down Expand Up @@ -149,11 +115,53 @@ private static void Bundle(BundleOptions options)
var nuspecText = NugetConsole.CreateNuspec(
options.packId, options.packTitle, options.packAuthors, options.packVersion, options.releaseNotes, options.includePdb, "osx");

File.WriteAllText(Path.Combine(contentsDir, Utility.SpecVersionFileName), nuspecText);
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"));

Log.Info("MacOS '.app' bundle prepared for Squirrel at: " + appBundlePath);
Log.Info("CodeSign and Notarize this app bundle before packing a Squirrel release.");
// code signing
var machoFiles = Directory.EnumerateFiles(appBundlePath, "*", SearchOption.AllDirectories)
.Where(f => PlatformUtil.IsMachOImage(f))
.ToArray();

HelperExe.CodeSign(options.signAppIdentity, options.signEntitlements, machoFiles);

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 List<ReleaseEntry>();
if (File.Exists(releaseFilePath)) {
releases.AddRange(ReleaseEntry.ParseReleaseFile(File.ReadAllText(releaseFilePath, Encoding.UTF8)));
}

var rp = new ReleasePackageBuilder(nupkgPath);
var newPkgPath = rp.CreateReleasePackage(Path.Combine(releaseDir.FullName, rp.SuggestedReleaseFileName));

Log.Info("Creating Delta Packages");
var prev = ReleasePackageBuilder.GetPreviousRelease(releases, rp, releaseDir.FullName);
if (prev != null && !options.noDelta) {
var deltaBuilder = new DeltaPackageBuilder();
var deltaFile = Path.Combine(releaseDir.FullName, rp.SuggestedReleaseFileName.Replace("-full", "-delta"));
var dp = deltaBuilder.CreateDeltaPackage(prev, rp, deltaFile);
releases.Add(ReleaseEntry.GenerateFromFile(deltaFile));
}

releases.Add(ReleaseEntry.GenerateFromFile(newPkgPath));
ReleaseEntry.WriteReleaseFile(releases, releaseFilePath);

// need to pack as zip using ditto for notarization to succeed
// var zipPath = Path.Combine(releaseDir.FullName, options.packId + ".zip");

var pkgPath = Path.Combine(releaseDir.FullName, options.packId + ".pkg");
HelperExe.CreateInstallerPkg(appBundlePath, pkgPath, options.signInstallIdentity);

HelperExe.NotarizePkg(pkgPath, options.notaryProfile);
HelperExe.StapleNotarization(pkgPath);
}

private static void CopyFiles(DirectoryInfo source, DirectoryInfo target)
Expand Down
142 changes: 140 additions & 2 deletions src/Squirrel.CommandLine/OSX/HelperExe.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,149 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.Versioning;
using System.Threading;
using Newtonsoft.Json;
using Squirrel.SimpleSplat;

namespace Squirrel.CommandLine.OSX
{
internal class HelperExe : HelperFile
{
public static string UpdateMacPath
public static string UpdateMacPath
=> FindHelperFile("UpdateMac", p => Microsoft.NET.HostModel.AppHost.HostWriter.IsBundle(p, out var _));

public static string SquirrelEntitlements => FindHelperFile("Squirrel.entitlements");

[SupportedOSPlatform("osx")]
public static void CodeSign(string identity, string entitlements, string[] files)
{
Log.Info($"Preparing to code-sign {files.Length} Mach-O files.");

if (String.IsNullOrEmpty(entitlements)) {
Log.Info("No entitlements provided, using default dotnet entitlements: " +
"https://docs.microsoft.com/en-us/dotnet/core/install/macos-notarization-issues");
entitlements = SquirrelEntitlements;
}

if (!File.Exists(entitlements)) {
throw new Exception("Could not find entitlements file at: " + entitlements);
}

var args = new List<string> {
"-s", identity,
"-f",
"-v",
"--timestamp",
"--options", "runtime",
"--entitlements", entitlements
};

args.AddRange(files);

InvokeAndThrowIfNonZero("codesign", args, null);

Log.Info("Code-sign completed successfully");
}

[SupportedOSPlatform("osx")]
public static void CreateInstallerPkg(string appBundlePath, string pkgOutputPath, string signIdentity)
{
Log.Info($"Creating installer '.pkg' for app at '{appBundlePath}'");

var args = new List<string> {
"--install-location", "~/Applications",
"--component", appBundlePath,
};

if (!String.IsNullOrEmpty(signIdentity)) {
args.Add("--sign");
args.Add(signIdentity);
} else {
Log.Warn("No Installer signing identity provided. The '.pkg' will not be signed.");
}

args.Add(pkgOutputPath);

InvokeAndThrowIfNonZero("pkgbuild", args, null);

Log.Info("Installer created successfully");
}

[SupportedOSPlatform("osx")]
public static void StapleNotarization(string filePath)
{
Log.Info($"Stapling Notarization to '{filePath}'");
var args = new List<string> {
"stapler", "staple", filePath,
};
InvokeAndThrowIfNonZero("xcrun", args, null);
}

[SupportedOSPlatform("osx")]
public static void NotarizePkg(string pkgPath, string profileName)
{
Log.Info($"Preparing to Notarize '{pkgPath}'. This will upload to Apple and usually takes minutes, but could take hours.");

var args = new List<string> {
"notarytool",
"submit",
// "--apple-id", appleId,
// "--password", appPwd,
// "--team-id", teamId,
"--keychain-profile", profileName,
"-f", "json",
"--wait",
pkgPath
};

var ntresultjson = PlatformUtil.InvokeProcess("xcrun", args, null, CancellationToken.None);
Log.Info(ntresultjson);

var ntresult = JsonConvert.DeserializeObject<NotaryToolResult>(ntresultjson.StdOutput);

if (ntresultjson.ExitCode != 0) {
// find and report notarization errors
if (ntresult?.id != null) {
var logargs = new List<string> {
"notarytool",
"log",
ntresult?.id,
"--keychain-profile", profileName,
// "--apple-id", appleId,
// "--password", appPwd,
// "--team-id", teamId,
};

var result = PlatformUtil.InvokeProcess("xcrun", logargs, null, CancellationToken.None);
Log.Warn(result.StdOutput);
}

throw new Exception("Notarization failed.");
}

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 static void CreateDittoZip(string folder, string outputZip)
{
var args = new List<string> {
"-c",
"-k",
"--keepParent",
folder,
outputZip
};

InvokeAndThrowIfNonZero("ditto", args, null);
}
}
}
}
39 changes: 14 additions & 25 deletions src/Squirrel.CommandLine/OSX/Options.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

namespace Squirrel.CommandLine.OSX
{
internal class BundleOptions : BaseOptions
internal class PackOptions : BaseOptions
{
public string packId { get; private set; }
public string packTitle { get; private set; }
Expand All @@ -19,8 +19,13 @@ internal class BundleOptions : BaseOptions
public string releaseNotes { get; private set; }
public string icon { get; private set; }
public string mainExe { get; private set; }
public bool noDelta { get; private set; }
public string signAppIdentity { get; private set; }
public string signInstallIdentity { get; private set; }
public string signEntitlements { get; private set; }
public string notaryProfile { get; private set; }

public BundleOptions()
public PackOptions()
{
Add("u=|packId=", "Unique {ID} for bundle", v => packId = v);
Add("v=|packVersion=", "Current {VERSION} for bundle", v => packVersion = v);
Expand All @@ -32,36 +37,20 @@ public BundleOptions()
Add("releaseNotes=", "{PATH} to file with markdown notes for version", v => releaseNotes = v);
Add("e=|mainExe=", "The file {NAME} of the main executable", v => mainExe = v);
Add("i=|icon=", "{PATH} to the .icns file for this bundle", v => icon = v);
Add("noDelta", "Skip the generation of delta packages", v => noDelta = true);
Add("signAppIdentity=", "The {SUBJECT} name of the cert to use for app code signing", v => signAppIdentity = v);
Add("signInstallIdentity=", "The {SUBJECT} name of the cert to use for installation packages", v => signInstallIdentity = v);
Add("signEntitlements=", "{PATH} to entitlements file for hardened runtime", v => signEntitlements = v);
Add("notaryProfile=", "{NAME} of profile containing Apple credentials stored with notarytool", v => notaryProfile = v);
}

public override void Validate()
{
IsRequired(nameof(packId), nameof(packVersion), nameof(packDirectory));
IsValidFile(nameof(signEntitlements), "entitlements");
NuGet.NugetUtil.ThrowIfInvalidNugetId(packId);
NuGet.NugetUtil.ThrowIfVersionNotSemverCompliant(packVersion, false);
NuGet.NugetUtil.ThrowIfVersionNotSemverCompliant(packVersion);
IsValidDirectory(nameof(packDirectory), true);
}
}

internal class PackOptions : BaseOptions
{
public string package { get; set; }
public bool noDelta { get; private set; }
// public string baseUrl { get; private set; }

public PackOptions()
{
// Add("b=|baseUrl=", "Provides a base URL to prefix the RELEASES file packages with", v => baseUrl = v, true);
Add("p=|package=", "{PATH} to a '.app' directory to releasify", v => package = v);
Add("noDelta", "Skip the generation of delta packages", v => noDelta = true);
}

public override void Validate()
{
IsRequired(nameof(package));
IsValidDirectory(nameof(package), true);
if (!Utility.PathPartEndsWith(package, ".app"))
throw new OptionValidationException("-p argument must point to a macos bundle directory ending in '.app'.");
}
}
}
Loading

0 comments on commit b6be1b3

Please sign in to comment.