UNPKG

@electron-forge/core

Version:

A complete tool for building modern Electron applications

457 lines (414 loc) 17.3 kB
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], })); };