Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add Fuses plugin #3132

Merged
merged 13 commits into from
Feb 21, 2023
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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}": [
Expand Down
34 changes: 34 additions & 0 deletions packages/plugin/fuses/README.md
Original file line number Diff line number Diff line change
@@ -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;
```
39 changes: 39 additions & 0 deletions packages/plugin/fuses/package.json
Original file line number Diff line number Diff line change
@@ -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 <erikian@erikian.dev>",
"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"
}
}
45 changes: 45 additions & 0 deletions packages/plugin/fuses/src/FusesPlugin.ts
Original file line number Diff line number Diff line change
@@ -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<FuseConfig> {
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 };
16 changes: 16 additions & 0 deletions packages/plugin/fuses/src/util/getElectronExecutablePath.ts
Original file line number Diff line number Diff line change
@@ -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('')
);
}
67 changes: 67 additions & 0 deletions packages/plugin/fuses/test/FusesPlugin_spec_slow.ts
Original file line number Diff line number Diff line change
@@ -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');
});
});
31 changes: 31 additions & 0 deletions packages/plugin/fuses/test/fixtures/app/forge.config.ts
Original file line number Diff line number Diff line change
@@ -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;
18 changes: 18 additions & 0 deletions packages/plugin/fuses/test/fixtures/app/package.json
Original file line number Diff line number Diff line change
@@ -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",
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if there's a better way to test unreleased plugins

"@electron-forge/shared-types": "6.0.4",
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ESLint throws a node/no-unpublished-import at me when importing some packages if they're under devDependencies, so I figured moving them to dependencies would be better than adding a bunch of es-ignore comments since it's a mock app anyway

"@electron/fuses": "^1.6.1",
"fs-extra": "^10.0.0"
},
"devDependencies": {
"@electron-forge/cli": "6.0.4",
"electron": "12.0.0"
}
}
5 changes: 5 additions & 0 deletions packages/plugin/fuses/test/fixtures/app/src/main.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
const { app } = require('electron');

console.log('The Fuses plugin is working');

app.exit(0);