"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.createNotarizeOpts = exports.createSignOpts = exports.filterCFBundleIdentifier = exports.App = exports.MacApp = void 0; const platform_1 = require("./platform"); const common_1 = require("./common"); const fs_extra_1 = __importDefault(require("fs-extra")); const path_1 = __importDefault(require("path")); const plist_1 = __importDefault(require("plist")); const notarize_1 = require("@electron/notarize"); const osx_sign_1 = require("@electron/osx-sign"); class MacApp extends platform_1.App { constructor(opts, templatePath) { super(opts, templatePath); this.appName = opts.name; } get appCategoryType() { return this.opts.appCategoryType; } get appCopyright() { return this.opts.appCopyright; } get appVersion() { return this.opts.appVersion; } get buildVersion() { return this.opts.buildVersion; } get enableDarkMode() { return this.opts.darwinDarkModeSupport; } get usageDescription() { return this.opts.usageDescription; } get protocols() { return (this.opts.protocols || []).map((protocol) => { return { CFBundleURLName: protocol.name, CFBundleURLSchemes: [...protocol.schemes], }; }); } get dotAppName() { return `${(0, common_1.sanitizeAppName)(this.appName)}.app`; } get defaultBundleName() { return `com.electron.${(0, common_1.sanitizeAppName)(this.appName).toLowerCase()}`; } get bundleName() { return filterCFBundleIdentifier(this.opts.appBundleId || this.defaultBundleName); } get originalResourcesDir() { return path_1.default.join(this.contentsPath, 'Resources'); } get resourcesDir() { return path_1.default.join(this.dotAppName, 'Contents', 'Resources'); } get electronBinaryDir() { return path_1.default.join(this.contentsPath, 'MacOS'); } get originalElectronName() { return 'Electron'; } get newElectronName() { return this.appPlist.CFBundleExecutable; } get renamedAppPath() { return path_1.default.join(this.stagingPath, this.dotAppName); } get electronAppPath() { return path_1.default.join(this.stagingPath, `${this.originalElectronName}.app`); } get contentsPath() { return path_1.default.join(this.electronAppPath, 'Contents'); } get frameworksPath() { return path_1.default.join(this.contentsPath, 'Frameworks'); } get loginItemsPath() { return path_1.default.join(this.contentsPath, 'Library', 'LoginItems'); } get loginHelperPath() { return path_1.default.join(this.loginItemsPath, 'Electron Login Helper.app'); } updatePlist(basePlist, displayName, identifier, name) { return Object.assign(basePlist, { CFBundleDisplayName: displayName, CFBundleExecutable: (0, common_1.sanitizeAppName)(displayName), CFBundleIdentifier: identifier, CFBundleName: (0, common_1.sanitizeAppName)(name), }); } updateHelperPlist(helperPlist, suffix, identifierIgnoresSuffix) { let helperSuffix, identifier, name; if (suffix) { helperSuffix = `Helper ${suffix}`; if (identifierIgnoresSuffix) { identifier = this.helperBundleIdentifier; } else { identifier = `${this.helperBundleIdentifier}.${suffix}`; } name = `${this.appName} ${helperSuffix}`; } else { helperSuffix = 'Helper'; identifier = this.helperBundleIdentifier; name = this.appName; } return this.updatePlist(helperPlist, `${this.appName} ${helperSuffix}`, identifier, name); } async extendPlist(basePlist, propsOrFilename) { if (!propsOrFilename) { return Promise.resolve(); } if (typeof propsOrFilename === 'string') { const plist = await this.loadPlist(propsOrFilename); return Object.assign(basePlist, plist); } else { return Object.assign(basePlist, propsOrFilename); } } async loadPlist(filename, propName) { const loadedPlist = plist_1.default.parse((await fs_extra_1.default.readFile(filename)).toString()); if (propName) { this[propName] = loadedPlist; } return loadedPlist; } ehPlistFilename(helper) { return this.helperPlistFilename(path_1.default.join(this.frameworksPath, helper)); } helperPlistFilename(helperApp) { return path_1.default.join(helperApp, 'Contents', 'Info.plist'); } async determinePlistFilesToUpdate() { const appPlistFilename = path_1.default.join(this.contentsPath, 'Info.plist'); const plists = [ [appPlistFilename, 'appPlist'], [this.ehPlistFilename('Electron Helper.app'), 'helperPlist'], ]; const possiblePlists = [ [this.ehPlistFilename('Electron Helper (Renderer).app'), 'helperRendererPlist'], [this.ehPlistFilename('Electron Helper (Plugin).app'), 'helperPluginPlist'], [this.ehPlistFilename('Electron Helper (GPU).app'), 'helperGPUPlist'], [this.ehPlistFilename('Electron Helper EH.app'), 'helperEHPlist'], [this.ehPlistFilename('Electron Helper NP.app'), 'helperNPPlist'], [this.helperPlistFilename(this.loginHelperPath), 'loginHelperPlist'], ]; const optional = await Promise.all(possiblePlists.map(async (item) => (await fs_extra_1.default.pathExists(item[0])) ? item : null)); return [...plists, ...optional.filter(item => item)]; } appRelativePath(p) { return path_1.default.relative(this.contentsPath, p); } async updatePlistFiles() { const appBundleIdentifier = this.bundleName; this.helperBundleIdentifier = filterCFBundleIdentifier(this.opts.helperBundleId || `${appBundleIdentifier}.helper`); const plists = await this.determinePlistFilesToUpdate(); await Promise.all(plists.map(plistArgs => this.loadPlist(...plistArgs))); await this.extendPlist(this.appPlist, this.opts.extendInfo); if (this.asarIntegrity) { await this.extendPlist(this.appPlist, { ElectronAsarIntegrity: this.asarIntegrity, }); } else { delete this.appPlist?.ElectronAsarIntegrity; } this.appPlist = this.updatePlist(this.appPlist, this.executableName, appBundleIdentifier, this.appName); const updateIfExists = [ ['helperRendererPlist', '(Renderer)', true], ['helperPluginPlist', '(Plugin)', true], ['helperGPUPlist', '(GPU)', true], ['helperEHPlist', 'EH'], ['helperNPPlist', 'NP'], ]; for (const [plistKey] of [...updateIfExists, ['helperPlist']]) { if (!this[plistKey]) { continue; } await this.extendPlist(this[plistKey], this.opts.extendHelperInfo); } this.helperPlist = this.updateHelperPlist(this.helperPlist); for (const [plistKey, ...suffixArgs] of updateIfExists) { if (!this[plistKey]) { continue; } this[plistKey] = this.updateHelperPlist(this[plistKey], ...suffixArgs); } // Some properties need to go on all helpers as well, version, usage info, etc. const plistsToUpdate = updateIfExists .filter(([key]) => !!this[key]) .map(([key]) => key) .concat(['appPlist', 'helperPlist']); if (this.loginHelperPlist) { const loginHelperName = (0, common_1.sanitizeAppName)(`${this.appName} Login Helper`); this.loginHelperPlist.CFBundleExecutable = loginHelperName; this.loginHelperPlist.CFBundleIdentifier = `${appBundleIdentifier}.loginhelper`; this.loginHelperPlist.CFBundleName = loginHelperName; } if (this.appVersion) { const appVersionString = '' + this.appVersion; for (const plistKey of plistsToUpdate) { this[plistKey].CFBundleShortVersionString = this[plistKey].CFBundleVersion = appVersionString; } } if (this.buildVersion) { const buildVersionString = '' + this.buildVersion; for (const plistKey of plistsToUpdate) { this[plistKey].CFBundleVersion = buildVersionString; } } if (this.opts.protocols && this.opts.protocols.length) { this.appPlist.CFBundleURLTypes = this.protocols; } if (this.appCategoryType) { this.appPlist.LSApplicationCategoryType = this.appCategoryType; } if (this.appCopyright) { this.appPlist.NSHumanReadableCopyright = this.appCopyright; } if (this.enableDarkMode) { this.appPlist.NSRequiresAquaSystemAppearance = false; } if (this.usageDescription) { for (const [type, description] of Object.entries(this.usageDescription)) { const usageTypeKey = `NS${type}UsageDescription`; for (const plistKey of plistsToUpdate) { this[plistKey][usageTypeKey] = description; } this.appPlist[usageTypeKey] = description; } } await Promise.all(plists.map(([filename, varName]) => fs_extra_1.default.writeFile(filename, plist_1.default.build(this[varName])))); } async moveHelpers() { const helpers = [' Helper', ' Helper EH', ' Helper NP', ' Helper (Renderer)', ' Helper (Plugin)', ' Helper (GPU)']; await Promise.all(helpers.map(suffix => this.moveHelper(this.frameworksPath, suffix))); if (await fs_extra_1.default.pathExists(this.loginItemsPath)) { await this.moveHelper(this.loginItemsPath, ' Login Helper'); } } async moveHelper(helperDirectory, suffix) { const originalBasename = `Electron${suffix}`; if (await fs_extra_1.default.pathExists(path_1.default.join(helperDirectory, `${originalBasename}.app`))) { return this.renameHelperAndExecutable(helperDirectory, originalBasename, `${(0, common_1.sanitizeAppName)(this.appName)}${suffix}`); } else { return Promise.resolve(); } } async renameHelperAndExecutable(helperDirectory, originalBasename, newBasename) { const originalAppname = `${originalBasename}.app`; const executableBasePath = path_1.default.join(helperDirectory, originalAppname, 'Contents', 'MacOS'); await this.relativeRename(executableBasePath, originalBasename, newBasename); await this.relativeRename(helperDirectory, originalAppname, `${newBasename}.app`); } async copyIcon() { if (!this.opts.icon) { return Promise.resolve(); } let icon; try { icon = await this.normalizeIconExtension('.icns'); } catch { // Ignore error if icon doesn't exist, in case it's only available for other OSes /* istanbul ignore next */ return Promise.resolve(); } if (icon) { (0, common_1.debug)(`Copying icon "${icon}" to app's Resources as "${this.appPlist.CFBundleIconFile}"`); await fs_extra_1.default.copy(icon, path_1.default.join(this.originalResourcesDir, this.appPlist.CFBundleIconFile)); } } async renameAppAndHelpers() { await this.moveHelpers(); await fs_extra_1.default.rename(this.electronAppPath, this.renamedAppPath); } async signAppIfSpecified() { const osxSignOpt = this.opts.osxSign; const platform = this.opts.platform; const version = this.opts.electronVersion; if ((platform === 'all' || platform === 'mas') && osxSignOpt === undefined) { (0, common_1.warning)('signing is required for mas builds. Provide the osx-sign option, ' + 'or manually sign the app later.', this.opts.quiet); } if (osxSignOpt) { const signOpts = createSignOpts(osxSignOpt, platform, this.renamedAppPath, version, this.opts.quiet); (0, common_1.debug)(`Running @electron/osx-sign with the options ${JSON.stringify(signOpts)}`); try { await (0, osx_sign_1.signApp)(signOpts); } catch (err) { // Although not signed successfully, the application is packed. if (signOpts.continueOnError) { (0, common_1.warning)(`Code sign failed; please retry manually. ${err}`, this.opts.quiet); } else { throw err; } } } } async notarizeAppIfSpecified() { const osxNotarizeOpt = this.opts.osxNotarize; /* istanbul ignore if */ if (osxNotarizeOpt) { const notarizeOpts = createNotarizeOpts(osxNotarizeOpt, this.bundleName, this.renamedAppPath, Boolean(this.opts.quiet)); if (notarizeOpts) { return (0, notarize_1.notarize)(notarizeOpts); } } } async create() { await this.initialize(); await this.updatePlistFiles(); await this.copyIcon(); await this.renameElectron(); await this.renameAppAndHelpers(); await this.copyExtraResources(); await this.signAppIfSpecified(); await this.notarizeAppIfSpecified(); return this.move(); } } exports.MacApp = MacApp; exports.App = MacApp; /** * Remove special characters and allow only alphanumeric (A-Z,a-z,0-9), hyphen (-), and period (.) * Apple documentation: * https://developer.apple.com/library/mac/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html#//apple_ref/doc/uid/20001431-102070 */ function filterCFBundleIdentifier(identifier) { return identifier.replace(/ /g, '-').replace(/[^a-zA-Z0-9.-]/g, ''); } exports.filterCFBundleIdentifier = filterCFBundleIdentifier; function createSignOpts(properties, platform, app, version, quiet) { // use default sign opts if osx-sign is true, otherwise clone osx-sign object const signOpts = (properties === true ? { identity: null } : { ...properties }); // osx-sign options are handed off to sign module, but // with a few additions from the main options // user may think they can pass platform, app, or version, but they will be ignored (0, common_1.subOptionWarning)(signOpts, 'osx-sign', 'platform', platform, quiet); (0, common_1.subOptionWarning)(signOpts, 'osx-sign', 'app', app, quiet); (0, common_1.subOptionWarning)(signOpts, 'osx-sign', 'version', version, quiet); if (signOpts.binaries) { (0, common_1.warning)('osx-sign.binaries is not an allowed sub-option. Not passing to @electron/osx-sign.', quiet); delete signOpts.binaries; } // Take argument osx-sign as signing identity: // if opts.osxSign is true (bool), fallback to identity=null for // autodiscovery. Otherwise, provide signing certificate info. if (signOpts.identity === true) { signOpts.identity = null; } // Default to `continueOnError: true` since this was the default behavior before this option was added if (signOpts.continueOnError !== false) { signOpts.continueOnError = true; } return signOpts; } exports.createSignOpts = createSignOpts; function createNotarizeOpts(properties, appBundleId, appPath, quiet) { // osxNotarize options are handed off to the @electron/notarize module, but with a few // additions from the main options. The user may think they can pass appPath, // but it will be ignored. (0, common_1.subOptionWarning)(properties, 'osxNotarize', 'appPath', appPath, quiet); return properties; } exports.createNotarizeOpts = createNotarizeOpts; //# sourceMappingURL=mac.js.map