diff --git a/docs/api/electron-builder.md b/docs/api/electron-builder.md index ba2775ccdad..e16116c09e1 100644 --- a/docs/api/electron-builder.md +++ b/docs/api/electron-builder.md @@ -126,7 +126,7 @@ Developer API only. See [Configuration](/configuration/configuration.md) for use * category String - The [application category](https://specifications.freedesktop.org/menu-spec/latest/apa.html#main-category-registry). * desktop any - The [Desktop file](https://developer.gnome.org/integration-guide/stable/desktop-files.html.en) entries (name to value). * artifactName String - The [artifact file name template](/configuration/configuration.md#artifact-file-name-template). -* publish String | [GithubOptions](/configuration/publish.md#githuboptions) | [S3Options](/configuration/publish.md#s3options) | [GenericServerOptions](/configuration/publish.md#genericserveroptions) | [BintrayOptions](/configuration/publish.md#bintrayoptions) | Array +* publish String | [GithubOptions](/configuration/publish.md#githuboptions) | [S3Options](/configuration/publish.md#s3options) | [SpacesOptions](/configuration/publish.md#spacesoptions) | [GenericServerOptions](/configuration/publish.md#genericserveroptions) | [BintrayOptions](/configuration/publish.md#bintrayoptions) | Array ## `PlatformSpecificBuildOptions` ⇐ [TargetSpecificOptions](#TargetSpecificOptions) @@ -143,7 +143,7 @@ Developer API only. See [Configuration](/configuration/configuration.md) for use * fileAssociations Array<[FileAssociation](#FileAssociation)> | [FileAssociation](#FileAssociation) * forceCodeSigning Boolean * artifactName String - The [artifact file name template](/configuration/configuration.md#artifact-file-name-template). -* publish String | [GithubOptions](/configuration/publish.md#githuboptions) | [S3Options](/configuration/publish.md#s3options) | [GenericServerOptions](/configuration/publish.md#genericserveroptions) | [BintrayOptions](/configuration/publish.md#bintrayoptions) | Array +* publish String | [GithubOptions](/configuration/publish.md#githuboptions) | [S3Options](/configuration/publish.md#s3options) | [SpacesOptions](/configuration/publish.md#spacesoptions) | [GenericServerOptions](/configuration/publish.md#genericserveroptions) | [BintrayOptions](/configuration/publish.md#bintrayoptions) | Array ## `SourceRepositoryInfo` diff --git a/docs/auto-update.md b/docs/auto-update.md index 0e586967784..e5bfbf1375d 100644 --- a/docs/auto-update.md +++ b/docs/auto-update.md @@ -191,7 +191,7 @@ Start downloading update manually. You can use this method if `autoDownload` opt Configure update provider. If value is `string`, [GenericServerOptions](/configuration/publish.md#genericserveroptions) will be set with value as `url`. -- options [PublishConfiguration](/configuration/publish.md#publishconfiguration) | [GenericServerOptions](/configuration/publish.md#genericserveroptions) | [S3Options](/configuration/publish.md#s3options) | [BintrayOptions](/configuration/publish.md#bintrayoptions) | [GithubOptions](/configuration/publish.md#githuboptions) | String - If you want to override configuration in the `app-update.yml`. +- options [PublishConfiguration](/configuration/publish.md#publishconfiguration) | [GenericServerOptions](/configuration/publish.md#genericserveroptions) | [S3Options](/configuration/publish.md#s3options) | [SpacesOptions](/configuration/publish.md#spacesoptions) | [BintrayOptions](/configuration/publish.md#bintrayoptions) | [GithubOptions](/configuration/publish.md#githuboptions) | String - If you want to override configuration in the `app-update.yml`. #### `appUpdater.quitAndInstall(isSilent, isForceRunAfter)` diff --git a/docs/configuration/configuration.md b/docs/configuration/configuration.md index d9319786859..0997626e66e 100644 --- a/docs/configuration/configuration.md +++ b/docs/configuration/configuration.md @@ -144,8 +144,8 @@ Env file `electron-builder.env` in the current dir ([example](https://github.com --- -* afterPack callback - *programmatic API only* The function to be run after pack (but before pack into distributable format and sign). Promise must be returned. -* beforeBuild callback - *programmatic API only* The function to be run before dependencies are installed or rebuilt. Works when `npmRebuild` is set to `true`. Promise must be returned. Resolving to `false` will skip dependencies install or rebuild. +* afterPack (context: AfterPackContext) => Promise | null - The function (or path to file or module id) to be run after pack (but before pack into distributable format and sign). +* beforeBuild (context: BeforeBuildContext) => Promise | null - The function (or path to file or module id) to be run before dependencies are installed or rebuilt. Works when `npmRebuild` is set to `true`. Resolving to `false` will skip dependencies install or rebuild. ## Metadata Some standard fields should be defined in the `package.json`. diff --git a/docs/configuration/nsis.md b/docs/configuration/nsis.md index 641002ffbcb..ac9255d0ecd 100644 --- a/docs/configuration/nsis.md +++ b/docs/configuration/nsis.md @@ -117,6 +117,7 @@ You can explicitly set guid using option [nsis.guid](#NsisOptions-guid), but it It is also important to set the Application User Model ID (AUMID) to the [appId](configuration.md#Configuration-appId) of the application, in order for notifications on Windows 8/8.1 to function and for Window 10 notifications to display the app icon within the notifications by default. The AUMID should be set within the Main process and before any BrowserWindows have been opened, it is normally the first piece of code executed: `app.setAppUserModelId(appId)` +--- ## Common Questions #### How do change the default installation directory to custom? diff --git a/docs/configuration/win.md b/docs/configuration/win.md index 2e5a6db0a97..7b9a1990aa5 100644 --- a/docs/configuration/win.md +++ b/docs/configuration/win.md @@ -6,9 +6,13 @@ The top-level [win](configuration.md#Configuration-win) key contains set of opti To use Squirrel.Windows please install `electron-builder-squirrel-windows` dependency. For `portable` app, `PORTABLE_EXECUTABLE_DIR` env is set (dir where portable executable located). -* signingHashAlgorithms = `['sha1', 'sha256']` Array<"sha1" | "sha256"> - Array of signing algorithms used. For AppX `sha256` is always used. * icon = `build/icon.ico` String - The path to application icon. * legalTrademarks String - The trademarks and registered trademarks. + +--- + +* signingHashAlgorithms = `['sha1', 'sha256']` Array<"sha1" | "sha256"> - Array of signing algorithms used. For AppX `sha256` is always used. +* sign String | (configuration: CustomWindowsSignTaskConfiguration) => Promise - The custom function (or path to file or module id) to sign Windows executable. * certificateFile String - The path to the *.pfx certificate you want to sign with. Please use it only if you cannot use env variable `CSC_LINK` (`WIN_CSC_LINK`) for some reason. Please see [Code Signing](../code-signing.md). * certificatePassword String - The password to the certificate provided in `certificateFile`. Please use it only if you cannot use env variable `CSC_KEY_PASSWORD` (`WIN_CSC_KEY_PASSWORD`) for some reason. Please see [Code Signing](../code-signing.md). * certificateSubjectName String - The name of the subject of the signing certificate. Required only for EV Code Signing and works only on Windows. @@ -16,7 +20,30 @@ The top-level [win](configuration.md#Configuration-win) key contains set of opti * additionalCertificateFile String - The path to an additional certificate file you want to add to the signature block. * rfc3161TimeStampServer = `http://timestamp.comodoca.com/rfc3161` String - The URL of the RFC 3161 time stamp server. * timeStampServer = `http://timestamp.verisign.com/scripts/timstamp.dll` String - The URL of the time stamp server. + +--- + * publisherName String | Array<String> - [The publisher name](https://github.com/electron-userland/electron-builder/issues/1187#issuecomment-278972073), exactly as in your code signed certificate. Several names can be provided. Defaults to common name from your code signing certificate. * verifyUpdateCodeSignature = `true` Boolean - Whether to verify the signature of an available update before installation. The [publisher name](#publisherName) will be used for the signature verification. * requestedExecutionLevel = `asInvoker` "asInvoker" | "highestAvailable" | "requireAdministrator" - The [security level](https://msdn.microsoft.com/en-us/library/6ad1fshk.aspx#Anchor_9) at which the application requests to be executed. Cannot be specified per target, allowed only in the `win`. - \ No newline at end of file + + +--- + +## Common Questions +#### How do delegate code signing? + +Use [sign](#WindowsConfiguration-sign) option. + +```json +"win": { + "sign": "./customSign.js" +} +``` + +File `customSign.js` in the project root directory: +```js +exports.default = async function(configuration) { + // your custom code +} +``` \ No newline at end of file diff --git a/packages/builder-util-runtime/src/publishOptions.ts b/packages/builder-util-runtime/src/publishOptions.ts index 7e942a268c7..1feb243b39a 100644 --- a/packages/builder-util-runtime/src/publishOptions.ts +++ b/packages/builder-util-runtime/src/publishOptions.ts @@ -1,7 +1,7 @@ export type PublishProvider = "github" | "bintray" | "s3" | "spaces" | "generic" // typescript-json-schema generates only PublishConfiguration if it is specified in the list, so, it is not added here -export type AllPublishOptions = string | GithubOptions | S3Options | GenericServerOptions | BintrayOptions +export type AllPublishOptions = string | GithubOptions | S3Options | SpacesOptions | GenericServerOptions | BintrayOptions // https://github.com/YousefED/typescript-json-schema/issues/80 export type Publish = AllPublishOptions | Array | null diff --git a/packages/electron-builder/src/configuration.ts b/packages/electron-builder/src/configuration.ts index fcddf652e2c..29606b256bb 100644 --- a/packages/electron-builder/src/configuration.ts +++ b/packages/electron-builder/src/configuration.ts @@ -222,11 +222,11 @@ export interface Configuration extends PlatformSpecificBuildOptions { readonly releaseInfo?: ReleaseInfo /** - * *programmatic API only* The function to be run after pack (but before pack into distributable format and sign). Promise must be returned. + * The function (or path to file or module id) to be run after pack (but before pack into distributable format and sign). */ readonly afterPack?: (context: AfterPackContext) => Promise | null /** - * *programmatic API only* The function to be run before dependencies are installed or rebuilt. Works when `npmRebuild` is set to `true`. Promise must be returned. Resolving to `false` will skip dependencies install or rebuild. + * The function (or path to file or module id) to be run before dependencies are installed or rebuilt. Works when `npmRebuild` is set to `true`. Resolving to `false` will skip dependencies install or rebuild. */ readonly beforeBuild?: (context: BeforeBuildContext) => Promise | null diff --git a/packages/electron-builder/src/index.ts b/packages/electron-builder/src/index.ts index 147d3b5480a..6ccd1ac323e 100644 --- a/packages/electron-builder/src/index.ts +++ b/packages/electron-builder/src/index.ts @@ -12,4 +12,5 @@ export { SnapOptions } from "./options/SnapOptions" export { buildForge, ForgeOptions } from "./forge/forge-maker" export { Metadata, AuthorMetadata, RepositoryInfo } from "./options/metadata" export { AppInfo } from "./appInfo" -export { SquirrelWindowsOptions } from "./options/SquirrelWindowsOptions" \ No newline at end of file +export { SquirrelWindowsOptions } from "./options/SquirrelWindowsOptions" +export { WindowsSignOptions, CustomWindowsSignTaskConfiguration, WindowsSignTaskConfiguration, CustomWindowsSign } from "./windowsCodeSign" \ No newline at end of file diff --git a/packages/electron-builder/src/options/winOptions.ts b/packages/electron-builder/src/options/winOptions.ts index 2740ee7dba1..aeaa9cbc279 100644 --- a/packages/electron-builder/src/options/winOptions.ts +++ b/packages/electron-builder/src/options/winOptions.ts @@ -1,5 +1,6 @@ import { PlatformSpecificBuildOptions } from "../configuration" import { TargetConfigType, TargetSpecificOptions } from "../core" +import { CustomWindowsSign } from "../windowsCodeSign" export interface WindowsConfiguration extends PlatformSpecificBuildOptions { /** @@ -14,12 +15,6 @@ export interface WindowsConfiguration extends PlatformSpecificBuildOptions { */ readonly target?: TargetConfigType - /** - * Array of signing algorithms used. For AppX `sha256` is always used. - * @default ['sha1', 'sha256'] - */ - readonly signingHashAlgorithms?: Array<"sha1" | "sha256"> | null - /** * The path to application icon. * @default build/icon.ico @@ -31,39 +26,42 @@ export interface WindowsConfiguration extends PlatformSpecificBuildOptions { */ readonly legalTrademarks?: string | null + /** + * Array of signing algorithms used. For AppX `sha256` is always used. + * @default ['sha1', 'sha256'] + */ + readonly signingHashAlgorithms?: Array<"sha1" | "sha256"> | null + /** + * The custom function (or path to file or module id) to sign Windows executable. + */ + readonly sign?: CustomWindowsSign| string | null /** * The path to the *.pfx certificate you want to sign with. Please use it only if you cannot use env variable `CSC_LINK` (`WIN_CSC_LINK`) for some reason. * Please see [Code Signing](../code-signing.md). */ readonly certificateFile?: string | null - /** * The password to the certificate provided in `certificateFile`. Please use it only if you cannot use env variable `CSC_KEY_PASSWORD` (`WIN_CSC_KEY_PASSWORD`) for some reason. * Please see [Code Signing](../code-signing.md). */ readonly certificatePassword?: string | null - /** * The name of the subject of the signing certificate. Required only for EV Code Signing and works only on Windows. */ readonly certificateSubjectName?: string | null - /** * The SHA1 hash of the signing certificate. The SHA1 hash is commonly specified when multiple certificates satisfy the criteria specified by the remaining switches. Works only on Windows. */ readonly certificateSha1?: string | null - /** * The path to an additional certificate file you want to add to the signature block. */ readonly additionalCertificateFile?: string | null - /** * The URL of the RFC 3161 time stamp server. * @default http://timestamp.comodoca.com/rfc3161 */ readonly rfc3161TimeStampServer?: string | null - /** * The URL of the time stamp server. * @default http://timestamp.verisign.com/scripts/timstamp.dll diff --git a/packages/electron-builder/src/packager.ts b/packages/electron-builder/src/packager.ts index 5204c93c58a..fe825385696 100644 --- a/packages/electron-builder/src/packager.ts +++ b/packages/electron-builder/src/packager.ts @@ -15,7 +15,7 @@ import { Platform, SourceRepositoryInfo, Target } from "./core" import MacPackager from "./macPackager" import { Metadata } from "./options/metadata" import { ArtifactCreated, PackagerOptions } from "./packagerApi" -import { PlatformPackager } from "./platformPackager" +import { PlatformPackager, resolveFunction } from "./platformPackager" import { computeArchToTargetNamesMap, createTargets, NoOpTarget } from "./targets/targetFactory" import { computeDefaultAppDirectory, getConfig, validateConfig } from "./util/config" import { computeElectronVersion, getElectronVersionFromInstalled } from "./util/electronVersion" @@ -143,7 +143,7 @@ export class Packager { if (debug.enabled) { debug(`Effective config:\n${safeDump(JSON.parse(safeStringifyJson(config)))}`) } - await validateConfig(config) + await validateConfig(config, this.debugLogger) this._configuration = config this.appDir = await computeDefaultAppDirectory(projectDir, use(config.directories, it => it!.app)) @@ -318,7 +318,7 @@ export class Packager { return } - const beforeBuild = config.beforeBuild + const beforeBuild = resolveFunction(config.beforeBuild) if (beforeBuild != null) { const performDependenciesInstallOrRebuild = await beforeBuild({ appDir: this.appDir, @@ -345,7 +345,7 @@ export class Packager { } afterPack(context: AfterPackContext): Promise { - const afterPack = this.config.afterPack + const afterPack = resolveFunction(this.config.afterPack) const handlers = this.afterPackHandlers.slice() if (afterPack != null) { // user handler should be last diff --git a/packages/electron-builder/src/platformPackager.ts b/packages/electron-builder/src/platformPackager.ts index 575364cd21c..f902566bcac 100644 --- a/packages/electron-builder/src/platformPackager.ts +++ b/packages/electron-builder/src/platformPackager.ts @@ -1,6 +1,6 @@ import { computeData } from "asar-integrity" import BluebirdPromise from "bluebird-lst" -import { Arch, asArray, AsyncTaskManager, DebugLogger, getArchSuffix, isEmptyOrSpaces, log, use, warn } from "builder-util" +import { Arch, asArray, AsyncTaskManager, debug, DebugLogger, getArchSuffix, isEmptyOrSpaces, log, use, warn } from "builder-util" import { PackageBuilder } from "builder-util/out/api" import { statOrNull, unlinkIfExists } from "builder-util/out/fs" import { orIfFileNotExist } from "builder-util/out/promise" @@ -552,3 +552,24 @@ export abstract class PlatformPackager export function normalizeExt(ext: string) { return ext.startsWith(".") ? ext.substring(1) : ext } + +export function resolveFunction(executor: T | string): T { + if (typeof executor !== "string") { + return executor + } + + let p = executor as string + if (p.startsWith(".")) { + p = path.resolve(p) + } + try { + p = require.resolve(p) + } + catch (e) { + debug(e) + p = path.resolve(p) + } + + const m = require(p) + return m.default || m +} diff --git a/packages/electron-builder/src/publish/updateUnfoBuilder.ts b/packages/electron-builder/src/publish/updateUnfoBuilder.ts index a7a8e0c2739..ff33b9774de 100644 --- a/packages/electron-builder/src/publish/updateUnfoBuilder.ts +++ b/packages/electron-builder/src/publish/updateUnfoBuilder.ts @@ -57,7 +57,9 @@ export async function writeUpdateInfo(event: ArtifactCreated, _publishConfigs: A } // spaces is a new publish provider, no need to keep backward compatibility - if (isMac && publishConfig.provider !== "spaces") { + const isElectronUpdater1xCompatibility = publishConfig.provider !== "spaces" + + if (isMac && isElectronUpdater1xCompatibility) { await writeOldMacInfo(publishConfig, outDir, dir, channel, createdFiles, version, packager) } @@ -69,12 +71,12 @@ export async function writeUpdateInfo(event: ArtifactCreated, _publishConfigs: A createdFiles.add(updateInfoFile) // noinspection JSDeprecatedSymbols - if (packager.platform === Platform.WINDOWS && info.sha2 == null) { + if (isElectronUpdater1xCompatibility && packager.platform === Platform.WINDOWS && info.sha2 == null) { // backward compatibility (info as any).sha2 = await sha2.value } - if (event.safeArtifactName != null) { + if (event.safeArtifactName != null && publishConfig.provider === "github") { info = { ...info, githubArtifactName: event.safeArtifactName, diff --git a/packages/electron-builder/src/util/config.ts b/packages/electron-builder/src/util/config.ts index 5515f68444c..2f28ae04e99 100644 --- a/packages/electron-builder/src/util/config.ts +++ b/packages/electron-builder/src/util/config.ts @@ -1,4 +1,4 @@ -import { asArray, debug, log, warn } from "builder-util" +import { asArray, DebugLogger, log, warn } from "builder-util" import { statOrNull } from "builder-util/out/fs" import { readJson } from "fs-extra-p" import { Lazy } from "lazy-val" @@ -84,7 +84,7 @@ export async function getConfig(projectDir: string, configPath: string | null, c const schemeDataPromise = new Lazy(() => readJson(path.join(__dirname, "..", "..", "scheme.json"))) /** @internal */ -export async function validateConfig(config: Configuration) { +export async function validateConfig(config: Configuration, debugLogger: DebugLogger) { const extraMetadata = config.extraMetadata if (extraMetadata != null) { if (extraMetadata.build != null) { @@ -101,8 +101,8 @@ export async function validateConfig(config: Configuration) { } await _validateConfig(config, schemeDataPromise, (message, errors) => { - if (debug.enabled) { - debug(JSON.stringify(errors, null, 2)) + if (debugLogger.enabled) { + debugLogger.add("invalidConfig", JSON.stringify(errors, null, 2)) } return `${message} diff --git a/packages/electron-builder/src/winPackager.ts b/packages/electron-builder/src/winPackager.ts index 81a9260b83b..596e6401223 100644 --- a/packages/electron-builder/src/winPackager.ts +++ b/packages/electron-builder/src/winPackager.ts @@ -21,7 +21,7 @@ import { createCommonTarget } from "./targets/targetFactory" import { BuildCacheManager, digest } from "./util/cacheManager" import { isBuildCacheEnabled } from "./util/flags" import { time } from "./util/timer" -import { FileCodeSigningInfo, getSignVendorPath, sign, SignOptions } from "./windowsCodeSign" +import { FileCodeSigningInfo, getSignVendorPath, sign, WindowsSignOptions } from "./windowsCodeSign" export class WinPackager extends PlatformPackager { readonly cscInfo = new Lazy(() => { @@ -195,12 +195,21 @@ export class WinPackager extends PlatformPackager { } async sign(file: string, logMessagePrefix?: string) { + const signOptions: WindowsSignOptions = { + path: file, + name: this.appInfo.productName, + site: await this.appInfo.computePackageUrl(), + options: this.platformSpecificBuildOptions, + } + const cscInfo = await this.cscInfo.value if (cscInfo == null) { - if (this.forceCodeSigning) { + if (this.platformSpecificBuildOptions.sign != null) { + await sign(signOptions) + } + else if (this.forceCodeSigning) { throw new Error(`App is not signed and "forceCodeSigning" is set to true, please ensure that code signing configuration is correct, please see https://electron.build/code-signing`) } - return } @@ -221,14 +230,10 @@ export class WinPackager extends PlatformPackager { log(`${logMessagePrefix} (certificate file: "${certFile}")`) } - await this.doSign({ - path: file, - + await WinPackager.doSign({ + ...signOptions, cert: certFile, - password: cscInfo.password, - name: this.appInfo.productName, - site: await this.appInfo.computePackageUrl(), options: { ...this.platformSpecificBuildOptions, certificateSubjectName: cscInfo.subjectName, @@ -237,8 +242,7 @@ export class WinPackager extends PlatformPackager { }) } - //noinspection JSMethodCanBeStatic - protected async doSign(options: SignOptions) { + private static async doSign(options: WindowsSignOptions) { for (let i = 0; i < 3; i++) { try { await sign(options) diff --git a/packages/electron-builder/src/windowsCodeSign.ts b/packages/electron-builder/src/windowsCodeSign.ts index c84746f4fc6..5324f380461 100644 --- a/packages/electron-builder/src/windowsCodeSign.ts +++ b/packages/electron-builder/src/windowsCodeSign.ts @@ -6,6 +6,7 @@ import isCi from "is-ci" import * as os from "os" import * as path from "path" import { WindowsConfiguration } from "./options/winOptions" +import { resolveFunction } from "./platformPackager" import { isUseSystemSigncode } from "./util/flags" export function getSignVendorPath() { @@ -21,7 +22,9 @@ export interface FileCodeSigningInfo { readonly certificateSha1?: string | null } -export interface SignOptions { +export type CustomWindowsSign = (configuration: CustomWindowsSignTaskConfiguration) => Promise + +export interface WindowsSignOptions { readonly path: string readonly cert?: string | null @@ -33,7 +36,19 @@ export interface SignOptions { readonly options: WindowsConfiguration } -export async function sign(options: SignOptions) { +export interface WindowsSignTaskConfiguration extends WindowsSignOptions { + // set if output path differs from input (e.g. osslsigncode cannot sign file inplace) + resultOutputPath?: string + + hash: string + isNest: boolean +} + +export interface CustomWindowsSignTaskConfiguration extends WindowsSignTaskConfiguration { + computeSignToolArgs(isWin: boolean): Array +} + +export async function sign(options: WindowsSignOptions) { let hashes = options.options.signingHashAlgorithms // msi does not support dual-signing if (options.path.endsWith(".msi")) { @@ -42,38 +57,50 @@ export async function sign(options: SignOptions) { else if (options.path.endsWith(".appx")) { hashes = ["sha256"] } + else if (hashes == null) { + hashes = ["sha1", "sha256"] + } else { - if (hashes == null) { - hashes = ["sha1", "sha256"] - } - else { - hashes = Array.isArray(hashes) ? hashes.slice() : [hashes] - } + hashes = Array.isArray(hashes) ? hashes : [hashes] } - const isWin = process.platform === "win32" - let nest = false - //noinspection JSUnusedAssignment - let outputPath = "" + const executor = resolveFunction(options.options.sign) || doSign + let isNest = false for (const hash of hashes) { - outputPath = isWin ? options.path : getOutputPath(options.path, hash) - await spawnSign(options, options.path, outputPath, hash, nest) - nest = true - if (!isWin) { - await rename(outputPath, options.path) + const taskConfiguration: WindowsSignTaskConfiguration = {...options, hash, isNest} + await executor({ + ...taskConfiguration, + computeSignToolArgs: isWin => computeSignToolArgs(taskConfiguration, isWin) + }) + isNest = true + if (taskConfiguration.resultOutputPath != null) { + await rename(taskConfiguration.resultOutputPath, options.path) } } } +async function doSign(configuration: CustomWindowsSignTaskConfiguration) { + const toolInfo = await getToolPath() + await exec(toolInfo.path, configuration.computeSignToolArgs(process.platform === "win32"), { + // https://github.com/electron-userland/electron-builder/pull/1944 + timeout: parseInt(process.env.SIGNTOOL_TIMEOUT as any, 10) || 10 * 60 * 1000, + env: toolInfo.env || process.env + }) +} + // on windows be aware of http://stackoverflow.com/a/32640183/1910191 -async function spawnSign(options: SignOptions, inputPath: string, outputPath: string, hash: string, nest: boolean) { - const isWin = process.platform === "win32" - const args = isWin ? ["sign"] : ["-in", inputPath, "-out", outputPath] +function computeSignToolArgs(options: WindowsSignTaskConfiguration, isWin: boolean): Array { + const outputPath = isWin ? options.path : getOutputPath(options.path, options.hash) + if (!isWin) { + options.resultOutputPath = outputPath + } + + const args = isWin ? ["sign"] : ["-in", options.path, "-out", outputPath] if (process.env.ELECTRON_BUILDER_OFFLINE !== "true") { const timestampingServiceUrl = options.options.timeStampServer || "http://timestamp.verisign.com/scripts/timstamp.dll" if (isWin) { - args.push(nest || hash === "sha256" ? "/tr" : "/t", nest || hash === "sha256" ? (options.options.rfc3161TimeStampServer || "http://timestamp.comodoca.com/rfc3161") : timestampingServiceUrl) + args.push(options.isNest || options.hash === "sha256" ? "/tr" : "/t", options.isNest || options.hash === "sha256" ? (options.options.rfc3161TimeStampServer || "http://timestamp.comodoca.com/rfc3161") : timestampingServiceUrl) } else { args.push("-t", timestampingServiceUrl) @@ -104,8 +131,8 @@ async function spawnSign(options: SignOptions, inputPath: string, outputPath: st } } - if (!isWin || hash !== "sha1") { - args.push(isWin ? "/fd" : "-h", hash) + if (!isWin || options.hash !== "sha1") { + args.push(isWin ? "/fd" : "-h", options.hash) if (isWin && process.env.ELECTRON_BUILDER_OFFLINE !== "true") { args.push("/td", "sha256") } @@ -120,7 +147,7 @@ async function spawnSign(options: SignOptions, inputPath: string, outputPath: st } // msi does not support dual-signing - if (nest) { + if (options.isNest) { args.push(isWin ? "/as" : "-nest") } @@ -134,14 +161,10 @@ async function spawnSign(options: SignOptions, inputPath: string, outputPath: st if (isWin) { // must be last argument - args.push(inputPath) + args.push(options.path) } - const toolInfo = await getToolPath() - return await exec(toolInfo.path, args, { - timeout: parseInt(process.env.SIGNTOOL_TIMEOUT as any, 10) || 120 * 1000, - env: toolInfo.env || process.env - }) + return args } function getOutputPath(inputPath: string, hash: string) { diff --git a/packages/electron-updater/CHANGELOG.md b/packages/electron-updater/CHANGELOG.md index c12f6281ed8..8cd76671754 100644 --- a/packages/electron-updater/CHANGELOG.md +++ b/packages/electron-updater/CHANGELOG.md @@ -1,3 +1,9 @@ +# 2.10.0 (2017-09-22) + +### Features + +* [DigitalOcean Spaces support](https://github.com/electron-userland/electron-builder/releases/tag/v19.30.0). + # 2.9.3 (2017-09-10) ### Features diff --git a/scripts/jsdoc/helpers.js b/scripts/jsdoc/helpers.js index 41bc78af780..3c24f2e20cb 100644 --- a/scripts/jsdoc/helpers.js +++ b/scripts/jsdoc/helpers.js @@ -289,7 +289,7 @@ function identifierToLink(id, root) { id !== "module:https.RequestOptions" && !id.endsWith(".__type") ) { - for (const name of ["GithubOptions", "GenericServerOptions", "BintrayOptions", "S3Options", "PublishConfiguration"]) { + for (const name of ["GithubOptions", "GenericServerOptions", "BintrayOptions", "S3Options", "SpacesOptions", "PublishConfiguration"]) { if (id.endsWith(`.${name}`)) { return `[${name}](/configuration/publish.md#${name.toLowerCase()})` } diff --git a/scripts/jsdoc2md.js b/scripts/jsdoc2md.js index aa127c13723..9e9c7405271 100644 --- a/scripts/jsdoc2md.js +++ b/scripts/jsdoc2md.js @@ -65,7 +65,7 @@ async function render2(files, jsdoc2MdOptions) { const blockedPropertyName = new Set([ "fileAssociations", "directories", "buildVersion", "mac", "linux", "win", "buildDependenciesFromSource", "afterPack", - "installerIcon", "include", "createDesktopShortcut", "displayLanguageSelector", + "installerIcon", "include", "createDesktopShortcut", "displayLanguageSelector", "signingHashAlgorithms", "publisherName" ]) renderer.isInsertHorizontalLineBefore = item => { return blockedPropertyName.has(item.name) @@ -110,6 +110,18 @@ async function render2(files, jsdoc2MdOptions) { } return `The [${label}](/configuration/contents.md#${propertyName.toLowerCase()}) configuration.` } + + if (context.property.name === "sign" && context.object.name === "WindowsConfiguration") { + return "String | (configuration: CustomWindowsSignTaskConfiguration) => Promise" + } + if (context.object.name === "Configuration") { + if (context.property.name === "afterPack") { + return "(context: AfterPackContext) => Promise | null" + } + if (context.property.name === "beforeBuild") { + return "(context: BeforeBuildContext) => Promise | null" + } + } } if (types.some(it => it.endsWith("TargetConfiguration"))) { diff --git a/test/out/__snapshots__/ExtraBuildTest.js.snap b/test/out/__snapshots__/ExtraBuildTest.js.snap index 05e494e0bf0..91691de97e4 100644 --- a/test/out/__snapshots__/ExtraBuildTest.js.snap +++ b/test/out/__snapshots__/ExtraBuildTest.js.snap @@ -47,7 +47,6 @@ Object { Object { "file": "beta.yml", "fileContent": Object { - "githubArtifactName": "TestApp-Setup-1.0.0-beta.1.exe", "path": "beta-TestApp.exe", "version": "1.0.0-beta.1", }, @@ -71,7 +70,6 @@ Object { exports[`override targets in the config - only arch 3`] = ` Object { - "githubArtifactName": "TestApp-Setup-1.0.0-beta.1.exe", "path": "beta-TestApp.exe", "version": "1.0.0-beta.1", } diff --git a/test/out/__snapshots__/PublishManagerTest.js.snap b/test/out/__snapshots__/PublishManagerTest.js.snap index 35548a8e324..c26e4486fa1 100644 --- a/test/out/__snapshots__/PublishManagerTest.js.snap +++ b/test/out/__snapshots__/PublishManagerTest.js.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`generic and github 1`] = ` +exports[`generic, github and spaces 1`] = ` Object { "mac": Array [ Object { @@ -12,7 +12,6 @@ Object { Object { "file": "latest-mac.yml", "fileContent": Object { - "githubArtifactName": "TestApp-1.1.0-mac.zip", "path": "Test App ßW-1.1.0-mac.zip", "version": "1.1.0", }, @@ -25,6 +24,13 @@ Object { "version": "1.1.0", }, }, + Object { + "file": "latest-mac.yml", + "fileContent": Object { + "path": "Test App ßW-1.1.0-mac.zip", + "version": "1.1.0", + }, + }, Object { "arch": "x64", "file": "Test App ßW-1.1.0-mac.zip", @@ -50,7 +56,6 @@ Object { Object { "file": "latest-mac.yml", "fileContent": Object { - "githubArtifactName": "TestApp-1.1.0-mac.zip", "path": "Test App ßW-1.1.0-mac.zip", "version": "1.1.0", }, diff --git a/test/out/mac/__snapshots__/macPackagerTest.js.snap b/test/out/mac/__snapshots__/macPackagerTest.js.snap index 8533c770150..b605d6d6643 100644 --- a/test/out/mac/__snapshots__/macPackagerTest.js.snap +++ b/test/out/mac/__snapshots__/macPackagerTest.js.snap @@ -16,7 +16,6 @@ Object { Object { "file": "latest-mac.yml", "fileContent": Object { - "githubArtifactName": "TestApp-1.1.0-mac.zip", "path": "Test App ßW-1.1.0-mac.zip", "version": "1.1.0", }, diff --git a/test/out/updater/__snapshots__/differentialUpdateTest.js.snap b/test/out/updater/__snapshots__/differentialUpdateTest.js.snap index 73f9f319f15..2967fcf5864 100644 --- a/test/out/updater/__snapshots__/differentialUpdateTest.js.snap +++ b/test/out/updater/__snapshots__/differentialUpdateTest.js.snap @@ -6,7 +6,6 @@ Object { Object { "file": "latest.yml", "fileContent": Object { - "githubArtifactName": "TestApp-WebSetup-1.0.0.exe", "packages": Object { "x64": Object { "file": "TestApp-1.0.0-x64.nsis.7z", diff --git a/test/out/windows/__snapshots__/installerTest.js.snap b/test/out/windows/__snapshots__/assistedInstallerTest.js.snap similarity index 100% rename from test/out/windows/__snapshots__/installerTest.js.snap rename to test/out/windows/__snapshots__/assistedInstallerTest.js.snap diff --git a/test/out/windows/__snapshots__/oneClickInstallerTest.js.snap b/test/out/windows/__snapshots__/oneClickInstallerTest.js.snap index 2d3ea815cfe..2ac9476fbcb 100644 --- a/test/out/windows/__snapshots__/oneClickInstallerTest.js.snap +++ b/test/out/windows/__snapshots__/oneClickInstallerTest.js.snap @@ -112,7 +112,6 @@ Object { Object { "file": "latest.yml", "fileContent": Object { - "githubArtifactName": "TestApp-Setup-1.1.0.exe", "path": "TestApp Setup 1.1.0.exe", "version": "1.1.0", }, @@ -135,7 +134,6 @@ Object { exports[`perMachine, no run after finish 3`] = ` Object { - "githubArtifactName": "TestApp-Setup-1.1.0.exe", "path": "TestApp Setup 1.1.0.exe", "version": "1.1.0", } @@ -202,7 +200,6 @@ Object { Object { "file": "latest.yml", "fileContent": Object { - "githubArtifactName": "TestApp-WebSetup-1.1.0.exe", "packages": Object { "x64": Object { "file": "TestApp-1.1.0-x64.nsis.7z", diff --git a/test/out/windows/__snapshots__/winCodeSignTest.js.snap b/test/out/windows/__snapshots__/winCodeSignTest.js.snap new file mode 100644 index 00000000000..e785f8d1842 --- /dev/null +++ b/test/out/windows/__snapshots__/winCodeSignTest.js.snap @@ -0,0 +1,9 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`sign certificateSha1 1`] = `"certificateSha1 supported only on Windows"`; + +exports[`sign electronDist 1`] = `"ENOENT: no such file or directory, scandir '/foo'"`; + +exports[`sign ev 1`] = `"certificateSubjectName supported only on Windows"`; + +exports[`sign forceCodeSigning 1`] = `"App is not signed and \\"forceCodeSigning\\" is set to true, please ensure that code signing configuration is correct, please see https://electron.build/code-signing"`; diff --git a/test/out/windows/__snapshots__/winPackagerTest.js.snap b/test/out/windows/__snapshots__/winPackagerTest.js.snap index a8d5c5c2746..a74dfaf7a8b 100644 --- a/test/out/windows/__snapshots__/winPackagerTest.js.snap +++ b/test/out/windows/__snapshots__/winPackagerTest.js.snap @@ -4,14 +4,6 @@ exports[`icon < 256 1`] = `"Windows icon size must be at least 256x256, please f exports[`icon not an image 1`] = `"Windows icon is not valid ico file, please fix \\"/icon.ico\\""`; -exports[`sign certificateSha1 1`] = `"certificateSha1 supported only on Windows"`; - -exports[`sign electronDist 1`] = `"ENOENT: no such file or directory, scandir '/foo'"`; - -exports[`sign ev 1`] = `"certificateSubjectName supported only on Windows"`; - -exports[`sign forceCodeSigning 1`] = `"App is not signed and \\"forceCodeSigning\\" is set to true, please ensure that code signing configuration is correct, please see https://electron.build/code-signing"`; - exports[`win zip 1`] = ` Object { "win": Array [ diff --git a/test/src/PublishManagerTest.ts b/test/src/PublishManagerTest.ts index 491264764ea..fd92719051e 100644 --- a/test/src/PublishManagerTest.ts +++ b/test/src/PublishManagerTest.ts @@ -4,7 +4,7 @@ import { app, checkDirContents } from "./helpers/packTester" const target = Platform.MAC.createTarget("zip") -test.ifDevOrLinuxCi("generic and github", app({ +test.ifDevOrLinuxCi("generic, github and spaces", app({ targets: target, config: { publish: [ @@ -16,6 +16,11 @@ test.ifDevOrLinuxCi("generic and github", app({ provider: "github", repo: "foo/foo" }, + { + provider: "spaces", + name: "mySpaceName", + region: "nyc3" + }, ] }, })) diff --git a/test/src/helpers/CheckingPackager.ts b/test/src/helpers/CheckingPackager.ts index 69b96340540..81c15f8a21d 100644 --- a/test/src/helpers/CheckingPackager.ts +++ b/test/src/helpers/CheckingPackager.ts @@ -4,13 +4,11 @@ import SquirrelWindowsTarget from "electron-builder-squirrel-windows" import { Identity } from "electron-builder/out/codeSign" import MacPackager from "electron-builder/out/macPackager" import { DmgTarget } from "electron-builder/out/targets/dmg" -import { SignOptions } from "electron-builder/out/windowsCodeSign" import { WinPackager } from "electron-builder/out/winPackager" import { SignOptions as MacSignOptions } from "electron-osx-sign" export class CheckingWinPackager extends WinPackager { effectiveDistOptions: any - signOptions: SignOptions | null constructor(info: Packager) { super(info) @@ -29,11 +27,6 @@ export class CheckingWinPackager extends WinPackager { packageInDistributableFormat(appOutDir: string, arch: Arch, targets: Array, taskManager: AsyncTaskManager): void { // skip } - - //noinspection JSUnusedGlobalSymbols - protected async doSign(opts: SignOptions): Promise { - this.signOptions = opts - } } export class CheckingMacPackager extends MacPackager { diff --git a/test/src/helpers/customWindowsSign.ts b/test/src/helpers/customWindowsSign.ts new file mode 100644 index 00000000000..a63553a1b95 --- /dev/null +++ b/test/src/helpers/customWindowsSign.ts @@ -0,0 +1,8 @@ +// test custom windows sign using path to file + +import { CustomWindowsSignTaskConfiguration } from "electron-builder" + +export default async function(configuration: CustomWindowsSignTaskConfiguration) { + expect(configuration.cert).toEqual("secretFile") + expect(configuration.password).toEqual("pass") +} \ No newline at end of file diff --git a/test/src/helpers/runTests.ts b/test/src/helpers/runTests.ts index fb2d0a1ae01..aa1888ad5f2 100644 --- a/test/src/helpers/runTests.ts +++ b/test/src/helpers/runTests.ts @@ -48,10 +48,12 @@ async function runTests() { args.push("debTest") args.push("fpmTest") args.push("winPackagerTest") + args.push("winCodeSignTest") args.push("squirrelWindowsTest") + args.push("nsisUpdaterTest") } else if (circleNodeIndex === 1) { - args.push("BuildTest", "extraMetadataTest", "globTest", "filesTest", "ignoreTest", "nsisUpdaterTest") + args.push("BuildTest", "extraMetadataTest") args.push("mac.+") args.push("oneClickInstallerTest") } @@ -62,8 +64,12 @@ async function runTests() { args.push("PublishManagerTest", "ArtifactPublisherTest", "httpRequestTest", "RepoSlugTest") } else { - args.push("installerTest", "portableTest") + args.push("assistedInstallerTest") + args.push("portableTest") args.push("linuxArchiveTest") + args.push("filesTest") + args.push("globTest") + args.push("ignoreTest") } console.log(`Test files for node ${circleNodeIndex}: ${args.join(", ")}`) } diff --git a/test/src/windows/installerTest.ts b/test/src/windows/assistedInstallerTest.ts similarity index 100% rename from test/src/windows/installerTest.ts rename to test/src/windows/assistedInstallerTest.ts diff --git a/test/src/windows/squirrelWindowsTest.ts b/test/src/windows/squirrelWindowsTest.ts index 97d51eeeca2..16fb923bad2 100644 --- a/test/src/windows/squirrelWindowsTest.ts +++ b/test/src/windows/squirrelWindowsTest.ts @@ -1,8 +1,7 @@ -import BluebirdPromise from "bluebird-lst" import { Arch, Platform } from "electron-builder" import * as path from "path" import { CheckingWinPackager } from "../helpers/CheckingPackager" -import { app, assertPack, copyTestAsset, modifyPackageJson } from "../helpers/packTester" +import { app, assertPack, copyTestAsset } from "../helpers/packTester" test.ifAll.ifNotCiMac("Squirrel.Windows", app({targets: Platform.WINDOWS.createTarget(["squirrel", "zip"])}, {signedWin: true})) @@ -27,34 +26,20 @@ test.skip("delta and msi", app({ }, })) -test.ifAll("detect install-spinner, certificateFile/password", () => { +test.ifAll("detect install-spinner", () => { let platformPackager: CheckingWinPackager | null = null let loadingGifPath: string | null = null return assertPack("test-app-one", { targets: Platform.WINDOWS.createTarget("squirrel"), platformPackagerFactory: (packager, platform) => platformPackager = new CheckingWinPackager(packager), - config: { - win: { - certificatePassword: "pass", - } - } }, { projectDirCreated: it => { loadingGifPath = path.join(it, "build", "install-spinner.gif") - return BluebirdPromise.all([ - copyTestAsset("install-spinner.gif", loadingGifPath), - modifyPackageJson(it, data => { - data.build.win = { - certificateFile: "secretFile", - certificatePassword: "mustBeOverridden", - } - })]) + return copyTestAsset("install-spinner.gif", loadingGifPath) }, packed: async () => { expect(platformPackager!!.effectiveDistOptions.loadingGif).toEqual(loadingGifPath) - expect(platformPackager!!.signOptions!!.cert).toEqual("secretFile") - expect(platformPackager!!.signOptions!!.password).toEqual("pass") }, }) }) \ No newline at end of file diff --git a/test/src/windows/winCodeSignTest.ts b/test/src/windows/winCodeSignTest.ts new file mode 100644 index 00000000000..3ba6c960530 --- /dev/null +++ b/test/src/windows/winCodeSignTest.ts @@ -0,0 +1,81 @@ +import { DIR_TARGET, Platform } from "electron-builder" +import * as path from "path" +import { CheckingWinPackager } from "../helpers/CheckingPackager" +import { app, appThrows } from "../helpers/packTester" + +describe.ifAll("sign", () => { + const windowsDirTarget = Platform.WINDOWS.createTarget(["dir"]) + + test.ifNotWindows("ev", appThrows({ + targets: windowsDirTarget, + config: { + win: { + certificateSubjectName: "ev", + } + } + })) + + function testCustomSign(sign: any) { + return app({ + targets: Platform.WINDOWS.createTarget(DIR_TARGET), + platformPackagerFactory: (packager, platform) => new CheckingWinPackager(packager), + config: { + win: { + certificatePassword: "pass", + certificateFile: "secretFile", + sign, + signingHashAlgorithms: ["sha256"], + // to be sure that sign code will be executed + forceCodeSigning: true, + } + }, + }) + } + + test.ifNotCiMac("certificateFile/password - sign as function", testCustomSign(require("../helpers/customWindowsSign").default)) + test.ifNotCiMac("certificateFile/password - sign as path", testCustomSign(path.join(__dirname, "../helpers/customWindowsSign"))) + + test.ifNotCiMac("custom sign if no code sign info", () => { + let called = false + return app({ + targets: Platform.WINDOWS.createTarget(DIR_TARGET), + platformPackagerFactory: (packager, platform) => new CheckingWinPackager(packager), + config: { + win: { + // to be sure that sign code will be executed + forceCodeSigning: true, + sign: async () => { + called = true + }, + }, + }, + }, { + packed: async () => { + expect(called).toBe(true) + } + })() + }) + + test.ifNotWindows("certificateSha1", appThrows({ + targets: windowsDirTarget, + config: { + win: { + certificateSha1: "boo", + } + } + })) + + test.ifNotCiMac("forceCodeSigning", appThrows({ + targets: windowsDirTarget, + config: { + forceCodeSigning: true, + } + })) + + test.ifNotCiMac("electronDist", appThrows({ + targets: Platform.WINDOWS.createTarget(DIR_TARGET), + config: { + electronDist: "foo", + } + })) +}) \ No newline at end of file diff --git a/test/src/windows/winPackagerTest.ts b/test/src/windows/winPackagerTest.ts index bc8629c9692..60c6b28ec34 100644 --- a/test/src/windows/winPackagerTest.ts +++ b/test/src/windows/winPackagerTest.ts @@ -1,4 +1,4 @@ -import { DIR_TARGET, Platform } from "electron-builder" +import { Platform } from "electron-builder" import { rename, unlink, writeFile } from "fs-extra-p" import * as path from "path" import { CheckingWinPackager } from "../helpers/CheckingPackager" @@ -37,49 +37,13 @@ test.ifMac("custom icon", () => { platformPackagerFactory: (packager, platform) => platformPackager = new CheckingWinPackager(packager), config: { win: { - icon: "customIcon" + icon: "customIcon", }, - } + }, }, { projectDirCreated: projectDir => rename(path.join(projectDir, "build", "icon.ico"), path.join(projectDir, "customIcon.ico")), packed: async context => { expect(await platformPackager!!.getIconPath()).toEqual(path.join(context.projectDir, "customIcon.ico")) }, }) -}) - -describe.ifAll("sign", () => { - const windowsDirTarget = Platform.WINDOWS.createTarget(["dir"]) - - test.ifNotWindows("ev", appThrows({ - targets: windowsDirTarget, - config: { - win: { - certificateSubjectName: "ev", - } - } - })) - - test.ifNotWindows("certificateSha1", appThrows({ - targets: windowsDirTarget, - config: { - win: { - certificateSha1: "boo", - } - } - })) - - test.ifNotCiMac("forceCodeSigning", appThrows({ - targets: windowsDirTarget, - config: { - forceCodeSigning: true, - } - })) - - test.ifNotCiMac("electronDist", appThrows({ - targets: Platform.WINDOWS.createTarget(DIR_TARGET), - config: { - electronDist: "foo", - } - })) }) \ No newline at end of file