diff --git a/docs/Options.md b/docs/Options.md
index 5c9e6b2acca..846d18dce98 100644
--- a/docs/Options.md
+++ b/docs/Options.md
@@ -164,14 +164,15 @@ Linux specific build options.
### `.build.fileAssociations`
-NSIS only, [in progress](https://github.com/electron-userland/electron-builder/issues/409).
+NSIS and MacOS only.
| Name | Description
| --- | ---
| **ext** | The extension (minus the leading period). e.g. `png`.
| **name** | The name. e.g. `PNG`.
| description | *windows-only.* The description.
-| icon | *windows-only.* The path to icon (`.ico`), relative to `build` (build resources directory). Defaults to `${ext}.ico`.
+| icon | The path to icon (`.icns` for MacOS and `.ico` for Windows), relative to `build` (build resources directory). Defaults to `${firstExt}.icns`/`${firstExt}.ico` (if several extensions specified, first is used) or to application icon.
+| role | *macOS-only* The app’s role with respect to the type. The value can be `Editor`, `Viewer`, `Shell`, or `None`. Defaults to `Editor`.
### `.build.protocols`
diff --git a/package.json b/package.json
index 27d4671fc58..a4abed18814 100644
--- a/package.json
+++ b/package.json
@@ -62,7 +62,7 @@
"7zip-bin": "^1.0.6",
"ansi-escapes": "^1.4.0",
"archiver": "^1.0.1",
- "archiver-utils": "^1.2.0",
+ "archiver-utils": "^1.3.0",
"asar-electron-builder": "^0.13.2",
"bluebird": "^3.4.3",
"chalk": "^1.1.3",
@@ -73,7 +73,7 @@
"electron-download": "^2.1.2",
"electron-osx-sign": "^0.4.0-beta4",
"extract-zip": "^1.5.0",
- "fs-extra-p": "^1.1.7",
+ "fs-extra-p": "^1.1.8",
"hosted-git-info": "^2.1.5",
"image-size": "^0.5.0",
"isbinaryfile": "^3.0.1",
diff --git a/src/metadata.ts b/src/metadata.ts
index 2662ee1a1a5..11b521aeb25 100755
--- a/src/metadata.ts
+++ b/src/metadata.ts
@@ -355,8 +355,6 @@ export interface WinBuildOptions extends PlatformSpecificBuildOptions {
*/
readonly icon?: string | null
- readonly fileAssociations?: Array | FileAssociation
-
/*
The trademarks and registered trademarks.
*/
@@ -493,13 +491,13 @@ export interface LinuxBuildOptions extends PlatformSpecificBuildOptions {
/*
### `.build.fileAssociations`
- NSIS only, [in progress](https://github.com/electron-userland/electron-builder/issues/409).
+ NSIS and MacOS only.
*/
export interface FileAssociation {
/*
The extension (minus the leading period). e.g. `png`.
*/
- readonly ext: string
+ readonly ext: string | Array
/*
The name. e.g. `PNG`.
@@ -512,9 +510,14 @@ export interface FileAssociation {
readonly description?: string
/*
- *windows-only.* The path to icon (`.ico`), relative to `build` (build resources directory). Defaults to `${ext}.ico`.
+ The path to icon (`.icns` for MacOS and `.ico` for Windows), relative to `build` (build resources directory). Defaults to `${firstExt}.icns`/`${firstExt}.ico` (if several extensions specified, first is used) or to application icon.
*/
readonly icon?: string
+
+ /*
+ *macOS-only* The app’s role with respect to the type. The value can be `Editor`, `Viewer`, `Shell`, or `None`. Defaults to `Editor`.
+ */
+ readonly role?: string
}
/*
@@ -564,6 +567,8 @@ export interface PlatformSpecificBuildOptions {
readonly target?: Array | null
readonly icon?: string | null
+
+ readonly fileAssociations?: Array | FileAssociation
}
export class Platform {
diff --git a/src/packager/mac.ts b/src/packager/mac.ts
index 816f37350ad..de7ca61c319 100644
--- a/src/packager/mac.ts
+++ b/src/packager/mac.ts
@@ -4,6 +4,8 @@ import * as path from "path"
import { parse as parsePlist, build as buildPlist } from "plist"
import { Promise as BluebirdPromise } from "bluebird"
import { use, asArray } from "../util/util"
+import { normalizeExt } from "../platformPackager"
+import { FileAssociation } from "../metadata"
//noinspection JSUnusedLocalSymbols
const __awaiter = require("../util/awaiter")
@@ -79,7 +81,8 @@ export async function createApp(opts: ElectronPackagerOptions, appOutDir: string
})
use(appInfo.buildVersion, it => appPlist.CFBundleVersion = it)
- const protocols = asArray(opts.platformPackager.devMetadata.build.protocols).concat(asArray(opts.platformPackager.platformSpecificBuildOptions.protocols))
+ const packager = opts.platformPackager
+ const protocols = asArray(packager.devMetadata.build.protocols).concat(asArray(packager.platformSpecificBuildOptions.protocols))
if (protocols.length > 0) {
appPlist.CFBundleURLTypes = protocols.map(protocol => {
return {
@@ -89,6 +92,21 @@ export async function createApp(opts: ElectronPackagerOptions, appOutDir: string
})
}
+ const fileAssociations = packager.getFileAssociations()
+ if (fileAssociations.length > 0) {
+ appPlist.CFBundleDocumentTypes = await BluebirdPromise.map(fileAssociations, async fileAssociation => {
+ const extensions = asArray(fileAssociation.ext).map(normalizeExt)
+ const customIcon = await packager.getResource(fileAssociation.icon, `${extensions[0]}.icns`)
+ // todo rename electron.icns
+ return {
+ CFBundleTypeExtensions: extensions,
+ CFBundleTypeName: fileAssociation.name,
+ CFBundleTypeRole: fileAssociation.role || "Editor",
+ CFBundleTypeIconFile: customIcon || "electron.icns"
+ }
+ })
+ }
+
use(appInfo.category, it => appPlist.LSApplicationCategoryType = it)
use(appInfo.copyright, it => appPlist.NSHumanReadableCopyright = it)
@@ -100,7 +118,7 @@ export async function createApp(opts: ElectronPackagerOptions, appOutDir: string
doRename(path.join(contentsPath, "MacOS"), "Electron", appPlist.CFBundleExecutable)
]
- const icon = await opts.platformPackager.getIconPath()
+ const icon = await packager.getIconPath()
if (icon != null) {
promises.push(copy(icon, path.join(contentsPath, "Resources", appPlist.CFBundleIconFile)))
}
diff --git a/src/platformPackager.ts b/src/platformPackager.ts
index 1aa85ebd32e..42e39d361b3 100644
--- a/src/platformPackager.ts
+++ b/src/platformPackager.ts
@@ -1,9 +1,9 @@
-import { AppMetadata, DevMetadata, Platform, PlatformSpecificBuildOptions, Arch } from "./metadata"
+import { AppMetadata, DevMetadata, Platform, PlatformSpecificBuildOptions, Arch, FileAssociation } from "./metadata"
import EventEmitter = NodeJS.EventEmitter
import { Promise as BluebirdPromise } from "bluebird"
import * as path from "path"
import { readdir, remove } from "fs-extra-p"
-import { statOrNull, use, unlinkIfExists, isEmptyOrSpaces } from "./util/util"
+import { statOrNull, use, unlinkIfExists, isEmptyOrSpaces, asArray } from "./util/util"
import { Packager } from "./packager"
import { AsarOptions } from "asar-electron-builder"
import { archiveApp } from "./targets/archive"
@@ -432,6 +432,25 @@ export abstract class PlatformPackager
getTempFile(suffix: string): Promise {
return this.info.tempDirManager.getTempFile(suffix)
}
+
+ getFileAssociations(): Array {
+ return asArray(this.devMetadata.build.fileAssociations).concat(asArray(this.platformSpecificBuildOptions.fileAssociations))
+ }
+
+ async getResource(custom: string | n, name: string): Promise {
+ let result = custom
+ if (result === undefined) {
+ const resourceList = await this.resourceList
+ if (resourceList.includes(name)) {
+ return path.join(this.buildResourcesDir, name)
+ }
+ }
+ else {
+ return path.resolve(this.projectDir, result)
+ }
+
+ return null
+ }
}
export function getArchSuffix(arch: Arch): string {
@@ -457,4 +476,9 @@ export function smarten(s: string): string {
// closing doubles
s = s.replace(/"/g, "\u201d")
return s
+}
+
+// remove leading dot
+export function normalizeExt(ext: string) {
+ return ext.startsWith(".") ? ext.substring(1) : ext
}
\ No newline at end of file
diff --git a/src/targets/nsis.ts b/src/targets/nsis.ts
index 0fedf9da37a..51fa80964d0 100644
--- a/src/targets/nsis.ts
+++ b/src/targets/nsis.ts
@@ -1,11 +1,11 @@
import { WinPackager } from "../winPackager"
-import { Arch, NsisOptions, FileAssociation } from "../metadata"
+import { Arch, NsisOptions } from "../metadata"
import { exec, debug, doSpawn, handleProcess, use, asArray } from "../util/util"
import * as path from "path"
import { Promise as BluebirdPromise } from "bluebird"
import { getBinFromBintray } from "../util/binDownload"
import { v5 as uuid5 } from "uuid-1345"
-import { Target } from "../platformPackager"
+import { Target, normalizeExt } from "../platformPackager"
import { archiveApp } from "./archive"
import { subTask, task, log } from "../util/log"
import { unlink, readFile } from "fs-extra-p"
@@ -30,8 +30,6 @@ export default class NsisTarget extends Target {
private readonly nsisTemplatesDir = path.join(__dirname, "..", "..", "templates", "nsis")
- private readonly fileAssociations: Array
-
constructor(private packager: WinPackager, private outDir: string) {
super("nsis")
@@ -40,7 +38,6 @@ export default class NsisTarget extends Target {
// CFBundleTypeName
// https://developer.apple.com/library/ios/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html#//apple_ref/doc/uid/20001431-101685
// CFBundleTypeExtensions
- this.fileAssociations = asArray(packager.devMetadata.build.fileAssociations).concat(asArray(packager.platformSpecificBuildOptions.fileAssociations))
}
async build(arch: Arch, appOutDir: string) {
@@ -87,14 +84,14 @@ export default class NsisTarget extends Target {
const oneClick = this.options.oneClick !== false
- const installerHeader = oneClick ? null : await this.getResource(this.options.installerHeader, "installerHeader.bmp")
+ const installerHeader = oneClick ? null : await this.packager.getResource(this.options.installerHeader, "installerHeader.bmp")
if (installerHeader != null) {
defines.MUI_HEADERIMAGE = null
defines.MUI_HEADERIMAGE_RIGHT = null
defines.MUI_HEADERIMAGE_BITMAP = installerHeader
}
- const installerHeaderIcon = oneClick ? await this.getResource(this.options.installerHeaderIcon, "installerHeaderIcon.ico") : null
+ const installerHeaderIcon = oneClick ? await this.packager.getResource(this.options.installerHeaderIcon, "installerHeaderIcon.ico") : null
if (installerHeaderIcon != null) {
defines.HEADER_ICO = installerHeaderIcon
}
@@ -159,7 +156,7 @@ export default class NsisTarget extends Target {
return
}
- const customScriptPath = await this.getResource(this.options.script, "installer.nsi")
+ const customScriptPath = await this.packager.getResource(this.options.script, "installer.nsi")
const script = await readFile(customScriptPath || path.join(this.nsisTemplatesDir, "installer.nsi"), "utf8")
if (customScriptPath == null) {
@@ -185,21 +182,6 @@ export default class NsisTarget extends Target {
this.packager.dispatchArtifactCreated(installerPath, `${appInfo.name}-Setup-${version}.exe`)
}
- protected async getResource(custom: string | n, name: string): Promise {
- let result = custom
- if (result === undefined) {
- const resourceList = await this.packager.resourceList
- if (resourceList.includes(name)) {
- return path.join(this.packager.buildResourcesDir, name)
- }
- }
- else {
- return path.resolve(this.packager.projectDir, result)
- }
-
- return null
- }
-
private async executeMakensis(defines: any, commands: any, isInstaller: boolean, originalScript: string) {
const args: Array = ["-WX"]
for (let name of Object.keys(defines)) {
@@ -230,35 +212,42 @@ export default class NsisTarget extends Target {
const nsisPath = await nsisPathPromise
let script = originalScript
- const customInclude = await this.getResource(this.options.include, "installer.nsh")
+ const packager = this.packager
+ const customInclude = await packager.getResource(this.options.include, "installer.nsh")
if (customInclude != null) {
- script = `!include "${customInclude}"\n!addincludedir "${this.packager.buildResourcesDir}"\n${script}`
+ script = `!include "${customInclude}"\n!addincludedir "${packager.buildResourcesDir}"\n${script}`
}
- if (this.fileAssociations.length !== 0) {
+ const fileAssociations = packager.getFileAssociations()
+ if (fileAssociations.length !== 0) {
script = "!include FileAssociation.nsh\n" + script
if (isInstaller) {
let registerFileAssociationsScript = ""
- for (let item of this.fileAssociations) {
- const customIcon = await this.getResource(item.icon, `${normalizeExt(item.ext)}.ico`)
- let installedIconPath = "${APP_EXECUTABLE_FILENAME},0"
- if (customIcon != null) {
- installedIconPath = `resources\\${path.basename(customIcon)}`
- //noinspection SpellCheckingInspection
- registerFileAssociationsScript += ` File "/oname=${installedIconPath}" "${customIcon}"\n`
+ for (let item of fileAssociations) {
+ const extensions = asArray(item.ext).map(normalizeExt)
+ for (let ext of extensions) {
+ const customIcon = await packager.getResource(item.icon, `${extensions[0]}.ico`)
+ let installedIconPath = "${APP_EXECUTABLE_FILENAME},0"
+ if (customIcon != null) {
+ installedIconPath = `resources\\${path.basename(customIcon)}`
+ //noinspection SpellCheckingInspection
+ registerFileAssociationsScript += ` File "/oname=${installedIconPath}" "${customIcon}"\n`
+ }
+
+ const icon = `"$INSTDIR\\${installedIconPath}"`
+ const commandText = `"Open with ${packager.appInfo.productName}"`
+ const command = '"$INSTDIR\\${APP_EXECUTABLE_FILENAME} $\\"%1$\\""'
+ registerFileAssociationsScript += ` !insertmacro APP_ASSOCIATE "${ext}" "${item.name}" "${item.description || ""}" ${icon} ${commandText} ${command}\n`
}
-
- const icon = `"$INSTDIR\\${installedIconPath}"`
- const commandText = `"Open with ${this.packager.appInfo.productName}"`
- const command = '"$INSTDIR\\${APP_EXECUTABLE_FILENAME} $\\"%1$\\""'
- registerFileAssociationsScript += ` !insertmacro APP_ASSOCIATE "${normalizeExt(item.ext)}" "${item.name}" "${item.description || ""}" ${icon} ${commandText} ${command}\n`
}
script = `!macro registerFileAssociations\n${registerFileAssociationsScript}!macroend\n${script}`
}
else {
let unregisterFileAssociationsScript = ""
- for (let item of this.fileAssociations) {
- unregisterFileAssociationsScript += ` !insertmacro APP_UNASSOCIATE "${normalizeExt(item.ext)}" "${item.name}"\n`
+ for (let item of fileAssociations) {
+ for (let ext of asArray(item.ext)) {
+ unregisterFileAssociationsScript += ` !insertmacro APP_UNASSOCIATE "${normalizeExt(ext)}" "${item.name}"\n`
+ }
}
script = `!macro unregisterFileAssociations\n${unregisterFileAssociationsScript}!macroend\n${script}`
}
@@ -280,9 +269,4 @@ export default class NsisTarget extends Target {
childProcess.stdin.end(script)
})
}
-}
-
-// remove leading dot
-function normalizeExt(ext: string) {
- return ext.startsWith(".") ? ext.substring(1) : ext
}
\ No newline at end of file