Skip to content

Commit

Permalink
feat: mac app store
Browse files Browse the repository at this point in the history
Closes #332
  • Loading branch information
develar committed Apr 25, 2016
1 parent 0f19455 commit 260ca0b
Show file tree
Hide file tree
Showing 24 changed files with 213 additions and 88 deletions.
1 change: 1 addition & 0 deletions .idea/dictionaries/develar.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions .idea/runConfigurations/CodeSignTest.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

21 changes: 1 addition & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ npm install electron-builder --save-dev

* [Native application dependencies](http://electron.atom.io/docs/latest/tutorial/using-native-node-modules/) compilation (only if [two-package.json project structure](#two-packagejson-structure) used).
* [Auto Update](#auto-update) ready application packaging.
* [Code Signing](#code-signing) on a CI server or development machine.
* [Code Signing](https://github.com/electron-userland/electron-builder/wiki/Code-Signing) on a CI server or development machine.
* [Build version management](#build-version-management).
* [Publishing artifacts to GitHub Releases](https://github.com/electron-userland/electron-builder/wiki/Publishing-Artifacts).

Expand Down Expand Up @@ -86,25 +86,6 @@ Please note — packaged into an asar archive [by default](https://github.com/el
You need to deploy somewhere releases/downloads server.
Consider to use [Nuts](https://github.com/GitbookIO/nuts) (GitHub as a backend to store assets) or [Electron Release Server](https://github.com/ArekSredzki/electron-release-server).

# Code Signing
OS X and Windows code singing is supported.
On a development machine set environment variable `CSC_NAME` to your identity (recommended). Or pass `--sign` parameter.
```
export CSC_NAME="Developer ID Application: Your Name (code)"
```
## Travis, AppVeyor and other CI servers
To sign app on build server:
1. [Export](https://developer.apple.com/library/ios/documentation/IDEs/Conceptual/AppDistributionGuide/MaintainingCertificates/MaintainingCertificates.html#//apple_ref/doc/uid/TP40012582-CH31-SW7) certificate.
[Strong password](http://security.stackexchange.com/a/54773) must be used. Consider to not use special characters (for bash) because “*values are not escaped when your builds are executed*”.
2. Upload `*.p12` file (e.g. on [Google Drive](http://www.syncwithtech.org/p/direct-download-link-generator.html)).
3. Set ([Travis](https://docs.travis-ci.com/user/environment-variables/#Encrypted-Variables) or [AppVeyor](https://ci.appveyor.com/tools/encrypt)) `CSC_LINK` and `CSC_KEY_PASSWORD` environment variables:
```
travis encrypt "CSC_LINK='https://drive.google.com/uc?export=download&id=***'" --add
travis encrypt 'CSC_KEY_PASSWORD=beAwareAboutBashEscaping!!!' --add
```
# Build Version Management
`CFBundleVersion` (OS X) and `FileVersion` (Windows) will be set automatically to `version`.`build_number` on CI server (Travis, AppVeyor and CircleCI supported).

Expand Down
28 changes: 28 additions & 0 deletions docs/Code Signing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
OS X and Windows code singing is supported.

On a development machine set environment variable `CSC_NAME` (and `CSC_INSTALLER_NAME` if you build for Mac App Store) to your identity.

| Env name | Description
| -------------- | -----------
| `CSC_LINK` | The HTTPS link to certificate (`*.p12` file).
| `CSC_KEY_PASSWORD` | The password to decrypt the certificate given in `CSC_LINK`.
| `CSC_INSTALLER_LINK` | *osx-only* The HTTPS link to certificate to sign Mac App Store build (`*.p12` file).
| `CSC_INSTALLER_KEY_PASSWORD` | *osx-only* The password to decrypt the certificate given in `CSC_INSTALLER_LINK`.
| `CSC_NAME` | *osx-only* Name of certificate (to retrieve from login.keychain). Useful on a development machine (not on CI).
| `CSC_INSTALLER_NAME` | *osx-only* Name of installer certificate (to retrieve from login.keychain). Useful on a development machine (not on CI).

```
export CSC_NAME="Developer ID Application: Your Name (code)"
```

## Travis, AppVeyor and other CI servers
To sign app on build server you need to set `CSC_LINK`, `CSC_KEY_PASSWORD` (and `CSC_INSTALLER_LINK`, `CSC_INSTALLER_KEY_PASSWORD` if you build for Mac App Store):

1. [Export](https://developer.apple.com/library/ios/documentation/IDEs/Conceptual/AppDistributionGuide/MaintainingCertificates/MaintainingCertificates.html#//apple_ref/doc/uid/TP40012582-CH31-SW7) certificate.
[Strong password](http://security.stackexchange.com/a/54773) must be used. Consider to not use special characters (for bash) because “*values are not escaped when your builds are executed*”.
2. Upload `*.p12` file (e.g. on [Google Drive](http://www.syncwithtech.org/p/direct-download-link-generator.html)).
3. Set ([Travis](https://docs.travis-ci.com/user/environment-variables/#Encrypted-Variables) or [AppVeyor](https://ci.appveyor.com/tools/encrypt)) `CSC_LINK` and `CSC_KEY_PASSWORD` environment variables:
```
travis encrypt "CSC_LINK='https://drive.google.com/uc?export=download&id=***'" --add
travis encrypt 'CSC_KEY_PASSWORD=beAwareAboutBashEscaping!!!' --add
```
4 changes: 2 additions & 2 deletions docs/Options.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ Here documented only `electron-builder` specific options:
| app-bundle-id | <a name="BuildMetadata-app-bundle-id"></a>*OS X-only.* The bundle identifier to use in the application's plist.
| app-category-type | <a name="BuildMetadata-app-category-type"></a><p>*OS X-only.* The application category type, as shown in the Finder via *View -&gt; Arrange by Application Category* when viewing the Applications directory.</p> <p>For example, <code>app-category-type=public.app-category.developer-tools</code> will set the application category to *Developer Tools*.</p> <p>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).</p>
| asar | <a name="BuildMetadata-asar"></a><p>Whether to package the application’s source code into an archive, using [Electron’s archive format](https://github.com/electron/asar). Defaults to <code>true</code>. Reasons why you may want to disable this feature are described in [an application packaging tutorial in Electron’s documentation](http://electron.atom.io/docs/latest/tutorial/application-packaging/#limitations-on-node-api/).</p>
| iconUrl | <a name="BuildMetadata-iconUrl"></a><p>*windows-only.* A URL to an ICO file to use as the application icon (displayed in Control Panel &gt; Programs and Features). Defaults to the Electron icon.</p> <p>Please note — [local icon file url is not accepted](https://github.com/atom/grunt-electron-installer/issues/73), must be https/http.</p> <ul> <li>If you don’t plan to build windows installer, you can omit it.</li> <li>If your project repository is public on GitHub, it will be <code>https://raw.githubusercontent.com/${user}/${project}/master/build/icon.ico</code> by default.</li> </ul>
| iconUrl | <a name="BuildMetadata-iconUrl"></a><p>*windows-only.* A URL to an ICO file to use as the application icon (displayed in Control Panel &gt; Programs and Features). Defaults to the Electron icon.</p> <p>Please note — [local icon file url is not accepted](https://github.com/atom/grunt-electron-installer/issues/73), must be https/http.</p> <ul> <li>If you don’t plan to build windows installer, you can omit it.</li> <li>If your project repository is public on GitHub, it will be <code>https://raw.githubusercontent.com/${u}/${p}/master/build/icon.ico</code> by default.</li> </ul>
| productName | <a name="BuildMetadata-productName"></a>See [AppMetadata.productName](#AppMetadata-productName).
| extraResources | <a name="BuildMetadata-extraResources"></a><p>A [glob expression](https://www.npmjs.com/package/glob#glob-primer), when specified, copy the file or directory with matching names directly into the app’s directory (<code>Contents/Resources</code> for OS X).</p> <p>You can use <code>${os}</code> (expanded to osx, linux or win according to current platform) and <code>${arch}</code> in the pattern.</p> <p>If directory matched, all contents are copied. So, you can just specify <code>foo</code> to copy <code>&lt;project_dir&gt;/foo</code> directory.</p> <p>May be specified in the platform options (i.e. in the <code>build.osx</code>).</p>
| osx | <a name="BuildMetadata-osx"></a>See [.build.osx](#OsXBuildOptions).
Expand All @@ -70,7 +70,7 @@ See all [appdmg options](https://www.npmjs.com/package/appdmg#json-specification
| --- | ---
| icon | <a name="OsXBuildOptions-icon"></a>The path to icon, which will be shown when mounted (default: `build/icon.icns`).
| background | <a name="OsXBuildOptions-background"></a>The path to background (default: `build/background.png`).
| target | <a name="OsXBuildOptions-target"></a>Target package type: list of `default`, `dmg`, `zip`.
| target | <a name="OsXBuildOptions-target"></a>Target package type: list of `default`, `dmg`, `zip`, `mas`.

<a name="WinBuildOptions"></a>
### `.build.win`
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,8 @@
"chalk": "^1.1.3",
"command-line-args": "^2.1.6",
"deep-assign": "^2.0.0",
"electron-packager": "^7.0.1",
"electron-osx-sign-tf": "^0.4.0-beta.0",
"electron-packager-tf": "^7.0.2-beta.0",
"electron-winstaller-fixed": "~2.3.0-beta.4",
"fs-extra-p": "^0.3.0",
"globby": "^4.0.0",
Expand Down
57 changes: 35 additions & 22 deletions src/codeSign.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@ import { randomBytes } from "crypto"
const __awaiter = require("./awaiter")

export interface CodeSigningInfo {
cscName: string
cscKeychainName?: string
name: string
keychainName?: string

installerName?: string
}

function randomString(): string {
Expand All @@ -23,45 +25,56 @@ export function generateKeychainName(): string {
return "csc-" + randomString() + ".keychain"
}

export function createKeychain(keychainName: string, cscLink: string, cscKeyPassword: string, csaLink?: string): Promise<CodeSigningInfo> {
const authorityCerts = [csaLink || "https://developer.apple.com/certificationauthority/AppleWWDRCA.cer"]
export function createKeychain(keychainName: string, cscLink: string, cscKeyPassword: string, cscILink?: string, cscIKeyPassword?: string, csaLink?: string): Promise<CodeSigningInfo> {
const certLinks = [csaLink || "https://developer.apple.com/certificationauthority/AppleWWDRCA.cer"]
if (csaLink == null) {
authorityCerts.push("https://startssl.com/certs/sca.code2.crt", "https://startssl.com/certs/sca.code3.crt")
certLinks.push("https://startssl.com/certs/sca.code2.crt", "https://startssl.com/certs/sca.code3.crt")
}
const authorityCertPaths = authorityCerts.map(() => path.join(tmpdir(), randomString() + ".cer"))

const developerCertPath = path.join(tmpdir(), randomString() + ".p12")
certLinks.push(cscLink)
if (cscILink != null) {
certLinks.push(cscILink)
}

const certPaths = certLinks.map(it => path.join(tmpdir(), randomString() + (it.endsWith(".cer") ? ".cer" : ".p12")))
const keychainPassword = randomString()
return executeFinally(BluebirdPromise.all([
BluebirdPromise.map(authorityCertPaths, (p, i) => download(authorityCerts[i], p)),
download(cscLink, developerCertPath),
BluebirdPromise.map(certPaths, (p, i) => download(certLinks[i], p)),
BluebirdPromise.mapSeries([
["create-keychain", "-p", keychainPassword, keychainName],
["unlock-keychain", "-p", keychainPassword, keychainName],
["set-keychain-settings", "-t", "3600", "-u", keychainName]
], it => exec("security", it))
])
.then(() => importCerts(keychainName, authorityCertPaths, developerCertPath, cscKeyPassword)),
.then(() => importCerts(keychainName, certPaths, [cscKeyPassword, cscIKeyPassword].filter(it => it != null))),
errorOccurred => {
const tasks = authorityCertPaths.map(it => deleteFile(it, true))
tasks.push(deleteFile(developerCertPath, true))
const tasks = certPaths.map(it => deleteFile(it, true))
if (errorOccurred) {
tasks.push(deleteKeychain(keychainName))
}
return all(tasks)
})
}

async function importCerts(keychainName: string, authorityCertPaths: Array<string>, developerCertPath: string, cscKeyPassword: string): Promise<CodeSigningInfo> {
for (let p of authorityCertPaths) {
await exec("security", ["import", p, "-k", keychainName, "-T", "/usr/bin/codesign"])
async function importCerts(keychainName: string, paths: Array<string>, keyPasswords: Array<string>): Promise<CodeSigningInfo> {
for (let f of paths.slice(0, -keyPasswords.length)) {
await exec("security", ["import", f, "-k", keychainName, "-T", "/usr/bin/codesign"])
}

const namePromises: Array<Promise<string>> = []
for (let i = paths.length - keyPasswords.length, j = 0; i < paths.length; i++, j++) {
const password = keyPasswords[j]
const certPath = paths[i]
await exec("security", ["import", certPath, "-k", keychainName, "-T", "/usr/bin/codesign", "-T", "/usr/bin/productbuild", "-P", password])

namePromises.push(extractCommonName(password, certPath))
}
await exec("security", ["import", developerCertPath, "-k", keychainName, "-T", "/usr/bin/codesign", "-P", cscKeyPassword])
let cscName = await extractCommonName(cscKeyPassword, developerCertPath)

const names = await BluebirdPromise.all(namePromises)
return {
cscName: cscName,
cscKeychainName: keychainName
name: names[0],
installerName: names.length > 1 ? names[1] : null,
keychainName: keychainName,
}
}

Expand All @@ -79,9 +92,9 @@ function extractCommonName(password: string, certPath: string): BluebirdPromise<
}

export function sign(path: string, options: CodeSigningInfo): BluebirdPromise<any> {
const args = ["--deep", "--force", "--sign", options.cscName, path]
if (options.cscKeychainName != null) {
args.push("--keychain", options.cscKeychainName)
const args = ["--deep", "--force", "--sign", options.name, path]
if (options.keychainName != null) {
args.push("--keychain", options.keychainName)
}
return exec("codesign", args)
}
Expand Down
67 changes: 54 additions & 13 deletions src/macPackager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ import { Platform, OsXBuildOptions } from "./metadata"
import * as path from "path"
import { Promise as BluebirdPromise } from "bluebird"
import { log, spawn, statOrNull } from "./util"
import { createKeychain, deleteKeychain, CodeSigningInfo, generateKeychainName, sign } from "./codeSign"
import { createKeychain, deleteKeychain, CodeSigningInfo, generateKeychainName } from "./codeSign"
import { path7za } from "7zip-bin"
import deepAssign = require("deep-assign")
import { sign, flat, BaseSignOptions } from "electron-osx-sign-tf"

//noinspection JSUnusedLocalSymbols
const __awaiter = require("./awaiter")
Expand All @@ -21,7 +22,7 @@ export default class OsXPackager extends PlatformPackager<OsXBuildOptions> {
if (this.options.cscLink != null && this.options.cscKeyPassword != null) {
const keychainName = generateKeychainName()
cleanupTasks.push(() => deleteKeychain(keychainName))
this.codeSigningInfo = createKeychain(keychainName, this.options.cscLink, this.options.cscKeyPassword, this.options.csaLink)
this.codeSigningInfo = createKeychain(keychainName, this.options.cscLink, this.options.cscKeyPassword, this.options.cscInstallerLink, this.options.cscInstallerKeyPassword, this.options.csaLink)
}
else {
this.codeSigningInfo = BluebirdPromise.resolve(null)
Expand All @@ -32,7 +33,7 @@ export default class OsXPackager extends PlatformPackager<OsXBuildOptions> {
target = Array.isArray(target) ? target : [target]
target = target.map(it => it.toLowerCase().trim())
for (let t of target) {
if (t !== "default" && t !== "dmg" && t !== "zip") {
if (t !== "default" && t !== "dmg" && t !== "zip" && t !== "mas") {
throw new Error("Unknown target: " + t)
}
}
Expand All @@ -44,23 +45,63 @@ export default class OsXPackager extends PlatformPackager<OsXBuildOptions> {
return Platform.OSX
}

async pack(outDir: string, appOutDir: string, arch: string): Promise<any> {
await super.pack(outDir, appOutDir, arch)
await this.signMac(path.join(appOutDir, this.appName + ".app"), await this.codeSigningInfo)
protected computeAppOutDir(outDir: string, arch: string): string {
return this.target.includes("mas") ? path.join(outDir, `${this.appName}-mas-${arch}`) : super.computeAppOutDir(outDir, arch)
}

private signMac(distPath: string, codeSigningInfo: CodeSigningInfo): Promise<any> {
async doPack(outDir: string, appOutDir: string, arch: string): Promise<any> {
await super.doPack(outDir, appOutDir, arch)
await this.sign(appOutDir, await this.codeSigningInfo)
}

protected beforePack(options: any): void {
if (this.target.includes("mas")) {
options.platform = "mas"
}
// disable warning
options["osx-sign"] = false
}

private async sign(appOutDir: string, codeSigningInfo: CodeSigningInfo): Promise<any> {
if (codeSigningInfo == null) {
codeSigningInfo = {cscName: this.options.sign || process.env.CSC_NAME}
codeSigningInfo = {
name: this.options.sign || process.env.CSC_NAME,
installerName: this.options.sign || process.env.CSC_INSTALLER_NAME,
}
}

if (codeSigningInfo.cscName == null) {
if (codeSigningInfo.name == null) {
log("App is not signed: CSC_LINK or CSC_NAME are not specified")
return BluebirdPromise.resolve()
return
}
else {
log("Signing app")
return sign(distPath, codeSigningInfo)

log("Signing app")

const isMas = this.target.includes("mas")
const baseSignOptions: BaseSignOptions = {
app: path.join(appOutDir, this.appName + ".app"),
platform: isMas ? "mas" : "darwin"
}
if (codeSigningInfo.keychainName != null) {
baseSignOptions.keychain = codeSigningInfo.keychainName
}

await BluebirdPromise.promisify(sign)(Object.assign({
identity: codeSigningInfo.name,
}, (<any>this.devMetadata.build)["osx-sign"], baseSignOptions))

if (isMas) {
const installerIdentity = codeSigningInfo.installerName
if (installerIdentity == null) {
throw new Error("Signing is required for mas builds but CSC_INSTALLER_LINK or CSC_INSTALLER_NAME are not specified")
}

const pkg = path.join(appOutDir, `${this.appName}-${this.metadata.version}.pkg`)
await BluebirdPromise.promisify(flat)(Object.assign({
pkg: pkg,
identity: installerIdentity,
}, baseSignOptions))
this.dispatchArtifactCreated(pkg, `${this.metadata.name}-${this.metadata.version}.pkg`)
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/metadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ export interface OsXBuildOptions extends PlatformSpecificBuildOptions {
readonly background?: string

/*
Target package type: list of `default`, `dmg`, `zip`.
Target package type: list of `default`, `dmg`, `zip`, `mas`.
*/
readonly target?: Array<string>
}
Expand Down
Loading

0 comments on commit 260ca0b

Please sign in to comment.