import { cpSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs' import path from 'node:path' import { fileURLToPath } from 'node:url' const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..') const electrobunDir = path.join(rootDir, 'node_modules/electrobun') const sharedSrcDir = path.join(electrobunDir, 'src/shared') const sharedDistDir = path.join(electrobunDir, 'dist/api/shared') if (!existsSync(sharedDistDir)) { console.error('patch-electrobun-src: electrobun dist/api/shared not found') process.exit(1) } mkdirSync(sharedSrcDir, { recursive: true }) for (const fileName of [ 'cef-version.ts', 'bun-version.ts', 'electrobun-version.ts', 'naming.ts', 'rpc.ts' ]) { const source = path.join(sharedDistDir, fileName) const destination = path.join(sharedSrcDir, fileName) if (existsSync(source)) { cpSync(source, destination) } } const platformPatch = `import { platform, arch } from "os"; export type SupportedOS = "macos" | "win" | "linux"; export type SupportedArch = "arm64" | "x64"; const platformName = platform(); const archName = arch(); export const OS: SupportedOS = (() => { switch (platformName) { case "win32": return "win"; case "darwin": return "macos"; case "linux": return "linux"; default: throw new Error(\`Unsupported platform: \${platformName}\`); } })(); function normalizeForcedArch(value) { if (value === "arm64" || value === "aarch64") { return "arm64"; } if (value === "x64" || value === "amd64") { return "x64"; } return null; } export const ARCH: SupportedArch = (() => { const forcedArch = normalizeForcedArch( process.env.ELECTROBUN_FORCE_ARCH || process.env.ELECTROBUN_TARGET_ARCH, ); if (forcedArch) { return forcedArch; } if (OS === "win") { return "x64"; } switch (archName) { case "arm64": return "arm64"; case "x64": return "x64"; default: throw new Error(\`Unsupported architecture: \${archName}\`); } })(); export const HOST_ARCH: SupportedArch = (() => { if (OS === "win") { return "x64"; } switch (archName) { case "arm64": return "arm64"; case "x64": return "x64"; default: throw new Error(\`Unsupported architecture: \${archName}\`); } })(); export function getPlatformOS(): SupportedOS { return OS; } export function getPlatformArch(): SupportedArch { return ARCH; } ` writeFileSync(path.join(sharedSrcDir, 'platform.ts'), platformPatch) const templatesDir = path.join(electrobunDir, 'src/cli/templates') mkdirSync(templatesDir, { recursive: true }) writeFileSync( path.join(templatesDir, 'embedded.ts'), `export function getTemplateNames() { return []; } export function getTemplate(name: string) { throw new Error(\`Template not available in patched electrobun CLI: \${name}\`); } ` ) const RCEDIT_RESOLVER_FN = `function resolveProjectRceditPkgPath(projectRoot) { const candidates = [ join(projectRoot, "node_modules/rcedit/package.json"), join(projectRoot, "node_modules/electrobun/node_modules/rcedit/package.json"), ]; for (const candidate of candidates) { if (existsSync(candidate)) { return candidate; } } throw new Error( "rcedit not found under " + projectRoot + ". Install rcedit in the project (bun add -d rcedit).", ); }` const cliPath = path.join(electrobunDir, 'src/cli/index.ts') let cliSource = readFileSync(cliPath, 'utf8') if (!cliSource.includes('HOST_ARCH')) { cliSource = cliSource.replace( 'import { OS, ARCH } from "../shared/platform";', 'import { OS, ARCH, HOST_ARCH } from "../shared/platform";' ) cliSource = cliSource.replace( 'const hostPaths = getPlatformPaths(OS, ARCH);', 'const hostPaths = getPlatformPaths(OS, HOST_ARCH);' ) } if (!cliSource.includes('toolPaths')) { cliSource = cliSource.replace( 'const targetPaths = getPlatformPaths(currentTarget.os, currentTarget.arch);', 'const targetPaths = getPlatformPaths(currentTarget.os, currentTarget.arch);\n\t\tconst toolPaths = getPlatformPaths(currentTarget.os, ARCH !== HOST_ARCH ? HOST_ARCH : ARCH);' ) cliSource = cliSource.replaceAll( 'const zstdPath = targetPaths.ZSTD;', 'const zstdPath = toolPaths.ZSTD;' ) cliSource = cliSource.replaceAll( 'const bsdiffpath = targetPaths.BSDIFF;', 'const bsdiffpath = toolPaths.BSDIFF;' ) cliSource = cliSource.replace( 'zigAsarCli = join(targetPaths.BSPATCH).replace("bspatch", "zig-asar");', 'zigAsarCli = join(toolPaths.BSPATCH).replace("bspatch", "zig-asar");' ) writeFileSync(cliPath, cliSource) cliSource = readFileSync(cliPath, 'utf8') } if (!cliSource.includes('hookBunBinary')) { cliSource = cliSource.replace( `const hostPaths = getPlatformPaths(OS, HOST_ARCH); const result = Bun.spawnSync([hostPaths.BUN_BINARY, hookScript],`, `const hostPaths = getPlatformPaths(OS, HOST_ARCH); const hookBunBinary = existsSync(hostPaths.BUN_BINARY) ? hostPaths.BUN_BINARY : process.execPath; const result = Bun.spawnSync([hookBunBinary, hookScript],` ) cliSource = cliSource.replace( 'console.error("Tried to run with bun at:", hostPaths.BUN_BINARY);', 'console.error("Tried to run with bun at:", hookBunBinary);' ) writeFileSync(cliPath, cliSource) cliSource = readFileSync(cliPath, 'utf8') } if (!cliSource.includes('resolveProjectRceditPkgPath')) { cliSource = cliSource.replaceAll( 'const rceditPkgPath = require.resolve("rcedit/package.json");', 'const rceditPkgPath = resolveProjectRceditPkgPath(projectRoot);' ) cliSource = cliSource.replace( 'function getPlatformPaths(', `${RCEDIT_RESOLVER_FN} function getPlatformPaths(` ) writeFileSync(cliPath, cliSource) cliSource = readFileSync(cliPath, 'utf8') } else { cliSource = cliSource.replace( /function resolveProjectRceditPkgPath\(projectRoot\) \{[\s\S]*?\n\}/m, RCEDIT_RESOLVER_FN ) writeFileSync(cliPath, cliSource) } const electrobunCjsPath = path.join(electrobunDir, 'bin/electrobun.cjs') let electrobunCjs = readFileSync(electrobunCjsPath, 'utf8') if (!electrobunCjs.includes('farmcontrol-use-patched-cli')) { electrobunCjs = electrobunCjs.replace( `async function main() { try { const args = process.argv.slice(2); const cliPath = await ensureCliBinary(); // Replace this process with the actual CLI const child = spawn(cliPath, args, { stdio: 'inherit', cwd: process.cwd() });`, `async function main() { try { const args = process.argv.slice(2); const patchedCliPath = join(electrobunDir, 'src', 'cli', 'index.ts'); if (existsSync(patchedCliPath)) { // farmcontrol-use-patched-cli: bundled electrobun.exe cannot resolve project rcedit const bunBinary = process.env.BUN_INSTALL ? join(process.env.BUN_INSTALL, 'bin', 'bun' + binExt) : 'bun'; const child = spawn(bunBinary, [patchedCliPath, ...args], { stdio: 'inherit', cwd: process.cwd(), env: process.env, shell: platform === 'win', }); child.on('exit', (code) => { process.exit(code || 0); }); child.on('error', (error) => { console.error('Failed to start electrobun patched CLI:', error.message); process.exit(1); }); return; } const cliPath = await ensureCliBinary(); // Replace this process with the actual CLI const child = spawn(cliPath, args, { stdio: 'inherit', cwd: process.cwd() });` ) writeFileSync(electrobunCjsPath, electrobunCjs) } const rceditSrc = path.join(rootDir, 'node_modules/rcedit') const rceditDest = path.join(electrobunDir, 'node_modules/rcedit') if (existsSync(rceditSrc)) { mkdirSync(path.join(electrobunDir, 'node_modules'), { recursive: true }) cpSync(rceditSrc, rceditDest, { recursive: true, force: true }) } function patchNativeWrapperPath(nativeTsPath) { if (!existsSync(nativeTsPath)) { return } let source = readFileSync(nativeTsPath, 'utf8') if (source.includes('function resolveNativeWrapperPath()')) { return } if (!source.includes('existsSync')) { if (source.includes('import { dirname, join } from "path";')) { source = source.replace( 'import { dirname, join } from "path";', 'import { existsSync } from "fs";\nimport { dirname, join } from "path";' ) } else { source = source.replace( 'import { join } from "path";', 'import { existsSync } from "fs";\nimport { dirname, join } from "path";' ) } } else if (!source.includes('dirname')) { source = source.replace( 'import { join } from "path";', 'import { dirname, join } from "path";' ) } const helper = ` function resolveNativeWrapperPath() { \tconst fileName = \`libNativeWrapper.\${suffix}\`; \tconst candidates = [ \t\tjoin(dirname(process.execPath), fileName), \t\tjoin(process.cwd(), fileName), \t]; \tfor (const candidate of candidates) { \t\tif (existsSync(candidate)) { \t\t\treturn candidate; \t\t} \t} \treturn candidates[0]!; } ` source = source.replace( 'export const native = (() => {', `${helper}\nexport const native = (() => {` ) source = source.replace( /const nativeWrapperPath = join\((?:dirname\(process\.execPath\)|process\.cwd\(\)), `libNativeWrapper\.\$\{suffix\}`\);/, 'const nativeWrapperPath = resolveNativeWrapperPath();' ) writeFileSync(nativeTsPath, source) } for (const distDir of ['dist', 'dist-macos-arm64', 'dist-win-x64']) { patchNativeWrapperPath( path.join(electrobunDir, distDir, 'api/bun/proc/native.ts') ) } const markerPath = path.join(electrobunDir, '.farmcontrol-electrobun-patched') writeFileSync(markerPath, `patched-at=${new Date().toISOString()}\n`) console.log( 'patch-electrobun-src: electrobun src/shared ready for cross-arch builds' )