Skip to content

Commit

Permalink
feat: NSIS target (electron-userland#495)
Browse files Browse the repository at this point in the history
  • Loading branch information
develar authored Jun 14, 2016
1 parent 3fc7a89 commit d8762db
Show file tree
Hide file tree
Showing 29 changed files with 1,256 additions and 68 deletions.
10 changes: 10 additions & 0 deletions .idea/dictionaries/develar.xml

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

35 changes: 35 additions & 0 deletions docker/nsis.sh
Original file line number Diff line number Diff line change
@@ -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}
10 changes: 10 additions & 0 deletions docs/NSIS.md
Original file line number Diff line number Diff line change
@@ -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).
14 changes: 13 additions & 1 deletion docs/Options.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ Here documented only `electron-builder` specific options:
## `.build`
| Name | Description
| --- | ---
| app-bundle-id | <a name="BuildMetadata-app-bundle-id"></a>*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 | <a name="BuildMetadata-appId"></a><p>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.</p> <p>For windows only NSIS target supports it. Squirrel.Windows is not fixed yet.</p> <p>Defaults to <code>com.electron.${name}</code>. It is strongly recommended that an explicit ID be set.</p>
| app-category-type | <a name="BuildMetadata-app-category-type"></a><p>*OS X-only.* The application category type, as shown in the Finder via *View -&gt; Arrange by Application Category* when viewing the Applications directory.</p> <p>For example, <code>app-category-type=public.app-category.developer-tools</code> will set the application category to *Developer Tools*.</p> <p>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).</p>
| asar | <a name="BuildMetadata-asar"></a><p>Whether to package the application’s source code into an archive, using [Electron’s archive format](https://github.com/electron/asar). Defaults to <code>true</code>. 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/).</p> <p>Or you can pass object of any asar options.</p>
| productName | <a name="BuildMetadata-productName"></a>See [AppMetadata.productName](#AppMetadata-productName).
Expand All @@ -60,6 +60,7 @@ Here documented only `electron-builder` specific options:
| osx | <a name="BuildMetadata-osx"></a>See [.build.osx](#OsXBuildOptions).
| mas | <a name="BuildMetadata-mas"></a>See [.build.mas](#MasBuildOptions).
| win | <a name="BuildMetadata-win"></a>See [.build.win](#LinuxBuildOptions).
| nsis | <a name="BuildMetadata-nsis"></a>See [.build.nsis](#NsisOptions).
| linux | <a name="BuildMetadata-linux"></a>See [.build.linux](#LinuxBuildOptions).
| compression | <a name="BuildMetadata-compression"></a>The compression level, one of `store`, `normal`, `maximum` (default: `normal`). If you want to rapidly test build, `store` can reduce build time significantly.
| afterPack | <a name="BuildMetadata-afterPack"></a>*programmatic API only* The function to be run after pack (but before pack into distributable format and sign). Promise must be returned.
Expand Down Expand Up @@ -100,6 +101,17 @@ MAS (Mac Application Store) specific options (in addition to `build.osx`).
| remoteToken | <a name="WinBuildOptions-remoteToken"></a>Authentication token for remote updates
| signingHashAlgorithms | <a name="WinBuildOptions-signingHashAlgorithms"></a>Array of signing algorithms used. Defaults to `['sha1', 'sha256']`

<a name="NsisOptions"></a>
### `.build.nsis`

NSIS target support in progress — not polished and not fully tested and checked.

| Name | Description
| --- | ---
| perMachine | <a name="NsisOptions-perMachine"></a>Mark "all users" (per-machine) as default. Not recommended. Defaults to `false`.
| allowElevation | <a name="NsisOptions-allowElevation"></a>Allow requesting for elevation. If false, user will have to restart installer with elevated permissions. Defaults to `true`.
| oneClick | <a name="NsisOptions-oneClick"></a>One-click installation. Defaults to `true`.

<a name="LinuxBuildOptions"></a>
### `.build.linux`
| Name | Description
Expand Down
6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down Expand Up @@ -128,5 +129,8 @@
"test/out/*"
]
},
"typings": "./out/electron-builder.d.ts"
"typings": "./out/electron-builder.d.ts",
"publishConfig": {
"tag": "next"
}
}
5 changes: 2 additions & 3 deletions src/errorMessages.ts
Original file line number Diff line number Diff line change
@@ -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"
}
}
Expand Down
63 changes: 34 additions & 29 deletions src/fpmDownload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,59 +13,64 @@ const versionToPromise = new Map<string, BluebirdPromise<string>>()

// 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<string> {
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<string> {
let promise = versionToPromise.get(dirName)
// if rejected, we will try to download again
if (<any>promise != null && !promise!.isRejected()) {
return promise!
if (promise != null && !promise.isRejected()) {
return promise
}

promise = <BluebirdPromise<string>>doDownloadFpm(version, osAndArch)
versionToPromise.set(version, promise)
promise = <BluebirdPromise<string>>doGetBin(name, dirName, url, sha1)
versionToPromise.set(dirName, promise)
return promise
}

async function doDownloadFpm(version: string, osAndArch: string): Promise<string> {
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<string> {
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
}
32 changes: 24 additions & 8 deletions src/httpRequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,19 @@ import * as path from "path"

const maxRedirects = 10

export const download = <(url: string, destination: string, isCreateDir?: boolean | undefined) => BluebirdPromise<any>>(BluebirdPromise.promisify(_download))
export interface DownloadOptions {
skipDirCreation?: boolean
sha2?: string
}

export const download = <(url: string, destination: string, options?: DownloadOptions) => BluebirdPromise<any>>(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 = <any>isCreateDir
isCreateDir = true
callback = <any>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) {
Expand All @@ -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
Expand All @@ -47,14 +52,25 @@ 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 + ")"))
}
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)
Expand Down
41 changes: 40 additions & 1 deletion src/metadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -150,6 +160,11 @@ export interface BuildMetadata {
*/
readonly win?: WinBuildOptions | null

/**
See [.build.nsis](#NsisOptions).
*/
readonly nsis?: NsisOptions | null

/*
See [.build.linux](#LinuxBuildOptions).
*/
Expand Down Expand Up @@ -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`
*/
Expand Down
Loading

0 comments on commit d8762db

Please sign in to comment.