unfix: add some bugs

呃这个可能有点炸了
This commit is contained in:
2025-10-16 17:39:50 +08:00
Unverified
parent 392a6a604c
commit 730a701deb
2049 changed files with 188059 additions and 122 deletions

View File

@@ -0,0 +1,143 @@
'use strict'
const { info, hostInfo, warning } = require('./common')
const fs = require('fs-extra')
const { initializeProxy } = require('@electron/get')
const packager = require('..')
const path = require('path')
const yargs = require('yargs-parser')
/* istanbul ignore next */
async function printUsageAndExit (isError) {
const usage = (await fs.readFile(path.resolve(__dirname, '..', 'usage.txt'))).toString()
const print = isError ? console.error : console.log
print(usage)
process.exit(isError ? 1 : 0)
}
module.exports = {
parseArgs: function parseArgs (argv) {
const args = yargs(argv, {
boolean: [
'all',
'deref-symlinks',
'download.rejectUnauthorized',
'junk',
'overwrite',
'prune',
'quiet'
],
default: {
'deref-symlinks': true,
'download.rejectUnauthorized': true,
junk: true,
prune: true
},
string: [
'electron-version',
'out'
]
})
args.dir = args._[0]
args.name = args._[1]
const protocolSchemes = [].concat(args.protocol || [])
const protocolNames = [].concat(args.protocolName || [])
if (protocolSchemes && protocolNames && protocolNames.length === protocolSchemes.length) {
args.protocols = protocolSchemes.map(function (scheme, i) {
return { schemes: [scheme], name: protocolNames[i] }
})
}
if (args.out === '') {
warning('Specifying --out= without a value is the same as the default value', args.quiet)
args.out = null
}
// Overrides for multi-typed arguments, because minimist doesn't support it
// asar: `Object` or `true`
if (args.asar === 'true' || args.asar instanceof Array) {
warning('--asar does not take any arguments, it only has sub-properties (see --help)', args.quiet)
args.asar = true
}
// osx-sign: `Object` or `true`
if (args.osxSign === 'true') {
warning('--osx-sign does not take any arguments, it only has sub-properties (see --help)', args.quiet)
args.osxSign = true
} else if (typeof args['osx-sign'] === 'object') {
if (Array.isArray(args['osx-sign'])) {
warning('Remove --osx-sign (the bare flag) from the command line, only specify sub-properties (see --help)', args.quiet)
} else {
// Keep kebab case of sub properties
args.osxSign = args['osx-sign']
}
}
if (args.osxNotarize) {
let notarize = true
if (typeof args.osxNotarize !== 'object' || Array.isArray(args.osxNotarize)) {
warning('--osx-notarize does not take any arguments, it only has sub-properties (see --help)', args.quiet)
notarize = false
} else if (!args.osxSign) {
warning('Notarization was enabled but macOS code signing was not, code signing is a requirement for notarization, notarize will not run', args.quiet)
notarize = false
}
if (!notarize) {
args.osxNotarize = null
}
}
// tmpdir: `String` or `false`
if (args.tmpdir === 'false') {
warning('--tmpdir=false is deprecated, use --no-tmpdir instead', args.quiet)
args.tmpdir = false
}
return args
},
run: /* istanbul ignore next */ async function run (argv) {
const args = module.exports.parseArgs(argv)
// temporary fix for https://github.com/nodejs/node/issues/6456
for (const stdioWriter of [process.stdout, process.stderr]) {
if (stdioWriter._handle && stdioWriter._handle.setBlocking) {
stdioWriter._handle.setBlocking(true)
}
}
if (args.help) {
await printUsageAndExit(false)
} else if (args.version) {
if (typeof args.version !== 'boolean') {
console.error('--version does not take an argument. Perhaps you meant --app-version or --electron-version?\n')
}
console.log(hostInfo())
process.exit(0)
} else if (!args.dir) {
await printUsageAndExit(true)
}
initializeProxy()
try {
const appPaths = await packager(args)
if (appPaths.length > 1) {
info(`Wrote new apps to:\n${appPaths.join('\n')}`, args.quiet)
} else if (appPaths.length === 1) {
info(`Wrote new app to: ${appPaths[0]}`, args.quiet)
}
} catch (err) {
if (err.message) {
console.error(err.message)
} else {
console.error(err, err.stack)
}
process.exit(1)
}
}
}

View File

@@ -0,0 +1,128 @@
'use strict'
const debug = require('debug')('electron-packager')
const filenamify = require('filenamify')
const fs = require('fs-extra')
const metadata = require('../package.json')
const os = require('os')
const path = require('path')
function sanitizeAppName (name) {
return filenamify(name, { replacement: '-' })
}
function generateFinalBasename (opts) {
return `${sanitizeAppName(opts.name)}-${opts.platform}-${opts.arch}`
}
function generateFinalPath (opts) {
return path.join(opts.out || process.cwd(), generateFinalBasename(opts))
}
function info (message, quiet) {
if (!quiet) {
console.error(message)
}
}
function warning (message, quiet) {
if (!quiet) {
console.warn(`WARNING: ${message}`)
}
}
function subOptionWarning (properties, optionName, parameter, value, quiet) {
if (Object.prototype.hasOwnProperty.call(properties, parameter)) {
warning(`${optionName}.${parameter} will be inferred from the main options`, quiet)
}
properties[parameter] = value
}
function createAsarOpts (opts) {
let asarOptions
if (opts.asar === true) {
asarOptions = {}
} else if (typeof opts.asar === 'object') {
asarOptions = opts.asar
} else if (opts.asar === false || opts.asar === undefined) {
return false
} else {
warning(`asar parameter set to an invalid value (${opts.asar}), ignoring and disabling asar`, opts.quiet)
return false
}
return asarOptions
}
module.exports = {
ensureArray: function ensureArray (value) {
return Array.isArray(value) ? value : [value]
},
isPlatformMac: function isPlatformMac (platform) {
return platform === 'darwin' || platform === 'mas'
},
createAsarOpts: createAsarOpts,
deprecatedParameter: function deprecatedParameter (properties, oldName, newName, newCLIName, quiet) {
if (Object.prototype.hasOwnProperty.call(properties, oldName)) {
warning(`The ${oldName} parameter is deprecated, use ${newName} (or --${newCLIName} in the CLI) instead`, quiet)
if (!Object.prototype.hasOwnProperty.call(properties, newName)) {
properties[newName] = properties[oldName]
}
delete properties[oldName]
}
},
subOptionWarning: subOptionWarning,
baseTempDir: function baseTempDir (opts) {
return path.join(opts.tmpdir || os.tmpdir(), 'electron-packager')
},
generateFinalBasename: generateFinalBasename,
generateFinalPath: generateFinalPath,
sanitizeAppName,
/**
* Convert slashes to UNIX-format separators.
*/
normalizePath: function normalizePath (pathToNormalize) {
return pathToNormalize.replace(/\\/g, '/')
},
/**
* Validates that the application directory contains a package.json file, and that there exists an
* appropriate main entry point file, per the rules of the "main" field in package.json.
*
* See: https://docs.npmjs.com/cli/v6/configuring-npm/package-json#main
*
* @param appDir - the directory specified by the user
* @param bundledAppDir - the directory where the appDir is copied to in the bundled Electron app
*/
validateElectronApp: async function validateElectronApp (appDir, bundledAppDir) {
debug('Validating bundled Electron app')
debug('Checking for a package.json file')
const bundledPackageJSONPath = path.join(bundledAppDir, 'package.json')
if (!(await fs.pathExists(bundledPackageJSONPath))) {
const originalPackageJSONPath = path.join(appDir, 'package.json')
throw new Error(`Application manifest was not found. Make sure "${originalPackageJSONPath}" exists and does not get ignored by your ignore option`)
}
debug('Checking for the main entry point file')
const packageJSON = await fs.readJson(bundledPackageJSONPath)
const mainScriptBasename = packageJSON.main || 'index.js'
const mainScript = path.resolve(bundledAppDir, mainScriptBasename)
if (!(await fs.pathExists(mainScript))) {
const originalMainScript = path.join(appDir, mainScriptBasename)
throw new Error(`The main entry point to your app was not found. Make sure "${originalMainScript}" exists and does not get ignored by your ignore option`)
}
debug('Validation complete')
},
hostInfo: function hostInfo () {
return `Electron Packager ${metadata.version}\n` +
`Node ${process.version}\n` +
`Host Operating system: ${process.platform} ${os.release()} (${process.arch})`
},
info: info,
warning: warning
}

View File

@@ -0,0 +1,110 @@
'use strict'
const common = require('./common')
const debug = require('debug')('electron-packager')
const junk = require('junk')
const path = require('path')
const prune = require('./prune')
const targets = require('./targets')
const DEFAULT_IGNORES = [
'/package-lock\\.json$',
'/yarn\\.lock$',
'/\\.git($|/)',
'/node_modules/\\.bin($|/)',
'\\.o(bj)?$',
'/node_gyp_bins($|/)'
]
function populateIgnoredPaths (opts) {
opts.originalIgnore = opts.ignore
if (typeof (opts.ignore) !== 'function') {
if (opts.ignore) {
opts.ignore = common.ensureArray(opts.ignore).concat(DEFAULT_IGNORES)
} else {
opts.ignore = [].concat(DEFAULT_IGNORES)
}
if (process.platform === 'linux') {
opts.ignore.push(common.baseTempDir(opts))
}
debug('Ignored path regular expressions:', opts.ignore)
}
}
function generateIgnoredOutDirs (opts) {
const normalizedOut = opts.out ? path.resolve(opts.out) : null
const ignoredOutDirs = []
if (normalizedOut === null || normalizedOut === process.cwd()) {
for (const [platform, archs] of Object.entries(targets.officialPlatformArchCombos)) {
for (const arch of archs) {
const basenameOpts = {
arch: arch,
name: opts.name,
platform: platform
}
ignoredOutDirs.push(path.join(process.cwd(), common.generateFinalBasename(basenameOpts)))
}
}
} else {
ignoredOutDirs.push(normalizedOut)
}
debug('Ignored paths based on the out param:', ignoredOutDirs)
return ignoredOutDirs
}
function generateFilterFunction (ignore) {
if (typeof (ignore) === 'function') {
return file => !ignore(file)
} else {
const ignoredRegexes = common.ensureArray(ignore)
return function filterByRegexes (file) {
return !ignoredRegexes.some(regex => file.match(regex))
}
}
}
function userPathFilter (opts) {
const filterFunc = generateFilterFunction(opts.ignore || [])
const ignoredOutDirs = generateIgnoredOutDirs(opts)
const pruner = opts.prune ? new prune.Pruner(opts.dir, opts.quiet) : null
return async function filter (file) {
const fullPath = path.resolve(file)
if (ignoredOutDirs.includes(fullPath)) {
return false
}
if (opts.junk !== false) { // defaults to true
if (junk.is(path.basename(fullPath))) {
return false
}
}
let name = fullPath.split(path.resolve(opts.dir))[1]
if (path.sep === '\\') {
name = common.normalizePath(name)
}
if (pruner && name.startsWith('/node_modules/')) {
if (await prune.isModule(file)) {
return pruner.pruneModule(name)
} else {
return filterFunc(name)
}
}
return filterFunc(name)
}
}
module.exports = {
populateIgnoredPaths,
generateIgnoredOutDirs,
userPathFilter
}

