Files
iofficeai--aionui/scripts/rebuildNativeModules.js
2026-07-13 13:20:22 +08:00

402 lines
12 KiB
JavaScript

/**
* 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 <cmd>' 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,
};