Skip to content

Commit

Permalink
feat(squirrel.windows): Update Squirrel.Windows to 1.7.8
Browse files Browse the repository at this point in the history
Close #1762
  • Loading branch information
develar committed Oct 30, 2017
1 parent 5726a03 commit e574db8
Show file tree
Hide file tree
Showing 10 changed files with 117 additions and 104 deletions.
1 change: 1 addition & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ jobs:
- image: electronuserland/builder:wine-mono
environment:
JEST_JUNIT_OUTPUT: ./test-reports/test.xml
TZ: Europe/Berlin
steps:
- checkout
- run:
Expand Down
3 changes: 3 additions & 0 deletions packages/electron-builder-squirrel-windows/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand Down
133 changes: 70 additions & 63 deletions packages/electron-builder-squirrel-windows/src/squirrelPack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand Down Expand Up @@ -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<any>([
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<any>([
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)
Expand Down Expand Up @@ -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,
Expand Down
30 changes: 17 additions & 13 deletions packages/electron-builder-squirrel-windows/src/squirrelWindows.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand All @@ -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)
Expand Down Expand Up @@ -84,7 +88,7 @@ export default class SquirrelWindowsTarget extends Target {
extraMetadataSpecs: projectUrl == null ? null : `\n <projectUrl>${projectUrl}</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,
}

Expand Down
15 changes: 9 additions & 6 deletions packages/electron-builder/src/targets/archive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down Expand Up @@ -65,12 +65,13 @@ export interface ArchiveOptions {
dictSize?: number
excluded?: Array<string>

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")

Expand Down Expand Up @@ -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"}`)
}

Expand All @@ -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<string> {
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)

Expand Down
13 changes: 1 addition & 12 deletions packages/electron-builder/src/vm/mono.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,17 @@ 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<string>, options?: ExecOptions, isLogOutIfDebug = true): Promise<string> {
return exec("mono", [file].concat(args), {
cwd: this.currentDirectory,
...options,
}, isLogOutIfDebug)
}

spawn(file: string, args: Array<string>, options?: SpawnOptions, extraOptions?: ExtraSpawnOptions): Promise<any> {
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)
}
}
}
1 change: 0 additions & 1 deletion packages/electron-builder/src/vm/wine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
10 changes: 5 additions & 5 deletions test/out/windows/__snapshots__/squirrelWindowsTest.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -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",
},
],
}
`;
Expand Down Expand Up @@ -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",
},
],
}
`;
Expand Down
2 changes: 1 addition & 1 deletion test/src/windows/appxTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
13 changes: 10 additions & 3 deletions test/src/windows/squirrelWindowsTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}",
}
}
}))
Expand Down

0 comments on commit e574db8

Please sign in to comment.