@electron-forge/core
Version:
A complete tool for building modern Electron applications
457 lines (414 loc) • 17.3 kB
text/typescript
import path from 'path';
import { promisify } from 'util';
import { getElectronVersion, listrCompatibleRebuildHook } from '@electron-forge/core-utils';
import { ForgeArch, ForgeListrTask, ForgeListrTaskDefinition, ForgePlatform, ResolvedForgeConfig } from '@electron-forge/shared-types';
import { getHostArch } from '@electron/get';
import chalk from 'chalk';
import debug from 'debug';
import packager, { FinalizePackageTargetsHookFunction, HookFunction, TargetDefinition } from 'electron-packager';
import glob from 'fast-glob';
import fs from 'fs-extra';
import { Listr } from 'listr2';
import getForgeConfig from '../util/forge-config';
import { getHookListrTasks, runHook } from '../util/hook';
import { warn } from '../util/messages';
import getCurrentOutDir from '../util/out-dir';
import { readMutatedPackageJson } from '../util/read-package-json';
import requireSearch from '../util/require-search';
import resolveDir from '../util/resolve-dir';
const d = debug('electron-forge:packager');
/**
* Resolves hooks if they are a path to a file (instead of a `Function`).
*/
function resolveHooks<F = HookFunction>(hooks: (string | F)[] | undefined, dir: string) {
if (hooks) {
return hooks.map((hook) => (typeof hook === 'string' ? (requireSearch<F>(dir, [hook]) as F) : hook));
}
return [];
}
type DoneFunction = (err?: Error) => void;
type PromisifiedHookFunction = (buildPath: string, electronVersion: string, platform: string, arch: string) => Promise<void>;
type PromisifiedFinalizePackageTargetsHookFunction = (targets: TargetDefinition[]) => Promise<void>;
/**
* Runs given hooks sequentially by mapping them to promises and iterating
* through while awaiting
*/
function sequentialHooks(hooks: HookFunction[]): PromisifiedHookFunction[] {
return [
async (buildPath: string, electronVersion: string, platform: string, arch: string, done: DoneFunction) => {
for (const hook of hooks) {
try {
await promisify(hook)(buildPath, electronVersion, platform, arch);
} catch (err) {
d('hook failed:', hook.toString(), err);
return done(err as Error);
}
}
done();
},
] as PromisifiedHookFunction[];
}
function sequentialFinalizePackageTargetsHooks(hooks: FinalizePackageTargetsHookFunction[]): PromisifiedFinalizePackageTargetsHookFunction[] {
return [
async (targets: TargetDefinition[], done: DoneFunction) => {
for (const hook of hooks) {
try {
await promisify(hook)(targets);
} catch (err) {
return done(err as Error);
}
}
done();
},
] as PromisifiedFinalizePackageTargetsHookFunction[];
}
type PackageContext = {
dir: string;
forgeConfig: ResolvedForgeConfig;
packageJSON: any;
calculatedOutDir: string;
packagerPromise: Promise<string[]>;
targets: InternalTargetDefinition[];
};
type InternalTargetDefinition = TargetDefinition & {
forUniversal?: boolean;
};
type PackageResult = TargetDefinition & {
packagedPath: string;
};
export interface PackageOptions {
/**
* The path to the app to package
*/
dir?: string;
/**
* Whether to use sensible defaults or prompt the user visually
*/
interactive?: boolean;
/**
* The target arch
*/
arch?: ForgeArch;
/**
* The target platform.
*/
platform?: ForgePlatform;
/**
* The path to the output directory for packaged apps
*/
outDir?: string;
}
export const listrPackage = ({
dir: providedDir = process.cwd(),
interactive = false,
arch = getHostArch() as ForgeArch,
platform = process.platform as ForgePlatform,
outDir,
}: PackageOptions) => {
const runner = new Listr<PackageContext>(
[
{
title: 'Preparing to package application',
task: async (ctx) => {
const resolvedDir = await resolveDir(providedDir);
if (!resolvedDir) {
throw new Error('Failed to locate compilable Electron application');
}
ctx.dir = resolvedDir;
ctx.forgeConfig = await getForgeConfig(resolvedDir);
ctx.packageJSON = await readMutatedPackageJson(resolvedDir, ctx.forgeConfig);
if (!ctx.packageJSON.main) {
throw new Error('packageJSON.main must be set to a valid entry point for your Electron app');
}
ctx.calculatedOutDir = outDir || getCurrentOutDir(resolvedDir, ctx.forgeConfig);
},
},
{
title: 'Running packaging hooks',
task: async ({ forgeConfig }, task) => {
return task.newListr([
{
title: `Running ${chalk.yellow('generateAssets')} hook`,
task: async (_, task) => {
return task.newListr(await getHookListrTasks(forgeConfig, 'generateAssets', platform, arch));
},
},
{
title: `Running ${chalk.yellow('prePackage')} hook`,
task: async (_, task) => {
return task.newListr(await getHookListrTasks(forgeConfig, 'prePackage', platform, arch));
},
},
]);
},
},
{
title: 'Packaging application',
task: async (ctx, task) => {
const { calculatedOutDir, forgeConfig, packageJSON } = ctx;
const getTargetKey = (target: TargetDefinition) => `${target.platform}/${target.arch}`;
task.output = 'Determining targets...';
let provideTargets: (targets: TargetDefinition[]) => void;
const targetsPromise = new Promise<InternalTargetDefinition[]>((resolve) => {
provideTargets = resolve;
});
type StepDoneSignalMap = Map<string, (() => void)[]>;
const signalCopyDone: StepDoneSignalMap = new Map();
const signalRebuildDone: StepDoneSignalMap = new Map();
const signalPackageDone: StepDoneSignalMap = new Map();
const rejects: ((err: any) => void)[] = [];
const signalDone = (map: StepDoneSignalMap, target: TargetDefinition) => {
map.get(getTargetKey(target))?.pop()?.();
};
const addSignalAndWait = async (map: StepDoneSignalMap, target: TargetDefinition) => {
const targetKey = getTargetKey(target);
await new Promise<void>((resolve, reject) => {
rejects.push(reject);
map.set(targetKey, (map.get(targetKey) || []).concat([resolve]));
});
};
const rebuildTasks = new Map<string, Promise<ForgeListrTask<never>>[]>();
const signalRebuildStart = new Map<string, ((task: ForgeListrTask<never>) => void)[]>();
const afterFinalizePackageTargetsHooks: FinalizePackageTargetsHookFunction[] = [
(targets, done) => {
provideTargets(targets);
done();
},
...resolveHooks(forgeConfig.packagerConfig.afterFinalizePackageTargets, ctx.dir),
];
const pruneEnabled = !('prune' in forgeConfig.packagerConfig) || forgeConfig.packagerConfig.prune;
const afterCopyHooks: HookFunction[] = [
async (buildPath, electronVersion, platform, arch, done) => {
signalDone(signalCopyDone, { platform, arch });
done();
},
async (buildPath, electronVersion, pPlatform, pArch, done) => {
const bins = await glob(path.join(buildPath, '**/.bin/**/*'));
for (const bin of bins) {
await fs.remove(bin);
}
done();
},
async (buildPath, electronVersion, pPlatform, pArch, done) => {
await runHook(forgeConfig, 'packageAfterCopy', buildPath, electronVersion, pPlatform, pArch);
done();
},
async (buildPath, electronVersion, pPlatform, pArch, done) => {
const targetKey = getTargetKey({ platform: pPlatform, arch: pArch });
await listrCompatibleRebuildHook(
buildPath,
electronVersion,
pPlatform,
pArch,
forgeConfig.rebuildConfig,
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
await rebuildTasks.get(targetKey)!.pop()!
);
signalRebuildDone.get(targetKey)?.pop()?.();
done();
},
async (buildPath, electronVersion, pPlatform, pArch, done) => {
const copiedPackageJSON = await readMutatedPackageJson(buildPath, forgeConfig);
if (copiedPackageJSON.config && copiedPackageJSON.config.forge) {
delete copiedPackageJSON.config.forge;
}
await fs.writeJson(path.resolve(buildPath, 'package.json'), copiedPackageJSON, { spaces: 2 });
done();
},
...resolveHooks(forgeConfig.packagerConfig.afterCopy, ctx.dir),
];
const afterCompleteHooks: HookFunction[] = [
async (buildPath, electronVersion, pPlatform, pArch, done) => {
signalPackageDone.get(getTargetKey({ platform: pPlatform, arch: pArch }))?.pop()?.();
done();
},
];
const afterPruneHooks = [];
if (pruneEnabled) {
afterPruneHooks.push(...resolveHooks(forgeConfig.packagerConfig.afterPrune, ctx.dir));
}
afterPruneHooks.push((async (buildPath, electronVersion, pPlatform, pArch, done) => {
await runHook(forgeConfig, 'packageAfterPrune', buildPath, electronVersion, pPlatform, pArch);
done();
}) as HookFunction);
const afterExtractHooks = [
(async (buildPath, electronVersion, pPlatform, pArch, done) => {
await runHook(forgeConfig, 'packageAfterExtract', buildPath, electronVersion, pPlatform, pArch);
done();
}) as HookFunction,
];
afterExtractHooks.push(...resolveHooks(forgeConfig.packagerConfig.afterExtract, ctx.dir));
type PackagerArch = Exclude<ForgeArch, 'arm'>;
const packageOpts: packager.Options = {
asar: false,
overwrite: true,
ignore: [/^\/out\//g],
...forgeConfig.packagerConfig,
quiet: true,
dir: ctx.dir,
arch: arch as PackagerArch,
platform,
afterFinalizePackageTargets: sequentialFinalizePackageTargetsHooks(afterFinalizePackageTargetsHooks),
afterComplete: sequentialHooks(afterCompleteHooks),
afterCopy: sequentialHooks(afterCopyHooks),
afterExtract: sequentialHooks(afterExtractHooks),
afterPrune: sequentialHooks(afterPruneHooks),
out: calculatedOutDir,
electronVersion: await getElectronVersion(ctx.dir, packageJSON),
};
packageOpts.quiet = true;
if (packageOpts.all) {
throw new Error('config.forge.packagerConfig.all is not supported by Electron Forge');
}
if (!packageJSON.version && !packageOpts.appVersion) {
warn(
interactive,
chalk.yellow('Please set "version" or "config.forge.packagerConfig.appVersion" in your application\'s package.json so auto-updates work properly')
);
}
if (packageOpts.prebuiltAsar) {
throw new Error('config.forge.packagerConfig.prebuiltAsar is not supported by Electron Forge');
}
d('packaging with options', packageOpts);
ctx.packagerPromise = packager(packageOpts);
// Handle error by failing this task
// rejects is populated by the reject handlers for every
// signal based promise in every subtask
ctx.packagerPromise.catch((err) => {
for (const reject of rejects) reject(err);
});
const targets = await targetsPromise;
// Copy the resolved targets into the context for later
ctx.targets = [...targets];
// If we are targetting a universal build we need to add the "fake"
// x64 and arm64 builds into the list of targets so that we can
// show progress for those
for (const target of targets) {
if (target.arch === 'universal') {
targets.push(
{
platform: target.platform,
arch: 'x64',
forUniversal: true,
},
{
platform: target.platform,
arch: 'arm64',
forUniversal: true,
}
);
}
}
// Populate rebuildTasks with promises that resolve with the rebuild tasks
// that will eventually run
for (const target of targets) {
// Skip universal tasks as they do not have rebuild sub-tasks
if (target.arch === 'universal') continue;
const targetKey = getTargetKey(target);
rebuildTasks.set(
targetKey,
(rebuildTasks.get(targetKey) || []).concat([
new Promise((resolve) => {
signalRebuildStart.set(targetKey, (signalRebuildStart.get(targetKey) || []).concat([resolve]));
}),
])
);
}
d('targets:', targets);
return task.newListr(
targets.map(
(target): ForgeListrTaskDefinition =>
target.arch === 'universal'
? {
title: `Stitching ${chalk.cyan(`${target.platform}/x64`)} and ${chalk.cyan(`${target.platform}/arm64`)} into a ${chalk.green(
`${target.platform}/universal`
)} package`,
task: async () => {
await addSignalAndWait(signalPackageDone, target);
},
options: {
showTimer: true,
},
}
: {
title: `Packaging for ${chalk.cyan(target.arch)} on ${chalk.cyan(target.platform)}${
target.forUniversal ? chalk.italic(' (for universal package)') : ''
}`,
task: async (_, task) => {
return task.newListr(
[
{
title: 'Copying files',
task: async () => {
await addSignalAndWait(signalCopyDone, target);
},
},
{
title: 'Preparing native dependencies',
task: async (_, task) => {
signalRebuildStart.get(getTargetKey(target))?.pop()?.(task);
await addSignalAndWait(signalRebuildDone, target);
},
options: {
persistentOutput: true,
bottomBar: Infinity,
showTimer: true,
},
},
{
title: 'Finalizing package',
task: async () => {
await addSignalAndWait(signalPackageDone, target);
},
},
],
{ rendererOptions: { collapse: true, collapseErrors: false } }
);
},
options: {
showTimer: true,
},
}
),
{ concurrent: true, rendererOptions: { collapse: false, collapseErrors: false } }
);
},
},
{
title: `Running ${chalk.yellow('postPackage')} hook`,
task: async ({ packagerPromise, forgeConfig }, task) => {
const outputPaths = await packagerPromise;
d('outputPaths:', outputPaths);
return task.newListr(
await getHookListrTasks(forgeConfig, 'postPackage', {
arch,
outputPaths,
platform,
})
);
},
},
],
{
concurrent: false,
rendererSilent: !interactive,
rendererFallback: Boolean(process.env.DEBUG && process.env.DEBUG.includes('electron-forge')),
rendererOptions: {
collapse: false,
collapseErrors: false,
},
ctx: {} as PackageContext,
}
);
return runner;
};
export default async (opts: PackageOptions): Promise<PackageResult[]> => {
const runner = listrPackage(opts);
await runner.run();
const outputPaths = await runner.ctx.packagerPromise;
return runner.ctx.targets.map((target, index) => ({
platform: target.platform,
arch: target.arch,
packagedPath: outputPaths[index],
}));
};