Skip to content

Commit cbe3ff8

Browse files
committed
feat: copy extra resources to packaged app
Closes: #230
1 parent 3c90af6 commit cbe3ff8

17 files changed

+256
-72
lines changed

.idea/dictionaries/develar.xml

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

README.md

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
Complete solution to build ready for distribution and "auto update" installers of your app for OS X, Windows and Linux.
22

3-
* [Native application dependencies](http://electron.atom.io/docs/latest/tutorial/using-native-node-modules/) compilation (only if two-package.json project layout used).
3+
* [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).
44
* [Auto Update](#auto-update) ready application packaging.
55
* [Code Signing](#code-signing) on a CI server or development machine.
66
* [Build version management](#build-version-management).
@@ -12,9 +12,30 @@ Complete solution to build ready for distribution and "auto update" installers o
1212

1313
Real project example — [onshape-desktop-shell](https://github.com/develar/onshape-desktop-shell).
1414

15+
# Two package.json structure
16+
17+
We strongly recommend to use **two** package.json files (it is not required, you can build project with any structure).
18+
19+
1. For development
20+
21+
In the root of the project.
22+
Here you declare dependencies for your development environment and build scripts.
23+
24+
2. For your application
25+
26+
In the `app` directory. *Only this directory is distributed with real application.*
27+
28+
Why the two package.json structure is ideal and how it solves a lot of issues
29+
([#39](https://github.com/loopline-systems/electron-builder/issues/39),
30+
[#182](https://github.com/loopline-systems/electron-builder/issues/182),
31+
[#230](https://github.com/loopline-systems/electron-builder/issues/230))?
32+
33+
1. Native npm modules (those written in C, not JavaScript) need to be compiled, and here we have two different compilation targets for them. Those used in application need to be compiled against electron runtime, and all `devDependencies` need to be compiled against your locally installed node.js. Thanks to having two files this is trivial.
34+
2. When you package the app for distribution there is no need to add up to size of the app with your `devDependencies`. Here those are always not included (because reside outside the `app` directory).
35+
1536
# Configuration
1637
## In short
17-
1. Ensure that required fields are specified in the application `package.json`:
38+
1. Ensure that required fields are specified in the application `package.json`:
1839

1940
Standard `name`, `description`, `version` and `author`.
2041

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
"electron-winstaller-fixed": "^2.0.5-beta.7",
5757
"fs-extra": "^0.26.5",
5858
"fs-extra-p": "^0.1.0",
59+
"globby": "^4.0.0",
5960
"gm": "^1.21.1",
6061
"hosted-git-info": "^2.1.4",
6162
"lodash.template": "^4.2.2",
@@ -88,8 +89,8 @@
8889
"ts-babel": "^0.6.1",
8990
"tsconfig-glob": "^0.4.1",
9091
"tslint": "next",
91-
"typescript": "1.9.0-dev.20160307",
92-
"validate-commit-msg": "^2.3.1"
92+
"typescript": "^1.9.0-dev.20160313",
93+
"validate-commit-msg": "^2.4.0"
9394
},
9495
"babel": {
9596
"plugins": [

src/linuxPackager.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ export class LinuxPackager extends PlatformPackager<DebOptions> {
2525
this.debOptions = Object.assign({
2626
name: this.metadata.name,
2727
comment: this.metadata.description,
28-
}, this.customDistOptions)
28+
}, this.customBuildOptions)
2929

3030
if (this.options.dist) {
3131
const tempDir = tmpDir({
@@ -45,7 +45,7 @@ export class LinuxPackager extends PlatformPackager<DebOptions> {
4545
const tempDir = await tempDirPromise
4646

4747
const promises: Array<Promise<Array<string>>> = []
48-
if (this.customDistOptions == null || this.customDistOptions.desktop == null) {
48+
if (this.customBuildOptions == null || this.customBuildOptions.desktop == null) {
4949
promises.push(this.computeDesktopIconPath(tempDir))
5050
}
5151

src/macPackager.ts

Lines changed: 8 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { PlatformPackager, BuildInfo } from "./platformPackager"
2-
import { Platform } from "./metadata"
2+
import { Platform, PlatformSpecificBuildOptions } from "./metadata"
33
import * as path from "path"
44
import { Promise as BluebirdPromise } from "bluebird"
55
import { log, spawn } from "./util"
@@ -8,7 +8,10 @@ import { createKeychain, deleteKeychain, CodeSigningInfo, generateKeychainName,
88
const __awaiter = require("./awaiter")
99
Array.isArray(__awaiter)
1010

11-
export default class MacPackager extends PlatformPackager<appdmg.Specification> {
11+
export interface OsXBuildOptions extends PlatformSpecificBuildOptions, appdmg.Specification {
12+
}
13+
14+
export default class MacPackager extends PlatformPackager<OsXBuildOptions> {
1215
codeSigningInfo: Promise<CodeSigningInfo>
1316

1417
constructor(info: BuildInfo, cleanupTasks: Array<() => Promise<any>>) {
@@ -55,7 +58,7 @@ export default class MacPackager extends PlatformPackager<appdmg.Specification>
5558
new BluebirdPromise<any>((resolve, reject) => {
5659
log("Creating DMG")
5760

58-
const specification: appdmg.Specification = {
61+
const specification: appdmg.Specification = Object.assign({
5962
title: this.appName,
6063
icon: path.join(this.buildResourcesDir, "icon.icns"),
6164
"icon-size": 80,
@@ -68,15 +71,7 @@ export default class MacPackager extends PlatformPackager<appdmg.Specification>
6871
"x": 130, "y": 220, "type": "file"
6972
}
7073
]
71-
}
72-
73-
if (this.customDistOptions != null) {
74-
Object.assign(specification, this.customDistOptions)
75-
}
76-
77-
if (specification.title == null) {
78-
specification.title = this.appName
79-
}
74+
}, this.customBuildOptions)
8075

8176
specification.contents[1].path = path.join(appOutDir, this.appName + ".app")
8277

@@ -110,6 +105,6 @@ export default class MacPackager extends PlatformPackager<appdmg.Specification>
110105
cwd: outDir,
111106
stdio: "inherit",
112107
})
113-
.thenReturn(outDir + "/" + resultPath)
108+
.thenReturn(path.join(outDir, resultPath))
114109
}
115110
}

src/metadata.ts

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ export function getProductName(metadata: AppMetadata) {
2121
}
2222

2323
export interface DevMetadata extends Metadata {
24-
readonly build: DevBuildMetadata
24+
readonly build?: DevBuildMetadata
2525

2626
readonly directories?: MetadataDirectories
2727
}
@@ -56,9 +56,15 @@ export interface MetadataDirectories {
5656
}
5757

5858
export interface DevBuildMetadata {
59-
readonly osx: appdmg.Specification
60-
readonly win: any,
61-
readonly linux: any
59+
readonly osx?: appdmg.Specification
60+
readonly win?: any,
61+
readonly linux?: any
62+
63+
readonly extraResources?: Array<string>
64+
}
65+
66+
export interface PlatformSpecificBuildOptions {
67+
readonly extraResources?: Array<string>
6268
}
6369

6470
export class Platform {
@@ -72,4 +78,14 @@ export class Platform {
7278
toString() {
7379
return this.name
7480
}
81+
82+
public static fromNodePlatform(name: string): Platform {
83+
switch (name) {
84+
case "darwin": return Platform.OSX
85+
case "win32": return Platform.WINDOWS
86+
case "linux": return Platform.LINUX
87+
}
88+
89+
throw new Error("Unknown platform: " + name)
90+
}
7591
}

src/packager.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import * as fs from "fs"
1+
import { accessSync } from "fs"
22
import * as path from "path"
33
import { DEFAULT_APP_DIR_NAME, installDependencies, log, getElectronVersion, readPackageJson } from "./util"
44
import { all, executeFinally } from "./promise"
@@ -120,7 +120,7 @@ export class Packager implements BuildInfo {
120120

121121
const absoluteAppPath = path.join(this.projectDir, customAppPath)
122122
try {
123-
fs.accessSync(absoluteAppPath)
123+
accessSync(absoluteAppPath)
124124
}
125125
catch (e) {
126126
if (required) {

src/platformPackager.ts

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import { InfoRetriever, ProjectMetadataProvider } from "./repositoryInfo"
2-
import { AppMetadata, DevMetadata, Platform, getProductName } from "./metadata"
2+
import { AppMetadata, DevMetadata, Platform, PlatformSpecificBuildOptions, getProductName } from "./metadata"
33
import EventEmitter = NodeJS.EventEmitter
44
import { Promise as BluebirdPromise } from "bluebird"
55
import * as path from "path"
66
import packager = require("electron-packager-tf")
7+
import globby = require("globby")
8+
import { copy } from "fs-extra-p"
79

810
//noinspection JSUnusedLocalSymbols
911
const __awaiter = require("./awaiter")
@@ -44,7 +46,7 @@ export interface BuildInfo extends ProjectMetadataProvider {
4446
eventEmitter: EventEmitter
4547
}
4648

47-
export abstract class PlatformPackager<DC> implements ProjectMetadataProvider {
49+
export abstract class PlatformPackager<DC extends PlatformSpecificBuildOptions> implements ProjectMetadataProvider {
4850
protected readonly options: PackagerOptions
4951

5052
protected readonly projectDir: string
@@ -53,7 +55,7 @@ export abstract class PlatformPackager<DC> implements ProjectMetadataProvider {
5355
readonly metadata: AppMetadata
5456
readonly devMetadata: DevMetadata
5557

56-
customDistOptions: DC
58+
customBuildOptions: DC
5759

5860
readonly appName: string
5961

@@ -67,10 +69,8 @@ export abstract class PlatformPackager<DC> implements ProjectMetadataProvider {
6769

6870
this.buildResourcesDir = path.resolve(this.projectDir, this.relativeBuildResourcesDirname)
6971

70-
if (this.options.dist) {
71-
const buildMetadata: any = info.devMetadata.build
72-
this.customDistOptions = buildMetadata == null ? buildMetadata : buildMetadata[this.platform.buildConfigurationKey]
73-
}
72+
const buildMetadata: any = info.devMetadata.build
73+
this.customBuildOptions = buildMetadata == null ? buildMetadata : buildMetadata[this.platform.buildConfigurationKey]
7474

7575
this.appName = getProductName(this.metadata)
7676
}
@@ -84,7 +84,7 @@ export abstract class PlatformPackager<DC> implements ProjectMetadataProvider {
8484
this.info.eventEmitter.emit("artifactCreated", file, this.platform)
8585
}
8686

87-
pack(platform: string, outDir: string, appOutDir: string, arch: string): Promise<any> {
87+
async pack(platform: string, outDir: string, appOutDir: string, arch: string): Promise<any> {
8888
const version = this.metadata.version
8989
let buildVersion = version
9090
const buildNumber = process.env.TRAVIS_BUILD_NUMBER || process.env.APPVEYOR_BUILD_NUMBER || process.env.CIRCLE_BUILD_NUM
@@ -116,7 +116,28 @@ export abstract class PlatformPackager<DC> implements ProjectMetadataProvider {
116116

117117
// this option only for windows-installer
118118
delete options.iconUrl
119-
return pack(options)
119+
await pack(options)
120+
121+
const buildMetadata: any = this.devMetadata.build
122+
let extraResources: Array<string> = buildMetadata == null ? null : buildMetadata.extraResources
123+
124+
const platformSpecificExtraResources = this.customBuildOptions == null ? null : this.customBuildOptions.extraResources
125+
if (platformSpecificExtraResources != null) {
126+
extraResources = extraResources == null ? platformSpecificExtraResources : extraResources.concat(platformSpecificExtraResources)
127+
}
128+
129+
if (extraResources != null) {
130+
const expandedPatterns = extraResources.map(it => it
131+
.replace(/\$\{arch\}/g, arch)
132+
.replace(/\$\{os\}/g, this.platform.buildConfigurationKey))
133+
await BluebirdPromise.map(await globby(expandedPatterns, {cwd: this.projectDir}), it => {
134+
let resourcesDir = appOutDir
135+
if (platform === "darwin") {
136+
resourcesDir = path.join(resourcesDir, this.appName + ".app", "Contents", "Resources")
137+
}
138+
return copy(path.join(this.projectDir, it), path.join(resourcesDir, it))
139+
})
140+
}
120141
}
121142

122143
abstract packageInDistributableFormat(outDir: string, appOutDir: string, arch: string): Promise<any>

src/winPackager.ts

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,23 @@
11
import { downloadCertificate } from "./codeSign"
22
import { Promise as BluebirdPromise } from "bluebird"
33
import { PlatformPackager, BuildInfo } from "./platformPackager"
4-
import { Platform } from "./metadata"
4+
import { Platform, PlatformSpecificBuildOptions } from "./metadata"
55
import * as path from "path"
66
import { log } from "./util"
77
import { readFile, deleteFile, stat, rename, copy, emptyDir, Stats, writeFile } from "fs-extra-p"
88

9+
//noinspection JSUnusedLocalSymbols
910
const __awaiter = require("./awaiter")
10-
Array.isArray(__awaiter)
1111

12-
export default class WinPackager extends PlatformPackager<any> {
12+
export interface WinBuildOptions extends PlatformSpecificBuildOptions {
13+
readonly certificateFile?: string
14+
readonly certificatePassword?: string
15+
16+
readonly icon?: string
17+
readonly iconUrl?: string
18+
}
19+
20+
export default class WinPackager extends PlatformPackager<WinBuildOptions> {
1321
certFilePromise: Promise<string>
1422
isNsis: boolean
1523

@@ -23,7 +31,7 @@ export default class WinPackager extends PlatformPackager<any> {
2331
// "Error: EBUSY: resource busy or locked, unlink 'C:\Users\appveyor\AppData\Local\Temp\1\icon.ico'"
2432
// on appveyor (well, yes, it is a Windows bug)
2533
// Because NSIS support will be dropped some day, correct solution is not implemented
26-
const iconPath = this.customDistOptions == null ? null : this.customDistOptions.icon
34+
const iconPath = this.customBuildOptions == null ? null : this.customBuildOptions.icon
2735
require("../lib/win").copyAssetsToTmpFolder(iconPath || path.join(this.buildResourcesDir, "icon.ico"))
2836
}
2937

@@ -66,8 +74,8 @@ export default class WinPackager extends PlatformPackager<any> {
6674
async packageInDistributableFormat(outDir: string, appOutDir: string, arch: string): Promise<any> {
6775
let iconUrl = this.metadata.build.iconUrl
6876
if (!iconUrl) {
69-
if (this.customDistOptions != null) {
70-
iconUrl = this.customDistOptions.iconUrl
77+
if (this.customBuildOptions != null) {
78+
iconUrl = this.customBuildOptions.iconUrl
7179
}
7280
if (!iconUrl) {
7381
if (this.info.repositoryInfo != null) {
@@ -103,7 +111,7 @@ export default class WinPackager extends PlatformPackager<any> {
103111
certificatePassword: this.options.cscKeyPassword,
104112
fixUpPaths: false,
105113
usePackageJson: false
106-
}, this.customDistOptions)
114+
}, this.customBuildOptions)
107115

108116
// we use metadata.name instead of appName because appName can contains unsafe chars
109117
const installerExePath = path.join(installerOutDir, this.metadata.name + "Setup-" + version + archSuffix + ".exe")
@@ -185,7 +193,7 @@ export default class WinPackager extends PlatformPackager<any> {
185193
icon: options.setupIcon,
186194
publisher: options.authors,
187195
verbosity: 2
188-
}, this.customDistOptions)
196+
}, this.customBuildOptions)
189197
}
190198
}))
191199
}

0 commit comments

Comments
 (0)