/** * Unified native module rebuild utility * Handles rebuilding native modules for different platforms and architectures * * Supports vx toolchain management: * - Uses 'vx --with msvc' on Windows to ensure MSVC compiler is available * - Falls back to standard bunx if vx is not available */ const { execSync, execFileSync, spawnSync } = require('child_process'); const fs = require('fs'); const path = require('path'); /** * Check if vx is available in the system */ function isVxAvailable() { try { const result = spawnSync('vx', ['--version'], { stdio: 'ignore' }); return result.status === 0; } catch { return false; } } /** * Get bunx command for the current platform * Windows requires bunx.cmd, others use bunx * Note: does NOT add 'vx' prefix here — the caller's cmdPrefix (e.g. 'vx --with msvc') * already provides the vx entry point, so we must not nest another 'vx' call. */ function getBunxCommand() { return process.platform === 'win32' ? 'bun x' : 'bun x'; } /** * Get command prefix for native compilation with proper toolchain. * On Windows, returns 'vx --with msvc' so MSVC env vars are injected into * the subprocess environment before bunx/node-gyp runs. * On other platforms returns 'vx' to ensure the correct bun version is used. * Returns '' when vx is not available. */ function getCommandPrefix(platform, useVx = true) { if (!useVx || !isVxAvailable()) { return ''; } if (platform === 'win32' || platform === 'windows') { // 'vx --with msvc ' injects VCINSTALLDIR and related env vars so // node-gyp can locate the MSVC compiler without a separate choco install. return 'vx --with msvc'; } return 'vx'; } /** * Normalize architecture names */ function normalizeArch(arch) { const archMap = { x64: 'x64', arm64: 'arm64', ia32: 'ia32', armv7l: 'arm', }; return archMap[arch] || arch; } /** * Get modules to rebuild based on platform */ function getModulesToRebuild(platform) { // Windows: Skip node-pty (cross-compilation fails with missing conpty API types) // Linux: Skip node-pty (no ARM64 prebuilds available, cross-compilation requires ARM64 toolchain) // macOS: Skip node-pty (cross-compilation from ARM64→x64 fails, use @lydell/node-pty-* prebuilts) if (platform === 'win32' || platform === 'windows') { return ['better-sqlite3']; } else if (platform === 'linux') { return ['better-sqlite3']; } // macOS: only rebuild better-sqlite3, skip node-pty return ['better-sqlite3']; } /** * Build environment variables for native module compilation */ function buildEnvironment(platform, targetArch, electronVersion) { const env = { ...process.env, npm_config_arch: targetArch, npm_config_target_arch: targetArch, npm_config_build_from_source: 'true', npm_config_runtime: 'electron', npm_config_disturl: 'https://electronjs.org/headers', npm_config_target: electronVersion, }; // Windows-specific environment if (platform === 'win32' || platform === 'windows') { env.MSVS_VERSION = '2022'; env.GYP_MSVS_VERSION = '2022'; env.WindowsTargetPlatformVersion = '10.0.19041.0'; env._WIN32_WINNT = '0x0A00'; } return env; } /** * Rebuild native modules using electron-rebuild * * @param {Object} options * @param {string} options.platform - Platform name (win32, darwin, linux) * @param {string} options.arch - Target architecture (x64, arm64, etc.) * @param {string} options.electronVersion - Electron version * @param {string} options.cwd - Working directory (default: project root) * @param {string[]} [options.modules] - Modules to rebuild (default: auto-detect by platform) */ function rebuildWithElectronRebuild(options) { const { platform, arch, electronVersion, cwd = path.resolve(__dirname, '..'), modules = getModulesToRebuild(platform), } = options; const targetArch = normalizeArch(arch); const env = buildEnvironment(platform, targetArch, electronVersion); const bunxCmd = getBunxCommand(); const rebuildCmd = `${bunxCmd} electron-rebuild --only ${modules.join(',')} --force --arch ${targetArch} --electron-version ${electronVersion}`; execSync(rebuildCmd, { stdio: 'inherit', cwd, env, }); } /** * Check if cross-compilation from source is supported */ function canCrossCompileFromSource(buildArch, targetArch, platform) { // macOS can cross-compile between x64 and arm64 if (platform === 'darwin') { return true; } // Windows x64 can cross-compile to arm64 with proper toolchain if (platform === 'win32' && buildArch === 'x64' && targetArch === 'arm64') { return true; } // Linux cannot reliably cross-compile without ARM64 toolchain // Must use prebuild-install for cross-arch builds return buildArch === targetArch; } /** * Rebuild a single module using prebuild-install (faster for prebuilt binaries) * Falls back to electron-rebuild if prebuild-install fails * * @param {Object} options * @param {string} options.moduleName - Module name (e.g., 'better-sqlite3') * @param {string} options.moduleRoot - Path to module directory * @param {string} options.platform - Platform name * @param {string} options.arch - Target architecture * @param {string} options.electronVersion - Electron version * @param {string} [options.projectRoot] - Project root for fallback rebuild * @param {boolean} [options.forceRebuild] - Force rebuild from source (skip prebuild-install) * @param {string} [options.buildArch] - Build machine architecture (for cross-compilation detection) */ function rebuildSingleModule(options) { const { moduleName, moduleRoot, platform, arch, electronVersion, projectRoot = path.resolve(__dirname, '..'), forceRebuild = false, buildArch = process.arch, } = options; const targetArch = normalizeArch(arch); const normalizedBuildArch = normalizeArch(buildArch); const isCrossCompile = normalizedBuildArch !== targetArch; const env = buildEnvironment(platform, targetArch, electronVersion); env.npm_config_platform = platform; env.npm_config_target_platform = platform; const bunxCmd = getBunxCommand(); const cmdPrefix = getCommandPrefix(platform); const useShell = cmdPrefix.length > 0; // Need shell for vx prefix // For Linux cross-compilation, ALWAYS use prebuild-install // because electron-rebuild cannot cross-compile without ARM64 toolchain const mustUsePrebuild = platform === 'linux' && isCrossCompile; if (mustUsePrebuild) { console.log(` Linux cross-compilation detected (${normalizedBuildArch} → ${targetArch})`); // Check if module already has prebuilds const prebuildsDir = path.join(moduleRoot, 'prebuilds', `${platform}-${targetArch}`); if (fs.existsSync(prebuildsDir)) { const files = fs.readdirSync(prebuildsDir); const hasNodeFile = files.some((f) => f.endsWith('.node')); if (hasNodeFile) { console.log(` ✓ Found existing prebuilds in ${prebuildsDir}, skipping rebuild`); // Delete build/ and bin/ to prevent node-gyp-build from loading wrong architecture // node-gyp-build search order: bin/ -> build/Release/ -> prebuilds/ const buildDir = path.join(moduleRoot, 'build'); if (fs.existsSync(buildDir)) { console.log(` Removing build/ directory to force use of prebuilds/`); fs.rmSync(buildDir, { recursive: true, force: true }); } const binDir = path.join(moduleRoot, 'bin'); if (fs.existsSync(binDir)) { console.log(` Removing bin/ directory to force use of prebuilds/`); fs.rmSync(binDir, { recursive: true, force: true }); } return true; } } console.log(` No existing prebuilds found, trying prebuild-install...`); } // Try prebuild-install first (required for Linux cross-compile) if (!forceRebuild || mustUsePrebuild) { try { env.npm_config_build_from_source = 'false'; const prebuildArgs = [ '--yes', 'prebuild-install', '--runtime=electron', `--target=${electronVersion}`, `--platform=${platform}`, `--arch=${targetArch}`, '--force', ]; const fullCmd = cmdPrefix ? `${cmdPrefix} ${bunxCmd} ${prebuildArgs.join(' ')}` : `${bunxCmd} ${prebuildArgs.join(' ')}`; console.log(` Running: ${fullCmd}`); if (useShell) { execSync(fullCmd, { cwd: moduleRoot, env, stdio: 'inherit', shell: true, }); } else { execFileSync(bunxCmd, prebuildArgs, { cwd: moduleRoot, env, stdio: 'inherit', shell: true, }); } console.log(` ✓ prebuild-install succeeded`); return true; } catch (error) { if (mustUsePrebuild) { // For Linux cross-compile, prebuild-install MUST succeed console.error(` ✗ prebuild-install failed and cross-compilation from source not supported`); console.error(` Error: ${error.message}`); return false; } // For other cases, fall back to rebuild console.log(` prebuild-install failed, falling back to electron-rebuild...`); } } // Use electron-rebuild to build from source if (!canCrossCompileFromSource(normalizedBuildArch, targetArch, platform)) { console.error(` ✗ Cross-compilation from ${normalizedBuildArch} to ${targetArch} not supported on ${platform}`); return false; } try { env.npm_config_build_from_source = 'true'; const rebuildArgs = [ '--yes', 'electron-rebuild', '--only', moduleName, '--force', `--platform=${platform}`, `--arch=${targetArch}`, ]; const fullCmd = cmdPrefix ? `${cmdPrefix} ${bunxCmd} ${rebuildArgs.join(' ')}` : `${bunxCmd} ${rebuildArgs.join(' ')}`; console.log(` Running: ${fullCmd}`); if (useShell) { execSync(fullCmd, { cwd: projectRoot, env, stdio: 'inherit', shell: true, }); } else { execFileSync(bunxCmd, rebuildArgs, { cwd: projectRoot, env, stdio: 'inherit', shell: true, }); } return true; } catch (error) { console.error(`❌ Failed to rebuild ${moduleName}:`, error.message); return false; } } /** * Recursively search for .node files in a directory */ function findNodeFiles(dir, maxDepth = 3, currentDepth = 0) { if (currentDepth >= maxDepth || !fs.existsSync(dir)) { return []; } const results = []; try { const entries = fs.readdirSync(dir, { withFileTypes: true }); for (const entry of entries) { const fullPath = path.join(dir, entry.name); if (entry.isDirectory()) { results.push(...findNodeFiles(fullPath, maxDepth, currentDepth + 1)); } else if (entry.isFile() && entry.name.endsWith('.node')) { results.push(fullPath); } } } catch (error) { // Ignore permission errors } return results; } /** * Verify native module binary exists */ function verifyModuleBinary(moduleRoot, moduleName) { const binaryPathsToCheck = { 'better-sqlite3': [path.join(moduleRoot, 'build', 'Release', 'better_sqlite3.node')], 'node-pty': [ path.join(moduleRoot, 'build', 'Release', 'pty.node'), path.join(moduleRoot, 'build', 'Release', 'conpty.node'), path.join(moduleRoot, 'build', 'Release', 'conpty_console_list.node'), ], }; const pathsToCheck = binaryPathsToCheck[moduleName] || []; // First check known paths for (const binaryPath of pathsToCheck) { if (fs.existsSync(binaryPath)) { console.log(` Debug: Found binary at ${binaryPath}`); return true; } } // If not found, search recursively console.log(` Debug: Binary not found in expected locations, searching recursively...`); const foundFiles = findNodeFiles(moduleRoot); if (foundFiles.length > 0) { console.log(` Debug: Found .node files:`); foundFiles.forEach((f) => console.log(` - ${f}`)); return true; } console.log(` Debug: No .node files found in ${moduleRoot}`); return false; } module.exports = { normalizeArch, getModulesToRebuild, buildEnvironment, rebuildWithElectronRebuild, rebuildSingleModule, verifyModuleBinary, canCrossCompileFromSource, isVxAvailable, getBunxCommand, getCommandPrefix, };