diff --git a/src/Squirrel.CommandLine/OSX/Commands.cs b/src/Squirrel.CommandLine/OSX/Commands.cs index 946a3daf8..c8214ea96 100644 --- a/src/Squirrel.CommandLine/OSX/Commands.cs +++ b/src/Squirrel.CommandLine/OSX/Commands.cs @@ -114,9 +114,9 @@ private static void Pack(PackOptions options) if (!File.Exists(Path.Combine(contentsDir, "Info.plist"))) throw new Exception("Invalid bundle structure (missing Info.plist)"); + var pkgTitle = options.packTitle ?? options.packId; var nuspecText = NugetConsole.CreateNuspec( - options.packId, options.packTitle, options.packAuthors, options.packVersion, options.releaseNotes, options.includePdb, "osx"); - + options.packId, pkgTitle, options.packAuthors, options.packVersion, options.releaseNotes, options.includePdb, "osx"); var nuspecPath = Path.Combine(contentsDir, Utility.SpecVersionFileName); // nuspec and UpdateMac need to be in contents dir or this package can't update @@ -179,7 +179,7 @@ private static void Pack(PackOptions options) // create installer package, sign and notarize if (SquirrelRuntimeInfo.IsOSX) { var pkgPath = Path.Combine(releaseDir.FullName, options.packId + ".pkg"); - HelperExe.CreateInstallerPkg(appBundlePath, pkgPath, options.signInstallIdentity); + HelperExe.CreateInstallerPkg(appBundlePath, pkgTitle, options.pkgContent, pkgPath, options.signInstallIdentity); if (!String.IsNullOrEmpty(options.signInstallIdentity) && !String.IsNullOrEmpty(options.notaryProfile)) { HelperExe.Notarize(pkgPath, options.notaryProfile); HelperExe.Staple(pkgPath); diff --git a/src/Squirrel.CommandLine/OSX/HelperExe.cs b/src/Squirrel.CommandLine/OSX/HelperExe.cs index ab6ab0642..e07a8e206 100644 --- a/src/Squirrel.CommandLine/OSX/HelperExe.cs +++ b/src/Squirrel.CommandLine/OSX/HelperExe.cs @@ -3,6 +3,7 @@ using System.IO; using System.Linq; using System.Runtime.Versioning; +using System.Security; using System.Threading; using Newtonsoft.Json; @@ -58,10 +59,11 @@ public static void SpctlAssess(string filePath) } [SupportedOSPlatform("osx")] - public static void CreateInstallerPkg(string appBundlePath, string pkgOutputPath, string signIdentity) + public static void CreateInstallerPkg(string appBundlePath, string appTitle, KeyValuePair[] 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); @@ -70,12 +72,13 @@ public static void CreateInstallerPkg(string appBundlePath, string pkgOutputPath 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"); @@ -97,19 +100,31 @@ public static void CreateInstallerPkg(string appBundlePath, string pkgOutputPath }; InvokeAndThrowIfNonZero("pkgbuild", args1, null); - - // create product package that installs to home dir + + // create final product package that contains app component var distributionPath = Path.Combine(tmp, "distribution.xml"); InvokeAndThrowIfNonZero("productbuild", new[] { "--synthesize", "--package", pkg1Path, distributionPath }, null); - // disable local system installation and build final package + // 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, ""); File.WriteAllLines(distributionPath, distXml); + + // add extra landing content (eg. license, readme) + foreach (var kvp in extraContent) { + var fileName = Path.GetFileName(kvp.Value); + File.Copy(kvp.Value, Path.Combine(tmpResources, fileName)); + distXml.Insert(2, $"<{kvp.Key} file=\"{fileName}\" />"); + } List args2 = new() { "--distribution", distributionPath, "--package-path", tmpPayload2, + "--resources", tmpResources, pkgOutputPath }; diff --git a/src/Squirrel.CommandLine/OSX/Options.cs b/src/Squirrel.CommandLine/OSX/Options.cs index f4560a726..bc4281d77 100644 --- a/src/Squirrel.CommandLine/OSX/Options.cs +++ b/src/Squirrel.CommandLine/OSX/Options.cs @@ -25,6 +25,9 @@ internal class PackOptions : BaseOptions public string signEntitlements { get; private set; } public string notaryProfile { get; private set; } public string appleId { get; private set; } + public KeyValuePair[] pkgContent => _pkgContent.ToArray(); + + private Dictionary _pkgContent = new Dictionary(); public PackOptions() { @@ -40,6 +43,8 @@ public PackOptions() 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("appleId", "Override the apple bundle ID for generated bundles", v => appleId = v); + Add("noPkg", "Skip generating a .pkg installer", v => appleId = v); + Add("pkgContent=", "Add content files (eg. readme, license) to pkg installer.", (v1, v2) => _pkgContent.Add(v1, v2)); if (SquirrelRuntimeInfo.IsOSX) { Add("signAppIdentity=", "The {SUBJECT} name of the cert to use for app code signing", v => signAppIdentity = v); @@ -56,6 +61,23 @@ public override void Validate() NuGet.NugetUtil.ThrowIfInvalidNugetId(packId); NuGet.NugetUtil.ThrowIfVersionNotSemverCompliant(packVersion); IsValidDirectory(nameof(packDirectory), true); + + var validContentKeys = new string[] { + "welcome", + "readme", + "license", + "conclusion", + }; + + foreach (var kvp in _pkgContent) { + if (!validContentKeys.Contains(kvp.Key)) { + throw new OptionValidationException($"Invalid pkgContent key: {kvp.Key}. Must be one of: " + string.Join(", ", validContentKeys)); + } + + if (!File.Exists(kvp.Value)) { + throw new OptionValidationException("pkgContent file not found: " + kvp.Value); + } + } } } } \ No newline at end of file