| |
| |
| |
| |
| |
|
|
| import { spawnSync } from 'node:child_process'; |
| import { |
| cpSync, |
| rmSync, |
| mkdirSync, |
| existsSync, |
| copyFileSync, |
| writeFileSync, |
| readFileSync, |
| chmodSync, |
| } from 'node:fs'; |
| import { join, dirname } from 'node:path'; |
| import { fileURLToPath } from 'node:url'; |
| import process from 'node:process'; |
| import { globSync } from 'glob'; |
| import { createHash } from 'node:crypto'; |
|
|
| const __dirname = dirname(fileURLToPath(import.meta.url)); |
| const root = join(__dirname, '..'); |
| const distDir = join(root, 'dist'); |
| const bundleDir = join(root, 'bundle'); |
| const stagingDir = join(bundleDir, 'native_modules'); |
| const seaConfigPath = join(root, 'sea-config.json'); |
| const manifestPath = join(bundleDir, 'manifest.json'); |
| const entitlementsPath = join(root, 'scripts/entitlements.plist'); |
|
|
| |
|
|
| |
| |
| |
| |
| |
| |
| function runCommand(command, args, options = {}) { |
| let finalCommand = command; |
| let useShell = options.shell || false; |
|
|
| |
| if ( |
| process.platform === 'win32' && |
| (command === 'npm' || command === 'npx') |
| ) { |
| finalCommand = `${command}.cmd`; |
| useShell = true; |
| } |
|
|
| const finalOptions = { |
| stdio: 'inherit', |
| cwd: root, |
| shell: useShell, |
| ...options, |
| }; |
|
|
| const result = spawnSync(finalCommand, args, finalOptions); |
|
|
| if (result.status !== 0) { |
| if (result.error) { |
| throw result.error; |
| } |
| throw new Error( |
| `Command failed with exit code ${result.status}: ${command}`, |
| ); |
| } |
|
|
| return result; |
| } |
|
|
| |
| |
| |
| |
| function removeSignature(filePath) { |
| console.log(`Removing signature from ${filePath}...`); |
| const platform = process.platform; |
| try { |
| if (platform === 'darwin') { |
| spawnSync('codesign', ['--remove-signature', filePath], { |
| stdio: 'ignore', |
| }); |
| } else if (platform === 'win32') { |
| spawnSync('signtool', ['remove', '/s', filePath], { |
| stdio: 'ignore', |
| }); |
| } |
| } catch { |
| |
| } |
| } |
|
|
| |
| |
| |
| |
| function signFile(filePath) { |
| if (process.env.SKIP_SIGNING === 'true') { |
| console.log(`Skipping signing for ${filePath} (SKIP_SIGNING=true)`); |
| return; |
| } |
|
|
| const platform = process.platform; |
|
|
| if (platform === 'darwin') { |
| const identity = process.env.APPLE_IDENTITY || '-'; |
| console.log(`Signing ${filePath} (Identity: ${identity})...`); |
|
|
| const args = [ |
| '--sign', |
| identity, |
| '--force', |
| '--timestamp', |
| '--options', |
| 'runtime', |
| ]; |
|
|
| if (existsSync(entitlementsPath)) { |
| args.push('--entitlements', entitlementsPath); |
| } |
|
|
| args.push(filePath); |
|
|
| runCommand('codesign', args); |
| } else if (platform === 'win32') { |
| const args = ['sign']; |
|
|
| if (process.env.WINDOWS_PFX_FILE && process.env.WINDOWS_PFX_PASSWORD) { |
| args.push( |
| '/f', |
| process.env.WINDOWS_PFX_FILE, |
| '/p', |
| process.env.WINDOWS_PFX_PASSWORD, |
| ); |
| } else { |
| args.push('/a'); |
| } |
|
|
| args.push( |
| '/fd', |
| 'SHA256', |
| '/td', |
| 'SHA256', |
| '/tr', |
| 'http://timestamp.digicert.com', |
| filePath, |
| ); |
|
|
| console.log(`Signing ${filePath}...`); |
| try { |
| runCommand('signtool', args, { stdio: 'pipe' }); |
| } catch (e) { |
| let msg = e.message; |
| if (process.env.WINDOWS_PFX_PASSWORD) { |
| msg = msg.replaceAll(process.env.WINDOWS_PFX_PASSWORD, '******'); |
| } |
| throw new Error(msg); |
| } |
| } else if (platform === 'linux') { |
| console.log(`Skipping signing for ${filePath} on Linux.`); |
| } |
| } |
|
|
| console.log('Build Binary Script Started...'); |
|
|
| |
| if (existsSync(distDir)) { |
| console.log('Cleaning dist directory...'); |
| rmSync(distDir, { recursive: true, force: true }); |
| } |
| mkdirSync(distDir, { recursive: true }); |
|
|
| |
| console.log('Running npm clean, install, and bundle...'); |
| try { |
| runCommand('npm', ['run', 'clean']); |
| runCommand('npm', ['install']); |
| runCommand('npm', ['run', 'bundle']); |
| } catch (e) { |
| console.error('Build step failed:', e.message); |
| process.exit(1); |
| } |
|
|
| |
| |
| const ripgrepVendorSrc = join(root, 'packages/core/vendor/ripgrep'); |
| const ripgrepVendorDest = join(bundleDir, 'vendor', 'ripgrep'); |
| if (existsSync(ripgrepVendorSrc)) { |
| const rgBinName = `rg-${process.platform}-${process.arch}${ |
| process.platform === 'win32' ? '.exe' : '' |
| }`; |
| const rgSrc = join(ripgrepVendorSrc, rgBinName); |
| if (existsSync(rgSrc)) { |
| mkdirSync(ripgrepVendorDest, { recursive: true }); |
| cpSync(rgSrc, join(ripgrepVendorDest, rgBinName), { dereference: true }); |
| console.log(`Copied ${rgBinName} to bundle/vendor/ripgrep/`); |
| } else { |
| console.warn( |
| `Warning: bundled ripgrep binary not found for ${process.platform}/${process.arch} at ${rgSrc}. ` + |
| `The SEA will fall back to system grep at runtime.`, |
| ); |
| } |
| } |
|
|
| |
| const includeNativeModules = process.env.BUNDLE_NATIVE_MODULES !== 'false'; |
| console.log(`Include Native Modules: ${includeNativeModules}`); |
|
|
| if (includeNativeModules) { |
| console.log('Staging and signing native modules...'); |
| |
| if (existsSync(stagingDir)) |
| rmSync(stagingDir, { recursive: true, force: true }); |
| mkdirSync(stagingDir, { recursive: true }); |
|
|
| |
| const lydellSrc = join(root, 'node_modules/@lydell'); |
| const lydellStaging = join(stagingDir, 'node_modules/@lydell'); |
|
|
| if (existsSync(lydellSrc)) { |
| mkdirSync(dirname(lydellStaging), { recursive: true }); |
| cpSync(lydellSrc, lydellStaging, { recursive: true }); |
| } else { |
| console.warn( |
| 'Warning: @lydell/node-pty not found in node_modules. Native terminal features may fail.', |
| ); |
| } |
|
|
| |
| const githubSrc = join(root, 'node_modules/@github'); |
| const githubStaging = join(stagingDir, 'node_modules/@github'); |
|
|
| if (existsSync(githubSrc)) { |
| mkdirSync(dirname(githubStaging), { recursive: true }); |
| cpSync(githubSrc, githubStaging, { recursive: true }); |
| } else { |
| console.warn( |
| 'Warning: @github/keytar not found in node_modules. Secure keychain features will use file fallback.', |
| ); |
| } |
|
|
| |
| try { |
| const nodeFiles = globSync('**/*.node', { |
| cwd: stagingDir, |
| absolute: true, |
| }); |
| for (const file of nodeFiles) { |
| signFile(file); |
| } |
| } catch (e) { |
| console.warn('Warning: Failed to sign native modules:', e.code); |
| } |
| } else { |
| console.log('Skipping native modules bundling (BUNDLE_NATIVE_MODULES=false)'); |
| } |
|
|
| |
| console.log('Generating SEA configuration and manifest...'); |
| const packageJson = JSON.parse( |
| readFileSync(join(root, 'package.json'), 'utf8'), |
| ); |
|
|
| |
| const sha256 = (content) => createHash('sha256').update(content).digest('hex'); |
|
|
| const assets = { |
| 'manifest.json': 'bundle/manifest.json', |
| }; |
|
|
| const manifest = { |
| main: 'gemini.mjs', |
| mainHash: '', |
| version: packageJson.version, |
| files: [], |
| }; |
|
|
| |
| const jsFiles = globSync('*.js', { cwd: bundleDir }); |
| for (const jsFile of jsFiles) { |
| const fsPath = join(bundleDir, jsFile); |
| const content = readFileSync(fsPath); |
| const hash = sha256(content); |
|
|
| |
| if (jsFile === 'gemini.js') { |
| assets['gemini.mjs'] = fsPath; |
| manifest.mainHash = hash; |
| } else { |
| |
| assets[jsFile] = fsPath; |
| manifest.files.push({ key: jsFile, path: jsFile, hash: hash }); |
| } |
| } |
|
|
| |
| function addAssetsFromDir(baseDir, runtimePrefix) { |
| const fullDir = join(stagingDir, baseDir); |
| if (!existsSync(fullDir)) return; |
|
|
| const items = globSync('**/*', { cwd: fullDir, nodir: true }); |
| for (const item of items) { |
| const relativePath = join(runtimePrefix, item); |
| const assetKey = `files:${relativePath}`; |
| const fsPath = join(fullDir, item); |
|
|
| |
| const content = readFileSync(fsPath); |
| const hash = sha256(content); |
|
|
| assets[assetKey] = fsPath; |
| manifest.files.push({ key: assetKey, path: relativePath, hash: hash }); |
| } |
| } |
|
|
| |
| const sbFiles = globSync('sandbox-macos-*.sb', { cwd: bundleDir }); |
| for (const sbFile of sbFiles) { |
| const fsPath = join(bundleDir, sbFile); |
| const content = readFileSync(fsPath); |
| const hash = sha256(content); |
| assets[sbFile] = fsPath; |
| manifest.files.push({ key: sbFile, path: sbFile, hash: hash }); |
| } |
|
|
| |
| const policyDir = join(bundleDir, 'policies'); |
| if (existsSync(policyDir)) { |
| const policyFiles = globSync('*.toml', { cwd: policyDir }); |
| for (const policyFile of policyFiles) { |
| const fsPath = join(policyDir, policyFile); |
| const relativePath = join('policies', policyFile); |
| const content = readFileSync(fsPath); |
| const hash = sha256(content); |
| |
| |
| const assetKey = `policies:${policyFile}`; |
| assets[assetKey] = fsPath; |
| manifest.files.push({ key: assetKey, path: relativePath, hash: hash }); |
| } |
| } |
|
|
| |
| |
| |
| |
| if (existsSync(ripgrepVendorDest)) { |
| const rgFiles = globSync('*', { cwd: ripgrepVendorDest, nodir: true }); |
| for (const rgFile of rgFiles) { |
| const fsPath = join(ripgrepVendorDest, rgFile); |
| const relativePath = join('vendor', 'ripgrep', rgFile); |
| const content = readFileSync(fsPath); |
| const hash = sha256(content); |
| const assetKey = `vendor:${rgFile}`; |
| assets[assetKey] = fsPath; |
| manifest.files.push({ key: assetKey, path: relativePath, hash: hash }); |
| } |
| } |
|
|
| |
| if (includeNativeModules) { |
| addAssetsFromDir('node_modules/@lydell', 'node_modules/@lydell'); |
| addAssetsFromDir('node_modules/@github', 'node_modules/@github'); |
| } |
|
|
| writeFileSync(manifestPath, JSON.stringify(manifest, null, 2)); |
|
|
| const seaConfig = { |
| main: 'sea/sea-launch.cjs', |
| output: 'dist/sea-prep.blob', |
| disableExperimentalSEAWarning: true, |
| assets: assets, |
| }; |
|
|
| writeFileSync(seaConfigPath, JSON.stringify(seaConfig, null, 2)); |
| console.log(`Configured ${Object.keys(assets).length} embedded assets.`); |
|
|
| |
| console.log('Generating SEA blob...'); |
| try { |
| runCommand('node', ['--experimental-sea-config', 'sea-config.json']); |
| } catch (e) { |
| console.error('Failed to generate SEA blob:', e.message); |
| |
| if (existsSync(seaConfigPath)) rmSync(seaConfigPath); |
| if (existsSync(manifestPath)) rmSync(manifestPath); |
| if (existsSync(stagingDir)) |
| rmSync(stagingDir, { recursive: true, force: true }); |
| process.exit(1); |
| } |
|
|
| |
| const blobPath = join(distDir, 'sea-prep.blob'); |
| if (!existsSync(blobPath)) { |
| console.error('Error: sea-prep.blob not found in dist/'); |
| process.exit(1); |
| } |
|
|
| |
| const platform = process.platform; |
| const arch = process.arch; |
| const targetName = `${platform}-${arch}`; |
| console.log(`Targeting: ${targetName}`); |
|
|
| const targetDir = join(distDir, targetName); |
| mkdirSync(targetDir, { recursive: true }); |
|
|
| const nodeBinary = process.execPath; |
| const binaryName = platform === 'win32' ? 'gemini.exe' : 'gemini'; |
| const targetBinaryPath = join(targetDir, binaryName); |
|
|
| console.log(`Copying node binary from ${nodeBinary} to ${targetBinaryPath}...`); |
| copyFileSync(nodeBinary, targetBinaryPath); |
|
|
| if (platform === 'darwin') { |
| console.log(`Thinning universal binary for ${arch}...`); |
| try { |
| |
| runCommand('lipo', [ |
| targetBinaryPath, |
| '-thin', |
| arch, |
| '-output', |
| targetBinaryPath, |
| ]); |
| } catch (e) { |
| console.log(`Skipping lipo thinning: ${e.message}`); |
| } |
| } |
|
|
| |
| removeSignature(targetBinaryPath); |
|
|
| |
| console.log('Copying additional resources...'); |
| if (existsSync(bundleDir)) { |
| cpSync(bundleDir, targetDir, { recursive: true }); |
| } |
|
|
| |
| const filesToRemove = [ |
| 'gemini.mjs', |
| 'gemini.mjs.map', |
| 'gemini-sea.cjs', |
| 'sea-launch.cjs', |
| 'manifest.json', |
| 'native_modules', |
| 'policies', |
| ]; |
|
|
| filesToRemove.forEach((f) => { |
| const p = join(targetDir, f); |
| if (existsSync(p)) rmSync(p, { recursive: true, force: true }); |
| }); |
|
|
| |
| const jsFilesToRemove = globSync('*.{js,js.map}', { cwd: targetDir }); |
| for (const f of jsFilesToRemove) { |
| rmSync(join(targetDir, f)); |
| } |
|
|
| |
| const sbFilesToRemove = globSync('sandbox-macos-*.sb', { cwd: targetDir }); |
| for (const f of sbFilesToRemove) { |
| rmSync(join(targetDir, f)); |
| } |
|
|
| |
| console.log('Injecting SEA blob...'); |
| const sentinelFuse = 'NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2'; |
|
|
| try { |
| chmodSync(targetBinaryPath, 0o755); |
| const args = [ |
| 'postject', |
| targetBinaryPath, |
| 'NODE_SEA_BLOB', |
| blobPath, |
| '--sentinel-fuse', |
| sentinelFuse, |
| ]; |
|
|
| if (platform === 'darwin') { |
| args.push('--macho-segment-name', 'NODE_SEA'); |
| } |
|
|
| runCommand('npx', ['--yes', ...args]); |
| console.log('Injection successful.'); |
| } catch (e) { |
| console.error('Postject failed:', e.message); |
| process.exit(1); |
| } |
|
|
| |
| console.log('Signing final executable...'); |
| try { |
| signFile(targetBinaryPath); |
| } catch (e) { |
| console.warn('Warning: Final signing failed:', e.code); |
| console.warn('Continuing without signing...'); |
| } |
|
|
| |
| console.log('Cleaning up artifacts...'); |
| rmSync(blobPath); |
| if (existsSync(seaConfigPath)) rmSync(seaConfigPath); |
| if (existsSync(manifestPath)) rmSync(manifestPath); |
| if (existsSync(stagingDir)) |
| rmSync(stagingDir, { recursive: true, force: true }); |
|
|
| console.log(`Binary built successfully in ${targetDir}`); |
|
|