diff --git a/docs/Options.md b/docs/Options.md index 9cb14152f73..c5e3c250427 100644 --- a/docs/Options.md +++ b/docs/Options.md @@ -54,15 +54,17 @@ 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). -| extraResources |

A [glob expression](https://www.npmjs.com/package/glob#glob-primer), 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). [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 <project_dir>/foo directory.

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

-| extraFiles | The same as [extraResources](#BuildMetadata-extraResources) but copy into the app's content directory (`Contents` for OS X, `` for Linux/Windows). +| 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).

+| 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). | mas | See [.build.mas](#MasBuildOptions). | win | See [.build.win](#LinuxBuildOptions). | 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. -| npmRebuild | Whether to rebuild native dependencies (`npm rebuild`) before starting to package the app. Defaults to `true`. +| 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` @@ -134,4 +136,9 @@ MAS (Mac Application Store) specific options (in addition to `build.osx`). // unless it's foo/bar.js "foo/bar.js", ] - ``` \ No newline at end of file + ``` + +## Excluding directories + +Remember that `!doNotCopyMe/**/*` would match the files *in* the `doNotCopyMe` directory, but not the directory itself, so the [empty directory](https://github.com/gulpjs/gulp/issues/165#issuecomment-32613179) would be created. +Solution — use macro `${/*}`, e.g. `!doNotCopyMe${/*}`. \ No newline at end of file diff --git a/package.json b/package.json index 979bfbd5b31..a071ce6462e 100644 --- a/package.json +++ b/package.json @@ -65,9 +65,9 @@ "debug": "^2.2.0", "deep-assign": "^2.0.0", "electron-osx-sign-tf": "0.6.0", - "electron-packager-tf": "~7.3.0", + "electron-packager-tf": "~7.3.2", "electron-winstaller-fixed": "~2.9.6", - "fs-extra-p": "^1.0.1", + "fs-extra-p": "^1.0.2", "glob": "^7.0.3", "hosted-git-info": "^2.1.5", "image-size": "^0.5.0", diff --git a/src/metadata.ts b/src/metadata.ts index 2ec3367ca2f..1632d95571f 100755 --- a/src/metadata.ts +++ b/src/metadata.ts @@ -108,21 +108,29 @@ export interface BuildMetadata { readonly productName?: string | null /** - A [glob expression](https://www.npmjs.com/package/glob#glob-primer), 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). - [Multiple patterns](#multiple-glob-patterns) are supported. + 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)). - You can use `${os}` (expanded to osx, linux or win according to current platform) and `${arch}` in the pattern. + [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. + 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`). */ - readonly extraResources?: Array | null + readonly files?: Array | string | null /** - The same as [extraResources](#BuildMetadata-extraResources) but copy into the app's content directory (`Contents` for OS X, `` for Linux/Windows). + 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). */ - readonly extraFiles?: Array | null + readonly extraResources?: Array | string | null + + /** + The same as [extraResources](#BuildMetadata-extraResources) but copy into the app's content directory (`Contents` for OS X, root directory for Linux/Windows). + */ + readonly extraFiles?: Array | string | null /* See [.build.osx](#OsXBuildOptions). @@ -157,7 +165,15 @@ export interface BuildMetadata { readonly afterPack?: (context: AfterPackContext) => Promise | null /* - Whether to rebuild native dependencies (`npm rebuild`) before starting to package the app. Defaults to `true`. + 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. + */ + readonly npmPrune?: boolean + // deprecated + 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`. */ readonly npmRebuild?: boolean } @@ -346,6 +362,7 @@ export interface MetadataDirectories { } export interface PlatformSpecificBuildOptions { + readonly files?: Array | null readonly extraFiles?: Array | null readonly extraResources?: Array | null diff --git a/src/platformPackager.ts b/src/platformPackager.ts index aee6f6b7b96..5c4e1a8c482 100644 --- a/src/platformPackager.ts +++ b/src/platformPackager.ts @@ -3,15 +3,15 @@ import { AppMetadata, DevMetadata, Platform, PlatformSpecificBuildOptions, getPr import EventEmitter = NodeJS.EventEmitter import { Promise as BluebirdPromise } from "bluebird" import * as path from "path" -import { pack, ElectronPackagerOptions } from "electron-packager-tf" +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 } from "./util" +import { statOrNull, use, spawn, debug7zArgs, debug, warn, log, spawnNpmProduction } from "./util" import { Packager } from "./packager" import { listPackage, statFile, AsarFileMetadata, createPackageFromFiles, AsarOptions } from "asar" import { path7za } from "7zip-bin" -import deepAssign = require("deep-assign") import { Glob } from "glob" import { Minimatch } from "minimatch" +import deepAssign = require("deep-assign") //noinspection JSUnusedLocalSymbols const __awaiter = require("./awaiter") @@ -163,17 +163,77 @@ export abstract class PlatformPackager abstract pack(outDir: string, arch: Arch, targets: Array, postAsyncTasks: Array>): Promise protected async doPack(options: ElectronPackagerOptions, outDir: string, appOutDir: string, arch: Arch, customBuildOptions: DC) { - const asar = options.asar - options.asar = false - await pack(options) - options.asar = asar - const asarOptions = this.computeAsarOptions(customBuildOptions) - if (asarOptions != null) { - await this.createAsarArchive(appOutDir, asarOptions) + options.initializeApp = async (opts, buildDir, appRelativePath) => { + const appPath = path.join(buildDir, appRelativePath) + const resourcesPath = path.dirname(appPath) + + let promise: Promise | null = null + const deprecatedIgnore = (this.devMetadata.build).ignore + if (deprecatedIgnore) { + if (typeof deprecatedIgnore === "function") { + log(`"ignore is specified as function, may be new "files" option will be suit your needs? Please see https://github.com/electron-userland/electron-builder/wiki/Options#BuildMetadata-files`) + } + else { + warn(`"ignore is deprecated, please use "files", see https://github.com/electron-userland/electron-builder/wiki/Options#BuildMetadata-files`) + } + + promise = copy(this.info.appDir, appPath, {filter: userIgnoreFilter(opts), dereference: true}) + } + else { + 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) + } + + const promises = [promise] + if (this.info.electronVersion[0] === "0") { + // electron release >= 0.37.4 - the default_app/ folder is a default_app.asar file + promises.push(remove(path.join(resourcesPath, "default_app.asar")), remove(path.join(resourcesPath, "default_app"))) + } + else { + promises.push(unlink(path.join(resourcesPath, "default_app.asar"))) + } + + 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 (asarOptions != null) { + await this.createAsarArchive(appPath, resourcesPath, asarOptions) + } } + await pack(options) - await this.copyExtraFiles(appOutDir, arch, customBuildOptions) + await this.doCopyExtraFiles(true, appOutDir, arch, customBuildOptions) + await this.doCopyExtraFiles(false, appOutDir, arch, customBuildOptions) const afterPack = this.devMetadata.build.afterPack if (afterPack != null) { @@ -218,14 +278,6 @@ export abstract class PlatformPackager } }, this.devMetadata.build) - if (!this.info.isTwoPackageJsonProjectLayoutUsed && typeof options.ignore !== "function") { - const defaultIgnores = ["/node_modules/electron-builder($|/)", "^/" + path.relative(this.projectDir, this.buildResourcesDir) + "($|/)"] - if (options.ignore != null && !Array.isArray(options.ignore)) { - options.ignore = [options.ignore] - } - options.ignore = options.ignore == null ? defaultIgnores : options.ignore.concat(defaultIgnores) - } - delete options.osx delete options.win delete options.linux @@ -264,16 +316,12 @@ export abstract class PlatformPackager } } - private async createAsarArchive(appOutDir: string, options: AsarOptions): Promise { - const src = path.join(this.getResourcesDir(appOutDir), "app") - + private async createAsarArchive(src: string, resourcesPath: string, options: AsarOptions): Promise { + // dot: true as in the asar by default by we use glob default - do not copy hidden files let glob: Glob | null = null const files = (await new BluebirdPromise>((resolve, reject) => { glob = new Glob("**/*", { cwd: src, - // dot: true as in the asar by default - dot: true, - ignore: "**/.DS_Store", }, (error, matches) => { if (error == null) { resolve(matches) @@ -284,23 +332,39 @@ export abstract class PlatformPackager }) })).map(it => path.join(src, it)) + const metadata: { [key: string]: AsarFileMetadata; } = {} + const stats = await BluebirdPromise.map(files, it => { - // const stat = glob!.statCache[it] - // return stat == null ? lstat(it) : stat - // todo check is it safe to reuse glob stat - return lstat(it) + if (glob!.symlinks[it]) { + // asar doesn't use stat for link + metadata[it] = { + type: "link", + } + } + else if (glob!.cache[it] === "FILE") { + const stat = glob!.statCache[it] + return stat == null ? lstat(it) : stat + } + else { + // asar doesn't use stat for dir + metadata[it] = { + type: "directory", + } + } + return null }) - const metadata: { [key: string]: AsarFileMetadata; } = {} for (let i = 0, n = files.length; i < n; i++) { const stat = stats[i] - metadata[files[i]] = { - type: stat.isFile() ? "file" : (stat.isDirectory() ? "directory" : "link"), - stat: stat, + if (stat != null) { + metadata[files[i]] = { + type: "file", + stat: stat, + } } } - await BluebirdPromise.promisify(createPackageFromFiles)(src, path.join(this.getResourcesDir(appOutDir), "app.asar"), files, metadata, options) + await BluebirdPromise.promisify(createPackageFromFiles)(src, path.join(resourcesPath, "app.asar"), files, metadata, options) await remove(src) } @@ -308,46 +372,38 @@ export abstract class PlatformPackager return pattern .replace(/\$\{arch}/g, Arch[arch]) .replace(/\$\{os}/g, this.platform.buildConfigurationKey) - } - - protected async copyExtraFiles(appOutDir: string, arch: Arch, customBuildOptions: DC): Promise { - await this.doCopyExtraFiles(true, appOutDir, arch, customBuildOptions) - await this.doCopyExtraFiles(false, appOutDir, arch, customBuildOptions) + .replace(/\$\{\/\*}/g, "{,/**/*,/**/.*}") } private async doCopyExtraFiles(isResources: boolean, appOutDir: string, arch: Arch, customBuildOptions: DC): Promise { const base = isResources ? this.getResourcesDir(appOutDir) : this.platform === Platform.OSX ? path.join(appOutDir, `${this.appName}.app`, "Contents") : appOutDir + const patterns = this.getFilePatterns(isResources ? "extraResources" : "extraFiles", customBuildOptions) + return patterns == null || patterns.length === 0 ? null : copyFiltered(this.projectDir, base, this.getParsedPatterns(patterns, arch)) + } - let patterns: Array | n = (this.devMetadata.build)[isResources ? "extraResources" : "extraFiles"] - const platformSpecificPatterns = isResources ? customBuildOptions.extraResources : customBuildOptions.extraFiles - if (platformSpecificPatterns != null) { - patterns = patterns == null ? platformSpecificPatterns : patterns.concat(platformSpecificPatterns) - } - - if (patterns == null) { - return - } - + private getParsedPatterns(patterns: Array, arch: Arch): Array { const minimatchOptions = {} const parsedPatterns: Array = [] for (let i = 0; i < patterns.length; i++) { parsedPatterns[i] = new Minimatch(this.expandPattern(patterns[i], arch), minimatchOptions) } + return parsedPatterns + } - const src = this.projectDir - return copy(src, base, { - filter: (it) => { - if (src === it) { - return true - } + private getFilePatterns(name: "files" | "extraFiles" | "extraResources", customBuildOptions: DC): Array | n { + let patterns: Array | string | n = (this.devMetadata.build)[name] + if (patterns != null && !Array.isArray(patterns)) { + patterns = [patterns] + } - let relative = it.substring(src.length + 1) - if (path.sep === "\\") { - relative = relative.replace(/\\/g, "/") - } - return minimatchAll(relative, parsedPatterns) + let platformSpecificPatterns: Array | string | n = (customBuildOptions)[name] + if (platformSpecificPatterns != null) { + if (!Array.isArray(platformSpecificPatterns)) { + platformSpecificPatterns = [platformSpecificPatterns] } - }) + return patterns == null ? platformSpecificPatterns : Array.from(new Set(patterns.concat(platformSpecificPatterns))) + } + return patterns } protected async computePackageUrl(): Promise { @@ -534,7 +590,8 @@ function minimatchAll(path: string, patterns: Array): boolean { } // partial match — pattern: foo/bar.txt path: foo — we must allow foo - match = pattern.match(path, true) + // use it only for non-negate patterns: const m = new Minimatch("!node_modules/@(electron-download|electron-prebuilt)/**/*", {dot: true }); m.match("node_modules", true) will return false, but must be true + match = pattern.match(path, !pattern.negate) if (!match && !pattern.negate) { const rawPattern = pattern.pattern // 1 - slash @@ -546,4 +603,21 @@ function minimatchAll(path: string, patterns: Array): boolean { } } return match +} + +function copyFiltered(src: string, destination: string, patterns: Array, dereference: boolean = false): Promise { + return copy(src, destination, { + dereference: dereference, + filter: it => { + if (src === it) { + return true + } + + let relative = it.substring(src.length + 1) + if (path.sep === "\\") { + relative = relative.replace(/\\/g, "/") + } + return minimatchAll(relative, patterns) + } + }) } \ No newline at end of file diff --git a/src/util.ts b/src/util.ts index 87cd7f9b039..9fa01925a4c 100644 --- a/src/util.ts +++ b/src/util.ts @@ -25,17 +25,20 @@ const DEFAULT_APP_DIR_NAMES = ["app", "www"] export const readPackageJson = BluebirdPromise.promisify(readPackageJsonAsync) export function installDependencies(appDir: string, electronVersion: string, arch: string = process.arch, command: string = "install"): BluebirdPromise { - log((command === "install" ? "Installing" : "Rebuilding") + " app dependencies for arch %s to %s", arch, appDir) + log(`${(command === "install" ? "Installing" : "Rebuilding")} app dependencies for arch ${arch} to ${appDir}`) const gypHome = path.join(os.homedir(), ".electron-gyp") - const env = Object.assign({}, process.env, { - npm_config_disturl: "https://atom.io/download/atom-shell", - npm_config_target: electronVersion, - npm_config_runtime: "electron", - npm_config_arch: arch, - HOME: gypHome, - USERPROFILE: gypHome, - }) + return spawnNpmProduction(command, appDir, Object.assign({}, process.env, { + npm_config_disturl: "https://atom.io/download/atom-shell", + npm_config_target: electronVersion, + npm_config_runtime: "electron", + npm_config_arch: arch, + HOME: gypHome, + USERPROFILE: gypHome, + }) + ) +} +export function spawnNpmProduction(command: string, appDir: string, env?: any): BluebirdPromise { let npmExecPath = process.env.npm_execpath || process.env.NPM_CLI_JS const npmExecArgs = [command, "--production"] if (npmExecPath == null) { @@ -49,7 +52,7 @@ export function installDependencies(appDir: string, electronVersion: string, arc return spawn(npmExecPath, npmExecArgs, { cwd: appDir, stdio: "inherit", - env: env + env: env || process.env }) } diff --git a/test/src/globTest.ts b/test/src/globTest.ts index f03c3ae31dc..c8e1a8f2188 100644 --- a/test/src/globTest.ts +++ b/test/src/globTest.ts @@ -29,6 +29,43 @@ test.ifDevOrLinuxCi("ignore build resources", () => { }) }) +test.ifDevOrLinuxCi("files", () => { + return assertPack("test-app-one", { + targets: Platform.LINUX.createTarget(DIR_TARGET), + devMetadata: { + build: { + asar: false, + files: ["**/*", "!ignoreMe${/*}"] + } + } + }, { + tempDirCreated: projectDir => { + return outputFile(path.join(projectDir, "ignoreMe", "foo"), "data") + }, + packed: projectDir => { + return assertThat(path.join(projectDir, outDirName, "linux", "resources", "app", "ignoreMe")).doesNotExist() + }, + }) +}) + +test.ifDevOrLinuxCi("ignore node_modules known dev dep", () => { + return assertPack("test-app-one", { + targets: Platform.LINUX.createTarget(DIR_TARGET), + devMetadata: { + build: { + asar: false, + } + } + }, { + tempDirCreated: projectDir => { + return outputFile(path.join(projectDir, "node_modules", "electron-osx-sign", "foo.js"), "") + }, + packed: projectDir => { + return assertThat(path.join(projectDir, outDirName, "linux", "resources", "app", "node_modules", "electron-osx-sign")).doesNotExist() + }, + }) +}) + test("extraResources", async () => { for (let platform of getPossiblePlatforms().keys()) { const osName = platform.buildConfigurationKey diff --git a/typings/asar.d.ts b/typings/asar.d.ts index 41ab7a713e4..024fce4d474 100644 --- a/typings/asar.d.ts +++ b/typings/asar.d.ts @@ -8,7 +8,7 @@ declare module "asar" { interface AsarFileMetadata { type: "file" | "directory" | "link" - stat: Stats + stat?: Stats } interface AsarOptions { diff --git a/typings/electron-packager.d.ts b/typings/electron-packager.d.ts index fd87b0609fb..f16b806f0b0 100644 --- a/typings/electron-packager.d.ts +++ b/typings/electron-packager.d.ts @@ -62,6 +62,8 @@ declare module "electron-packager-tf" { "app-copyright"?: string generateFinalBasename?: (context: any) => void + + initializeApp?: (opts: ElectronPackagerOptions, buildDir: string, appRelativePath: string) => Promise } /** Object hash of application metadata to embed into the executable (Windows only). */ @@ -76,5 +78,7 @@ declare module "electron-packager-tf" { InternalName?: string; } + export function userIgnoreFilter(opts: ElectronPackagerOptions): any + export function pack(opts: ElectronPackagerOptions): Promise } \ No newline at end of file