diff --git a/.travis.yml b/.travis.yml index b514f5a3ad8..0b8c13901c2 100755 --- a/.travis.yml +++ b/.travis.yml @@ -30,6 +30,8 @@ addons: - icnsutils - graphicsmagick - mono-devel + - bsdtar + - rpm before_install: - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew update ; fi diff --git a/docker/Dockerfile b/docker/Dockerfile index 3aed0a6fa1d..6a18e9c24e5 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -3,7 +3,7 @@ FROM buildpack-deps:xenial-curl # rpm is required for FPM to build rpm package RUN apt-get update -y && \ -apt-get install --no-install-recommends -y build-essential icnsutils graphicsmagick gcc-multilib g++-multilib libgnome-keyring-dev zip rpm && \ +apt-get install --no-install-recommends -y bsdtar build-essential autoconf libssl-dev icnsutils graphicsmagick gcc-multilib g++-multilib libgnome-keyring-dev zip rpm && \ apt-get clean && \ rm -rf /var/lib/apt/lists/* @@ -42,4 +42,74 @@ COPY test.sh /test.sh WORKDIR /project ENV DEBUG_COLORS true -ENV FORCE_COLOR true \ No newline at end of file +ENV FORCE_COLOR true + +# copied from https://github.com/docker-library/ruby/blob/0b94677b368947b64dcdcb312cd81ba946df3676/2.3/Dockerfile + +# skip installing gem documentation +RUN mkdir -p /usr/local/etc \ + && { \ + echo 'install: --no-document'; \ + echo 'update: --no-document'; \ + } >> /usr/local/etc/gemrc + +ENV RUBY_MAJOR 2.3 +ENV RUBY_VERSION 2.3.1 +ENV RUBY_DOWNLOAD_SHA256 b87c738cb2032bf4920fef8e3864dc5cf8eae9d89d8d523ce0236945c5797dcd +ENV RUBYGEMS_VERSION 2.6.4 + +# some of ruby's build scripts are written in ruby +# we purge this later to make sure our final image uses what we just built +RUN set -ex \ + && buildDeps=' \ + bison \ + libgdbm-dev \ + ruby \ + ' \ + && apt-get update \ + && apt-get install -y --no-install-recommends $buildDeps \ + && rm -rf /var/lib/apt/lists/* \ + && curl -fSL -o ruby.tar.gz "http://cache.ruby-lang.org/pub/ruby/$RUBY_MAJOR/ruby-$RUBY_VERSION.tar.gz" \ + && echo "$RUBY_DOWNLOAD_SHA256 *ruby.tar.gz" | sha256sum -c - \ + && mkdir -p /usr/src/ruby \ + && tar -xzf ruby.tar.gz -C /usr/src/ruby --strip-components=1 \ + && rm ruby.tar.gz \ + && cd /usr/src/ruby \ + && { echo '#define ENABLE_PATH_CHECK 0'; echo; cat file.c; } > file.c.new && mv file.c.new file.c \ + && autoconf \ + && ./configure --disable-install-doc \ + && make -j"$(nproc)" \ + && make install \ + && apt-get purge -y --auto-remove $buildDeps \ + && gem update --system $RUBYGEMS_VERSION \ + && rm -r /usr/src/ruby + +ENV BUNDLER_VERSION 1.12.4 + +RUN gem install bundler --version "$BUNDLER_VERSION" + +# install things globally, for great justice +# and don't create ".bundle" in all our apps +ENV GEM_HOME /usr/local/bundle +ENV BUNDLE_PATH="$GEM_HOME" \ + BUNDLE_BIN="$GEM_HOME/bin" \ + BUNDLE_SILENCE_ROOT_WARNING=1 \ + BUNDLE_APP_CONFIG="$GEM_HOME" +ENV PATH $BUNDLE_BIN:$PATH +RUN mkdir -p "$GEM_HOME" "$BUNDLE_BIN" \ + && chmod 777 "$GEM_HOME" "$BUNDLE_BIN" \ + && mkdir /fpm && curl -L https://github.com/jordansissel/fpm/archive/6e2514df27664912826b4fcd89affa19df0e713b.tar.gz | tar -xz -C /fpm --strip-components 1 && cd /fpm && bundle install && make install && cd .. + +# use fpm commit https://github.com/jordansissel/fpm/commit/6e2514df27664912826b4fcd89affa19df0e713b because of some important unreleased fixes: +# https://github.com/jordansissel/fpm/commit/94be82c0a23c8cd641ab9e60f3eb4a8db445fff0 +# https://github.com/jordansissel/fpm/commit/77b95747b9cc01ca420ee24084a449b3ac19e6d5 + +ENV USE_SYSTEM_FPM true + +# fix error /usr/local/bundle/gems/fpm-1.5.0/lib/fpm/package/freebsd.rb:72:in `encode': "\xE2" from ASCII-8BIT to UTF-8 (Encoding::UndefinedConversionError) +# http://jaredmarkell.com/docker-and-locales/ +# http://askubuntu.com/a/601498 +RUN locale-gen en_US.UTF-8 +ENV LANG en_US.UTF-8 +ENV LANGUAGE en_US:en +ENV LC_ALL en_US.UTF-8 \ No newline at end of file diff --git a/docker/readme.md b/docker/readme.md index 423ce2b2da8..662b6f92597 100644 --- a/docker/readme.md +++ b/docker/readme.md @@ -1,14 +1,17 @@ # Development machine +To build Linux: ```sh docker run --rm -ti -v `pwd`:/project -v `pwd`/node_modules/.linux:/project/node_modules -v ~/.electron:/root/.electron electronuserland/electron-builder ``` -Wine: +To build windows: ```sh docker run --rm -ti -v ${PWD}:/project -v ${PWD##*/}-node-modules:/project/node_modules -v ~/.electron:/root/.electron electronuserland/electron-builder:wine ``` +Consider using `/test.sh` to install npm dependencies and run tests. + # CI Server ```sh @@ -22,6 +25,10 @@ docker build -t electronuserland/electron-builder docker docker build -t electronuserland/electron-builder:wine docker/wine ``` +Or just `npm run docker-images` + +# Notes + * We use [named data volume](https://madcoda.com/2016/03/docker-named-volume-explained/) instead of mounted host directory to store `node_modules` because NPM is unreliable and NPM team [doesn't want to fix it](https://github.com/npm/npm/issues/3565). `${PWD##*/}-node-modules` is used as name of data volume — it is your current directory name (e. g. `foo`) and suffix `-node-modules`. diff --git a/docs/Multi Platform Build.md b/docs/Multi Platform Build.md index fbc2991dd7f..44de072c4b0 100755 --- a/docs/Multi Platform Build.md +++ b/docs/Multi Platform Build.md @@ -25,19 +25,25 @@ To build app in distributable format for Linux on OS X: brew install gnu-tar libicns graphicsmagick ``` +To build rpm: `brew install rpm`. + ## Linux To build app in distributable format for Linux: ``` -sudo apt-get install icnsutils graphicsmagick xz-utils +sudo apt-get install --no-install-recommends -y icnsutils graphicsmagick xz-utils ``` +To build rpm: `sudo apt-get install --no-install-recommends -y rpm`. + +To build pacman: `sudo apt-get install --no-install-recommends -y bsdtar`. + To build app in distributable format for Windows on Linux: * Install Wine (1.8+ is required): ``` sudo add-apt-repository ppa:ubuntu-wine/ppa -y sudo apt-get update - sudo apt-get install wine1.8 -y + sudo apt-get install --no-install-recommends -y wine1.8 ``` * Install [Mono](http://www.mono-project.com/docs/getting-started/install/linux/#usage) (4.2+ is required): @@ -46,18 +52,18 @@ To build app in distributable format for Windows on Linux: sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 3FA7E0328081BFF6A14DA29AA6A19B38D3D831EF echo "deb http://download.mono-project.com/repo/debian wheezy main" | sudo tee /etc/apt/sources.list.d/mono-xamarin.list sudo apt-get update - sudo apt-get install mono-devel ca-certificates-mono -y + sudo apt-get install --no-install-recommends -y mono-devel ca-certificates-mono ``` * Install zip. ``` - apt-get install zip + apt-get install --no-install-recommends -y zip ``` To build app in 32 bit from a machine with 64 bit: ``` -sudo apt-get install -y gcc-multilib g++-multilib +sudo apt-get install --no-install-recommends -y gcc-multilib g++-multilib ``` ### Travis Linux diff --git a/docs/Options.md b/docs/Options.md index ece85f6f0b9..68359cf329d 100644 --- a/docs/Options.md +++ b/docs/Options.md @@ -71,7 +71,7 @@ See all [appdmg options](https://www.npmjs.com/package/appdmg#json-specification | --- | --- | icon | The path to icon, which will be shown when mounted (default: `build/icon.icns`). | background |

