Add installer changes

This commit is contained in:
Reinier Criel 2025-11-19 09:46:09 -08:00
parent e765ccf303
commit 2158478894
13 changed files with 674 additions and 21 deletions

254
installer/build.js Normal file
View file

@ -0,0 +1,254 @@
#!/usr/bin/env node
/**
* Build script for creating standalone Safe Chain binaries
* Uses esbuild to bundle ES modules, then @yao-pkg/pkg to create executable
*/
import { exec } from 'node:child_process';
import { promisify } from 'node:util';
import { existsSync, mkdirSync, copyFileSync, writeFileSync, rmSync, readFileSync, readdirSync } from 'node:fs';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import * as esbuild from 'esbuild';
const execAsync = promisify(exec);
const __dirname = dirname(fileURLToPath(import.meta.url));
const ROOT_DIR = join(__dirname, '..');
const DIST_DIR = join(__dirname, 'dist');
const BUNDLE_DIR = join(DIST_DIR, 'bundle');
const SAFE_CHAIN_DIR = join(ROOT_DIR, 'packages/safe-chain');
/**
* Parse command line arguments
*/
function parseArgs() {
const args = process.argv.slice(2);
const platform = args.find(arg => arg.startsWith('--platform='))?.split('=')[1] || 'macos';
return { platform };
}
/**
* Ensure dist directory exists
*/
function ensureDistDirectory() {
if (!existsSync(DIST_DIR)) {
mkdirSync(DIST_DIR, { recursive: true });
}
if (!existsSync(BUNDLE_DIR)) {
mkdirSync(BUNDLE_DIR, { recursive: true });
}
}
/**
* Bundle ES modules using esbuild
* This converts ES modules to CommonJS that pkg can handle
*/
async function bundleWithEsbuild() {
console.log('Bundling with esbuild...');
const entryPoint = join(SAFE_CHAIN_DIR, 'bin/safe-chain.js');
const outputFile = join(BUNDLE_DIR, 'safe-chain-bundled.cjs');
await esbuild.build({
entryPoints: [entryPoint],
bundle: true,
platform: 'node',
target: 'node20',
format: 'cjs',
outfile: outputFile,
external: [
// Keep node: protocol imports external
'node:*',
],
loader: {
'.json': 'json', // Inline JSON files
},
banner: {
js: `// Polyfill for import.meta.url in CommonJS
var __filename = __filename || (() => {
try {
return require('url').fileURLToPath(__filename);
} catch (e) {
return __filename;
}
})();
var __dirname = __dirname || require('path').dirname(__filename);
var import_meta_url = typeof __filename !== 'undefined' ? require('url').pathToFileURL(__filename).href : undefined;
`,
},
define: {
'import.meta.url': 'import_meta_url',
},
minify: false, // Keep readable for debugging
sourcemap: false,
});
console.log('✓ Bundle created at:', outputFile);
return outputFile;
}
/**
* Build macOS binary and installer
*/
async function buildMacOS() {
console.log('Building macOS binary...');
// Step 1: Bundle with esbuild
const bundledFile = await bundleWithEsbuild();
// Step 2: Package with pkg
const targetPlatform = 'node20-macos-arm64';
const outputPath = join(DIST_DIR, 'safe-chain-macos-arm64');
// Copy shell-integration files to a staging directory with the structure we want in /snapshot
const stagingDir = join(DIST_DIR, 'staging');
const stagingShellInt = join(stagingDir, 'src/shell-integration');
// Clean and create staging directory
if (existsSync(stagingDir)) {
rmSync(stagingDir, { recursive: true });
}
mkdirSync(stagingShellInt, { recursive: true });
// Copy shell-integration directory
const shellIntegrationSrc = join(SAFE_CHAIN_DIR, 'src/shell-integration');
// Helper to copy directory recursively
const copyDir = (src, dest) => {
if (!existsSync(dest)) {
mkdirSync(dest, { recursive: true });
}
const entries = readdirSync(src, { withFileTypes: true });
for (const entry of entries) {
const srcPath = join(src, entry.name);
const destPath = join(dest, entry.name);
if (entry.isDirectory()) {
copyDir(srcPath, destPath);
} else {
copyFileSync(srcPath, destPath);
}
}
};
copyDir(shellIntegrationSrc, stagingShellInt);
const pkgArgs = [
bundledFile,
'--target', targetPlatform,
'--output', outputPath,
'--compress', 'GZip',
// Include contents of staging/src - files will be at /snapshot/src/shell-integration/
`--assets=${join(stagingDir, 'src')}/**/*`,
];
console.log(`Running: npx @yao-pkg/pkg ${pkgArgs.join(' ')}`);
// Use spawn instead of execFile to avoid issues with glob expansion
const { spawn } = await import('node:child_process');
const pkgProcess = spawn('npx', ['@yao-pkg/pkg', ...pkgArgs], {
cwd: __dirname,
stdio: 'inherit'
});
await new Promise((resolve, reject) => {
pkgProcess.on('close', (code) => {
if (code !== 0) {
reject(new Error(`pkg failed with code ${code}`));
} else {
resolve();
}
});
}); console.log('✓ Binary created at:', outputPath);
// Create installer package
await createMacOSInstaller(outputPath);
}
/**
* Create macOS installer with certificate installation
*/
async function createMacOSInstaller(binaryPath) {
console.log('Creating macOS installer package...');
const installerDir = join(DIST_DIR, 'macos-installer');
const scriptsDir = join(installerDir, 'scripts');
const resourcesDir = join(installerDir, 'resources');
// Create directory structure
mkdirSync(scriptsDir, { recursive: true });
mkdirSync(resourcesDir, { recursive: true });
// Copy binary to resources
const binaryDestination = join(resourcesDir, 'safe-chain');
copyFileSync(binaryPath, binaryDestination);
// Read installer scripts from separate files
const scriptsSourceDir = join(__dirname, 'scripts');
const preinstallScript = readFileSync(join(scriptsSourceDir, 'darwin_preinstall.sh'), 'utf8');
const postinstallScript = readFileSync(join(scriptsSourceDir, 'darwin_postinstall.sh'), 'utf8');
const uninstallScript = readFileSync(join(scriptsSourceDir, 'darwin_uninstall.sh'), 'utf8');
// Write scripts to installer directory
writeFileSync(join(scriptsDir, 'preinstall'), preinstallScript, { mode: 0o755 });
writeFileSync(join(scriptsDir, 'postinstall'), postinstallScript, { mode: 0o755 });
writeFileSync(join(installerDir, 'uninstall.sh'), uninstallScript, { mode: 0o755 });
console.log('✓ macOS installer package created at:', installerDir);
console.log('');
console.log('To create a .pkg installer, run:');
console.log(` cd ${installerDir}`);
console.log(' pkgbuild --root resources --scripts scripts --identifier com.aikido.safe-chain --version 1.0.0 --install-location /tmp/safe-chain-install SafeChain.pkg');
}
/**
* Build Linux binary and installer
*/
async function buildLinux() {
// TODO: Implement Linux binary creation
}
/**
* Build Windows binary and installer
*/
async function buildWindows() {
// TODO: Implement Windows binary creation
}
/**
* Main build function
*/
async function build() {
const { platform } = parseArgs();
console.log('=== Safe Chain Installer Builder ===');
console.log(`Platform: ${platform}`);
console.log('');
ensureDistDirectory();
try {
switch (platform) {
case 'macos':
await buildMacOS();
break;
case 'linux':
await buildLinux();
break;
case 'windows':
await buildWindows();
break;
default:
console.error(`Unknown platform: ${platform}`);
console.error('Valid options: macos, linux, windows');
process.exit(1);
}
} catch (error) {
process.exit(1);
}
}
// Run the build
build();