Skip to content

Commit

Permalink
Merge pull request #223 from sethlu/master
Browse files Browse the repository at this point in the history
Platform: Mac App Store support

Fixes #104, #163, #261.
  • Loading branch information
malept committed Feb 18, 2016
2 parents 79e3a4c + 9212919 commit 3a64e65
Show file tree
Hide file tree
Showing 14 changed files with 417 additions and 130 deletions.
11 changes: 8 additions & 3 deletions index.js
Expand Up @@ -20,9 +20,14 @@ var supportedPlatforms = {
// Maps to module ID for each platform (lazy-required if used)
darwin: './mac',
linux: './linux',
mas: './mac', // map to darwin
win32: './win32'
}

function isPlatformMac (platform) {
return platform === 'darwin' || platform === 'mas'
}

function validateList (list, supported, name) {
// Validates list of architectures or platforms.
// Returns a normalized array if successful, or an error message string otherwise.
Expand Down Expand Up @@ -95,7 +100,7 @@ function createSeries (opts, archs, platforms) {
archs.forEach(function (arch) {
platforms.forEach(function (platform) {
// Electron does not have 32-bit releases for Mac OS X, so skip that combination
if (platform === 'darwin' && arch === 'ia32') return
if (isPlatformMac(platform) && arch === 'ia32') return
combinations.push({
platform: platform,
arch: arch,
Expand Down Expand Up @@ -161,11 +166,11 @@ function createSeries (opts, archs, platforms) {
})
}

if (combination.platform === 'darwin') {
if (isPlatformMac(combination.platform)) {
testSymlink(function (result) {
if (result) return checkOverwrite()

console.error('Cannot create symlinks; skipping darwin platform')
console.error('Cannot create symlinks; skipping ' + combination.platform + ' platform')
callback()
})
} else {
Expand Down
59 changes: 53 additions & 6 deletions mac.js
@@ -1,12 +1,12 @@
var path = require('path')
var fs = require('fs')
var child = require('child_process')

var plist = require('plist')
var mv = require('mv')
var ncp = require('ncp').ncp
var series = require('run-series')
var common = require('./common')
var sign = require('electron-osx-sign')

function moveHelpers (frameworksPath, appName, callback) {
function rename (basePath, oldName, newName, cb) {
Expand All @@ -27,6 +27,12 @@ function moveHelpers (frameworksPath, appName, callback) {
})
}

function filterCFBundleIdentifier (identifier) {
// 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
return identifier.replace(/ /g, '-').replace(/[^a-zA-Z0-9.-]/g, '')
}

module.exports = {
createApp: function createApp (opts, templatePath, callback) {
var appRelativePath = path.join('Electron.app', 'Contents', 'Resources', 'app')
Expand All @@ -37,20 +43,38 @@ module.exports = {
var frameworksPath = path.join(contentsPath, 'Frameworks')
var appPlistFilename = path.join(contentsPath, 'Info.plist')
var helperPlistFilename = path.join(frameworksPath, 'Electron Helper.app', 'Contents', 'Info.plist')
var helperEHPlistFilename = path.join(frameworksPath, 'Electron Helper EH.app', 'Contents', 'Info.plist')
var helperNPPlistFilename = path.join(frameworksPath, 'Electron Helper NP.app', 'Contents', 'Info.plist')
var appPlist = plist.parse(fs.readFileSync(appPlistFilename).toString())
var helperPlist = plist.parse(fs.readFileSync(helperPlistFilename).toString())
var helperEHPlist = plist.parse(fs.readFileSync(helperEHPlistFilename).toString())
var helperNPPlist = plist.parse(fs.readFileSync(helperNPPlistFilename).toString())

// Update plist files
var defaultBundleName = 'com.electron.' + opts.name.toLowerCase().replace(/ /g, '_')
var defaultBundleName = 'com.electron.' + opts.name.toLowerCase()
var appBundleIdentifier = filterCFBundleIdentifier(opts['app-bundle-id'] || defaultBundleName)
var helperBundleIdentifier = filterCFBundleIdentifier(opts['helper-bundle-id'] || appBundleIdentifier + '.helper')

var appVersion = opts['app-version']
var buildVersion = opts['build-version']
var appCategoryType = opts['app-category-type']
var humanReadableCopyright = opts['app-copyright']

appPlist.CFBundleDisplayName = opts.name
appPlist.CFBundleIdentifier = opts['app-bundle-id'] || defaultBundleName
appPlist.CFBundleIdentifier = appBundleIdentifier
appPlist.CFBundleName = opts.name
helperPlist.CFBundleIdentifier = opts['helper-bundle-id'] || defaultBundleName + '.helper'
helperPlist.CFBundleDisplayName = opts.name + ' Helper'
helperPlist.CFBundleIdentifier = helperBundleIdentifier
helperPlist.CFBundleName = opts.name
helperPlist.CFBundleExecutable = opts.name + ' Helper'
helperEHPlist.CFBundleDisplayName = opts.name + ' Helper EH'
helperEHPlist.CFBundleIdentifier = helperBundleIdentifier + '.EH'
helperEHPlist.CFBundleName = opts.name + ' Helper EH'
helperEHPlist.CFBundleExecutable = opts.name + ' Helper EH'
helperNPPlist.CFBundleDisplayName = opts.name + ' Helper NP'
helperNPPlist.CFBundleIdentifier = helperBundleIdentifier + '.NP'
helperNPPlist.CFBundleName = opts.name + ' Helper NP'
helperNPPlist.CFBundleExecutable = opts.name + ' Helper NP'

if (appVersion) {
appPlist.CFBundleShortVersionString = appPlist.CFBundleVersion = '' + appVersion
Expand All @@ -73,8 +97,14 @@ module.exports = {
appPlist.LSApplicationCategoryType = appCategoryType
}

if (humanReadableCopyright) {
appPlist.NSHumanReadableCopyright = humanReadableCopyright
}

fs.writeFileSync(appPlistFilename, plist.build(appPlist))
fs.writeFileSync(helperPlistFilename, plist.build(helperPlist))
fs.writeFileSync(helperEHPlistFilename, plist.build(helperEHPlist))
fs.writeFileSync(helperNPPlistFilename, plist.build(helperNPPlist))

var operations = []

Expand All @@ -101,7 +131,23 @@ module.exports = {

if (opts.sign) {
operations.push(function (cb) {
child.exec('codesign --deep --force --sign "' + opts.sign + '" "' + finalAppPath + '"', cb)
sign({
app: finalAppPath,
platform: opts.platform,
// Take argument sign as signing identity:
// Provided in command line --sign, opts.sign will be recognized
// as boolean value true. Then fallback to null for auto discovery,
// otherwise provided signing certificate.
identity: opts.sign === true ? null : opts.sign,
entitlements: opts['sign-entitlements']
}, function (err) {
if (err) {
console.warn('Code sign failed; please retry manually.')
// Though not signed successfully, the application is packed.
// It might have to be signed for another time manually.
}
cb()
})
})
}

Expand All @@ -110,5 +156,6 @@ module.exports = {
common.moveApp(opts, tempPath, callback)
})
})
}
},
filterCFBundleIdentifier: filterCFBundleIdentifier
}
1 change: 1 addition & 0 deletions package.json
Expand Up @@ -19,6 +19,7 @@
"dependencies": {
"asar": "^0.8.2",
"electron-download": "^1.0.0",
"electron-osx-sign": "^0.1.6",
"extract-zip": "^1.0.3",
"get-package-info": "0.0.2",
"minimist": "^1.1.1",
Expand Down
30 changes: 27 additions & 3 deletions readme.md
@@ -1,6 +1,6 @@
# electron-packager

Package your [Electron](http://electron.atom.io) app into OS-specific bundles (`.app`, `.exe`, etc.) via JavaScript or the command line. Supports building Windows, Linux or Mac executables.
Package your [Electron](http://electron.atom.io) app into OS-specific bundles (`.app`, `.exe`, etc.) via JavaScript or the command line.

[![Build Status](https://travis-ci.org/maxogden/electron-packager.svg?branch=master)](https://travis-ci.org/maxogden/electron-packager)
[![Coverage Status](https://coveralls.io/repos/github/maxogden/electron-packager/badge.svg?branch=master)](https://coveralls.io/github/maxogden/electron-packager?branch=master)
Expand All @@ -13,6 +13,22 @@ This module was developed as part of [Dat](http://dat-data.com/), a grant funded

Note that packaged Electron applications can be relatively large. A zipped barebones OS X Electron application is around 40MB.

## Supported Platforms

Electron Packager is known to run on the following **host** platforms:

* Windows (32/64 bit)
* OS X
* Linux (x86/x86_64)

It generates executables/bundles for the following **target** platforms:

* Windows (also known as `win32`, for both 32/64 bit)
* OS X (also known as `darwin`) / [Mac App Store](http://electron.atom.io/docs/v0.36.0/tutorial/mac-app-store-submission-guide/) (also known as `mas`)<sup>*</sup>
* Linux (for both x86/x86_64)

<sup>*</sup> *Note for OS X / MAS target bundles: the `.app` bundle can only be signed when building on a host OS X platform.*

## Installation

```sh
Expand Down Expand Up @@ -137,6 +153,10 @@ packager(opts, function done (err, appPath) { })

Valid values are listed in [Apple's documentation](https://developer.apple.com/library/ios/documentation/General/Reference/InfoPlistKeyReference/Articles/LaunchServicesKeys.html#//apple_ref/doc/uid/TP40009250-SW8).

`app-copyright` - *String*

The copyrights string to use in the app plist, will be displayed in the application About box (OS X only).

`app-version` - *String*

The release version of the application. Maps to the `ProductVersion` metadata property on Windows, and `CFBundleShortVersionString` on OS X.
Expand Down Expand Up @@ -199,7 +219,11 @@ If the file extension is omitted, it is auto-completed to the correct extension

`sign` - *String*

The identity used when signing the package via `codesign`. (Only for the OS X target platform, when XCode is present on the build platform.)
The identity used when signing the package via `codesign`. (Only for the OS X / Mac App Store target platforms, when XCode is present on the host platform.)

`sign-entitlements` - *String*

The path to entitlements used in signing. (Currently limited to Mac App Store distribution.)

`strict-ssl` - *Boolean*

Expand Down Expand Up @@ -237,7 +261,7 @@ If the file extension is omitted, it is auto-completed to the correct extension

## Building Windows apps from non-Windows platforms

Building an Electron app for the Windows platform with a custom icon requires editing the `Electron.exe` file. Currently, electron-packager uses [node-rcedit](https://github.com/atom/node-rcedit) to accomplish this. A Windows executable is bundled in that node package and needs to be run in order for this functionality to work, so on non-Windows platforms, [Wine](https://www.winehq.org/) needs to be installed. On OS X, it is installable via [Homebrew](http://brew.sh/).
Building an Electron app for the Windows platform with a custom icon requires editing the `Electron.exe` file. Currently, electron-packager uses [node-rcedit](https://github.com/atom/node-rcedit) to accomplish this. A Windows executable is bundled in that node package and needs to be run in order for this functionality to work, so on non-Windows host platforms, [Wine](https://www.winehq.org/) needs to be installed. On OS X, it is installable via [Homebrew](http://brew.sh/).

## Related

Expand Down
6 changes: 3 additions & 3 deletions test/basic.js
Expand Up @@ -15,7 +15,7 @@ function generateNamePath (opts) {
// Generates path to verify reflects the name given in the options.
// Returns the Helper.app location on darwin since the top-level .app is already tested for the resources path;
// returns the executable for other OSes
if (opts.platform === 'darwin') {
if (util.isPlatformMac(opts.platform)) {
return path.join(opts.name + '.app', 'Contents', 'Frameworks', opts.name + ' Helper.app')
}

Expand Down Expand Up @@ -49,7 +49,7 @@ function createDefaultsTest (combination) {
resourcesPath = path.join(finalPath, util.generateResourcesPath(opts))
fs.stat(path.join(finalPath, generateNamePath(opts)), cb)
}, function (stats, cb) {
if (opts.platform === 'darwin') {
if (util.isPlatformMac(opts.platform)) {
t.true(stats.isDirectory(), 'The Helper.app should reflect opts.name')
} else {
t.true(stats.isFile(), 'The executable should reflect opts.name')
Expand Down Expand Up @@ -296,7 +296,7 @@ function createInferTest (combination) {
opts.name = packageJSON.productName
fs.stat(path.join(finalPath, generateNamePath(opts)), cb)
}, function (stats, cb) {
if (opts.platform === 'darwin') {
if (util.isPlatformMac(opts.platform)) {
t.true(stats.isDirectory(), 'The Helper.app should reflect productName')
} else {
t.true(stats.isFile(), 'The executable should reflect productName')
Expand Down
2 changes: 1 addition & 1 deletion test/config.json
@@ -1,4 +1,4 @@
{
"timeout": 30000,
"version": "0.28.3"
"version": "0.35.6"
}
13 changes: 13 additions & 0 deletions test/darwin.js
@@ -0,0 +1,13 @@
var path = require('path')

var config = require('./config.json')

var baseOpts = {
name: 'basicTest',
dir: path.join(__dirname, 'fixtures', 'basic'),
version: config.version,
arch: 'x64',
platform: 'darwin'
}

require('./mac')(baseOpts)
2 changes: 1 addition & 1 deletion test/fixtures/basic/package.json
Expand Up @@ -7,6 +7,6 @@
"devDependencies": {
"ncp": "^2.0.0",
"run-waterfall": "^1.1.1",
"electron-prebuilt": "0.36.4"
"electron-prebuilt": "0.35.6"
}
}
3 changes: 2 additions & 1 deletion test/index.js
Expand Up @@ -23,6 +23,7 @@ series([

if (process.platform !== 'win32') {
// Perform additional tests specific to building for OS X
require('./mac')
require('./darwin')
require('./mas')
}
})

0 comments on commit 3a64e65

Please sign in to comment.