From 5d376e15e887d08ffc5690ff0ffcddc7981f11f4 Mon Sep 17 00:00:00 2001 From: develar Date: Tue, 8 Mar 2016 20:36:40 +0100 Subject: [PATCH] feat: use productName from app/package.json if present #204 Closes #204, #223 --- .idea/runConfigurations/linuxPackagerTest.xml | 8 + .idea/runConfigurations/winPackagerTest.xml | 9 + lib/linux.d.ts | 5 - lib/linux.js | 2 +- package.json | 4 +- src/builder.ts | 15 +- src/codeSign.ts | 7 +- src/index.ts | 3 +- src/linuxPackager.ts | 169 ++++++++++++------ src/macPackager.ts | 16 +- src/metadata.ts | 75 ++++++++ src/packager.ts | 17 +- src/platformPackager.ts | 52 ++---- src/promise.ts | 8 +- src/repositoryInfo.ts | 29 +-- src/util.ts | 18 +- src/winPackager.ts | 54 +++--- templates/linux/after-install.tpl | 2 +- templates/linux/after-remove.tpl | 2 +- test/README.md | 6 +- .../package.json | 2 +- test/fixtures/test-app-one/package.json | 2 +- test/fixtures/test-app/package.json | 2 +- test/src/BuildTest.ts | 42 +++-- test/src/helpers/expectedContents.ts | 1 - test/src/helpers/packTester.ts | 63 +++++-- test/src/helpers/runTests.ts | 2 +- test/src/linuxPackagerTest.ts | 2 +- tsconfig.json | 3 +- 29 files changed, 395 insertions(+), 225 deletions(-) create mode 100644 .idea/runConfigurations/linuxPackagerTest.xml create mode 100644 .idea/runConfigurations/winPackagerTest.xml delete mode 100644 lib/linux.d.ts create mode 100644 src/metadata.ts rename test/fixtures/{test-app-no-author-email => no-author-email}/package.json (91%) diff --git a/.idea/runConfigurations/linuxPackagerTest.xml b/.idea/runConfigurations/linuxPackagerTest.xml new file mode 100644 index 00000000000..341ddddd92d --- /dev/null +++ b/.idea/runConfigurations/linuxPackagerTest.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/runConfigurations/winPackagerTest.xml b/.idea/runConfigurations/winPackagerTest.xml new file mode 100644 index 00000000000..96d010e19e8 --- /dev/null +++ b/.idea/runConfigurations/winPackagerTest.xml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/lib/linux.d.ts b/lib/linux.d.ts deleted file mode 100644 index 741c0266955..00000000000 --- a/lib/linux.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -declare class Linux { - build(options: any, callback: (error: Error, path: string) => void): void -} - -export function init(): Linux diff --git a/lib/linux.js b/lib/linux.js index 7166446a12f..a3bfa9dcbbe 100644 --- a/lib/linux.js +++ b/lib/linux.js @@ -121,7 +121,7 @@ function _buildPackage( options, scripts, tmpFolder, destination, callback ) { '-t', linux.target, '--architecture', linux.archName, '--rpm-os', 'linux', - '--name', linux.title, + '--name', linux.name || linux.title, '--force', '--after-install', scripts[0], '--after-remove', scripts[1], diff --git a/package.json b/package.json index 7874dde4ca6..50211d25b6d 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,7 @@ "bluebird": "^3.3.4", "command-line-args": "^2.1.6", "electron-packager-tf": "^5.2.3", - "electron-winstaller-fixed": "^2.0.5-beta.4", + "electron-winstaller-fixed": "^2.0.5-beta.7", "fs-extra": "^0.26.5", "fs-extra-p": "^0.1.0", "gm": "^1.21.1", @@ -73,7 +73,7 @@ "devDependencies": { "ava-tf": "^0.12.4-beta.6", "babel-plugin-array-includes": "^2.0.3", - "babel-plugin-transform-es2015-parameters": "^6.6.5", + "babel-plugin-transform-es2015-parameters": "^6.7.0", "electron-download": "^2.0.0", "eslint": "^2.3.0", "eslint-plugin-ava": "sindresorhus/eslint-plugin-ava", diff --git a/src/builder.ts b/src/builder.ts index 39799f0023b..30af89fa112 100644 --- a/src/builder.ts +++ b/src/builder.ts @@ -27,7 +27,7 @@ export async function createPublisher(packager: Packager, options: BuildOptions, export interface BuildOptions extends PackagerOptions, PublishOptions { } -export function build(options: BuildOptions = {}): Promise { +export async function build(options: BuildOptions = {}): Promise { if (options.cscLink == null) { options.cscLink = process.env.CSC_LINK } @@ -83,15 +83,16 @@ export function build(options: BuildOptions = {}): Promise { } }) } - return executeFinally(packager.build(), error => { - if (error == null) { - return Promise.all(publishTasks) - } - else { + + await executeFinally(packager.build(), errorOccurred => { + if (errorOccurred) { for (let task of publishTasks) { task.cancel() } - return null + return BluebirdPromise.resolve(null) + } + else { + return BluebirdPromise.all(publishTasks) } }) } \ No newline at end of file diff --git a/src/codeSign.ts b/src/codeSign.ts index 233aaa7ecc7..47f5f1e826c 100644 --- a/src/codeSign.ts +++ b/src/codeSign.ts @@ -7,6 +7,7 @@ import { executeFinally, all } from "./promise" import { Promise as BluebirdPromise } from "bluebird" import { randomBytes } from "crypto" +//noinspection JSUnusedLocalSymbols const __awaiter = require("./awaiter") export interface CodeSigningInfo { @@ -27,7 +28,7 @@ export function createKeychain(keychainName: string, cscLink: string, cscKeyPass const developerCertPath = path.join(tmpdir(), randomString() + ".p12") const keychainPassword = randomString() - return executeFinally(Promise.all([ + return executeFinally(BluebirdPromise.all([ download("https://developer.apple.com/certificationauthority/AppleWWDRCA.cer", appleCertPath), download(cscLink, developerCertPath), BluebirdPromise.mapSeries([ @@ -37,9 +38,9 @@ export function createKeychain(keychainName: string, cscLink: string, cscKeyPass ], it => exec("security", it)) ]) .then(() => importCerts(keychainName, appleCertPath, developerCertPath, cscKeyPassword)), - error => { + errorOccurred => { const tasks = [deleteFile(appleCertPath, true), deleteFile(developerCertPath, true)] - if (error != null) { + if (errorOccurred) { tasks.push(deleteKeychain(keychainName)) } return all(tasks) diff --git a/src/index.ts b/src/index.ts index 5e438ca26b5..65f40fdf8b6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,7 +3,8 @@ import * as path from "path" import * as fs from "fs-extra-p" export { Packager } from "./packager" -export { PackagerOptions, Platform } from "./platformPackager" +export { PackagerOptions } from "./platformPackager" +export { AppMetadata, DevMetadata, Platform, getProductName } from "./metadata" /** * Prototype for electron-builder diff --git a/src/linuxPackager.ts b/src/linuxPackager.ts index 47279d97716..22110935a81 100644 --- a/src/linuxPackager.ts +++ b/src/linuxPackager.ts @@ -1,28 +1,39 @@ import * as path from "path" import { Promise as BluebirdPromise } from "bluebird" -import { init } from "../lib/linux" -import { PlatformPackager, BuildInfo, Platform } from "./platformPackager" +import { PlatformPackager, BuildInfo } from "./platformPackager" +import { Platform } from "./metadata" import { dir as _tpmDir, TmpOptions } from "tmp" import { exec, log } from "./util" import { State as Gm } from "gm" +import { outputFile, readFile } from "fs-extra-p" +const template = require("lodash.template") +//noinspection JSUnusedLocalSymbols const __awaiter = require("./awaiter") -Array.isArray(__awaiter) -const buildDeb = BluebirdPromise.promisify(init().build) const tmpDir = BluebirdPromise.promisify(<(config: TmpOptions, callback: (error: Error, path: string, cleanupCallback: () => void) => void) => void>_tpmDir) export class LinuxPackager extends PlatformPackager { - desktopIcons: Promise> + private readonly debOptions: DebOptions + + private readonly packageFiles: Promise> + private readonly scriptFiles: Promise> constructor(info: BuildInfo) { super(info) - if (this.options.dist && (this.customDistOptions == null || this.customDistOptions.desktopTemplate == null)) { - this.desktopIcons = this.computeDesktopIconPath() - } - else { - this.desktopIcons = BluebirdPromise.resolve(null) + this.debOptions = Object.assign({ + name: this.metadata.name, + comment: this.metadata.description, + }, this.customDistOptions) + + if (this.options.dist) { + const tempDir = tmpDir({ + unsafeCleanup: true, + prefix: "electron-builder-" + }) + this.packageFiles = this.computePackageFiles(tempDir) + this.scriptFiles = this.createScripts(tempDir) } } @@ -30,12 +41,33 @@ export class LinuxPackager extends PlatformPackager { return Platform.LINUX } - private async computeDesktopIconPath(): Promise> { - const tempDir = await tmpDir({ - unsafeCleanup: true, - prefix: "png-icons" - }) + private async computePackageFiles(tempDirPromise: Promise): Promise> { + const tempDir = await tempDirPromise + + const promises: Array>> = [] + if (this.customDistOptions == null || this.customDistOptions.desktop == null) { + promises.push(this.computeDesktopIconPath(tempDir)) + } + + promises.push(this.computeDesktop(tempDir)) + return Array.prototype.concat.apply([], await BluebirdPromise.all(promises)) + } + + private async computeDesktop(tempDir: string): Promise> { + const tempFile = path.join(tempDir, this.appName + ".desktop") + await outputFile(tempFile, this.debOptions.desktop || `[Desktop Entry] +Name=${this.appName} +Comment=${this.debOptions.comment} +Exec="${this.appName}" +Terminal=false +Type=Application +Icon=${this.metadata.name} +`) + return [`${tempFile}=/usr/share/applications/${this.appName}.desktop`] + } + + private async computeDesktopIconPath(tempDir: string): Promise> { const outputs = await exec("icns2png", ["-x", "-o", tempDir, path.join(this.buildResourcesDir, "icon.icns")]) if (!outputs[0].toString().includes("ih32")) { log("48x48 is not found in the icns, 128x128 will be resized") @@ -47,9 +79,10 @@ export class LinuxPackager extends PlatformPackager { }) } - const appName = this.metadata.name + const name = this.metadata.name + function createMapping(size: string) { - return `${tempDir}/icon_${size}x${size}x32.png=/usr/share/icons/hicolor/${size}x${size}/apps/${appName}.png` + return `${tempDir}/icon_${size}x${size}x32.png=/usr/share/icons/hicolor/${size}x${size}/apps/${name}.png` } return [ @@ -62,54 +95,80 @@ export class LinuxPackager extends PlatformPackager { ] } - async packageInDistributableFormat(outDir: string, appOutDir: string, arch: string): Promise { - const specification: DebOptions = { - version: this.metadata.version, - title: this.metadata.name, - comment: this.metadata.description, - maintainer: `${this.metadata.author.name} <${this.metadata.author.email}>`, - arch: arch === "ia32" ? 32 : 64, - target: "deb", - executable: this.metadata.name, - desktop: `[Desktop Entry] - Name=${this.metadata.name} - Comment=${this.metadata.description} - Exec=${this.metadata.name} - Terminal=false - Type=Application - Icon=${this.metadata.name} - `, - dirs: await this.desktopIcons - } + private async createScripts(tempDirPromise: Promise): Promise> { + const tempDir = await tempDirPromise + const defaultTemplatesDir = path.join(__dirname, "..", "templates", "linux") - if (this.customDistOptions != null) { - Object.assign(specification, this.customDistOptions) - } - return await buildDeb({ - log: function emptyLog() {/* ignore out */}, - appPath: appOutDir, - out: outDir, - config: { - linux: specification - } - }) + const templateOptions = Object.assign({ + // old API compatibility + executable: this.appName, + }, this.debOptions) + + const afterInstallTemplate = this.debOptions.afterInstall || path.join(defaultTemplatesDir, "after-install.tpl") + const afterInstallFilePath = writeConfigFile(tempDir, afterInstallTemplate, templateOptions) + + const afterRemoveTemplate = this.debOptions.afterRemove || path.join(defaultTemplatesDir, "after-remove.tpl") + const afterRemoveFilePath = writeConfigFile(tempDir, afterRemoveTemplate, templateOptions) + + return await BluebirdPromise.all([afterInstallFilePath, afterRemoveFilePath]) + } + + async packageInDistributableFormat(outDir: string, appOutDir: string, arch: string): Promise { + return await this.buildDeb(this.debOptions, outDir, appOutDir, arch) .then(it => this.dispatchArtifactCreated(it)) } + + private async buildDeb(options: DebOptions, outDir: string, appOutDir: string, arch: string): Promise { + const archName = arch === "ia32" ? "i386" : "amd64" + const target = "deb" + const outFilename = `${this.metadata.name}-${this.metadata.version}-${archName}.${target}` + const destination = path.join(outDir, outFilename) + const scripts = await this.scriptFiles + await exec("fpm", [ + "-s", "dir", + "-t", target, + "--architecture", archName, + "--rpm-os", "linux", + "--name", this.metadata.name, + "--force", + "--after-install", scripts[0], + "--after-remove", scripts[1], + "--description", options.comment, + "--maintainer", options.maintainer || `${this.metadata.author.name} <${this.metadata.author.email}>`, + "--version", this.metadata.version, + "--package", destination, + "--deb-compression", options.compression || "xz", + appOutDir + "/=/opt/" + this.appName, + ].concat(await this.packageFiles)) + return outFilename + } +} + +async function writeConfigFile(tempDir: string, templatePath: string, options: any): Promise { + const config = template(await readFile(templatePath, "utf8"), + { + // set interpolate explicitely to avoid troubles with templating of installer.nsi.tpl + interpolate: /<%=([\s\S]+?)%>/g + })(options) + + const outputPath = path.join(tempDir, path.basename(templatePath, ".tpl")) + await outputFile(outputPath, config) + return outputPath } export interface DebOptions { - title: string + name: string comment: string - version: string - - arch: number maintainer: string - executable: string - target: string - desktopTemplate?: string + /** + * .desktop file template + */ desktop?: string - dirs?: Array + afterInstall?: string + afterRemove?: string + + compression?: string } \ No newline at end of file diff --git a/src/macPackager.ts b/src/macPackager.ts index a63a8280841..3a0ac3a7601 100644 --- a/src/macPackager.ts +++ b/src/macPackager.ts @@ -1,4 +1,5 @@ -import { PlatformPackager, BuildInfo, Platform } from "./platformPackager" +import { PlatformPackager, BuildInfo } from "./platformPackager" +import { Platform } from "./metadata" import * as path from "path" import { Promise as BluebirdPromise } from "bluebird" import { log, spawn } from "./util" @@ -30,7 +31,7 @@ export default class MacPackager extends PlatformPackager async pack(platform: string, outDir: string, appOutDir: string, arch: string): Promise { await super.pack(platform, outDir, appOutDir, arch) let codeSigningInfo = await this.codeSigningInfo - return await this.signMac(path.join(appOutDir, this.metadata.name + ".app"), codeSigningInfo) + return await this.signMac(path.join(appOutDir, this.appName + ".app"), codeSigningInfo) } private signMac(distPath: string, codeSigningInfo: CodeSigningInfo): Promise { @@ -55,7 +56,7 @@ export default class MacPackager extends PlatformPackager log("Creating DMG") const specification: appdmg.Specification = { - title: this.metadata.name, + title: this.appName, icon: path.join(this.buildResourcesDir, "icon.icns"), "icon-size": 80, background: path.join(this.buildResourcesDir, "background.png"), @@ -74,10 +75,10 @@ export default class MacPackager extends PlatformPackager } if (specification.title == null) { - specification.title = this.metadata.name + specification.title = this.appName } - specification.contents[1].path = path.join(appOutDir, this.metadata.name + ".app") + specification.contents[1].path = path.join(appOutDir, this.appName + ".app") const emitter = require("appdmg")({ target: artifactPath, @@ -96,10 +97,9 @@ export default class MacPackager extends PlatformPackager private zipMacApp(outDir: string): Promise { log("Creating ZIP for Squirrel.Mac") - const appName = this.metadata.name // -y param is important - "store symbolic links as the link instead of the referenced file" - const resultPath = `${appName}-${this.metadata.version}-mac.zip` - const args = ["-ryXq", resultPath, appName + ".app"] + const resultPath = `${this.metadata.name}-${this.metadata.version}-mac.zip` + const args = ["-ryXq", resultPath, this.appName + ".app"] // todo move to options if (process.env.TEST_MODE === "true") { diff --git a/src/metadata.ts b/src/metadata.ts new file mode 100644 index 00000000000..861ad51b253 --- /dev/null +++ b/src/metadata.ts @@ -0,0 +1,75 @@ +export interface AppMetadata extends Metadata { + readonly version: string + + /** The application name */ + readonly name: string + + /** + * As {@link AppMetadata#name}, but allows you to specify a product name for your executable which contains spaces and other special characters + * not allowed in the [name property]{@link https://docs.npmjs.com/files/package.json#name}. + */ + readonly productName?: string + + readonly description: string + readonly author: AuthorMetadata + + readonly build: BuildMetadata +} + +export function getProductName(metadata: AppMetadata) { + return metadata.build.productName || metadata.productName || metadata.name +} + +export interface DevMetadata extends Metadata { + readonly build: DevBuildMetadata + + readonly directories?: MetadataDirectories +} + +export interface BuildMetadata { + readonly "app-bundle-id": string + readonly "app-category-type": string + + readonly iconUrl: string + + /** + * See {@link AppMetadata#productName}. + */ + readonly productName?: string +} + +export interface RepositoryInfo { + readonly url: string +} + +export interface Metadata { + readonly repository: string | RepositoryInfo +} + +export interface AuthorMetadata { + readonly name: string + readonly email: string +} + +export interface MetadataDirectories { + readonly buildResources?: string +} + +export interface DevBuildMetadata { + readonly osx: appdmg.Specification + readonly win: any, + readonly linux: any +} + +export class Platform { + public static OSX = new Platform("osx", "osx") + public static LINUX = new Platform("linux", "linux") + public static WINDOWS = new Platform("windows", "win") + + constructor(public name: string, public buildConfigurationKey: string) { + } + + toString() { + return this.name + } +} \ No newline at end of file diff --git a/src/packager.ts b/src/packager.ts index 93567d350a2..c3511623f1c 100644 --- a/src/packager.ts +++ b/src/packager.ts @@ -4,8 +4,9 @@ import { DEFAULT_APP_DIR_NAME, installDependencies, log, getElectronVersion, rea import { all, executeFinally } from "./promise" import { EventEmitter } from "events" import { Promise as BluebirdPromise } from "bluebird" -import { AppMetadata, InfoRetriever } from "./repositoryInfo" -import { PackagerOptions, PlatformPackager, BuildInfo, DevMetadata, Platform } from "./platformPackager" +import { InfoRetriever } from "./repositoryInfo" +import { AppMetadata, Platform, DevMetadata } from "./metadata" +import { PackagerOptions, PlatformPackager, BuildInfo } from "./platformPackager" import MacPackager from "./macPackager" import WinPackager from "./winPackager" import * as errorMessages from "./errorMessages" @@ -19,9 +20,8 @@ function addHandler(emitter: EventEmitter, event: string, handler: Function) { } export class Packager implements BuildInfo { - projectDir: string - - appDir: string + readonly projectDir: string + readonly appDir: string metadata: AppMetadata devMetadata: DevMetadata @@ -30,7 +30,7 @@ export class Packager implements BuildInfo { electronVersion: string - eventEmitter = new EventEmitter() + readonly eventEmitter = new EventEmitter() //noinspection JSUnusedLocalSymbols constructor(public options: PackagerOptions, public repositoryInfo: InfoRetriever = null) { @@ -61,7 +61,7 @@ export class Packager implements BuildInfo { }) const cleanupTasks: Array<() => Promise> = [] - return executeFinally(this.doBuild(platforms, cleanupTasks), error => all(cleanupTasks.map(it => it()))) + return executeFinally(this.doBuild(platforms, cleanupTasks), () => all(cleanupTasks.map(it => it()))) } private async doBuild(platforms: Array, cleanupTasks: Array<() => Promise>): Promise { @@ -72,7 +72,8 @@ export class Packager implements BuildInfo { await this.installAppDependencies(arch) const outDir = path.join(this.projectDir, "dist") - const appOutDir = path.join(outDir, this.metadata.name + "-" + platform + "-" + arch) + // electron-packager uses productName in the directory name + const appOutDir = path.join(outDir, helper.appName + "-" + platform + "-" + arch) await helper.pack(platform, outDir, appOutDir, arch) if (this.options.dist) { distTasks.push(helper.packageInDistributableFormat(outDir, appOutDir, arch)) diff --git a/src/platformPackager.ts b/src/platformPackager.ts index 8935693864b..e907a2691f5 100644 --- a/src/platformPackager.ts +++ b/src/platformPackager.ts @@ -1,4 +1,5 @@ -import { AppMetadata, InfoRetriever, ProjectMetadataProvider, Metadata } from "./repositoryInfo" +import { InfoRetriever, ProjectMetadataProvider } from "./repositoryInfo" +import { AppMetadata, DevMetadata, Platform, getProductName } from "./metadata" import EventEmitter = NodeJS.EventEmitter import { Promise as BluebirdPromise } from "bluebird" import * as path from "path" @@ -9,35 +10,6 @@ const __awaiter = require("./awaiter") const pack = BluebirdPromise.promisify(packager) -export class Platform { - public static OSX = new Platform("osx", "osx") - public static LINUX = new Platform("linux", "linux") - public static WINDOWS = new Platform("windows", "win") - - constructor(public name: string, public buildConfigurationKey: string) { - } - - toString() { - return this.name - } -} - -export interface DevMetadata extends Metadata { - build: DevBuildMetadata - - directories?: MetadataDirectories -} - -export interface MetadataDirectories { - buildResources?: string -} - -export interface DevBuildMetadata { - osx: appdmg.Specification - win: any, - linux: any -} - export interface PackagerOptions { arch?: string @@ -72,16 +44,18 @@ export interface BuildInfo extends ProjectMetadataProvider { } export abstract class PlatformPackager implements ProjectMetadataProvider { - protected options: PackagerOptions + protected readonly options: PackagerOptions - protected projectDir: string - protected buildResourcesDir: string + protected readonly projectDir: string + protected readonly buildResourcesDir: string - metadata: AppMetadata - devMetadata: DevMetadata + readonly metadata: AppMetadata + readonly devMetadata: DevMetadata customDistOptions: DC + readonly appName: string + protected abstract get platform(): Platform constructor(protected info: BuildInfo) { @@ -96,6 +70,8 @@ export abstract class PlatformPackager implements ProjectMetadataProvider { const buildMetadata: any = info.devMetadata.build this.customDistOptions = buildMetadata == null ? buildMetadata : buildMetadata[this.platform.buildConfigurationKey] } + + this.appName = getProductName(this.metadata) } protected get relativeBuildResourcesDirname() { @@ -118,7 +94,7 @@ export abstract class PlatformPackager implements ProjectMetadataProvider { const options = Object.assign({ dir: this.info.appDir, out: outDir, - name: this.metadata.name, + name: this.appName, platform: platform, arch: arch, version: this.info.electronVersion, @@ -132,8 +108,8 @@ export abstract class PlatformPackager implements ProjectMetadataProvider { FileDescription: this.metadata.description, ProductVersion: version, FileVersion: buildVersion, - ProductName: this.metadata.name, - InternalName: this.metadata.name, + ProductName: this.appName, + InternalName: this.appName, } }, this.metadata.build, {"tmpdir": false}) diff --git a/src/promise.ts b/src/promise.ts index ee811fdb2ea..0b81edea1d0 100644 --- a/src/promise.ts +++ b/src/promise.ts @@ -1,7 +1,7 @@ import { Promise as BluebirdPromise } from "bluebird" +//noinspection JSUnusedLocalSymbols const __awaiter = require("./awaiter") -Array.isArray(__awaiter) export function printErrorAndExit(error: Error) { console.error(error.stack || error.message || error) @@ -9,14 +9,14 @@ export function printErrorAndExit(error: Error) { } // you don't need to handle error in your task - it is passed only indicate status of promise -export async function executeFinally(promise: Promise, task: (error?: Error) => Promise): Promise { +export async function executeFinally(promise: Promise, task: (errorOccurred: boolean) => Promise): Promise { let result: any = null try { result = await promise } catch (originalError) { try { - await task(originalError) + await task(true) } catch (taskError) { throw new NestedError([originalError, taskError]) @@ -26,7 +26,7 @@ export async function executeFinally(promise: Promise, task: (error?: Error } try { - await task(null) + await task(false) } catch (taskError) { throw taskError diff --git a/src/repositoryInfo.ts b/src/repositoryInfo.ts index e0055e6dcdf..b42cf43e34b 100644 --- a/src/repositoryInfo.ts +++ b/src/repositoryInfo.ts @@ -1,38 +1,11 @@ import { fromUrl as parseRepositoryUrl, Info } from "hosted-git-info" import { readText } from "./promisifed-fs" +import { AppMetadata, Metadata } from "./metadata" import * as path from "path" const __awaiter = require("./awaiter") Array.isArray(__awaiter) -export interface RepositoryInfo { - url: string -} - -export interface Metadata { - repository: string | RepositoryInfo -} - -export interface MetadataAuthor { - name: string - email: string -} - -export interface AppMetadata extends Metadata { - version: string - name: string - description: string - author: MetadataAuthor - - build: BuildMetadata - - windowsPackager: any -} - -export interface BuildMetadata { - iconUrl: string -} - export interface ProjectMetadataProvider { metadata: AppMetadata devMetadata: Metadata diff --git a/src/util.ts b/src/util.ts index ce76f633131..9a5b0b0566e 100644 --- a/src/util.ts +++ b/src/util.ts @@ -12,7 +12,6 @@ export const commonArgs: any[] = [{ description: "Relative (to the working directory) path to the folder containing the application package.json. Working directory or app/ by default." }] -const execFileAsync: (file: string, args?: string[], options?: ExecOptions) => BluebirdPromise = (BluebirdPromise.promisify(execFile, {multiArgs: true})) export const readPackageJson = BluebirdPromise.promisify(readPackageJsonAsync) export function installDependencies(appDir: string, arch: string, electronVersion: string): BluebirdPromise { @@ -65,7 +64,22 @@ export interface SpawnOptions extends BaseExecOptions { } export function exec(file: string, args?: string[], options?: ExecOptions): BluebirdPromise { - return execFileAsync(file, args, options) + return new BluebirdPromise((resolve, reject) => { + execFile(file, args, options, function (error, stdout, stderr) { + if (error == null) { + resolve([stdout, stderr]) + } + else { + if (stdout.length !== 0) { + console.error(stdout.toString()) + } + if (stderr.length !== 0) { + console.error(stderr.toString()) + } + reject(error) + } + }) + }) } export function spawn(command: string, args?: string[], options?: SpawnOptions): BluebirdPromise { diff --git a/src/winPackager.ts b/src/winPackager.ts index b82f1adf149..7bdd082a922 100644 --- a/src/winPackager.ts +++ b/src/winPackager.ts @@ -1,9 +1,10 @@ import { downloadCertificate } from "./codeSign" import { Promise as BluebirdPromise } from "bluebird" -import { PlatformPackager, BuildInfo, Platform } from "./platformPackager" +import { PlatformPackager, BuildInfo } from "./platformPackager" +import { Platform } from "./metadata" import * as path from "path" import { log } from "./util" -import { deleteFile, stat, rename, copy, emptyDir, Stats } from "fs-extra-p" +import { readFile, deleteFile, stat, rename, copy, emptyDir, Stats, writeFile } from "fs-extra-p" const __awaiter = require("./awaiter") Array.isArray(__awaiter) @@ -85,14 +86,14 @@ export default class WinPackager extends PlatformPackager { const certificateFile = await this.certFilePromise const version = this.metadata.version const installerOutDir = this.computeDistOut(outDir, arch) - const appName = this.metadata.name const archSuffix = arch === "x64" ? "-x64" : "" - const installerExePath = path.join(installerOutDir, appName + "Setup-" + version + archSuffix + ".exe") const options = Object.assign({ name: this.metadata.name, + productName: this.appName, + exe: this.appName + ".exe", + title: this.appName, appDirectory: appOutDir, outputDirectory: installerOutDir, - productName: appName, version: version, description: this.metadata.description, authors: this.metadata.author.name, @@ -100,9 +101,12 @@ export default class WinPackager extends PlatformPackager { setupIcon: path.join(this.buildResourcesDir, "icon.ico"), certificateFile: certificateFile, certificatePassword: this.options.cscKeyPassword, - fixUpPaths: false + fixUpPaths: false, + usePackageJson: false }, this.customDistOptions) + // 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") if (this.isNsis) { return await this.nsis(options, installerExePath) } @@ -129,22 +133,32 @@ export default class WinPackager extends PlatformPackager { } } - const promises = [ - rename(path.join(installerOutDir, "Setup.exe"), installerExePath) - .then(it => this.dispatchArtifactCreated(it)), - rename(path.join(installerOutDir, appName + "-" + version + "-full.nupkg"), path.join(installerOutDir, appName + "-" + version + archSuffix + "-full.nupkg")) - .then(it => this.dispatchArtifactCreated(it)) - ] + const releasesFile = path.join(installerOutDir, "RELEASES") + const nupkgPathOriginal = this.metadata.name + "-" + version + "-full.nupkg" + const nupkgPathWithArch = this.metadata.name + "-" + version + archSuffix + "-full.nupkg" - if (arch === "x64") { - this.dispatchArtifactCreated(path.join(installerOutDir, "RELEASES")) - } - else { - promises.push(copy(path.join(installerOutDir, "RELEASES"), path.join(installerOutDir, "RELEASES-ia32")) - .then(it => this.dispatchArtifactCreated(it))) + async function changeFileNameInTheReleasesFile() { + const data = (await readFile(releasesFile, "utf8")).replace(new RegExp(" " + nupkgPathOriginal + " ", "g"), " " + nupkgPathWithArch + " ") + await writeFile(releasesFile, data) } - return await BluebirdPromise.all(promises) + await BluebirdPromise.all([ + rename(path.join(installerOutDir, "Setup.exe"), installerExePath) + .then(it => this.dispatchArtifactCreated(it)), + rename(path.join(installerOutDir, nupkgPathOriginal), path.join(installerOutDir, nupkgPathWithArch)) + .then(it => this.dispatchArtifactCreated(it)), + changeFileNameInTheReleasesFile() + .then(() => { + if (arch === "x64") { + this.dispatchArtifactCreated(releasesFile) + return null + } + else { + return copy(releasesFile, path.join(installerOutDir, "RELEASES-ia32")) + .then(it => this.dispatchArtifactCreated(it)) + } + }), + ]) } private async nsis(options: any, installerFile: string) { @@ -160,7 +174,7 @@ export default class WinPackager extends PlatformPackager { copyAssetsToTmpFolder: false, config: { win: Object.assign({ - title: options.name, + title: options.title, version: options.version, icon: options.setupIcon, publisher: options.authors, diff --git a/templates/linux/after-install.tpl b/templates/linux/after-install.tpl index fb92b45a91d..dac9990edcd 100644 --- a/templates/linux/after-install.tpl +++ b/templates/linux/after-install.tpl @@ -1,4 +1,4 @@ #!/bin/bash # Link to the binary -ln -sf /opt/<%= executable %>/<%= executable %> /usr/local/bin/<%= executable %> +ln -sf '/opt/<%= executable %>/<%= executable %>' '/usr/local/bin/<%= executable %>' diff --git a/templates/linux/after-remove.tpl b/templates/linux/after-remove.tpl index 7087dd46cc2..f7014bc8d70 100644 --- a/templates/linux/after-remove.tpl +++ b/templates/linux/after-remove.tpl @@ -1,4 +1,4 @@ #!/bin/bash # Delete the link to the binary -rm -f /usr/local/bin/<%= executable %> +rm -f '/usr/local/bin/<%= executable %>' diff --git a/test/README.md b/test/README.md index 69bcc34042f..0aa2c73add3 100644 --- a/test/README.md +++ b/test/README.md @@ -10,4 +10,8 @@ Do not use OS X bundled Ruby. Install using `brew`. ``` brew install ruby gnu-tar dpkg libicns gem install fpm -``` \ No newline at end of file +``` + +# Inspect output if test uses temporary directory +Set environment variable `TEST_APP_TMP_DIR` (e.g. `/tmp/electron-builder-test`). +Specified directory will be used instead of random temporary directory and *cleared* on each run. \ No newline at end of file diff --git a/test/fixtures/test-app-no-author-email/package.json b/test/fixtures/no-author-email/package.json similarity index 91% rename from test/fixtures/test-app-no-author-email/package.json rename to test/fixtures/no-author-email/package.json index 62d74748411..86849c8d459 100644 --- a/test/fixtures/test-app-no-author-email/package.json +++ b/test/fixtures/no-author-email/package.json @@ -5,7 +5,7 @@ "description": "Test Application", "author": "Foo Bar", "devDependencies": { - "electron-prebuilt": "^0.36.9" + "electron-prebuilt": "^0.36.10" }, "build": { "app-bundle-id": "your.id", diff --git a/test/fixtures/test-app-one/package.json b/test/fixtures/test-app-one/package.json index 355709e9e71..12c0bbe85a7 100644 --- a/test/fixtures/test-app-one/package.json +++ b/test/fixtures/test-app-one/package.json @@ -8,7 +8,7 @@ }, "author": "Foo Bar ", "devDependencies": { - "electron-prebuilt": "^0.36.9" + "electron-prebuilt": "^0.36.10" }, "build": { "app-bundle-id": "your.id", diff --git a/test/fixtures/test-app/package.json b/test/fixtures/test-app/package.json index 2079c28dd2e..375a414b2a7 100644 --- a/test/fixtures/test-app/package.json +++ b/test/fixtures/test-app/package.json @@ -4,6 +4,6 @@ "start": "electron ." }, "devDependencies": { - "electron-prebuilt": "^0.36.9" + "electron-prebuilt": "^0.36.10" } } diff --git a/test/src/BuildTest.ts b/test/src/BuildTest.ts index 159ba7e4e38..a77b518f642 100644 --- a/test/src/BuildTest.ts +++ b/test/src/BuildTest.ts @@ -21,19 +21,7 @@ test.ifOsx("mac: one-package.json", async () => { }) test("custom app dir", async () => { - let platforms: Array - if (process.platform === "darwin") { - platforms = ["darwin", "linux"] - } - else if (process.platform === "linux") { - // todo install wine on Linux agent - platforms = ["linux"] - } - else { - platforms = ["win32"] - } - - await assertPack("test-app-one", platforms, { + await assertPack("test-app-one", getPossiblePlatforms(), { // speed up tests, we don't need check every arch arch: process.arch }, true, async (projectDir) => { @@ -44,9 +32,35 @@ test("custom app dir", async () => { } return await BluebirdPromise.all([ - writeJson(file, data, {spaces: 2}), + writeJson(file, data), move(path.join(projectDir, "build"), path.join(projectDir, "custom")) ]) }) }) +test("productName with space", async () => { + await assertPack("test-app-one", getPossiblePlatforms(), { + // speed up tests, we don't need check every arch + arch: process.arch + }, true, async (projectDir) => { + const file = path.join(projectDir, "package.json") + const data = await readJson(file) + data.productName = "Test App" + + return await writeJson(file, data) + }) +}) + +function getPossiblePlatforms(): Array { + const isCi = process.env.CI != null + if (process.platform === "darwin") { + return isCi ? ["darwin", "linux"] : ["darwin", "linux", "win32"] + } + else if (process.platform === "linux") { + // todo install wine on Linux agent + return ["linux"] + } + else { + return ["win32"] + } +} \ No newline at end of file diff --git a/test/src/helpers/expectedContents.ts b/test/src/helpers/expectedContents.ts index d5db75e3132..bf6067e6415 100644 --- a/test/src/helpers/expectedContents.ts +++ b/test/src/helpers/expectedContents.ts @@ -10,7 +10,6 @@ export const expectedLinuxContents = [ "/opt/TestApp/LICENSE", "/opt/TestApp/LICENSES.chromium.html", "/opt/TestApp/natives_blob.bin", - "/opt/TestApp/pkgtarget", "/opt/TestApp/snapshot_blob.bin", "/opt/TestApp/TestApp", "/opt/TestApp/version", diff --git a/test/src/helpers/packTester.ts b/test/src/helpers/packTester.ts index 4782f477839..ea72b155b99 100644 --- a/test/src/helpers/packTester.ts +++ b/test/src/helpers/packTester.ts @@ -5,7 +5,7 @@ import { parse as parsePlist } from "plist" import { CSC_LINK, CSC_KEY_PASSWORD } from "./codeSignData" import { expectedLinuxContents } from "./expectedContents" import { readText } from "out/promisifed-fs" -import { Packager, PackagerOptions, Platform } from "out" +import { Packager, PackagerOptions, Platform, getProductName } from "out" import { normalizePlatforms } from "out/packager" import { exec } from "out/util" import pathSorter = require("path-sort") @@ -20,9 +20,13 @@ let tmpDirCounter = 0 export async function assertPack(fixtureName: string, platform: string | Array, packagerOptions?: PackagerOptions, useTempDir?: boolean, tempDirCreated?: (projectDir: string) => Promise) { let projectDir = path.join(__dirname, "..", "..", "fixtures", fixtureName) // const isDoNotUseTempDir = platform === "darwin" + const customTmpDir = process.env.TEST_APP_TMP_DIR if (useTempDir) { // non-osx test uses the same dir as osx test, but we cannot share node_modules (because tests executed in parallel) - const dir = path.join(tmpdir(), tmpDirPrefix + fixtureName + tmpDirCounter++) + const dir = customTmpDir == null ? path.join(tmpdir(), tmpDirPrefix + fixtureName + "-" + tmpDirCounter++) : path.resolve(customTmpDir) + if (customTmpDir != null) { + console.log("Custom temp dir used: %s", customTmpDir) + } await emptyDir(dir) await copy(projectDir, dir, { filter: it => { @@ -42,7 +46,7 @@ export async function assertPack(fixtureName: string, platform: string | Array { + if (it === "/opt/TestApp/TestApp") { + return "/opt/" + productName + "/" + productName + } + else if (it === "/usr/share/applications/TestApp.desktop") { + return `/usr/share/applications/${productName}.desktop` + } + else { + return it.replace(new RegExp("/opt/TestApp/", "g"), `/opt/${productName}/`) + } + }) + // let normalizedAppName = getProductName(packager.metadata).toLowerCase().replace(/ /g, '-') + // expectedContents[expectedContents.indexOf("/usr/share/doc/testapp/")] = "/usr/share/doc/" + normalizedAppName + "/" + // expectedContents[expectedContents.indexOf("/usr/share/doc/testapp/changelog.Debian.gz")] = "/usr/share/doc/" + normalizedAppName + "/changelog.Debian.gz" + + assertThat(await getContents(projectDir + "/dist/TestApp-1.0.0-amd64.deb", productName)).deepEqual(expectedContents) if (packagerOptions == null || packagerOptions.arch === null || packagerOptions.arch === "ia32") { - assertThat(await getContents(projectDir + "/dist/TestApp-1.0.0-i386.deb")).deepEqual(expectedLinuxContents) + assertThat(await getContents(projectDir + "/dist/TestApp-1.0.0-i386.deb", productName)).deepEqual(expectedContents) } // console.log(JSON.stringify(await getContents(projectDir + "/dist/TestApp-1.0.0-amd64.deb"), null, 2)) // console.log(JSON.stringify(await getContents(projectDir + "/dist/TestApp-1.0.0-i386.deb"), null, 2)) } else if (expandedPlatforms.includes("win32") && (packagerOptions == null || packagerOptions.target == null)) { - checkWindowsResult(packagerOptions, artifacts.get(Platform.WINDOWS)) + await checkWindowsResult(packagerOptions, artifacts.get(Platform.WINDOWS)) } } -async function checkOsXResult(projectDir: string, artifacts: Array) { - const packedAppDir = projectDir + "/dist/TestApp-darwin-x64/TestApp.app" - const info = parsePlist(await readText(packedAppDir + "/Contents/Info.plist")) +async function checkOsXResult(packager: Packager, artifacts: Array) { + const productName = getProductName(packager.metadata) + const packedAppDir = path.join(path.dirname(artifacts[0]), (productName || packager.metadata.name) + ".app") + const info = parsePlist(await readText(path.join(packedAppDir, "Contents", "Info.plist"))) assertThat(info).has.properties({ - CFBundleDisplayName: "TestApp", + CFBundleDisplayName: productName, CFBundleIdentifier: "your.id", LSApplicationCategoryType: "your.app.category.type", CFBundleVersion: "1.0.0" + "." + (process.env.TRAVIS_BUILD_NUMBER || process.env.CIRCLE_BUILD_NUM) @@ -108,13 +129,13 @@ async function checkOsXResult(projectDir: string, artifacts: Array) { const result = await exec("codesign", ["--verify", packedAppDir]) assertThat(result[0].toString()).not.match(/is not signed at all/) - assertThat(artifacts).deepEqual(pathSorter([ + assertThat(artifacts.map(it => path.basename((it))).sort()).deepEqual([ "TestApp-1.0.0-mac.zip", "TestApp-1.0.0.dmg" - ])) + ].sort()) } -function checkWindowsResult(packagerOptions: PackagerOptions, artifacts: Array) { +async function checkWindowsResult(packagerOptions: PackagerOptions, artifacts: Array) { const expected32 = [ "RELEASES-ia32", "TestApp-1.0.0-full.nupkg", @@ -126,14 +147,20 @@ function checkWindowsResult(packagerOptions: PackagerOptions, artifacts: Array path.basename((it))) + assertThat(filenames.slice().sort()).deepEqual(expected.sort()) + + let i = filenames.indexOf("RELEASES") + if (i !== -1) { + assertThat((await readText(artifacts[i])).indexOf("x64")).not.equal(-1) + } } -async function getContents(path: string) { +async function getContents(path: string, productName: string) { const result = await exec("dpkg", ["--contents", path]) return pathSorter(result[0].toString() .split("\n") .map(it => it.length === 0 ? null : it.substring(it.indexOf(".") + 1)) - .filter(it => it != null && !(it.startsWith("/opt/TestApp/locales/") || it.startsWith("/opt/TestApp/libgcrypt"))) + .filter(it => it != null && !(it.startsWith(`/opt/${productName}/locales/`) || it.startsWith(`/opt/${productName}/libgcrypt`))) ) } \ No newline at end of file diff --git a/test/src/helpers/runTests.ts b/test/src/helpers/runTests.ts index 23d4f48b4f2..a058a55db65 100644 --- a/test/src/helpers/runTests.ts +++ b/test/src/helpers/runTests.ts @@ -14,7 +14,7 @@ const rootDir = path.join(__dirname, "..", "..", "..") const testPackageDir = path.join(require("os").tmpdir(), "electron_builder_published") const testNodeModules = path.join(testPackageDir, "node_modules") -const electronVersion = "0.36.9" +const electronVersion = "0.36.10" BluebirdPromise.all([ deleteOldElectronVersion(), diff --git a/test/src/linuxPackagerTest.ts b/test/src/linuxPackagerTest.ts index ededfa22d90..b30c5e7c806 100644 --- a/test/src/linuxPackagerTest.ts +++ b/test/src/linuxPackagerTest.ts @@ -9,5 +9,5 @@ test.ifNotWindows("linux", async () => { }) test.ifNotWindows("no-author-email", async (t) => { - t.throws(assertPack("test-app-no-author-email", "linux"), /Please specify author 'email' in .*/) + t.throws(assertPack("no-author-email", "linux"), /Please specify author 'email' in .*/) }) diff --git a/tsconfig.json b/tsconfig.json index 73c1e935851..cd7ce639b8d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,7 +3,6 @@ "module": "commonjs", "target": "es6", "noImplicitAny": true, - "removeComments": true, "outDir": "out", "newLine": "LF", "noResolve": true, @@ -30,7 +29,6 @@ "node_modules/fs-extra-p/bluebird.d.ts" ], "files": [ - "lib/linux.d.ts", "typings/appdmg.d.ts", "typings/command-line-args.d.ts", "typings/electron-packager.d.ts", @@ -59,6 +57,7 @@ "src/install-app-deps.ts", "src/linuxPackager.ts", "src/macPackager.ts", + "src/metadata.ts", "src/packager.ts", "src/platformPackager.ts", "src/platforms.ts",