diff --git a/.idea/dictionaries/develar.xml b/.idea/dictionaries/develar.xml index 35ae13010f8..c18c70f3813 100644 --- a/.idea/dictionaries/develar.xml +++ b/.idea/dictionaries/develar.xml @@ -7,6 +7,7 @@ appimage appleid appveyor + archiver archs aspx atime @@ -88,6 +89,7 @@ promisify psmdcp readpass + rels repos rimraf semver diff --git a/.travis.yml b/.travis.yml index 668ebaabca1..0c0c4639d9a 100755 --- a/.travis.yml +++ b/.travis.yml @@ -1,4 +1,4 @@ -osx_image: xcode7.3 +osx_image: xcode8 matrix: include: diff --git a/docker/winSign.sh b/docker/winSign.sh deleted file mode 100755 index 9a48e8bc86a..00000000000 --- a/docker/winSign.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env bash - -dir=${PWD##*/} -rm -rf ../${dir}.7z -7za a -m0=lzma2 -mx=9 -mfb=64 -md=64m -ms=on ../winCodeSign.7z . diff --git a/package.json b/package.json index 4aecb80bd36..571660248db 100644 --- a/package.json +++ b/package.json @@ -60,6 +60,8 @@ "dependencies": { "7zip-bin": "^1.0.6", "ansi-escapes": "^1.4.0", + "archiver": "^1.0.1", + "archiver-utils": "^1.2.0", "asar-electron-builder": "^0.13.2", "bluebird": "^3.4.1", "chalk": "^1.1.3", @@ -69,7 +71,6 @@ "debug": "^2.2.0", "electron-download": "^2.1.2", "electron-osx-sign": "^0.4.0-beta4", - "electron-winstaller-fixed": "~4.0.0", "extract-zip": "^1.5.0", "fs-extra-p": "^1.0.6", "hosted-git-info": "^2.1.5", @@ -102,7 +103,6 @@ ] }, "devDependencies": { - "path-sort": "^0.1.0", "@develar/semantic-release": "^6.3.6", "@types/debug": "0.0.28", "@types/mime": "0.0.28", @@ -117,6 +117,7 @@ "decompress-zip": "^0.3.0", "diff": "^2.2.3", "json8": "^0.9.2", + "path-sort": "^0.1.0", "pre-git": "^3.10.0", "ts-babel": "^1.0.4", "tslint": "^3.14.0-dev.1", diff --git a/src/targets/nsis.ts b/src/targets/nsis.ts index afa2a1a846a..7092c1876af 100644 --- a/src/targets/nsis.ts +++ b/src/targets/nsis.ts @@ -3,7 +3,7 @@ import { Arch, NsisOptions } from "../metadata" import { debug, doSpawn, handleProcess, use } from "../util/util" import * as path from "path" import { Promise as BluebirdPromise } from "bluebird" -import { getBin } from "../util/binDownload" +import { getBinFromBintray } from "../util/binDownload" import { v5 as uuid5 } from "uuid-1345" import { Target } from "../platformPackager" import { archiveApp } from "./archive" @@ -14,14 +14,14 @@ import semver = require("semver") //noinspection JSUnusedLocalSymbols const __awaiter = require("../util/awaiter") -const NSIS_VERSION = "nsis-3.0.0" +const NSIS_VERSION = "3.0.0" //noinspection SpellCheckingInspection const NSIS_SHA2 = "7741089f3ca13de879f87836156ef785eab49844cacbeeabaeaefd1ade325ee7" //noinspection SpellCheckingInspection const ELECTRON_BUILDER_NS_UUID = "50e065bc-3134-11e6-9bab-38c9862bdaf3" -const nsisPathPromise = getBin("nsis", NSIS_VERSION, `https://dl.bintray.com/electron-userland/bin/${NSIS_VERSION}.7z`, NSIS_SHA2) +const nsisPathPromise = getBinFromBintray("nsis", NSIS_VERSION, NSIS_SHA2) export default class NsisTarget extends Target { private readonly options: NsisOptions diff --git a/src/targets/squirrelPack.ts b/src/targets/squirrelPack.ts new file mode 100644 index 00000000000..4c8ac2beb66 --- /dev/null +++ b/src/targets/squirrelPack.ts @@ -0,0 +1,250 @@ +import * as path from "path" +import { Promise as BluebirdPromise } from "bluebird" +import { emptyDir, copy, createWriteStream, unlink } from "fs-extra-p" +import { spawn, exec } from "../util/util" +import { debug } from "../util/util" +import { WinPackager } from "../winPackager" + +const archiverUtil = require("archiver-utils") +const archiver = require("archiver") + +//noinspection JSUnusedLocalSymbols +const __awaiter = require("../util/awaiter") + +export function convertVersion(version: string): string { + const parts = version.split("-") + const mainVersion = parts.shift() + if (parts.length > 0) { + return [mainVersion, parts.join("-").replace(/\./g, "")].join("-") + } + else { + return mainVersion! + } +} + +function syncReleases(outputDirectory: string, options: SquirrelOptions) { + const args = prepareArgs(["-u", options.remoteReleases!, "-r", outputDirectory], path.join(options.vendorPath, "SyncReleases.exe")) + if (options.remoteToken) { + args.push("-t", options.remoteToken) + } + return spawn(process.platform === "win32" ? path.join(options.vendorPath, "SyncReleases.exe") : "mono", args) +} + +export interface SquirrelOptions { + vendorPath: string + remoteReleases?: string + remoteToken?: string + loadingGif?: string + productName?: string + name: string + packageCompressionLevel?: number + version: string + msi?: any + + owners?: string + description?: string + iconUrl?: string + authors?: string + extraMetadataSpecs?: string + copyright?: string +} + +export async function buildInstaller(options: SquirrelOptions, outputDirectory: string, stageDir: string, setupExe: string, packager: WinPackager, appOutDir: string) { + const appUpdate = path.join(stageDir, "Update.exe") + const promises = [ + copy(path.join(options.vendorPath, "Update.exe"), appUpdate) + .then(() => packager.sign(appUpdate)), + emptyDir(outputDirectory) + ] + if (options.remoteReleases) { + promises.push(syncReleases(outputDirectory, options)) + } + await BluebirdPromise.all(promises) + + const embeddedArchiveFile = path.join(stageDir, "setup.zip") + const embeddedArchive = archiver("zip", {zlib: {level: options.packageCompressionLevel == null ? 6 : options.packageCompressionLevel}}) + const embeddedArchiveOut = createWriteStream(embeddedArchiveFile) + const embeddedArchivePromise = new BluebirdPromise(function (resolve, reject) { + embeddedArchive.on("error", reject) + embeddedArchiveOut.on("close", resolve) + }) + embeddedArchive.pipe(embeddedArchiveOut) + + embeddedArchive.file(appUpdate, {name: "Update.exe"}) + embeddedArchive.file(options.loadingGif ? path.resolve(options.loadingGif) : path.join(__dirname, "..", "..", "templates", "install-spinner.gif"), {name: "background.gif"}) + + const version = convertVersion(options.version) + const packageName = `${options.name}-${version}-full.nupkg` + const nupkgPath = path.join(outputDirectory, packageName) + const setupPath = path.join(outputDirectory, setupExe || `${options.name || options.productName}Setup.exe`) + + await BluebirdPromise.all([ + pack(options, appOutDir, appUpdate, nupkgPath, version, options.packageCompressionLevel), + copy(path.join(options.vendorPath, "Setup.exe"), setupPath), + ]) + + embeddedArchive.file(nupkgPath, {name: packageName}) + + const releaseEntry = await releasify(options, nupkgPath, outputDirectory, packageName) + + embeddedArchive.append(releaseEntry, {name: "RELEASES"}) + embeddedArchive.finalize() + await embeddedArchivePromise + + const writeZipToSetup = path.join(options.vendorPath, "WriteZipToSetup.exe") + await exec(process.platform === "win32" ? writeZipToSetup : "wine", prepareArgs([setupPath, embeddedArchiveFile], writeZipToSetup)) + + await packager.signAndEditResources(setupPath) + if (options.msi && process.platform === "win32") { + const outFile = setupExe.replace(".exe", ".msi") + await msi(options, nupkgPath, setupPath, outputDirectory, outFile) + await packager.signAndEditResources(path.join(outputDirectory, outFile)) + } +} + +async function pack(options: SquirrelOptions, directory: string, updateFile: string, outFile: string, version: string, packageCompressionLevel?: number) { + const archive = archiver("zip", {zlib: {level: packageCompressionLevel == null ? 9 : packageCompressionLevel}}) + // const archiveOut = createWriteStream('/Users/develar/test.zip') + const archiveOut = createWriteStream(outFile) + const archivePromise = new BluebirdPromise(function (resolve, reject) { + archive.on("error", reject) + archiveOut.on("close", resolve) + }) + archive.pipe(archiveOut) + + const author = options.authors || options.owners + const copyright = options.copyright || `Copyright © ${new Date().getFullYear()} ${author}` + const nuspecContent = ` + + + ${options.name} + ${version} + ${options.productName} + ${author} + ${options.owners || options.authors} + ${options.iconUrl} + false + ${options.description} + ${copyright}${options.extraMetadataSpecs || ""} + +` + debug(`Created NuSpec file:\n${nuspecContent}`) + archive.append(nuspecContent.replace(/\n/, "\r\n"), {name: `${encodeURI(options.name).replace(/%5B/g, "[").replace(/%5D/g, "]")}.nuspec`}) + + //noinspection SpellCheckingInspection + archive.append(` + + + +`.replace(/\n/, "\r\n"), {name: ".rels", prefix: "_rels"}) + + //noinspection SpellCheckingInspection + archive.append(` + + + + + + + + + + + + + + +`.replace(/\n/, "\r\n"), {name: "[Content_Types].xml"}) + + archive.append(` + + ${author} + ${options.description} + ${options.name} + ${version} + + ${options.productName} + NuGet, Version=2.8.50926.602, Culture=neutral, PublicKeyToken=null;Microsoft Windows NT 6.2.9200.0;.NET Framework 4 +`.replace(/\n/, "\r\n"), {name: "1.psmdcp", prefix: "package/services/metadata/core-properties"}) + + archive.file(updateFile, {name: "Update.exe", prefix: "lib/net45"}) + encodedZip(archive, directory, "lib/net45") + 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(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") +} + +async function msi(options: SquirrelOptions, nupkgPath: string, setupPath: string, outputDirectory: string, outFile: string) { + const args = [ + "--createMsi", nupkgPath, + "--bootstrapperExe", setupPath + ] + await exec(process.platform === "win32" ? path.join(options.vendorPath, "Update.com") : "mono", prepareArgs(args, path.join(options.vendorPath, "Update-Mono.exe"))) + //noinspection SpellCheckingInspection + await exec(path.join(options.vendorPath, "candle.exe"), ["-nologo", "-ext", "WixNetFxExtension", "-out", "Setup.wixobj", "Setup.wxs"], { + cwd: outputDirectory, + }) + //noinspection SpellCheckingInspection + await exec(path.join(options.vendorPath, "light.exe"), ["-ext", "WixNetFxExtension", "-sval", "-out", outFile, "Setup.wixobj"], { + cwd: outputDirectory, + }) + + //noinspection SpellCheckingInspection + await BluebirdPromise.all([ + unlink(path.join(outputDirectory, "Setup.wxs")), + unlink(path.join(outputDirectory, "Setup.wixobj")), + unlink(path.join(outputDirectory, outFile.replace(".msi", ".wixpdb"))).catch(e => debug(e.toString())), + ]) +} + +function prepareArgs(args: Array, exePath: string) { + if (process.platform !== "win32") { + args.unshift(exePath) + } + return args +} + +function encodedZip(archive: any, dir: string, prefix: string) { + archiverUtil.walkdir(dir, function (error: any, files: any) { + if (error) { + archive.emit("error", error) + return + } + + for (let file of files) { + if (file.stats.isDirectory()) { + continue + } + + // GBK file name encoding (or Non-English file name) caused a problem + const entryData = { + name: encodeURI(file.relative.replace(/\\/g, "/")).replace(/%5B/g, "[").replace(/%5D/g, "]"), + prefix: prefix, + stats: file.stats, + } + archive._append(file.path, entryData) + } + + archive.finalize() + }) +} \ No newline at end of file diff --git a/src/targets/squirrelWindows.ts b/src/targets/squirrelWindows.ts index c1797e07d1c..deb42b094dc 100644 --- a/src/targets/squirrelWindows.ts +++ b/src/targets/squirrelWindows.ts @@ -1,14 +1,22 @@ import { WinPackager } from "../winPackager" import { getArchSuffix, Target } from "../platformPackager" -import { Arch, WinBuildOptions } from "../metadata" -import { createWindowsInstaller, convertVersion } from "electron-winstaller-fixed" +import { Arch } from "../metadata" import * as path from "path" import { warn, log } from "../util/log" import { getRepositoryInfo } from "../repositoryInfo" +import { getBinFromBintray } from "../util/binDownload" +import { tmpdir } from "os" +import { getTempName } from "../util/util" +import { emptyDir, remove } from "fs-extra-p" +import { buildInstaller, convertVersion, SquirrelOptions } from "./squirrelPack" //noinspection JSUnusedLocalSymbols const __awaiter = require("../util/awaiter") +const SW_VERSION = "1.4.4" +//noinspection SpellCheckingInspection +const SW_SHA2 = "98e1d81c80d7afc1bcfb37f3b224dc4f761088506b9c28ccd72d1cf8752853ba" + export default class SquirrelWindowsTarget extends Target { constructor(private packager: WinPackager) { super("squirrel") @@ -26,8 +34,22 @@ export default class SquirrelWindowsTarget extends Target { const installerOutDir = path.join(appOutDir, "..", `win${getArchSuffix(arch)}`) - const distOptions = await this.computeEffectiveDistOptions(appOutDir, installerOutDir, setupFileName) - await createWindowsInstaller(distOptions) + const distOptions = await this.computeEffectiveDistOptions() + + const stageDir = path.join(tmpdir(), getTempName("squirrel-windows-builder")) + await emptyDir(stageDir) + try { + await buildInstaller(distOptions, installerOutDir, stageDir, setupFileName, this.packager, appOutDir) + } + finally { + try { + await remove(stageDir) + } + catch (e) { + // ignore + } + } + this.packager.dispatchArtifactCreated(path.join(installerOutDir, setupFileName), `${appInfo.name}-Setup-${version}${archSuffix}.exe`) const packagePrefix = `${appInfo.name}-${convertVersion(version)}-` @@ -39,7 +61,7 @@ export default class SquirrelWindowsTarget extends Target { this.packager.dispatchArtifactCreated(path.join(installerOutDir, "RELEASES")) } - async computeEffectiveDistOptions(appOutDir: string, installerOutDir: string, setupExeName: string): Promise { + async computeEffectiveDistOptions(): Promise { const packager = this.packager let iconUrl = packager.platformSpecificBuildOptions.iconUrl || packager.devMetadata.build.iconUrl if (iconUrl == null) { @@ -57,30 +79,17 @@ export default class SquirrelWindowsTarget extends Target { const appInfo = packager.appInfo const projectUrl = await appInfo.computePackageUrl() - const cscInfo = await packager.cscInfo const options: any = Object.assign({ name: appInfo.name, productName: appInfo.productName, - exe: `${appInfo.productFilename}.exe`, - setupExe: setupExeName, - msiExe: setupExeName.replace(".exe", ".msi"), - title: appInfo.productName, - appDirectory: appOutDir, - outputDirectory: installerOutDir, version: appInfo.version, description: appInfo.description, authors: appInfo.companyName, iconUrl: iconUrl, - setupIcon: await packager.getIconPath(), - certificateFile: cscInfo == null ? null : cscInfo.file, - certificatePassword: cscInfo == null ? null : cscInfo.password, - fixUpPaths: false, - skipUpdateIcon: true, - usePackageJson: false, extraMetadataSpecs: projectUrl == null ? null : `\n ${projectUrl}`, copyright: appInfo.copyright, packageCompressionLevel: packager.devMetadata.build.compression === "store" ? 0 : 9, - sign: this.packager.signAndEditResources.bind(this.packager), + vendorPath: await getBinFromBintray("Squirrel.Windows", SW_VERSION, SW_SHA2) }, packager.platformSpecificBuildOptions) if (!("loadingGif" in options)) { diff --git a/src/util/binDownload.ts b/src/util/binDownload.ts index 09ef083ed4a..b7d3eaf520d 100644 --- a/src/util/binDownload.ts +++ b/src/util/binDownload.ts @@ -17,6 +17,11 @@ export function downloadFpm(version: string, osAndArch: string): Promise .then(it => path.join(it, "fpm")) } +export function getBinFromBintray(name: string, version: string, sha2?: string): Promise { + const dirName = `${name}-${version}` + return getBin(name, dirName, `https://dl.bintray.com/electron-userland/bin/${dirName}.7z`, sha2) +} + export function getBin(name: string, dirName: string, url: string, sha2?: string): Promise { let promise = versionToPromise.get(dirName) // if rejected, we will try to download again diff --git a/src/windowsCodeSign.ts b/src/windowsCodeSign.ts index a0ef1cc6ce5..3b567321407 100644 --- a/src/windowsCodeSign.ts +++ b/src/windowsCodeSign.ts @@ -2,14 +2,14 @@ import { spawn } from "./util/util" import { rename } from "fs-extra-p" import * as path from "path" import { release } from "os" -import { getBin } from "./util/binDownload" +import { getBinFromBintray } from "./util/binDownload" //noinspection JSUnusedLocalSymbols const __awaiter = require("./util/awaiter") -const TOOLS_VERSION = "winCodeSign-1.3.0" +const TOOLS_VERSION = "1.4.0" export function getSignVendorPath() { - return getBin("winCodeSign", TOOLS_VERSION, `https://dl.bintray.com/electron-userland/bin/${TOOLS_VERSION}.7z`, "cfe9569f7e5aef605c11704d90a3ce22d2445984b51f145c97140eec68bd9833") + return getBinFromBintray("winCodeSign", TOOLS_VERSION, "0496cf9d3c68cf00c3873a20794361c782d355c566f0b31a69422571deffeb69") } export interface SignOptions { diff --git a/templates/install-spinner.gif b/templates/install-spinner.gif new file mode 100644 index 00000000000..2ee0e124289 --- /dev/null +++ b/templates/install-spinner.gif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ce636d201ef86ffbf4ee8c8762b4d9dc255be9d5f490d0a22e36fe0c938f7244 +size 44410 diff --git a/test/src/helpers/packTester.ts b/test/src/helpers/packTester.ts index 46d28198445..9e0ecba1d31 100755 --- a/test/src/helpers/packTester.ts +++ b/test/src/helpers/packTester.ts @@ -12,7 +12,7 @@ import { tmpdir } from "os" import { getArchSuffix, Target } from "out/platformPackager" import pathSorter = require("path-sort") import DecompressZip = require("decompress-zip") -import { convertVersion } from "electron-winstaller-fixed" +import { convertVersion } from "out/targets/squirrelPack" import { spawnNpmProduction } from "out/util/util" //noinspection JSUnusedLocalSymbols diff --git a/test/src/winPackagerTest.ts b/test/src/winPackagerTest.ts index 3a7b7403b7c..5c14c97cd09 100755 --- a/test/src/winPackagerTest.ts +++ b/test/src/winPackagerTest.ts @@ -145,14 +145,12 @@ class CheckingWinPackager extends WinPackager { async pack(outDir: string, arch: Arch, targets: Array, postAsyncTasks: Array>): Promise { // skip pack - const appOutDir = this.computeAppOutDir(outDir, arch) - this.effectivePackOptions = await this.computePackOptions() const helperClass: typeof SquirrelWindowsTarget = require("out/targets/squirrelWindows").default - this.effectiveDistOptions = await (new helperClass(this).computeEffectiveDistOptions(appOutDir, "foo", "Foo.exe")) + this.effectiveDistOptions = await (new helperClass(this).computeEffectiveDistOptions()) - await this.sign(appOutDir) + await this.sign(this.computeAppOutDir(outDir, arch)) } packageInDistributableFormat(outDir: string, appOutDir: string, arch: Arch, targets: Array, promises: Array>): void { diff --git a/typings/electron-winstaller-fixed.d.ts b/typings/electron-winstaller-fixed.d.ts deleted file mode 100644 index 6739ffc9ad8..00000000000 --- a/typings/electron-winstaller-fixed.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -declare module "electron-winstaller-fixed" { - export function createWindowsInstaller(options: any): Promise - - export function convertVersion(version: string): string -} \ No newline at end of file