The path to background (default: build/background.png if exists). The resolution of this file determines the resolution of the installer window. If background is not specified, use window.size, see [specification](https://github.com/LinusU/node-appdmg#json-specification).

-| target | Target package type: list of `default`, `dmg`, `zip`, `mas`, `7z`. Defaults to `default` (dmg and zip for Squirrel.Mac). +| target | Target package type: list of `default`, `dmg`, `zip`, `mas`, `7z`, `tar.xz`, `tar.gz`, `tar.bz2`, `tar.7z`. Defaults to `default` (dmg and zip for Squirrel.Mac). | identity |

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

| entitlements |

The path to entitlements file for signing the app. build/osx.entitlements will be used if exists (it is a recommended way to set). MAS entitlements is specified in the [.build.mas](#MasBuildOptions-entitlements).

| entitlementsInherit |

The path to child entitlements which inherit the security settings for signing frameworks and bundles of a distribution. build/osx.inherit.entitlements will be used if exists (it is a recommended way to set). Otherwise [default](https://github.com/electron-userland/electron-osx-sign/blob/master/default.darwin.inherit.entitlements).

This option only applies when signing with entitlements provided.

@@ -107,6 +107,7 @@ MAS (Mac Application Store) specific options (in addition to `build.osx`). | vendor | The vendor. Defaults to [author](#AppMetadata-author). | compression | *deb-only.* The compression type, one of `gz`, `bzip2`, `xz` (default: `xz`). | depends | Package dependencies. Defaults to `["libappindicator1", "libnotify-bin"]`. +| target |

Target package type: list of default, deb, rpm, freebsd, pacman, p5p, apk, 7z, zip, tar.xz, tar.gz, tar.bz2, tar.7z. Defaults to default (deb).

Only deb is tested. Feel free to file issues for rpm and other package formats.

## `.directories` diff --git a/package.json b/package.json index 9e98ff3ab9d..917fcdb541c 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,6 @@ "lint": "tslint src/*.ts test/src/*.ts", "pretest": "npm run compile && npm run lint", "test": "node ./test/out/helpers/runTests.js", - "ci-test": "git-lfs pull && npm install && npm prune && npm run test", "semantic-release": "semantic-release pre && npm publish && semantic-release post", "//": "Update wiki if docs changed. Update only if functionalily are generally available (latest release, not next)", "update-wiki": "git subtree split -b wiki --prefix docs/ && git push wiki wiki:master", @@ -70,11 +69,12 @@ "electron-packager-tf": "~7.1.0", "electron-winstaller-fixed": "~2.8.3", "fs-extra-p": "^1.0.1", - "globby": "^4.0.0", + "globby": "^4.1.0", "hosted-git-info": "^2.1.5", "image-size": "^0.5.0", "lodash.template": "^4.2.5", "mime": "^1.3.4", + "pipe-io": "^1.2.1", "progress": "^1.1.8", "progress-stream": "^1.2.0", "read-package-json": "^2.0.4", @@ -105,11 +105,11 @@ "plist": "^1.2.0", "pre-git": "^3.8.4", "semantic-release": "^6.2.2", - "should": "^8.3.1", + "should": "^8.3.2", "ts-babel": "^0.8.6", "tsconfig-glob": "^0.4.3", - "tslint": "3.10.2", - "typescript": "1.9.0-dev.20160515", + "tslint": "3.10.0-dev.2", + "typescript": "1.9.0-dev.20160520-1.0", "whitespace": "^2.0.0" }, "babel": { diff --git a/src/fpmDownload.ts b/src/fpmDownload.ts index ece9d01d704..3d8ff107498 100644 --- a/src/fpmDownload.ts +++ b/src/fpmDownload.ts @@ -1,4 +1,4 @@ -import { statOrNull, spawn, debug, debug7z } from "./util" +import { statOrNull, spawn, debug, debug7zArgs } from "./util" import { writeFile, rename, remove, unlink, emptyDir } from "fs-extra-p" import { download } from "./httpRequest" import { path7za } from "7zip-bin" @@ -55,15 +55,7 @@ async function doDownloadFpm(version: string, osAndArch: string): Promise { - private readonly debOptions: LinuxBuildOptions + private readonly buildOptions: LinuxBuildOptions private readonly packageFiles: Promise> private readonly scriptFiles: Promise> @@ -25,7 +25,7 @@ export class LinuxPackager extends PlatformPackager { constructor(info: BuildInfo) { super(info) - this.debOptions = Object.assign({ + this.buildOptions = Object.assign({ name: this.metadata.name, description: this.metadata.description, }, this.customBuildOptions) @@ -47,6 +47,10 @@ export class LinuxPackager extends PlatformPackager { } } + protected get supportedTargets(): Array { + return ["deb", "rpm", "sh", "freebsd", "pacman", "apk", "p5p"] + } + get platform() { return Platform.LINUX } @@ -75,9 +79,9 @@ export class LinuxPackager extends PlatformPackager { private async computeDesktop(tempDir: string): Promise> { const tempFile = path.join(tempDir, this.appName + ".desktop") - await outputFile(tempFile, this.debOptions.desktop || `[Desktop Entry] + await outputFile(tempFile, this.buildOptions.desktop || `[Desktop Entry] Name=${this.appName} -Comment=${this.debOptions.description} +Comment=${this.buildOptions.description} Exec="${installPrefix}/${this.appName}/${this.appName}" Terminal=false Type=Application @@ -168,26 +172,27 @@ Icon=${this.metadata.name} const templateOptions = Object.assign({ // old API compatibility executable: this.appName, - }, this.debOptions) + }, this.buildOptions) - const afterInstallTemplate = this.debOptions.afterInstall || path.join(defaultTemplatesDir, "after-install.tpl") + const afterInstallTemplate = this.buildOptions.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 afterRemoveTemplate = this.buildOptions.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)) + packageInDistributableFormat(outDir: string, appOutDir: string, arch: string): Promise { + return BluebirdPromise.map(this.targets, (target): any => { + target = target === "default" ? "deb" : target + const destination = path.join(outDir, `${this.metadata.name}-${this.metadata.version}${archSuffix(arch)}.${target}`) + return (target === "zip" || target === "7z" || target.startsWith("tar.") ? this.archiveApp(target, appOutDir, destination) : this.buildPackage(destination, target, this.buildOptions, appOutDir, arch)) + .then(() => this.dispatchArtifactCreated(destination)) + }) } - private async buildDeb(options: LinuxBuildOptions, outDir: string, appOutDir: string, arch: string): Promise { - const archName = arch === "ia32" ? "i386" : "amd64" - const target = "deb" - const destination = path.join(outDir, `${this.metadata.name}-${this.metadata.version}-${archName}.${target}`) + private async buildPackage(destination: string, target: string, options: LinuxBuildOptions, appOutDir: string, arch: string): Promise { const scripts = await this.scriptFiles const projectUrl = await this.computePackageUrl() @@ -196,24 +201,35 @@ Icon=${this.metadata.name} } const author = options.maintainer || `${this.metadata.author.name} <${this.metadata.author.email}>` + const synopsis = options.synopsis const args = [ "-s", "dir", "-t", target, - "--architecture", archName, - "--rpm-os", "linux", + "--architecture", arch === "ia32" ? "i386" : "amd64", "--name", this.metadata.name, "--force", "--after-install", scripts[0], "--after-remove", scripts[1], - "--description", `${options.synopsis || ""}\n ${this.debOptions.description}`, + "--description", smarten(target === "rpm" ? this.buildOptions.description! : `${synopsis || ""}\n ${this.buildOptions.description}`), "--maintainer", author, "--vendor", options.vendor || author, "--version", this.metadata.version, "--package", destination, - "--deb-compression", options.compression || (this.devMetadata.build.compression === "store" ? "gz" : "xz"), "--url", projectUrl, ] + if (target === "deb") { + args.push("--deb-compression", options.compression || (this.devMetadata.build.compression === "store" ? "gz" : "xz")) + } + else if (target === "rpm") { + // args.push("--rpm-compression", options.compression || (this.devMetadata.build.compression === "store" ? "none" : "xz")) + args.push("--rpm-os", "linux") + + if (synopsis != null) { + args.push("--rpm-summary", smarten(synopsis)) + } + } + let depends = options.depends if (depends == null) { depends = ["libappindicator1", "libnotify-bin"] @@ -239,7 +255,6 @@ Icon=${this.metadata.name} args.push(`${appOutDir}/=${installPrefix}/${this.appName}`) args.push(...(await this.packageFiles)!) await exec(await this.fpmPath, args) - return destination } } diff --git a/src/metadata.ts b/src/metadata.ts index 81d1ef2cece..19b77f91099 100755 --- a/src/metadata.ts +++ b/src/metadata.ts @@ -170,7 +170,7 @@ export interface OsXBuildOptions extends PlatformSpecificBuildOptions { readonly background?: string | null /* - Target package type: list of `default`, `dmg`, `zip`, `mas`, `7z`. Defaults to `default` (dmg and zip for Squirrel.Mac). + Target package type: list of `default`, `dmg`, `zip`, `mas`, `7z`, `tar.xz`, `tar.gz`, `tar.bz2`, `tar.7z`. Defaults to `default` (dmg and zip for Squirrel.Mac). */ readonly target?: Array | null @@ -261,7 +261,7 @@ export interface WinBuildOptions extends PlatformSpecificBuildOptions { /* ### `.build.linux` */ -export interface LinuxBuildOptions { +export interface LinuxBuildOptions extends PlatformSpecificBuildOptions { /* As [description](#AppMetadata-description) from application package.json, but allows you to specify different for Linux. */ @@ -300,6 +300,13 @@ export interface LinuxBuildOptions { Package dependencies. Defaults to `["libappindicator1", "libnotify-bin"]`. */ readonly depends?: string[] | null + + /* + Target package type: list of `default`, `deb`, `rpm`, `freebsd`, `pacman`, `p5p`, `apk`, `7z`, `zip`, `tar.xz`, `tar.gz`, `tar.bz2`, `tar.7z`. Defaults to `default` (`deb`). + + Only `deb` is tested. Feel free to file issues for `rpm` and other package formats. + */ + readonly target?: Array | null } /* @@ -324,6 +331,8 @@ export interface MetadataDirectories { export interface PlatformSpecificBuildOptions { readonly extraResources?: Array | null + + readonly target?: Array | null } export class Platform { diff --git a/src/osxPackager.ts b/src/osxPackager.ts index 43c17c7894a..b5aa756933c 100644 --- a/src/osxPackager.ts +++ b/src/osxPackager.ts @@ -1,10 +1,9 @@ -import { PlatformPackager, BuildInfo, normalizeTargets } from "./platformPackager" +import { PlatformPackager, BuildInfo } from "./platformPackager" import { Platform, OsXBuildOptions, MasBuildOptions } from "./metadata" import * as path from "path" import { Promise as BluebirdPromise } from "bluebird" -import { log, debug, debug7z, spawn, statOrNull, warn } from "./util" +import { log, debug, statOrNull, warn } from "./util" import { createKeychain, deleteKeychain, CodeSigningInfo, generateKeychainName } from "./codeSign" -import { path7za } from "7zip-bin" import deepAssign = require("deep-assign") import { sign, flat, BaseSignOptions, SignOptions, FlatOptions } from "electron-osx-sign-tf" import { readdir } from "fs-extra-p" @@ -15,8 +14,6 @@ const __awaiter = require("./awaiter") export default class OsXPackager extends PlatformPackager { codeSigningInfo: Promise - readonly targets: Array - readonly resourceList: Promise> constructor(info: BuildInfo, cleanupTasks: Array<() => Promise>) { @@ -31,16 +28,6 @@ export default class OsXPackager extends PlatformPackager { this.codeSigningInfo = BluebirdPromise.resolve(null) } - const targets = normalizeTargets(this.customBuildOptions.target) - if (targets != null) { - for (let target of targets) { - if (target !== "default" && target !== "dmg" && target !== "zip" && target !== "mas" && target !== "7z") { - throw new Error("Unknown target: " + target) - } - } - } - this.targets = targets == null ? ["default"] : targets - this.resourceList = readdir(this.buildResourcesDir) } @@ -48,6 +35,10 @@ export default class OsXPackager extends PlatformPackager { return Platform.OSX } + protected get supportedTargets(): Array { + return ["dmg", "mas"] + } + async pack(outDir: string, arch: string, postAsyncTasks: Array>): Promise { const packOptions = this.computePackOptions(outDir, arch) let nonMasPromise: Promise | null = null @@ -217,46 +208,12 @@ export default class OsXPackager extends PlatformPackager { log("Creating OS X " + format) // for default we use mac to be compatible with Squirrel.Mac const classifier = target === "default" ? "mac" : "osx" - promises.push(this.archiveApp(appOutDir, format, classifier) - .then(it => this.dispatchArtifactCreated(it, `${this.metadata.name}-${this.metadata.version}-${classifier}.${format}`))) + // we use app name here - see https://github.com/electron-userland/electron-builder/pull/204 + const outFile = path.join(appOutDir, `${this.appName}-${this.metadata.version}-${classifier}.${format}`) + promises.push(this.archiveApp(format, appOutDir, outFile) + .then(() => this.dispatchArtifactCreated(outFile, `${this.metadata.name}-${this.metadata.version}-${classifier}.${format}`))) } } return BluebirdPromise.all(promises) } - - private archiveApp(outDir: string, format: string, classifier: string): Promise { - const args = ["a", "-bd"] - if (debug7z.enabled) { - args.push("-bb3") - } - else if (!debug.enabled) { - args.push("-bb0") - } - - const compression = this.devMetadata.build.compression - const storeOnly = compression === "store" - if (format === "zip" || storeOnly) { - args.push("-mm=" + (storeOnly ? "Copy" : "Deflate")) - } - if (compression === "maximum") { - // http://superuser.com/a/742034 - //noinspection SpellCheckingInspection - if (format === "zip") { - args.push("-mfb=258", "-mpass=15") - } - else if (format === "7z") { - args.push("-m0=lzma2", "-mx=9", "-mfb=64", "-md=32m", "-ms=on") - } - } - - // we use app name here - see https://github.com/electron-userland/electron-builder/pull/204 - const resultPath = `${this.appName}-${this.metadata.version}-${classifier}.${format}` - args.push(resultPath, this.appName + ".app") - - return spawn(path7za, args, { - cwd: outDir, - stdio: ["ignore", debug.enabled ? "inherit" : "ignore", "inherit"], - }) - .thenReturn(path.join(outDir, resultPath)) - } } \ No newline at end of file diff --git a/src/platformPackager.ts b/src/platformPackager.ts index 079adccc19e..6b4a0905b85 100644 --- a/src/platformPackager.ts +++ b/src/platformPackager.ts @@ -5,17 +5,20 @@ 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" -import { statOrNull, use } from "./util" +import { copy, unlink } from "fs-extra-p" +import { statOrNull, use, spawn, debug7zArgs, debug, doSpawn, handleProcess } from "./util" import { Packager } from "./packager" import deepAssign = require("deep-assign") +import pipeCallback = require("pipe-io") import { listPackage, statFile } from "asar" import ElectronPackagerOptions = ElectronPackager.ElectronPackagerOptions +import { path7za } from "7zip-bin" //noinspection JSUnusedLocalSymbols const __awaiter = require("./awaiter") const pack = BluebirdPromise.promisify(packager) +const pipe = BluebirdPromise.promisify(pipeCallback) export interface PackagerOptions { arch?: string | null @@ -76,6 +79,8 @@ export abstract class PlatformPackager readonly appName: string + readonly targets: Array + public abstract get platform(): Platform constructor(protected info: BuildInfo) { @@ -87,12 +92,25 @@ export abstract class PlatformPackager this.buildResourcesDir = path.resolve(this.projectDir, this.relativeBuildResourcesDirname) this.customBuildOptions = (info.devMetadata.build)[this.platform.buildConfigurationKey] || Object.create(null) this.appName = getProductName(this.metadata, this.devMetadata) + + const targets = normalizeTargets(this.customBuildOptions.target) + if (targets != null) { + const supportedTargets = this.supportedTargets.concat("default", "zip", "7z", "tar.xz", "tar.7z", "tar.gz", "tar.bz2") + for (let target of targets) { + if (!supportedTargets.includes(target)) { + throw new Error("Unknown target: " + target) + } + } + } + this.targets = targets == null ? ["default"] : targets } protected get relativeBuildResourcesDirname() { return use(this.devMetadata.directories, it => it!.buildResources) || "build" } + protected abstract get supportedTargets(): Array + protected computeAppOutDir(outDir: string, arch: string): string { return path.join(outDir, `${this.appName}-${this.platform.nodeName}-${arch}`) } @@ -136,7 +154,7 @@ export abstract class PlatformPackager tmpdir: false, "version-string": { CompanyName: this.metadata.author.name, - FileDescription: this.metadata.description, + FileDescription: smarten(this.metadata.description), ProductName: this.appName, InternalName: this.appName, } @@ -271,6 +289,73 @@ export abstract class PlatformPackager throw new Error(`Application entry file ${mainFile} could not be found in package. Seems like a wrong configuration.`) } } + + protected async archiveApp(format: string, appOutDir: string, outFile: string): Promise { + const compression = this.devMetadata.build.compression + const storeOnly = compression === "store" + // remove file before - 7z doesn't overwrite file, but update + try { + await unlink(outFile) + } + catch (e) { + // ignore + } + + const args = debug7zArgs("a") + if (compression === "maximum") { + if (format === "7z" || format.endsWith(".7z")) { + args.push("-mx=9", "-mfb=64", "-md=32m", "-ms=on") + } + else if (format === "zip") { + // http://superuser.com/a/742034 + //noinspection SpellCheckingInspection + args.push("-mfb=258", "-mpass=15") + } + else { + args.push("-mx=9") + } + } + else if (storeOnly) { + if (format !== "zip") { + args.push("-mx=1") + } + } + + const fileToArchive = this.platform === Platform.OSX ? path.join(appOutDir, `${this.appName}.app`) : appOutDir + const baseDir = path.dirname(fileToArchive) + if (format.startsWith("tar.")) { + const compressProcess = doSpawn(path7za, args.concat("-si", outFile), {cwd: baseDir, stdio: ["pipe", "ignore", "inherit"]}) + const tarProcess = doSpawn(path7za, debug7zArgs("a").concat("-so", "anyName.tar", fileToArchive), {cwd: baseDir, stdio: ["ignore", "pipe", "inherit"]}) + + // we must not use "inherit" - read/write pipe don't work in this case + // nodejs pipe is very tricky, so, we use hgh-level library + await BluebirdPromise.all([ + pipe([tarProcess.stdout, compressProcess.stdin]), + new BluebirdPromise((resolve, reject) => { + handleProcess("exit", tarProcess, "7za", resolve, reject) + }), + new BluebirdPromise((resolve, reject) => { + handleProcess("exit", compressProcess, "7za", resolve, reject) + }), + ]) + return + } + + if (format === "zip" || storeOnly) { + args.push("-mm=" + (storeOnly ? "Copy" : "Deflate")) + } + + args.push(outFile, fileToArchive) + + await spawn(path7za, args, { + cwd: baseDir, + stdio: ["ignore", debug.enabled ? "inherit" : "ignore", "inherit"], + }) + } +} + +export function archSuffix(arch: string) { + return arch === "x64" ? "" : `-${arch}` } export interface ArtifactCreated { @@ -288,3 +373,17 @@ export function normalizeTargets(targets: Array | string | null | undefi return (Array.isArray(targets) ? targets : [targets]).map(it => it.toLowerCase().trim()) } } + +// fpm bug - rpm build --description is not escaped, well... decided to replace quite to smart quote +// http://leancrew.com/all-this/2010/11/smart-quotes-in-javascript/ +export function smarten(s: string): string { + // opening singles + s = s.replace(/(^|[-\u2014\s(\["])'/g, "$1\u2018") + // closing singles & apostrophes + s = s.replace(/'/g, "\u2019") + // opening doubles + s = s.replace(/(^|[-\u2014/\[(\u2018\s])"/g, "$1\u201c") + // closing doubles + s = s.replace(/"/g, "\u201d") + return s +} \ No newline at end of file diff --git a/src/util.ts b/src/util.ts index 9e869470d9b..6c788e5517a 100644 --- a/src/util.ts +++ b/src/util.ts @@ -92,18 +92,37 @@ export function exec(file: string, args?: Array | null, options?: ExecOp }) } -export function spawn(command: string, args?: Array | null, options?: SpawnOptions, processConsumer?: (it: ChildProcess, reject: (error: Error) => void) => void): BluebirdPromise { - const notNullArgs = args || [] +export function doSpawn(command: string, args: Array, options?: SpawnOptions): ChildProcess { if (debug.enabled) { - debug(`Spawning ${command} ${notNullArgs.join(" ")}`) + debug(`Spawning ${command} ${args.join(" ")}`) } + const childProcess = _spawn(command, args, options) + if (debug.enabled) { + debug(`Spawned (${childProcess.pid}) ${command} ${args.join(" ")}`) + } + return childProcess +} +export function spawn(command: string, args?: Array | null, options?: SpawnOptions): BluebirdPromise { return new BluebirdPromise((resolve, reject) => { - const p = _spawn(command, notNullArgs, options) - p.on("error", reject) - p.on("close", (code: number) => code === 0 ? resolve() : reject(new Error(command + " exited with code " + code))) - if (processConsumer != null) { - processConsumer(p, reject) + const notNullArgs = args || [] + const childProcess = doSpawn(command, notNullArgs, options) + handleProcess("close", childProcess, command, resolve, reject) + }) +} + +export function handleProcess(event: string, childProcess: ChildProcess, command: string, resolve: ((value?: any) => void) | null, reject: (reason?: any) => void) { + childProcess.on("error", reject) + childProcess.on(event, (code: number) => { + if (debug.enabled) { + debug(`${command} (${childProcess.pid}) exited with code ${code}`) + } + + if (code !== 0) { + reject(new Error(`${command} exited with code ${code}`)) + } + else if (resolve != null) { + resolve() } }) } @@ -172,4 +191,15 @@ export async function computeDefaultAppDirectory(projectDir: string, userAppDir: export function use(value: T | null, task: (it: T) => R): R | null { return value == null ? null : task(value) +} + +export function debug7zArgs(command: "a" | "x"): Array { + const args = [command, "-bd"] + if (debug7z.enabled) { + args.push("-bb3") + } + else if (!debug.enabled) { + args.push("-bb0") + } + return args } \ No newline at end of file diff --git a/src/winPackager.ts b/src/winPackager.ts index 5e0070b950e..dbbcc200d19 100644 --- a/src/winPackager.ts +++ b/src/winPackager.ts @@ -1,6 +1,6 @@ import { downloadCertificate } from "./codeSign" import { Promise as BluebirdPromise } from "bluebird" -import { PlatformPackager, BuildInfo } from "./platformPackager" +import { PlatformPackager, BuildInfo, smarten, archSuffix } from "./platformPackager" import { Platform, WinBuildOptions } from "./metadata" import * as path from "path" import { log, statOrNull, warn } from "./util" @@ -45,6 +45,10 @@ export class WinPackager extends PlatformPackager { return Platform.WINDOWS } + protected get supportedTargets(): Array { + return [] + } + private async getValidIconPath(): Promise { const iconPath = path.join(this.buildResourcesDir, "icon.ico") await checkIcon(iconPath) @@ -137,7 +141,7 @@ export class WinPackager extends PlatformPackager { appDirectory: appOutDir, outputDirectory: installerOutDir, version: this.metadata.version, - description: this.metadata.description, + description: smarten(this.metadata.description), authors: this.metadata.author.name, iconUrl: iconUrl, setupIcon: await this.iconPath, @@ -235,7 +239,7 @@ function isIco(buffer: Buffer): boolean { } export function computeDistOut(outDir: string, arch: string): string { - return path.join(outDir, `win${arch === "x64" ? "" : `-${arch}` }`) + return path.join(outDir, `win${archSuffix(arch)}`) } function checkConflictingOptions(options: any) { diff --git a/test/fixtures/test-app-one/index.js b/test/fixtures/test-app-one/index.js index 11a4e82d04c..dd2fbe001c6 100644 --- a/test/fixtures/test-app-one/index.js +++ b/test/fixtures/test-app-one/index.js @@ -1,6 +1,6 @@ 'use strict' -const app = require('app') +const app = require('electron').app // this should be placed at top of main.js to handle setup events quickly if (handleSquirrelEvent()) { diff --git a/test/install-linux-dependencies.sh b/test/install-linux-dependencies.sh index 2544d643b20..98c0f1cd2e4 100755 --- a/test/install-linux-dependencies.sh +++ b/test/install-linux-dependencies.sh @@ -3,4 +3,4 @@ wget -q -O - https://dl.google.com/linux/linux_signing_key.pub | sudo apt-key ad sudo dpkg --add-architecture i386 sudo add-apt-repository ppa:ubuntu-wine/ppa -y sudo apt-get update -sudo apt-get install wine1.8 ca-certificates-mono -y \ No newline at end of file +sudo apt-get install --no-install-recommends wine1.8 ca-certificates-mono -y \ No newline at end of file diff --git a/test/src/helpers/avaEx.ts b/test/src/helpers/avaEx.ts index b3e88200e48..e5caa3b3166 100644 --- a/test/src/helpers/avaEx.ts +++ b/test/src/helpers/avaEx.ts @@ -7,6 +7,8 @@ declare module "ava-tf" { export const ifNotCi: typeof test; export const ifNotCiOsx: typeof test; export const ifDevOrWinCi: typeof test; + export const ifWinCi: typeof test; + export const ifDevOrLinuxCi: typeof test; export const ifNotTravis: typeof test; } @@ -45,6 +47,16 @@ Object.defineProperties(test, { get: function () { return !process.env.CI || process.platform === "win32" ? this : this.skip } + }, + "ifDevOrLinuxCi": { + get: function () { + return !process.env.CI || process.platform === "linux" ? this : this.skip + } + }, + "ifWinCi": { + get: function () { + return process.env.CI && process.platform === "win32" ? this : this.skip + } } }) diff --git a/test/src/helpers/packTester.ts b/test/src/helpers/packTester.ts index 1b35ca82341..30830490734 100755 --- a/test/src/helpers/packTester.ts +++ b/test/src/helpers/packTester.ts @@ -106,7 +106,7 @@ async function packAndCheck(projectDir: string, packagerOptions: PackagerOptions await checkOsXResult(packager, packagerOptions, checkOptions, artifacts.get(Platform.OSX)) } else if (platform === Platform.LINUX) { - await checkLinuxResult(projectDir, packager, packagerOptions, checkOptions) + await checkLinuxResult(projectDir, packager, packagerOptions, checkOptions, artifacts.get(Platform.LINUX)) } else if (platform === Platform.WINDOWS) { await checkWindowsResult(packager, packagerOptions, checkOptions, artifacts.get(Platform.WINDOWS)) @@ -114,7 +114,24 @@ async function packAndCheck(projectDir: string, packagerOptions: PackagerOptions } } -async function checkLinuxResult(projectDir: string, packager: Packager, packagerOptions: PackagerOptions, checkOptions: AssertPackOptions) { +async function checkLinuxResult(projectDir: string, packager: Packager, packagerOptions: PackagerOptions, checkOptions: AssertPackOptions, artifacts: Array) { + const customBuildOptions = packager.devMetadata.build.linux + const targets = customBuildOptions == null || customBuildOptions.target == null ? ["default"] : customBuildOptions.target + + function getExpected(): Array { + const result: Array = [] + for (let target of targets) { + result.push(`TestApp-1.1.0.${target === "default" ? "deb" : target}`) + } + return result + } + + assertThat(getFileNames(artifacts)).deepEqual((checkOptions == null || checkOptions.expectedArtifacts == null ? getExpected() : checkOptions.expectedArtifacts.slice()).sort()) + + if (!targets.includes("deb") || !targets.includes("default")) { + return + } + const productName = getProductName(packager.metadata, packager.devMetadata) const expectedContents = expectedLinuxContents.map(it => { if (it === "/opt/TestApp/TestApp") { @@ -143,7 +160,7 @@ async function checkLinuxResult(projectDir: string, packager: Packager, packager Maintainer: "Foo Bar ", Vendor: "Foo Bar ", Package: "testapp", - Description: " \n Test Application (test quite \" #378)", + Description: " \n Test Application (test quite “ #378)", Depends: checkOptions == null || checkOptions.expectedDepends == null ? "libappindicator1, libnotify-bin" : checkOptions.expectedDepends, }) } @@ -200,31 +217,36 @@ async function checkOsXResult(packager: Packager, packagerOptions: PackagerOptio } } +function getFileNames(list: Array): Array { + return list.map(it => path.basename(it.file)).sort() +} + async function checkWindowsResult(packager: Packager, packagerOptions: PackagerOptions, checkOptions: AssertPackOptions, artifacts: Array) { const productName = getProductName(packager.metadata, packager.devMetadata) - function getWinExpected(archSuffix: string) { - return [ + function getExpectedFileNames(archSuffix: string) { + const result = [ `RELEASES`, `${productName} Setup 1.1.0${archSuffix}.exe`, - `TestApp-1.1.0${archSuffix}-full.nupkg`, + `TestApp-1.1.0-full.nupkg`, ] + const buildOptions = packager.devMetadata.build.win + if (buildOptions != null && buildOptions.remoteReleases != null) { + result.push(`${productName}-1.1.0-delta.nupkg`) + } + return result } - const archSuffix = (packagerOptions.arch || process.arch) === "x64" ? "" : "-ia32" - const expected = checkOptions == null || checkOptions.expectedArtifacts == null ? (archSuffix == "" ? getWinExpected(archSuffix) : getWinExpected(archSuffix).concat(getWinExpected(""))) : checkOptions.expectedArtifacts - const filenames = artifacts.map(it => path.basename(it.file)) - assertThat(filenames.slice().sort()).deepEqual(expected.slice().sort()) + const archSuffix = (packagerOptions.arch || process.arch) === "x64" ? "" : "-ia32" + assertThat(getFileNames(artifacts)).deepEqual((checkOptions == null || checkOptions.expectedArtifacts == null ? getExpectedFileNames(archSuffix) : checkOptions.expectedArtifacts.slice()).sort()) if (checkOptions != null && checkOptions.expectedArtifacts != null) { return } - const expectedArtifactNames = expected.slice() - expectedArtifactNames[1] = `TestAppSetup-1.1.0${archSuffix}.exe` assertThat(artifacts.map(it => it.artifactName).filter(it => it != null)).deepEqual([`TestApp-Setup-1.1.0${archSuffix}.exe`]) - const packageFile = path.join(path.dirname(artifacts[0].file), `TestApp-1.1.0${archSuffix}-full.nupkg`) + const packageFile = path.join(path.dirname(artifacts[0].file), `TestApp-1.1.0-full.nupkg`) const unZipper = new DecompressZip(packageFile) const fileDescriptors = await unZipper.getFiles() @@ -257,7 +279,7 @@ async function checkWindowsResult(packager: Packager, packagerOptions: PackagerO Foo Bar https://raw.githubusercontent.com/szwacz/electron-boilerplate/master/resources/windows/icon.ico false - Test Application (test quite \" #378) + Test Application (test quite “ #378) Copyright © ${new Date().getFullYear()} Foo Bar http://foo.example.com diff --git a/test/src/linuxPackagerTest.ts b/test/src/linuxPackagerTest.ts index 1d853c86880..5cae9a796f6 100755 --- a/test/src/linuxPackagerTest.ts +++ b/test/src/linuxPackagerTest.ts @@ -7,7 +7,42 @@ import { Platform } from "out" //noinspection JSUnusedLocalSymbols const __awaiter = require("out/awaiter") -test.ifNotWindows("linux", () => assertPack("test-app-one", platform(Platform.LINUX))) +test.ifNotWindows("deb", () => assertPack("test-app-one", platform(Platform.LINUX))) + +test.ifDevOrLinuxCi("rpm", () => assertPack("test-app-one", { + platform: [Platform.LINUX], + devMetadata: { + build: { + linux: { + target: ["rpm"] + } + } + } +})) + +test.ifDevOrLinuxCi("targets", () => assertPack("test-app-one", { + platform: [Platform.LINUX], + devMetadata: { + build: { + linux: { + // "apk" is very slow, don't test for now + target: ["sh", "freebsd", "pacman", "zip", "7z"], + } + } + } +})) + +test.ifDevOrLinuxCi("tar", () => assertPack("test-app-one", { + platform: [Platform.LINUX], + devMetadata: { + build: { + linux: { + // "apk" is very slow, don't test for now + target: ["tar.gz", "tar.xz", "tar.7z", "tar.bz2"], + } + } + } +})) test.ifNotWindows("icons from ICNS", () => assertPack("test-app-one", { platform: [Platform.LINUX], diff --git a/test/src/winPackagerTest.ts b/test/src/winPackagerTest.ts index 307a97122ec..a6e21b7da13 100755 --- a/test/src/winPackagerTest.ts +++ b/test/src/winPackagerTest.ts @@ -15,17 +15,11 @@ const __awaiter = require("out/awaiter") test.ifNotCiOsx("win", () => assertPack("test-app-one", signed({ platform: [Platform.WINDOWS], arch: "x64", - }), - { - expectedArtifacts: [ - "RELEASES", - "TestApp Setup 1.1.0.exe", - "TestApp-1.1.0-full.nupkg" - ], - } + }) )) -test.ifDevOrWinCi("delta", () => assertPack("test-app-one", { +// very slow +test.ifWinCi("delta", () => assertPack("test-app-one", { platform: [Platform.WINDOWS], arch: "ia32", devMetadata: { @@ -35,14 +29,6 @@ test.ifDevOrWinCi("delta", () => assertPack("test-app-one", { } } }, - }, - { - expectedArtifacts: [ - "RELEASES", - "TestApp Setup 1.1.0-ia32.exe", - "TestApp-1.1.0-delta.nupkg", - "TestApp-1.1.0-full.nupkg" - ], } )) diff --git a/test/tsconfig.json b/test/tsconfig.json index 5ce8a55cbd4..6a1a24342fc 100755 --- a/test/tsconfig.json +++ b/test/tsconfig.json @@ -44,6 +44,7 @@ "../typings/main/definitions/debug/index.d.ts", "../typings/main/definitions/source-map-support/source-map-support.d.ts", "../typings/node.d.ts", + "../typings/pipe-io.d.ts", "../typings/progress-stream.d.ts", "../typings/read-package-json.d.ts", "../typings/signcode.d.ts", diff --git a/tsconfig.json b/tsconfig.json index 92150df1651..c7216ec1b21 100755 --- a/tsconfig.json +++ b/tsconfig.json @@ -49,6 +49,7 @@ "typings/main/definitions/debug/index.d.ts", "typings/main/definitions/source-map-support/source-map-support.d.ts", "typings/node.d.ts", + "typings/pipe-io.d.ts", "typings/progress-stream.d.ts", "typings/read-package-json.d.ts", "typings/signcode.d.ts", diff --git a/typings/node.d.ts b/typings/node.d.ts index 1b24c768aa5..e0e87196746 100644 --- a/typings/node.d.ts +++ b/typings/node.d.ts @@ -5,17 +5,6 @@ // Definitions by: Microsoft TypeScript , DefinitelyTyped // Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped -/************************************************ -* * -* Node.js v4.x API * -* * -************************************************/ - -interface Error { - stack?: string; -} - - // compat for TypeScript 1.8 // if you use with --target es3 or --target es5 and use below definitions, // use the lib.es6.d.ts that is bundled with TypeScript 1.8. diff --git a/typings/pipe-io.d.ts b/typings/pipe-io.d.ts new file mode 100644 index 00000000000..e2fc301df13 --- /dev/null +++ b/typings/pipe-io.d.ts @@ -0,0 +1,8 @@ +declare module "pipe-io" { + import ReadableStream = NodeJS.ReadableStream + import WritableStream = NodeJS.WritableStream + + function pipe(streams: Array, callback: (error?: Error) => void): void + + export = pipe +} \ No newline at end of file