Skip to content

Commit

Permalink
feat: NSIS sign uninstaller
Browse files Browse the repository at this point in the history
Closes #526
  • Loading branch information
develar committed Aug 14, 2016
1 parent 361b369 commit 17c0a82
Show file tree
Hide file tree
Showing 22 changed files with 637 additions and 671 deletions.
1 change: 1 addition & 0 deletions .idea/dictionaries/develar.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions docs/Options.md
Expand Up @@ -167,6 +167,7 @@ NSIS only, [in progress](https://github.com/electron-userland/electron-builder/i
| --- | ---
| **ext** | <a name="FileAssociation-ext"></a>The extension (minus the leading period). e.g. `png`
| **name** | <a name="FileAssociation-name"></a>The name. e.g. `PNG`
| description | <a name="FileAssociation-description"></a>*windows-only.* The description.

<a name="MetadataDirectories"></a>
## `.directories`
Expand Down
2 changes: 1 addition & 1 deletion package.json
Expand Up @@ -76,7 +76,7 @@
"hosted-git-info": "^2.1.5",
"image-size": "^0.5.0",
"isbinaryfile": "^3.0.1",
"lodash.template": "^4.3.0",
"lodash.template": "^4.4.0",
"mime": "^1.3.4",
"minimatch": "^3.0.3",
"normalize-package-data": "^2.3.5",
Expand Down
5 changes: 5 additions & 0 deletions src/metadata.ts
Expand Up @@ -488,6 +488,11 @@ export interface FileAssociation {
The name. e.g. `PNG`
*/
readonly name: string

/*
*windows-only.* The description.
*/
readonly description?: string
}

/*
Expand Down
4 changes: 1 addition & 3 deletions src/platformPackager.ts
Expand Up @@ -122,9 +122,7 @@ export abstract class PlatformPackager<DC extends PlatformSpecificBuildOptions>
return options == null ? Object.create(null) : options
}

createTargets(targets: Array<string>, mapper: (name: string, factory: (outDir: string) => Target) => void, cleanupTasks: Array<() => Promise<any>>): void {
throw new Error("not implemented")
}
abstract createTargets(targets: Array<string>, mapper: (name: string, factory: (outDir: string) => Target) => void, cleanupTasks: Array<() => Promise<any>>): void

protected getCscPassword(): string {
const password = this.options.cscKeyPassword || process.env.CSC_KEY_PASSWORD
Expand Down
7 changes: 2 additions & 5 deletions src/targets/LinuxTargetHelper.ts
Expand Up @@ -19,11 +19,8 @@ export class LinuxTargetHelper {

constructor(private packager: PlatformPackager<LinuxBuildOptions>, cleanupTasks: Array<() => Promise<any>>) {
const tempDir = path.join(tmpdir(), getTempName("electron-builder-linux"))
this.tempDirPromise = emptyDir(tempDir)
.then(() => {
cleanupTasks.push(() => remove(tempDir))
return tempDir
})
this.tempDirPromise = emptyDir(tempDir).thenReturn(tempDir)
cleanupTasks.push(() => remove(tempDir))

this.icons = this.computeDesktopIcons()
}
Expand Down
78 changes: 54 additions & 24 deletions src/targets/nsis.ts
@@ -1,15 +1,16 @@
import { WinPackager } from "../winPackager"
import { Arch, NsisOptions } from "../metadata"
import { debug, doSpawn, handleProcess, use } from "../util/util"
import { exec, debug, doSpawn, handleProcess, use, getTempName } from "../util/util"
import * as path from "path"
import { Promise as BluebirdPromise } from "bluebird"
import { getBinFromBintray } from "../util/binDownload"
import { v5 as uuid5 } from "uuid-1345"
import { Target } from "../platformPackager"
import { archiveApp } from "./archive"
import { subTask, task } from "../util/log"
import { unlink, readFile } from "fs-extra-p"
import { subTask, task, log } from "../util/log"
import { unlink, readFile, remove } from "fs-extra-p"
import semver = require("semver")
import { tmpdir } from "os"

//noinspection JSUnusedLocalSymbols
const __awaiter = require("../util/awaiter")
Expand All @@ -28,7 +29,9 @@ export default class NsisTarget extends Target {

private archs: Map<Arch, Promise<string>> = new Map()

constructor(private packager: WinPackager, private outDir: string) {
private readonly nsisTemplatesDir = path.join(__dirname, "..", "..", "templates", "nsis")

constructor(private packager: WinPackager, private outDir: string, private cleanupTasks: Array<() => Promise<any>>) {
super("nsis")

this.options = packager.info.devMetadata.build.nsis || Object.create(null)
Expand All @@ -52,8 +55,8 @@ export default class NsisTarget extends Target {
const iconPath = await packager.getIconPath()
const appInfo = packager.appInfo
const version = appInfo.version
const installerPath = path.join(this.outDir, `${appInfo.productFilename} Setup ${version}.exe`)

const installerPath = path.join(this.outDir, `${appInfo.productFilename} Setup ${version}.exe`)
const guid = this.options.guid || await BluebirdPromise.promisify(uuid5)({namespace: ELECTRON_BUILDER_NS_UUID, name: appInfo.id})
const defines: any = {
APP_ID: appInfo.id,
Expand Down Expand Up @@ -122,9 +125,7 @@ export default class NsisTarget extends Target {

const commands: any = {
OutFile: `"${installerPath}"`,
// LoadLanguageFile: '"${NSISDIR}/Contrib/Language files/English.nlf"',
VIProductVersion: `${parsedVersion.major}.${parsedVersion.minor}.${parsedVersion.patch}.${appInfo.buildNumber || "0"}`,
// VIFileVersion: packager.appInfo.buildVersion,
VIAddVersionKey: versionKey,
}

Expand All @@ -151,7 +152,29 @@ export default class NsisTarget extends Target {
return
}

await subTask(`Executing makensis`, this.executeMakensis(defines, commands))
const customScriptPath = await this.getResource(this.options.script, "installer.nsi")
const script = await readFile(customScriptPath || path.join(this.nsisTemplatesDir, "installer.nsi"), "utf8")

if (customScriptPath == null) {
const uninstallerPath = path.join(tmpdir(), `${getTempName("electron-builder")}.exe`)
this.cleanupTasks.push(() => remove(uninstallerPath))

defines.BUILD_UNINSTALLER = null
defines.UNINSTALLER_OUT_FILE = path.win32.join("Z:", uninstallerPath)
await subTask(`Executing makensis — uninstaller`, this.executeMakensis(defines, commands, false, script))
const isWin = process.platform === "win32"
await exec(isWin ? installerPath : "wine", isWin ? [] : [installerPath])
await packager.sign(uninstallerPath)

delete defines.BUILD_UNINSTALLER
// platform-specific path, not wine
defines.UNINSTALLER_OUT_FILE = uninstallerPath
}
else {
log("Custom NSIS script is used - uninstaller is not signed by electron-builder")
}

await subTask(`Executing makensis — installer`, this.executeMakensis(defines, commands, true, script))
await packager.sign(installerPath)

this.packager.dispatchArtifactCreated(installerPath, `${appInfo.name}-Setup-${version}.exe`)
Expand All @@ -172,7 +195,7 @@ export default class NsisTarget extends Target {
return null
}

private async executeMakensis(defines: any, commands: any) {
private async executeMakensis(defines: any, commands: any, isInstaller: boolean, originalScript: string) {
const args: Array<string> = ["-WX"]
for (let name of Object.keys(defines)) {
const value = defines[name]
Expand All @@ -196,37 +219,44 @@ export default class NsisTarget extends Target {
}
}

const nsisTemplatesDir = path.join(__dirname, "..", "..", "templates", "nsis")
args.push("-")

const binDir = process.platform === "darwin" ? "mac" : (process.platform === "win32" ? "Bin" : "linux")
const nsisPath = await nsisPathPromise

const packager = this.packager
// CFBundleTypeName
// https://developer.apple.com/library/ios/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html#//apple_ref/doc/uid/20001431-101685
// CFBundleTypeExtensions
const fileAssociations = asArray(packager.devMetadata.build.fileAssociations).concat(asArray(packager.platformSpecificBuildOptions.fileAssociations))
let registerFileAssociationsScript = ""
let unregisterFileAssociationsScript = ""
let script = await readFile((await this.getResource(this.options.script, "installer.nsi")) || path.join(nsisTemplatesDir, "installer.nsi"), "utf8")

let script = originalScript
const customInclude = await this.getResource(this.options.include, "installer.nsh")
if (customInclude != null) {
script = `!include "${customInclude}"\n!addincludedir "${this.packager.buildResourcesDir}"\n${script}`
}

if (fileAssociations.length !== 0) {
script = "!include FileAssociation.nsh\n" + script
for (let item of fileAssociations) {
registerFileAssociationsScript += '${RegisterExtension} "$INSTDIR\\${APP_EXECUTABLE_FILENAME}" ' + `"${normalizeExt(item.ext)}" "${item.name}"\n`
if (isInstaller) {
let registerFileAssociationsScript = ""
for (let item of fileAssociations) {
const icon = '"$INSTDIR\\${APP_EXECUTABLE_FILENAME},0"'
const commandText = `"Open with ${this.packager.appInfo.productName}"`
const command = '"$INSTDIR\\${APP_EXECUTABLE_FILENAME} $\\"%1$\\""'
registerFileAssociationsScript += ` !insertmacro APP_ASSOCIATE "${normalizeExt(item.ext)}" "${item.name}" "${item.description || ""}" ${icon} ${commandText} ${command}\n`
}
script = `!macro registerFileAssociations\n${registerFileAssociationsScript}!macroend\n${script}`
}

for (let item of fileAssociations) {
unregisterFileAssociationsScript += "${UnRegisterExtension} " + `"${normalizeExt(item.ext)}" "${item.name}"\n`
else {
let unregisterFileAssociationsScript = ""
for (let item of fileAssociations) {
unregisterFileAssociationsScript += ` !insertmacro APP_UNASSOCIATE "${normalizeExt(item.ext)}" "${item.name}"\n`
}
script = `!macro unregisterFileAssociations\n${unregisterFileAssociationsScript}!macroend\n${script}`
}
}

script = script.replace("!insertmacro registerFileAssociations", registerFileAssociationsScript)
script = script.replace("!insertmacro unregisterFileAssociations", unregisterFileAssociationsScript)

if (debug.enabled) {
process.stdout.write("\n\nNSIS script:\n\n" + script + "\n\n---\nEnd of NSIS script.\n\n")
}
Expand All @@ -236,7 +266,7 @@ export default class NsisTarget extends Target {
const childProcess = doSpawn(command, args, {
// we use NSIS_CONFIG_CONST_DATA_PATH=no to build makensis on Linux, but in any case it doesn't use stubs as MacOS/Windows version, so, we explicitly set NSISDIR
env: Object.assign({}, process.env, {NSISDIR: nsisPath}),
cwd: nsisTemplatesDir,
cwd: this.nsisTemplatesDir,
}, true)
handleProcess("close", childProcess, command, resolve, reject)

Expand All @@ -245,9 +275,9 @@ export default class NsisTarget extends Target {
}
}

// nsis — add leading dot, mac — remove leading dot
// remove leading dot
function normalizeExt(ext: string) {
return ext.startsWith(".") ? ext : `.${ext}`
return ext.startsWith(".") ? ext.substring(1) : ext
}

function asArray<T>(v: n | T | Array<T>): Array<T> {
Expand Down
15 changes: 3 additions & 12 deletions src/targets/squirrelWindows.ts
Expand Up @@ -18,7 +18,7 @@ const SW_VERSION = "1.4.4"
const SW_SHA2 = "98e1d81c80d7afc1bcfb37f3b224dc4f761088506b9c28ccd72d1cf8752853ba"

export default class SquirrelWindowsTarget extends Target {
constructor(private packager: WinPackager) {
constructor(private packager: WinPackager, private cleanupTasks: Array<() => Promise<any>>) {
super("squirrel")
}

Expand All @@ -38,17 +38,8 @@ export default class SquirrelWindowsTarget extends Target {

const stageDir = path.join(tmpdir(), getTempName("squirrel-windows-builder"))
await emptyDir(stageDir)
try {
await buildInstaller(<SquirrelOptions>distOptions, installerOutDir, stageDir, setupFileName, this.packager, appOutDir)
}
finally {
try {
await remove(stageDir)
}
catch (e) {
// ignore
}
}
this.cleanupTasks.push(() => remove(stageDir))
await buildInstaller(<SquirrelOptions>distOptions, installerOutDir, stageDir, setupFileName, this.packager, appOutDir)

this.packager.dispatchArtifactCreated(path.join(installerOutDir, setupFileName), `${appInfo.name}-Setup-${version}${archSuffix}.exe`)

Expand Down
4 changes: 2 additions & 2 deletions src/winPackager.ts
Expand Up @@ -75,13 +75,13 @@ export class WinPackager extends PlatformPackager<WinBuildOptions> {
if (name === DEFAULT_TARGET || name === "squirrel") {
mapper("squirrel", () => {
const targetClass: typeof SquirrelWindowsTarget = require("./targets/squirrelWindows").default
return new targetClass(this)
return new targetClass(this, cleanupTasks)
})
}
else if (name === "nsis") {
mapper(name, outDir => {
const targetClass: typeof NsisTarget = require("./targets/nsis").default
return new targetClass(this, outDir)
return new targetClass(this, outDir, cleanupTasks)
})
}
else {
Expand Down

0 comments on commit 17c0a82

Please sign in to comment.