diff --git a/.travis.yml b/.travis.yml index b67443c5f67..06a77544fe6 100755 --- a/.travis.yml +++ b/.travis.yml @@ -1,4 +1,4 @@ -osx_image: xcode7 +osx_image: xcode7.3 matrix: include: @@ -13,7 +13,6 @@ language: c cache: directories: - node_modules - - test/testApp/node_modules - $HOME/.electron - $HOME/.cache/fpm @@ -23,6 +22,7 @@ before_install: install: - nvm install $NODE_VERSION +- nvm use --delete-prefix $NODE_VERSION - if [[ "$TRAVIS_OS_NAME" == "osx" && "$NODE_VERSION" == "4" ]]; then npm install npm -g ; fi - npm install - npm prune diff --git a/docs/Options.md b/docs/Options.md index c5e3c250427..c79669d28e4 100644 --- a/docs/Options.md +++ b/docs/Options.md @@ -54,7 +54,7 @@ Here documented only `electron-builder` specific options: | app-category-type |

*OS X-only.* The application category type, as shown in the Finder via *View -> Arrange by Application Category* when viewing the Applications directory.

For example, app-category-type=public.app-category.developer-tools will set the application category to *Developer Tools*.

Valid values are listed in [Apple’s documentation](https://developer.apple.com/library/ios/documentation/General/Reference/InfoPlistKeyReference/Articles/LaunchServicesKeys.html#//apple_ref/doc/uid/TP40009250-SW8).

| asar |

Whether to package the application’s source code into an archive, using [Electron’s archive format](https://github.com/electron/asar). Defaults to true. Reasons why you may want to disable this feature are described in [an application packaging tutorial in Electron’s documentation](http://electron.atom.io/docs/latest/tutorial/application-packaging/#limitations-on-node-api/).

Or you can pass object of any asar options.

| productName | See [AppMetadata.productName](#AppMetadata-productName). -| files |

A [glob patterns](https://www.npmjs.com/package/glob#glob-primer) relative to the [app directory](#MetadataDirectories-app), which specifies which files to include when copying files to create the package. Defaults to \*\*\/\* (i.e. [hidden files are ignored by default](https://www.npmjs.com/package/glob#dots)).

[Multiple patterns](#multiple-glob-patterns) are supported. You can use ${os} (expanded to osx, linux or win according to current platform) and ${arch} in the pattern.

If directory matched, all contents are copied. So, you can just specify foo to copy foo directory.

Remember that default pattern \*\*\/\* is not added to your custom, so, you have to add it explicitly — e.g. ["\*\*\/\*", "!ignoreMe${/\*}"].

May be specified in the platform options (e.g. in the build.osx).

+| files |

A [glob patterns](https://www.npmjs.com/package/glob#glob-primer) relative to the [app directory](#MetadataDirectories-app), which specifies which files to include when copying files to create the package. Defaults to \*\*\/\* (i.e. [hidden files are ignored by default](https://www.npmjs.com/package/glob#dots)).

Development dependencies are never copied in any case. You don’t need to ignore it explicitly.

[Multiple patterns](#multiple-glob-patterns) are supported. You can use ${os} (expanded to osx, linux or win according to current platform) and ${arch} in the pattern. If directory matched, all contents are copied. So, you can just specify foo to copy foo directory.

Remember that default pattern \*\*\/\* is not added to your custom, so, you have to add it explicitly — e.g. ["\*\*\/\*", "!ignoreMe${/\*}"].

May be specified in the platform options (e.g. in the build.osx).

| extraResources |

A [glob patterns](https://www.npmjs.com/package/glob#glob-primer) relative to the project directory, when specified, copy the file or directory with matching names directly into the app’s resources directory (Contents/Resources for OS X, resources for Linux/Windows).

Glob rules the same as for [files](#BuildMetadata-files).

| extraFiles | The same as [extraResources](#BuildMetadata-extraResources) but copy into the app's content directory (`Contents` for OS X, root directory for Linux/Windows). | osx | See [.build.osx](#OsXBuildOptions). @@ -63,8 +63,6 @@ Here documented only `electron-builder` specific options: | linux | See [.build.linux](#LinuxBuildOptions). | compression | The compression level, one of `store`, `normal`, `maximum` (default: `normal`). If you want to rapidly test build, `store` can reduce build time significantly. | afterPack | *programmatic API only* The function to be run after pack (but before pack into distributable format and sign). Promise must be returned. -| npmPrune |

Whether to [prune](https://docs.npmjs.com/cli/prune) native dependencies (npm prune --production) before starting to package the app. Defaults to true if [two package.json structure](https://github.com/electron-userland/electron-builder#two-packagejson-structure) is not used.

-| npmRebuild | Whether to [rebuild](https://docs.npmjs.com/cli/rebuild) native dependencies (`npm rebuild`) before starting to package the app. Defaults to `true`. ### `.build.osx` diff --git a/package.json b/package.json index a071ce6462e..7c458049527 100644 --- a/package.json +++ b/package.json @@ -107,7 +107,7 @@ "pre-git": "^3.8.4", "semantic-release": "^6.3.0", "should": "^9.0.0", - "ts-babel": "^1.0.0", + "ts-babel": "^1.0.2", "tsconfig-glob": "^0.4.3", "tslint": "3.10.0-dev.2", "typescript": "1.9.0-dev.20160607-1.0", diff --git a/src/index.ts b/src/index.ts index 0ff481389b9..3779277901f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,4 +2,4 @@ export { Packager } from "./packager" export { PackagerOptions, ArtifactCreated, DIR_TARGET, BuildInfo } from "./platformPackager" export { BuildOptions, build, createPublisher, CliOptions, createTargets } from "./builder" export { PublishOptions, Publisher } from "./gitHubPublisher" -export { AppMetadata, DevMetadata, Platform, Arch, archFromString, getProductName, BuildMetadata, OsXBuildOptions, WinBuildOptions, LinuxBuildOptions } from "./metadata" \ No newline at end of file +export { AppMetadata, DevMetadata, Platform, Arch, archFromString, getProductName, BuildMetadata, OsXBuildOptions, WinBuildOptions, LinuxBuildOptions, CompressionLevel } from "./metadata" \ No newline at end of file diff --git a/src/metadata.ts b/src/metadata.ts index 34c3251a051..06c7fa912a3 100755 --- a/src/metadata.ts +++ b/src/metadata.ts @@ -74,6 +74,8 @@ export interface AuthorMetadata { readonly email: string } +export type CompressionLevel = "store" | "normal" | "maximum" + /* ## `.build` */ @@ -156,7 +158,7 @@ export interface BuildMetadata { /* The compression level, one of `store`, `normal`, `maximum` (default: `normal`). If you want to rapidly test build, `store` can reduce build time significantly. */ - readonly compression?: "store" | "normal" | "maximum" | null + readonly compression?: CompressionLevel | null readonly "build-version"?: string | null @@ -171,7 +173,7 @@ export interface BuildMetadata { // */ // readonly npmPrune?: boolean // deprecated - readonly prune?: boolean + // readonly prune?: boolean /* Whether to [rebuild](https://docs.npmjs.com/cli/rebuild) native dependencies (`npm rebuild`) before starting to package the app. Defaults to `true`. diff --git a/src/platformPackager.ts b/src/platformPackager.ts index e256475417c..262bf124c6c 100644 --- a/src/platformPackager.ts +++ b/src/platformPackager.ts @@ -4,11 +4,11 @@ import EventEmitter = NodeJS.EventEmitter import { Promise as BluebirdPromise } from "bluebird" import * as path from "path" import { pack, ElectronPackagerOptions, userIgnoreFilter } from "electron-packager-tf" -import { readdir, copy, unlink, lstat, remove } from "fs-extra-p" -import { statOrNull, use, spawn, debug7zArgs, debug, warn, log, spawnNpmProduction } from "./util" +import { readdir, copy, unlink, lstat, remove, realpath } from "fs-extra-p" +import { statOrNull, use, warn, log, exec } from "./util" import { Packager } from "./packager" import { listPackage, statFile, AsarFileMetadata, createPackageFromFiles, AsarOptions } from "asar" -import { path7za } from "7zip-bin" +import { archiveApp } from "./targets/archive" import { Glob } from "glob" import { Minimatch } from "minimatch" import deepAssign = require("deep-assign") @@ -16,18 +16,6 @@ import deepAssign = require("deep-assign") //noinspection JSUnusedLocalSymbols const __awaiter = require("./awaiter") -class CompressionDescriptor { - constructor(public flag: string, public env: string, public minLevel: string, public maxLevel: string = "-9") { - } -} - -const extToCompressionDescriptor: { [key: string]: CompressionDescriptor; } = { - "tar.xz": new CompressionDescriptor("--xz", "XZ_OPT", "-0", "-9e"), - "tar.lz": new CompressionDescriptor("--lzip", "LZOP", "-0"), - "tar.gz": new CompressionDescriptor("--gz", "GZIP", "-1"), - "tar.bz2": new CompressionDescriptor("--bzip2", "BZIP2", "-1"), -} - export const commonTargets = ["dir", "zip", "7z", "tar.xz", "tar.lz", "tar.gz", "tar.bz2"] export const DIR_TARGET = "dir" @@ -168,18 +156,32 @@ export abstract class PlatformPackager promise = copy(this.info.appDir, appPath, {filter: userIgnoreFilter(opts), dereference: true}) } else { + const ignoreFiles = new Set([path.relative(this.info.appDir, opts.out!), path.relative(this.info.appDir, this.buildResourcesDir)]) + if (!this.info.isTwoPackageJsonProjectLayoutUsed) { + const result = await BluebirdPromise.all([listDependencies(this.info.appDir, false), listDependencies(this.info.appDir, true)]) + const productionDepsSet = new Set(result[1]) + + // npm returns real path, so, we should use relative path to avoid any mismatch + const realAppDirPath = await realpath(this.info.appDir) + + for (let it of result[0]) { + if (!productionDepsSet.has(it)) { + if (it.startsWith(realAppDirPath)) { + it = it.substring(realAppDirPath.length + 1) + } + else if (it.startsWith(this.info.appDir)) { + it = it.substring(this.info.appDir.length + 1) + } + ignoreFiles.add(it) + } + } + } + let patterns = this.getFilePatterns("files", customBuildOptions) if (patterns == null || patterns.length === 0) { patterns = ["**/*"] } - - const parsedPatterns = this.getParsedPatterns(patterns, arch) - if (!this.info.isTwoPackageJsonProjectLayoutUsed) { - const dotOptions = {dot: true} - parsedPatterns.push(new Minimatch("!node_modules/@(appdmg|electron-download|electron-builder|electron-prebuilt|electron-packager-tf|electron-winstaller-fixed|electron-osx-sign-tf|electron-osx-sign){,/**/*}", dotOptions)) - parsedPatterns.push(new Minimatch(`!@(${path.relative(this.info.appDir, this.buildResourcesDir)}|${path.relative(this.info.appDir, opts.out!)}){,/**/*}`, dotOptions)) - } - promise = copyFiltered(this.info.appDir, appPath, parsedPatterns, true) + promise = copyFiltered(this.info.appDir, appPath, this.getParsedPatterns(patterns, arch), true, ignoreFiles) } const promises = [promise] @@ -193,24 +195,8 @@ export abstract class PlatformPackager await BluebirdPromise.all(promises) - let npmPrune = this.devMetadata.build.npmPrune - if (npmPrune == null) { - npmPrune = this.devMetadata.build.prune - if (npmPrune != null) { - warn("prune is deprecated and renamed to npmPrune, please specify as npmPrune") - } - } - - if (npmPrune == null) { - npmPrune = !this.info.isTwoPackageJsonProjectLayoutUsed - } - else if (typeof npmPrune !== "boolean") { - throw new Error(`npmPrune expected to be boolean value, but string '"${npmPrune}"' was specified`) - } - - if (npmPrune) { - log("Pruning app dependencies") - await spawnNpmProduction("prune", appPath) + if (opts.prune != null) { + warn("prune is deprecated — development dependencies are never copied in any case") } if (asarOptions != null) { @@ -469,66 +455,7 @@ export abstract class PlatformPackager } protected async archiveApp(format: string, appOutDir: string, outFile: string): Promise { - const compression = this.devMetadata.build.compression - const storeOnly = compression === "store" - - const dirToArchive = this.platform === Platform.OSX ? path.join(appOutDir, `${this.appName}.app`) : appOutDir - if (format.startsWith("tar.")) { - // we don't use 7z here - develar: I spent a lot of time making pipe working - but it works on OS X and often hangs on Linux (even if use pipe-io lib) - // and in any case it is better to use system tools (in the light of docker - it is not problem for user because we provide complete docker image). - const info = extToCompressionDescriptor[format] - let tarEnv = process.env - if (compression != null && compression !== "normal") { - tarEnv = Object.assign({}, process.env) - tarEnv[info.env] = storeOnly ? info.minLevel : info.maxLevel - } - - await spawn(process.platform === "darwin" || process.platform === "freebsd" ? "gtar" : "tar", [info.flag, "--transform", `s,^\.,${path.basename(outFile, "." + format)},`, "-cf", outFile, "."], { - cwd: dirToArchive, - stdio: ["ignore", debug.enabled ? "inherit" : "ignore", "inherit"], - env: tarEnv - }) - return - } - - 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") - } - } - - // remove file before - 7z doesn't overwrite file, but update - try { - await unlink(outFile) - } - catch (e) { - // ignore - } - - if (format === "zip" || storeOnly) { - args.push("-mm=" + (storeOnly ? "Copy" : "Deflate")) - } - - args.push(outFile, dirToArchive) - - await spawn(path7za, args, { - cwd: path.dirname(dirToArchive), - stdio: ["ignore", debug.enabled ? "inherit" : "ignore", "inherit"], - }) + return archiveApp(this.devMetadata.build.compression, format, outFile, this.platform === Platform.OSX ? path.join(appOutDir, `${this.appName}.app`) : appOutDir) } } @@ -592,15 +519,21 @@ function minimatchAll(path: string, patterns: Array): boolean { return match } -function copyFiltered(src: string, destination: string, patterns: Array, dereference: boolean = false): Promise { +// we use relative path to avoid canonical path issue - e.g. /tmp vs /private/tmp +function copyFiltered(src: string, destination: string, patterns: Array, dereference: boolean = false, ignoreFiles?: Set): Promise { return copy(src, destination, { dereference: dereference, filter: it => { if (src === it) { return true } - let relative = it.substring(src.length + 1) + + // yes, check before path sep normalization + if (ignoreFiles != null && ignoreFiles.has(relative)) { + return false + } + if (path.sep === "\\") { relative = relative.replace(/\\/g, "/") } @@ -612,4 +545,29 @@ function copyFiltered(src: string, destination: string, patterns: Array, targetsFromMetadata: Array | n): Array { let targets = normalizeTargets(rawList.length === 0 ? targetsFromMetadata : rawList) return targets == null ? ["default"] : targets +} + +async function listDependencies(appDir: string, production: boolean): Promise> { + let npmExecPath = process.env.npm_execpath || process.env.NPM_CLI_JS + const npmExecArgs = ["ls", production ? "--production" : "--dev", "--parseable"] + if (npmExecPath == null) { + npmExecPath = process.platform === "win32" ? "npm.cmd" : "npm" + } + else { + npmExecArgs.unshift(npmExecPath) + npmExecPath = process.env.npm_node_execpath || process.env.NODE_EXE || "node" + } + + const result = (await exec(npmExecPath, npmExecArgs, { + cwd: appDir, + stdio: "inherit", + maxBuffer: 1024 * 1024, + })).trim().split("\n") + if (result.length > 0 && !result[0].includes("/node_modules/")) { + // first line is a project dir + const lastIndex = result.length - 1 + result[0] = result[lastIndex] + result.length = result.length - 1 + } + return result } \ No newline at end of file diff --git a/src/targets/archive.ts b/src/targets/archive.ts new file mode 100644 index 00000000000..e3bec2a4b3b --- /dev/null +++ b/src/targets/archive.ts @@ -0,0 +1,81 @@ +import { spawn, debug, debug7zArgs } from "../util" +import { CompressionLevel } from "../metadata" +import * as path from "path" +import { unlink } from "fs-extra-p" +import { path7za } from "7zip-bin" + +//noinspection JSUnusedLocalSymbols +const __awaiter = require("../awaiter") + +class CompressionDescriptor { + constructor(public flag: string, public env: string, public minLevel: string, public maxLevel: string = "-9") { + } +} + +const extToCompressionDescriptor: { [key: string]: CompressionDescriptor; } = { + "tar.xz": new CompressionDescriptor("--xz", "XZ_OPT", "-0", "-9e"), + "tar.lz": new CompressionDescriptor("--lzip", "LZOP", "-0"), + "tar.gz": new CompressionDescriptor("--gz", "GZIP", "-1"), + "tar.bz2": new CompressionDescriptor("--bzip2", "BZIP2", "-1"), +} + +export async function archiveApp(compression: CompressionLevel | n, format: string, outFile: string, dirToArchive: string): Promise { + const storeOnly = compression === "store" + + if (format.startsWith("tar.")) { + // we don't use 7z here - develar: I spent a lot of time making pipe working - but it works on OS X and often hangs on Linux (even if use pipe-io lib) + // and in any case it is better to use system tools (in the light of docker - it is not problem for user because we provide complete docker image). + const info = extToCompressionDescriptor[format] + let tarEnv = process.env + if (compression != null && compression !== "normal") { + tarEnv = Object.assign({}, process.env) + tarEnv[info.env] = storeOnly ? info.minLevel : info.maxLevel + } + + await spawn(process.platform === "darwin" || process.platform === "freebsd" ? "gtar" : "tar", [info.flag, "--transform", `s,^\.,${path.basename(outFile, "." + format)},`, "-cf", outFile, "."], { + cwd: dirToArchive, + stdio: ["ignore", debug.enabled ? "inherit" : "ignore", "inherit"], + env: tarEnv + }) + return + } + + 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") + } + } + + // remove file before - 7z doesn't overwrite file, but update + try { + await unlink(outFile) + } + catch (e) { + // ignore + } + + if (format === "zip" || storeOnly) { + args.push("-mm=" + (storeOnly ? "Copy" : "Deflate")) + } + + args.push(outFile, dirToArchive) + + await spawn(path7za, args, { + cwd: path.dirname(dirToArchive), + stdio: ["ignore", debug.enabled ? "inherit" : "ignore", "inherit"], + }) +} \ No newline at end of file diff --git a/test/src/globTest.ts b/test/src/globTest.ts index c8e1a8f2188..12774726fa1 100644 --- a/test/src/globTest.ts +++ b/test/src/globTest.ts @@ -58,7 +58,14 @@ test.ifDevOrLinuxCi("ignore node_modules known dev dep", () => { } }, { tempDirCreated: projectDir => { - return outputFile(path.join(projectDir, "node_modules", "electron-osx-sign", "foo.js"), "") + return BluebirdPromise.all([ + modifyPackageJson(projectDir, data => { + data.devDependencies = Object.assign({ + "electron-osx-sign": "*", + }, data.devDependencies) + }), + outputFile(path.join(projectDir, "node_modules", "electron-osx-sign", "package.json"), "{}"), + ]) }, packed: projectDir => { return assertThat(path.join(projectDir, outDirName, "linux", "resources", "app", "node_modules", "electron-osx-sign")).doesNotExist() diff --git a/tsconfig.json b/tsconfig.json index b70f50b329d..ab9d4e96589 100755 --- a/tsconfig.json +++ b/tsconfig.json @@ -77,6 +77,7 @@ "src/platformPackager.ts", "src/promise.ts", "src/repositoryInfo.ts", + "src/targets/archive.ts", "src/targets/squirrelWindows.ts", "src/util.ts", "src/winPackager.ts"