View File

@@ -0,0 +1,37 @@
'use strict'
const common = require('./common')
const debug = require('debug')('electron-packager')
const { downloadArtifact } = require('@electron/get')
const semver = require('semver')
const targets = require('./targets')
function createDownloadOpts (opts, platform, arch) {
const downloadOpts = { ...opts.download }
common.subOptionWarning(downloadOpts, 'download', 'platform', platform, opts.quiet)
common.subOptionWarning(downloadOpts, 'download', 'arch', arch, opts.quiet)
common.subOptionWarning(downloadOpts, 'download', 'version', opts.electronVersion, opts.quiet)
common.subOptionWarning(downloadOpts, 'download', 'artifactName', 'electron', opts.quiet)
return downloadOpts
}
module.exports = {
createDownloadCombos: function createDownloadCombos (opts, selectedPlatforms, selectedArchs, ignoreFunc) {
return targets.createPlatformArchPairs(opts, selectedPlatforms, selectedArchs, ignoreFunc).map(([platform, arch]) => {
return createDownloadOpts(opts, platform, arch)
})
},
createDownloadOpts: createDownloadOpts,
downloadElectronZip: async function downloadElectronZip (downloadOpts) {
// armv7l builds have only been backfilled for Electron >= 1.0.0.
// See: https://github.com/electron/electron/pull/6986
/* istanbul ignore if */
if (downloadOpts.arch === 'armv7l' && semver.lt(downloadOpts.version, '1.0.0')) {
downloadOpts.arch = 'arm'
}
debug(`Downloading Electron with options ${JSON.stringify(downloadOpts)}`)
return downloadArtifact(downloadOpts)
}
}

View File

@@ -0,0 +1,24 @@
'use strict'
const { promisify } = require('util')
module.exports = {
promisifyHooks: async function promisifyHooks (hooks, args) {
if (!hooks || !Array.isArray(hooks)) {
return Promise.resolve()
}
await Promise.all(hooks.map(hookFn => promisify(hookFn).apply(this, args)))
},
serialHooks: function serialHooks (hooks) {
return async function () {
const args = Array.prototype.splice.call(arguments, 0, arguments.length - 1)
const done = arguments[arguments.length - 1]
for (const hook of hooks) {
await hook.apply(this, args)
}
return done() // eslint-disable-line promise/no-callback-in-promise
}
}
}

View File

