mac.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333
  1. 'use strict'
  2. const App = require('./platform')
  3. const common = require('./common')
  4. const debug = require('debug')('electron-packager')
  5. const fs = require('fs-extra')
  6. const path = require('path')
  7. const plist = require('plist')
  8. const sign = require('electron-osx-sign').signAsync
  9. class MacApp extends App {
  10. constructor (opts, templatePath) {
  11. super(opts, templatePath)
  12. this.appName = opts.name
  13. }
  14. get appCategoryType () {
  15. return this.opts.appCategoryType
  16. }
  17. get appCopyright () {
  18. return this.opts.appCopyright
  19. }
  20. get appVersion () {
  21. return this.opts.appVersion
  22. }
  23. get buildVersion () {
  24. return this.opts.buildVersion
  25. }
  26. get protocols () {
  27. return this.opts.protocols.map((protocol) => {
  28. return {
  29. CFBundleURLName: protocol.name,
  30. CFBundleURLSchemes: [].concat(protocol.schemes)
  31. }
  32. })
  33. }
  34. get dotAppName () {
  35. return `${common.sanitizeAppName(this.appName)}.app`
  36. }
  37. get defaultBundleName () {
  38. return `com.electron.${common.sanitizeAppName(this.appName).toLowerCase()}`
  39. }
  40. get originalResourcesDir () {
  41. return path.join(this.contentsPath, 'Resources')
  42. }
  43. get resourcesDir () {
  44. return path.join(this.dotAppName, 'Contents', 'Resources')
  45. }
  46. get electronBinaryDir () {
  47. return path.join(this.contentsPath, 'MacOS')
  48. }
  49. get originalElectronName () {
  50. return 'Electron'
  51. }
  52. get newElectronName () {
  53. return this.appPlist.CFBundleExecutable
  54. }
  55. get renamedAppPath () {
  56. return path.join(this.stagingPath, this.dotAppName)
  57. }
  58. get electronAppPath () {
  59. return path.join(this.stagingPath, `${this.originalElectronName}.app`)
  60. }
  61. get contentsPath () {
  62. return path.join(this.electronAppPath, 'Contents')
  63. }
  64. get frameworksPath () {
  65. return path.join(this.contentsPath, 'Frameworks')
  66. }
  67. get loginItemsPath () {
  68. return path.join(this.contentsPath, 'Library', 'LoginItems')
  69. }
  70. get loginHelperPath () {
  71. return path.join(this.loginItemsPath, 'Electron Login Helper.app')
  72. }
  73. updatePlist (base, displayName, identifier, name) {
  74. return Object.assign(base, {
  75. CFBundleDisplayName: displayName,
  76. CFBundleExecutable: common.sanitizeAppName(displayName),
  77. CFBundleIdentifier: identifier,
  78. CFBundleName: common.sanitizeAppName(name)
  79. })
  80. }
  81. updateHelperPlist (base, suffix) {
  82. let helperSuffix, identifier, name
  83. if (suffix) {
  84. helperSuffix = `Helper ${suffix}`
  85. identifier = `${this.helperBundleIdentifier}.${suffix}`
  86. name = `${this.appName} ${helperSuffix}`
  87. } else {
  88. helperSuffix = 'Helper'
  89. identifier = this.helperBundleIdentifier
  90. name = this.appName
  91. }
  92. return this.updatePlist(base, `${this.appName} ${helperSuffix}`, identifier, name)
  93. }
  94. extendAppPlist (propsOrFilename) {
  95. if (!propsOrFilename) {
  96. return Promise.resolve()
  97. }
  98. if (typeof propsOrFilename === 'string') {
  99. return this.loadPlist(propsOrFilename)
  100. .then(plist => Object.assign(this.appPlist, plist))
  101. } else {
  102. return Promise.resolve(Object.assign(this.appPlist, propsOrFilename))
  103. }
  104. }
  105. loadPlist (filename, propName) {
  106. return fs.readFile(filename)
  107. .then(buffer => plist.parse(buffer.toString()))
  108. .then(plist => {
  109. if (propName) this[propName] = plist
  110. return plist
  111. })
  112. }
  113. ehPlistFilename (helper) {
  114. return this.helperPlistFilename(path.join(this.frameworksPath, helper))
  115. }
  116. helperPlistFilename (helperApp) {
  117. return path.join(helperApp, 'Contents', 'Info.plist')
  118. }
  119. determinePlistFilesToUpdate () {
  120. const appPlistFilename = path.join(this.contentsPath, 'Info.plist')
  121. const helperPlistFilename = this.ehPlistFilename('Electron Helper.app')
  122. const helperEHPlistFilename = this.ehPlistFilename('Electron Helper EH.app')
  123. const helperNPPlistFilename = this.ehPlistFilename('Electron Helper NP.app')
  124. const loginHelperPlistFilename = this.helperPlistFilename(this.loginHelperPath)
  125. const plists = [
  126. [appPlistFilename, 'appPlist'],
  127. [helperPlistFilename, 'helperPlist'],
  128. [helperEHPlistFilename, 'helperEHPlist'],
  129. [helperNPPlistFilename, 'helperNPPlist']
  130. ]
  131. return fs.pathExists(loginHelperPlistFilename)
  132. .then(exists => {
  133. if (exists) {
  134. plists.push([loginHelperPlistFilename, 'loginHelperPlist'])
  135. }
  136. return plists
  137. })
  138. }
  139. updatePlistFiles () {
  140. let plists
  141. const appBundleIdentifier = filterCFBundleIdentifier(this.opts.appBundleId || this.defaultBundleName)
  142. this.helperBundleIdentifier = filterCFBundleIdentifier(this.opts.helperBundleId || `${appBundleIdentifier}.helper`)
  143. return this.determinePlistFilesToUpdate()
  144. .then(plistsToUpdate => {
  145. plists = plistsToUpdate
  146. return Promise.all(plists.map(plistArgs => this.loadPlist.apply(this, plistArgs)))
  147. }).then(() => this.extendAppPlist(this.opts.extendInfo))
  148. .then(() => {
  149. this.appPlist = this.updatePlist(this.appPlist, this.executableName, appBundleIdentifier, this.appName)
  150. this.helperPlist = this.updateHelperPlist(this.helperPlist)
  151. this.helperEHPlist = this.updateHelperPlist(this.helperEHPlist, 'EH')
  152. this.helperNPPlist = this.updateHelperPlist(this.helperNPPlist, 'NP')
  153. if (this.loginHelperPlist) {
  154. const loginHelperName = common.sanitizeAppName(`${this.appName} Login Helper`)
  155. this.loginHelperPlist.CFBundleExecutable = loginHelperName
  156. this.loginHelperPlist.CFBundleIdentifier = `${appBundleIdentifier}.loginhelper`
  157. this.loginHelperPlist.CFBundleName = loginHelperName
  158. }
  159. if (this.appVersion) {
  160. this.appPlist.CFBundleShortVersionString = this.appPlist.CFBundleVersion = '' + this.appVersion
  161. }
  162. if (this.buildVersion) {
  163. this.appPlist.CFBundleVersion = '' + this.buildVersion
  164. }
  165. if (this.opts.protocols && this.opts.protocols.length) {
  166. this.appPlist.CFBundleURLTypes = this.protocols
  167. }
  168. if (this.appCategoryType) {
  169. this.appPlist.LSApplicationCategoryType = this.appCategoryType
  170. }
  171. if (this.appCopyright) {
  172. this.appPlist.NSHumanReadableCopyright = this.appCopyright
  173. }
  174. return Promise.all(plists.map(plistArgs => {
  175. const filename = plistArgs[0]
  176. const varName = plistArgs[1]
  177. return fs.writeFile(filename, plist.build(this[varName]))
  178. }))
  179. })
  180. }
  181. moveHelpers () {
  182. const helpers = [' Helper', ' Helper EH', ' Helper NP']
  183. return Promise.all(helpers.map(suffix => this.moveHelper(this.frameworksPath, suffix)))
  184. .then(() => fs.pathExists(this.loginItemsPath))
  185. .then(exists => exists ? this.moveHelper(this.loginItemsPath, ' Login Helper') : null)
  186. }
  187. moveHelper (helperDirectory, suffix) {
  188. const originalBasename = `Electron${suffix}`
  189. const newBasename = `${common.sanitizeAppName(this.appName)}${suffix}`
  190. const originalAppname = `${originalBasename}.app`
  191. const executableBasePath = path.join(helperDirectory, originalAppname, 'Contents', 'MacOS')
  192. return this.relativeRename(executableBasePath, originalBasename, newBasename)
  193. .then(() => this.relativeRename(helperDirectory, originalAppname, `${newBasename}.app`))
  194. }
  195. copyIcon () {
  196. if (!this.opts.icon) {
  197. return Promise.resolve()
  198. }
  199. return this.normalizeIconExtension('.icns')
  200. // Ignore error if icon doesn't exist, in case it's only available for other OS
  201. .catch(Promise.resolve)
  202. .then(icon => {
  203. debug(`Copying icon "${icon}" to app's Resources as "${this.appPlist.CFBundleIconFile}"`)
  204. return fs.copy(icon, path.join(this.originalResourcesDir, this.appPlist.CFBundleIconFile))
  205. })
  206. }
  207. renameAppAndHelpers () {
  208. return this.moveHelpers()
  209. .then(() => fs.rename(this.electronAppPath, this.renamedAppPath))
  210. }
  211. signAppIfSpecified () {
  212. let osxSignOpt = this.opts.osxSign
  213. let platform = this.opts.platform
  214. let version = this.opts.electronVersion
  215. if ((platform === 'all' || platform === 'mas') &&
  216. osxSignOpt === undefined) {
  217. common.warning('signing is required for mas builds. Provide the osx-sign option, ' +
  218. 'or manually sign the app later.')
  219. }
  220. if (osxSignOpt) {
  221. const signOpts = createSignOpts(osxSignOpt, platform, this.renamedAppPath, version, this.opts.quiet)
  222. debug(`Running electron-osx-sign with the options ${JSON.stringify(signOpts)}`)
  223. return sign(signOpts)
  224. // Although not signed successfully, the application is packed.
  225. .catch(err => common.warning(`Code sign failed; please retry manually. ${err}`))
  226. } else {
  227. return Promise.resolve()
  228. }
  229. }
  230. create () {
  231. return this.initialize()
  232. .then(() => this.updatePlistFiles())
  233. .then(() => this.copyIcon())
  234. .then(() => this.renameElectron())
  235. .then(() => this.renameAppAndHelpers())
  236. .then(() => this.copyExtraResources())
  237. .then(() => this.signAppIfSpecified())
  238. .then(() => this.move())
  239. }
  240. }
  241. /**
  242. * Remove special characters and allow only alphanumeric (A-Z,a-z,0-9), hyphen (-), and period (.)
  243. * Apple documentation:
  244. * https://developer.apple.com/library/mac/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html#//apple_ref/doc/uid/20001431-102070
  245. */
  246. function filterCFBundleIdentifier (identifier) {
  247. return identifier.replace(/ /g, '-').replace(/[^a-zA-Z0-9.-]/g, '')
  248. }
  249. function createSignOpts (properties, platform, app, version, quiet) {
  250. // use default sign opts if osx-sign is true, otherwise clone osx-sign object
  251. let signOpts = properties === true ? {identity: null} : Object.assign({}, properties)
  252. // osx-sign options are handed off to sign module, but
  253. // with a few additions from the main options
  254. // user may think they can pass platform, app, or version, but they will be ignored
  255. common.subOptionWarning(signOpts, 'osx-sign', 'platform', platform, quiet)
  256. common.subOptionWarning(signOpts, 'osx-sign', 'app', app, quiet)
  257. common.subOptionWarning(signOpts, 'osx-sign', 'version', version, quiet)
  258. if (signOpts.binaries) {
  259. common.warning('osx-sign.binaries is not an allowed sub-option. Not passing to electron-osx-sign.')
  260. delete signOpts.binaries
  261. }
  262. // Take argument osx-sign as signing identity:
  263. // if opts.osxSign is true (bool), fallback to identity=null for
  264. // autodiscovery. Otherwise, provide signing certificate info.
  265. if (signOpts.identity === true) {
  266. signOpts.identity = null
  267. }
  268. return signOpts
  269. }
  270. module.exports = {
  271. App: MacApp,
  272. createSignOpts: createSignOpts,
  273. filterCFBundleIdentifier: filterCFBundleIdentifier
  274. }