mirror of
https://github.com/samsonjs/vibetunnel.git
synced 2026-03-25 09:25:50 +00:00
322 lines
No EOL
11 KiB
JavaScript
Executable file
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 }; |