@@ -0,0 +1,607 @@
// Originally based on the type definitions for electron-packager 14.0
// Project: https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/electron-packager
// Original Authors:
// * Maxime LUCE <https://github.com/SomaticIT>
// * Juan Jimenez-Anca <https://github.com/cortopy>
// * John Kleinschmidt <https://github.com/jkleinsc>
// * Brendan Forster <https://github.com/shiftkey>
// * Mark Lee <https://github.com/malept>
// * Florian Keller <https://github.com/ffflorian>
import { CreateOptions as AsarOptions } from '@electron/asar';
import { ElectronDownloadRequestOptions as ElectronDownloadOptions } from '@electron/get';
import {
LegacyNotarizeCredentials,
NotaryToolCredentials,
TransporterOptions
} from '@electron/notarize/lib/types';
import { SignOptions } from '@electron/osx-sign/dist/esm/types';
import type { makeUniversalApp } from '@electron/universal';
type MakeUniversalOpts = Parameters<typeof makeUniversalApp>[0]
type NotarizeLegacyOptions = LegacyNotarizeCredentials & TransporterOptions;
/**
* Bundles Electron-based application source code with a renamed/customized Electron executable and
* its supporting files into folders ready for distribution.
*
* Briefly, this function:
* - finds or downloads the correct release of Electron
* - uses that version of Electron to create a app in `<out>/<appname>-<platform>-<arch>`
*
* Short example:
*
* ```javascript
* const packager = require('electron-packager')
*
* async function bundleElectronApp(options) {
* const appPaths = await packager(options)
* console.log(`Electron app bundles created:\n${appPaths.join("\n")}`)
* }
* ```
*
* @param opts - Options to configure packaging.
*
* @returns A Promise containing the paths to the newly created application bundles.
*/
declare function electronPackager(opts: electronPackager.Options): Promise<string[]>;
declare namespace electronPackager {
/**
* Architectures that have been supported by the official Electron prebuilt binaries, past
* and present.
*/
type OfficialArch = 'ia32' | 'x64' | 'armv7l' | 'arm64' | 'mips64el' | 'universal';
/**
* Platforms that have been supported by the official Electron prebuilt binaries, past and present.
*/
type OfficialPlatform = 'linux' | 'win32' | 'darwin' | 'mas';
type TargetArch = OfficialArch | string;
type TargetPlatform = OfficialPlatform | string;
type ArchOption = TargetArch | 'all';
type PlatformOption = TargetPlatform | 'all';
/**
* A predicate function that, given an absolute file `path`, returns `true` if the file should be
* ignored, or `false` if the file should be kept. *This does not use any of the default ignored
* files/directories listed for the {@link ignore} option.*
*/
type IgnoreFunction = (path: string) => boolean;
/**
* A function that is called on the completion of a packaging stage.
*
* By default, the functions are called in parallel (via
* [`Promise.all`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/all)).
* If you need the functions called serially, there is a utility function provided. Please note that
* **callback-style functions are not supported by `serialHooks`.** For example:
*
* ```javascript
* const packager = require('electron-packager')
* const { serialHooks } = require('electron-packager/src/hooks')
*
* packager({
* // ...
* afterCopy: [serialHooks([
* (buildPath, electronVersion, platform, arch) => {
* return new Promise((resolve, reject) => {
* setTimeout(() => {
* console.log('first function')
* resolve()
* }, 1000)
* })
* },
* (buildPath, electronVersion, platform, arch) => {
* console.log('second function')
* }
* ])],
* // ...
* })
* ```
*
* For real-world examples of `HookFunction`s, see the [list of related
* plugins](https://github.com/electron/electron-packager#plugins).
*/
type HookFunction =
/**
* @param buildPath - For {@link afterExtract}, the path to the temporary folder where the prebuilt
* Electron binary has been extracted to. For {@link afterCopy} and {@link afterPrune}, the path to the
* folder where the Electron app has been copied to. For {@link afterComplete}, the final directory
* of the packaged application.
* @param electronVersion - the version of Electron that is being bundled with the application.
* @param platform - The target platform you are packaging for.
* @param arch - The target architecture you are packaging for.
* @param callback - Must be called once you have completed your actions.
*/
(
buildPath: string,
electronVersion: string,
platform: TargetArch,
arch: TargetArch,
callback: (err?: Error | null) => void
) => void;
type TargetDefinition = {
arch: TargetArch;
platform: TargetPlatform;
}
type FinalizePackageTargetsHookFunction = (targets: TargetDefinition[], callback: (err?: Error | null) => void) => void;
/** See the documentation for [`@electron/osx-sign`](https://npm.im/@electron/osx-sign#opts) for details. */
type OsxSignOptions = Omit<SignOptions, 'app' | 'binaries' | 'platform' | 'version'>;
/**
* See the documentation for [`@electron/notarize`](https://npm.im/@electron/notarize#method-notarizeopts-promisevoid)
* for details.
*/
type OsxNotarizeOptions =
| ({ tool?: 'legacy' } & NotarizeLegacyOptions)
| ({ tool: 'notarytool' } & NotaryToolCredentials);
/**
* See the documentation for [`@electron/universal`](https://github.com/electron/universal)
* for details.
*/
type OsxUniversalOptions = Omit<MakeUniversalOpts, 'x64AppPath' | 'arm64AppPath' | 'outAppPath' | 'force'>
/**
* Defines URL protocol schemes to be used on macOS.
*/
interface MacOSProtocol {
/**
* The descriptive name. Maps to the `CFBundleURLName` metadata property.
*/
name: string;
/**
* One or more protocol schemes associated with the app. For example, specifying `myapp`
* would cause URLs such as `myapp://path` to be opened with the app. Maps to the
* `CFBundleURLSchemes` metadata property.
*/
schemes: string[];
}
/**
* A collection of application metadata to embed into the Windows executable.
*
* For more information, read the [`rcedit` Node module documentation](https://github.com/electron/node-rcedit#docs).
*/
interface Win32MetadataOptions {
/** Defaults to the `author` name from the nearest `package.json`. */
CompanyName?: string;
/** Defaults to either `productName` or `name` from the nearest `package.json`. */
FileDescription?: string;
/** Defaults to the renamed Electron `.exe` file. */
OriginalFilename?: string;
/** Defaults to either `productName` or `name` from the nearest `package.json`. */
ProductName?: string;
/** Defaults to either `productName` or `name` from the nearest `package.json`. */
InternalName?: string;
/** See [MSDN](https://msdn.microsoft.com/en-us/library/6ad1fshk.aspx#Anchor_9) for details. */
'requested-execution-level'?: 'asInvoker' | 'highestAvailable' | 'requireAdministrator';
/**
* Path to a local manifest file.
*
* See [MSDN](https://msdn.microsoft.com/en-us/library/windows/desktop/aa374191.aspx) for more details.
*/
'application-manifest'?: string;
}
/** Options passed to the `packager()` function. */
interface Options {
/** The source directory. */
dir: string;
/**
* Functions to be called after your app directory has been packaged into an .asar file.
*
* **Note**: `afterAsar` will only be called if the {@link asar} option is set.
*/
afterAsar?: HookFunction[];
/** Functions to be called after the packaged application has been moved to the final directory. */
afterComplete?: HookFunction[];
/**
* Functions to be called after your app directory has been copied to a temporary directory.
*
* **Note**: `afterCopy` will not be called if the {@link prebuiltAsar} option is set.
*/
afterCopy?: HookFunction[];
/**
* Functions to be called after the files specified in the {@link extraResource} option have been copied.
**/
afterCopyExtraResources?: HookFunction[];
/** Functions to be called after the prebuilt Electron binary has been extracted to a temporary directory. */
afterExtract?: HookFunction[];
/**
* Functions to be called after the final matrix of platform/arch combination is determined. Use this to
* learn what archs/platforms packager is targetting when you pass "all" as a value.
*/
afterFinalizePackageTargets?: FinalizePackageTargetsHookFunction[];
/**
* Functions to be called after Node module pruning has been applied to the application.
*
* **Note**: None of these functions will be called if the {@link prune} option is `false` or
* the {@link prebuiltAsar} option is set.
*/
afterPrune?: HookFunction[];
/** When `true`, sets both {@link arch} and {@link platform} to `all`. */
all?: boolean;
/*
* The bundle identifier to use in the application's `Info.plist`.
*
* @category macOS
*/
appBundleId?: string;
/**
* 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).
*
* @category macOS
*/
appCategoryType?: string;
/**
* The human-readable copyright line for the app. Maps to the `LegalCopyright` metadata
* property on Windows, and `NSHumanReadableCopyright` on macOS.
*/
appCopyright?: string;
/**
* The release version of the application.
*
* By default the `version` property in the `package.json` is used, but it can be overridden
* with this argument. If neither are provided, the version of Electron will be used. Maps
* to the `ProductVersion` metadata property on Windows, and `CFBundleShortVersionString`
* on macOS.
*/
appVersion?: string;
/**
* The target system architecture(s) to build for.
*
* Not required if the {@link all} option is set. If `arch` is set to `all`, all supported
* architectures for the target platforms specified by {@link platform} will be built.
* Arbitrary combinations of individual architectures are also supported via a comma-delimited
* string or array of strings. The non-`all` values correspond to the architecture names used
* by [Electron releases](https://github.com/electron/electron/releases). This value
* is not restricted to the official set if [[download|`download.mirrorOptions`]] is set.
*
* Defaults to the arch of the host computer running Electron Packager.
*
* Arch values for the official prebuilt Electron binaries:
* - `ia32`
* - `x64`
* - `armv7l`
* - `arm64` _(Linux: Electron 1.8.0 and above; Windows: 6.0.8 and above; macOS: 11.0.0-beta.1 and above)_
* - `mips64el` _(Electron 1.8.2-beta.5 to 1.8.8)_
*/
arch?: ArchOption | ArchOption[];
/**
* Whether to package the application's source code into an archive, using [Electron's
* archive format](https://github.com/electron/asar). Reasons why you may want to enable
* this feature include mitigating issues around long path names on Windows, slightly speeding
* up `require`, and concealing your source code from cursory inspection. When the value
* is `true`, it passes the default configuration to the `asar` module. The configuration
* values can be customized when the value is an `Object`. Supported sub-options include, but
* are not limited to:
* - `ordering` (*string*): A path to an ordering file for packing files. An explanation can be
* found on the [Atom issue tracker](https://github.com/atom/atom/issues/10163).
* - `unpack` (*string*): A [glob expression](https://github.com/isaacs/minimatch#features),
* when specified, unpacks the file with matching names to the `app.asar.unpacked` directory.
* - `unpackDir` (*string*): Unpacks the dir to the `app.asar.unpacked` directory whose names
* exactly or pattern match this string. The `asar.unpackDir` is relative to {@link dir}.
*
* Defaults to `false`.
*
* Some examples:
*
* - `asar.unpackDir = 'sub_dir'` will unpack the directory `/<dir>/sub_dir`
* - `asar.unpackDir = path.join('**', '{sub_dir1/sub_sub_dir,sub_dir2}', '*')` will unpack the directories `/<dir>/sub_dir1/sub_sub_dir` and `/<dir>/sub_dir2`, but it will not include their subdirectories.
* - `asar.unpackDir = path.join('**', '{sub_dir1/sub_sub_dir,sub_dir2}', '**')` will unpack the subdirectories of the directories `/<dir>/sub_dir1/sub_sub_dir` and `/<dir>/sub_dir2`.
* - `asar.unpackDir = path.join('**', '{sub_dir1/sub_sub_dir,sub_dir2}', '**', '*')` will unpack the directories `/<dir>/sub_dir1/sub_sub_dir` and `/<dir>/sub_dir2` and their subdirectories.
*
* **Note:** `asar` will have no effect if the {@link prebuiltAsar} option is set.
*/
asar?: boolean | AsarOptions;
/**
* Functions to be called before your app directory is packaged into an .asar file.
*
* **Note**: `beforeAsar` will only be called if the {@link asar} option is set.
*/
beforeAsar?: HookFunction[];
/**
* Functions to be called before your app directory is copied to a temporary directory.
*
* **Note**: `beforeCopy` will not be called if the {@link prebuiltAsar} option is set.
*/
beforeCopy?: HookFunction[];
/**
* Functions to be called before the files specified in the {@link extraResource} option are copied.
**/
beforeCopyExtraResources?: HookFunction[];
/**
* The build version of the application. Defaults to the value of the {@link appVersion} option.
* Maps to the `FileVersion` metadata property on Windows, and `CFBundleVersion` on macOS.
*/
buildVersion?: string;
/**
* Forces support for Mojave (macOS 10.14) dark mode in your packaged app. This sets the
* `NSRequiresAquaSystemAppearance` key to `false` in your app's `Info.plist`. For more information,
* see the [Electron documentation](https://www.electronjs.org/docs/tutorial/mojave-dark-mode-guide)
* and the [Apple developer documentation](https://developer.apple.com/documentation/appkit/nsappearancecustomization/choosing_a_specific_appearance_for_your_app).
*
* @category macOS
*/
darwinDarkModeSupport?: boolean;
/**
* Whether symlinks should be dereferenced during the copying of the application source.
* Defaults to `true`.
*
* **Note:** `derefSymlinks` will have no effect if the {@link prebuiltAsar} option is set.
*/
derefSymlinks?: boolean;
/**
* If present, passes custom options to [`@electron/get`](https://npm.im/@electron/get). See
* the module for option descriptions, proxy support, and defaults. Supported parameters
* include, but are not limited to:
* - `cacheRoot` (*string*): The directory where prebuilt, pre-packaged Electron downloads are cached.
* - `mirrorOptions` (*Object*): Options to override the default Electron download location.
* - `rejectUnauthorized` (*boolean* - default: `true`): Whether SSL certificates are required to be
* valid when downloading Electron.
*
* **Note:** `download` sub-options will have no effect if the {@link electronZipDir} option is set.
*/
download?: ElectronDownloadOptions;
/**
* The Electron version with which the app is built (without the leading 'v') - for example,
* [`1.4.13`](https://github.com/electron/electron/releases/tag/v1.4.13). See [Electron
* releases](https://github.com/electron/electron/releases) for valid versions. If omitted, it
* will use the version of the nearest local installation of `electron`,
* `electron-prebuilt-compile`, or `electron-prebuilt`, defined in `package.json` in either
* `devDependencies` or `dependencies`.
*/
electronVersion?: string;
/**
* The local path to a directory containing Electron ZIP files for Electron Packager to unzip, instead
* of downloading them. The ZIP filenames should be in the same format as the ones downloaded from the
* [Electron releases](https://github.com/electron/electron/releases) site.
*
* **Note:** Setting this option prevents the {@link download} sub-options from being used, as
* the functionality gets skipped over.
*/
electronZipDir?: string;
/**
* The name of the executable file, sans file extension. Defaults to the value for the {@link name}
* option. For `darwin` or `mas` target platforms, this does not affect the name of the
* `.app` folder - this will use the {@link name} option instead.
*/
executableName?: string;
/**
* When the value is a string, specifies the filename of a `plist` file. Its contents are merged
* into the app's `Info.plist`.
* When the value is an `Object`, it specifies an already-parsed `plist` data structure that is
* merged into the app's `Info.plist`.
*
* Entries from `extendInfo` override entries in the base `Info.plist` file supplied by
* `electron`, `electron-prebuilt-compile`, or `electron-prebuilt`, but are overridden by other
* options such as {@link appVersion} or {@link appBundleId}.
*
* @category macOS
*/
extendInfo?: string | { [property: string]: any }; // eslint-disable-line @typescript-eslint/no-explicit-any
/**
* When the value is a string, specifies the filename of a `plist` file. Its contents are merged
* into all the Helper apps' `Info.plist` files.
* When the value is an `Object`, it specifies an already-parsed `plist` data structure that is
* merged into all the Helper apps' `Info.plist` files.
*
* Entries from `extendHelperInfo` override entries in the helper apps' `Info.plist` file supplied by
* `electron`, `electron-prebuilt-compile`, or `electron-prebuilt`, but are overridden by other
* options such as {@link appVersion} or {@link appBundleId}.
*
* @category macOS
*/
extendHelperInfo?: string | { [property: string]: any }; // eslint-disable-line @typescript-eslint/no-explicit-any
/**
* One or more files to be copied directly into the app's `Contents/Resources` directory for
* macOS target platforms, and the `resources` directory for other target platforms. The
* resources directory can be referenced in the packaged app via the
* [`process.resourcesPath`](https://www.electronjs.org/docs/api/process#processresourcespath-readonly) value.
*/
extraResource?: string | string[];
/**
* The bundle identifier to use in the application helper's `Info.plist`.
*
* @category macOS
*/
helperBundleId?: string;
/**
* The local path to the icon file, if the target platform supports setting embedding an icon.
*
* Currently you must look for conversion tools in order to supply an icon in the format required by the platform:
*
* - macOS: `.icns`
* - Windows: `.ico` ([See the readme](https://github.com/electron/electron-packager#building-windows-apps-from-non-windows-platforms) for details on non-Windows platforms)
* - Linux: this option is not supported, as the dock/window list icon is set via
* [the `icon` option in the `BrowserWindow` constructor](https://electronjs.org/docs/api/browser-window/#new-browserwindowoptions).
* *Please note that you need to use a PNG, and not the macOS or Windows icon formats, in order for it
* to show up in the dock/window list.* Setting the icon in the file manager is not currently supported.
*
* If the file extension is omitted, it is auto-completed to the correct extension based on the
* platform, including when [[platform|`platform: 'all'`]] is in effect.
*/
icon?: string;
/**
* One or more additional [regular expression](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions)
* patterns which specify which files to ignore when copying files to create the app bundle(s). The
* regular expressions are matched against the absolute path of a given file/directory to be copied.
*
* **Please note that [glob patterns](https://en.wikipedia.org/wiki/Glob_%28programming%29) will not work.**
*
* The following paths are always ignored (*when you aren't using an {@link IgnoreFunction}*):
*
* - the directory specified by the {@link out} option
* - the temporary directory used to build the Electron app
* - `node_modules/.bin`
* - `node_modules/electron`
* - `node_modules/electron-prebuilt`
* - `node_modules/electron-prebuilt-compile`
* - `.git`
* - files and folders ending in `.o` and `.obj`
*
* **Note**: Node modules specified in `devDependencies` are ignored by default, via the
* {@link prune} option.
*
* **Note:** `ignore` will have no effect if the {@link prebuiltAsar} option is set.
*/
ignore?: RegExp | RegExp[] | IgnoreFunction;
/**
* Ignores [system junk files](https://github.com/sindresorhus/junk) when copying the Electron app,
* regardless of the {@link ignore} option.
*
* **Note:** `junk` will have no effect if the {@link prebuiltAsar} option is set.
*/
junk?: boolean;
/**
* The application name. If omitted, it will use the `productName` or `name` value from the
* nearest `package.json`.
*
* **Regardless of source, characters in the Electron app name which are not allowed in all target
* platforms' filenames (e.g., `/`), will be replaced by hyphens (`-`).**
*/
name?: string;
/**
* If present, notarizes macOS target apps when the host platform is macOS and Xcode is installed.
* See [`@electron/notarize`](https://github.com/electron/notarize#method-notarizeopts-promisevoid)
* for option descriptions, such as how to use `appleIdPassword` safely or obtain an API key.
*
* **Requires the {@link osxSign} option to be set.**
*
* @category macOS
*/
osxNotarize?: OsxNotarizeOptions;
/**
* If present, signs macOS target apps when the host platform is macOS and Xcode is installed.
* When the value is `true`, pass default configuration to the signing module. See
* [@electron/osx-sign](https://npm.im/@electron/osx-sign#opts---options) for sub-option descriptions and
* their defaults. Options include, but are not limited to:
* - `identity` (*string*): The identity used when signing the package via `codesign`.
* - `binaries` (*array<string>*): Path to additional binaries that will be signed along with built-ins of Electron/
*
* @category macOS
*/
osxSign?: true | OsxSignOptions;
/**
* Used to provide custom options to the internal call to `@electron/universal` when building a macOS
* app with the target architecture of "universal". Unused otherwise, providing a value does not imply
* a universal app is built.
*/
osxUniversal?: OsxUniversalOptions;
/**
* The base directory where the finished package(s) are created.
*
* Defaults to the current working directory.
*/
out?: string;
/**
* Whether to replace an already existing output directory for a given platform (`true`) or
* skip recreating it (`false`). Defaults to `false`.
*/
overwrite?: boolean;
/**
* The target platform(s) to build for.
*
* Not required if the {@link all} option is set. If `platform` is set to `all`, all officially
* supported target platforms for the target architectures specified by the {@link arch} option
* will be built. Arbitrary combinations of individual platforms are also supported via a
* comma-delimited string or array of strings.
*
* The official non-`all` values correspond to the platform names used by [Electron
* releases](https://github.com/electron/electron/releases). This value is not restricted to
* the official set if [[download|`download.mirrorOptions]] is set.
*
* Defaults to the platform of the host computer running Electron Packager.
*
* Platform values for the official prebuilt Electron binaries:
* - `darwin` (macOS)
* - `linux`
* - `mas` (macOS, specifically for submitting to the Mac App Store)
* - `win32`
*/
platform?: PlatformOption | PlatformOption[];
/**
* The path to a prebuilt ASAR file.
*
* **Note:** Setting this option prevents the following options from being used, as the functionality
* gets skipped over:
*
* - {@link asar}
* - {@link afterCopy}
* - {@link afterPrune}
* - {@link derefSymlinks}
* - {@link ignore}
* - {@link junk}
* - {@link prune}
*/
prebuiltAsar?: string;
/**
* The URL protocol schemes associated with the Electron app.
*
* @category macOS
*/
protocols?: MacOSProtocol[];
/**
* Walks the `node_modules` dependency tree to remove all of the packages specified in the
* `devDependencies` section of `package.json` from the outputted Electron app.
*
* Defaults to `true`.
*
* **Note:** `prune` will have no effect if the {@link prebuiltAsar} option is set.
*/
prune?: boolean;
/**
* If `true`, disables printing informational and warning messages to the console when
* packaging the application. This does not disable errors.
*
* Defaults to `false`.
*/
quiet?: boolean;
/**
* The base directory to use as a temporary directory. Set to `false` to disable use of a
* temporary directory. Defaults to the system's temporary directory.
*/
tmpdir?: string | false;
/**
* Human-readable descriptions of how the Electron app uses certain macOS features. These are displayed
* in the App Store. A non-exhaustive list of available properties:
*
* * `Camera` - required for media access API usage in macOS Catalina
* * `Microphone` - required for media access API usage in macOS Catalina
*
* Valid properties are the [Cocoa keys for MacOS](https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CocoaKeys.html)
* of the pattern `NS(.*)UsageDescription`, where the captured group is the key to use.
*
* Example:
*
* ```javascript
* {
* usageDescription: {
* Camera: 'Needed for video calls',
* Microphone: 'Needed for voice calls'
* }
* }
* ```
*
* @category macOS
*/
usageDescription?: { [property: string]: string };
/**
* Application metadata to embed into the Windows executable.
* @category Windows
*/
win32metadata?: Win32MetadataOptions;
}
}
export = electronPackager;

