diff --git a/.circleci/config.yml b/.circleci/config.yml index 39f2d0fcb70..69c457a82bb 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -33,6 +33,7 @@ jobs: - image: electronuserland/builder:wine-mono environment: JEST_JUNIT_OUTPUT: ./test-reports/test.xml + TZ: Europe/Berlin steps: - checkout - run: diff --git a/packages/electron-builder-squirrel-windows/package.json b/packages/electron-builder-squirrel-windows/package.json index 480c1e9f074..52b10d06a2f 100644 --- a/packages/electron-builder-squirrel-windows/package.json +++ b/packages/electron-builder-squirrel-windows/package.json @@ -17,6 +17,9 @@ "archiver": "^2.1.0", "sanitize-filename": "^1.6.1" }, + "optionalDependencies": { + "7zip-bin": "~2.2.7" + }, "peerDependencies": { "electron-builder": "~0.0.0-semantic-release" }, diff --git a/packages/electron-builder-squirrel-windows/src/squirrelPack.ts b/packages/electron-builder-squirrel-windows/src/squirrelPack.ts index bc207fc6e21..dfd991b9765 100644 --- a/packages/electron-builder-squirrel-windows/src/squirrelPack.ts +++ b/packages/electron-builder-squirrel-windows/src/squirrelPack.ts @@ -2,8 +2,10 @@ import BluebirdPromise from "bluebird-lst" import { Arch, debug, exec, execWine, log, prepareWindowsExecutableArgs as prepareArgs, spawn } from "builder-util" import { copyFile, walk } from "builder-util/out/fs" import { WinPackager } from "electron-builder/out/winPackager" -import { createWriteStream, ensureDir, remove, stat, unlink } from "fs-extra-p" +import { createWriteStream, ensureDir, remove, stat, unlink, writeFile } from "fs-extra-p" import * as path from "path" +import { path7za } from "7zip-bin" +import { compute7zCompressArgs } from "electron-builder/out/targets/archive" const archiver = require("archiver") @@ -51,61 +53,87 @@ export interface OutFileNames { packageFile: string } -export async function buildInstaller(options: SquirrelOptions, outputDirectory: string, outFileNames: OutFileNames, packager: WinPackager, appOutDir: string, outDir: string, arch: Arch) { - const appUpdate = await packager.getTempFile("Update.exe") - await BluebirdPromise.all([ - copyFile(path.join(options.vendorPath, "Update.exe"), appUpdate) - .then(() => packager.sign(appUpdate)), - BluebirdPromise.all([remove(`${outputDirectory.replace(/\\/g, "/")}/*-full.nupkg`), remove(path.join(outputDirectory, "RELEASES"))]) - .then(() => ensureDir(outputDirectory)) - ]) - - if (options.remoteReleases) { - await syncReleases(outputDirectory, options) +export class SquirrelBuilder { + constructor(private readonly options: SquirrelOptions, private readonly outputDirectory: string, private readonly packager: WinPackager) { } - const embeddedArchiveFile = await packager.getTempFile("setup.zip") - const embeddedArchive = archiver("zip", {zlib: {level: options.packageCompressionLevel == null ? 6 : options.packageCompressionLevel}}) - const embeddedArchiveOut = createWriteStream(embeddedArchiveFile) - const embeddedArchivePromise = new BluebirdPromise((resolve, reject) => { - embeddedArchive.on("error", reject) - embeddedArchiveOut.on("close", resolve) - }) - embeddedArchive.pipe(embeddedArchiveOut) + async buildInstaller(outFileNames: OutFileNames, appOutDir: string, outDir: string, arch: Arch, dirToArchive: string) { + const outputDirectory = this.outputDirectory + const options = this.options + const appUpdate = path.join(dirToArchive, "Update.exe") + const packager = this.packager + await BluebirdPromise.all([ + copyFile(path.join(options.vendorPath, "Update.exe"), appUpdate) + .then(() => packager.sign(appUpdate)), + BluebirdPromise.all([remove(`${outputDirectory.replace(/\\/g, "/")}/*-full.nupkg`), remove(path.join(outputDirectory, "RELEASES"))]) + .then(() => ensureDir(outputDirectory)) + ]) + + if (options.remoteReleases) { + await syncReleases(outputDirectory, options) + } - embeddedArchive.file(appUpdate, {name: "Update.exe"}) - embeddedArchive.file(options.loadingGif ? path.resolve(packager.projectDir, options.loadingGif) : path.join(options.vendorPath, "install-spinner.gif"), {name: "background.gif"}) + const version = convertVersion(options.version) + const nupkgPath = path.join(outputDirectory, outFileNames.packageFile) + const setupPath = path.join(outputDirectory, outFileNames.setupFile) - const version = convertVersion(options.version) - const nupkgPath = path.join(outputDirectory, outFileNames.packageFile) - const setupPath = path.join(outputDirectory, outFileNames.setupFile) + await BluebirdPromise.all([ + pack(options, appOutDir, appUpdate, nupkgPath, version, packager), + copyFile(path.join(options.vendorPath, "Setup.exe"), setupPath), + copyFile(options.loadingGif ? path.resolve(packager.projectDir, options.loadingGif) : path.join(options.vendorPath, "install-spinner.gif"), path.join(dirToArchive, "background.gif")), + ]) - await BluebirdPromise.all([ - pack(options, appOutDir, appUpdate, nupkgPath, version, packager), - copyFile(path.join(options.vendorPath, "Setup.exe"), setupPath), - ]) + // releasify can be called only after pack nupkg and nupkg must be in the final output directory (where other old version nupkg can be located) + await this.releasify(nupkgPath, outFileNames.packageFile) + .then(it => writeFile(path.join(dirToArchive, "RELEASES"), it)) - embeddedArchive.file(nupkgPath, {name: outFileNames.packageFile}) + const embeddedArchiveFile = await this.createEmbeddedArchiveFile(nupkgPath, dirToArchive) - const releaseEntry = await releasify(options, nupkgPath, outputDirectory, outFileNames.packageFile) + await execWine(path.join(options.vendorPath, "WriteZipToSetup.exe"), [setupPath, embeddedArchiveFile]) - embeddedArchive.append(releaseEntry, {name: "RELEASES"}) - embeddedArchive.finalize() - await embeddedArchivePromise + await packager.signAndEditResources(setupPath, arch, outDir) + if (options.msi && process.platform === "win32") { + const outFile = outFileNames.setupFile.replace(".exe", ".msi") + await msi(options, nupkgPath, setupPath, outputDirectory, outFile) + // rcedit can only edit .exe resources + await packager.sign(path.join(outputDirectory, outFile)) + } + } + + private async releasify(nupkgPath: string, packageName: string) { + const args = [ + "--releasify", nupkgPath, + "--releaseDir", this.outputDirectory + ] + const out = (await exec(process.platform === "win32" ? path.join(this.options.vendorPath, "Update.com") : "mono", prepareArgs(args, path.join(this.options.vendorPath, "Update-Mono.exe")))).trim() + if (debug.enabled) { + debug(`Squirrel output: ${out}`) + } - await execWine(path.join(options.vendorPath, "WriteZipToSetup.exe"), [setupPath, embeddedArchiveFile]) + const lines = out.split("\n") + for (let i = lines.length - 1; i > -1; i--) { + const line = lines[i] + if (line.includes(packageName)) { + return line.trim() + } + } - await packager.signAndEditResources(setupPath, arch, outDir) - if (options.msi && process.platform === "win32") { - const outFile = outFileNames.setupFile.replace(".exe", ".msi") - await msi(options, nupkgPath, setupPath, outputDirectory, outFile) - // rcedit can only edit .exe resources - await packager.sign(path.join(outputDirectory, outFile)) + throw new Error(`Invalid output, cannot find last release entry, output: ${out}`) + } + + private async createEmbeddedArchiveFile(nupkgPath: string, dirToArchive: string) { + const embeddedArchiveFile = await this.packager.getTempFile("setup.zip") + await exec(path7za, compute7zCompressArgs("zip", this.packager.compression, {isRegularFile: true}).concat(embeddedArchiveFile, "."), { + cwd: dirToArchive, + }) + await exec(path7za, compute7zCompressArgs("zip", "store" /* nupkg is already compressed */, {isRegularFile: true}).concat(embeddedArchiveFile, nupkgPath)) + return embeddedArchiveFile } } async function pack(options: SquirrelOptions, directory: string, updateFile: string, outFile: string, version: string, packager: WinPackager) { - const archive = archiver("zip", {zlib: {level: options.packageCompressionLevel == null ? 9 : options.packageCompressionLevel}}) + // SW now doesn't support 0-level nupkg compressed files. It means that we are forced to use level 1 if store level requested. + const archive = archiver("zip", {zlib: {level: Math.max(1, (options.packageCompressionLevel == null ? 9 : options.packageCompressionLevel))}}) const archiveOut = createWriteStream(outFile) const archivePromise = new BluebirdPromise((resolve, reject) => { archive.on("error", reject) @@ -175,27 +203,6 @@ async function pack(options: SquirrelOptions, directory: string, updateFile: str await archivePromise } -async function releasify(options: SquirrelOptions, nupkgPath: string, outputDirectory: string, packageName: string) { - const args = [ - "--releasify", nupkgPath, - "--releaseDir", outputDirectory - ] - const out = (await exec(process.platform === "win32" ? path.join(options.vendorPath, "Update.com") : "mono", prepareArgs(args, path.join(options.vendorPath, "Update-Mono.exe")))).trim() - if (debug.enabled) { - debug(`Squirrel output: ${out}`) - } - - const lines = out.split("\n") - for (let i = lines.length - 1; i > -1; i--) { - const line = lines[i] - if (line.includes(packageName)) { - return line.trim() - } - } - - throw new Error(`Invalid output, cannot find last release entry, output: ${out}`) -} - async function msi(options: SquirrelOptions, nupkgPath: string, setupPath: string, outputDirectory: string, outFile: string) { const args = [ "--createMsi", nupkgPath, diff --git a/packages/electron-builder-squirrel-windows/src/squirrelWindows.ts b/packages/electron-builder-squirrel-windows/src/squirrelWindows.ts index c74d954f472..308ffccb65c 100644 --- a/packages/electron-builder-squirrel-windows/src/squirrelWindows.ts +++ b/packages/electron-builder-squirrel-windows/src/squirrelWindows.ts @@ -4,11 +4,8 @@ import { Arch, getArchSuffix, SquirrelWindowsOptions, Target } from "electron-bu import { WinPackager } from "electron-builder/out/winPackager" import * as path from "path" import sanitizeFileName from "sanitize-filename" -import { buildInstaller, convertVersion, SquirrelOptions } from "./squirrelPack" - -const SW_VERSION = "1.6.0.0" -//noinspection SpellCheckingInspection -const SW_SHA2 = "ipd/ZQXyCe2+CYmNiUa9+nzVuO2PsRfF6DT8Y2mbIzkc8SVH8tJ6uS4rdhwAI1rPsYkmsPe1AcJGqv8ZDZcFww==" +import { SquirrelBuilder, convertVersion, SquirrelOptions } from "./squirrelPack" +import { emptyDir, remove } from "fs-extra-p" export default class SquirrelWindowsTarget extends Target { readonly options: SquirrelWindowsOptions = {...this.packager.platformSpecificBuildOptions, ...this.packager.config.squirrelWindows} @@ -25,20 +22,27 @@ export default class SquirrelWindowsTarget extends Target { } const packager = this.packager - const appInfo = packager.appInfo - const version = appInfo.version - const archSuffix = getArchSuffix(arch) - + const version = packager.appInfo.version const sanitizedName = sanitizeFileName(this.appName) // tslint:disable-next-line:no-invalid-template-strings const setupFile = packager.expandArtifactNamePattern(this.options, "exe", arch, "${productName} Setup ${version}.${ext}") const packageFile = `${sanitizedName}-${convertVersion(version)}-full.nupkg` - const installerOutDir = path.join(this.outDir, `win${getArchSuffix(arch)}`) + const installerOutDir = path.join(this.outDir, `squirrel-windows${getArchSuffix(arch)}`) const distOptions = await this.computeEffectiveDistOptions() - await buildInstaller(distOptions as SquirrelOptions, installerOutDir, {setupFile, packageFile}, packager, appOutDir, this.outDir, arch) - packager.dispatchArtifactCreated(path.join(installerOutDir, setupFile), this, arch, `${sanitizedName}-Setup-${version}${archSuffix}.exe`) + const tempDir = path.join(installerOutDir, ".temp") + await emptyDir(tempDir) + try { + const squirrelBuilder = new SquirrelBuilder(distOptions as SquirrelOptions, installerOutDir, packager) + await squirrelBuilder.buildInstaller({setupFile, packageFile}, appOutDir, this.outDir, arch, tempDir) + } + finally { + await remove(tempDir) + .catch(e => warn(`Cannot delete temporary directory: ${e.message}`)) + } + + packager.dispatchArtifactCreated(path.join(installerOutDir, setupFile), this, arch, `${sanitizedName}-Setup-${version}${getArchSuffix(arch)}.exe`) const packagePrefix = `${this.appName}-${convertVersion(version)}-` packager.dispatchArtifactCreated(path.join(installerOutDir, `${packagePrefix}full.nupkg`), this, arch) @@ -84,7 +88,7 @@ export default class SquirrelWindowsTarget extends Target { extraMetadataSpecs: projectUrl == null ? null : `\n ${projectUrl}`, copyright: appInfo.copyright, packageCompressionLevel: parseInt((process.env.ELECTRON_BUILDER_COMPRESSION_LEVEL || packager.compression === "store" ? 0 : 9) as any, 10), - vendorPath: await getBinFromGithub("Squirrel.Windows", SW_VERSION, SW_SHA2), + vendorPath: await getBinFromGithub("Squirrel.Windows", "1.7.8", "p4Z7//ol4qih1xIl2l9lOeFf1RmX4y1eAJkol+3q7iZ0iEMotBhs3HXFLxU435xLRhKghYOjSYu7WiUktsP5Bg=="), ...this.options as any, } diff --git a/packages/electron-builder/src/targets/archive.ts b/packages/electron-builder/src/targets/archive.ts index b6281b89fce..2005a15e6be 100644 --- a/packages/electron-builder/src/targets/archive.ts +++ b/packages/electron-builder/src/targets/archive.ts @@ -37,7 +37,7 @@ export async function tar(compression: CompressionLevel | any | any, format: str return } - const args = compute7zCompressArgs(compression, format === "tar.xz" ? "xz" : (format === "tar.bz2" ? "bzip2" : "gzip"), {isRegularFile: true}) + const args = compute7zCompressArgs(format === "tar.xz" ? "xz" : (format === "tar.bz2" ? "bzip2" : "gzip"), compression, {isRegularFile: true, method: "DEFAULT"}) args.push(outFile, tarFile) await exec(path7za, args, { cwd: path.dirname(dirToArchive), @@ -65,12 +65,13 @@ export interface ArchiveOptions { dictSize?: number excluded?: Array - method?: "Copy" | "LZMA" | "Deflate" + // DEFAULT allows to disable custom logic and do not pass method switch at all + method?: "Copy" | "LZMA" | "Deflate" | "DEFAULT" isRegularFile?: boolean } -export function compute7zCompressArgs(compression: CompressionLevel | any | any, format: string, options: ArchiveOptions = {}) { +export function compute7zCompressArgs(format: string, compression: CompressionLevel | null | undefined, options: ArchiveOptions = {}) { let storeOnly = compression === "store" const args = debug7zArgs("a") @@ -117,9 +118,11 @@ export function compute7zCompressArgs(compression: CompressionLevel | any | any, } if (options.method != null) { - args.push(`-mm=${options.method}`) + if (options.method !== "DEFAULT") { + args.push(`-mm=${options.method}`) + } } - else if (!options.isRegularFile && (format === "zip" || storeOnly)) { + else if (format === "zip" || storeOnly) { args.push(`-mm=${storeOnly ? "Copy" : "Deflate"}`) } @@ -135,7 +138,7 @@ export function compute7zCompressArgs(compression: CompressionLevel | any | any, // 7z is very fast, so, use ultra compression /** @internal */ export async function archive(compression: CompressionLevel | null | undefined, format: string, outFile: string, dirToArchive: string, options: ArchiveOptions = {}): Promise { - const args = compute7zCompressArgs(compression, format, options) + const args = compute7zCompressArgs(format, compression, options) // remove file before - 7z doesn't overwrite file, but update await unlinkIfExists(outFile) diff --git a/packages/electron-builder/src/vm/mono.ts b/packages/electron-builder/src/vm/mono.ts index 3df20eb09fd..74f48db46ad 100644 --- a/packages/electron-builder/src/vm/mono.ts +++ b/packages/electron-builder/src/vm/mono.ts @@ -3,13 +3,12 @@ import { exec, ExecOptions, ExtraSpawnOptions, spawn } from "builder-util" import { VmManager } from "./vm" export class MonoVmManager extends VmManager { - constructor(private readonly currentDirectory: string) { + constructor() { super() } exec(file: string, args: Array, options?: ExecOptions, isLogOutIfDebug = true): Promise { return exec("mono", [file].concat(args), { - cwd: this.currentDirectory, ...options, }, isLogOutIfDebug) } @@ -17,14 +16,4 @@ export class MonoVmManager extends VmManager { spawn(file: string, args: Array, options?: SpawnOptions, extraOptions?: ExtraSpawnOptions): Promise { return spawn("mono", [file].concat(args), options, extraOptions) } - - toVmFile(file: string): string { - const parentPathLengthWithSlash = this.currentDirectory.length + 1 - if (parentPathLengthWithSlash < file.length && file[this.currentDirectory.length] === "/" && file.startsWith(this.currentDirectory)) { - return file.substring(parentPathLengthWithSlash) - } - else { - return super.toVmFile(file) - } - } } \ No newline at end of file diff --git a/packages/electron-builder/src/vm/wine.ts b/packages/electron-builder/src/vm/wine.ts index 8c7111c6d19..241f99fe698 100644 --- a/packages/electron-builder/src/vm/wine.ts +++ b/packages/electron-builder/src/vm/wine.ts @@ -3,7 +3,6 @@ import { ExecOptions, ExtraSpawnOptions, execWine } from "builder-util" import { VmManager } from "./vm" import * as path from "path" -/** @internal */ export class WineVmManager extends VmManager { constructor() { super() diff --git a/test/out/windows/__snapshots__/squirrelWindowsTest.js.snap b/test/out/windows/__snapshots__/squirrelWindowsTest.js.snap index 840ece57f46..2cc38c11610 100644 --- a/test/out/windows/__snapshots__/squirrelWindowsTest.js.snap +++ b/test/out/windows/__snapshots__/squirrelWindowsTest.js.snap @@ -16,11 +16,6 @@ Object { "arch": "x64", "file": "TestApp-1.1.0-full.nupkg", }, - Object { - "arch": "x64", - "file": "Test App ßW-1.1.0-win.zip", - "safeArtifactName": "TestApp-1.1.0-win.zip", - }, ], } `; @@ -114,6 +109,11 @@ Object { "arch": "x64", "file": "TestApp-1.1.0-full.nupkg", }, + Object { + "arch": "x64", + "file": "Test TestApp foo.zip", + "safeArtifactName": "TestApp-1.1.0-win.zip", + }, ], } `; diff --git a/test/src/windows/appxTest.ts b/test/src/windows/appxTest.ts index 06533c0d550..7338d996b2e 100644 --- a/test/src/windows/appxTest.ts +++ b/test/src/windows/appxTest.ts @@ -20,7 +20,7 @@ it.ifDevOrWinCi("AppX", app({ signedWin: true, })) -it.ifDevOrWinCi("certificateSubjectName", app({ +it.ifNotCi("certificateSubjectName", app({ targets: Platform.WINDOWS.createTarget(["appx"], Arch.x64), config: { win: { diff --git a/test/src/windows/squirrelWindowsTest.ts b/test/src/windows/squirrelWindowsTest.ts index 16fb923bad2..59af37ba517 100644 --- a/test/src/windows/squirrelWindowsTest.ts +++ b/test/src/windows/squirrelWindowsTest.ts @@ -3,14 +3,21 @@ import * as path from "path" import { CheckingWinPackager } from "../helpers/CheckingPackager" import { app, assertPack, copyTestAsset } from "../helpers/packTester" -test.ifAll.ifNotCiMac("Squirrel.Windows", app({targets: Platform.WINDOWS.createTarget(["squirrel", "zip"])}, {signedWin: true})) +test.ifAll.ifNotCiMac("Squirrel.Windows", app({ + targets: Platform.WINDOWS.createTarget(["squirrel"]), + config: { + win: { + compression: "normal", + } + } +}, {signedWin: true})) test.ifAll.ifNotCiMac("artifactName", app({ - targets: Platform.WINDOWS.createTarget(["squirrel"]), + targets: Platform.WINDOWS.createTarget(["squirrel", "zip"]), config: { win: { // tslint:disable:no-invalid-template-strings - artifactName: "Test ${name} foo.exe" + artifactName: "Test ${name} foo.${ext}", } } }))