mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-10 22:21:22 -07:00
Build binary (#18933)
Co-authored-by: Gal Zahavi <38544478+galz10@users.noreply.github.com>
This commit is contained in:
424
scripts/build_binary.js
Normal file
424
scripts/build_binary.js
Normal file
@@ -0,0 +1,424 @@
|
||||
/**
|
||||
* @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,
|
||||
} 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) {
|
||||
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);
|
||||
}
|
||||
|
||||
// 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.',
|
||||
);
|
||||
}
|
||||
|
||||
// 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');
|
||||
|
||||
// Read Main Bundle
|
||||
const geminiBundlePath = join(root, 'bundle/gemini.js');
|
||||
const geminiContent = readFileSync(geminiBundlePath);
|
||||
const geminiHash = sha256(geminiContent);
|
||||
|
||||
const assets = {
|
||||
'gemini.mjs': geminiBundlePath, // Use .js source but map to .mjs for runtime ESM
|
||||
'manifest.json': 'bundle/manifest.json',
|
||||
};
|
||||
|
||||
const manifest = {
|
||||
main: 'gemini.mjs',
|
||||
mainHash: geminiHash,
|
||||
version: packageJson.version,
|
||||
files: [],
|
||||
};
|
||||
|
||||
// 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 assets from Staging
|
||||
if (includeNativeModules) {
|
||||
addAssetsFromDir('node_modules/@lydell', 'node_modules/@lydell');
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
// 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.js',
|
||||
'gemini.mjs',
|
||||
'gemini.js.map',
|
||||
'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 .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 {
|
||||
const args = [
|
||||
'postject',
|
||||
targetBinaryPath,
|
||||
'NODE_SEA_BLOB',
|
||||
blobPath,
|
||||
'--sentinel-fuse',
|
||||
sentinelFuse,
|
||||
];
|
||||
|
||||
if (platform === 'darwin') {
|
||||
args.push('--macho-segment-name', 'NODE_SEA');
|
||||
}
|
||||
|
||||
runCommand('npx', 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}`);
|
||||
21
scripts/entitlements.plist
Normal file
21
scripts/entitlements.plist
Normal file
@@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<!-- Allow JIT compilation (Required for Node.js/V8) -->
|
||||
<key>com.apple.security.cs.allow-jit</key>
|
||||
<true/>
|
||||
|
||||
<!-- Allow executable memory modification (Required for Node.js/V8) -->
|
||||
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
|
||||
<true/>
|
||||
|
||||
<!-- Allow loading unsigned libraries (Helpful for native modules extracted to temp) -->
|
||||
<key>com.apple.security.cs.disable-library-validation</key>
|
||||
<true/>
|
||||
|
||||
<!-- Allow access to environment variables (Standard for CLI tools) -->
|
||||
<key>com.apple.security.cs.allow-dyld-environment-variables</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
Reference in New Issue
Block a user