View File

@@ -0,0 +1,207 @@
'use strict'
const common = require('./common')
const copyFilter = require('./copy-filter')
const debug = require('debug')('electron-packager')
const download = require('./download')
const fs = require('fs-extra')
const getMetadataFromPackageJSON = require('./infer')
const hooks = require('./hooks')
const path = require('path')
const targets = require('./targets')
const unzip = require('./unzip')
const { packageUniversalMac } = require('./universal')
function debugHostInfo () {
debug(common.hostInfo())
}
class Packager {
constructor (opts) {
this.opts = opts
this.tempBase = common.baseTempDir(opts)
this.useTempDir = opts.tmpdir !== false
this.canCreateSymlinks = undefined
}
async ensureTempDir () {
if (this.useTempDir) {
await fs.remove(this.tempBase)
} else {
return Promise.resolve()
}
}
async testSymlink (comboOpts, zipPath) {
await fs.mkdirp(this.tempBase)
const testPath = await fs.mkdtemp(path.join(this.tempBase, `symlink-test-${comboOpts.platform}-${comboOpts.arch}-`))
const testFile = path.join(testPath, 'test')
const testLink = path.join(testPath, 'testlink')
try {
await fs.outputFile(testFile, '')
await fs.symlink(testFile, testLink)
this.canCreateSymlinks = true
} catch (e) {
/* istanbul ignore next */
this.canCreateSymlinks = false
} finally {
await fs.remove(testPath)
}
if (this.canCreateSymlinks) {
return this.checkOverwrite(comboOpts, zipPath)
}
/* istanbul ignore next */
return this.skipHostPlatformSansSymlinkSupport(comboOpts)
}
/* istanbul ignore next */
skipHostPlatformSansSymlinkSupport (comboOpts) {
common.info(`Cannot create symlinks (on Windows hosts, it requires admin privileges); skipping ${comboOpts.platform} platform`, this.opts.quiet)
return Promise.resolve()
}
async overwriteAndCreateApp (outDir, comboOpts, zipPath) {
debug(`Removing ${outDir} due to setting overwrite: true`)
await fs.remove(outDir)
return this.createApp(comboOpts, zipPath)
}
async extractElectronZip (comboOpts, zipPath, buildDir) {
debug(`Extracting ${zipPath} to ${buildDir}`)
await unzip(zipPath, buildDir)
await hooks.promisifyHooks(this.opts.afterExtract, [buildDir, comboOpts.electronVersion, comboOpts.platform, comboOpts.arch])
}
async buildDir (platform, arch) {
let buildParentDir
if (this.useTempDir) {
buildParentDir = this.tempBase
} else {
buildParentDir = this.opts.out || process.cwd()
}
await fs.mkdirp(buildParentDir)
return await fs.mkdtemp(path.resolve(buildParentDir, `${platform}-${arch}-template-`))
}
async createApp (comboOpts, zipPath) {
const buildDir = await this.buildDir(comboOpts.platform, comboOpts.arch)
common.info(`Packaging app for platform ${comboOpts.platform} ${comboOpts.arch} using electron v${comboOpts.electronVersion}`, this.opts.quiet)
debug(`Creating ${buildDir}`)
await fs.ensureDir(buildDir)
await this.extractElectronZip(comboOpts, zipPath, buildDir)
const os = require(targets.osModules[comboOpts.platform])
const app = new os.App(comboOpts, buildDir)
return app.create()
}
async checkOverwrite (comboOpts, zipPath) {
const finalPath = common.generateFinalPath(comboOpts)
if (await fs.pathExists(finalPath)) {
if (this.opts.overwrite) {
return this.overwriteAndCreateApp(finalPath, comboOpts, zipPath)
} else {
common.info(`Skipping ${comboOpts.platform} ${comboOpts.arch} (output dir already exists, use --overwrite to force)`, this.opts.quiet)
return true
}
} else {
return this.createApp(comboOpts, zipPath)
}
}
async getElectronZipPath (downloadOpts) {
if (this.opts.electronZipDir) {
if (await fs.pathExists(this.opts.electronZipDir)) {
const zipPath = path.resolve(
this.opts.electronZipDir,
`electron-v${downloadOpts.version}-${downloadOpts.platform}-${downloadOpts.arch}.zip`
)
if (!await fs.pathExists(zipPath)) {
throw new Error(`The specified Electron ZIP file does not exist: ${zipPath}`)
}
return zipPath
}
throw new Error(`The specified Electron ZIP directory does not exist: ${this.opts.electronZipDir}`)
} else {
return download.downloadElectronZip(downloadOpts)
}
}
async packageForPlatformAndArchWithOpts (comboOpts, downloadOpts) {
const zipPath = await this.getElectronZipPath(downloadOpts)
if (!this.useTempDir) {
return this.createApp(comboOpts, zipPath)
}
if (common.isPlatformMac(comboOpts.platform)) {
/* istanbul ignore else */
if (this.canCreateSymlinks === undefined) {
return this.testSymlink(comboOpts, zipPath)
} else if (!this.canCreateSymlinks) {
return this.skipHostPlatformSansSymlinkSupport(comboOpts)
}
}
return this.checkOverwrite(comboOpts, zipPath)
}
async packageForPlatformAndArch (downloadOpts) {
// Create delegated options object with specific platform and arch, for output directory naming
const comboOpts = {
...this.opts,
arch: downloadOpts.arch,
platform: downloadOpts.platform,
electronVersion: downloadOpts.version
}
if (common.isPlatformMac(comboOpts.platform) && comboOpts.arch === 'universal') {
return packageUniversalMac(this.packageForPlatformAndArchWithOpts.bind(this), await this.buildDir(comboOpts.platform, comboOpts.arch), comboOpts, downloadOpts, this.tempBase)
}
return this.packageForPlatformAndArchWithOpts(comboOpts, downloadOpts)
}
}
async function packageAllSpecifiedCombos (opts, archs, platforms) {
const packager = new Packager(opts)
await packager.ensureTempDir()
return Promise.all(download.createDownloadCombos(opts, platforms, archs).map(
downloadOpts => packager.packageForPlatformAndArch(downloadOpts)
))
}
module.exports = async function packager (opts) {
debugHostInfo()
if (debug.enabled) debug(`Packager Options: ${JSON.stringify(opts)}`)
const archs = targets.validateListFromOptions(opts, 'arch')
const platforms = targets.validateListFromOptions(opts, 'platform')
if (!Array.isArray(archs)) return Promise.reject(archs)
if (!Array.isArray(platforms)) return Promise.reject(platforms)
debug(`Target Platforms: ${platforms.join(', ')}`)
debug(`Target Architectures: ${archs.join(', ')}`)
const packageJSONDir = path.resolve(process.cwd(), opts.dir) || process.cwd()
await getMetadataFromPackageJSON(platforms, opts, packageJSONDir)
if (opts.name.endsWith(' Helper')) {
throw new Error('Application names cannot end in " Helper" due to limitations on macOS')
}
debug(`Application name: ${opts.name}`)
debug(`Target Electron version: ${opts.electronVersion}`)
copyFilter.populateIgnoredPaths(opts)
await hooks.promisifyHooks(opts.afterFinalizePackageTargets, [targets.createPlatformArchPairs(opts, platforms, archs).map(([platform, arch]) => ({ platform, arch }))])
const appPaths = await packageAllSpecifiedCombos(opts, archs, platforms)
// Remove falsy entries (e.g. skipped platforms)
return appPaths.filter(appPath => appPath)
}

