Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Platform: Mac App Store support #223

Merged
merged 6 commits into from Feb 18, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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).
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"copyright", not "copyrights string". I'll fix this post-merge. (I'm doing #265 anyway.)


`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')
}
})