vibetunnel/web/build-custom-node.js

322 lines
No EOL
11 KiB
JavaScript
Executable file

#!/usr/bin/env node
/**
* Build a custom Node.js binary with reduced size by excluding features we don't need.
*
* This script automatically adapts to CI and local environments.
*
* Usage:
* node build-custom-node.js # Builds Node.js 24.2.0 (recommended)
* node build-custom-node.js --latest # Latest version
* node build-custom-node.js --version=24.2.0 # Specific version
* NODE_VERSION=24.2.0 node build-custom-node.js # Via environment variable (CI)
*
* In CI environments:
* - Outputs GitHub Actions variables
* - Uses ccache if available
* - Creates build summary files
*/
const { execSync } = require('child_process');
const fs = require('fs');
const path = require('path');
const https = require('https');
// Detect if running in CI
const isCI = process.env.CI === 'true' || process.env.GITHUB_ACTIONS === 'true';
// Parse command line arguments
const args = process.argv.slice(2);
let targetVersion = null;
let useLatest = false;
for (const arg of args) {
if (arg.startsWith('--version=')) {
targetVersion = arg.split('=')[1];
} else if (arg === '--latest') {
useLatest = true;
}
}
// Helper for GitHub Actions output
function setOutput(name, value) {
if (process.env.GITHUB_OUTPUT) {
fs.appendFileSync(process.env.GITHUB_OUTPUT, `${name}=${value}\n`);
}
}
// Helper to download files
function downloadFile(url, destPath) {
return new Promise((resolve, reject) => {
const file = fs.createWriteStream(destPath);
https.get(url, (response) => {
if (response.statusCode === 302 || response.statusCode === 301) {
// Handle redirect
downloadFile(response.headers.location, destPath).then(resolve).catch(reject);
return;
}
response.pipe(file);
file.on('finish', () => {
file.close(resolve);
});
}).on('error', (err) => {
fs.unlink(destPath, () => {});
reject(err);
});
});
}
// Helper to get latest Node.js version
async function getLatestNodeVersion() {
return new Promise((resolve, reject) => {
https.get('https://nodejs.org/dist/latest/SHASUMS256.txt', (res) => {
let data = '';
res.on('data', chunk => data += chunk);
res.on('end', () => {
// Extract version from first line like: "1234567890abcdef node-v24.2.0-darwin-arm64.tar.gz"
const match = data.match(/node-v(\d+\.\d+\.\d+)/);
if (match) {
resolve(match[1]);
} else {
reject(new Error('Could not determine latest Node.js version'));
}
});
}).on('error', reject);
});
}
async function buildCustomNode() {
// Determine version to build (CLI args take precedence over env vars)
let nodeSourceVersion;
if (useLatest) {
console.log('Fetching latest Node.js version...');
nodeSourceVersion = await getLatestNodeVersion();
console.log(`Latest Node.js version: ${nodeSourceVersion}`);
} else if (targetVersion) {
nodeSourceVersion = targetVersion;
} else if (process.env.NODE_VERSION) {
// Support CI environment variable
nodeSourceVersion = process.env.NODE_VERSION;
} else {
// Default to Node.js 24.3.0 (recommended version)
nodeSourceVersion = '24.3.0';
}
const platform = process.platform;
const arch = process.arch;
console.log(`Building custom Node.js ${nodeSourceVersion} for ${platform}-${arch}...`);
console.log('This will take 10-20 minutes on first run, but will be cached for future builds.');
const nodeSourceUrl = `https://nodejs.org/dist/v${nodeSourceVersion}/node-v${nodeSourceVersion}.tar.gz`;
const majorVersion = nodeSourceVersion.split('.')[0];
// In CI scripts directory, go up one level to find web root
const buildDir = path.join(__dirname, __dirname.endsWith('scripts') ? '..' : '.', '.node-builds');
const versionDir = path.join(buildDir, `node-v${nodeSourceVersion}-minimal`);
const markerFile = path.join(versionDir, '.build-complete');
const customNodePath = path.join(versionDir, 'out', 'Release', 'node');
// Check if already built
if (fs.existsSync(markerFile) && fs.existsSync(customNodePath)) {
console.log(`Using cached custom Node.js build from ${customNodePath}`);
const stats = fs.statSync(customNodePath);
console.log(`Cached custom Node.js size: ${(stats.size / 1024 / 1024).toFixed(2)} MB`);
if (isCI) {
// Set outputs for GitHub Actions
setOutput('node-path', customNodePath);
setOutput('node-size', stats.size);
setOutput('cache-hit', 'true');
} else {
console.log(`\nTo use this custom Node.js with build-native.js:`);
console.log(`node build-native.js --custom-node="${customNodePath}"`);
}
return customNodePath;
}
// Create build directory
if (!fs.existsSync(buildDir)) {
fs.mkdirSync(buildDir, { recursive: true });
}
// Clean up incomplete builds (check for marker file)
if (fs.existsSync(versionDir) && !fs.existsSync(markerFile)) {
console.log('Cleaning up incomplete build...');
fs.rmSync(versionDir, { recursive: true, force: true });
}
const tarPath = path.join(buildDir, `node-v${nodeSourceVersion}.tar.gz`);
const originalCwd = process.cwd();
try {
// Download Node.js source if not cached
if (!fs.existsSync(tarPath)) {
console.log(`Downloading Node.js source from ${nodeSourceUrl}...`);
await downloadFile(nodeSourceUrl, tarPath);
}
// Extract source
console.log('Extracting Node.js source...');
execSync(`tar -xzf "${tarPath}" -C "${buildDir}"`, { stdio: 'inherit' });
// Rename to version-specific directory
const extractedDir = path.join(buildDir, `node-v${nodeSourceVersion}`);
if (fs.existsSync(extractedDir)) {
fs.renameSync(extractedDir, versionDir);
}
// Configure and build
process.chdir(versionDir);
console.log('Configuring Node.js build...');
const configureArgs = [
'--without-intl', // Remove internationalization support
'--without-npm', // Don't include npm
'--without-corepack', // Don't include corepack
'--without-inspector', // Remove debugging/profiling features
'--without-node-code-cache', // Disable code cache
'--without-node-snapshot', // Don't create/use startup snapshot
'--shared-zlib', // Use system zlib instead of building custom
];
// Check if ninja is available
try {
execSync('which ninja', { stdio: 'ignore' });
configureArgs.push('--ninja');
console.log('Using Ninja for faster builds...');
} catch {
console.log('Ninja not found, using Make...');
}
// Enable ccache if available in system paths
try {
execSync('which ccache', { stdio: 'ignore' });
process.env.CC = 'ccache gcc';
process.env.CXX = 'ccache g++';
console.log('Using ccache for faster rebuilds...');
} catch {
console.log('ccache not found, proceeding without it...');
}
// Use -Os optimization which is proven to be safe
process.env.CFLAGS = '-Os';
process.env.CXXFLAGS = '-Os';
execSync(`./configure ${configureArgs.join(' ')}`, { stdio: 'inherit' });
console.log('Building Node.js (this will take a while)...');
const cores = require('os').cpus().length;
const startTime = Date.now();
// Check if we're using ninja or make
const buildCmd = configureArgs.includes('--ninja')
? `ninja -C out/Release -j ${cores}`
: `make -j${cores}`;
execSync(buildCmd, { stdio: 'inherit' });
const buildTime = Math.round((Date.now() - startTime) / 1000);
if (isCI) {
console.log(`Build completed in ${buildTime} seconds`);
}
// Verify the build
if (!fs.existsSync(customNodePath)) {
throw new Error('Node.js build failed - binary not found');
}
// Test the binary
const version = execSync(`"${customNodePath}" --version`, { encoding: 'utf8' }).trim();
console.log(`Built Node.js version: ${version}`);
// Strip the binary (different command for Linux vs macOS)
console.log('Stripping Node.js binary...');
const stripCmd = platform === 'darwin' ? 'strip -S' : 'strip -s';
execSync(`${stripCmd} "${customNodePath}"`, { stdio: 'inherit' });
// Check final size
const stats = fs.statSync(customNodePath);
console.log(`\n✅ Custom Node.js built successfully!`);
console.log(`Size: ${(stats.size / 1024 / 1024).toFixed(2)} MB`);
// Compare with system Node.js
try {
const systemNodeStats = fs.statSync(process.execPath);
const reduction = ((systemNodeStats.size - stats.size) / systemNodeStats.size * 100).toFixed(1);
console.log(`Size reduction: ${reduction}% compared to system Node.js`);
} catch (e) {
// Ignore if we can't stat system node
}
// Mark build as complete
const buildInfo = {
version: nodeSourceVersion,
buildDate: new Date().toISOString(),
size: stats.size,
platform: platform,
arch: arch,
configureArgs: configureArgs,
buildTime: buildTime
};
fs.writeFileSync(markerFile, JSON.stringify(buildInfo, null, 2));
// Create a summary file
const summaryPath = path.join(versionDir, 'build-summary.txt');
const summary = `
Custom Node.js Build Summary
============================
Version: ${nodeSourceVersion}
Platform: ${platform}-${arch}
Size: ${(stats.size / 1024 / 1024).toFixed(2)} MB
Build Time: ${buildTime} seconds
Configure Args: ${configureArgs.join(' ')}
Path: ${customNodePath}
`;
fs.writeFileSync(summaryPath, summary);
// Change back to original directory
process.chdir(originalCwd);
if (isCI) {
// Set outputs for GitHub Actions
setOutput('node-path', customNodePath);
setOutput('node-size', stats.size);
setOutput('node-version', version);
setOutput('build-time', buildTime);
setOutput('cache-hit', 'false');
}
// Output for both CI and local use
console.log(`\nCustom Node.js location: ${customNodePath}`);
console.log(`To use this custom Node.js with build-native.js:`);
console.log(`node build-native.js --custom-node="${customNodePath}"`);
return customNodePath;
} catch (error) {
process.chdir(originalCwd);
console.error('Failed to build custom Node.js:', error.message || error);
// Set error output for CI
if (isCI) {
setOutput('build-error', error.message || 'Unknown error');
}
process.exit(1);
}
}
// Run the build if called directly
if (require.main === module) {
buildCustomNode().catch(err => {
console.error('Build failed:', err);
process.exit(1);
});
}
// Export for use as a module
module.exports = { buildCustomNode };