View File

@@ -0,0 +1,178 @@
'use strict'
const debug = require('debug')('electron-packager')
const getPackageInfo = require('get-package-info')
const parseAuthor = require('parse-author')
const path = require('path')
const resolve = require('resolve')
const semver = require('semver')
function isMissingRequiredProperty (props) {
return props.some(prop => prop === 'productName' || prop === 'dependencies.electron')
}
function errorMessageForProperty (prop) {
let hash, propDescription
switch (prop) {
case 'productName':
hash = 'name'
propDescription = 'application name'
break
case 'dependencies.electron':
hash = 'electronversion'
propDescription = 'Electron version'
break
case 'version':
hash = 'appversion'
propDescription = 'application version'
break
/* istanbul ignore next */
default:
hash = ''
propDescription = `[Unknown Property (${prop})]`
}
return `Unable to determine ${propDescription}. Please specify an ${propDescription}\n\n` +
'For more information, please see\n' +
`https://electron.github.io/electron-packager/main/interfaces/electronpackager.options.html#${hash}\n`
}
function resolvePromise (id, options) {
// eslint-disable-next-line promise/param-names
return new Promise((accept, reject) => {
resolve(id, options, (err, mainPath, pkg) => {
if (err) {
/* istanbul ignore next */
reject(err)
} else {
accept([mainPath, pkg])
}
})
})
}
function rangeFromElectronVersion (electronVersion) {
try {
return new semver.Range(electronVersion)
} catch (error) {
return null
}
}
async function getVersion (opts, electronProp) {
const [depType, packageName] = electronProp.prop.split('.')
const src = electronProp.src
if (packageName === 'electron-prebuilt-compile') {
const electronVersion = electronProp.pkg[depType][packageName]
const versionRange = rangeFromElectronVersion(electronVersion)
if (versionRange !== null && versionRange.intersects(new semver.Range('< 1.6.5'))) {
if (!/^\d+\.\d+\.\d+/.test(electronVersion)) {
// electron-prebuilt-compile cannot be resolved because `main` does not point
// to a valid JS file.
throw new Error('Using electron-prebuilt-compile with Electron Packager requires specifying an exact Electron version')
}
opts.electronVersion = electronVersion
return Promise.resolve()
}
}
const pkg = (await resolvePromise(packageName, { basedir: path.dirname(src) }))[1]
debug(`Inferring target Electron version from ${packageName} in ${src}`)
opts.electronVersion = pkg.version
return null
}
async function handleMetadata (opts, result) {
if (result.values.productName) {
debug(`Inferring application name from ${result.source.productName.prop} in ${result.source.productName.src}`)
opts.name = result.values.productName
}
if (result.values.version) {
debug(`Inferring appVersion from version in ${result.source.version.src}`)
opts.appVersion = result.values.version
}
if (result.values.author && !opts.win32metadata) {
opts.win32metadata = {}
}
if (result.values.author) {
debug(`Inferring win32metadata.CompanyName from author in ${result.source.author.src}`)
if (typeof result.values.author === 'string') {
opts.win32metadata.CompanyName = parseAuthor(result.values.author).name
} else if (result.values.author.name) {
opts.win32metadata.CompanyName = result.values.author.name
} else {
debug('Cannot infer win32metadata.CompanyName from author, no name found')
}
}
// eslint-disable-next-line no-prototype-builtins
if (result.values.hasOwnProperty('dependencies.electron')) {
return getVersion(opts, result.source['dependencies.electron'])
} else {
return Promise.resolve()
}
}
function handleMissingProperties (opts, err) {
const missingProps = err.missingProps.map(prop => {
return Array.isArray(prop) ? prop[0] : prop
})
if (isMissingRequiredProperty(missingProps)) {
const messages = missingProps.map(errorMessageForProperty)
debug(err.message)
err.message = messages.join('\n') + '\n'
throw err
} else {
// Missing props not required, can continue w/ partial result
return handleMetadata(opts, err.result)
}
}
module.exports = async function getMetadataFromPackageJSON (platforms, opts, dir) {
const props = []
if (!opts.name) props.push(['productName', 'name'])
if (!opts.appVersion) props.push('version')
if (!opts.electronVersion) {
props.push([
'dependencies.electron',
'devDependencies.electron',
'dependencies.electron-nightly',
'devDependencies.electron-nightly',
'dependencies.electron-prebuilt-compile',
'devDependencies.electron-prebuilt-compile',
'dependencies.electron-prebuilt',
'devDependencies.electron-prebuilt'
])
}
if (platforms.includes('win32') && !(opts.win32metadata && opts.win32metadata.CompanyName)) {
debug('Requiring author in package.json, as CompanyName was not specified for win32metadata')
props.push('author')
}
// Name and version provided, no need to infer
if (props.length === 0) return Promise.resolve()
// Search package.json files to infer name and version from
try {
const result = await getPackageInfo(props, dir)
return handleMetadata(opts, result)
} catch (err) {
if (err.missingProps) {
if (err.missingProps.length === props.length) {
debug(err.message)
err.message = `Could not locate a package.json file in "${path.resolve(opts.dir)}" or its parent directories for an Electron app with the following fields: ${err.missingProps.join(', ')}`
} else {
return handleMissingProperties(opts, err)
}
}
throw err
}
}

View File

@@ -0,0 +1,25 @@
'use strict'
const App = require('./platform')
const common = require('./common')
class LinuxApp extends App {
get originalElectronName () {
return 'electron'
}
get newElectronName () {
return common.sanitizeAppName(this.executableName)
}
async create () {
await this.initialize()
await this.renameElectron()
await this.copyExtraResources()
return this.move()
}
}
module.exports = {
App: LinuxApp
}

View File

