Skip to content

Commit

Permalink
perf(appx): Use File Mappings instead of copy
Browse files Browse the repository at this point in the history
  • Loading branch information
develar committed Oct 1, 2017
1 parent fc23a58 commit f06324a
Show file tree
Hide file tree
Showing 6 changed files with 81 additions and 65 deletions.
9 changes: 9 additions & 0 deletions packages/electron-builder/src/parallels.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -39,6 +40,10 @@ export async function getWindowsVm(debugLogger: DebugLogger): Promise<VmManager>
}

export class VmManager {
get pathSep(): string {
return path.sep
}

exec(file: string, args: Array<string>, options?: ExecOptions, isLogOutIfDebug = true): Promise<string> {
return exec(file, args, options, isLogOutIfDebug)
}
Expand All @@ -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}`)
Expand Down
108 changes: 65 additions & 43 deletions packages/electron-builder/src/targets/appx.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
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"
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"

Expand All @@ -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<any> {
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<string>
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<Array<string>>) {
mapping += "\r\n" + list.join("\r\n")
}
await writeFile(mappingFile, mapping)
packager.debugLogger.add("appx.mapping", mapping)

use(this.options.makeappxArgs, (it: Array<string>) => makeAppXArgs.push(...it))
await vm.exec(vm.toVmFile(path.join(vendorPath, "windows-10", Arch[arch], "makeappx.exe")), makeAppXArgs, undefined, debug.enabled)
Expand All @@ -97,6 +85,40 @@ export default class AppXTarget extends Target {
})
}

private async computeUserAssets(vm: VmManager, vendorPath: string, arch: Arch, makeAppXArgs: Array<string>) {
const mappings: Array<string> = []
const userAssetDir = await this.packager.getResource(undefined, APPX_ASSETS_DIR_NAME)
let userAssets: Array<string>
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) {
Expand All @@ -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<string>) {
private async writeManifest(templatePath: string, outFile: string, arch: Arch, publisher: string, userAssets: Array<string>) {
const appInfo = this.packager.appInfo
const options = this.options
const manifest = (await readFile(path.join(templatePath, "appxmanifest.xml"), "utf8"))
Expand Down Expand Up @@ -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)
}
}

Expand Down
2 changes: 1 addition & 1 deletion packages/electron-builder/src/targets/nsis/nsis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
5 changes: 3 additions & 2 deletions packages/electron-builder/src/windowsCodeSign.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<CertInfo>(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<CertInfo>(JSON.parse(rawResult))
for (const certInfo of certList) {
if (certificateSubjectName != null) {
if (!certInfo.IssuerName.Name.includes(certificateSubjectName)) {
Expand All @@ -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) {
Expand Down
14 changes: 1 addition & 13 deletions test/out/windows/__snapshots__/appxTest.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
8 changes: 2 additions & 6 deletions test/src/windows/appxTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand All @@ -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",
Expand Down

0 comments on commit f06324a

Please sign in to comment.