Skip to content

Commit 265ad20

Browse files
committed
feat(mac): macOS pkg installer
Closes #52
1 parent adacf1e commit 265ad20

File tree

17 files changed

+132
-87
lines changed

17 files changed

+132
-87
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ A complete solution to package and build a ready for distribution Electron app f
99
* [Build version management](https://github.com/electron-userland/electron-builder/wiki/Options#build-version-management).
1010
* Numerous target formats:
1111
* All platforms: `7z`, `zip`, `tar.xz`, `tar.lz`, `tar.gz`, `tar.bz2`, `dir` (unpacked directory).
12-
* [MacOS](https://github.com/electron-userland/electron-builder/wiki/Options#MacOptions-target): `dmg`, `mas`.
12+
* [MacOS](https://github.com/electron-userland/electron-builder/wiki/Options#MacOptions-target): `dmg`, `pkg`, `mas`.
1313
* [Linux](https://github.com/electron-userland/electron-builder/wiki/Options#LinuxBuildOptions-target): `AppImage`, `deb`, `rpm`, `freebsd`, `pacman`, `p5p`, `apk`.
1414
* [Windows](https://github.com/electron-userland/electron-builder/wiki/Options#WinBuildOptions-target): NSIS, Squirrel.Windows.
1515
* [Publishing artifacts](https://github.com/electron-userland/electron-builder/wiki/Publishing-Artifacts) to GitHub Releases and Bintray.

docs/Code Signing.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
macOS and Windows code signing is supported. Windows is dual code-signed (SHA1 & SHA256 hashing algorithms).
2+
23
On a macOS development machine valid and appropriate identity from your keychain will be automatically used.
34

45
| Env Name | Description
56
| -------------- | -----------
67
| `CSC_LINK` | The HTTPS link (or base64-encoded data, or `file://` link) to certificate (`*.p12` or `*.pfx` file).
78
| `CSC_KEY_PASSWORD` | The password to decrypt the certificate given in `CSC_LINK`.
89
| `CSC_NAME` | *macOS-only* Name of certificate (to retrieve from login.keychain). Useful on a development machine (not on CI) if you have several identities (otherwise don't specify it).
10+
| `CSC_IDENTITY_AUTO_DISCOVERY`| `true` or `false`. Defaults to `true` — on a macOS development machine valid and appropriate identity from your keychain will be automatically used.
911

1012
If you are building Windows on macOS and need to set a different certificate and password (than the ones set in `CSC_*` env vars) you can use `WIN_CSC_LINK` and `WIN_CSC_KEY_PASSWORD`.
1113

@@ -34,6 +36,7 @@ Please note — Gatekeeper only recognises [Apple digital certificates](http://s
3436
3. Select all required certificates (hint: use cmd-click to select several):
3537
* `Developer ID Application:` to sign app for macOS.
3638
* `3rd Party Mac Developer Application:` and `3rd Party Mac Developer Installer:` to sign app for MAS (Mac App Store).
39+
* `Developer ID Application:` and `Developer ID Installer` to sign app and installer for distribution outside of the Mac App Store.
3740

3841
Please note – you can select as many certificates, as need. No restrictions on electron-builder side.
3942
All selected certificates will be imported into temporary keychain on CI server.

docs/Options.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@ MacOS specific build options.
158158
| Name | Description
159159
| --- | ---
160160
| category | <a name="MacOptions-category"></a><p>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>&quot;category&quot;: &quot;public.app-category.developer-tools&quot;</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>
161-
| target | <a name="MacOptions-target"></a>Target package type: list of `default`, `dmg`, `mas`, `7z`, `zip`, `tar.xz`, `tar.lz`, `tar.gz`, `tar.bz2`, `dir`. Defaults to `default` (dmg and zip for Squirrel.Mac).
161+
| target | <a name="MacOptions-target"></a>The target package type: list of `default`, `dmg`, `mas`, `pkg`, `7z`, `zip`, `tar.xz`, `tar.lz`, `tar.gz`, `tar.bz2`, `dir`. Defaults to `default` (dmg and zip for Squirrel.Mac).
162162
| identity | <a name="MacOptions-identity"></a><p>The name of certificate to use when signing. Consider using environment variables [CSC_LINK or CSC_NAME](https://github.com/electron-userland/electron-builder/wiki/Code-Signing). MAS installer identity is specified in the [.build.mas](#MasBuildOptions-identity).</p>
163163
| icon | <a name="MacOptions-icon"></a>The path to application icon. Defaults to `build/icon.icns` (consider using this convention instead of complicating your configuration).
164164
| entitlements | <a name="MacOptions-entitlements"></a><p>The path to entitlements file for signing the app. <code>build/entitlements.mac.plist</code> will be used if exists (it is a recommended way to set). MAS entitlements is specified in the [.build.mas](#MasBuildOptions-entitlements).</p>

src/codeSign.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@ import BluebirdPromise from "bluebird-lst-c"
77
import { randomBytes } from "crypto"
88
import { TmpDir } from "./util/tmp"
99

10-
const appleCertificatePrefixes = ["Developer ID Application:", "3rd Party Mac Developer Application:", "Developer ID Installer:", "3rd Party Mac Developer Installer:"]
10+
const appleCertificatePrefixes = ["Developer ID Application:", "Developer ID Installer:", "3rd Party Mac Developer Application:", "3rd Party Mac Developer Installer:"]
1111

12-
export type CertType = "Developer ID Application" | "3rd Party Mac Developer Application" | "Developer ID Installer" | "3rd Party Mac Developer Installer" | "Mac Developer"
12+
export type CertType = "Developer ID Application" | "Developer ID Installer" | "3rd Party Mac Developer Application" | "3rd Party Mac Developer Installer" | "Mac Developer"
1313

1414
export interface CodeSigningInfo {
1515
keychainName?: string | null

src/macPackager.ts

Lines changed: 58 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,20 @@
1-
import { PlatformPackager, BuildInfo, Target } from "./platformPackager"
1+
import { PlatformPackager, BuildInfo, Target, TargetEx } from "./platformPackager"
22
import { Platform, Arch } from "./metadata"
33
import { MasBuildOptions, MacOptions } from "./options/macOptions"
44
import * as path from "path"
55
import BluebirdPromise from "bluebird-lst-c"
66
import { log, warn, task } from "./util/log"
77
import { createKeychain, CodeSigningInfo, findIdentity } from "./codeSign"
88
import { deepAssign } from "./util/deepAssign"
9-
import { signAsync, BaseSignOptions, SignOptions } from "electron-osx-sign-tf"
9+
import { signAsync, SignOptions } from "electron-osx-sign-tf"
1010
import { DmgTarget } from "./targets/dmg"
11-
import { createCommonTarget, DEFAULT_TARGET } from "./targets/targetFactory"
11+
import { createCommonTarget, DEFAULT_TARGET, DIR_TARGET } from "./targets/targetFactory"
1212
import { AppInfo } from "./appInfo"
13-
import { flatApplication } from "./targets/pkg"
13+
import { PkgTarget, prepareProductBuildArgs } from "./targets/pkg"
14+
import { exec } from "./util/util"
1415

1516
export default class MacPackager extends PlatformPackager<MacOptions> {
16-
codeSigningInfo: Promise<CodeSigningInfo>
17+
readonly codeSigningInfo: Promise<CodeSigningInfo>
1718

1819
constructor(info: BuildInfo) {
1920
super(info)
@@ -44,19 +45,26 @@ export default class MacPackager extends PlatformPackager<MacOptions> {
4445

4546
createTargets(targets: Array<string>, mapper: (name: string, factory: () => Target) => void, cleanupTasks: Array<() => Promise<any>>): void {
4647
for (let name of targets) {
47-
if (name === "dir") {
48-
continue
49-
}
50-
51-
if (name === DEFAULT_TARGET) {
52-
mapper("dmg", () => new DmgTarget(this))
53-
mapper("zip", () => new Target("zip"))
54-
}
55-
else if (name === "dmg") {
56-
mapper("dmg", () => new DmgTarget(this))
57-
}
58-
else {
59-
mapper(name, () => name === "mas" ? new Target("mas") : createCommonTarget(name))
48+
switch (name) {
49+
case DIR_TARGET:
50+
break
51+
52+
case DEFAULT_TARGET:
53+
mapper("dmg", () => new DmgTarget(this))
54+
mapper("zip", () => new Target("zip"))
55+
break
56+
57+
case "dmg":
58+
mapper("dmg", () => new DmgTarget(this))
59+
break
60+
61+
case "pkg":
62+
mapper("pkg", () => new PkgTarget(this))
63+
break
64+
65+
default:
66+
mapper(name, () => name === "mas" ? new Target(name) : createCommonTarget(name))
67+
break
6068
}
6169
}
6270
}
@@ -74,16 +82,12 @@ export default class MacPackager extends PlatformPackager<MacOptions> {
7482
const appOutDir = this.computeAppOutDir(outDir, arch)
7583
nonMasPromise = this.doPack(outDir, appOutDir, this.platform.nodeName, arch, this.platformSpecificBuildOptions)
7684
.then(() => this.sign(appOutDir, null))
77-
.then(() => {
78-
this.packageInDistributableFormat(appOutDir, targets, postAsyncTasks)
79-
})
85+
.then(() => this.packageInDistributableFormat(appOutDir, targets, postAsyncTasks))
8086
}
8187

8288
if (hasMas) {
83-
// osx-sign - disable warning
8489
const appOutDir = path.join(outDir, "mas")
8590
const masBuildOptions = deepAssign({}, this.platformSpecificBuildOptions, (<any>this.devMetadata.build).mas)
86-
//noinspection JSUnusedGlobalSymbols
8791
await this.doPack(outDir, appOutDir, "mas", arch, masBuildOptions)
8892
await this.sign(appOutDir, masBuildOptions)
8993
}
@@ -95,11 +99,11 @@ export default class MacPackager extends PlatformPackager<MacOptions> {
9599

96100
private async sign(appOutDir: string, masOptions: MasBuildOptions | null): Promise<void> {
97101
if (process.platform !== "darwin") {
98-
warn("macOS application code signing is not supported on this platform, skipping.")
102+
warn("macOS application code signing is supported only on macOS, skipping.")
99103
return
100104
}
101105

102-
let keychainName = (await this.codeSigningInfo).keychainName
106+
const keychainName = (await this.codeSigningInfo).keychainName
103107
const isMas = masOptions != null
104108
const masQualifier = isMas ? (masOptions!!.identity || this.platformSpecificBuildOptions.identity) : null
105109

@@ -124,24 +128,14 @@ export default class MacPackager extends PlatformPackager<MacOptions> {
124128
}
125129
}
126130

127-
let installerName: string | null = null
128-
if (masOptions != null) {
129-
installerName = await findIdentity("3rd Party Mac Developer Installer", masQualifier, keychainName)
130-
if (installerName == null) {
131-
throw new Error('Cannot find valid "3rd Party Mac Developer Installer" identity to sign MAS installer, see https://github.com/electron-userland/electron-builder/wiki/Code-Signing')
132-
}
133-
}
134-
135-
const baseSignOptions: BaseSignOptions = {
136-
app: path.join(appOutDir, `${this.appInfo.productFilename}.app`),
137-
keychain: keychainName || undefined,
138-
}
139-
140-
const signOptions = Object.assign({
131+
const appPath = path.join(appOutDir, `${this.appInfo.productFilename}.app`)
132+
const signOptions: any = {
141133
identity: name,
142134
platform: isMas ? "mas" : "darwin",
143135
version: this.info.electronVersion,
144-
}, (<any>this.devMetadata.build)["osx-sign"], baseSignOptions)
136+
app: appPath,
137+
keychain: keychainName || undefined,
138+
}
145139

146140
const resourceList = await this.resourceList
147141
if (resourceList.includes(`entitlements.osx.plist`)) {
@@ -176,29 +170,47 @@ export default class MacPackager extends PlatformPackager<MacOptions> {
176170

177171
if (masOptions != null) {
178172
const pkg = path.join(appOutDir, `${this.appInfo.productFilename}-${this.appInfo.version}.pkg`)
179-
await this.doFlat(baseSignOptions, pkg, installerName!!)
173+
await this.doFlat(appPath, pkg, await this.findInstallerIdentity(true, keychainName), keychainName)
180174
this.dispatchArtifactCreated(pkg, `${this.appInfo.name}-${this.appInfo.version}.pkg`)
181175
}
182176
}
183177

178+
async findInstallerIdentity(isMas: boolean, keychainName: string | n): Promise<string> {
179+
const targetSpecificOptions: MacOptions = (<any>this.devMetadata.build)[isMas ? "mas" : "pkg"] || this.platformSpecificBuildOptions
180+
const name = isMas ? "3rd Party Mac Developer Installer" : "Developer ID Installer"
181+
let installerName = await findIdentity(name, targetSpecificOptions.identity, keychainName)
182+
if (installerName != null) {
183+
return installerName
184+
}
185+
186+
if (isMas) {
187+
throw new Error(`Cannot find valid "${name}" identity to sign MAS installer, see https://github.com/electron-userland/electron-builder/wiki/Code-Signing`)
188+
}
189+
else {
190+
throw new Error(`Cannot find valid "${name}" to sign standalone installer, see https://github.com/electron-userland/electron-builder/wiki/Code-Signing`)
191+
}
192+
}
193+
184194
//noinspection JSMethodCanBeStatic
185195
protected async doSign(opts: SignOptions): Promise<any> {
186196
return signAsync(opts)
187197
}
188198

189199
//noinspection JSMethodCanBeStatic
190-
protected async doFlat(opts: BaseSignOptions, outFile: string, identity: string): Promise<any> {
191-
return flatApplication(opts, outFile, identity)
200+
protected async doFlat(appPath: string, outFile: string, identity: string, keychain: string | n): Promise<any> {
201+
const args = prepareProductBuildArgs(appPath, identity, keychain)
202+
args.push(outFile)
203+
return exec("productbuild", args)
192204
}
193205

194206
protected packageInDistributableFormat(appOutDir: string, targets: Array<Target>, promises: Array<Promise<any>>): void {
195207
for (let t of targets) {
196208
const target = t.name
197-
if (t instanceof DmgTarget) {
198-
promises.push(t.build(appOutDir))
209+
if (t instanceof TargetEx) {
210+
promises.push(t.build(appOutDir, Arch.x64))
199211
}
200212
else if (target !== "mas") {
201-
log(`Creating MacOS ${target}`)
213+
log(`Building macOS ${target}`)
202214
// we use app name here - see https://github.com/electron-userland/electron-builder/pull/204
203215
const outFile = path.join(appOutDir, this.generateName2(target, "mac", false))
204216
promises.push(this.archiveApp(target, appOutDir, outFile)

src/options/macOptions.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { PlatformSpecificBuildOptions } from "../metadata"
22

3+
export type MacOsTargetName = "default" | "dmg" | "mas" | "pkg" | "7z" | "zip" | "tar.xz" | "tar.lz" | "tar.gz" | "tar.bz2" | "dir"
4+
35
/*
46
### `.build.mac`
57
@@ -16,9 +18,9 @@ export interface MacOptions extends PlatformSpecificBuildOptions {
1618
readonly category?: string | null
1719

1820
/*
19-
Target package type: list of `default`, `dmg`, `mas`, `7z`, `zip`, `tar.xz`, `tar.lz`, `tar.gz`, `tar.bz2`, `dir`. Defaults to `default` (dmg and zip for Squirrel.Mac).
21+
The target package type: list of `default`, `dmg`, `mas`, `pkg`, `7z`, `zip`, `tar.xz`, `tar.lz`, `tar.gz`, `tar.bz2`, `dir`. Defaults to `default` (dmg and zip for Squirrel.Mac).
2022
*/
21-
readonly target?: Array<string> | null
23+
readonly target?: Array<MacOsTargetName> | null
2224

2325
/*
2426
The name of certificate to use when signing. Consider using environment variables [CSC_LINK or CSC_NAME](https://github.com/electron-userland/electron-builder/wiki/Code-Signing).

src/packager.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,10 @@ export class Packager implements BuildInfo {
200200
throw new Error(util.format(errorMessages.buildIsMissed, devAppPackageFile))
201201
}
202202
else {
203+
if (build["osx-sign"] != null) {
204+
throw new Error("osx-sign is deprecated and not supported — please see https://github.com/electron-userland/electron-builder/wiki/Code-Signing")
205+
}
206+
203207
const author = appMetadata.author
204208
if (author == null) {
205209
throw new Error(`Please specify "author" in the application package.json ('${appPackageFile}') — it is used as company name.`)

src/packager/mac.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -84,11 +84,8 @@ export async function createApp(packager: PlatformPackager<any>, appOutDir: stri
8484
helperNPPlist.CFBundleName = `${appInfo.productName} Helper NP`
8585
helperNPPlist.CFBundleExecutable = `${appFilename} Helper NP`
8686

87-
use(appInfo.version, it => {
88-
appPlist.CFBundleShortVersionString = it
89-
appPlist.CFBundleVersion = it
90-
})
91-
use(appInfo.buildVersion, it => appPlist.CFBundleVersion = it)
87+
appPlist.CFBundleShortVersionString = appInfo.version
88+
appPlist.CFBundleVersion = appInfo.buildVersion
9289

9390
const protocols = asArray(buildMetadata.protocols).concat(asArray(packager.platformSpecificBuildOptions.protocols))
9491
if (protocols.length > 0) {

src/platformPackager.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ export interface BuildInfo {
7575
}
7676

7777
export class Target {
78-
constructor(public name: string) {
78+
constructor(public readonly name: string) {
7979
}
8080

8181
finishBuild(): Promise<any> {
@@ -103,7 +103,7 @@ export abstract class PlatformPackager<DC extends PlatformSpecificBuildOptions>
103103

104104
readonly appInfo: AppInfo
105105

106-
constructor(public info: BuildInfo) {
106+
constructor(public readonly info: BuildInfo) {
107107
this.devMetadata = info.devMetadata
108108
this.platformSpecificBuildOptions = this.normalizePlatformSpecificBuildOptions((<any>info.devMetadata.build)[this.platform.buildConfigurationKey])
109109
this.appInfo = this.prepareAppInfo(info.appInfo)

src/targets/dmg.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,26 @@
11
import { deepAssign } from "../util/deepAssign"
22
import * as path from "path"
33
import { log, warn } from "../util/log"
4-
import { Target, PlatformPackager } from "../platformPackager"
4+
import { PlatformPackager, TargetEx } from "../platformPackager"
55
import { MacOptions, DmgOptions, DmgContent } from "../options/macOptions"
66
import BluebirdPromise from "bluebird-lst-c"
77
import { debug, use, exec, statOrNull, isEmptyOrSpaces, spawn } from "../util/util"
88
import { copy, unlink, outputFile, remove } from "fs-extra-p"
99
import { executeFinally } from "../util/promise"
1010
import sanitizeFileName from "sanitize-filename"
11+
import { Arch } from "../metadata"
1112

12-
export class DmgTarget extends Target {
13+
export class DmgTarget extends TargetEx {
1314
private helperDir = path.join(__dirname, "..", "..", "templates", "dmg")
1415

1516
constructor(private packager: PlatformPackager<MacOptions>) {
1617
super("dmg")
1718
}
1819

19-
async build(appOutDir: string) {
20+
async build(appOutDir: string, arch: Arch) {
2021
const packager = this.packager
2122
const appInfo = packager.appInfo
22-
log("Creating DMG")
23+
log("Building DMG")
2324

2425
const specification = await this.computeDmgOptions()
2526

0 commit comments

Comments
 (0)