@@ -0,0 +1,440 @@
'use strict'
const App = require('./platform')
const common = require('./common')
const debug = require('debug')('electron-packager')
const fs = require('fs-extra')
const path = require('path')
const plist = require('plist')
const { notarize } = require('@electron/notarize')
const { signApp } = require('@electron/osx-sign')
class MacApp extends App {
constructor (opts, templatePath) {
super(opts, templatePath)
this.appName = opts.name
}
get appCategoryType () {
return this.opts.appCategoryType
}
get appCopyright () {
return this.opts.appCopyright
}
get appVersion () {
return this.opts.appVersion
}
get buildVersion () {
return this.opts.buildVersion
}
get enableDarkMode () {
return this.opts.darwinDarkModeSupport
}
get usageDescription () {
return this.opts.usageDescription
}
get protocols () {
return this.opts.protocols.map((protocol) => {
return {
CFBundleURLName: protocol.name,
CFBundleURLSchemes: [].concat(protocol.schemes)
}
})
}
get dotAppName () {
return `${common.sanitizeAppName(this.appName)}.app`
}
get defaultBundleName () {
return `com.electron.${common.sanitizeAppName(this.appName).toLowerCase()}`
}
get bundleName () {
return filterCFBundleIdentifier(this.opts.appBundleId || this.defaultBundleName)
}
get originalResourcesDir () {
return path.join(this.contentsPath, 'Resources')
}
get resourcesDir () {
return path.join(this.dotAppName, 'Contents', 'Resources')
}
get electronBinaryDir () {
return path.join(this.contentsPath, 'MacOS')
}
get originalElectronName () {
return 'Electron'
}
get newElectronName () {
return this.appPlist.CFBundleExecutable
}
get renamedAppPath () {
return path.join(this.stagingPath, this.dotAppName)
}
get electronAppPath () {
return path.join(this.stagingPath, `${this.originalElectronName}.app`)
}
get contentsPath () {
return path.join(this.electronAppPath, 'Contents')
}
get frameworksPath () {
return path.join(this.contentsPath, 'Frameworks')
}
get loginItemsPath () {
return path.join(this.contentsPath, 'Library', 'LoginItems')
}
get loginHelperPath () {
return path.join(this.loginItemsPath, 'Electron Login Helper.app')
}
updatePlist (basePlist, displayName, identifier, name) {
return Object.assign(basePlist, {
CFBundleDisplayName: displayName,
CFBundleExecutable: common.sanitizeAppName(displayName),
CFBundleIdentifier: identifier,
CFBundleName: common.sanitizeAppName(name)
})
}
updateHelperPlist (basePlist, suffix, identifierIgnoresSuffix) {
let helperSuffix, identifier, name
if (suffix) {
helperSuffix = `Helper ${suffix}`
if (identifierIgnoresSuffix) {
identifier = this.helperBundleIdentifier
} else {
identifier = `${this.helperBundleIdentifier}.${suffix}`
}
name = `${this.appName} ${helperSuffix}`
} else {
helperSuffix = 'Helper'
identifier = this.helperBundleIdentifier
name = this.appName
}
return this.updatePlist(basePlist, `${this.appName} ${helperSuffix}`, identifier, name)
}
async extendPlist (basePlist, propsOrFilename) {
if (!propsOrFilename) {
return Promise.resolve()
}
if (typeof propsOrFilename === 'string') {
const plist = await this.loadPlist(propsOrFilename)
return Object.assign(basePlist, plist)
} else {
return Object.assign(basePlist, propsOrFilename)
}
}
async loadPlist (filename, propName) {
const loadedPlist = plist.parse((await fs.readFile(filename)).toString())
if (propName) {
this[propName] = loadedPlist
}
return loadedPlist
}
ehPlistFilename (helper) {
return this.helperPlistFilename(path.join(this.frameworksPath, helper))
}
helperPlistFilename (helperApp) {
return path.join(helperApp, 'Contents', 'Info.plist')
}
async determinePlistFilesToUpdate () {
const appPlistFilename = path.join(this.contentsPath, 'Info.plist')
const plists = [
[appPlistFilename, 'appPlist'],
[this.ehPlistFilename('Electron Helper.app'), 'helperPlist']
]
const possiblePlists = [
[this.ehPlistFilename('Electron Helper (Renderer).app'), 'helperRendererPlist'],
[this.ehPlistFilename('Electron Helper (Plugin).app'), 'helperPluginPlist'],
[this.ehPlistFilename('Electron Helper (GPU).app'), 'helperGPUPlist'],
[this.ehPlistFilename('Electron Helper EH.app'), 'helperEHPlist'],
[this.ehPlistFilename('Electron Helper NP.app'), 'helperNPPlist'],
[this.helperPlistFilename(this.loginHelperPath), 'loginHelperPlist']
]
const optional = await Promise.all(possiblePlists.map(async item =>
(await fs.pathExists(item[0])) ? item : null))
return plists.concat(optional.filter(item => item))
}
appRelativePath (p) {
return path.relative(this.contentsPath, p)
}
async updatePlistFiles () {
const appBundleIdentifier = this.bundleName
this.helperBundleIdentifier = filterCFBundleIdentifier(this.opts.helperBundleId || `${appBundleIdentifier}.helper`)
const plists = await this.determinePlistFilesToUpdate()
await Promise.all(plists.map(plistArgs => this.loadPlist(...plistArgs)))
await this.extendPlist(this.appPlist, this.opts.extendInfo)
if (this.asarIntegrity) {
await this.extendPlist(this.appPlist, {
ElectronAsarIntegrity: this.asarIntegrity
})
} else {
delete this.appPlist.ElectronAsarIntegrity
}
this.appPlist = this.updatePlist(this.appPlist, this.executableName, appBundleIdentifier, this.appName)
const updateIfExists = [
['helperRendererPlist', '(Renderer)', true],
['helperPluginPlist', '(Plugin)', true],
['helperGPUPlist', '(GPU)', true],
['helperEHPlist', 'EH'],
['helperNPPlist', 'NP']
]
for (const [plistKey] of [...updateIfExists, ['helperPlist']]) {
if (!this[plistKey]) continue
await this.extendPlist(this[plistKey], this.opts.extendHelperInfo)
}
this.helperPlist = this.updateHelperPlist(this.helperPlist)
for (const [plistKey, ...suffixArgs] of updateIfExists) {
if (!this[plistKey]) continue
this[plistKey] = this.updateHelperPlist(this[plistKey], ...suffixArgs)
}
// Some properties need to go on all helpers as well, version, usage info, etc.
const plistsToUpdate = updateIfExists
.filter(([key]) => !!this[key])
.map(([key]) => key)
.concat(['appPlist', 'helperPlist'])
if (this.loginHelperPlist) {
const loginHelperName = common.sanitizeAppName(`${this.appName} Login Helper`)
this.loginHelperPlist.CFBundleExecutable = loginHelperName
this.loginHelperPlist.CFBundleIdentifier = `${appBundleIdentifier}.loginhelper`
this.loginHelperPlist.CFBundleName = loginHelperName
}
if (this.appVersion) {
const appVersionString = '' + this.appVersion
for (const plistKey of plistsToUpdate) {
this[plistKey].CFBundleShortVersionString = this[plistKey].CFBundleVersion = appVersionString
}
}
if (this.buildVersion) {
const buildVersionString = '' + this.buildVersion
for (const plistKey of plistsToUpdate) {
this[plistKey].CFBundleVersion = buildVersionString
}
}
if (this.opts.protocols && this.opts.protocols.length) {
this.appPlist.CFBundleURLTypes = this.protocols
}
if (this.appCategoryType) {
this.appPlist.LSApplicationCategoryType = this.appCategoryType
}
if (this.appCopyright) {
this.appPlist.NSHumanReadableCopyright = this.appCopyright
}
if (this.enableDarkMode) {
this.appPlist.NSRequiresAquaSystemAppearance = false
}
if (this.usageDescription) {
for (const [type, description] of Object.entries(this.usageDescription)) {
const usageTypeKey = `NS${type}UsageDescription`
for (const plistKey of plistsToUpdate) {
this[plistKey][usageTypeKey] = description
}
this.appPlist[usageTypeKey] = description
}
}
await Promise.all(plists.map(([filename, varName]) =>
fs.writeFile(filename, plist.build(this[varName]))))
}
async moveHelpers () {
const helpers = [' Helper', ' Helper EH', ' Helper NP', ' Helper (Renderer)', ' Helper (Plugin)', ' Helper (GPU)']
await Promise.all(helpers.map(suffix => this.moveHelper(this.frameworksPath, suffix)))
if (await fs.pathExists(this.loginItemsPath)) {
await this.moveHelper(this.loginItemsPath, ' Login Helper')
}
}
async moveHelper (helperDirectory, suffix) {
const originalBasename = `Electron${suffix}`
if (await fs.pathExists(path.join(helperDirectory, `${originalBasename}.app`))) {
return this.renameHelperAndExecutable(
helperDirectory,
originalBasename,
`${common.sanitizeAppName(this.appName)}${suffix}`
)
} else {
return Promise.resolve()
}
}
async renameHelperAndExecutable (helperDirectory, originalBasename, newBasename) {
const originalAppname = `${originalBasename}.app`
const executableBasePath = path.join(helperDirectory, originalAppname, 'Contents', 'MacOS')
await this.relativeRename(executableBasePath, originalBasename, newBasename)
await this.relativeRename(helperDirectory, originalAppname, `${newBasename}.app`)
}
async copyIcon () {
if (!this.opts.icon) {
return Promise.resolve()
}
let icon
try {
icon = await this.normalizeIconExtension('.icns')
} catch {
// Ignore error if icon doesn't exist, in case it's only available for other OSes
/* istanbul ignore next */
return Promise.resolve()
}
if (icon) {
debug(`Copying icon "${icon}" to app's Resources as "${this.appPlist.CFBundleIconFile}"`)
await fs.copy(icon, path.join(this.originalResourcesDir, this.appPlist.CFBundleIconFile))
}
}
async renameAppAndHelpers () {
await this.moveHelpers()
await fs.rename(this.electronAppPath, this.renamedAppPath)
}
async signAppIfSpecified () {
const osxSignOpt = this.opts.osxSign
const platform = this.opts.platform
const version = this.opts.electronVersion
if ((platform === 'all' || platform === 'mas') &&
osxSignOpt === undefined) {
common.warning('signing is required for mas builds. Provide the osx-sign option, ' +
'or manually sign the app later.', this.opts.quiet)
}
if (osxSignOpt) {
const signOpts = createSignOpts(osxSignOpt, platform, this.renamedAppPath, version, this.opts.quiet)
debug(`Running @electron/osx-sign with the options ${JSON.stringify(signOpts)}`)
try {
await signApp(signOpts)
} catch (err) {
// Although not signed successfully, the application is packed.
common.warning(`Code sign failed; please retry manually. ${err}`, this.opts.quiet)
}
}
}
async notarizeAppIfSpecified () {
const osxNotarizeOpt = this.opts.osxNotarize
/* istanbul ignore if */
if (osxNotarizeOpt) {
const notarizeOpts = createNotarizeOpts(
osxNotarizeOpt,
this.bundleName,
this.renamedAppPath,
this.opts.quiet
)
if (notarizeOpts) {
return notarize(notarizeOpts)
}
}
}
async create () {
await this.initialize()
await this.updatePlistFiles()
await this.copyIcon()
await this.renameElectron()
await this.renameAppAndHelpers()
await this.copyExtraResources()
await this.signAppIfSpecified()
await this.notarizeAppIfSpecified()
return this.move()
}
}
/**
* Remove special characters and allow only alphanumeric (A-Z,a-z,0-9), hyphen (-), and period (.)
* Apple documentation:
* https://developer.apple.com/library/mac/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html#//apple_ref/doc/uid/20001431-102070
*/
function filterCFBundleIdentifier (identifier) {
return identifier.replace(/ /g, '-').replace(/[^a-zA-Z0-9.-]/g, '')
}
function createSignOpts (properties, platform, app, version, quiet) {
// use default sign opts if osx-sign is true, otherwise clone osx-sign object
const signOpts = properties === true ? { identity: null } : { ...properties }
// osx-sign options are handed off to sign module, but
// with a few additions from the main options
// user may think they can pass platform, app, or version, but they will be ignored
common.subOptionWarning(signOpts, 'osx-sign', 'platform', platform, quiet)
common.subOptionWarning(signOpts, 'osx-sign', 'app', app, quiet)
common.subOptionWarning(signOpts, 'osx-sign', 'version', version, quiet)
if (signOpts.binaries) {
common.warning('osx-sign.binaries is not an allowed sub-option. Not passing to @electron/osx-sign.', quiet)
delete signOpts.binaries
}
// Take argument osx-sign as signing identity:
// if opts.osxSign is true (bool), fallback to identity=null for
// autodiscovery. Otherwise, provide signing certificate info.
if (signOpts.identity === true) {
signOpts.identity = null
}
return signOpts
}
function createNotarizeOpts (properties, appBundleId, appPath, quiet) {
// osxNotarize options are handed off to the @electron/notarize module, but with a few
// additions from the main options. The user may think they can pass bundle ID or appPath,
// but they will be ignored.
if (properties.tool !== 'notarytool') {
common.subOptionWarning(properties, 'osxNotarize', 'appBundleId', appBundleId, quiet)
}
common.subOptionWarning(properties, 'osxNotarize', 'appPath', appPath, quiet)
return properties
}
module.exports = {
App: MacApp,
createNotarizeOpts: createNotarizeOpts,
createSignOpts: createSignOpts,
filterCFBundleIdentifier: filterCFBundleIdentifier
}

