import type { ForgeConfig } from '@electron-forge/shared-types'; import { MakerSquirrel } from '@electron-forge/maker-squirrel'; import { MakerZIP } from '@electron-forge/maker-zip'; import { MakerDeb } from '@electron-forge/maker-deb'; import { MakerRpm } from '@electron-forge/maker-rpm'; import { AutoUnpackNativesPlugin } from '@electron-forge/plugin-auto-unpack-natives'; import { VitePlugin } from '@electron-forge/plugin-vite'; import { FusesPlugin } from '@electron-forge/plugin-fuses'; import { FuseV1Options, FuseVersion } from '@electron/fuses'; import path from 'node:path'; import fs from 'node:fs'; import { execSync } from 'node:child_process'; // Packages externalized in vite.main.config.mts that must be installed at runtime. // Keep this list in sync with the Vite external array. const externalPackages = [ 'better-sqlite3', '@github/copilot-sdk', '@langchain/core', '@langchain/langgraph', '@langchain/openai', '@langchain/anthropic', 'vectordb', 'electron-squirrel-startup', 'electron-store', ]; const config: ForgeConfig = { packagerConfig: { asar: { unpack: '**/{*.node,*.dll,*.so,*.dylib}', }, name: 'adiuva', }, rebuildConfig: {}, hooks: { packageAfterCopy: async (_forgeConfig, buildPath, _electronVersion, platform, arch) => { // The VitePlugin's ignore filter only copies .vite/ into the build. // Externalized packages need to be installed into node_modules here. // At this point, only .vite/ exists. The VitePlugin writes package.json // in its own afterCopy hook (which may run after ours). Read from source. const srcPjPath = path.resolve(__dirname, 'package.json'); const pjPath = path.resolve(buildPath, 'package.json'); const pj = JSON.parse(fs.readFileSync(srcPjPath, 'utf-8')); // Keep only externalized packages in dependencies const filtered: Record = {}; for (const pkg of externalPackages) { if (pj.dependencies?.[pkg]) { filtered[pkg] = pj.dependencies[pkg]; } } pj.dependencies = filtered; delete pj.devDependencies; fs.writeFileSync(pjPath, JSON.stringify(pj, null, 2)); // Copy lockfile for reproducible installs const lockSrc = path.resolve(buildPath, '..', '..', 'package-lock.json'); if (fs.existsSync(lockSrc)) { fs.copyFileSync(lockSrc, path.resolve(buildPath, 'package-lock.json')); } // Install only the externalized runtime deps console.log('[forge] Installing externalized dependencies...'); execSync('npm install --omit=dev', { cwd: buildPath, stdio: 'inherit', env: { ...process.env, npm_config_nodedir: '' }, }); // When cross-compiling, native addons (better-sqlite3) are built // for the build machine (Linux). Use @electron/rebuild to get the // correct binaries for the target platform. const targetKey = `${platform}-${arch}`; const buildKey = `${process.platform}-${process.arch}`; if (targetKey !== buildKey) { console.log(`[forge] Cross-compiling: ${buildKey} → ${targetKey}`); const electronVersion = JSON.parse( fs.readFileSync(path.resolve(__dirname, 'node_modules', 'electron', 'package.json'), 'utf-8'), ).version; // Rebuild native modules for target platform using prebuild-install. // prebuild-install supports cross-platform via --platform/--arch, // unlike @electron/rebuild which only supports --arch. // This is FATAL — a Linux .node in a Windows package is always broken. const nativeModules = ['better-sqlite3']; for (const mod of nativeModules) { const modDir = path.join(buildPath, 'node_modules', mod); if (!fs.existsSync(modDir)) continue; // Clean stale build artifacts — a leftover build/Release/*.node // for the host platform must not survive. const buildRelease = path.join(modDir, 'build', 'Release'); if (fs.existsSync(buildRelease)) { fs.rmSync(buildRelease, { recursive: true, force: true }); console.log(`[forge] Cleaned stale build/Release for ${mod}`); } console.log(`[forge] Downloading ${mod} prebuilt for ${targetKey} (Electron ${electronVersion})...`); execSync( `npx --yes prebuild-install -r electron -t ${electronVersion} ` + `--platform ${platform} --arch ${arch} --tag-prefix v --verbose`, { cwd: modDir, stdio: 'inherit' }, ); // Verify the downloaded binary is NOT an ELF (Linux) file. // node-gyp-build loads from prebuilds/ first, then build/Release/. const prebuildsTarget = path.join(modDir, 'prebuilds', targetKey); if (fs.existsSync(prebuildsTarget)) { for (const f of fs.readdirSync(prebuildsTarget)) { if (f.endsWith('.node')) { const buf = Buffer.alloc(4); const fd = fs.openSync(path.join(prebuildsTarget, f), 'r'); fs.readSync(fd, buf, 0, 4, 0); fs.closeSync(fd); // ELF magic: 0x7f 'E' 'L' 'F' if (buf[0] === 0x7f && buf[1] === 0x45 && buf[2] === 0x4c && buf[3] === 0x46) { throw new Error( `[forge] FATAL: ${mod} prebuilt for ${targetKey} is an ELF binary! ` + `Cross-compilation failed — refusing to package a Linux .node for Windows.`, ); } console.log(`[forge] Verified ${f} is not ELF ✓`); } } } else { throw new Error( `[forge] FATAL: No prebuilds/${targetKey}/ directory found for ${mod} after prebuild-install. ` + `The native module would fall back to a Linux binary at runtime.`, ); } } } // vectordb uses platform-specific optional deps (@lancedb/vectordb---*). // npm install on Linux only pulls the Linux variant. Force-install the target's. const platformNativePackages: Record> = { 'win32-x64': { '@lancedb/vectordb-win32-x64-msvc': '', }, 'linux-x64': { '@lancedb/vectordb-linux-x64-gnu': '', }, 'darwin-x64': { '@lancedb/vectordb-darwin-x64': '', }, 'darwin-arm64': { '@lancedb/vectordb-darwin-arm64': '', }, }; const nativePkgs = platformNativePackages[targetKey]; if (nativePkgs) { // Remove wrong-platform lancedb native packages const nmPath = path.join(buildPath, 'node_modules', '@lancedb'); if (fs.existsSync(nmPath)) { for (const entry of fs.readdirSync(nmPath)) { if (entry.startsWith('vectordb-') && !Object.keys(nativePkgs).includes(`@lancedb/${entry}`)) { fs.rmSync(path.join(nmPath, entry), { recursive: true, force: true }); console.log(`[forge] Removed non-target native package: @lancedb/${entry}`); } } } // Install correct platform packages const pkgsToInstall = Object.keys(nativePkgs).join(' '); console.log(`[forge] Installing platform-specific packages for ${targetKey}: ${pkgsToInstall}`); execSync(`npm install ${pkgsToInstall} --omit=dev --no-save --force`, { cwd: buildPath, stdio: 'inherit', }); } // Remove cross-platform prebuilt binaries that don't match the target. // Packages like @github/copilot ship prebuilds for all platforms; // keeping foreign-arch .node files breaks rpmbuild's strip step. const nodeModulesPath = path.join(buildPath, 'node_modules'); const findPrebuilds = (dir: string): string[] => { const results: string[] = []; if (!fs.existsSync(dir)) return results; for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { const full = path.join(dir, entry.name); if (entry.isDirectory()) { if (entry.name === 'prebuilds') { results.push(full); } else { results.push(...findPrebuilds(full)); } } } return results; }; for (const prebuildsDir of findPrebuilds(nodeModulesPath)) { for (const entry of fs.readdirSync(prebuildsDir)) { if (entry !== targetKey) { const fullPath = path.join(prebuildsDir, entry); fs.rmSync(fullPath, { recursive: true, force: true }); console.log(`[forge] Removed non-target prebuild: ${entry}`); } } } // @github/copilot ships @teddyzhu/clipboard-* platform packages outside // of prebuilds/. Remove non-target variants to avoid bundling wrong binaries. const clipboardDir = path.join(buildPath, 'node_modules', '@github', 'copilot', 'clipboard', 'node_modules', '@teddyzhu'); if (fs.existsSync(clipboardDir)) { const targetClipboardMap: Record = { 'win32-x64': 'clipboard-win32-x64-msvc', 'win32-arm64': 'clipboard-win32-arm64-msvc', 'linux-x64': 'clipboard-linux-x64-gnu', 'linux-arm64': 'clipboard-linux-arm64-gnu', 'darwin-x64': 'clipboard-darwin-x64', 'darwin-arm64': 'clipboard-darwin-arm64', }; const wantedPkg = targetClipboardMap[targetKey]; for (const entry of fs.readdirSync(clipboardDir)) { if (entry.startsWith('clipboard-') && entry !== wantedPkg) { fs.rmSync(path.join(clipboardDir, entry), { recursive: true, force: true }); console.log(`[forge] Removed non-target clipboard package: @teddyzhu/${entry}`); } } } }, }, makers: [ new MakerSquirrel({}), new MakerZIP({}, ['darwin']), new MakerRpm({}), new MakerDeb({}), ], plugins: [ new AutoUnpackNativesPlugin({}), new VitePlugin({ // `build` can specify multiple entry builds, which can be Main process, Preload scripts, Worker process, etc. // If you are familiar with Vite configuration, it will look really familiar. build: [ { // `entry` is just an alias for `build.lib.entry` in the corresponding file of `config`. entry: 'src/main/index.ts', config: 'vite.main.config.mts', target: 'main', }, { entry: 'src/preload/index.ts', config: 'vite.preload.config.mts', target: 'preload', }, ], renderer: [ { name: 'main_window', config: 'vite.renderer.config.mts', }, ], }), // Fuses are used to enable/disable various Electron functionality // at package time, before code signing the application new FusesPlugin({ version: FuseVersion.V1, [FuseV1Options.RunAsNode]: false, [FuseV1Options.EnableCookieEncryption]: true, [FuseV1Options.EnableNodeOptionsEnvironmentVariable]: false, [FuseV1Options.EnableNodeCliInspectArguments]: false, [FuseV1Options.EnableEmbeddedAsarIntegrityValidation]: true, [FuseV1Options.OnlyLoadAppFromAsar]: true, }), ], }; export default config;