Download scripts/build_binary.js from SaylorTwift/gemini-cli: direct link, hf CLI and curl.
- Browser
- Download file 14.8 kB
-
https://huggingface.co/SaylorTwift/gemini-cli/resolve/main/scripts/build_binary.js
- Command line
-
hf download hf://SaylorTwift/gemini-cli/scripts/build_binary.js
-
curl -L -o build_binary.js https://huggingface.co/SaylorTwift/gemini-cli/resolve/main/scripts/build_binary.js
14.8 kB
| /** | |
| * @license | |
| * Copyright 2025 Google LLC | |
| * SPDX-License-Identifier: Apache-2.0 | |
| */ | |
| 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'); | |
| // --- Helper Functions --- | |
| /** | |
| * Safely executes a command using spawnSync. | |
| * @param {string} command | |
| * @param {string[]} args | |
| * @param {object} options | |
| */ | |
| function runCommand(command, args, options = {}) { | |
| let finalCommand = command; | |
| let useShell = options.shell || false; | |
| // On Windows, npm/npx are batch files and need a shell | |
| 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; | |
| } | |
| /** | |
| * Removes existing digital signatures from a binary. | |
| * @param {string} filePath | |
| */ | |
| 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 { | |
| // Best effort: Ignore failures | |
| } | |
| } | |
| /** | |
| * Signs a binary using hardcoded tools for the platform. | |
| * @param {string} filePath | |
| */ | |
| 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...'); | |
| // 1. Clean dist | |
| if (existsSync(distDir)) { | |
| console.log('Cleaning dist directory...'); | |
| rmSync(distDir, { recursive: true, force: true }); | |
| } | |
| mkdirSync(distDir, { recursive: true }); | |
| // 2. Build Bundle | |
| 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); | |
| } | |
| // 2b. Copy host-platform ripgrep binary into the bundle for the SEA. | |
| // (npm tarballs omit these to stay under the registry upload limit.) | |
| 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.`, | |
| ); | |
| } | |
| } | |
| // 3. Stage & Sign Native Modules | |
| const includeNativeModules = process.env.BUNDLE_NATIVE_MODULES !== 'false'; | |
| console.log(`Include Native Modules: ${includeNativeModules}`); | |
| if (includeNativeModules) { | |
| console.log('Staging and signing native modules...'); | |
| // Prepare staging | |
| if (existsSync(stagingDir)) | |
| rmSync(stagingDir, { recursive: true, force: true }); | |
| mkdirSync(stagingDir, { recursive: true }); | |
| // Copy @lydell/node-pty to staging | |
| 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.', | |
| ); | |
| } | |
| // Copy @github/keytar to staging | |
| 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.', | |
| ); | |
| } | |
| // Sign Staged .node files | |
| 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)'); | |
| } | |
| // 4. Generate SEA Configuration and Manifest | |
| console.log('Generating SEA configuration and manifest...'); | |
| const packageJson = JSON.parse( | |
| readFileSync(join(root, 'package.json'), 'utf8'), | |
| ); | |
| // Helper to calc hash | |
| 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: [], | |
| }; | |
| // Add all javascript chunks from the bundle directory | |
| const jsFiles = globSync('*.js', { cwd: bundleDir }); | |
| for (const jsFile of jsFiles) { | |
| const fsPath = join(bundleDir, jsFile); | |
| const content = readFileSync(fsPath); | |
| const hash = sha256(content); | |
| // Node SEA requires the main entry point to be explicitly mapped | |
| if (jsFile === 'gemini.js') { | |
| assets['gemini.mjs'] = fsPath; | |
| manifest.mainHash = hash; | |
| } else { | |
| // Other chunks need to be mapped exactly as they are named so dynamic imports find them | |
| assets[jsFile] = fsPath; | |
| manifest.files.push({ key: jsFile, path: jsFile, hash: hash }); | |
| } | |
| } | |
| // Helper to recursively find files from STAGING | |
| 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); | |
| // Calc hash | |
| const content = readFileSync(fsPath); | |
| const hash = sha256(content); | |
| assets[assetKey] = fsPath; | |
| manifest.files.push({ key: assetKey, path: relativePath, hash: hash }); | |
| } | |
| } | |
| // Add sb files | |
| 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 }); | |
| } | |
| // Add policy files | |
| 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); | |
| // Use a unique key to avoid collision if filenames overlap (though unlikely here) | |
| // But sea-launch writes to 'path', so key is just for lookup. | |
| const assetKey = `policies:${policyFile}`; | |
| assets[assetKey] = fsPath; | |
| manifest.files.push({ key: assetKey, path: relativePath, hash: hash }); | |
| } | |
| } | |
| // Add ripgrep binary (copied in step 2b). Must be registered here so that | |
| // sea-launch.cjs extracts it to runtimeDir/vendor/ripgrep/ on startup; the | |
| // runtime resolver in packages/core/src/tools/ripGrep.ts uses __dirname- | |
| // relative paths to find it. | |
| 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 }); | |
| } | |
| } | |
| // Add assets from Staging | |
| 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.`); | |
| // 5. Generate SEA Blob | |
| 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); | |
| // Cleanup | |
| if (existsSync(seaConfigPath)) rmSync(seaConfigPath); | |
| if (existsSync(manifestPath)) rmSync(manifestPath); | |
| if (existsSync(stagingDir)) | |
| rmSync(stagingDir, { recursive: true, force: true }); | |
| process.exit(1); | |
| } | |
| // Check blob existence | |
| const blobPath = join(distDir, 'sea-prep.blob'); | |
| if (!existsSync(blobPath)) { | |
| console.error('Error: sea-prep.blob not found in dist/'); | |
| process.exit(1); | |
| } | |
| // 6. Identify Target & Prepare Binary | |
| 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 { | |
| // Attempt to thin the binary. Will fail safely if it's not a fat binary. | |
| runCommand('lipo', [ | |
| targetBinaryPath, | |
| '-thin', | |
| arch, | |
| '-output', | |
| targetBinaryPath, | |
| ]); | |
| } catch (e) { | |
| console.log(`Skipping lipo thinning: ${e.message}`); | |
| } | |
| } | |
| // Remove existing signature using helper | |
| removeSignature(targetBinaryPath); | |
| // Copy standard bundle assets (policies, .sb files) | |
| console.log('Copying additional resources...'); | |
| if (existsSync(bundleDir)) { | |
| cpSync(bundleDir, targetDir, { recursive: true }); | |
| } | |
| // Clean up source JS files from output (we only want embedded) | |
| 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 }); | |
| }); | |
| // Remove all chunk and entry .js/.js.map files | |
| const jsFilesToRemove = globSync('*.{js,js.map}', { cwd: targetDir }); | |
| for (const f of jsFilesToRemove) { | |
| rmSync(join(targetDir, f)); | |
| } | |
| // Remove .sb files from targetDir | |
| const sbFilesToRemove = globSync('sandbox-macos-*.sb', { cwd: targetDir }); | |
| for (const f of sbFilesToRemove) { | |
| rmSync(join(targetDir, f)); | |
| } | |
| // 7. Inject Blob | |
| 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); | |
| } | |
| // 8. Final Signing | |
| console.log('Signing final executable...'); | |
| try { | |
| signFile(targetBinaryPath); | |
| } catch (e) { | |
| console.warn('Warning: Final signing failed:', e.code); | |
| console.warn('Continuing without signing...'); | |
| } | |
| // 9. Cleanup | |
| 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}`); | |