View File

@@ -0,0 +1,277 @@
'use strict'
const asar = require('@electron/asar')
const crypto = require('crypto')
const debug = require('debug')('electron-packager')
const fs = require('fs-extra')
const path = require('path')
const common = require('./common')
const copyFilter = require('./copy-filter')
const hooks = require('./hooks')
class App {
constructor (opts, templatePath) {
this.opts = opts
this.templatePath = templatePath
this.asarOptions = common.createAsarOpts(opts)
if (this.opts.prune === undefined) {
this.opts.prune = true
}
}
/**
* Resource directory path before renaming.
*/
get originalResourcesDir () {
return this.resourcesDir
}
/**
* Resource directory path after renaming.
*/
get resourcesDir () {
return path.join(this.stagingPath, 'resources')
}
get originalResourcesAppDir () {
return path.join(this.originalResourcesDir, 'app')
}
get electronBinaryDir () {
return this.stagingPath
}
get originalElectronName () {
/* istanbul ignore next */
throw new Error('Child classes must implement this')
}
get newElectronName () {
/* istanbul ignore next */
throw new Error('Child classes must implement this')
}
get executableName () {
return this.opts.executableName || this.opts.name
}
get stagingPath () {
if (this.opts.tmpdir === false) {
return common.generateFinalPath(this.opts)
} else {
if (!this.cachedStagingPath) {
const parentDir = path.join(
common.baseTempDir(this.opts),
`${this.opts.platform}-${this.opts.arch}`
)
fs.mkdirpSync(parentDir)
this.cachedStagingPath = fs.mkdtempSync(path.join(parentDir, `${common.generateFinalBasename(this.opts)}-`))
}
return this.cachedStagingPath
}
}
get appAsarPath () {
return path.join(this.originalResourcesDir, 'app.asar')
}
get commonHookArgs () {
return [
this.opts.electronVersion,
this.opts.platform,
this.opts.arch
]
}
get hookArgsWithOriginalResourcesAppDir () {
return [
this.originalResourcesAppDir,
...this.commonHookArgs
]
}
async relativeRename (basePath, oldName, newName) {
debug(`Renaming ${oldName} to ${newName} in ${basePath}`)
await fs.rename(path.join(basePath, oldName), path.join(basePath, newName))
}
async renameElectron () {
return this.relativeRename(this.electronBinaryDir, this.originalElectronName, this.newElectronName)
}
/**
* Performs the following initial operations for an app:
* * Creates temporary directory
* * Remove default_app (which is either a folder or an asar file)
* * If a prebuilt asar is specified:
* * Copies asar into temporary directory as app.asar
* * Otherwise:
* * Copies template into temporary directory
* * Copies user's app into temporary directory
* * Prunes non-production node_modules (if opts.prune is either truthy or undefined)
* * Creates an asar (if opts.asar is set)
*
* Prune and asar are performed before platform-specific logic, primarily so that
* this.originalResourcesAppDir is predictable (e.g. before .app is renamed for Mac)
*/
async initialize () {
debug(`Initializing app in ${this.stagingPath} from ${this.templatePath} template`)
await fs.move(this.templatePath, this.stagingPath, { clobber: true })
await this.removeDefaultApp()
if (this.opts.prebuiltAsar) {
await this.copyPrebuiltAsar()
} else {
await this.buildApp()
}
await hooks.promisifyHooks(this.opts.afterInitialize, this.hookArgsWithOriginalResourcesAppDir)
}
async buildApp () {
await this.copyTemplate()
await common.validateElectronApp(this.opts.dir, this.originalResourcesAppDir)
await this.asarApp()
}
async copyTemplate () {
await hooks.promisifyHooks(this.opts.beforeCopy, this.hookArgsWithOriginalResourcesAppDir)
await fs.copy(this.opts.dir, this.originalResourcesAppDir, {
filter: copyFilter.userPathFilter(this.opts),
dereference: this.opts.derefSymlinks
})
await hooks.promisifyHooks(this.opts.afterCopy, this.hookArgsWithOriginalResourcesAppDir)
if (this.opts.prune) {
await hooks.promisifyHooks(this.opts.afterPrune, this.hookArgsWithOriginalResourcesAppDir)
}
}
async removeDefaultApp () {
await Promise.all(['default_app', 'default_app.asar'].map(async basename => fs.remove(path.join(this.originalResourcesDir, basename))))
}
/**
* Forces an icon filename to a given extension and returns the normalized filename,
* if it exists. Otherwise, returns null.
*
* This error path is used by win32 if no icon is specified.
*/
async normalizeIconExtension (targetExt) {
if (!this.opts.icon) throw new Error('No filename specified to normalizeIconExtension')
let iconFilename = this.opts.icon
const ext = path.extname(iconFilename)
if (ext !== targetExt) {
iconFilename = path.join(path.dirname(iconFilename), path.basename(iconFilename, ext) + targetExt)
}
if (await fs.pathExists(iconFilename)) {
return iconFilename
} else {
/* istanbul ignore next */
common.warning(`Could not find icon "${iconFilename}", not updating app icon`, this.opts.quiet)
}
}
prebuiltAsarWarning (option, triggerWarning) {
if (triggerWarning) {
common.warning(`prebuiltAsar and ${option} are incompatible, ignoring the ${option} option`, this.opts.quiet)
}
}
async copyPrebuiltAsar () {
if (this.asarOptions) {
common.warning('prebuiltAsar has been specified, all asar options will be ignored', this.opts.quiet)
}
for (const hookName of ['beforeCopy', 'afterCopy', 'afterPrune']) {
if (this.opts[hookName]) {
throw new Error(`${hookName} is incompatible with prebuiltAsar`)
}
}
this.prebuiltAsarWarning('ignore', this.opts.originalIgnore)
this.prebuiltAsarWarning('prune', !this.opts.prune)
this.prebuiltAsarWarning('derefSymlinks', this.opts.derefSymlinks !== undefined)
const src = path.resolve(this.opts.prebuiltAsar)
const stat = await fs.stat(src)
if (!stat.isFile()) {
throw new Error(`${src} specified in prebuiltAsar must be an asar file.`)
}
debug(`Copying asar: ${src} to ${this.appAsarPath}`)
await fs.copy(src, this.appAsarPath, { overwrite: false, errorOnExist: true })
}
appRelativePath (p) {
return path.relative(this.stagingPath, p)
}
async asarApp () {
if (!this.asarOptions) {
return Promise.resolve()
}
debug(`Running asar with the options ${JSON.stringify(this.asarOptions)}`)
await hooks.promisifyHooks(this.opts.beforeAsar, this.hookArgsWithOriginalResourcesAppDir)
await asar.createPackageWithOptions(this.originalResourcesAppDir, this.appAsarPath, this.asarOptions)
const { headerString } = asar.getRawHeader(this.appAsarPath)
this.asarIntegrity = {
[this.appRelativePath(this.appAsarPath)]: {
algorithm: 'SHA256',
hash: crypto.createHash('SHA256').update(headerString).digest('hex')
}
}
await fs.remove(this.originalResourcesAppDir)
await hooks.promisifyHooks(this.opts.afterAsar, this.hookArgsWithOriginalResourcesAppDir)
}
async copyExtraResources () {
if (!this.opts.extraResource) return Promise.resolve()
const extraResources = common.ensureArray(this.opts.extraResource)
const hookArgs = [
this.stagingPath,
...this.commonHookArgs
]
await hooks.promisifyHooks(this.opts.beforeCopyExtraResources, hookArgs)
await Promise.all(extraResources.map(
resource => fs.copy(resource, path.resolve(this.stagingPath, this.resourcesDir, path.basename(resource)))
))
await hooks.promisifyHooks(this.opts.afterCopyExtraResources, hookArgs)
}
async move () {
const finalPath = common.generateFinalPath(this.opts)
if (this.opts.tmpdir !== false) {
debug(`Moving ${this.stagingPath} to ${finalPath}`)
await fs.move(this.stagingPath, finalPath)
}
if (this.opts.afterComplete) {
const hookArgs = [
finalPath,
...this.commonHookArgs
]
await hooks.promisifyHooks(this.opts.afterComplete, hookArgs)
}
return finalPath
}
}
module.exports = App

View File

@@ -0,0 +1,70 @@
'use strict'
const common = require('./common')
const galactus = require('galactus')
const fs = require('fs-extra')
const path = require('path')
const ELECTRON_MODULES = [
'electron',
'electron-nightly',
'electron-prebuilt',
'electron-prebuilt-compile'
]
class Pruner {
constructor (dir, quiet) {
this.baseDir = common.normalizePath(dir)
this.quiet = quiet
this.galactus = new galactus.DestroyerOfModules({
rootDirectory: dir,
shouldKeepModuleTest: (module, isDevDep) => this.shouldKeepModule(module, isDevDep)
})
this.walkedTree = false
}
setModules (moduleMap) {
const modulePaths = Array.from(moduleMap.keys()).map(modulePath => `/${common.normalizePath(modulePath)}`)
this.modules = new Set(modulePaths)
this.walkedTree = true
}
async pruneModule (name) {
if (this.walkedTree) {
return this.isProductionModule(name)
} else {
const moduleMap = await this.galactus.collectKeptModules({ relativePaths: true })
this.setModules(moduleMap)
return this.isProductionModule(name)
}
}
shouldKeepModule (module, isDevDep) {
if (isDevDep || module.depType === galactus.DepType.ROOT) {
return false
}
if (ELECTRON_MODULES.includes(module.name)) {
common.warning(`Found '${module.name}' but not as a devDependency, pruning anyway`, this.quiet)
return false
}
return true
}
isProductionModule (name) {
return this.modules.has(name)
}
}
function isNodeModuleFolder (pathToCheck) {
return path.basename(path.dirname(pathToCheck)) === 'node_modules' ||
(path.basename(path.dirname(pathToCheck)).startsWith('@') && path.basename(path.resolve(pathToCheck, `..${path.sep}..`)) === 'node_modules')
}
module.exports = {
isModule: async function isModule (pathToCheck) {
return (await fs.pathExists(path.join(pathToCheck, 'package.json'))) && isNodeModuleFolder(pathToCheck)
},
Pruner: Pruner
}

