diff --git a/package.json b/package.json index b94f423f1f6..068f27c2e65 100644 --- a/package.json +++ b/package.json @@ -55,7 +55,7 @@ "bluebird": "^3.3.4", "command-line-args": "^2.1.6", "electron-packager-tf": "^5.2.4-beta.1", - "electron-winstaller-fixed": "^2.0.6-beta.2", + "electron-winstaller-fixed": "^2.0.6-beta.4", "fs-extra": "^0.26.7", "fs-extra-p": "^0.1.0", "globby": "^4.0.0", @@ -76,6 +76,7 @@ "ava-tf": "^0.12.4-beta.6", "babel-plugin-array-includes": "^2.0.3", "babel-plugin-transform-es2015-parameters": "^6.7.0", + "decompress-zip": "^0.2.0", "electron-download": "^2.1.0", "json-parse-helpfulerror": "^1.0.3", "path-sort": "^0.1.0", diff --git a/src/macPackager.ts b/src/macPackager.ts index f5469a1814b..c9392c54155 100644 --- a/src/macPackager.ts +++ b/src/macPackager.ts @@ -31,9 +31,9 @@ export default class MacPackager extends PlatformPackager { return Platform.OSX } - async pack(platform: string, outDir: string, appOutDir: string, arch: string): Promise { - await super.pack(platform, outDir, appOutDir, arch) - let codeSigningInfo = await this.codeSigningInfo + async pack(outDir: string, appOutDir: string, arch: string): Promise { + await super.pack(outDir, appOutDir, arch) + const codeSigningInfo = await this.codeSigningInfo return await this.signMac(path.join(appOutDir, this.appName + ".app"), codeSigningInfo) } diff --git a/src/metadata.ts b/src/metadata.ts index 64e99119c83..4d08ba3a363 100755 --- a/src/metadata.ts +++ b/src/metadata.ts @@ -69,11 +69,11 @@ export interface PlatformSpecificBuildOptions { } export class Platform { - public static OSX = new Platform("osx", "osx") - public static LINUX = new Platform("linux", "linux") - public static WINDOWS = new Platform("windows", "win") + public static OSX = new Platform("osx", "osx", "darwin") + public static LINUX = new Platform("linux", "linux", "linux") + public static WINDOWS = new Platform("windows", "win", "win32") - constructor(public name: string, public buildConfigurationKey: string) { + constructor(public name: string, public buildConfigurationKey: string, public nodeName: string) { } toString() { @@ -82,9 +82,9 @@ export class Platform { public static fromNodePlatform(name: string): Platform { switch (name) { - case "darwin": return Platform.OSX - case "win32": return Platform.WINDOWS - case "linux": return Platform.LINUX + case Platform.OSX.nodeName: return Platform.OSX + case Platform.WINDOWS.nodeName: return Platform.WINDOWS + case Platform.LINUX.nodeName: return Platform.LINUX } throw new Error("Unknown platform: " + name) diff --git a/src/packager.ts b/src/packager.ts index e03837fcff6..a50a674dad3 100644 --- a/src/packager.ts +++ b/src/packager.ts @@ -73,7 +73,7 @@ export class Packager implements BuildInfo { const outDir = path.join(this.projectDir, "dist") // electron-packager uses productName in the directory name const appOutDir = path.join(outDir, helper.appName + "-" + platform + "-" + arch) - await helper.pack(platform, outDir, appOutDir, arch) + await helper.pack(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 dc0b954f665..a11e29f16bb 100644 --- a/src/platformPackager.ts +++ b/src/platformPackager.ts @@ -88,7 +88,12 @@ export abstract class PlatformPackager }) } - async pack(platform: string, outDir: string, appOutDir: string, arch: string): Promise { + async pack(outDir: string, appOutDir: string, arch: string): Promise { + await this.doPack(outDir, arch) + await this.copyExtraResources(appOutDir, arch) + } + + protected async doPack(outDir: string, arch: string) { const version = this.metadata.version let buildVersion = version const buildNumber = process.env.TRAVIS_BUILD_NUMBER || process.env.APPVEYOR_BUILD_NUMBER || process.env.CIRCLE_BUILD_NUM @@ -103,7 +108,7 @@ export abstract class PlatformPackager dir: this.info.appDir, out: outDir, name: this.appName, - platform: platform, + platform: this.platform.nodeName, arch: arch, version: this.info.electronVersion, icon: path.join(this.buildResourcesDir, "icon"), @@ -123,11 +128,13 @@ export abstract class PlatformPackager delete options.osx delete options.win delete options.linux - // this option only for windows-installer delete options.iconUrl + await pack(options) + } + protected getExtraResources(arch: string): Promise> { const buildMetadata: any = this.devMetadata.build let extraResources: Array = buildMetadata == null ? null : buildMetadata.extraResources @@ -136,18 +143,23 @@ export abstract class PlatformPackager extraResources = extraResources == null ? platformSpecificExtraResources : extraResources.concat(platformSpecificExtraResources) } - if (extraResources != null) { - const expandedPatterns = extraResources.map(it => it - .replace(/\$\{arch}/g, arch) - .replace(/\$\{os}/g, this.platform.buildConfigurationKey)) - await BluebirdPromise.map(await globby(expandedPatterns, {cwd: this.projectDir}), it => { - let resourcesDir = appOutDir - if (platform === "darwin") { - resourcesDir = path.join(resourcesDir, this.appName + ".app", "Contents", "Resources") - } - return copy(path.join(this.projectDir, it), path.join(resourcesDir, it)) - }) + if (extraResources == null) { + return BluebirdPromise.resolve([]) } + + const expandedPatterns = extraResources.map(it => it + .replace(/\$\{arch}/g, arch) + .replace(/\$\{os}/g, this.platform.buildConfigurationKey)) + return globby(expandedPatterns, {cwd: this.projectDir}) + } + + protected async copyExtraResources(appOutDir: string, arch: string): Promise> { + let resourcesDir = appOutDir + if (this.platform === Platform.OSX) { + resourcesDir = path.join(resourcesDir, this.appName + ".app", "Contents", "Resources") + } + + return await BluebirdPromise.map(await this.getExtraResources(arch), it => copy(path.join(this.projectDir, it), path.join(resourcesDir, it))) } abstract packageInDistributableFormat(outDir: string, appOutDir: string, arch: string): Promise diff --git a/src/winPackager.ts b/src/winPackager.ts index aff67bb375c..e88aa40cd61 100644 --- a/src/winPackager.ts +++ b/src/winPackager.ts @@ -25,6 +25,8 @@ export interface WinBuildOptions extends PlatformSpecificBuildOptions { export default class WinPackager extends PlatformPackager { certFilePromise: Promise + extraNuGetFileSources: Promise> + constructor(info: BuildInfo, cleanupTasks: Array<() => Promise>) { super(info) @@ -46,17 +48,29 @@ export default class WinPackager extends PlatformPackager { return Platform.WINDOWS } - pack(platform: string, outDir: string, appOutDir: string, arch: string): Promise { + async pack(outDir: string, appOutDir: string, arch: string): Promise { if (this.options.dist) { const installerOut = WinPackager.computeDistOut(outDir, arch) log("Removing %s", installerOut) - return BluebirdPromise.all([ - super.pack(platform, outDir, appOutDir, arch), + await BluebirdPromise.all([ + this.doPack(outDir, arch), emptyDir(installerOut) ]) + + let extraResources = await this.copyExtraResources(appOutDir, arch) + if (extraResources.length > 0) { + this.extraNuGetFileSources = BluebirdPromise.map(extraResources, file => { + return stat(file) + .then(it => { + const relativePath = path.relative(appOutDir, file) + const src = it.isDirectory() ? `${relativePath}${path.sep}**` : relativePath + return `` + }) + }) + } } else { - return super.pack(platform, outDir, appOutDir, arch) + return super.pack(outDir, appOutDir, arch) } } @@ -105,6 +119,7 @@ export default class WinPackager extends PlatformPackager { fixUpPaths: false, usePackageJson: false, noMsi: true, + extraFileSpecs: this.extraNuGetFileSources == null ? null : ("\n" + (await this.extraNuGetFileSources).join("\n")) }, this.customBuildOptions) try { diff --git a/test/src/BuildTest.ts b/test/src/BuildTest.ts index 9729657686b..c2b3aac21c1 100755 --- a/test/src/BuildTest.ts +++ b/test/src/BuildTest.ts @@ -1,6 +1,6 @@ import test from "./helpers/avaEx" import { assertPack, modifyPackageJson, platform } from "./helpers/packTester" -import { move, mkdirs, outputFile, outputJson } from "fs-extra-p" +import { move, outputFile, outputJson } from "fs-extra-p" import { Promise as BluebirdPromise } from "bluebird" import * as path from "path" import { assertThat } from "./helpers/fileAssert" @@ -23,99 +23,144 @@ test.ifOsx("mac: one-package.json", async () => { }) test("custom app dir", async () => { - await assertPack("test-app-one", allPlatformsAndCurrentArch(), (projectDir) => { - return BluebirdPromise.all([ - modifyPackageJson(projectDir, data => { - data.directories = { - buildResources: "custom" - } - }), - move(path.join(projectDir, "build"), path.join(projectDir, "custom")) - ]) + await assertPack("test-app-one", allPlatformsAndCurrentArch(), { + tempDirCreated: (projectDir) => { + return BluebirdPromise.all([ + modifyPackageJson(projectDir, data => { + data.directories = { + buildResources: "custom" + } + }), + move(path.join(projectDir, "build"), path.join(projectDir, "custom")) + ]) + } }) }) test("productName with space", async () => { - await assertPack("test-app-one", allPlatformsAndCurrentArch(), projectDir => { - return modifyPackageJson(projectDir, data => { - data.productName = "Test App" - }) + await assertPack("test-app-one", allPlatformsAndCurrentArch(), { + tempDirCreated: projectDir => { + return modifyPackageJson(projectDir, data => { + data.productName = "Test App" + }) + } }) }) test("build in the app package.json", t => { - t.throws(assertPack("test-app", allPlatformsAndCurrentArch(), projectDir => { - return modifyPackageJson(projectDir, data => { - data.build = { - "iconUrl": "bar", - } - }, true) + t.throws(assertPack("test-app", allPlatformsAndCurrentArch(), { + tempDirCreated: projectDir => { + return modifyPackageJson(projectDir, data => { + data.build = { + "iconUrl": "bar", + } + }, true) + } }), /'build' in the application package\.json .+/) }) -test("version from electron-prebuilt dependency", async () => { +test("version from electron-prebuilt dependency", async() => { await assertPack("test-app-one", { platform: [process.platform], arch: process.arch, dist: false - }, projectDir => { - return BluebirdPromise.all([ - outputJson(path.join(projectDir, "node_modules", "electron-prebuilt", "package.json"), { - version: "0.37.2" - }), - modifyPackageJson(projectDir, data => { - data.devDependencies = {} - }) - ]) + }, { + tempDirCreated: projectDir => { + return BluebirdPromise.all([ + outputJson(path.join(projectDir, "node_modules", "electron-prebuilt", "package.json"), { + version: "0.37.2" + }), + modifyPackageJson(projectDir, data => { + data.devDependencies = {} + }) + ]) + } }) }) test("copy extra resource", async () => { - const platform = process.platform - const osName = Platform.fromNodePlatform(platform).buildConfigurationKey + for (let platform of getPossiblePlatforms()) { + const osName = Platform.fromNodePlatform(platform).buildConfigurationKey - await assertPack("test-app", { - platform: [platform], - arch: process.arch, - dist: false - }, (projectDir) => { - return BluebirdPromise.all([ - modifyPackageJson(projectDir, data => { - if (data.build == null) { - data.build = {} - } - data.build.extraResources = [ - "foo", - "bar/hello.txt", - "bar/${arch}.txt", - "${os}/${arch}.txt", - ] + await assertPack("test-app", { + platform: [platform], + arch: process.arch, + // to check NuGet package + dist: platform === "win32" + }, { + tempDirCreated: (projectDir) => { + return BluebirdPromise.all([ + modifyPackageJson(projectDir, data => { + if (data.build == null) { + data.build = {} + } + data.build.extraResources = [ + "foo", + "bar/hello.txt", + "bar/${arch}.txt", + "${os}/${arch}.txt", + ] - data.build[osName] = { - extraResources: [ - "platformSpecific" - ] + data.build[osName] = { + extraResources: [ + "platformSpecific" + ] + } + }), + outputFile(path.join(projectDir, "foo/nameWithoutDot"), "nameWithoutDot"), + outputFile(path.join(projectDir, "bar/hello.txt"), "data"), + outputFile(path.join(projectDir, `bar/${process.arch}.txt`), "data"), + outputFile(path.join(projectDir, `${osName}/${process.arch}.txt`), "data"), + outputFile(path.join(projectDir, "platformSpecific"), "platformSpecific"), + outputFile(path.join(projectDir, "ignoreMe.txt"), "ignoreMe"), + ]) + }, + packed: async(projectDir) => { + let resourcesDir = path.join(projectDir, "dist", "TestApp-" + platform + "-" + process.arch) + if (platform === "darwin") { + resourcesDir = path.join(resourcesDir, "TestApp.app", "Contents", "Resources") } - }), - mkdirs(path.join(projectDir, "foo")), - outputFile(path.join(projectDir, "bar/hello.txt"), "data"), - outputFile(path.join(projectDir, `bar/${process.arch}.txt`), "data"), - outputFile(path.join(projectDir, `${osName}/${process.arch}.txt`), "data"), - outputFile(path.join(projectDir, "platformSpecific"), "platformSpecific"), - outputFile(path.join(projectDir, "ignoreMe.txt"), "ignoreMe"), - ]) - }, async (projectDir) => { - let resourcesDir = path.join(projectDir, "dist", "TestApp-" + platform + "-" + process.arch) - if (platform === "darwin") { - resourcesDir = path.join(resourcesDir, "TestApp.app", "Contents", "Resources") - } - await assertThat(path.join(resourcesDir, "foo")).isDirectory() - await assertThat(path.join(resourcesDir, "bar/hello.txt")).isFile() - await assertThat(path.join(resourcesDir, `bar/${process.arch}.txt`)).isFile() - await assertThat(path.join(resourcesDir, `${osName}/${process.arch}.txt`)).isFile() - await assertThat(path.join(resourcesDir, "platformSpecific")).isFile() - await assertThat(path.join(resourcesDir, "ignoreMe.txt")).doesNotExist() - }) + await assertThat(path.join(resourcesDir, "foo")).isDirectory() + await assertThat(path.join(resourcesDir, "foo", "nameWithoutDot")).isFile() + await assertThat(path.join(resourcesDir, "bar", "hello.txt")).isFile() + await assertThat(path.join(resourcesDir, "bar", `${process.arch}.txt`)).isFile() + await assertThat(path.join(resourcesDir, osName, `${process.arch}.txt`)).isFile() + await assertThat(path.join(resourcesDir, "platformSpecific")).isFile() + await assertThat(path.join(resourcesDir, "ignoreMe.txt")).doesNotExist() + }, + expectedContents: platform === "win32" ? [ + "lib/net45/content_resources_200_percent.pak", + "lib/net45/content_shell.pak", + "lib/net45/d3dcompiler_47.dll", + "lib/net45/ffmpeg.dll", + "lib/net45/icudtl.dat", + "lib/net45/libEGL.dll", + "lib/net45/libGLESv2.dll", + "lib/net45/LICENSE", + "lib/net45/msvcp120.dll", + "lib/net45/msvcr120.dll", + "lib/net45/natives_blob.bin", + "lib/net45/node.dll", + "lib/net45/platformSpecific", + "lib/net45/snapshot_blob.bin", + "lib/net45/squirrel.exe", + "lib/net45/TestApp.exe", + "lib/net45/ui_resources_200_percent.pak", + "lib/net45/vccorlib120.dll", + "lib/net45/xinput1_3.dll", + "lib/net45/bar/hello.txt", + "lib/net45/bar/x64.txt", + "lib/net45/foo/nameWithoutDot", + "lib/net45/locales/en-US.pak", + "lib/net45/resources/app.asar", + "lib/net45/resources/atom.asar", + "lib/net45/win/x64.txt", + "TestApp.nuspec", + "[Content_Types].xml", + "_rels/.rels" + ] : null, + }) + } }) function allPlatformsAndCurrentArch(): PackagerOptions { diff --git a/test/src/helpers/expectedContents.ts b/test/src/helpers/expectedContents.ts index 45f613f3781..996240a1182 100755 --- a/test/src/helpers/expectedContents.ts +++ b/test/src/helpers/expectedContents.ts @@ -51,4 +51,31 @@ export const expectedLinuxContents = [ "/usr/share/icons/hicolor/64x64/apps/TestApp.png", "/usr/share/icons/hicolor/96x96/apps/", "/usr/share/icons/hicolor/96x96/apps/TestApp.png" +] + +export const expectedWinContents = [ + "lib/net45/content_resources_200_percent.pak", + "lib/net45/content_shell.pak", + "lib/net45/d3dcompiler_47.dll", + "lib/net45/ffmpeg.dll", + "lib/net45/icudtl.dat", + "lib/net45/libEGL.dll", + "lib/net45/libGLESv2.dll", + "lib/net45/LICENSE", + "lib/net45/msvcp120.dll", + "lib/net45/msvcr120.dll", + "lib/net45/natives_blob.bin", + "lib/net45/node.dll", + "lib/net45/snapshot_blob.bin", + "lib/net45/squirrel.exe", + "lib/net45/TestApp.exe", + "lib/net45/ui_resources_200_percent.pak", + "lib/net45/vccorlib120.dll", + "lib/net45/xinput1_3.dll", + "lib/net45/locales/en-US.pak", + "lib/net45/resources/app.asar", + "lib/net45/resources/atom.asar", + "TestApp.nuspec", + "[Content_Types].xml", + "_rels/.rels" ] \ No newline at end of file diff --git a/test/src/helpers/packTester.ts b/test/src/helpers/packTester.ts index 2c78c90cd1a..188ccab9146 100755 --- a/test/src/helpers/packTester.ts +++ b/test/src/helpers/packTester.ts @@ -3,13 +3,15 @@ import * as assertThat from "should/as-function" import * as path from "path" import { parse as parsePlist } from "plist" import { CSC_LINK, CSC_KEY_PASSWORD } from "./codeSignData" -import { expectedLinuxContents } from "./expectedContents" +import { expectedLinuxContents, expectedWinContents } from "./expectedContents" import { readText } from "out/promisifed-fs" import { Packager, PackagerOptions, Platform, getProductName, ArtifactCreated } from "out" import { normalizePlatforms } from "out/packager" import { exec } from "out/util" import pathSorter = require("path-sort") import { tmpdir } from "os" +import DecompressZip = require("decompress-zip") +import { Promise as BluebirdPromise } from "bluebird" //noinspection JSUnusedLocalSymbols const __awaiter = require("out/awaiter") @@ -17,10 +19,14 @@ const __awaiter = require("out/awaiter") const tmpDirPrefix = "electron-builder-test-" + process.pid + "-" let tmpDirCounter = 0 -export async function assertPack(fixtureName: string, - packagerOptions: PackagerOptions, - tempDirCreated?: (projectDir: string) => Promise, - packed?: (projectDir: string) => Promise): Promise { +interface AssertPackOptions { + readonly tempDirCreated?: (projectDir: string) => Promise + readonly packed?: (projectDir: string) => Promise + readonly expectedContents?: Array +} + +export async function assertPack(fixtureName: string, packagerOptions: PackagerOptions, checkOptions?: AssertPackOptions): Promise { + const tempDirCreated = checkOptions == null ? null : checkOptions.tempDirCreated const useTempDir = tempDirCreated != null || (packagerOptions != null && packagerOptions.target != null) let projectDir = path.join(__dirname, "..", "..", "fixtures", fixtureName) @@ -52,10 +58,10 @@ export async function assertPack(fixtureName: string, cscLink: CSC_LINK, cscKeyPassword: CSC_KEY_PASSWORD, dist: true, - }, packagerOptions)) + }, packagerOptions), checkOptions) - if (packed != null) { - await packed(projectDir) + if (checkOptions != null && checkOptions.packed != null) { + await checkOptions.packed(projectDir) } } finally { @@ -70,7 +76,7 @@ export async function assertPack(fixtureName: string, } } -async function packAndCheck(projectDir: string, packagerOptions: PackagerOptions): Promise { +async function packAndCheck(projectDir: string, packagerOptions: PackagerOptions, checkOptions: AssertPackOptions): Promise { const packager = new Packager(packagerOptions) const artifacts: Map> = new Map() @@ -117,7 +123,7 @@ async function packAndCheck(projectDir: string, packagerOptions: PackagerOptions } } else if (platform === "win32" && (packagerOptions == null || packagerOptions.target == null)) { - await checkWindowsResult(packager, packagerOptions, artifacts.get(Platform.WINDOWS)) + await checkWindowsResult(packager, packagerOptions, checkOptions, artifacts.get(Platform.WINDOWS)) } } } @@ -147,7 +153,7 @@ async function checkOsXResult(packager: Packager, artifacts: Array) { +async function checkWindowsResult(packager: Packager, packagerOptions: PackagerOptions, checkOptions: AssertPackOptions, artifacts: Array) { const productName = getProductName(packager.metadata, packager.devMetadata) function getWinExpected(archSuffix: string) { @@ -174,6 +180,24 @@ async function checkWindowsResult(packager: Packager, packagerOptions: PackagerO expectedArtifactNames[1] = `TestAppSetup-1.0.0${archSuffix}.exe` assertThat(artifacts.map(it => it.artifactName).filter(it => it != null)).deepEqual([`TestAppSetup-1.0.0${archSuffix}.exe`]) } + + const files = pathSorter((await new BluebirdPromise>((resolve, reject) => { + const unZipper = new DecompressZip(path.join(path.dirname(artifacts[0].file), `TestApp-1.0.0${archSuffix}-full.nupkg`)) + unZipper.on("list", resolve) + unZipper.on('error', reject) + unZipper.list() + })).map(it => it.replace(/\\/g, "/")).filter(it => (!it.startsWith("lib/net45/locales/") || it === "lib/net45/locales/en-US.pak") && !it.endsWith(".psmdcp"))) + + // console.log(JSON.stringify(files, null, 2)) + const expectedContents = checkOptions == null || checkOptions.expectedContents == null ? expectedWinContents : checkOptions.expectedContents + assertThat(files).deepEqual(expectedContents.map(it => { + if (it === "lib/net45/TestApp.exe") { + return `lib/net45/${productName.replace(/ /g, "%20")}.exe` + } + else { + return it + } + })) } async function getContents(path: string, productName: string) { diff --git a/test/src/linuxPackagerTest.ts b/test/src/linuxPackagerTest.ts index 86b4367c11e..0fcfad93676 100755 --- a/test/src/linuxPackagerTest.ts +++ b/test/src/linuxPackagerTest.ts @@ -14,13 +14,15 @@ test.ifNotWindows("linux - icons from ICNS", async () => { await assertPack("test-app-one", { platform: ["linux"], arch: process.arch, - }, (projectDir) => remove(path.join(projectDir, "build", "icons"))) + }, {tempDirCreated: (projectDir) => remove(path.join(projectDir, "build", "icons"))}) }) test.ifNotWindows("no-author-email", t => { - t.throws(assertPack("test-app-one", platform("linux"), projectDir => { + t.throws(assertPack("test-app-one", platform("linux"), { + tempDirCreated: projectDir => { return modifyPackageJson(projectDir, data => { data.author = "Foo" }) - }), /Please specify author 'email' in .+/) + } + }), /Please specify author 'email' in .+/) }) \ No newline at end of file diff --git a/test/tsconfig.json b/test/tsconfig.json index ad39f10527e..5c331a7c10a 100755 --- a/test/tsconfig.json +++ b/test/tsconfig.json @@ -42,6 +42,7 @@ "../typings/progress-stream.d.ts", "../typings/read-package-json.d.ts", "typings/ava.d.ts", + "typings/decompress-zip.d.ts", "typings/json-parse-helpfulerror.d.ts", "typings/path-sort.d.ts", "typings/plist.d.ts", diff --git a/test/typings/decompress-zip.d.ts b/test/typings/decompress-zip.d.ts new file mode 100644 index 00000000000..3e1e200b701 --- /dev/null +++ b/test/typings/decompress-zip.d.ts @@ -0,0 +1,9 @@ +declare module "decompress-zip" { + import { EventEmitter } from "events" + + export = class DecompressZip extends EventEmitter { + constructor(filename: string) + + list(): void + } +} \ No newline at end of file