diff --git a/package.json b/package.json index 0b69f75e50..1de476aa95 100644 --- a/package.json +++ b/package.json @@ -77,6 +77,7 @@ "yarn-or-npm": "^3.0.1" }, "devDependencies": { + "@electron/fuses": ">=1.0.0", "@knodes/typedoc-plugin-monorepo-readmes": "0.22.5", "@malept/eslint-config": "^2.0.0", "@types/chai": "^4.2.12", @@ -150,6 +151,9 @@ "electron-wix-msi": "^5.0.0", "macos-alias": "^0.2.11" }, + "peerDependencies": { + "@electron/fuses": ">=1.0.0" + }, "lint-staged": { "*.{html,json,md,yml}": "prettier --write", "*.{js,ts}": [ diff --git a/packages/plugin/fuses/README.md b/packages/plugin/fuses/README.md new file mode 100644 index 0000000000..3d93ab4d57 --- /dev/null +++ b/packages/plugin/fuses/README.md @@ -0,0 +1,34 @@ +@electron-forge/plugin-fuses +===== + +This plugin allows flipping [Electron Fuses](https://github.com/electron/fuses) when packaging your app with Electron Forge. + +## Usage + +Install `@electron-forge/plugin-fuses` and `@electron/fuses` as dev dependencies and add this plugin to the `plugins` array in your Forge configuration:: +```shell +# Yarn +yarn add --dev @electron-forge/plugin-fuses @electron/fuses + +# npm +npm i -D @electron-forge/plugin-fuses @electron/fuses +``` + +```js +// forge.config.js + +const { FusesPlugin } = require('@electron-forge/plugin-fuses'); +const { FuseV1Options, FuseVersion } = require('@electron/fuses'); + +const forgeConfig = { + plugins: [ + new FusesPlugin({ + version: FuseVersion.V1, + [FuseV1Options.RunAsNode]: false, + // ...any other options supported by @electron/fuses + }), + ], +}; + +module.exports = forgeConfig; +``` diff --git a/packages/plugin/fuses/package.json b/packages/plugin/fuses/package.json new file mode 100644 index 0000000000..25d8db1799 --- /dev/null +++ b/packages/plugin/fuses/package.json @@ -0,0 +1,39 @@ +{ + "name": "@electron-forge/plugin-fuses", + "version": "6.0.4", + "description": "A plugin for flipping Electron Fuses in Electron Forge", + "repository": "https://github.com/electron/forge", + "author": "Erik Moura ", + "license": "MIT", + "main": "dist/FusesPlugin.js", + "files": [ + "dist", + "package.json", + "README.md" + ], + "typings": "dist/FusesPlugin.d.ts", + "scripts": { + "test": "xvfb-maybe mocha --config ../../../.mocharc.js test/**/*_spec*.ts" + }, + "devDependencies": { + "@electron/fuses": ">=1.0.0", + "@malept/cross-spawn-promise": "^2.0.0", + "chai": "^4.3.3", + "fs-extra": "^10.0.0", + "mocha": "^9.0.1", + "xvfb-maybe": "^0.2.1" + }, + "peerDependencies": { + "@electron/fuses": ">=1.0.0" + }, + "engines": { + "node": ">= 14.17.5" + }, + "dependencies": { + "@electron-forge/plugin-base": "6.0.4", + "@electron-forge/shared-types": "6.0.4" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/plugin/fuses/src/FusesPlugin.ts b/packages/plugin/fuses/src/FusesPlugin.ts new file mode 100644 index 0000000000..392ccdac80 --- /dev/null +++ b/packages/plugin/fuses/src/FusesPlugin.ts @@ -0,0 +1,45 @@ +import path from 'path'; + +import { namedHookWithTaskFn, PluginBase } from '@electron-forge/plugin-base'; +import { ForgeMultiHookMap } from '@electron-forge/shared-types'; +import { flipFuses, FuseConfig } from '@electron/fuses'; + +import { getElectronExecutablePath } from './util/getElectronExecutablePath'; + +export default class FusesPlugin extends PluginBase { + name = 'fuses'; + + fusesConfig = {} as FuseConfig; + + constructor(fusesConfig: FuseConfig) { + super(fusesConfig); + + this.fusesConfig = fusesConfig; + } + + getHooks(): ForgeMultiHookMap { + return { + packageAfterCopy: namedHookWithTaskFn<'packageAfterCopy'>(async (listrTask, resolvedForgeConfig, resourcesPath, electronVersion, platform, arch) => { + const { fusesConfig } = this; + + if (Object.keys(fusesConfig).length) { + const pathToElectronExecutable = getElectronExecutablePath({ + appName: ['darwin', 'mas'].includes(platform) ? 'Electron' : 'electron', + basePath: path.resolve(resourcesPath, '../..'), + platform, + }); + + const osxSignConfig = resolvedForgeConfig.packagerConfig.osxSign; + const hasOSXSignConfig = (typeof osxSignConfig === 'object' && Boolean(Object.keys(osxSignConfig).length)) || Boolean(osxSignConfig); + + await flipFuses(pathToElectronExecutable, { + resetAdHocDarwinSignature: !hasOSXSignConfig && platform === 'darwin' && arch === 'arm64', + ...this.fusesConfig, + }); + } + }, 'Flipping Fuses'), + }; + } +} + +export { FusesPlugin }; diff --git a/packages/plugin/fuses/src/util/getElectronExecutablePath.ts b/packages/plugin/fuses/src/util/getElectronExecutablePath.ts new file mode 100644 index 0000000000..003a542b85 --- /dev/null +++ b/packages/plugin/fuses/src/util/getElectronExecutablePath.ts @@ -0,0 +1,16 @@ +import path from 'path'; + +import { ForgePlatform } from '@electron-forge/shared-types'; + +type GetElectronExecutablePathParams = { + appName: string; + basePath: string; + platform: ForgePlatform; +}; + +export function getElectronExecutablePath({ appName, basePath, platform }: GetElectronExecutablePathParams): string { + return path.join( + basePath, + ['darwin', 'mas'].includes(platform) ? path.join('MacOS', appName) : [appName, process.platform === 'win32' ? '.exe' : ''].join('') + ); +} diff --git a/packages/plugin/fuses/test/FusesPlugin_spec_slow.ts b/packages/plugin/fuses/test/FusesPlugin_spec_slow.ts new file mode 100644 index 0000000000..3a4e1e0d5e --- /dev/null +++ b/packages/plugin/fuses/test/FusesPlugin_spec_slow.ts @@ -0,0 +1,67 @@ +import fs from 'fs'; +import path from 'path'; + +import { CrossSpawnOptions, spawn } from '@malept/cross-spawn-promise'; +import { expect } from 'chai'; +import fsExtra from 'fs-extra'; + +import { getElectronExecutablePath } from '../src/util/getElectronExecutablePath'; + +describe('FusesPlugin', () => { + const appPath = path.join(__dirname, 'fixtures', 'app'); + + const spawnOptions: CrossSpawnOptions = { + cwd: appPath, + shell: true, + }; + + const packageJSON = JSON.parse( + fs.readFileSync(path.join(appPath, 'package.json'), { + encoding: 'utf-8', + }) + ); + + const { name: appName } = packageJSON; + + // @TODO get rid of this once https://github.com/electron/forge/pull/3123 lands + const platformArchSuffix = `${process.platform}-x64`; + + const outDir = path.join(appPath, 'out', `${appName}-${platformArchSuffix}`); + + before(async () => { + delete process.env.TS_NODE_PROJECT; + await spawn('yarn', ['install'], spawnOptions); + }); + + after(async () => { + await fsExtra.remove(path.resolve(outDir, '../')); + + // @TODO this can be removed once the mock app installs a published version of @electron-forge/plugin-fuses instead of a local package + await fsExtra.remove(path.join(__dirname, './fixtures/app/node_modules')); + }); + + it('should flip Fuses', async () => { + await spawn('yarn', ['package'], spawnOptions); + + const electronExecutablePath = getElectronExecutablePath({ + appName, + basePath: path.join(outDir, ...(process.platform === 'darwin' ? [`${appName}.app`, 'Contents'] : [])), + platform: process.platform, + }); + + /** + * If the `RunAsNode` fuse had not been flipped, + * this would return the Node.js version (e.g. `v14.16.0`) + * instead of the `console.log` from `main.js`. + */ + const output = ( + await spawn(electronExecutablePath, ['-v'], { + env: { + ELECTRON_RUN_AS_NODE: '1', + }, + }) + ).trim(); + + expect(output).to.equals('The Fuses plugin is working'); + }); +}); diff --git a/packages/plugin/fuses/test/fixtures/app/forge.config.ts b/packages/plugin/fuses/test/fixtures/app/forge.config.ts new file mode 100644 index 0000000000..cbdc03c5ef --- /dev/null +++ b/packages/plugin/fuses/test/fixtures/app/forge.config.ts @@ -0,0 +1,31 @@ +import path from 'path'; + +import { FusesPlugin } from '@electron-forge/plugin-fuses'; +import { ForgeConfig } from '@electron-forge/shared-types'; +import { FuseV1Options, FuseVersion } from '@electron/fuses'; +import fsExtra from 'fs-extra'; + +const forgeConfig: ForgeConfig = { + packagerConfig: { + afterComplete: [ + // makes tests a bit simpler by having a single output directory in every platform/arch + async (packagedAppLocation, _electronVersion, _targetPlatform, _targetArch, done) => { + const parentDir = path.resolve(packagedAppLocation, '..'); + await fsExtra.move(packagedAppLocation, path.join(parentDir, 'fuses-test-app'), { + overwrite: true, + }); + + done(); + }, + ], + }, + + plugins: [ + new FusesPlugin({ + version: FuseVersion.V1, + [FuseV1Options.RunAsNode]: false, + }), + ], +}; + +export default forgeConfig; diff --git a/packages/plugin/fuses/test/fixtures/app/package.json b/packages/plugin/fuses/test/fixtures/app/package.json new file mode 100644 index 0000000000..e018e85766 --- /dev/null +++ b/packages/plugin/fuses/test/fixtures/app/package.json @@ -0,0 +1,18 @@ +{ + "name": "fuses-test-app", + "version": "1.0.0", + "main": "./src/main.js", + "scripts": { + "package": "electron-forge package" + }, + "dependencies": { + "@electron-forge/plugin-fuses": "file:./../../../../fuses", + "@electron-forge/shared-types": "6.0.4", + "@electron/fuses": "^1.6.1", + "fs-extra": "^10.0.0" + }, + "devDependencies": { + "@electron-forge/cli": "6.0.4", + "electron": "12.0.0" + } +} diff --git a/packages/plugin/fuses/test/fixtures/app/src/main.js b/packages/plugin/fuses/test/fixtures/app/src/main.js new file mode 100644 index 0000000000..3ef07df595 --- /dev/null +++ b/packages/plugin/fuses/test/fixtures/app/src/main.js @@ -0,0 +1,5 @@ +const { app } = require('electron'); + +console.log('The Fuses plugin is working'); + +app.exit(0);