View File

@@ -0,0 +1,149 @@
'use strict'
const common = require('./common')
const { getHostArch } = require('@electron/get')
const semver = require('semver')
const officialArchs = ['ia32', 'x64', 'armv7l', 'arm64', 'mips64el', 'universal']
const officialPlatforms = ['darwin', 'linux', 'mas', 'win32']
const officialPlatformArchCombos = {
darwin: ['x64', 'arm64', 'universal'],
linux: ['ia32', 'x64', 'armv7l', 'arm64', 'mips64el'],
mas: ['x64', 'arm64', 'universal'],
win32: ['ia32', 'x64', 'arm64']
}
const buildVersions = {
darwin: {
arm64: '>= 11.0.0-beta.1',
universal: '>= 11.0.0-beta.1'
},
linux: {
arm64: '>= 1.8.0',
ia32: '<19.0.0-beta.1',
mips64el: '^1.8.2-beta.5'
},
mas: {
arm64: '>= 11.0.0-beta.1',
universal: '>= 11.0.0-beta.1'
},
win32: {
arm64: '>= 6.0.8'
}
}
// Maps to module filename for each platform (lazy-required if used)
const osModules = {
darwin: './mac',
linux: './linux',
mas: './mac', // map to darwin
win32: './win32'
}
const supported = {
arch: new Set(officialArchs),
platform: new Set(officialPlatforms)
}
function createPlatformArchPairs (opts, selectedPlatforms, selectedArchs, ignoreFunc) {
const combinations = []
for (const arch of selectedArchs) {
for (const platform of selectedPlatforms) {
if (usingOfficialElectronPackages(opts)) {
if (!validOfficialPlatformArch(opts, platform, arch)) {
warnIfAllNotSpecified(opts, `The platform/arch combination ${platform}/${arch} is not currently supported by Electron Packager`)
continue
} else if (buildVersions[platform] && buildVersions[platform][arch]) {
const buildVersion = buildVersions[platform][arch]
if (buildVersion && !officialBuildExists(opts, buildVersion)) {
warnIfAllNotSpecified(opts, `Official ${platform}/${arch} support only exists in Electron ${buildVersion}`)
continue
}
}
if (typeof ignoreFunc === 'function' && ignoreFunc(platform, arch)) continue
}
combinations.push([platform, arch])
}
}
return combinations
}
function unsupportedListOption (name, value, supported) {
return new Error(`Unsupported ${name}=${value} (${typeof value}); must be a string matching: ${Array.from(supported.values()).join(', ')}`)
}
function usingOfficialElectronPackages (opts) {
return !opts.download || !Object.prototype.hasOwnProperty.call(opts.download, 'mirrorOptions')
}
function validOfficialPlatformArch (opts, platform, arch) {
return officialPlatformArchCombos[platform] && officialPlatformArchCombos[platform].includes(arch)
}
function officialBuildExists (opts, buildVersion) {
return semver.satisfies(opts.electronVersion, buildVersion, { includePrerelease: true })
}
function allPlatformsOrArchsSpecified (opts) {
return opts.all || opts.arch === 'all' || opts.platform === 'all'
}
function warnIfAllNotSpecified (opts, message) {
if (!allPlatformsOrArchsSpecified(opts)) {
common.warning(message, opts.quiet)
}
}
module.exports = {
allOfficialArchsForPlatformAndVersion: function allOfficialArchsForPlatformAndVersion (platform, electronVersion) {
const archs = officialPlatformArchCombos[platform]
if (buildVersions[platform]) {
const excludedArchs = Object.keys(buildVersions[platform])
.filter(arch => !officialBuildExists({ electronVersion: electronVersion }, buildVersions[platform][arch]))
return archs.filter(arch => !excludedArchs.includes(arch))
}
return archs
},
createPlatformArchPairs,
officialArchs,
officialPlatformArchCombos,
officialPlatforms,
osModules,
supported,
// Validates list of architectures or platforms.
// Returns a normalized array if successful, or throws an Error.
validateListFromOptions: function validateListFromOptions (opts, name) {
if (opts.all) return Array.from(supported[name].values())
let list = opts[name]
if (!list) {
if (name === 'arch') {
list = getHostArch()
} else {
list = process[name]
}
} else if (list === 'all') {
return Array.from(supported[name].values())
}
if (!Array.isArray(list)) {
if (typeof list === 'string') {
list = list.split(/,\s*/)
} else {
return unsupportedListOption(name, list, supported[name])
}
}
const officialElectronPackages = usingOfficialElectronPackages(opts)
for (const value of list) {
if (officialElectronPackages && !supported[name].has(value)) {
return unsupportedListOption(name, value, supported[name])
}
}
return list
}
}

View File

@@ -0,0 +1,80 @@
'use strict'
const universal = require('@electron/universal')
const common = require('./common')
const fs = require('fs-extra')
const path = require('path')
async function packageUniversalMac (packageForPlatformAndArchWithOpts, buildDir, comboOpts, downloadOpts, tempBase) {
// In order to generate a universal macOS build we actually need to build the x64 and the arm64 app
// and then glue them together
common.info(`Packaging app for platform ${comboOpts.platform} universal using electron v${comboOpts.electronVersion} - Building x64 and arm64 slices now`, comboOpts.quiet)
await fs.mkdirp(tempBase)
const tempDir = await fs.mkdtemp(path.resolve(tempBase, 'electron-packager-universal-'))
const { App } = require('./mac')
const app = new App(comboOpts, buildDir)
const universalStagingPath = app.stagingPath
const finalUniversalPath = common.generateFinalPath(app.opts)
if (await fs.pathExists(finalUniversalPath)) {
if (comboOpts.overwrite) {
await fs.remove(finalUniversalPath)
} else {
common.info(`Skipping ${comboOpts.platform} ${comboOpts.arch} (output dir already exists, use --overwrite to force)`, comboOpts.quiet)
return true
}
}
const tempPackages = {}
for (const tempArch of ['x64', 'arm64']) {
const tempOpts = {
...comboOpts,
arch: tempArch,
out: tempDir
}
const tempDownloadOpts = {
...downloadOpts,
arch: tempArch
}
// Do not sign or notarize the individual slices, we sign and notarize the merged app later
delete tempOpts.osxSign
delete tempOpts.osxNotarize
tempPackages[tempArch] = await packageForPlatformAndArchWithOpts(tempOpts, tempDownloadOpts)
}
const x64AppPath = tempPackages.x64
const arm64AppPath = tempPackages.arm64
common.info(`Stitching universal app for platform ${comboOpts.platform}`, comboOpts.quiet)
const generatedFiles = await fs.readdir(x64AppPath)
const appName = generatedFiles.filter(file => path.extname(file) === '.app')[0]
await universal.makeUniversalApp({
...comboOpts.osxUniversal,
x64AppPath: path.resolve(x64AppPath, appName),
arm64AppPath: path.resolve(arm64AppPath, appName),
outAppPath: path.resolve(universalStagingPath, appName)
})
await app.signAppIfSpecified()
await app.notarizeAppIfSpecified()
await app.move()
for (const generatedFile of generatedFiles) {
if (path.extname(generatedFile) === '.app') continue
await fs.copy(path.resolve(x64AppPath, generatedFile), path.resolve(finalUniversalPath, generatedFile))
}
await fs.remove(tempDir)
return finalUniversalPath
}
module.exports = {
packageUniversalMac
}

View File

@@ -0,0 +1,7 @@
'use strict'
const extractZip = require('extract-zip')
module.exports = async function extractElectronZip (zipPath, targetDir) {
await extractZip(zipPath, { dir: targetDir })
}

View File

@@ -0,0 +1,113 @@
'use strict'
const debug = require('debug')('electron-packager')
const path = require('path')
const { WrapperError } = require('cross-spawn-windows-exe')
const App = require('./platform')
const common = require('./common')
function updateWineMissingException (err) {
if (err instanceof WrapperError) {
err.message += '\n\n' +
'Wine is required to use the appCopyright, appVersion, buildVersion, icon, and \n' +
'win32metadata parameters for Windows targets.\n\n' +
'See https://github.com/electron/electron-packager#building-windows-apps-from-non-windows-platforms for details.'
}
return err
}
class WindowsApp extends App {
get originalElectronName () {
return 'electron.exe'
}
get newElectronName () {
return `${common.sanitizeAppName(this.executableName)}.exe`
}
get electronBinaryPath () {
return path.join(this.stagingPath, this.newElectronName)
}
generateRceditOptionsSansIcon () {
const win32metadata = {
FileDescription: this.opts.name,
InternalName: this.opts.name,
OriginalFilename: this.newElectronName,
ProductName: this.opts.name,
...this.opts.win32metadata
}
const rcOpts = { 'version-string': win32metadata }
if (this.opts.appVersion) {
rcOpts['product-version'] = rcOpts['file-version'] = this.opts.appVersion
}
if (this.opts.buildVersion) {
rcOpts['file-version'] = this.opts.buildVersion
}
if (this.opts.appCopyright) {
rcOpts['version-string'].LegalCopyright = this.opts.appCopyright
}
const manifestProperties = ['application-manifest', 'requested-execution-level']
for (const manifestProperty of manifestProperties) {
if (win32metadata[manifestProperty]) {
rcOpts[manifestProperty] = win32metadata[manifestProperty]
}
}
return rcOpts
}
async getIconPath () {
if (!this.opts.icon) {
return Promise.resolve()
}
return this.normalizeIconExtension('.ico')
}
needsRcedit () {
return this.opts.icon || this.opts.win32metadata || this.opts.appCopyright || this.opts.appVersion || this.opts.buildVersion
}
async runRcedit () {
/* istanbul ignore if */
if (!this.needsRcedit()) {
return Promise.resolve()
}
const rcOpts = this.generateRceditOptionsSansIcon()
// Icon might be omitted or only exist in one OS's format, so skip it if normalizeExt reports an error
const icon = await this.getIconPath()
if (icon) {
rcOpts.icon = icon
}
debug(`Running rcedit with the options ${JSON.stringify(rcOpts)}`)
try {
await require('rcedit')(this.electronBinaryPath, rcOpts)
} catch (err) {
throw updateWineMissingException(err)
}
}
async create () {
await this.initialize()
await this.renameElectron()
await this.copyExtraResources()
await this.runRcedit()
return this.move()
}
}
module.exports = {
App: WindowsApp,
updateWineMissingException: updateWineMissingException
}