diff --git a/README.md b/README.md
index 4d4c4dcd..c380877b 100644
--- a/README.md
+++ b/README.md
@@ -45,7 +45,7 @@ Electron Packager is known to run on the following **host** platforms:
It generates executables/bundles for the following **target** platforms:
* Windows (also known as `win32`, for x86, x86_64, and arm64 architectures)
-* macOS (also known as `darwin`) / [Mac App Store](https://electronjs.org/docs/tutorial/mac-app-store-submission-guide/) (also known as `mas`)* (for x86_64 and arm64 architectures)
+* macOS (also known as `darwin`) / [Mac App Store](https://electronjs.org/docs/tutorial/mac-app-store-submission-guide/) (also known as `mas`)* (for x86_64, arm64, and universal architectures)
* Linux (for x86, x86_64, armv7l, arm64, and mips64el architectures)
* *Note for macOS / Mac App Store target bundles: the `.app` bundle can only be signed when building on a host macOS platform.*
diff --git a/package.json b/package.json
index 8a0be124..d4110011 100644
--- a/package.json
+++ b/package.json
@@ -27,6 +27,7 @@
],
"dependencies": {
"@electron/get": "^1.6.0",
+ "@electron/universal": "^1.2.1",
"asar": "^3.1.0",
"cross-spawn-windows-exe": "^1.2.0",
"debug": "^4.0.1",
diff --git a/src/index.d.ts b/src/index.d.ts
index 8d2f77f1..316ba01d 100644
--- a/src/index.d.ts
+++ b/src/index.d.ts
@@ -16,6 +16,9 @@ import {
TransporterOptions
} from 'electron-notarize/lib/types';
import { SignOptions } from 'electron-osx-sign';
+import type { makeUniversalApp } from '@electron/universal';
+
+type MakeUniversalOpts = Parameters[0]
type NotarizeLegacyOptions = LegacyNotarizeCredentials & TransporterOptions;
@@ -128,6 +131,12 @@ declare namespace electronPackager {
| ({ tool?: 'legacy' } & NotarizeLegacyOptions)
| ({ tool: 'notarytool' } & NotaryToolCredentials);
+ /**
+ * See the documentation for [`@electron/universal`](https://github.com/electron/universal)
+ * for details.
+ */
+ type OsxUniversalOptions = Omit
+
/**
* Defines URL protocol schemes to be used on macOS.
*/
@@ -444,6 +453,12 @@ declare namespace electronPackager {
* @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.
*
diff --git a/src/index.js b/src/index.js
index e1323835..bab5fa90 100644
--- a/src/index.js
+++ b/src/index.js
@@ -10,6 +10,7 @@ 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())
@@ -73,14 +74,18 @@ class Packager {
await hooks.promisifyHooks(this.opts.afterExtract, [buildDir, comboOpts.electronVersion, comboOpts.platform, comboOpts.arch])
}
- async createApp (comboOpts, zipPath) {
+ buildDir (platform, arch) {
let buildParentDir
if (this.useTempDir) {
buildParentDir = this.tempBase
} else {
buildParentDir = this.opts.out || process.cwd()
}
- const buildDir = path.resolve(buildParentDir, `${comboOpts.platform}-${comboOpts.arch}-template`)
+ return path.resolve(buildParentDir, `${platform}-${arch}-template`)
+ }
+
+ async createApp (comboOpts, zipPath) {
+ const buildDir = 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}`)
@@ -125,15 +130,8 @@ class Packager {
}
}
- async packageForPlatformAndArch (downloadOpts) {
+ async packageForPlatformAndArchWithOpts (comboOpts, downloadOpts) {
const zipPath = await this.getElectronZipPath(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 (!this.useTempDir) {
return this.createApp(comboOpts, zipPath)
@@ -150,6 +148,22 @@ class Packager {
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), this.buildDir(comboOpts.platform, comboOpts.arch), comboOpts, downloadOpts, this.tempBase)
+ }
+
+ return this.packageForPlatformAndArchWithOpts(comboOpts, downloadOpts)
+ }
}
async function packageAllSpecifiedCombos (opts, archs, platforms) {
diff --git a/src/targets.js b/src/targets.js
index 5a5cc3c0..39b50dd7 100644
--- a/src/targets.js
+++ b/src/targets.js
@@ -4,25 +4,27 @@ const common = require('./common')
const { getHostArch } = require('@electron/get')
const semver = require('semver')
-const officialArchs = ['ia32', 'x64', 'armv7l', 'arm64', 'mips64el']
+const officialArchs = ['ia32', 'x64', 'armv7l', 'arm64', 'mips64el', 'universal']
const officialPlatforms = ['darwin', 'linux', 'mas', 'win32']
const officialPlatformArchCombos = {
- darwin: ['x64', 'arm64'],
+ darwin: ['x64', 'arm64', 'universal'],
linux: ['ia32', 'x64', 'armv7l', 'arm64', 'mips64el'],
- mas: ['x64', 'arm64'],
+ mas: ['x64', 'arm64', 'universal'],
win32: ['ia32', 'x64', 'arm64']
}
const buildVersions = {
darwin: {
- arm64: '>= 11.0.0-beta.1'
+ arm64: '>= 11.0.0-beta.1',
+ universal: '>= 11.0.0-beta.1'
},
linux: {
arm64: '>= 1.8.0',
mips64el: '^1.8.2-beta.5'
},
mas: {
- arm64: '>= 11.0.0-beta.1'
+ arm64: '>= 11.0.0-beta.1',
+ universal: '>= 11.0.0-beta.1'
},
win32: {
arm64: '>= 6.0.8'
diff --git a/src/universal.js b/src/universal.js
new file mode 100644
index 00000000..54a2a7c7
--- /dev/null
+++ b/src/universal.js
@@ -0,0 +1,75 @@
+'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 [x64AppPath, arm64AppPath] = await Promise.all(['x64', 'arm64'].map((tempArch) => {
+ 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
+
+ return packageForPlatformAndArchWithOpts(tempOpts, tempDownloadOpts)
+ }))
+
+ 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
+}
diff --git a/test/_setup.js b/test/_setup.js
index ef156aef..a5fc0b69 100644
--- a/test/_setup.js
+++ b/test/_setup.js
@@ -38,7 +38,7 @@ async function downloadAll (version) {
await downloadElectronChecksum(version)
return Promise.all(
[
- ...combinations.map(combination => downloadElectronZip(version, combination)),
+ ...combinations.map(combination => combination.arch === 'universal' ? null : downloadElectronZip(version, combination)),
downloadElectronZip('6.0.0', {
platform: 'darwin'
})
diff --git a/test/_util.js b/test/_util.js
index 64a82493..66ae45f8 100644
--- a/test/_util.js
+++ b/test/_util.js
@@ -47,7 +47,7 @@ function packagerTestOptions (t) {
}
module.exports = {
- allPlatformArchCombosCount: 12,
+ allPlatformArchCombosCount: 14,
assertDirectory: async function assertDirectory (t, pathToCheck, message) {
const stats = await fs.stat(pathToCheck)
t.true(stats.isDirectory(), message)
diff --git a/test/basic.js b/test/basic.js
index 1b954377..01a45ef6 100644
--- a/test/basic.js
+++ b/test/basic.js
@@ -152,7 +152,7 @@ test.serial('overwrite', util.testSinglePlatform(async (t, opts) => {
}))
test.serial('overwrite sans platform/arch set', util.testSinglePlatform(async (t, opts) => {
- delete opts.platfrom
+ delete opts.platform
delete opts.arch
opts.dir = util.fixtureSubdir('basic')
opts.overwrite = true
diff --git a/test/targets.js b/test/targets.js
index f7507628..b88f21bc 100644
--- a/test/targets.js
+++ b/test/targets.js
@@ -77,10 +77,10 @@ test('validateListFromOptions works for armv7l host and target arch', t => {
})
test('build for all available official targets',
- testMultiTarget({ all: true, electronVersion: '1.8.2' }, util.allPlatformArchCombosCount - 3,
+ testMultiTarget({ all: true, electronVersion: '1.8.2' }, util.allPlatformArchCombosCount - 5,
'Packages should be generated for all possible platforms (except win32/arm64)'))
test('build for all available official targets for a version without arm64 or mips64el support',
- testMultiTarget({ all: true }, util.allPlatformArchCombosCount - 5,
+ testMultiTarget({ all: true }, util.allPlatformArchCombosCount - 7,
'Packages should be generated for all possible platforms (except linux/arm64, linux/mips64el, or win32/arm64)'))
test('platform=all (one arch)',
testMultiTarget({ arch: 'ia32', platform: 'all' }, 2, 'Packages should be generated for both 32-bit platforms'))
@@ -119,6 +119,12 @@ test('platform=darwin and arch=arm64 with an unsupported official Electron versi
test('platform=mas and arch=arm64 with a supported official Electron version', testMultiTarget({ arch: 'arm64', platform: 'mas', electronVersion: '11.0.0-beta.5' }, 1, 'Package should be generated for mas/arm64'))
test('platform=mas and arch=arm64 with an unsupported official Electron version', testMultiTarget({ arch: 'arm64', platform: 'mas' }, 0, 'Package should not be generated for mas/arm64'))
+test('platform=darwin and arch=universal with a supported official Electron version', testMultiTarget({ arch: 'universal', platform: 'darwin', electronVersion: '11.0.0-beta.5' }, 1, 'Package should be generated for darwin/universal'))
+test('platform=darwin and arch=universal with an unsupported official Electron version', testMultiTarget({ arch: 'universal', platform: 'darwin' }, 0, 'Package should not be generated for darwin/universal'))
+
+test('platform=mas and arch=universal with a supported official Electron version', testMultiTarget({ arch: 'universal', platform: 'mas', electronVersion: '11.0.0-beta.5' }, 1, 'Package should be generated for mas/universal'))
+test('platform=mas and arch=universal with an unsupported official Electron version', testMultiTarget({ arch: 'universal', platform: 'mas' }, 0, 'Package should not be generated for mas/universal'))
+
test('unofficial arch', testMultiTarget({ arch: 'z80', platform: 'linux', download: { mirrorOptions: { mirror: 'mirror' } } }, 1,
'Package should be generated for non-standard arch from non-official mirror'))
test('unofficial platform', testMultiTarget({ arch: 'ia32', platform: 'minix', download: { mirrorOptions: { mirror: 'mirror' } } }, 1,
diff --git a/usage.txt b/usage.txt
index 54b80f9e..c0ff9459 100644
--- a/usage.txt
+++ b/usage.txt
@@ -22,8 +22,9 @@ version prints the version of Electron Packager and Node, plus the ta
all equivalent to --platform=all --arch=all
app-copyright human-readable copyright line for the app
app-version release version to set for the app
-arch all, or one or more of: ia32, x64, armv7l, arm64, mips64el (comma-delimited if
- multiple). Defaults to the host arch
+arch all, or one or more of: ia32, x64, armv7l, arm64, mips64el, universal (comma-delimited if
+ multiple). Defaults to the host arch.
+ For info on arch/platform support see https://github.com/electron/electron-packager/#supported-platforms
asar whether to package the source code within your app into an archive. You can either
pass --asar by itself to use the default configuration, OR use dot notation to
configure a list of sub-properties, e.g. --asar.unpackDir=sub_dir - do not use