UNPKG

@electron-forge/core

Version:

A complete tool for building modern Electron applications

760 lines (717 loc) 26.5 kB
import path from 'node:path'; import { promisify } from 'node:util'; import { getHostArch } from '@electron/get'; import { FinalizePackageTargetsHookFunction, HookFunction, Options, packager, TargetDefinition, } from '@electron/packager'; import { getElectronVersion, listrCompatibleRebuildHook, } from '@electron-forge/core-utils'; import { ForgeArch, ForgeListrTask, ForgeListrTaskDefinition, ForgeListrTaskFn, ForgePlatform, ResolvedForgeConfig, } from '@electron-forge/shared-types'; import { autoTrace, delayTraceTillSignal } from '@electron-forge/tracer'; import chalk from 'chalk'; import debug from 'debug'; import glob from 'fast-glob'; import fs from 'fs-extra'; import { Listr, PRESET_TIMER } from 'listr2'; import getForgeConfig from '../util/forge-config'; import { getHookListrTasks, runHook } from '../util/hook'; import importSearch from '../util/import-search'; import { warn } from '../util/messages'; import getCurrentOutDir from '../util/out-dir'; import { readMutatedPackageJson } from '../util/read-package-json'; 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`). */ async function resolveHooks<F = HookFunction>( hooks: (string | F)[] | undefined, dir: string, ) { if (hooks) { return await Promise.all( hooks.map(async (hook) => typeof hook === 'string' ? ((await importSearch<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>; /** * @deprecated Only use until \@electron/packager publishes a new major version with promise based hooks */ function hidePromiseFromPromisify<P extends unknown[]>( fn: (...args: P) => Promise<void>, ): (...args: P) => void { return (...args: P) => { void fn(...args); }; } /** * Runs given hooks sequentially by mapping them to promises and iterating * through while awaiting */ function sequentialHooks(hooks: HookFunction[]): PromisifiedHookFunction[] { return [ hidePromiseFromPromisify( 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 [ hidePromiseFromPromisify( 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 = ( childTrace: typeof autoTrace, { 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: childTrace<Parameters<ForgeListrTaskFn<PackageContext>>>( { name: 'package-prepare', category: '@electron-forge/core' }, 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: childTrace<Parameters<ForgeListrTaskFn<PackageContext>>>( { name: 'run-packaging-hooks', category: '@electron-forge/core' }, async (childTrace, { forgeConfig }, task) => { return delayTraceTillSignal( childTrace, task.newListr([ { title: `Running ${chalk.yellow('generateAssets')} hook`, task: childTrace<Parameters<ForgeListrTaskFn>>( { name: 'run-generateAssets-hook', category: '@electron-forge/core', }, async (childTrace, _, task) => { return delayTraceTillSignal( childTrace, task.newListr( await getHookListrTasks( childTrace, forgeConfig, 'generateAssets', platform, arch, ), ), 'run', ); }, ), }, { title: `Running ${chalk.yellow('prePackage')} hook`, task: childTrace<Parameters<ForgeListrTaskFn>>( { name: 'run-prePackage-hook', category: '@electron-forge/core', }, async (childTrace, _, task) => { return delayTraceTillSignal( childTrace, task.newListr( await getHookListrTasks( childTrace, forgeConfig, 'prePackage', platform, arch, ), ), 'run', ); }, ), }, ]), 'run', ); }, ), }, { title: 'Packaging application', task: childTrace<Parameters<ForgeListrTaskFn<PackageContext>>>( { name: 'packaging-application', category: '@electron-forge/core' }, async (childTrace, ctx, task) => { const { calculatedOutDir, forgeConfig, packageJSON } = ctx; const getTargetKey = (target: TargetDefinition) => `${target.platform}/${target.arch}`; task.output = 'Determining targets...'; 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]), ); }); }; let provideTargets: (targets: TargetDefinition[]) => void; const targetsPromise = new Promise<InternalTargetDefinition[]>( (resolve, reject) => { provideTargets = resolve; rejects.push(reject); }, ); 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(); }, ...(await resolveHooks( forgeConfig.packagerConfig.afterFinalizePackageTargets, ctx.dir, )), ]; const pruneEnabled = !('prune' in forgeConfig.packagerConfig) || forgeConfig.packagerConfig.prune; const afterCopyHooks: HookFunction[] = [ hidePromiseFromPromisify( async (buildPath, electronVersion, platform, arch, done) => { signalDone(signalCopyDone, { platform, arch }); done(); }, ), hidePromiseFromPromisify( async (buildPath, electronVersion, pPlatform, pArch, done) => { const bins = await glob(path.join(buildPath, '**/.bin/**/*')); for (const bin of bins) { await fs.remove(bin); } done(); }, ), hidePromiseFromPromisify( async (buildPath, electronVersion, pPlatform, pArch, done) => { await runHook( forgeConfig, 'packageAfterCopy', buildPath, electronVersion, pPlatform, pArch, ); done(); }, ), hidePromiseFromPromisify( 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(); }, ), hidePromiseFromPromisify( 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(); }, ), ...(await resolveHooks( forgeConfig.packagerConfig.afterCopy, ctx.dir, )), ]; const afterCompleteHooks: HookFunction[] = [ hidePromiseFromPromisify( async (buildPath, electronVersion, pPlatform, pArch, done) => { signalPackageDone .get(getTargetKey({ platform: pPlatform, arch: pArch })) ?.pop()?.(); done(); }, ), ...(await resolveHooks( forgeConfig.packagerConfig.afterComplete, ctx.dir, )), ]; const afterPruneHooks = []; if (pruneEnabled) { afterPruneHooks.push( ...(await resolveHooks( forgeConfig.packagerConfig.afterPrune, ctx.dir, )), ); } afterPruneHooks.push( hidePromiseFromPromisify( async (buildPath, electronVersion, pPlatform, pArch, done) => { await runHook( forgeConfig, 'packageAfterPrune', buildPath, electronVersion, pPlatform, pArch, ); done(); }, ) as HookFunction, ); const afterExtractHooks = [ hidePromiseFromPromisify( async (buildPath, electronVersion, pPlatform, pArch, done) => { await runHook( forgeConfig, 'packageAfterExtract', buildPath, electronVersion, pPlatform, pArch, ); done(); }, ) as HookFunction, ]; afterExtractHooks.push( ...(await resolveHooks( forgeConfig.packagerConfig.afterExtract, ctx.dir, )), ); type PackagerArch = Exclude<ForgeArch, 'arm'>; const packageOpts: Options = { asar: false, overwrite: true, ignore: [/^\/out\//g], quiet: true, ...forgeConfig.packagerConfig, 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), }; 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 delayTraceTillSignal( childTrace, 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); }, rendererOptions: { timer: { ...PRESET_TIMER }, }, } : { title: `Packaging for ${chalk.cyan(target.arch)} on ${chalk.cyan(target.platform)}${ target.forUniversal ? chalk.italic(' (for universal package)') : '' }`, task: childTrace<Parameters<ForgeListrTaskFn<never>>>( { name: `package-app-${target.platform}-${target.arch}${target.forUniversal ? '-universal-tmp' : ''}`, category: '@electron-forge/core', extraDetails: { arch: target.arch, platform: target.platform, }, newRoot: true, }, async (childTrace, _, task) => { return delayTraceTillSignal( childTrace, task.newListr( [ { title: 'Copying files', task: childTrace( { name: 'copy-files', category: '@electron-forge/core', }, async () => { await addSignalAndWait( signalCopyDone, target, ); }, ), }, { title: 'Preparing native dependencies', task: childTrace( { name: 'prepare-native-dependencies', category: '@electron-forge/core', }, async (_, __, task) => { signalRebuildStart .get(getTargetKey(target)) ?.pop()?.(task); await addSignalAndWait( signalRebuildDone, target, ); }, ), rendererOptions: { persistentOutput: true, bottomBar: Infinity, timer: { ...PRESET_TIMER }, }, }, { title: 'Finalizing package', task: childTrace( { name: 'finalize-package', category: '@electron-forge/core', }, async () => { await addSignalAndWait( signalPackageDone, target, ); }, ), }, ], { rendererOptions: { collapseSubtasks: true, collapseErrors: false, }, }, ), 'run', ); }, ), rendererOptions: { timer: { ...PRESET_TIMER }, }, }, ), { concurrent: true, rendererOptions: { collapseSubtasks: false, collapseErrors: false, }, }, ), 'run', ); }, ), }, { title: `Running ${chalk.yellow('postPackage')} hook`, task: childTrace<Parameters<ForgeListrTaskFn<PackageContext>>>( { name: 'run-postPackage-hook', category: '@electron-forge/core' }, async (childTrace, { packagerPromise, forgeConfig }, task) => { const outputPaths = await packagerPromise; d('outputPaths:', outputPaths); return delayTraceTillSignal( childTrace, task.newListr( await getHookListrTasks( childTrace, forgeConfig, 'postPackage', { arch, outputPaths, platform, }, ), ), 'run', ); }, ), }, ], { concurrent: false, silentRendererCondition: !interactive, fallbackRendererCondition: Boolean(process.env.DEBUG) || Boolean(process.env.CI), rendererOptions: { collapseSubtasks: false, collapseErrors: false, }, ctx: {} as PackageContext, }, ); return runner; }; export default autoTrace( { name: 'package()', category: '@electron-forge/core' }, async (childTrace, opts: PackageOptions): Promise<PackageResult[]> => { const runner = listrPackage(childTrace, 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], })); }, );