Skip to content

Commit

Permalink
feat: copy extra resources to packaged app
Browse files Browse the repository at this point in the history
Closes: #230
  • Loading branch information
develar committed Mar 13, 2016
1 parent 3c90af6 commit cbe3ff8
Show file tree
Hide file tree
Showing 17 changed files with 256 additions and 72 deletions.
2 changes: 2 additions & 0 deletions .idea/dictionaries/develar.xml

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

25 changes: 23 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
Complete solution to build ready for distribution and "auto update" installers of your app for OS X, Windows and Linux.

* [Native application dependencies](http://electron.atom.io/docs/latest/tutorial/using-native-node-modules/) compilation (only if two-package.json project layout used).
* [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.
* [Build version management](#build-version-management).
Expand All @@ -12,9 +12,30 @@ Complete solution to build ready for distribution and "auto update" installers o

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

# Two package.json structure

We strongly recommend to use **two** package.json files (it is not required, you can build project with any structure).

1. For development

In the root of the project.
Here you declare dependencies for your development environment and build scripts.

2. For your application

In the `app` directory. *Only this directory is distributed with real application.*

Why the two package.json structure is ideal and how it solves a lot of issues
([#39](https://github.com/loopline-systems/electron-builder/issues/39),
[#182](https://github.com/loopline-systems/electron-builder/issues/182),
[#230](https://github.com/loopline-systems/electron-builder/issues/230))?

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.
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).

# Configuration
## In short
1. Ensure that required fields are specified in the application `package.json`:
1. Ensure that required fields are specified in the application `package.json`:

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

Expand Down
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
"electron-winstaller-fixed": "^2.0.5-beta.7",
"fs-extra": "^0.26.5",
"fs-extra-p": "^0.1.0",
"globby": "^4.0.0",
"gm": "^1.21.1",
"hosted-git-info": "^2.1.4",
"lodash.template": "^4.2.2",
Expand Down Expand Up @@ -88,8 +89,8 @@
"ts-babel": "^0.6.1",
"tsconfig-glob": "^0.4.1",
"tslint": "next",
"typescript": "1.9.0-dev.20160307",
"validate-commit-msg": "^2.3.1"
"typescript": "^1.9.0-dev.20160313",
"validate-commit-msg": "^2.4.0"
},
"babel": {
"plugins": [
Expand Down
4 changes: 2 additions & 2 deletions src/linuxPackager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export class LinuxPackager extends PlatformPackager<DebOptions> {
this.debOptions = Object.assign({
name: this.metadata.name,
comment: this.metadata.description,
}, this.customDistOptions)
}, this.customBuildOptions)

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

const promises: Array<Promise<Array<string>>> = []
if (this.customDistOptions == null || this.customDistOptions.desktop == null) {
if (this.customBuildOptions == null || this.customBuildOptions.desktop == null) {
promises.push(this.computeDesktopIconPath(tempDir))
}

Expand Down
21 changes: 8 additions & 13 deletions src/macPackager.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { PlatformPackager, BuildInfo } from "./platformPackager"
import { Platform } from "./metadata"
import { Platform, PlatformSpecificBuildOptions } from "./metadata"
import * as path from "path"
import { Promise as BluebirdPromise } from "bluebird"
import { log, spawn } from "./util"
Expand All @@ -8,7 +8,10 @@ import { createKeychain, deleteKeychain, CodeSigningInfo, generateKeychainName,
const __awaiter = require("./awaiter")
Array.isArray(__awaiter)

export default class MacPackager extends PlatformPackager<appdmg.Specification> {
export interface OsXBuildOptions extends PlatformSpecificBuildOptions, appdmg.Specification {
}

export default class MacPackager extends PlatformPackager<OsXBuildOptions> {
codeSigningInfo: Promise<CodeSigningInfo>

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

const specification: appdmg.Specification = {
const specification: appdmg.Specification = Object.assign({
title: this.appName,
icon: path.join(this.buildResourcesDir, "icon.icns"),
"icon-size": 80,
Expand All @@ -68,15 +71,7 @@ export default class MacPackager extends PlatformPackager<appdmg.Specification>
"x": 130, "y": 220, "type": "file"
}
]
}

if (this.customDistOptions != null) {
Object.assign(specification, this.customDistOptions)
}

if (specification.title == null) {
specification.title = this.appName
}
}, this.customBuildOptions)

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

Expand Down Expand Up @@ -110,6 +105,6 @@ export default class MacPackager extends PlatformPackager<appdmg.Specification>
cwd: outDir,
stdio: "inherit",
})
.thenReturn(outDir + "/" + resultPath)
.thenReturn(path.join(outDir, resultPath))
}
}
24 changes: 20 additions & 4 deletions src/metadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export function getProductName(metadata: AppMetadata) {
}

export interface DevMetadata extends Metadata {
readonly build: DevBuildMetadata
readonly build?: DevBuildMetadata

readonly directories?: MetadataDirectories
}
Expand Down Expand Up @@ -56,9 +56,15 @@ export interface MetadataDirectories {
}

export interface DevBuildMetadata {
readonly osx: appdmg.Specification
readonly win: any,
readonly linux: any
readonly osx?: appdmg.Specification
readonly win?: any,
readonly linux?: any

readonly extraResources?: Array<string>
}

export interface PlatformSpecificBuildOptions {
readonly extraResources?: Array<string>
}

export class Platform {
Expand All @@ -72,4 +78,14 @@ export class Platform {
toString() {
return this.name
}

public static fromNodePlatform(name: string): Platform {
switch (name) {
case "darwin": return Platform.OSX
case "win32": return Platform.WINDOWS
case "linux": return Platform.LINUX
}

throw new Error("Unknown platform: " + name)
}
}
4 changes: 2 additions & 2 deletions src/packager.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import * as fs from "fs"
import { accessSync } from "fs"
import * as path from "path"
import { DEFAULT_APP_DIR_NAME, installDependencies, log, getElectronVersion, readPackageJson } from "./util"
import { all, executeFinally } from "./promise"
Expand Down Expand Up @@ -120,7 +120,7 @@ export class Packager implements BuildInfo {

const absoluteAppPath = path.join(this.projectDir, customAppPath)
try {
fs.accessSync(absoluteAppPath)
accessSync(absoluteAppPath)
}
catch (e) {
if (required) {
Expand Down
39 changes: 30 additions & 9 deletions src/platformPackager.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { InfoRetriever, ProjectMetadataProvider } from "./repositoryInfo"
import { AppMetadata, DevMetadata, Platform, getProductName } from "./metadata"
import { AppMetadata, DevMetadata, Platform, PlatformSpecificBuildOptions, getProductName } from "./metadata"
import EventEmitter = NodeJS.EventEmitter
import { Promise as BluebirdPromise } from "bluebird"
import * as path from "path"
import packager = require("electron-packager-tf")
import globby = require("globby")
import { copy } from "fs-extra-p"

//noinspection JSUnusedLocalSymbols
const __awaiter = require("./awaiter")
Expand Down Expand Up @@ -44,7 +46,7 @@ export interface BuildInfo extends ProjectMetadataProvider {
eventEmitter: EventEmitter
}

export abstract class PlatformPackager<DC> implements ProjectMetadataProvider {
export abstract class PlatformPackager<DC extends PlatformSpecificBuildOptions> implements ProjectMetadataProvider {
protected readonly options: PackagerOptions

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

customDistOptions: DC
customBuildOptions: DC

readonly appName: string

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

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

if (this.options.dist) {
const buildMetadata: any = info.devMetadata.build
this.customDistOptions = buildMetadata == null ? buildMetadata : buildMetadata[this.platform.buildConfigurationKey]
}
const buildMetadata: any = info.devMetadata.build
this.customBuildOptions = buildMetadata == null ? buildMetadata : buildMetadata[this.platform.buildConfigurationKey]

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

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

// this option only for windows-installer
delete options.iconUrl
return pack(options)
await pack(options)

const buildMetadata: any = this.devMetadata.build
let extraResources: Array<string> = buildMetadata == null ? null : buildMetadata.extraResources

const platformSpecificExtraResources = this.customBuildOptions == null ? null : this.customBuildOptions.extraResources
if (platformSpecificExtraResources != null) {
extraResources = extraResources == null ? platformSpecificExtraResources : extraResources.concat(platformSpecificExtraResources)
}

if (extraResources != null) {
const expandedPatterns = extraResources.map(it => it
.replace(/\$\{arch\}/g, arch)
.replace(/\$\{os\}/g, this.platform.buildConfigurationKey))
await BluebirdPromise.map(await globby(expandedPatterns, {cwd: this.projectDir}), it => {
let resourcesDir = appOutDir
if (platform === "darwin") {
resourcesDir = path.join(resourcesDir, this.appName + ".app", "Contents", "Resources")
}
return copy(path.join(this.projectDir, it), path.join(resourcesDir, it))
})
}
}

abstract packageInDistributableFormat(outDir: string, appOutDir: string, arch: string): Promise<any>
Expand Down
24 changes: 16 additions & 8 deletions src/winPackager.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,23 @@
import { downloadCertificate } from "./codeSign"
import { Promise as BluebirdPromise } from "bluebird"
import { PlatformPackager, BuildInfo } from "./platformPackager"
import { Platform } from "./metadata"
import { Platform, PlatformSpecificBuildOptions } from "./metadata"
import * as path from "path"
import { log } from "./util"
import { readFile, deleteFile, stat, rename, copy, emptyDir, Stats, writeFile } from "fs-extra-p"

//noinspection JSUnusedLocalSymbols
const __awaiter = require("./awaiter")
Array.isArray(__awaiter)

export default class WinPackager extends PlatformPackager<any> {
export interface WinBuildOptions extends PlatformSpecificBuildOptions {
readonly certificateFile?: string
readonly certificatePassword?: string

readonly icon?: string
readonly iconUrl?: string
}

export default class WinPackager extends PlatformPackager<WinBuildOptions> {
certFilePromise: Promise<string>
isNsis: boolean

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

Expand Down Expand Up @@ -66,8 +74,8 @@ export default class WinPackager extends PlatformPackager<any> {
async packageInDistributableFormat(outDir: string, appOutDir: string, arch: string): Promise<any> {
let iconUrl = this.metadata.build.iconUrl
if (!iconUrl) {
if (this.customDistOptions != null) {
iconUrl = this.customDistOptions.iconUrl
if (this.customBuildOptions != null) {
iconUrl = this.customBuildOptions.iconUrl
}
if (!iconUrl) {
if (this.info.repositoryInfo != null) {
Expand Down Expand Up @@ -103,7 +111,7 @@ export default class WinPackager extends PlatformPackager<any> {
certificatePassword: this.options.cscKeyPassword,
fixUpPaths: false,
usePackageJson: false
}, this.customDistOptions)
}, this.customBuildOptions)

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

0 comments on commit cbe3ff8

Please sign in to comment.