From f06324a5c0e4fba8b6853056a8b1e93650df180c Mon Sep 17 00:00:00 2001 From: develar Date: Sun, 1 Oct 2017 09:02:10 +0200 Subject: [PATCH] perf(appx): Use File Mappings instead of copy --- packages/electron-builder/src/parallels.ts | 9 ++ packages/electron-builder/src/targets/appx.ts | 108 +++++++++++------- .../electron-builder/src/targets/nsis/nsis.ts | 2 +- .../electron-builder/src/windowsCodeSign.ts | 5 +- .../windows/__snapshots__/appxTest.js.snap | 14 +-- test/src/windows/appxTest.ts | 8 +- 6 files changed, 81 insertions(+), 65 deletions(-) diff --git a/packages/electron-builder/src/parallels.ts b/packages/electron-builder/src/parallels.ts index f8b37e71588..947510f0163 100644 --- a/packages/electron-builder/src/parallels.ts +++ b/packages/electron-builder/src/parallels.ts @@ -1,5 +1,6 @@ import { exec, spawn, ExecOptions, DebugLogger, ExtraSpawnOptions } from "builder-util" import { SpawnOptions, execFileSync } from "child_process" +import * as path from "path" async function parseVmList(debugLogger: DebugLogger) { // do not log output if debug - it is huge, logged using debugLogger @@ -39,6 +40,10 @@ export async function getWindowsVm(debugLogger: DebugLogger): Promise } export class VmManager { + get pathSep(): string { + return path.sep + } + exec(file: string, args: Array, options?: ExecOptions, isLogOutIfDebug = true): Promise { return exec(file, args, options, isLogOutIfDebug) } @@ -63,6 +68,10 @@ class ParallelsVmManager extends VmManager { this.startPromise = this.doStartVm() } + get pathSep(): string { + return "/" + } + private handleExecuteError(error: Error): any { if (error.message.includes("Unable to open new session in this virtual machine")) { throw new Error(`Please ensure that your are logged in "${this.vm.name}" parallels virtual machine. In the future please do not stop VM, but suspend.\n\n${error.message}`) diff --git a/packages/electron-builder/src/targets/appx.ts b/packages/electron-builder/src/targets/appx.ts index b477f1b8af5..c3a7dadf900 100644 --- a/packages/electron-builder/src/targets/appx.ts +++ b/packages/electron-builder/src/targets/appx.ts @@ -1,8 +1,8 @@ import BluebirdPromise from "bluebird-lst" -import { Arch, asArray, AsyncTaskManager, getArchSuffix, use, log } from "builder-util" -import { copyDir, copyFile } from "builder-util/out/fs" +import { Arch, asArray, AsyncTaskManager, use, log } from "builder-util" +import { walk } from "builder-util/out/fs" import _debug from "debug" -import { emptyDir, mkdir, readdir, readFile, writeFile } from "fs-extra-p" +import { emptyDir, readdir, readFile, writeFile } from "fs-extra-p" import * as path from "path" import { deepAssign } from "read-config-file/out/deepAssign" import { Target } from "../core" @@ -10,6 +10,7 @@ import { AppXOptions } from "../options/winOptions" import { getTemplatePath } from "../util/pathManager" import { getSignVendorPath, isOldWin6 } from "../windowsCodeSign" import { WinPackager } from "../winPackager" +import { VmManager } from "../parallels" const APPX_ASSETS_DIR_NAME = "appx" @@ -34,54 +35,41 @@ export default class AppXTarget extends Target { } } + // https://docs.microsoft.com/en-us/windows/uwp/packaging/create-app-package-with-makeappx-tool#mapping-files async build(appOutDir: string, arch: Arch): Promise { const packager = this.packager + const vendorPath = await getSignVendorPath() + const vm = await packager.vm.value - const preAppx = path.join(this.outDir, `pre-appx-${getArchSuffix(arch)}`) - await emptyDir(preAppx) - - const assetOutDir = path.join(preAppx, "assets") - await mkdir(assetOutDir) - - const userAssetDir = await packager.getResource(undefined, APPX_ASSETS_DIR_NAME) - let userAssets: Array - if (userAssetDir == null) { - userAssets = [] - } - else { - userAssets = (await readdir(userAssetDir)).filter(it => !it.startsWith(".") && !it.endsWith(".db") && it.includes(".")) - await BluebirdPromise.map(userAssets, it => copyFile(path.join(userAssetDir, it), path.join(assetOutDir, it), false)) + const mappingFile = path.join(this.outDir, `.__appx-mapping-${Arch[arch]}.txt`) + const artifactName = packager.expandArtifactNamePattern(this.options, "appx", arch) + const artifactPath = path.join(this.outDir, artifactName) + const makeAppXArgs = ["pack", "/o" /* overwrite the output file if it exists */, "/f", vm.toVmFile(mappingFile), "/p", vm.toVmFile(artifactPath)] + if (packager.config.compression === "store") { + makeAppXArgs.push("/nc") } - const vendorPath = await getSignVendorPath() const taskManager = new AsyncTaskManager(packager.info.cancellationToken) - taskManager.addTask(BluebirdPromise.map(Object.keys(vendorAssetsForDefaultAssets), defaultAsset => { - if (!isDefaultAssetIncluded(userAssets, defaultAsset)) { - return copyFile(path.join(vendorPath, "appxAssets", vendorAssetsForDefaultAssets[defaultAsset]), path.join(assetOutDir, defaultAsset), false) + taskManager.addTask(BluebirdPromise.map(walk(appOutDir), file => { + let appxPath = file.substring(appOutDir.length + 1) + if (path.sep !== "\\") { + appxPath = appxPath.replace(/\//g, "\\") } - return null + return `"${vm.toVmFile(file)}" "app\\${appxPath}"` })) + taskManager.add(async () => { + const manifestFile = path.join(this.outDir, `.__AppxManifest-${Arch[arch]}.xml`) + const {userAssets, mappings: userAssetMappings } = await this.computeUserAssets(vm, vendorPath, arch, makeAppXArgs) + await this.writeManifest(getTemplatePath("appx"), manifestFile, arch, await this.computePublisherName(), userAssets) + return userAssetMappings.concat(`"${vm.toVmFile(manifestFile)}" "AppxManifest.xml"`) + }) - const publisher = await this.computePublisherName() - taskManager.addTask(this.writeManifest(getTemplatePath("appx"), preAppx, arch, publisher, userAssets)) - taskManager.addTask(copyDir(appOutDir, path.join(preAppx, "app"))) - await taskManager.awaitTasks() - - const artifactName = packager.expandArtifactNamePattern(this.options, "appx", arch) - const artifactPath = path.join(this.outDir, artifactName) - - const vm = await packager.vm.value - const makeAppXArgs = ["pack", "/o", "/d", vm.toVmFile(preAppx), "/p", vm.toVmFile(artifactPath)] - - // we do not use process.arch to build path to tools, because even if you are on x64, ia32 appx tool must be used if you build appx for ia32 - if (isScaledAssetsProvided(userAssets)) { - const priConfigPath = vm.toVmFile(path.join(preAppx, "priconfig.xml")) - const makePriPath = vm.toVmFile(path.join(vendorPath, "windows-10", Arch[arch], "makepri.exe")) - await vm.exec(makePriPath, ["createconfig", "/cf", priConfigPath, "/dq", "en-US", "/pv", "10.0.0", "/o"], undefined, debug.enabled) - await vm.exec(makePriPath, ["new", "/pr", vm.toVmFile(preAppx), "/cf", priConfigPath, "/of", vm.toVmFile(preAppx)], undefined, debug.enabled) - - makeAppXArgs.push("/l") + let mapping = "[Files]" + for (const list of (await taskManager.awaitTasks()) as Array>) { + mapping += "\r\n" + list.join("\r\n") } + await writeFile(mappingFile, mapping) + packager.debugLogger.add("appx.mapping", mapping) use(this.options.makeappxArgs, (it: Array) => makeAppXArgs.push(...it)) await vm.exec(vm.toVmFile(path.join(vendorPath, "windows-10", Arch[arch], "makeappx.exe")), makeAppXArgs, undefined, debug.enabled) @@ -97,6 +85,40 @@ export default class AppXTarget extends Target { }) } + private async computeUserAssets(vm: VmManager, vendorPath: string, arch: Arch, makeAppXArgs: Array) { + const mappings: Array = [] + const userAssetDir = await this.packager.getResource(undefined, APPX_ASSETS_DIR_NAME) + let userAssets: Array + if (userAssetDir == null) { + userAssets = [] + } + else { + userAssets = (await readdir(userAssetDir)).filter(it => !it.startsWith(".") && !it.endsWith(".db") && it.includes(".")) + for (const name of userAssets) { + mappings.push(`"${vm.toVmFile(userAssetDir)}${vm.pathSep}${name}" "assets\\${it}"`) + } + } + + for (const defaultAsset of Object.keys(vendorAssetsForDefaultAssets)) { + if (!isDefaultAssetIncluded(userAssets, defaultAsset)) { + mappings.push(`"${vm.toVmFile(path.join(vendorPath, "appxAssets", vendorAssetsForDefaultAssets[defaultAsset]))}" "assets\\${defaultAsset}"`) + } + } + + // we do not use process.arch to build path to tools, because even if you are on x64, ia32 appx tool must be used if you build appx for ia32 + if (isScaledAssetsProvided(userAssets)) { + const tempDir = path.join(this.outDir, `.__appx-${Arch[arch]}`) + await emptyDir(tempDir) + const priConfigPath = vm.toVmFile(path.join(tempDir, "priconfig.xml")) + const makePriPath = vm.toVmFile(path.join(vendorPath, "windows-10", Arch[arch], "makepri.exe")) + await vm.exec(makePriPath, ["createconfig", "/ConfigXml", priConfigPath, "/Default", "en-US", "/pv", "10.0.0", "/o"], undefined, debug.enabled) + await vm.exec(makePriPath, ["new", "/Overwrite", "/ProjectRoot", vm.toVmFile(userAssetDir!), "/ConfigXml", priConfigPath, "/OutputFile", vm.toVmFile(tempDir)], undefined, debug.enabled) + + makeAppXArgs.push("/l") + } + return {userAssets, mappings} + } + // https://github.com/electron-userland/electron-builder/issues/2108#issuecomment-333200711 private async computePublisherName() { if (await this.packager.cscInfo.value == null) { @@ -111,7 +133,7 @@ export default class AppXTarget extends Target { return publisher } - private async writeManifest(templatePath: string, preAppx: string, arch: Arch, publisher: string, userAssets: Array) { + private async writeManifest(templatePath: string, outFile: string, arch: Arch, publisher: string, userAssets: Array) { const appInfo = this.packager.appInfo const options = this.options const manifest = (await readFile(path.join(templatePath, "appxmanifest.xml"), "utf8")) @@ -184,7 +206,7 @@ export default class AppXTarget extends Target { throw new Error(`Macro ${p1} is not defined`) } }) - await writeFile(path.join(preAppx, "appxmanifest.xml"), manifest) + await writeFile(outFile, manifest) } } diff --git a/packages/electron-builder/src/targets/nsis/nsis.ts b/packages/electron-builder/src/targets/nsis/nsis.ts index 3915291e93c..5b66d294842 100644 --- a/packages/electron-builder/src/targets/nsis/nsis.ts +++ b/packages/electron-builder/src/targets/nsis/nsis.ts @@ -256,7 +256,7 @@ export class NsisTarget extends Target { // https://github.com/electron-userland/electron-builder/issues/2103 // it is more safe and reliable to write uninstaller to our out dir - const uninstallerPath = path.join(this.outDir, `.__uninstaller-${this.name}-${process.pid.toString(16)}-${Date.now().toString(16)}.exe`) + const uninstallerPath = path.join(this.outDir, `.__uninstaller-${this.name}-${this.packager.appInfo.sanitizedName}.exe`) const isWin = process.platform === "win32" defines.BUILD_UNINSTALLER = null defines.UNINSTALLER_OUT_FILE = isWin ? uninstallerPath : path.win32.join("Z:", uninstallerPath) diff --git a/packages/electron-builder/src/windowsCodeSign.ts b/packages/electron-builder/src/windowsCodeSign.ts index 5575272380f..6f58af0b6a9 100644 --- a/packages/electron-builder/src/windowsCodeSign.ts +++ b/packages/electron-builder/src/windowsCodeSign.ts @@ -92,7 +92,8 @@ export async function getCertificateFromStoreInfo(options: WindowsConfiguration, const certificateSha1 = options.certificateSha1 // ExcludeProperty doesn't work, so, we cannot exclude RawData, it is ok // powershell can return object if the only item - const certList = asArray(JSON.parse(await vm.exec("powershell.exe", ["Get-ChildItem -Recurse Cert: -CodeSigningCert | Select-Object -Property Subject,PSParentPath,Thumbprint,IssuerName | ConvertTo-Json -Compress"]))) + const rawResult = await vm.exec("powershell.exe", ["Get-ChildItem -Recurse Cert: -CodeSigningCert | Select-Object -Property Subject,PSParentPath,Thumbprint,IssuerName | ConvertTo-Json -Compress"]) + const certList = rawResult.length === 0 ? [] : asArray(JSON.parse(rawResult)) for (const certInfo of certList) { if (certificateSubjectName != null) { if (!certInfo.IssuerName.Name.includes(certificateSubjectName)) { @@ -117,7 +118,7 @@ export async function getCertificateFromStoreInfo(options: WindowsConfiguration, } } - throw new Error(`Cannot find certificate ${certificateSubjectName || certificateSha1}`) + throw new Error(`Cannot find certificate ${certificateSubjectName || certificateSha1}, all certs: ${rawResult}`) } async function doSign(configuration: CustomWindowsSignTaskConfiguration, packager: WinPackager) { diff --git a/test/out/windows/__snapshots__/appxTest.js.snap b/test/out/windows/__snapshots__/appxTest.js.snap index fbb4c715258..9c035126558 100644 --- a/test/out/windows/__snapshots__/appxTest.js.snap +++ b/test/out/windows/__snapshots__/appxTest.js.snap @@ -29,19 +29,7 @@ Object { } `; -exports[`languages 1`] = ` -Object { - "win": Array [ - Object { - "arch": "x64", - "file": "Test App ßW-1.1.0.appx", - "safeArtifactName": "TestApp-1.1.0.appx", - }, - ], -} -`; - -exports[`not signed (windows store only) 1`] = ` +exports[`languages and not signed (windows store only) 1`] = ` Object { "win": Array [ Object { diff --git a/test/src/windows/appxTest.ts b/test/src/windows/appxTest.ts index ceee29ea7fc..281c7604054 100644 --- a/test/src/windows/appxTest.ts +++ b/test/src/windows/appxTest.ts @@ -12,7 +12,7 @@ it.ifDevOrWinCi("AppX", app({ signedWin: true, })) -it.ifDevOrWinCi("certificateSubjectName", app({ +it.ifNotCi("certificateSubjectName", app({ targets: Platform.WINDOWS.createTarget(["appx"], Arch.x64), config: { win: { @@ -21,12 +21,8 @@ it.ifDevOrWinCi("certificateSubjectName", app({ }, })) -it.ifDevOrWinCi("not signed (windows store only)", app({ - targets: Platform.WINDOWS.createTarget(["appx"], Arch.x64), -})) - // todo - check manifest -test.ifWindows("languages", app({ +it("languages and not signed (windows store only)", app({ targets: Platform.WINDOWS.createTarget(["appx"]), cscLink: protectedCscLink, cscKeyPassword: "test",