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