diff --git a/.idea/dictionaries/develar.xml b/.idea/dictionaries/develar.xml
index 9dd44fdb82c..283d9b26bb1 100644
--- a/.idea/dictionaries/develar.xml
+++ b/.idea/dictionaries/develar.xml
@@ -4,8 +4,10 @@
actperepo
appveyor
archs
+ aspx
authenticode
awaiter
+ bintray
buildpack
circleci
clcerts
@@ -27,6 +29,9 @@
gtar
hicolor
icnsutils
+ inno
+ installmode
+ instdir
keyserver
libappindicator
libexec
@@ -37,10 +42,12 @@
lzma
lzop
makedeb
+ makensis
minimatch
mkdirp
mpass
multilib
+ multiuser
nokeys
nomacver
noninteractive
@@ -54,6 +61,7 @@
pkcs
postinstall
productbuild
+ progexe
promisify
psmdcp
repos
@@ -63,7 +71,9 @@
tsconfig
udbz
udro
+ unicon
userprofile
+ valuename
veyor
winstaller
xamarin
diff --git a/docker/nsis.sh b/docker/nsis.sh
new file mode 100755
index 00000000000..4340c43ae89
--- /dev/null
+++ b/docker/nsis.sh
@@ -0,0 +1,35 @@
+#!/usr/bin/env bash
+set -e
+
+rm -rf Docs
+rm -rf NSIS.chm
+rm -rf Examples
+rm -rf Plugins/x86-ansi
+
+# nsProcess plugin
+curl -L http://nsis.sourceforge.net/mediawiki/images/1/18/NsProcess.zip > a.zip
+7za x a.zip -oa
+mv a/Plugin/nsProcessW.dll Plugins/x86-unicode/nsProcess.dll
+mv a/Include/nsProcess.nsh Include/nsProcess.nsh
+unlink a.zip
+rm -rf a
+
+# UAC plugin
+curl -L http://nsis.sourceforge.net/mediawiki/images/8/8f/UAC.zip > a.zip
+7za x a.zip -oa
+mv a/Plugins/x86-unicode/UAC.dll Plugins/x86-unicode/UAC.dll
+mv a/UAC.nsh Include/UAC.nsh
+unlink a.zip
+rm -rf a
+
+# WinShell
+curl -L http://nsis.sourceforge.net/mediawiki/images/5/54/WinShell.zip > a.zip
+7za x a.zip -oa
+mv a/Plugins/x86-unicode/WinShell.dll Plugins/x86-unicode/WinShell.dll
+unlink a.zip
+rm -rf a
+
+dir=${PWD##*/}
+cd ..
+rm -rf ${dir}.7z
+7za a -m0=lzma2 -mx=9 -mfb=64 -md=64m -ms=on ${dir}.7z ${dir}
diff --git a/docs/NSIS.md b/docs/NSIS.md
new file mode 100644
index 00000000000..6a64b419b69
--- /dev/null
+++ b/docs/NSIS.md
@@ -0,0 +1,10 @@
+# GUID vs Application Name
+
+Windows requires to use registry keys (e.g. INSTALL/UNINSTALL info). Squirrel.Windows simply uses application name as key.
+But it is not robust — Google can use key Google Chrome SxS, because it is a Google.
+
+So, it is better to use [GUID](http://stackoverflow.com/a/246935/1910191).
+You are not forced to explicitly specify it — name-based [UUID v5](https://en.wikipedia.org/wiki/Universally_unique_identifier#Version_5_.28SHA-1_hash_.26_namespace.29) will be generated from your [appId](https://github.com/electron-userland/electron-builder/wiki/Options#BuildMetadata-appId) or [name](https://github.com/electron-userland/electron-builder/wiki/Options#AppMetadata-name).
+It means that you **should not change appId** once your application in use (or name if `appId` was not set). Application product name (title) or description can be safely changed.
+
+You can explicitly set guid using option [nsis.guid](https://github.com/electron-userland/electron-builder/wiki/Options#NsisOptions-guid), but it is not recommended — consider using [appId](https://github.com/electron-userland/electron-builder/wiki/Options#BuildMetadata-appId).
\ No newline at end of file
diff --git a/docs/Options.md b/docs/Options.md
index 130ee0960d1..1b92d85927f 100644
--- a/docs/Options.md
+++ b/docs/Options.md
@@ -50,7 +50,7 @@ Here documented only `electron-builder` specific options:
## `.build`
| Name | Description
| --- | ---
-| app-bundle-id | *OS X-only.* The app bundle ID. See [CFBundleIdentifier](https://developer.apple.com/library/ios/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html#//apple_ref/doc/uid/20001431-102070).
+| appId |
The application id. Used as [CFBundleIdentifier](https://developer.apple.com/library/ios/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html#//apple_ref/doc/uid/20001431-102070) for OS X and as [Application User Model ID](https://msdn.microsoft.com/en-us/library/windows/desktop/dd378459(v=vs.85).aspx) for Windows.
For windows only NSIS target supports it. Squirrel.Windows is not fixed yet.
Defaults to com.electron.${name}
. It is strongly recommended that an explicit ID be set.
| app-category-type | *OS X-only.* The application category type, as shown in the Finder via *View -> Arrange by Application Category* when viewing the Applications directory.
For example, app-category-type=public.app-category.developer-tools
will set the application category to *Developer Tools*.
Valid values are listed in [Apple’s documentation](https://developer.apple.com/library/ios/documentation/General/Reference/InfoPlistKeyReference/Articles/LaunchServicesKeys.html#//apple_ref/doc/uid/TP40009250-SW8).
| asar | Whether to package the application’s source code into an archive, using [Electron’s archive format](https://github.com/electron/asar). Defaults to true
. Reasons why you may want to disable this feature are described in [an application packaging tutorial in Electron’s documentation](http://electron.atom.io/docs/latest/tutorial/application-packaging/#limitations-on-node-api/).
Or you can pass object of any asar options.
| productName | See [AppMetadata.productName](#AppMetadata-productName).
@@ -60,6 +60,7 @@ Here documented only `electron-builder` specific options:
| osx | See [.build.osx](#OsXBuildOptions).
| mas | See [.build.mas](#MasBuildOptions).
| win | See [.build.win](#LinuxBuildOptions).
+| nsis | See [.build.nsis](#NsisOptions).
| linux | See [.build.linux](#LinuxBuildOptions).
| compression | The compression level, one of `store`, `normal`, `maximum` (default: `normal`). If you want to rapidly test build, `store` can reduce build time significantly.
| afterPack | *programmatic API only* The function to be run after pack (but before pack into distributable format and sign). Promise must be returned.
@@ -100,6 +101,17 @@ MAS (Mac Application Store) specific options (in addition to `build.osx`).
| remoteToken | Authentication token for remote updates
| signingHashAlgorithms | Array of signing algorithms used. Defaults to `['sha1', 'sha256']`
+
+### `.build.nsis`
+
+NSIS target support in progress — not polished and not fully tested and checked.
+
+| Name | Description
+| --- | ---
+| perMachine | Mark "all users" (per-machine) as default. Not recommended. Defaults to `false`.
+| allowElevation | Allow requesting for elevation. If false, user will have to restart installer with elevated permissions. Defaults to `true`.
+| oneClick | One-click installation. Defaults to `true`.
+
### `.build.linux`
| Name | Description
diff --git a/package.json b/package.json
index 80fbaa056be..4eada13f533 100644
--- a/package.json
+++ b/package.json
@@ -79,6 +79,7 @@
"read-package-json": "^2.0.4",
"signcode-tf": "~0.7.3",
"source-map-support": "^0.4.0",
+ "uuid-1345": "^0.99.6",
"yargs": "^4.7.1"
},
"optionalDependencies": {
@@ -128,5 +129,8 @@
"test/out/*"
]
},
- "typings": "./out/electron-builder.d.ts"
+ "typings": "./out/electron-builder.d.ts",
+ "publishConfig": {
+ "tag": "next"
+ }
}
diff --git a/src/errorMessages.ts b/src/errorMessages.ts
index d28db3b7bc0..a0af52e282c 100644
--- a/src/errorMessages.ts
+++ b/src/errorMessages.ts
@@ -1,9 +1,8 @@
export const buildIsMissed = `Please specify 'build' configuration in the development package.json ('%s'), at least
build: {
- "app-bundle-id": "your.id",
- "app-category-type": "your.app.category.type",
- "iconUrl": "see https://github.com/develar/electron-builder#in-short",
+ "appId": "your.id",
+ "app-category-type": "your.app.category.type"
}
}
diff --git a/src/fpmDownload.ts b/src/fpmDownload.ts
index 7f41c70f13e..bd2970531e0 100644
--- a/src/fpmDownload.ts
+++ b/src/fpmDownload.ts
@@ -13,59 +13,64 @@ const versionToPromise = new Map>()
// can be called in parallel, all calls for the same version will get the same promise - will be downloaded only once
export function downloadFpm(version: string, osAndArch: string): Promise {
- let promise = versionToPromise.get(version)
+ return getBin("fpm", `fpm-${version}-${osAndArch}`, `https://github.com/develar/fpm-self-contained/releases/download/v${version}/${`fpm-${version}-${osAndArch}`}.7z`)
+ .then(it => path.join(it, "fpm"))
+}
+
+export function getBin(name: string, dirName: string, url: string, sha1?: string): Promise {
+ let promise = versionToPromise.get(dirName)
// if rejected, we will try to download again
- if (promise != null && !promise!.isRejected()) {
- return promise!
+ if (promise != null && !promise.isRejected()) {
+ return promise
}
- promise = >doDownloadFpm(version, osAndArch)
- versionToPromise.set(version, promise)
+ promise = >doGetBin(name, dirName, url, sha1)
+ versionToPromise.set(dirName, promise)
return promise
}
-async function doDownloadFpm(version: string, osAndArch: string): Promise {
- const dirName = `fpm-${version}-${osAndArch}`
- const url = `https://github.com/develar/fpm-self-contained/releases/download/v${version}/${dirName}.7z`
+// we cache in the global location - in the home dir, not in the node_modules/.cache (https://www.npmjs.com/package/find-cache-dir) because
+// * don't need to find node_modules
+// * don't pollute user project dir (important in case of 1-package.json project structure)
+// * simplify/speed-up tests (don't download fpm for each test project)
+async function doGetBin(name: string, dirName: string, url: string, sha2?: string): Promise {
+ const cachePath = path.join(homedir(), ".cache", name)
+ const dirPath = path.join(cachePath, dirName)
- // we cache in the global location - in the home dir, not in the node_modules/.cache (https://www.npmjs.com/package/find-cache-dir) because
- // * don't need to find node_modules
- // * don't pollute user project dir (important in case of 1-package.json project structure)
- // * simplify/speed-up tests (don't download fpm for each test project)
- const cacheDir = path.join(homedir(), ".cache", "fpm")
- const fpmDir = path.join(cacheDir, dirName)
-
- const fpmDirStat = await statOrNull(fpmDir)
- if (fpmDirStat != null && fpmDirStat.isDirectory()) {
- debug(`Found existing fpm ${fpmDir}`)
- return path.join(fpmDir, "fpm")
+ const dirStat = await statOrNull(dirPath)
+ if (dirStat != null && dirStat.isDirectory()) {
+ debug(`Found existing ${name} ${dirPath}`)
+ return dirPath
}
// 7z cannot be extracted from the input stream, temp file is required
- const tempUnpackDir = path.join(cacheDir, getTempName())
+ const tempUnpackDir = path.join(cachePath, getTempName())
const archiveName = `${tempUnpackDir}.7z`
- debug(`Download fpm from ${url} to ${archiveName}`)
- // 7z doesn't create out dir
+ debug(`Download ${name} from ${url} to ${archiveName}`)
+ // 7z doesn't create out dir, so, we don't create dir in parallel to download - dir creation will create parent dirs for archive file also
await emptyDir(tempUnpackDir)
- await download(url, archiveName, false)
+ await download(url, archiveName, {
+ skipDirCreation: true,
+ sha2: sha2,
+ })
await spawn(path7za, debug7zArgs("x").concat(archiveName, `-o${tempUnpackDir}`), {
- cwd: cacheDir,
+ cwd: cachePath,
stdio: ["ignore", debug.enabled ? "inherit" : "ignore", "inherit"],
})
await BluebirdPromise.all([
- rename(path.join(tempUnpackDir, dirName), fpmDir)
+ rename(path.join(tempUnpackDir, dirName), dirPath)
.catch(e => {
- console.warn("Cannot move downloaded fpm into final location (another process downloaded faster?): " + e)
+ console.warn(`Cannot move downloaded ${name} into final location (another process downloaded faster?): ${e}`)
}),
unlink(archiveName),
])
await BluebirdPromise.all([
remove(tempUnpackDir),
- writeFile(path.join(fpmDir, ".lastUsed"), Date.now().toString())
+ writeFile(path.join(dirPath, ".lastUsed"), Date.now().toString())
])
- debug(`fpm downloaded to ${fpmDir}`)
- return path.join(fpmDir, "fpm")
+ debug(`${name}} downloaded to ${dirPath}`)
+ return dirPath
}
\ No newline at end of file
diff --git a/src/httpRequest.ts b/src/httpRequest.ts
index cb680491ab7..4b3df8ee5f4 100644
--- a/src/httpRequest.ts
+++ b/src/httpRequest.ts
@@ -8,14 +8,19 @@ import * as path from "path"
const maxRedirects = 10
-export const download = <(url: string, destination: string, isCreateDir?: boolean | undefined) => BluebirdPromise>(BluebirdPromise.promisify(_download))
+export interface DownloadOptions {
+ skipDirCreation?: boolean
+ sha2?: string
+}
+
+export const download = <(url: string, destination: string, options?: DownloadOptions) => BluebirdPromise>(BluebirdPromise.promisify(_download))
-function _download(url: string, destination: string, isCreateDir: boolean | undefined, callback: (error: Error) => void): void {
+function _download(url: string, destination: string, options: DownloadOptions | n, callback: (error: Error) => void): void {
if (callback == null) {
- callback = isCreateDir
- isCreateDir = true
+ callback = options
+ options = null
}
- doDownload(url, destination, 0, isCreateDir === undefined ? true : isCreateDir, callback)
+ doDownload(url, destination, 0, options || {}, callback)
}
export function addTimeOutHandler(request: ClientRequest, callback: (error: Error) => void) {
@@ -27,8 +32,8 @@ export function addTimeOutHandler(request: ClientRequest, callback: (error: Erro
})
}
-function doDownload(url: string, destination: string, redirectCount: number, isCreateDir: boolean, callback: (error: Error) => void) {
- const ensureDirPromise = isCreateDir ? ensureDir(path.dirname(destination)) : BluebirdPromise.resolve()
+function doDownload(url: string, destination: string, redirectCount: number, options: DownloadOptions, callback: (error: Error) => void) {
+ const ensureDirPromise = options.skipDirCreation ? BluebirdPromise.resolve() : ensureDir(path.dirname(destination))
const parsedUrl = parseUrl(url)
// user-agent must be specified, otherwise some host can return 401 unauthorised
@@ -47,7 +52,7 @@ function doDownload(url: string, destination: string, redirectCount: number, isC
const redirectUrl = response.headers.location
if (redirectUrl != null) {
if (redirectCount < maxRedirects) {
- doDownload(redirectUrl, destination, redirectCount++, isCreateDir, callback)
+ doDownload(redirectUrl, destination, redirectCount++, options, callback)
}
else {
callback(new Error("Too many redirects (> " + maxRedirects + ")"))
@@ -55,6 +60,17 @@ function doDownload(url: string, destination: string, redirectCount: number, isC
return
}
+ const sha1Header = response.headers["X-Checksum-Sha1"]
+ if (sha1Header != null && options.sha2 != null) {
+ // todo why bintray doesn't send this header always
+ if (sha1Header == null) {
+ throw new Error("checksum is required, but server response doesn't contain X-Checksum-Sha2 header")
+ }
+ else if (sha1Header !== options.sha2) {
+ throw new Error(`checksum mismatch: expected ${options.sha2} but got ${sha1Header} (X-Checksum-Sha2 header)`)
+ }
+ }
+
ensureDirPromise
.then(() => {
const downloadStream = createWriteStream(destination)
diff --git a/src/metadata.ts b/src/metadata.ts
index 0307113685d..dde63a820cc 100755
--- a/src/metadata.ts
+++ b/src/metadata.ts
@@ -81,9 +81,19 @@ export type CompressionLevel = "store" | "normal" | "maximum"
*/
export interface BuildMetadata {
/*
- *OS X-only.* The app bundle ID. See [CFBundleIdentifier](https://developer.apple.com/library/ios/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html#//apple_ref/doc/uid/20001431-102070).
+ The application id. Used as
+ [CFBundleIdentifier](https://developer.apple.com/library/ios/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html#//apple_ref/doc/uid/20001431-102070) for OS X and as
+ [Application User Model ID](https://msdn.microsoft.com/en-us/library/windows/desktop/dd378459(v=vs.85).aspx) for Windows.
+
+ For windows only NSIS target supports it. Squirrel.Windows is not fixed yet.
+
+ Defaults to `com.electron.${name}`. It is strongly recommended that an explicit ID be set.
*/
+ readonly appId?: string | null
+
+ // deprecated
readonly "app-bundle-id"?: string | null
+
/*
*OS X-only.* The application category type, as shown in the Finder via *View -> Arrange by Application Category* when viewing the Applications directory.
@@ -150,6 +160,11 @@ export interface BuildMetadata {
*/
readonly win?: WinBuildOptions | null
+ /**
+ See [.build.nsis](#NsisOptions).
+ */
+ readonly nsis?: NsisOptions | null
+
/*
See [.build.linux](#LinuxBuildOptions).
*/
@@ -298,6 +313,30 @@ export interface WinBuildOptions extends PlatformSpecificBuildOptions {
readonly signcodePath?: string | null
}
+/*
+ ### `.build.nsis`
+
+ NSIS target support in progress — not polished and not fully tested and checked.
+ */
+export interface NsisOptions {
+ /*
+ Mark "all users" (per-machine) as default. Not recommended. Defaults to `false`.
+ */
+ readonly perMachine?: boolean | null
+
+ /*
+ Allow requesting for elevation. If false, user will have to restart installer with elevated permissions. Defaults to `true`.
+ */
+ readonly allowElevation?: boolean | null
+
+ readonly guid?: string | null
+
+ /*
+ One-click installation. Defaults to `true`.
+ */
+ readonly oneClick?: boolean | null
+}
+
/*
### `.build.linux`
*/
diff --git a/src/packager.ts b/src/packager.ts
index 8f243706668..08d331ef3ba 100644
--- a/src/packager.ts
+++ b/src/packager.ts
@@ -41,6 +41,22 @@ export class Packager implements BuildInfo {
this.projectDir = options.projectDir == null ? process.cwd() : path.resolve(options.projectDir)
}
+ get appId(): string {
+ const appId = this.devMetadata.build["app-bundle-id"]
+ if (appId != null) {
+ warn("app-bundle-id is deprecated, please use appId")
+ }
+
+ if (this.devMetadata.build.appId != null) {
+ return this.devMetadata.build.appId
+ }
+
+ if (appId == null) {
+ return `com.electron.${this.metadata.name.toLowerCase()}`
+ }
+ return appId
+ }
+
artifactCreated(handler: (event: ArtifactCreated) => void): Packager {
addHandler(this.eventEmitter, "artifactCreated", handler)
return this
@@ -172,7 +188,7 @@ export class Packager implements BuildInfo {
else {
const author = appMetadata.author
if (author == null) {
- reportError("author")
+ throw new Error(`Please specify "author" in the application package.json ('${appPackageFile}') — it is used as company name.`)
}
else if (author.email == null && this.options.targets!.has(Platform.LINUX)) {
throw new Error(util.format(errorMessages.authorEmailIsMissed, appPackageFile))
diff --git a/src/platformPackager.ts b/src/platformPackager.ts
index da64a079b66..cae918a43f7 100644
--- a/src/platformPackager.ts
+++ b/src/platformPackager.ts
@@ -62,6 +62,9 @@ export interface BuildInfo extends ProjectMetadataProvider {
eventEmitter: EventEmitter
isTwoPackageJsonProjectLayoutUsed: boolean
+
+ // computed final effective appId
+ appId: string
}
export abstract class PlatformPackager implements ProjectMetadataProvider {
@@ -237,6 +240,7 @@ export abstract class PlatformPackager
//noinspection JSUnusedGlobalSymbols
const options: any = deepAssign({
dir: this.info.appDir,
+ "app-bundle-id": this.info.appId,
out: outDir,
name: this.appName,
productName: this.appName,
diff --git a/src/targets/nsis.ts b/src/targets/nsis.ts
new file mode 100644
index 00000000000..d858bfe87dd
--- /dev/null
+++ b/src/targets/nsis.ts
@@ -0,0 +1,129 @@
+import { WinPackager } from "../winPackager"
+import { Arch, NsisOptions } from "../metadata"
+import { exec, log, debug } from "../util"
+import * as path from "path"
+import { Promise as BluebirdPromise } from "bluebird"
+import { getBin } from "../fpmDownload"
+import { v5 as uuid5 } from "uuid-1345"
+import { smarten, getArchSuffix } from "../platformPackager"
+
+//noinspection JSUnusedLocalSymbols
+const __awaiter = require("../awaiter")
+
+const NSIS_VERSION = "3.0rc1"
+const NSIS_SHA2 = "d9f8ad16d516f907db59814da4bc5da53619365ed8de42e21db69d3cd2afd8ec"
+
+//noinspection SpellCheckingInspection
+const ELECTRON_BUILDER_NS_UUID = "50e065bc-3134-11e6-9bab-38c9862bdaf3"
+
+export default class NsisTarget {
+ private readonly nsisPath: Promise
+
+ private readonly nsisOptions: NsisOptions
+
+ constructor(private packager: WinPackager, private outDir: string, private appOutDir: string) {
+ if (process.env.USE_SYSTEM_MAKENSIS) {
+ this.nsisPath = BluebirdPromise.resolve("makensis")
+ }
+ else {
+ this.nsisPath = getBin("nsis", `nsis-${NSIS_VERSION}`, `https://dl.bintray.com/electron-userland/bin/nsis-${NSIS_VERSION}.7z`, NSIS_SHA2)
+ }
+
+ this.nsisOptions = packager.info.devMetadata.build.nsis || Object.create(null)
+ }
+
+ async build(arch: Arch) {
+ const packager = this.packager
+
+ const iconPath = await packager.iconPath
+
+ const guid = this.nsisOptions.guid || await BluebirdPromise.promisify(uuid5)({namespace: ELECTRON_BUILDER_NS_UUID, name: packager.info.appId})
+ const version = this.packager.metadata.version
+ const productName = packager.appName
+ const defines: any = {
+ PRODUCT_NAME: productName,
+ APP_ID: packager.info.appId,
+ APP_DESCRIPTION: smarten(packager.metadata.description),
+ APP_BUILD_DIR: this.appOutDir,
+ VERSION: version,
+
+ MUI_ICON: iconPath,
+ MUI_UNICON: iconPath,
+
+ COMPANY_NAME: packager.metadata.author.name,
+ APP_EXECUTABLE_FILENAME: `${packager.appName}.exe`,
+ UNINSTALL_FILENAME: `Uninstall ${productName}.exe`,
+ MULTIUSER_INSTALLMODE_INSTDIR: guid,
+ MULTIUSER_INSTALLMODE_INSTALL_REGISTRY_KEY: guid,
+ MULTIUSER_INSTALLMODE_UNINSTALL_REGISTRY_KEY: guid,
+ MULTIUSER_INSTALLMODE_DEFAULT_REGISTRY_VALUENAME: "UninstallString",
+ MULTIUSER_INSTALLMODE_INSTDIR_REGISTRY_VALUENAME: "InstallLocation",
+ }
+
+ if (this.nsisOptions.perMachine === true) {
+ defines.MULTIUSER_INSTALLMODE_DEFAULT_ALLUSERS = null
+ }
+ else {
+ defines.MULTIUSER_INSTALLMODE_DEFAULT_CURRENTUSER = null
+ }
+
+ if (this.nsisOptions.allowElevation !== false) {
+ defines.MULTIUSER_INSTALLMODE_ALLOW_ELEVATION = null
+ }
+
+ const archSuffix = getArchSuffix(arch)
+ const installerPath = path.join(this.outDir, `${this.packager.appName} Setup ${version}${archSuffix}.exe`)
+ const commands: any = {
+ FileBufSize: "64",
+ Name: `"${productName}"`,
+ OutFile: `"${installerPath}"`,
+ Unicode: "true",
+ }
+
+ if (packager.devMetadata.build.compression !== "store") {
+ commands.SetCompressor = "/SOLID lzma"
+ // default is 8: test app installer size 37.2 vs 36 if dict size 64
+ commands.SetCompressorDictSize = "64"
+ }
+ else {
+ commands.SetCompress = "off"
+ }
+
+ const oneClick = this.nsisOptions.oneClick !== false
+ log(`Building ${oneClick ? "one-click " : ""}NSIS installer using nsis ${NSIS_VERSION}`)
+ if (oneClick) {
+ defines.ONE_CLICK = null
+ commands.AutoCloseWindow = "true"
+ }
+
+ debug(defines)
+ debug(commands)
+
+ const args: Array = []
+ for (let name of Object.keys(defines)) {
+ const value = defines[name]
+ if (value == null) {
+ args.push(`-D${name}`)
+ }
+ else {
+ args.push(`-D${name}=${value}`)
+ }
+ }
+ for (let name of Object.keys(commands)) {
+ args.push(`-X${name} ${commands[name]}`)
+ }
+
+ args.push(path.join(__dirname, "..", "..", "templates", "nsis", "installer.nsi"))
+
+ const binDir = process.platform === "darwin" ? "osx" : (process.platform === "win32" ? "Bin" : "linux")
+ const nsisPath = await this.nsisPath
+ // we use NSIS_CONFIG_CONST_DATA_PATH=no to build makensis on Linux, but in any case it doesn't use stubs as OS X/Windows version, so, we explicitly set NSISDIR
+ await exec(path.join(nsisPath, binDir, process.platform === "win32" ? "makensis.exe" : "makensis"), args, {
+ env: Object.assign({}, process.env, {NSISDIR: nsisPath})
+ })
+
+ await packager.sign(installerPath)
+
+ this.packager.dispatchArtifactCreated(installerPath, `${this.packager.metadata.name}-Setup-${version}${archSuffix}.exe`)
+ }
+}
\ No newline at end of file
diff --git a/src/targets/squirrelWindows.ts b/src/targets/squirrelWindows.ts
index 70b56714618..0d74c781756 100644
--- a/src/targets/squirrelWindows.ts
+++ b/src/targets/squirrelWindows.ts
@@ -11,20 +11,20 @@ import { emptyDir } from "fs-extra-p"
const __awaiter = require("../awaiter")
export default class SquirrelWindowsTarget {
- constructor(private packager: WinPackager, private appOutDir: string, private arch: Arch) {
+ constructor(private packager: WinPackager, private appOutDir: string) {
}
- async build(packOptions: ElectronPackagerOptions) {
+ async build(packOptions: ElectronPackagerOptions, arch: Arch) {
const version = this.packager.metadata.version
- const archSuffix = getArchSuffix(this.arch)
- const setupExeName = `${this.packager.appName} Setup ${version}${archSuffix}.exe`
+ const archSuffix = getArchSuffix(arch)
+ const setupFileName = `${this.packager.appName} Setup ${version}${archSuffix}.exe`
- const installerOutDir = path.join(this.appOutDir, "..", `win${getArchSuffix(this.arch)}`)
+ const installerOutDir = path.join(this.appOutDir, "..", `win${getArchSuffix(arch)}`)
await emptyDir(installerOutDir)
- const distOptions = await this.computeEffectiveDistOptions(installerOutDir, packOptions, setupExeName)
+ const distOptions = await this.computeEffectiveDistOptions(installerOutDir, packOptions, setupFileName)
await createWindowsInstaller(distOptions)
- this.packager.dispatchArtifactCreated(path.join(installerOutDir, setupExeName), `${this.packager.metadata.name}-Setup-${version}${archSuffix}.exe`)
+ this.packager.dispatchArtifactCreated(path.join(installerOutDir, setupFileName), `${this.packager.metadata.name}-Setup-${version}${archSuffix}.exe`)
const packagePrefix = `${this.packager.metadata.name}-${convertVersion(version)}-`
this.packager.dispatchArtifactCreated(path.join(installerOutDir, `${packagePrefix}full.nupkg`))
@@ -65,7 +65,7 @@ export default class SquirrelWindowsTarget {
const options: any = Object.assign({
name: packager.metadata.name,
productName: packager.appName,
- exe: packager.appName + ".exe",
+ exe: `${packager.appName}.exe`,
setupExe: setupExeName,
title: packager.appName,
appDirectory: this.appOutDir,
diff --git a/src/winPackager.ts b/src/winPackager.ts
index b5d3b94294b..2d0f0061415 100644
--- a/src/winPackager.ts
+++ b/src/winPackager.ts
@@ -8,6 +8,7 @@ import { deleteFile, open, close, read } from "fs-extra-p"
import { sign, SignOptions } from "signcode-tf"
import { ElectronPackagerOptions } from "electron-packager-tf"
import SquirrelWindowsTarget from "./targets/squirrelWindows"
+import NsisTarget from "./targets/nsis"
//noinspection JSUnusedLocalSymbols
const __awaiter = require("./awaiter")
@@ -55,7 +56,7 @@ export class WinPackager extends PlatformPackager {
}
get supportedTargets(): Array {
- return ["squirrel"]
+ return ["squirrel", "nsis"]
}
private async getValidIconPath(): Promise {
@@ -76,7 +77,7 @@ export class WinPackager extends PlatformPackager {
const packOptions = this.computePackOptions(outDir, appOutDir, arch)
await this.doPack(packOptions, outDir, appOutDir, arch, this.customBuildOptions)
- await this.sign(appOutDir)
+ await this.sign(path.join(appOutDir, `${this.appName}.exe`))
this.packageInDistributableFormat(outDir, appOutDir, arch, packOptions, targets, postAsyncTasks)
}
@@ -84,13 +85,12 @@ export class WinPackager extends PlatformPackager {
return path.join(outDir, `win${getArchSuffix(arch)}-unpacked`)
}
- protected async sign(appOutDir: string) {
+ async sign(file: string) {
const cscInfo = await this.cscInfo
if (cscInfo != null) {
- const filename = `${this.appName}.exe`
- log(`Signing ${filename} (certificate file "${cscInfo.file}")`)
+ log(`Signing ${path.basename(file)} (certificate file "${cscInfo.file}")`)
await this.doSign({
- path: path.join(appOutDir, filename),
+ path: file,
cert: cscInfo.file,
password: cscInfo.password!,
name: this.appName,
@@ -107,9 +107,13 @@ export class WinPackager extends PlatformPackager {
protected packageInDistributableFormat(outDir: string, appOutDir: string, arch: Arch, packOptions: ElectronPackagerOptions, targets: Array, promises: Array>): void {
for (let target of targets) {
- if (target === "squirrel.windows" || target === "default") {
+ if (target === "squirrel" || target === "default") {
const helperClass: typeof SquirrelWindowsTarget = require("./targets/squirrelWindows").default
- promises.push(new helperClass(this, appOutDir, arch).build(packOptions))
+ promises.push(new helperClass(this, appOutDir).build(packOptions, arch))
+ }
+ else if (target === "nsis") {
+ const helperClass: typeof NsisTarget = require("./targets/nsis").default
+ promises.push(new helperClass(this, outDir, appOutDir).build(arch))
}
else {
log(`Creating Windows ${target}`)
diff --git a/templates/nsis/FileAssociation.nsh b/templates/nsis/FileAssociation.nsh
new file mode 100644
index 00000000000..fa4885e3816
--- /dev/null
+++ b/templates/nsis/FileAssociation.nsh
@@ -0,0 +1,176 @@
+/*
+_____________________________________________________________________________
+ File Association
+_____________________________________________________________________________
+ Based on code taken from http://nsis.sourceforge.net/File_Association
+ Usage in script:
+ 1. !include "FileAssociation.nsh"
+ 2. [Section|Function]
+ ${FileAssociationFunction} "Param1" "Param2" "..." $var
+ [SectionEnd|FunctionEnd]
+ FileAssociationFunction=[RegisterExtension|UnRegisterExtension]
+_____________________________________________________________________________
+ ${RegisterExtension} "[executable]" "[extension]" "[description]"
+"[executable]" ; executable which opens the file format
+ ;
+"[extension]" ; extension, which represents the file format to open
+ ;
+"[description]" ; description for the extension. This will be display in Windows Explorer.
+ ;
+ ${UnRegisterExtension} "[extension]" "[description]"
+"[extension]" ; extension, which represents the file format to open
+ ;
+"[description]" ; description for the extension. This will be display in Windows Explorer.
+ ;
+_____________________________________________________________________________
+ Macros
+_____________________________________________________________________________
+ Change log window verbosity (default: 3=no script)
+ Example:
+ !include "FileAssociation.nsh"
+ !insertmacro RegisterExtension
+ ${FileAssociation_VERBOSE} 4 # all verbosity
+ !insertmacro UnRegisterExtension
+ ${FileAssociation_VERBOSE} 3 # no script
+*/
+
+
+!ifndef FileAssociation_INCLUDED
+!define FileAssociation_INCLUDED
+
+!include Util.nsh
+
+!verbose push
+!verbose 3
+!ifndef _FileAssociation_VERBOSE
+ !define _FileAssociation_VERBOSE 3
+!endif
+!verbose ${_FileAssociation_VERBOSE}
+!define FileAssociation_VERBOSE `!insertmacro FileAssociation_VERBOSE`
+!verbose pop
+
+!macro FileAssociation_VERBOSE _VERBOSE
+ !verbose push
+ !verbose 3
+ !undef _FileAssociation_VERBOSE
+ !define _FileAssociation_VERBOSE ${_VERBOSE}
+ !verbose pop
+!macroend
+
+
+
+!macro RegisterExtensionCall _EXECUTABLE _EXTENSION _DESCRIPTION
+ !verbose push
+ !verbose ${_FileAssociation_VERBOSE}
+ Push `${_DESCRIPTION}`
+ Push `${_EXTENSION}`
+ Push `${_EXECUTABLE}`
+ ${CallArtificialFunction} RegisterExtension_
+ !verbose pop
+!macroend
+
+!macro UnRegisterExtensionCall _EXTENSION _DESCRIPTION
+ !verbose push
+ !verbose ${_FileAssociation_VERBOSE}
+ Push `${_EXTENSION}`
+ Push `${_DESCRIPTION}`
+ ${CallArtificialFunction} UnRegisterExtension_
+ !verbose pop
+!macroend
+
+
+
+!define RegisterExtension `!insertmacro RegisterExtensionCall`
+!define un.RegisterExtension `!insertmacro RegisterExtensionCall`
+
+!macro RegisterExtension
+!macroend
+
+!macro un.RegisterExtension
+!macroend
+
+!macro RegisterExtension_
+ !verbose push
+ !verbose ${_FileAssociation_VERBOSE}
+
+ Exch $R2 ;exe
+ Exch
+ Exch $R1 ;ext
+ Exch
+ Exch 2
+ Exch $R0 ;desc
+ Exch 2
+ Push $0
+ Push $1
+
+ ReadRegStr $1 HKCR $R1 "" ; read current file association
+ StrCmp "$1" "" NoBackup ; is it empty
+ StrCmp "$1" "$R0" NoBackup ; is it our own
+ WriteRegStr HKCR $R1 "backup_val" "$1" ; backup current value
+NoBackup:
+ WriteRegStr HKCR $R1 "" "$R0" ; set our file association
+
+ ReadRegStr $0 HKCR $R0 ""
+ StrCmp $0 "" 0 Skip
+ WriteRegStr HKCR "$R0" "" "$R0"
+ WriteRegStr HKCR "$R0\shell" "" "open"
+ WriteRegStr HKCR "$R0\DefaultIcon" "" "$R2,0"
+Skip:
+ WriteRegStr HKCR "$R0\shell\open\command" "" '"$R2" "%1"'
+ WriteRegStr HKCR "$R0\shell\edit" "" "Edit $R0"
+ WriteRegStr HKCR "$R0\shell\edit\command" "" '"$R2" "%1"'
+
+ Pop $1
+ Pop $0
+ Pop $R2
+ Pop $R1
+ Pop $R0
+
+ !verbose pop
+!macroend
+
+
+
+!define UnRegisterExtension `!insertmacro UnRegisterExtensionCall`
+!define un.UnRegisterExtension `!insertmacro UnRegisterExtensionCall`
+
+!macro UnRegisterExtension
+!macroend
+
+!macro un.UnRegisterExtension
+!macroend
+
+!macro UnRegisterExtension_
+ !verbose push
+ !verbose ${_FileAssociation_VERBOSE}
+
+ Exch $R1 ;desc
+ Exch
+ Exch $R0 ;ext
+ Exch
+ Push $0
+ Push $1
+
+ ReadRegStr $1 HKCR $R0 ""
+ StrCmp $1 $R1 0 NoOwn ; only do this if we own it
+ ReadRegStr $1 HKCR $R0 "backup_val"
+ StrCmp $1 "" 0 Restore ; if backup="" then delete the whole key
+ DeleteRegKey HKCR $R0
+ Goto NoOwn
+
+Restore:
+ WriteRegStr HKCR $R0 "" $1
+ DeleteRegValue HKCR $R0 "backup_val"
+ DeleteRegKey HKCR $R1 ;Delete key with association name settings
+
+NoOwn:
+
+ Pop $1
+ Pop $0
+ Pop $R1
+ Pop $R0
+
+ !verbose pop
+!macroend
+
+!endif # !FileAssociation_INCLUDED
\ No newline at end of file
diff --git a/templates/nsis/NsisMultiUser.nsh b/templates/nsis/NsisMultiUser.nsh
new file mode 100755
index 00000000000..e8795b4ffd4
--- /dev/null
+++ b/templates/nsis/NsisMultiUser.nsh
@@ -0,0 +1,524 @@
+/*
+SimpleMultiUser.nsh - Installer/Uninstaller that allows installations "per-user" (no admin required) or "per-machine" (asks elevation *only when necessary*)
+By Ricardo Drizin (contact at http://drizin.com.br)
+
+This plugin is based on [MultiUser.nsh (by Joost Verburg)](http://nsis.sourceforge.net/Docs/MultiUser/Readme.html) but with some new features and some simplifications:
+- Installer allows installations "per-user" (no admin required) or "per-machine" (as original)
+- If running user IS part of Administrators group, he is not forced to elevate (only if necessary - for per-machine install)
+- If running user is NOT part of Administrators group, he is still able to elevate and install per-machine (I expect that power-users will have administrator password, but will not be part of the administrators group)
+- UAC Elevation happens only when necessary (when per-machine is selected), not in the start of the installer
+- Uninstaller block is mandatory (why shouldn't it be?)
+- If there are both per-user and per-machine installations, user can choose which one to remove during uninstall
+- Correctly creates and removes shortcuts and registry (per-user and per-machine are totally independent)
+- Fills uninstall information in registry like Icon and Estimated Size.
+- If running as non-elevated user, the "per-machine" install can be allowed (automatically invoking UAC elevation) or can be disabled (suggesting to run again as elevated user)
+- If elevation is invoked for per-machine install, the calling process automatically hides itself, and the elevated inner process automatically skips the choice screen (cause in this case we know that per-machine installation was chosen)
+- If uninstalling from the "add/remove programs", automatically detects if user is trying to remove per-machine or per-user install
+
+*/
+
+!verbose push
+!verbose 3
+
+;Standard NSIS header files
+!include MUI2.nsh
+!include nsDialogs.nsh
+!include LogicLib.nsh
+!include WinVer.nsh
+!include FileFunc.nsh
+!include UAC.nsh
+
+;Variables
+Var MultiUser.Privileges ; Current user level: "Admin", "Power" (up to Windows XP), or else regular user.
+Var MultiUser.InstallMode ; Current Install Mode ("AllUsers" or "CurrentUser")
+Var IsAdmin ; 0 (false) or 1 (true)
+Var HasPerUserInstallation ; 0 (false) or 1 (true)
+Var HasPerMachineInstallation ; 0 (false) or 1 (true)
+Var PerUserInstallationFolder
+Var PerMachineInstallationFolder
+Var HasTwoAvailableOptions ; 0 (false) or 1 (true)
+Var RadioButtonLabel1
+;Var RadioButtonLabel2
+;Var RadioButtonLabel3
+
+!define FOLDERID_UserProgramFiles {5CD7AEE2-2219-4A67-B85D-6C9CE15660CB}
+!define KF_FLAG_CREATE 0x00008000
+
+!ifdef MULTIUSER_INSTALLMODE_INSTALL_REGISTRY_KEY & MULTIUSER_INSTALLMODE_UNINSTALL_REGISTRY_KEY & MULTIUSER_INSTALLMODE_INSTDIR_REGISTRY_VALUENAME & UNINSTALL_FILENAME & VERSION & APP_EXECUTABLE_FILENAME & PRODUCT_NAME & COMPANY_NAME
+!else
+ !error "Should define all variables: MULTIUSER_INSTALLMODE_INSTALL_REGISTRY_KEY & MULTIUSER_INSTALLMODE_UNINSTALL_REGISTRY_KEY & MULTIUSER_INSTALLMODE_INSTDIR_REGISTRY_VALUENAME & UNINSTALL_FILENAME & VERSION & APP_EXECUTABLE_FILENAME & PRODUCT_NAME & COMPANY_NAME"
+!endif
+
+!define MULTIUSER_INSTALLMODE_INSTALL_REGISTRY_KEY2 "Software\${MULTIUSER_INSTALLMODE_INSTALL_REGISTRY_KEY}"
+!define MULTIUSER_INSTALLMODE_UNINSTALL_REGISTRY_KEY2 "Software\Microsoft\Windows\CurrentVersion\Uninstall\${MULTIUSER_INSTALLMODE_UNINSTALL_REGISTRY_KEY}"
+
+!ifndef MULTIUSER_INSTALLMODE_DISPLAYNAME
+ !define MULTIUSER_INSTALLMODE_DISPLAYNAME "${PRODUCT_NAME} ${VERSION}"
+!endif
+
+RequestExecutionLevel user ; will ask elevation only if necessary
+
+; Sets install mode to "per-machine" (all users).
+!macro MULTIUSER_INSTALLMODE_ALLUSERS UNINSTALLER_PREFIX UNINSTALLER_FUNCPREFIX
+ ;Install mode initialization - per-machine
+ StrCpy $MultiUser.InstallMode AllUsers
+
+ SetShellVarContext all
+
+ !if "${UNINSTALLER_PREFIX}" != UN
+ ;Set default installation location for installer
+ StrCpy $INSTDIR "$PROGRAMFILES\${PRODUCT_NAME}"
+ !endif
+
+ ; Checks registry for previous installation path (both for upgrading, reinstall, or uninstall)
+ ReadRegStr $PerMachineInstallationFolder HKLM "${MULTIUSER_INSTALLMODE_INSTALL_REGISTRY_KEY2}" "${MULTIUSER_INSTALLMODE_INSTDIR_REGISTRY_VALUENAME}"
+ ${if} $PerMachineInstallationFolder != ""
+ StrCpy $INSTDIR $PerMachineInstallationFolder
+ ${endif}
+
+ !ifdef MULTIUSER_INSTALLMODE_${UNINSTALLER_PREFIX}FUNCTION
+ Call "${MULTIUSER_INSTALLMODE_${UNINSTALLER_PREFIX}FUNCTION}"
+ !endif
+!macroend
+
+; Sets install mode to "per-user".
+!macro MULTIUSER_INSTALLMODE_CURRENTUSER UNINSTALLER_PREFIX UNINSTALLER_FUNCPREFIX
+ StrCpy $MultiUser.InstallMode CurrentUser
+
+ SetShellVarContext current
+
+ !if "${UNINSTALLER_PREFIX}" != UN
+ # http://www.mathiaswestin.net/2012/09/how-to-make-per-user-installation-with.html
+ StrCpy $0 "$LocalAppData\Programs"
+ ${If} ${IsNT}
+ ;Win7 has a per-user programfiles known folder and this can be a non-default location
+ System::Call 'Shell32::SHGetKnownFolderPath(g "${FOLDERID_UserProgramFiles}",i ${KF_FLAG_CREATE},i0,*i.r2)i.r1'
+ ${If} $1 == 0
+ System::Call '*$2(&w${NSIS_MAX_STRLEN} .r1)'
+ StrCpy $0 $1
+ System::Call 'Ole32::CoTaskMemFree(ir2)'
+ ${EndIf}
+ ${Else}
+ ;Everyone is admin on Win9x, so falling back to $ProgramFiles is ok
+ ${IfThen} $LocalAppData == "" ${|} StrCpy $0 $ProgramFiles ${|}
+ ${EndIf}
+ StrCpy $Instdir "$0\${PRODUCT_NAME}"
+ !endif
+
+ ; Checks registry for previous installation path (both for upgrading, reinstall, or uninstall)
+ ReadRegStr $PerUserInstallationFolder HKCU "${MULTIUSER_INSTALLMODE_INSTALL_REGISTRY_KEY2}" "${MULTIUSER_INSTALLMODE_INSTDIR_REGISTRY_VALUENAME}"
+ ${if} $PerUserInstallationFolder != ""
+ StrCpy $INSTDIR $PerUserInstallationFolder
+ ${endif}
+
+ !ifdef MULTIUSER_INSTALLMODE_${UNINSTALLER_PREFIX}FUNCTION
+ Call "${MULTIUSER_INSTALLMODE_${UNINSTALLER_PREFIX}FUNCTION}"
+ !endif
+!macroend
+
+Function MultiUser.InstallMode.AllUsers
+ !insertmacro MULTIUSER_INSTALLMODE_ALLUSERS "" ""
+FunctionEnd
+
+Function MultiUser.InstallMode.CurrentUser
+ !insertmacro MULTIUSER_INSTALLMODE_CURRENTUSER "" ""
+FunctionEnd
+
+Function un.MultiUser.InstallMode.AllUsers
+ !insertmacro MULTIUSER_INSTALLMODE_ALLUSERS UN un.
+FunctionEnd
+
+Function un.MultiUser.InstallMode.CurrentUser
+ !insertmacro MULTIUSER_INSTALLMODE_CURRENTUSER UN un.
+FunctionEnd
+
+/****** Installer/uninstaller initialization ******/
+
+!macro MULTIUSER_INIT_QUIT UNINSTALLER_FUNCPREFIX
+ !ifdef MULTIUSER_INIT_${UNINSTALLER_FUNCPREFIX}FUNCTIONQUIT
+ Call "${MULTIUSER_INIT_${UNINSTALLER_FUNCPREFIX}FUCTIONQUIT}"
+ !else
+ Quit
+ !endif
+!macroend
+
+!macro MULTIUSER_INIT_TEXTS
+ !ifndef MULTIUSER_INIT_TEXT_ADMINREQUIRED
+ !define MULTIUSER_INIT_TEXT_ADMINREQUIRED "$(^Caption) requires administrator privileges."
+ !endif
+
+ !ifndef MULTIUSER_INIT_TEXT_POWERREQUIRED
+ !define MULTIUSER_INIT_TEXT_POWERREQUIRED "$(^Caption) requires at least Power User privileges."
+ !endif
+
+ !ifndef MULTIUSER_INIT_TEXT_ALLUSERSNOTPOSSIBLE
+ !define MULTIUSER_INIT_TEXT_ALLUSERSNOTPOSSIBLE "Your user account does not have sufficient privileges to install $(^Name) for all users of this computer."
+ !endif
+!macroend
+
+!macro MULTIUSER_INIT_CHECKS UNINSTALLER_PREFIX UNINSTALLER_FUNCPREFIX
+
+ ;Installer initialization - check privileges and set default install mode
+ !insertmacro MULTIUSER_INIT_TEXTS
+
+ UserInfo::GetAccountType
+ Pop $MultiUser.Privileges
+ ${if} $MultiUser.Privileges == "Admin"
+ ${orif} $MultiUser.Privileges == "Power"
+ StrCpy $IsAdmin 1
+ ${else}
+ StrCpy $IsAdmin 0
+ ${endif}
+
+ ; Checks registry for previous installation path (both for upgrading, reinstall, or uninstall)
+ StrCpy $HasPerMachineInstallation 0
+ StrCpy $HasPerUserInstallation 0
+ ;Set installation mode to setting from a previous installation
+ ReadRegStr $PerMachineInstallationFolder HKLM "${MULTIUSER_INSTALLMODE_INSTALL_REGISTRY_KEY2}" "${MULTIUSER_INSTALLMODE_INSTDIR_REGISTRY_VALUENAME}"
+ ${if} $PerMachineInstallationFolder != ""
+ StrCpy $HasPerMachineInstallation 1
+ ${endif}
+ ReadRegStr $PerUserInstallationFolder HKCU "${MULTIUSER_INSTALLMODE_INSTALL_REGISTRY_KEY2}" "${MULTIUSER_INSTALLMODE_INSTDIR_REGISTRY_VALUENAME}"
+ ${if} $PerUserInstallationFolder != ""
+ StrCpy $HasPerUserInstallation 1
+ ${endif}
+
+ ${if} $HasPerUserInstallation == "1" ; if there is only one installation... set it as default...
+ ${andif} $HasPerMachineInstallation == "0"
+ Call ${UNINSTALLER_FUNCPREFIX}MultiUser.InstallMode.CurrentUser
+ ${elseif} $HasPerUserInstallation == "0" ; if there is only one installation... set it as default...
+ ${andif} $HasPerMachineInstallation == "1"
+ Call ${UNINSTALLER_FUNCPREFIX}MultiUser.InstallMode.AllUsers
+ ${else} ; if there is no installation, or there is both per-user and per-machine...
+ ${if} ${IsNT}
+ ${if} $IsAdmin == "1" ;If running as admin, default to per-machine installation if possible (unless default is forced by MULTIUSER_INSTALLMODE_DEFAULT_CURRENTUSER)
+ !if MULTIUSER_INSTALLMODE_DEFAULT_CURRENTUSER
+ Call ${UNINSTALLER_FUNCPREFIX}MultiUser.InstallMode.CurrentUser
+ !else
+ Call ${UNINSTALLER_FUNCPREFIX}MultiUser.InstallMode.AllUsers
+ !endif
+ ${else} ;If not running as admin, default to per-user installation (unless default is forced by MULTIUSER_INSTALLMODE_DEFAULT_ALLUSERS and elevation is allowed MULTIUSER_INSTALLMODE_ALLOW_ELEVATION)
+ !ifdef MULTIUSER_INSTALLMODE_DEFAULT_ALLUSERS & MULTIUSER_INSTALLMODE_ALLOW_ELEVATION
+ Call ${UNINSTALLER_FUNCPREFIX}MultiUser.InstallMode.AllUsers
+ !else
+ Call ${UNINSTALLER_FUNCPREFIX}MultiUser.InstallMode.CurrentUser
+ !endif
+ ${endif}
+ ${else} ; Not running Windows NT, (so it's Windows XP at best), so per-user installation not supported
+ Call ${UNINSTALLER_FUNCPREFIX}MultiUser.InstallMode.AllUsers
+ ${endif}
+ ${endif}
+
+!macroend
+
+!macro MULTIUSER_INIT
+ !verbose push
+ !verbose 3
+
+ ; se for inner (sub processo) e ainda assim não for admin... algo errado
+ ${If} ${UAC_IsInnerInstance}
+ ${AndIfNot} ${UAC_IsAdmin}
+ ;MessageBox MB_OK "This account doesn't have admin rights"
+ SetErrorLevel 0x666666 ;special return value for outer instance so it knows we did not have admin rights
+ Quit
+ ${EndIf}
+
+ !insertmacro MULTIUSER_INIT_CHECKS "" ""
+ !verbose pop
+!macroend
+
+!macro MULTIUSER_UNINIT
+ !verbose push
+ !verbose 3
+ !insertmacro MULTIUSER_INIT_CHECKS Un un.
+ !verbose pop
+!macroend
+
+/****** Modern UI 2 page ******/
+!macro MULTIUSER_INSTALLMODEPAGE_INTERFACE
+ Var MultiUser.InstallModePage
+ Var MultiUser.InstallModePage.Text
+ Var MultiUser.InstallModePage.AllUsers
+ Var MultiUser.InstallModePage.CurrentUser
+ Var MultiUser.InstallModePage.ReturnValue
+!macroend
+
+!macro MULTIUSER_PAGEDECLARATION_INSTALLMODE
+ !insertmacro MUI_SET MULTIUSER_${MUI_PAGE_UNINSTALLER_PREFIX}INSTALLMODEPAGE ""
+ !insertmacro MULTIUSER_INSTALLMODEPAGE_INTERFACE
+ !insertmacro MULTIUSER_FUNCTION_INSTALLMODEPAGE MultiUser.InstallModePre_${MUI_UNIQUEID} MultiUser.InstallModeLeave_${MUI_UNIQUEID} "" ""
+ !insertmacro MULTIUSER_FUNCTION_INSTALLMODEPAGE MultiUser.InstallModePre_${MUI_UNIQUEID} MultiUser.InstallModeLeave_${MUI_UNIQUEID} UN un.
+
+ PageEx custom
+ PageCallbacks MultiUser.InstallModePre_${MUI_UNIQUEID} MultiUser.InstallModeLeave_${MUI_UNIQUEID}
+ Caption " "
+ PageExEnd
+
+ UninstPage custom un.MultiUser.InstallModePre_${MUI_UNIQUEID} un.MultiUser.InstallModeLeave_${MUI_UNIQUEID}
+!macroend
+
+!macro MULTIUSER_PAGE_INSTALLMODE
+ ;Modern UI page for install mode
+ !verbose push
+ !verbose 3
+ !insertmacro MUI_PAGE_INIT
+ !insertmacro MULTIUSER_PAGEDECLARATION_INSTALLMODE
+ !verbose pop
+!macroend
+
+!macro MULTIUSER_FUNCTION_INSTALLMODEPAGE PRE LEAVE UNINSTALLER_PREFIX UNINSTALLER_FUNCPREFIX
+ Function "${UNINSTALLER_FUNCPREFIX}${PRE}"
+
+ ${If} ${UAC_IsInnerInstance}
+ ${AndIf} ${UAC_IsAdmin}
+ ;MessageBox MB_OK
+ Call ${UNINSTALLER_FUNCPREFIX}MultiUser.InstallMode.AllUsers ; Inner Process (and Admin) - skip selection, inner process is always used for elevation (machine-wide)
+ Abort ; // next page
+ ${EndIf}
+
+ ; If uninstalling, will check if there is both a per-user and per-machine installation. If there is only one, will skip the form.
+ ; If uninstallation was invoked from the "add/remove programs" Windows will automatically requests elevation (depending if uninstall keys are in HKLM or HKCU)
+ ; so (for uninstallation) just checking UAC_IsAdmin would probably be enought to determine if it's a per-user or per-machine. However, user can run the uninstall.exe from the folder itself, do I'd rather check.
+ !if "${UNINSTALLER_PREFIX}" == UN
+ ${if} $HasPerUserInstallation == "1" ; if there is only one installation... skip form.. only one uninstall available
+ ${andif} $HasPerMachineInstallation == "0"
+ Call ${UNINSTALLER_FUNCPREFIX}MultiUser.InstallMode.CurrentUser ; Uninstaller has only HasPerUserInstallation
+ Abort ; // next page
+ ${elseif} $HasPerUserInstallation == "0" ; if there is only one installation... skip form.. only one uninstall available
+ ${andif} $HasPerMachineInstallation == "1"
+ Call ${UNINSTALLER_FUNCPREFIX}MultiUser.InstallMode.AllUsers ; Uninstaller has only HasPerMachineInstallation
+ Abort ; // next page
+ ${endif}
+ !endif
+
+ ${GetParameters} $R0
+ ${GetOptions} $R0 "/allusers" $R1
+ IfErrors notallusers
+ ${if} $IsAdmin == "0"
+ ShowWindow $HWNDPARENT ${SW_HIDE} ; HideWindow would work?
+ !insertmacro UAC_RunElevated
+ Quit ;we are the outer process, the inner process has done its work (ExitCode is $2), we are done
+ ${endif}
+ Call ${UNINSTALLER_FUNCPREFIX}MultiUser.InstallMode.AllUsers ; Uninstaller has only HasPerMachineInstallation
+ Abort ; // next page
+ notallusers:
+ ${GetOptions} $R0 "/currentuser" $R1
+ IfErrors notcurrentuser
+ Call ${UNINSTALLER_FUNCPREFIX}MultiUser.InstallMode.CurrentUser ; Uninstaller has only HasPerUserInstallation
+ Abort ; // next page
+ notcurrentuser:
+
+
+ !insertmacro MUI_PAGE_FUNCTION_CUSTOM PRE
+ ;!insertmacro MUI_HEADER_TEXT_PAGE $(MULTIUSER_TEXT_INSTALLMODE_TITLE) $(MULTIUSER_TEXT_INSTALLMODE_SUBTITLE) ; "Choose Users" and "Choose for which users you want to install $(^NameDA)."
+
+ !if "${UNINSTALLER_PREFIX}" != UN
+ !insertmacro MUI_HEADER_TEXT "Choose Installation Options" "Who should this application be installed for?"
+ !else
+ !insertmacro MUI_HEADER_TEXT "Choose Uninstallation Options" "Which installation should be removed?"
+ !endif
+
+ nsDialogs::Create 1018
+ Pop $MultiUser.InstallModePage
+
+ ; default was MULTIUSER_TEXT_INSTALLMODE_TITLE "Choose Users"
+ !if "${UNINSTALLER_PREFIX}" != UN
+ ${NSD_CreateLabel} 0u 0u 300u 20u "Please select whether you wish to make this software available to all users or just yourself"
+ StrCpy $8 "Anyone who uses this computer (&all users)" ; this was MULTIUSER_INNERTEXT_INSTALLMODE_ALLUSERS "Install for anyone using this computer"
+ StrCpy $9 "Only for &me" ; this was MULTIUSER_INNERTEXT_INSTALLMODE_CURRENTUSER "Install just for me"
+ !else
+ ${NSD_CreateLabel} 0u 0u 300u 20u "This software is installed both per-machine (all users) and per-user. $\r$\nWhich installation you wish to remove?"
+ StrCpy $8 "Anyone who uses this computer (&all users)" ; this was MULTIUSER_INNERTEXT_INSTALLMODE_ALLUSERS "Install for anyone using this computer"
+ StrCpy $9 "Only for &me" ; this was MULTIUSER_INNERTEXT_INSTALLMODE_CURRENTUSER "Install just for me"
+ !endif
+ Pop $MultiUser.InstallModePage.Text
+
+ ; criando os radios (disabled se não for admin/power) e pegando os hwnds (handles)
+ ${NSD_CreateRadioButton} 10u 30u 280u 20u "$8"
+ Pop $MultiUser.InstallModePage.AllUsers
+ ${if} $IsAdmin == "0"
+ !ifdef MULTIUSER_INSTALLMODE_ALLOW_ELEVATION ; if elevation is allowed.. "(will prompt for admin credentials)" (will appear at bottom when option is chosen)
+ StrCpy $HasTwoAvailableOptions 1
+ !else
+ SendMessage $MultiUser.InstallModePage.AllUsers ${WM_SETTEXT} 0 "STR:$8 (must run as admin)" ; since radio button is disabled, we add that comment to the disabled control itself
+ EnableWindow $MultiUser.InstallModePage.AllUsers 0 # start out disabled
+ StrCpy $HasTwoAvailableOptions 0
+ !endif
+ ${else}
+ StrCpy $HasTwoAvailableOptions 1
+ ${endif}
+
+ ;${NSD_CreateRadioButton} 20u 70u 280u 10u "$9"
+ System::Call "advapi32::GetUserName(t.r0,*i${NSIS_MAX_STRLEN})i"
+ ${NSD_CreateRadioButton} 10u 50u 280u 20u "$9 ($0)"
+ Pop $MultiUser.InstallModePage.CurrentUser
+
+
+ nsDialogs::SetUserData $MultiUser.InstallModePage.AllUsers 1 ; Install for All Users (1, pra exibir o icone SHIELD de elevation)
+ nsDialogs::SetUserData $MultiUser.InstallModePage.CurrentUser 0 ; Install for Single User (0 pra não exibir)
+
+ ${if} $HasTwoAvailableOptions == "1" ; if there are 2 available options, bind to radiobutton change
+ ${NSD_OnClick} $MultiUser.InstallModePage.CurrentUser ${UNINSTALLER_FUNCPREFIX}InstModeChange
+ ${NSD_OnClick} $MultiUser.InstallModePage.AllUsers ${UNINSTALLER_FUNCPREFIX}InstModeChange
+ ${endif}
+
+ ${NSD_CreateLabel} 0u 110u 280u 50u ""
+ Pop $RadioButtonLabel1
+ ;${NSD_CreateLabel} 0u 120u 280u 20u ""
+ ;Pop $RadioButtonLabel2
+ ;${NSD_CreateLabel} 0u 130u 280u 20u ""
+ ;Pop $RadioButtonLabel3
+
+
+
+ ${if} $MultiUser.InstallMode == "AllUsers" ; setting defaults
+ SendMessage $MultiUser.InstallModePage.AllUsers ${BM_SETCHECK} ${BST_CHECKED} 0 ; set as default
+ SendMessage $MultiUser.InstallModePage.AllUsers ${BM_CLICK} 0 0 ; trigger click event
+ ${else}
+ SendMessage $MultiUser.InstallModePage.CurrentUser ${BM_SETCHECK} ${BST_CHECKED} 0 ; set as default
+ SendMessage $MultiUser.InstallModePage.CurrentUser ${BM_CLICK} 0 0 ; trigger click event
+ ${endif}
+
+ !insertmacro MUI_PAGE_FUNCTION_CUSTOM SHOW
+ nsDialogs::Show
+
+ FunctionEnd
+
+ Function "${UNINSTALLER_FUNCPREFIX}${LEAVE}"
+ SendMessage $MultiUser.InstallModePage.AllUsers ${BM_GETCHECK} 0 0 $MultiUser.InstallModePage.ReturnValue
+
+ ${if} $MultiUser.InstallModePage.ReturnValue = ${BST_CHECKED}
+ ${if} $IsAdmin == "0"
+ !ifdef MULTIUSER_INSTALLMODE_ALLOW_ELEVATION ; if it's not Power or Admin, but elevation is allowed, then elevate...
+ ;MessageBox MB_OK "Will elevate and quit"
+ ShowWindow $HWNDPARENT ${SW_HIDE} ; HideWindow would work?
+ !insertmacro UAC_RunElevated
+ ;MessageBox MB_OK "[$0]/[$1]/[$2]/[$3]"
+
+ ;http://www.videolan.org/developers/vlc/extras/package/win32/NSIS/UAC/Readme.html
+ ;http://nsis.sourceforge.net/UAC_plug-in
+ ${Switch} $0
+ ${Case} 0
+ ${If} $1 = 1
+ Quit ;we are the outer process, the inner process has done its work (ExitCode is $2), we are done
+ ${EndIf}
+ ${If} $1 = 3 ;RunAs completed successfully, but with a non-admin user
+ ${OrIf} $2 = 0x666666 ;our special return, the new process was not admin after all
+ MessageBox mb_IconStop|mb_TopMost|mb_SetForeground "You need to login with an account that is a member of the admin group to continue..."
+ ${EndIf}
+ ${Break}
+ ${Case} 1223 ;user aborted
+ ;MessageBox mb_IconStop|mb_TopMost|mb_SetForeground "This option requires admin privileges, aborting!"
+ ;Quit ; instead of quit just abort going to the next page, and stay in the radiobuttons
+ ${Break}
+ ${Case} 1062
+ MessageBox mb_IconStop|mb_TopMost|mb_SetForeground "Logon service not running, aborting!" ; "Unable to elevate, Secondary Logon service not running!"
+ ;Quit ; instead of quit just abort going to the next page, and stay in the radiobuttons
+ ${Break}
+ ${Default}
+ MessageBox mb_IconStop|mb_TopMost|mb_SetForeground "Unable to elevate, error $0"
+ ;Quit ; instead of quit just abort going to the next page, and stay in the radiobuttons
+ ${Break}
+ ${EndSwitch}
+
+ ShowWindow $HWNDPARENT ${SW_SHOW}
+ BringToFront
+ Abort ; Stay on page - http://nsis.sourceforge.net/Abort
+ !else
+ ;se não é Power ou Admin, e não é permitida elevation, então nem deveria ter chegado aqui... o radiobutton deveria estar disabled
+ !endif
+ ${else}
+ Call ${UNINSTALLER_FUNCPREFIX}MultiUser.InstallMode.AllUsers ; if it's Power or Admin, just go on with installation...
+ ${endif}
+ ${else}
+ Call ${UNINSTALLER_FUNCPREFIX}MultiUser.InstallMode.CurrentUser
+ ${endif}
+
+ !insertmacro MUI_PAGE_FUNCTION_CUSTOM LEAVE
+ FunctionEnd
+
+ Function "${UNINSTALLER_FUNCPREFIX}InstModeChange"
+ pop $1
+ nsDialogs::GetUserData $1
+ pop $1
+ GetDlgItem $0 $hwndParent 1 ; get item 1 (next button) at parent window, store in $0 - (0 is back, 1 is next .. what about CANCEL? http://nsis.sourceforge.net/Buttons_Header )
+
+ StrCpy $7 ""
+ ${if} "$1" == "0" ; current user
+ ${if} $HasPerUserInstallation == "1"
+ !if "${UNINSTALLER_PREFIX}" != UN
+ StrCpy $7 "There is already a per-user installation. ($PerUserInstallationFolder)$\r$\nWill reinstall/upgrade."
+ !else
+ StrCpy $7 "There is a per-user installation. ($PerUserInstallationFolder)$\r$\nWill uninstall."
+ !endif
+ ${else}
+ StrCpy $7 "Fresh install for current user only"
+ ${endif}
+ SendMessage $0 ${BCM_SETSHIELD} 0 0 ; hide SHIELD
+ ${else} ; all users
+ ${if} $HasPerMachineInstallation == "1"
+ !if "${UNINSTALLER_PREFIX}" != UN
+ StrCpy $7 "There is already a per-machine installation. ($PerMachineInstallationFolder)$\r$\nWill reinstall/upgrade."
+ !else
+ StrCpy $7 "There is a per-machine installation. ($PerMachineInstallationFolder)$\r$\nWill uninstall."
+ !endif
+ ${else}
+ StrCpy $7 "Fresh install for all users"
+ ${endif}
+ ${if} $IsAdmin == "0"
+ StrCpy $7 "$7 (will prompt for admin credentials)"
+ SendMessage $0 ${BCM_SETSHIELD} 0 1 ; display SHIELD
+ ${else}
+ SendMessage $0 ${BCM_SETSHIELD} 0 0 ; hide SHIELD
+ ${endif}
+ ${endif}
+ SendMessage $RadioButtonLabel1 ${WM_SETTEXT} 0 "STR:$7"
+ ;SendMessage $RadioButtonLabel2 ${WM_SETTEXT} 0 "STR:$8"
+ ;SendMessage $RadioButtonLabel3 ${WM_SETTEXT} 0 "STR:$9"
+ FunctionEnd
+
+!macroend
+
+; SHCTX is the hive HKLM if SetShellVarContext all, or HKCU if SetShellVarContext user
+!macro MULTIUSER_RegistryAddInstallInfo
+ !verbose push
+ !verbose 3
+
+ ; Write the installation path into the registry
+ WriteRegStr SHCTX "${MULTIUSER_INSTALLMODE_INSTALL_REGISTRY_KEY2}" "${MULTIUSER_INSTALLMODE_INSTDIR_REGISTRY_VALUENAME}" "$INSTDIR" ; "InstallLocation"
+
+ ; Write the uninstall keys for Windows
+ ${if} $MultiUser.InstallMode == "AllUsers" ; setting defaults
+ WriteRegStr SHCTX "${MULTIUSER_INSTALLMODE_UNINSTALL_REGISTRY_KEY2}" "DisplayName" "${MULTIUSER_INSTALLMODE_DISPLAYNAME}"
+ WriteRegStr SHCTX "${MULTIUSER_INSTALLMODE_UNINSTALL_REGISTRY_KEY2}" "${MULTIUSER_INSTALLMODE_DEFAULT_REGISTRY_VALUENAME}" '"$INSTDIR\${UNINSTALL_FILENAME}" /allusers' ; "UninstallString"
+ ${else}
+ WriteRegStr SHCTX "${MULTIUSER_INSTALLMODE_UNINSTALL_REGISTRY_KEY2}" "DisplayName" "${MULTIUSER_INSTALLMODE_DISPLAYNAME} (only current user)" ; "add/remove programs" will show if installation is per-user
+ WriteRegStr SHCTX "${MULTIUSER_INSTALLMODE_UNINSTALL_REGISTRY_KEY2}" "${MULTIUSER_INSTALLMODE_DEFAULT_REGISTRY_VALUENAME}" '"$INSTDIR\${UNINSTALL_FILENAME}" /currentuser' ; "UninstallString"
+ ${endif}
+
+ WriteRegStr SHCTX "${MULTIUSER_INSTALLMODE_UNINSTALL_REGISTRY_KEY2}" "DisplayVersion" "${VERSION}"
+ WriteRegStr SHCTX "${MULTIUSER_INSTALLMODE_UNINSTALL_REGISTRY_KEY2}" "DisplayIcon" "$INSTDIR\${APP_EXECUTABLE_FILENAME},0"
+ WriteRegStr SHCTX "${MULTIUSER_INSTALLMODE_UNINSTALL_REGISTRY_KEY2}" "Publisher" "${COMPANY_NAME}"
+ WriteRegDWORD SHCTX "${MULTIUSER_INSTALLMODE_UNINSTALL_REGISTRY_KEY2}" "NoModify" 1
+ WriteRegDWORD SHCTX "${MULTIUSER_INSTALLMODE_UNINSTALL_REGISTRY_KEY2}" "NoRepair" 1
+ ${GetSize} "$INSTDIR" "/S=0K" $0 $1 $2 ; get folder size, convert to KB
+ IntFmt $0 "0x%08X" $0
+ WriteRegDWORD SHCTX "${MULTIUSER_INSTALLMODE_UNINSTALL_REGISTRY_KEY2}" "EstimatedSize" "$0"
+
+ !verbose pop
+!macroend
+
+!macro MULTIUSER_RegistryRemoveInstallInfo
+ !verbose push
+ !verbose 3
+
+ ; Remove registry keys
+ DeleteRegKey SHCTX "${MULTIUSER_INSTALLMODE_UNINSTALL_REGISTRY_KEY2}"
+ DeleteRegKey SHCTX "${MULTIUSER_INSTALLMODE_INSTALL_REGISTRY_KEY2}"
+
+ !verbose pop
+!macroend
+
+
+
+!verbose pop
diff --git a/templates/nsis/allowOnlyOneInstallerInstace.nsh b/templates/nsis/allowOnlyOneInstallerInstace.nsh
new file mode 100644
index 00000000000..d606800479a
--- /dev/null
+++ b/templates/nsis/allowOnlyOneInstallerInstace.nsh
@@ -0,0 +1,21 @@
+# http://nsis.sourceforge.net/Allow_only_one_installer_instance
+!macro ALLOW_ONLY_ONE_INSTALLER_INSTACE
+ BringToFront
+ !define /ifndef SYSTYPE_PTR p ; NSIS v3.0+
+ System::Call 'kernel32::CreateMutex(${SYSTYPE_PTR}0, i1, t"${APP_ID}")?e'
+ Pop $0
+ IntCmpU $0 183 0 launch launch ; ERROR_ALREADY_EXISTS
+ StrLen $0 "$(^SetupCaption)"
+ IntOp $0 $0 + 1 ; GetWindowText count includes \0
+ StrCpy $1 "" ; Start FindWindow with NULL
+ loop:
+ FindWindow $1 "#32770" "" "" $1
+ StrCmp 0 $1 notfound
+ System::Call 'user32::GetWindowText(${SYSTYPE_PTR}r1, t.r2, ir0)'
+ StrCmp $2 "$(^SetupCaption)" 0 loop
+ SendMessage $1 0x112 0xF120 0 /TIMEOUT=2000 ; WM_SYSCOMMAND:SC_RESTORE to restore the window if it is minimized
+ System::Call "user32::SetForegroundWindow(${SYSTYPE_PTR}r1)"
+ notfound:
+ Abort
+ launch:
+!macroend
\ No newline at end of file
diff --git a/templates/nsis/boring-installer.nsh b/templates/nsis/boring-installer.nsh
new file mode 100644
index 00000000000..eaf3bf57c6c
--- /dev/null
+++ b/templates/nsis/boring-installer.nsh
@@ -0,0 +1,18 @@
+BrandingText "${PRODUCT_NAME} ${VERSION}"
+
+# http://nsis.sourceforge.net/Run_an_application_shortcut_after_an_install
+#!define MUI_FINISHPAGE_RUN_TEXT "Start ${PRODUCT_NAME}"
+!define MUI_FINISHPAGE_RUN
+!define MUI_FINISHPAGE_RUN_FUNCTION "StartApp"
+
+!insertmacro MUI_PAGE_WELCOME
+!insertmacro MULTIUSER_PAGE_INSTALLMODE
+!insertmacro MUI_PAGE_INSTFILES
+!insertmacro MUI_PAGE_FINISH
+
+!insertmacro MUI_LANGUAGE "English"
+
+# uninstall pages
+!insertmacro MUI_UNPAGE_CONFIRM
+!insertmacro MUI_UNPAGE_INSTFILES
+!insertmacro MUI_UNPAGE_FINISH
\ No newline at end of file
diff --git a/templates/nsis/checkAppRunning.nsh b/templates/nsis/checkAppRunning.nsh
new file mode 100644
index 00000000000..5f0c818fa4b
--- /dev/null
+++ b/templates/nsis/checkAppRunning.nsh
@@ -0,0 +1,13 @@
+!macro CHECK_APP_RUNNING MODE
+ ${nsProcess::FindProcess} "${APP_EXECUTABLE_FILENAME}" $R0
+ ${If} $R0 == 0
+ MessageBox MB_OKCANCEL|MB_ICONEXCLAMATION "${PRODUCT_NAME} is running. $\r$\nClick OK to close it and continue with ${MODE}." /SD IDCANCEL IDOK doStopProcess
+ Abort
+ doStopProcess:
+ DetailPrint "Closing running ${PRODUCT_NAME} ..."
+ ${nsProcess::KillProcess} "${APP_EXECUTABLE_FILENAME}" $R0
+ DetailPrint "Waiting for ${PRODUCT_NAME} to close."
+ Sleep 2000
+ ${EndIf}
+ ${nsProcess::Unload}
+!macroend
\ No newline at end of file
diff --git a/templates/nsis/installer.nsi b/templates/nsis/installer.nsi
new file mode 100644
index 00000000000..ab1fc3a74fb
--- /dev/null
+++ b/templates/nsis/installer.nsi
@@ -0,0 +1,93 @@
+!include "MUI2.nsh"
+!include "NsisMultiUser.nsh"
+!include "nsProcess.nsh"
+!include "allowOnlyOneInstallerInstace.nsh"
+!include "checkAppRunning.nsh"
+
+Function StartApp
+ ExecShell "" "$SMPROGRAMS\${PRODUCT_NAME}.lnk"
+FunctionEnd
+
+!ifndef ONE_CLICK
+ !include "boring-installer.nsh"
+!endif
+
+!ifdef ONE_CLICK
+ !insertmacro MUI_PAGE_INSTFILES
+ !insertmacro MUI_UNPAGE_INSTFILES
+!endif
+
+Var startMenuLink
+Var desktopLink
+
+Function .onInit
+ !insertmacro MULTIUSER_INIT
+ !insertmacro ALLOW_ONLY_ONE_INSTALLER_INSTACE
+FunctionEnd
+
+Function un.onInit
+ !insertmacro MULTIUSER_UNINIT
+FunctionEnd
+
+# default section start
+Section "install"
+ !insertmacro CHECK_APP_RUNNING "install"
+
+ # delete the installed files
+ RMDir /r $INSTDIR
+
+ # define the path to which the installer should install
+ SetOutPath $INSTDIR
+
+ # specify the files to go in the output path
+ File /r "${APP_BUILD_DIR}\*"
+
+# <% if(fileAssociation){ %>
+ # specify file association
+# ${registerExtension} "$INSTDIR\${PRODUCT_NAME}.exe" "<%= fileAssociation.extension %>" "<%= fileAssociation.fileType %>"
+# <% } %>
+
+ WriteUninstaller "${UNINSTALL_FILENAME}"
+ !insertmacro MULTIUSER_RegistryAddInstallInfo
+
+ StrCpy $startMenuLink "$SMPROGRAMS\${PRODUCT_NAME}.lnk"
+ StrCpy $desktopLink "$DESKTOP\${PRODUCT_NAME}.lnk"
+
+ # create shortcuts in the start menu and on the desktop
+ # shortcut for uninstall is bad cause user can choose this by mistake during search, so, we don't add it
+ CreateShortCut "$startMenuLink" "$INSTDIR\${APP_EXECUTABLE_FILENAME}" "" "$INSTDIR\${APP_EXECUTABLE_FILENAME}" 0 "" "" "${APP_DESCRIPTION}"
+ CreateShortCut "$desktopLink" "$INSTDIR\${APP_EXECUTABLE_FILENAME}" "" "$INSTDIR\${APP_EXECUTABLE_FILENAME}" 0 "" "" "${APP_DESCRIPTION}"
+
+ WinShell::SetLnkAUMI "$startMenuLink" "${APP_ID}"
+ WinShell::SetLnkAUMI "$desktopLink" "${APP_ID}"
+
+ !ifdef ONE_CLICK
+ # otherwise app window will be in backround
+ HideWindow
+ Call StartApp
+ !endif
+SectionEnd
+
+Section "un.install"
+ !insertmacro CHECK_APP_RUNNING "uninstall"
+
+ StrCpy $startMenuLink "$SMPROGRAMS\${PRODUCT_NAME}.lnk"
+ StrCpy $desktopLink "$DESKTOP\${PRODUCT_NAME}.lnk"
+
+ WinShell::UninstAppUserModelId "${APP_ID}"
+ WinShell::UninstShortcut "$startMenuLink"
+ WinShell::UninstShortcut "$$desktopLink"
+
+ Delete "$startMenuLink"
+ Delete "$desktopLink"
+
+ # delete the installed files
+ RMDir /r $INSTDIR
+
+ !insertmacro MULTIUSER_RegistryRemoveInstallInfo
+
+ !ifdef ONE_CLICK
+ # strange, AutoCloseWindow=true doesn't work for uninstaller, so, just quit
+ Quit
+ !endif
+SectionEnd
\ No newline at end of file
diff --git a/templates/nsis/readme.md b/templates/nsis/readme.md
new file mode 100644
index 00000000000..2e3195a3e29
--- /dev/null
+++ b/templates/nsis/readme.md
@@ -0,0 +1,22 @@
+It is developer documentation. See [wiki](https://github.com/electron-userland/electron-builder/wiki/nsis).
+
+http://www.mathiaswestin.net/2012/09/how-to-make-per-user-installation-with.html
+
+https://msdn.microsoft.com/en-us/library/windows/desktop/dd378457(v=vs.85).aspx#FOLDERID_UserProgramFiles
+
+https://github.com/Drizin/NsisMultiUser
+
+NSIS vs Inno Setup — it is not easy to choose because both are far from ideal, e.g. inno also doesn't have built-in per-user installation implementation — http://stackoverflow.com/questions/34330668/inno-setup-custom-dialog-with-per-user-or-per-machine-installation.
+
+http://stackoverflow.com/questions/2565215/checking-if-the-application-is-running-in-nsis-before-uninstalling
+
+One-click installer: http://forums.winamp.com/showthread.php?t=300479
+
+# GUID
+See NSIS.md.
+
+We use https://github.com/scravy/uuid-1345 to generate sha-1 name-based UUID.
+
+http://stackoverflow.com/questions/3029994/convert-uri-to-guid
+https://alexandrebrisebois.wordpress.com/2013/11/14/create-predictable-guids-for-your-windows-azure-table-storage-entities/
+https://github.com/Squirrel/Squirrel.Windows/pull/658
\ No newline at end of file
diff --git a/test/fixtures/test-app-one/package.json b/test/fixtures/test-app-one/package.json
index e485c9b305d..c69efd9b0df 100755
--- a/test/fixtures/test-app-one/package.json
+++ b/test/fixtures/test-app-one/package.json
@@ -13,7 +13,7 @@
"electron-prebuilt": "^1.2.1"
},
"build": {
- "app-bundle-id": "your.id",
+ "appId": "org.electron-builder.testApp",
"app-category-type": "your.app.category.type",
"iconUrl": "https://raw.githubusercontent.com/szwacz/electron-boilerplate/master/resources/windows/icon.ico",
"compression": "store"
diff --git a/test/fixtures/test-app/package.json b/test/fixtures/test-app/package.json
index 9123d6acc14..fcf4391403e 100755
--- a/test/fixtures/test-app/package.json
+++ b/test/fixtures/test-app/package.json
@@ -7,7 +7,7 @@
"electron-prebuilt": "^1.2.1"
},
"build": {
- "app-bundle-id": "your.id",
+ "appId": "org.electron-builder.testApp",
"app-category-type": "your.app.category.type",
"iconUrl": "https://raw.githubusercontent.com/szwacz/electron-boilerplate/master/resources/windows/icon.ico",
"compression": "store"
diff --git a/test/src/helpers/avaEx.ts b/test/src/helpers/avaEx.ts
index 71dbc711292..bb447fb23f8 100644
--- a/test/src/helpers/avaEx.ts
+++ b/test/src/helpers/avaEx.ts
@@ -5,6 +5,7 @@ declare module "ava-tf" {
export const ifNotWindows: typeof test;
export const ifOsx: typeof test;
export const ifNotCi: typeof test;
+ export const ifCi: typeof test;
export const ifNotCiOsx: typeof test;
export const ifDevOrWinCi: typeof test;
export const ifWinCi: typeof test;
@@ -28,6 +29,11 @@ Object.defineProperties(test, {
return process.env.CI ? this.skip : this
}
},
+ "ifCi": {
+ get: function () {
+ return process.env.CI ? this : this.skip
+ }
+ },
"ifNotCiOsx": {
get: function () {
return process.env.CI && process.platform === "darwin" ? this.skip : this
diff --git a/test/src/helpers/packTester.ts b/test/src/helpers/packTester.ts
index b9779aa6a68..39a1176fa8d 100755
--- a/test/src/helpers/packTester.ts
+++ b/test/src/helpers/packTester.ts
@@ -30,11 +30,13 @@ interface AssertPackOptions {
readonly expectedArtifacts?: Array
readonly expectedDepends?: string
+
+ readonly useTempDir?: boolean
}
export async function assertPack(fixtureName: string, packagerOptions: PackagerOptions, checkOptions?: AssertPackOptions): Promise {
const tempDirCreated = checkOptions == null ? null : checkOptions.tempDirCreated
- const useTempDir = tempDirCreated != null || packagerOptions.devMetadata != null
+ const useTempDir = tempDirCreated != null || packagerOptions.devMetadata != null || (checkOptions != null && checkOptions.useTempDir)
let projectDir = path.join(__dirname, "..", "..", "fixtures", fixtureName)
// const isDoNotUseTempDir = platform === "darwin"
@@ -195,7 +197,7 @@ async function checkOsXResult(packager: Packager, packagerOptions: PackagerOptio
const info = parsePlist(await readFile(path.join(packedAppDir, "Contents", "Info.plist"), "utf8"))
assertThat2(info).has.properties({
CFBundleDisplayName: productName,
- CFBundleIdentifier: "your.id",
+ CFBundleIdentifier: "org.electron-builder.testApp",
LSApplicationCategoryType: "your.app.category.type",
CFBundleVersion: "1.1.0" + "." + (process.env.TRAVIS_BUILD_NUMBER || process.env.CIRCLE_BUILD_NUM)
})
@@ -245,6 +247,10 @@ async function checkWindowsResult(packager: Packager, targets: Array, ch
artifactNames.push(`TestApp-Setup-1.1.0${archSuffix}.exe`)
}
+ else if (target === "nsis") {
+ expectedFileNames.push(`${productName} Setup 1.1.0${archSuffix}.exe`)
+ artifactNames.push(`TestApp-Setup-1.1.0${archSuffix}.exe`)
+ }
else {
expectedFileNames.push(`${productName}-1.1.0${archSuffix}-win.${target}`)
diff --git a/test/src/winPackagerTest.ts b/test/src/winPackagerTest.ts
index 4d655e208bf..2fe460d5cf6 100755
--- a/test/src/winPackagerTest.ts
+++ b/test/src/winPackagerTest.ts
@@ -1,4 +1,4 @@
-import { Platform, Arch, BuildInfo } from "out"
+import { Platform, Arch, BuildInfo, PackagerOptions } from "out"
import test from "./helpers/avaEx"
import { assertPack, platform, modifyPackageJson, signed } from "./helpers/packTester"
import { move, outputFile } from "fs-extra-p"
@@ -13,18 +13,33 @@ import SquirrelWindowsTarget from "out/targets/squirrelWindows"
//noinspection JSUnusedLocalSymbols
const __awaiter = require("out/awaiter")
-test.ifDevOrWinCi("win", () => assertPack("test-app-one", signed({
+function _signed(packagerOptions: PackagerOptions): PackagerOptions {
+ if (process.platform !== "win32") {
+ // todo Linux Signing failed with SIGBUS
+ return packagerOptions
+ }
+ return signed(packagerOptions)
+}
+
+test.ifNotCiOsx("win", () => assertPack("test-app-one", _signed({
targets: Platform.WINDOWS.createTarget(["default", "zip"]),
})
))
+test.ifNotCiOsx("nsis", () => assertPack("test-app-one", _signed({
+ targets: Platform.WINDOWS.createTarget(["nsis"]),
+ }), {
+ useTempDir: true,
+ }
+))
+
// test.ifNotCiOsx("win 32", () => assertPack("test-app-one", signed({
// targets: Platform.WINDOWS.createTarget(null, Arch.ia32),
// })
// ))
// very slow
-test.ifWinCi("delta", () => assertPack("test-app-one", {
+test.skip("delta", () => assertPack("test-app-one", {
targets: Platform.WINDOWS.createTarget(null, Arch.ia32),
devMetadata: {
build: {
@@ -120,7 +135,7 @@ class CheckingWinPackager extends WinPackager {
const packOptions = this.computePackOptions(outDir, appOutDir, arch)
const helperClass: typeof SquirrelWindowsTarget = require("out/targets/squirrelWindows").default
- this.effectiveDistOptions = await (new helperClass(this, appOutDir, arch).computeEffectiveDistOptions("foo", packOptions, "Foo.exe"))
+ this.effectiveDistOptions = await (new helperClass(this, appOutDir).computeEffectiveDistOptions("foo", packOptions, "Foo.exe"))
await this.sign(appOutDir)
}
diff --git a/test/tsconfig.json b/test/tsconfig.json
index b910be012e8..12b033b31e2 100755
--- a/test/tsconfig.json
+++ b/test/tsconfig.json
@@ -48,6 +48,7 @@
"../typings/progress-stream.d.ts",
"../typings/read-package-json.d.ts",
"../typings/signcode.d.ts",
+ "../typings/uuid-1345.d.ts",
"../typings/yargs.d.ts",
"typings/decompress-zip.d.ts",
"typings/diff.d.ts",
diff --git a/tsconfig.json b/tsconfig.json
index afe4c8b5510..b698828dfa9 100755
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -52,6 +52,7 @@
"typings/progress-stream.d.ts",
"typings/read-package-json.d.ts",
"typings/signcode.d.ts",
+ "typings/uuid-1345.d.ts",
"typings/yargs.d.ts",
"node_modules/fs-extra-p/index.d.ts",
"node_modules/7zip-bin/index.d.ts",
@@ -79,6 +80,7 @@
"src/promise.ts",
"src/repositoryInfo.ts",
"src/targets/archive.ts",
+ "src/targets/nsis.ts",
"src/targets/squirrelWindows.ts",
"src/util.ts",
"src/winPackager.ts"
diff --git a/typings/uuid-1345.d.ts b/typings/uuid-1345.d.ts
new file mode 100644
index 00000000000..fa9c206d4a2
--- /dev/null
+++ b/typings/uuid-1345.d.ts
@@ -0,0 +1,8 @@
+declare module "uuid-1345" {
+ interface NameUuidOptions {
+ namespace: string
+ name: string
+ }
+
+ export function v5(options: NameUuidOptions, callback: (error: Error, result: string) => void): void
+}
\ No newline at end of file