mirror of
https://github.com/samsonjs/vibetunnel.git
synced 2026-03-25 09:25:50 +00:00
420 lines
No EOL
15 KiB
JavaScript
Executable file
420 lines
No EOL
15 KiB
JavaScript
Executable file
#!/usr/bin/env node
|
|
|
|
/**
|
|
* Build standalone vibetunnel executable using Node.js SEA (Single Executable Application)
|
|
*
|
|
* This script creates a portable executable that bundles the VibeTunnel server into a single
|
|
* binary using Node.js's built-in SEA feature. The resulting executable can run on any machine
|
|
* with the same OS/architecture without requiring Node.js to be installed.
|
|
*
|
|
* ## Output
|
|
* Creates a `native/` directory with just 3 files:
|
|
* - `vibetunnel` - The standalone executable (includes all JS code and sourcemaps)
|
|
* - `pty.node` - Native binding for terminal emulation
|
|
* - `spawn-helper` - Helper binary for spawning processes (Unix only)
|
|
*
|
|
* ## Usage
|
|
* ```bash
|
|
* node build-native.js # Build with system Node.js
|
|
* node build-native.js --sourcemap # Build with inline sourcemaps
|
|
* node build-native.js --custom-node # Auto-discover custom Node.js (uses most recent)
|
|
* node build-native.js --custom-node=/path/to/node # Use specific custom Node.js binary
|
|
* node build-native.js --custom-node /path/to/node # Alternative syntax
|
|
* ```
|
|
*/
|
|
|
|
const { execSync } = require('child_process');
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
// Parse command line arguments
|
|
const includeSourcemaps = process.argv.includes('--sourcemap');
|
|
let customNodePath = null;
|
|
|
|
// Parse --custom-node argument
|
|
for (let i = 0; i < process.argv.length; i++) {
|
|
const arg = process.argv[i];
|
|
if (arg.startsWith('--custom-node=')) {
|
|
customNodePath = arg.split('=')[1];
|
|
} else if (arg === '--custom-node') {
|
|
if (i + 1 < process.argv.length && !process.argv[i + 1].startsWith('--')) {
|
|
// Next argument is the path
|
|
customNodePath = process.argv[i + 1];
|
|
} else {
|
|
// No path provided, use auto-discovery
|
|
customNodePath = 'auto';
|
|
}
|
|
}
|
|
}
|
|
|
|
console.log('Building standalone vibetunnel executable using Node.js SEA...');
|
|
console.log(`System Node.js version: ${process.version}`);
|
|
if (includeSourcemaps) {
|
|
console.log('Including sourcemaps in build');
|
|
}
|
|
|
|
// Check Node.js version
|
|
const nodeVersion = parseInt(process.version.split('.')[0].substring(1));
|
|
if (nodeVersion < 20) {
|
|
console.error('Error: Node.js 20 or higher is required for SEA feature');
|
|
process.exit(1);
|
|
}
|
|
|
|
// Cleanup function
|
|
function cleanup() {
|
|
if (fs.existsSync('build') && !process.argv.includes('--keep-build')) {
|
|
console.log('Cleaning up build directory...');
|
|
fs.rmSync('build', { recursive: true, force: true });
|
|
}
|
|
}
|
|
|
|
// Ensure cleanup happens on exit
|
|
process.on('exit', cleanup);
|
|
process.on('SIGINT', () => {
|
|
console.log('\nBuild interrupted');
|
|
process.exit(1);
|
|
});
|
|
process.on('SIGTERM', () => {
|
|
console.log('\nBuild terminated');
|
|
process.exit(1);
|
|
});
|
|
|
|
// No patching needed - SEA support is built into our vendored node-pty
|
|
|
|
async function main() {
|
|
try {
|
|
// No patching needed - SEA support is built into our vendored node-pty
|
|
console.log('Using vendored node-pty with built-in SEA support...');
|
|
|
|
// Ensure native modules are built (in case postinstall didn't run)
|
|
const nativePtyDir = 'node_modules/node-pty/build/Release';
|
|
const nativeAuthDir = 'node_modules/authenticate-pam/build/Release';
|
|
|
|
if (!fs.existsSync(nativePtyDir)) {
|
|
console.log('Building node-pty native module...');
|
|
// Find the actual node-pty path (could be in .pnpm directory)
|
|
const nodePtyPath = require.resolve('node-pty/package.json');
|
|
const nodePtyDir = path.dirname(nodePtyPath);
|
|
console.log(`Found node-pty at: ${nodePtyDir}`);
|
|
|
|
// Build node-pty using node-gyp directly to avoid TypeScript compilation
|
|
execSync(`cd "${nodePtyDir}" && npx node-gyp rebuild`, {
|
|
stdio: 'inherit',
|
|
shell: true
|
|
});
|
|
}
|
|
|
|
if (!fs.existsSync(nativeAuthDir)) {
|
|
console.log('Building authenticate-pam native module...');
|
|
execSync('npm rebuild authenticate-pam', {
|
|
stdio: 'inherit',
|
|
cwd: __dirname
|
|
});
|
|
}
|
|
|
|
// Create build directory
|
|
if (!fs.existsSync('build')) {
|
|
fs.mkdirSync('build');
|
|
}
|
|
|
|
// Create native directory
|
|
if (!fs.existsSync('native')) {
|
|
fs.mkdirSync('native');
|
|
}
|
|
|
|
// 0. Determine which Node.js to use
|
|
let nodeExe = process.execPath;
|
|
if (customNodePath) {
|
|
if (customNodePath === 'auto') {
|
|
// Auto-discover custom Node.js build
|
|
const buildDir = path.join(__dirname, '.node-builds');
|
|
if (fs.existsSync(buildDir)) {
|
|
// Find the most recent custom Node.js build
|
|
const builds = fs.readdirSync(buildDir)
|
|
.filter(name => name.startsWith('node-v') && name.endsWith('-minimal'))
|
|
.map(name => {
|
|
const nodePath = path.join(buildDir, name, 'out', 'Release', 'node');
|
|
if (fs.existsSync(nodePath)) {
|
|
const match = name.match(/node-v(.+)-minimal/);
|
|
if (!match || !match[1]) {
|
|
console.warn(`Warning: Skipping directory with invalid name format: ${name}`);
|
|
return null;
|
|
}
|
|
return {
|
|
path: nodePath,
|
|
version: match[1],
|
|
mtime: fs.statSync(nodePath).mtime
|
|
};
|
|
}
|
|
return null;
|
|
})
|
|
.filter(Boolean)
|
|
.sort((a, b) => b.mtime - a.mtime);
|
|
|
|
if (builds.length > 0) {
|
|
customNodePath = builds[0].path;
|
|
console.log(`Auto-discovered custom Node.js v${builds[0].version} at ${customNodePath}`);
|
|
} else {
|
|
console.error('Error: No custom Node.js builds found in .node-builds/');
|
|
console.error('Build one using: node build-custom-node.js');
|
|
process.exit(1);
|
|
}
|
|
} else {
|
|
console.error('Error: No .node-builds directory found');
|
|
console.error('Build a custom Node.js using: node build-custom-node.js');
|
|
process.exit(1);
|
|
}
|
|
} else {
|
|
// Validate custom node exists at specified path
|
|
if (!fs.existsSync(customNodePath)) {
|
|
console.error(`Error: Custom Node.js not found at ${customNodePath}`);
|
|
console.error('Build one using: node build-custom-node.js');
|
|
process.exit(1);
|
|
}
|
|
}
|
|
nodeExe = customNodePath;
|
|
}
|
|
|
|
console.log(`Using Node.js binary: ${nodeExe}`);
|
|
const nodeStats = fs.statSync(nodeExe);
|
|
console.log(`Node.js binary size: ${(nodeStats.size / 1024 / 1024).toFixed(2)} MB`);
|
|
|
|
// 1. Rebuild native modules if using custom Node.js
|
|
if (customNodePath) {
|
|
console.log('\nCustom Node.js detected - rebuilding native modules...');
|
|
const customVersion = execSync(`"${nodeExe}" --version`, { encoding: 'utf8' }).trim();
|
|
console.log(`Custom Node.js version: ${customVersion}`);
|
|
|
|
// Save original PATH and use clean environment
|
|
const originalPath = process.env.PATH;
|
|
const cleanEnv = {
|
|
...process.env,
|
|
// Use only system paths to avoid Homebrew contamination
|
|
PATH: '/usr/bin:/bin:/usr/sbin:/sbin',
|
|
npm_config_runtime: 'node',
|
|
npm_config_target: customVersion.substring(1), // Remove 'v' prefix
|
|
npm_config_arch: process.arch,
|
|
npm_config_target_arch: process.arch,
|
|
npm_config_disturl: 'https://nodejs.org/dist',
|
|
npm_config_build_from_source: 'true',
|
|
// Node.js 24 requires C++20
|
|
CXXFLAGS: '-std=c++20',
|
|
npm_config_cxxflags: '-std=c++20'
|
|
};
|
|
|
|
// Remove any Homebrew-related environment variables
|
|
delete cleanEnv.LDFLAGS;
|
|
delete cleanEnv.LIBRARY_PATH;
|
|
delete cleanEnv.CPATH;
|
|
delete cleanEnv.C_INCLUDE_PATH;
|
|
delete cleanEnv.CPLUS_INCLUDE_PATH;
|
|
delete cleanEnv.PKG_CONFIG_PATH;
|
|
|
|
console.log('Using clean PATH to avoid Homebrew dependencies during native module rebuild...');
|
|
|
|
execSync(`pnpm rebuild node-pty authenticate-pam`, {
|
|
stdio: 'inherit',
|
|
env: cleanEnv
|
|
});
|
|
|
|
// Restore original PATH
|
|
process.env.PATH = originalPath;
|
|
}
|
|
|
|
// 2. Bundle TypeScript with esbuild
|
|
console.log('\nBundling TypeScript with esbuild...');
|
|
|
|
// Use deterministic timestamps based on git commit or source
|
|
let buildDate = new Date().toISOString();
|
|
let buildTimestamp = Date.now();
|
|
|
|
try {
|
|
// Try to use the last commit date for reproducible builds
|
|
const gitDate = execSync('git log -1 --format=%cI', { encoding: 'utf8' }).trim();
|
|
buildDate = gitDate;
|
|
buildTimestamp = new Date(gitDate).getTime();
|
|
console.log(`Using git commit date for reproducible build: ${buildDate}`);
|
|
} catch (e) {
|
|
// Fallback to current time
|
|
console.warn('Warning: Using current time for build - output will not be reproducible');
|
|
}
|
|
|
|
let esbuildCmd = `NODE_NO_WARNINGS=1 npx esbuild src/cli.ts \\
|
|
--bundle \\
|
|
--platform=node \\
|
|
--target=node20 \\
|
|
--outfile=build/bundle.js \\
|
|
--format=cjs \\
|
|
--keep-names \\
|
|
--external:authenticate-pam \\
|
|
--external:../build/Release/pty.node \\
|
|
--external:./build/Release/pty.node \\
|
|
--define:process.env.BUILD_DATE='"${buildDate}"' \\
|
|
--define:process.env.BUILD_TIMESTAMP='"${buildTimestamp}"' \\
|
|
--define:process.env.VIBETUNNEL_SEA='"true"'`;
|
|
|
|
// Also inject git commit hash for version tracking
|
|
try {
|
|
const gitCommit = execSync('git rev-parse --short HEAD', { encoding: 'utf8' }).trim();
|
|
esbuildCmd += ` \\\n --define:process.env.GIT_COMMIT='"${gitCommit}"'`;
|
|
} catch (e) {
|
|
esbuildCmd += ` \\\n --define:process.env.GIT_COMMIT='"unknown"'`;
|
|
}
|
|
|
|
if (includeSourcemaps) {
|
|
esbuildCmd += ' \\\n --sourcemap=inline \\\n --source-root=/';
|
|
}
|
|
|
|
console.log('Running:', esbuildCmd);
|
|
execSync(esbuildCmd, {
|
|
stdio: 'inherit',
|
|
env: {
|
|
...process.env,
|
|
NODE_NO_WARNINGS: '1'
|
|
}
|
|
});
|
|
|
|
// 2b. Post-process bundle to ensure VIBETUNNEL_SEA is properly set
|
|
console.log('\nPost-processing bundle for SEA compatibility...');
|
|
let bundleContent = fs.readFileSync('build/bundle.js', 'utf8');
|
|
|
|
// Remove shebang line if present (not valid in SEA bundles)
|
|
if (bundleContent.startsWith('#!')) {
|
|
bundleContent = bundleContent.substring(bundleContent.indexOf('\n') + 1);
|
|
}
|
|
|
|
// Add VIBETUNNEL_SEA environment variable at the top of the bundle
|
|
// This ensures the patched node-pty knows it's running in SEA mode
|
|
const seaEnvSetup = `// Set VIBETUNNEL_SEA environment variable for SEA mode
|
|
if (typeof process !== 'undefined' && process.versions && process.versions.node) {
|
|
process.env.VIBETUNNEL_SEA = 'true';
|
|
}
|
|
|
|
`;
|
|
|
|
bundleContent = seaEnvSetup + bundleContent;
|
|
|
|
fs.writeFileSync('build/bundle.js', bundleContent);
|
|
console.log('Bundle post-processing complete');
|
|
|
|
// 3. Create SEA configuration
|
|
console.log('\nCreating SEA configuration...');
|
|
const seaConfig = {
|
|
main: 'build/bundle.js',
|
|
output: 'build/sea-prep.blob',
|
|
disableExperimentalSEAWarning: true,
|
|
useSnapshot: false,
|
|
useCodeCache: false
|
|
};
|
|
|
|
fs.writeFileSync('build/sea-config.json', JSON.stringify(seaConfig, null, 2));
|
|
|
|
// 4. Generate SEA blob
|
|
console.log('Generating SEA blob...');
|
|
execSync('node --experimental-sea-config build/sea-config.json', { stdio: 'inherit' });
|
|
|
|
// 5. Create executable
|
|
console.log('\nCreating executable...');
|
|
const targetExe = process.platform === 'win32' ? 'native/vibetunnel.exe' : 'native/vibetunnel';
|
|
|
|
// Copy node binary
|
|
fs.copyFileSync(nodeExe, targetExe);
|
|
if (process.platform !== 'win32') {
|
|
fs.chmodSync(targetExe, 0o755);
|
|
}
|
|
|
|
// 6. Inject the blob
|
|
console.log('Injecting SEA blob...');
|
|
let postjectCmd = `npx postject ${targetExe} NODE_SEA_BLOB build/sea-prep.blob \\
|
|
--sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2`;
|
|
|
|
if (process.platform === 'darwin') {
|
|
postjectCmd += ' --macho-segment-name NODE_SEA';
|
|
}
|
|
|
|
execSync(postjectCmd, { stdio: 'inherit' });
|
|
|
|
// 7. Strip the executable first (before signing)
|
|
console.log('Stripping final executable...');
|
|
execSync(`strip -S ${targetExe} 2>&1 | grep -v "warning: changes being made" || true`, {
|
|
stdio: 'inherit',
|
|
shell: true
|
|
});
|
|
|
|
// 8. Sign on macOS (after stripping)
|
|
if (process.platform === 'darwin') {
|
|
console.log('Signing executable...');
|
|
execSync(`codesign --sign - ${targetExe}`, { stdio: 'inherit' });
|
|
}
|
|
|
|
// Check final size
|
|
const finalStats = fs.statSync(targetExe);
|
|
console.log(`Final executable size: ${(finalStats.size / 1024 / 1024).toFixed(2)} MB`);
|
|
console.log(`Size reduction: ${((nodeStats.size - finalStats.size) / 1024 / 1024).toFixed(2)} MB`);
|
|
|
|
// 9. Copy native modules
|
|
console.log('\nCopying native modules...');
|
|
|
|
// Find the actual node-pty build directory (could be in .pnpm directory)
|
|
const nodePtyPath = require.resolve('node-pty/package.json');
|
|
const nodePtyBaseDir = path.dirname(nodePtyPath);
|
|
const nativeModulesDir = path.join(nodePtyBaseDir, 'build/Release');
|
|
|
|
// Check if native modules exist
|
|
if (!fs.existsSync(nativeModulesDir)) {
|
|
console.error(`Error: Native modules directory not found at ${nativeModulesDir}`);
|
|
console.error('This usually means the native module build failed.');
|
|
process.exit(1);
|
|
}
|
|
|
|
// Copy pty.node
|
|
const ptyNodePath = path.join(nativeModulesDir, 'pty.node');
|
|
if (!fs.existsSync(ptyNodePath)) {
|
|
console.error('Error: pty.node not found. Native module build may have failed.');
|
|
process.exit(1);
|
|
}
|
|
fs.copyFileSync(ptyNodePath, 'native/pty.node');
|
|
console.log(' - Copied pty.node');
|
|
|
|
// Copy spawn-helper (macOS only)
|
|
// Note: spawn-helper is only built and required on macOS where it's used for pty_posix_spawn()
|
|
// On Linux, node-pty uses forkpty() directly and doesn't need spawn-helper
|
|
if (process.platform === 'darwin') {
|
|
const spawnHelperPath = path.join(nativeModulesDir, 'spawn-helper');
|
|
if (!fs.existsSync(spawnHelperPath)) {
|
|
console.error('Error: spawn-helper not found. Native module build may have failed.');
|
|
process.exit(1);
|
|
}
|
|
fs.copyFileSync(spawnHelperPath, 'native/spawn-helper');
|
|
fs.chmodSync('native/spawn-helper', 0o755);
|
|
console.log(' - Copied spawn-helper');
|
|
}
|
|
|
|
// Copy authenticate_pam.node
|
|
const authPamPath = 'node_modules/authenticate-pam/build/Release/authenticate_pam.node';
|
|
if (fs.existsSync(authPamPath)) {
|
|
fs.copyFileSync(authPamPath, 'native/authenticate_pam.node');
|
|
console.log(' - Copied authenticate_pam.node');
|
|
} else {
|
|
console.error('Error: authenticate_pam.node not found. PAM authentication is required.');
|
|
process.exit(1);
|
|
}
|
|
|
|
console.log('\n✅ Build complete!');
|
|
console.log(`\nPortable executable created in native/ directory:`);
|
|
console.log(` - vibetunnel (executable)`);
|
|
console.log(` - pty.node`);
|
|
if (process.platform === 'darwin') {
|
|
console.log(` - spawn-helper`);
|
|
}
|
|
console.log(` - authenticate_pam.node`);
|
|
console.log('\nAll files must be kept together in the same directory.');
|
|
console.log('This bundle will work on any machine with the same OS/architecture.');
|
|
|
|
} catch (error) {
|
|
console.error('\n❌ Build failed:', error.message);
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
main(); |