282 lines
12 KiB
TypeScript
282 lines
12 KiB
TypeScript
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<string, string> = {};
|
|
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: '' },
|
|
});
|
|
|
|
const targetKey = `${platform}-${arch}`;
|
|
|
|
// vectordb uses platform-specific optional deps (@lancedb/vectordb-<platform>-<arch>-*).
|
|
// npm install on Linux only pulls the Linux variant. Force-install the target's.
|
|
const platformNativePackages: Record<string, Record<string, string>> = {
|
|
'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<string, string> = {
|
|
'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}`);
|
|
}
|
|
}
|
|
}
|
|
},
|
|
|
|
// ── Post-rebuild: fix native binaries for cross-compilation ──────
|
|
// Forge runs @electron/rebuild AFTER packageAfterCopy, which
|
|
// recompiles native addons for the BUILD platform (Linux).
|
|
// packageAfterPrune runs AFTER rebuild+prune, so we can safely
|
|
// replace the Linux .node files with the correct target prebuilts.
|
|
packageAfterPrune: async (_forgeConfig, buildPath, _electronVersion, platform, arch) => {
|
|
const targetKey = `${platform}-${arch}`;
|
|
const buildKey = `${process.platform}-${process.arch}`;
|
|
if (targetKey === buildKey) return; // native build — nothing to fix
|
|
|
|
console.log(`[forge:afterPrune] Cross-compile fixup: ${buildKey} → ${targetKey}`);
|
|
const electronVersion = JSON.parse(
|
|
fs.readFileSync(path.resolve(__dirname, 'node_modules', 'electron', 'package.json'), 'utf-8'),
|
|
).version;
|
|
|
|
// Replace native addons that @electron/rebuild compiled for the host.
|
|
const nativeModules = ['better-sqlite3'];
|
|
for (const mod of nativeModules) {
|
|
const modDir = path.join(buildPath, 'node_modules', mod);
|
|
if (!fs.existsSync(modDir)) continue;
|
|
|
|
// Remove the host-platform binary left by @electron/rebuild
|
|
const buildRelease = path.join(modDir, 'build', 'Release');
|
|
if (fs.existsSync(buildRelease)) {
|
|
fs.rmSync(buildRelease, { recursive: true, force: true });
|
|
console.log(`[forge:afterPrune] Cleaned host-platform build/Release for ${mod}`);
|
|
}
|
|
|
|
// Download the correct prebuilt for the TARGET platform
|
|
console.log(`[forge:afterPrune] 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 binary exists and is for the correct platform.
|
|
const releaseDir = path.join(modDir, 'build', 'Release');
|
|
if (!fs.existsSync(releaseDir)) {
|
|
throw new Error(
|
|
`[forge] FATAL: build/Release/ not found for ${mod} after prebuild-install. ` +
|
|
`The native binary was not downloaded.`,
|
|
);
|
|
}
|
|
const nodeFiles = fs.readdirSync(releaseDir).filter((f) => f.endsWith('.node'));
|
|
if (nodeFiles.length === 0) {
|
|
throw new Error(
|
|
`[forge] FATAL: No .node files in build/Release/ for ${mod} after prebuild-install.`,
|
|
);
|
|
}
|
|
for (const f of nodeFiles) {
|
|
const buf = Buffer.alloc(4);
|
|
const fd = fs.openSync(path.join(releaseDir, 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} build/Release/${f} is an ELF binary! ` +
|
|
`Cross-compilation failed — refusing to package a Linux .node for Windows.`,
|
|
);
|
|
}
|
|
// PE magic: 'M' 'Z' (0x4d 0x5a) — expected for win32
|
|
if (platform === 'win32' && !(buf[0] === 0x4d && buf[1] === 0x5a)) {
|
|
throw new Error(
|
|
`[forge] FATAL: ${mod} build/Release/${f} is not a PE (Windows) binary! ` +
|
|
`Magic bytes: ${buf.toString('hex')}. Refusing to package.`,
|
|
);
|
|
}
|
|
console.log(`[forge:afterPrune] Verified ${mod}/${f} — correct platform ✓`);
|
|
}
|
|
}
|
|
},
|
|
},
|
|
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;
|