Skip to content

Commit

Permalink
feat: integrate @electron/universal (#1346)
Browse files Browse the repository at this point in the history
* feat: integrate @electron/universal

* chore: fix platform typo

* spec: add universal generation tests

* spec: fix tests

* fix: add universal to supported list for mas

* spec: fix tests

* spec: fix tests

* docs: update usage

* Update README.md

* Update usage.txt

Co-authored-by: Erick Zhao <erick@hotmail.ca>
  • Loading branch information
MarshallOfSound and erickzhao authored Apr 19, 2022
1 parent b66e97f commit c6849c2
Show file tree
Hide file tree
Showing 11 changed files with 137 additions and 23 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`)<sup>*</sup> (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`)<sup>*</sup> (for x86_64, arm64, and universal architectures)
* Linux (for x86, x86_64, armv7l, arm64, and mips64el architectures)

<sup>*</sup> *Note for macOS / Mac App Store target bundles: the `.app` bundle can only be signed when building on a host macOS platform.*
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
15 changes: 15 additions & 0 deletions src/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof makeUniversalApp>[0]

type NotarizeLegacyOptions = LegacyNotarizeCredentials & TransporterOptions;

Expand Down Expand Up @@ -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<MakeUniversalOpts, 'x64AppPath' | 'arm64AppPath' | 'outAppPath' | 'force'>

/**
* Defines URL protocol schemes to be used on macOS.
*/
Expand Down Expand Up @@ -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.
*
Expand Down
34 changes: 24 additions & 10 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down Expand Up @@ -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}`)
Expand Down Expand Up @@ -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)
Expand All @@ -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) {
Expand Down
12 changes: 7 additions & 5 deletions src/targets.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
75 changes: 75 additions & 0 deletions src/universal.js
Original file line number Diff line number Diff line change
@@ -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
}
2 changes: 1 addition & 1 deletion test/_setup.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'
})
Expand Down
2 changes: 1 addition & 1 deletion test/_util.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion test/basic.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 8 additions & 2 deletions test/targets.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'))
Expand Down Expand Up @@ -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,
Expand Down
5 changes: 3 additions & 2 deletions usage.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit c6849c2

Please sign in to comment.