|
| 1 | +import { getMacroDefines } from "./scripts/defines.ts"; |
| 2 | +import { exit } from "process"; |
| 3 | +import { join, resolve } from "path"; |
| 4 | +import { readFile, writeFile } from "fs/promises"; |
| 5 | + |
| 6 | +const outfile = process.platform === "win32" ? "claude.exe" : "claude"; |
| 7 | + |
| 8 | +// Use the currently running bun executable |
| 9 | +const bunExe = process.execPath; |
| 10 | + |
| 11 | +// Collect FEATURE_* env vars from environment |
| 12 | +const features = Object.keys(process.env) |
| 13 | + .filter(k => k.startsWith("FEATURE_")) |
| 14 | + .map(k => k.replace("FEATURE_", "")); |
| 15 | + |
| 16 | +// Auto-enable CHICAGO_MCP so @ant packages (computer-use-mcp, etc.) |
| 17 | +// are bundled into the standalone exe. Without this flag, the feature-gated |
| 18 | +// dynamic imports are tree-shaken and the native .node files are not embedded. |
| 19 | +if (!features.includes("CHICAGO_MCP")) { |
| 20 | + features.push("CHICAGO_MCP"); |
| 21 | +} |
| 22 | + |
| 23 | +const defines = getMacroDefines(); |
| 24 | + |
| 25 | +// Build --define flags |
| 26 | +const defineArgs = Object.entries(defines).flatMap(([k, v]) => [ |
| 27 | + "--define", |
| 28 | + `${k}:${v}`, |
| 29 | +]); |
| 30 | + |
| 31 | +// Pass BUNDLED_MODE flag so ripgrepAsset.ts knows we're in compiled mode |
| 32 | +const defineArgsWithBundled = [ |
| 33 | + ...defineArgs, |
| 34 | + "--define", |
| 35 | + `BUNDLED_MODE:"true"`, |
| 36 | +]; |
| 37 | + |
| 38 | +// Build --feature flags |
| 39 | +const featureArgs = features.flatMap(f => ["--feature", f]); |
| 40 | + |
| 41 | +// ─── Native module embedding ────────────────────────────────────────────────── |
| 42 | +// bun build --compile embeds .node files as assets. When the bundler sees |
| 43 | +// process.env.XXX_NODE_PATH with the var set to an absolute .node path, |
| 44 | +// it rewrites the string to the bunfs asset path. This lets the runtime |
| 45 | +// require() the embedded .node from within the compiled exe. |
| 46 | +// |
| 47 | +// Paths must use forward slashes and be absolute at compile time. |
| 48 | +const repoRoot = resolve(__dirname); |
| 49 | + |
| 50 | +const nativeNodePaths: Record<string, string> = { |
| 51 | + // @ant packages — macOS only. Path is used at compile time for Bun asset embedding. |
| 52 | + // Runtime: TS files check process.platform !== "darwin" and skip native load. |
| 53 | + COMPUTER_USE_INPUT_NODE_PATH: join(repoRoot, |
| 54 | + "packages/@ant/computer-use-input/prebuilds/arm64-darwin/computer-use-input.node"), |
| 55 | + COMPUTER_USE_SWIFT_NODE_PATH: join(repoRoot, |
| 56 | + "packages/@ant/computer-use-swift/prebuilds/arm64-darwin/computer_use.node"), |
| 57 | + |
| 58 | + // vendor modules — cross-platform (win32/linux/darwin) |
| 59 | + AUDIO_CAPTURE_NODE_PATH: join(repoRoot, |
| 60 | + `vendor/audio-capture/${process.arch}-${process.platform}/audio-capture.node`), |
| 61 | + IMAGE_PROCESSOR_NODE_PATH: join(repoRoot, |
| 62 | + `vendor/image-processor/${process.arch}-${process.platform}/image-processor.node`), |
| 63 | + // modifiers and url-handler are macOS only — paths point to darwin builds |
| 64 | + MODIFIERS_NODE_PATH: join(repoRoot, |
| 65 | + `vendor/modifiers-napi/${process.arch}-darwin/modifiers.node`), |
| 66 | + URL_HANDLER_NODE_PATH: join(repoRoot, |
| 67 | + `vendor/url-handler/${process.arch}-darwin/url-handler.node`), |
| 68 | +}; |
| 69 | + |
| 70 | +// Build env with native paths (forward slashes for Bun compatibility) |
| 71 | +const compileEnv: Record<string, string> = { |
| 72 | + ...process.env, |
| 73 | + ...Object.fromEntries( |
| 74 | + Object.entries(nativeNodePaths).map(([k, v]) => [k, v.replace(/\\/g, "/")]), |
| 75 | + ), |
| 76 | +}; |
| 77 | + |
| 78 | +// ─── Step 0: Generate ripgrep base64 asset ─────────────────────────────────── |
| 79 | +// Bun's bundler does not support ?url imports or arbitrary file embedding |
| 80 | +// for non-.node files. The only reliable way to embed a binary into the |
| 81 | +// compiled exe is to base64-encode it and store it as a JS string constant. |
| 82 | +// At runtime, we decode to a temp file and execute. |
| 83 | +async function generateRipgrepAsset() { |
| 84 | + const rgCache = join(repoRoot, |
| 85 | + `node_modules/.bun/@anthropic-ai+claude-agent-sdk@0.2.87+3c5d820c62823f0b/node_modules/@anthropic-ai/claude-agent-sdk/vendor/ripgrep`); |
| 86 | + |
| 87 | + const ripgrepBinaries: Record<string, string> = {} |
| 88 | + |
| 89 | + // Map platform+arch to filename |
| 90 | + const allPlatforms: Array<{ key: string; subdir: string; file: string }> = [ |
| 91 | + { key: 'windows_x64', subdir: 'x64-win32', file: 'rg.exe' }, |
| 92 | + { key: 'darwin_x64', subdir: 'x64-darwin', file: 'rg' }, |
| 93 | + { key: 'darwin_arm64', subdir: 'arm64-darwin', file: 'rg' }, |
| 94 | + { key: 'linux_x64', subdir: 'x64-linux', file: 'rg' }, |
| 95 | + { key: 'linux_arm64', subdir: 'arm64-linux', file: 'rg' }, |
| 96 | + ]; |
| 97 | + |
| 98 | + // Only embed the current platform's binary to minimize exe size. |
| 99 | + // The other platforms are available in the SDK for dev-mode fallback. |
| 100 | + const currentPlatformKey = (() => { |
| 101 | + if (process.platform === 'win32') return 'windows_x64' |
| 102 | + if (process.platform === 'darwin') return process.arch === 'arm64' ? 'darwin_arm64' : 'darwin_x64' |
| 103 | + return process.arch === 'arm64' ? 'linux_arm64' : 'linux_x64' |
| 104 | + })() |
| 105 | + |
| 106 | + for (const { key, subdir, file } of allPlatforms) { |
| 107 | + if (key !== currentPlatformKey) continue // Skip other platforms |
| 108 | + const binPath = join(rgCache, subdir, file); |
| 109 | + try { |
| 110 | + const data = await readFile(binPath); |
| 111 | + ripgrepBinaries[key] = data.toString('base64'); |
| 112 | + console.log(`Encoded ${key}: ${data.length} bytes -> ${Math.round(data.length * 1.37)} chars`); |
| 113 | + } catch (e) { |
| 114 | + console.warn(`Warning: could not read ${binPath}: ${e}`); |
| 115 | + } |
| 116 | + } |
| 117 | + |
| 118 | + // Generate TypeScript asset file |
| 119 | + const assetFile = join(repoRoot, "src", "utils", "ripgrepAssetBase64.ts"); |
| 120 | + const content = `/** |
| 121 | + * AUTO-GENERATED by compile.ts — do not edit manually. |
| 122 | + * Ripgrep binaries encoded as base64 strings. |
| 123 | + * Decoded at runtime to temp files for execution. |
| 124 | + */ |
| 125 | +export const RIPGREP_BINARIES: Record<string, string> = ${JSON.stringify(ripgrepBinaries, null, 2)}; |
| 126 | +`; |
| 127 | + await writeFile(assetFile, content); |
| 128 | + console.log(`Generated ${assetFile}`); |
| 129 | +} |
| 130 | + |
| 131 | +// ─── Step 1: Patch SDK ripgrep path ─────────────────────────────────────────── |
| 132 | +// The SDK's cli.js computes dy_ from import.meta.url which points to B:\~BUN\root\... |
| 133 | +// in --compile mode. Patch it to use path.dirname(process.execPath) instead. |
| 134 | +async function patchRipgrepPaths() { |
| 135 | + // --- Patch bun cache SDK cli.js --- |
| 136 | + const sdkCachePath = join(repoRoot, |
| 137 | + "node_modules/.bun/@anthropic-ai+claude-agent-sdk@0.2.87+3c5d820c62823f0b/node_modules/@anthropic-ai/claude-agent-sdk/cli.js"); |
| 138 | + const sdkContent = await readFile(sdkCachePath, "utf-8"); |
| 139 | + const patchedSdk = sdkContent |
| 140 | + .replace( |
| 141 | + /import\{fileURLToPath as Uy_\}from"url";/, |
| 142 | + ";", |
| 143 | + ) |
| 144 | + .replace( |
| 145 | + /dy_=Uy_\(import\.meta\.url\),dy_=Z16\.join\(dy_,"\.\/"\)/, |
| 146 | + "dy_=Z16.dirname(process.execPath)", |
| 147 | + ); |
| 148 | + if (patchedSdk === sdkContent) { |
| 149 | + console.warn("Warning: SDK patch did not match"); |
| 150 | + } else { |
| 151 | + await writeFile(sdkCachePath, patchedSdk); |
| 152 | + console.log("Patched SDK cli.js (bun cache)"); |
| 153 | + } |
| 154 | +} |
| 155 | + |
| 156 | +// ─── Step 2: Run the compile ─────────────────────────────────────────────────── |
| 157 | +async function run() { |
| 158 | + await generateRipgrepAsset(); |
| 159 | + await patchRipgrepPaths(); |
| 160 | + |
| 161 | + console.log("\nCompiling standalone executable with native modules..."); |
| 162 | + console.log(`Outfile: ${outfile}`); |
| 163 | + console.log(`Defines: ${Object.keys(defines).join(", ")}`); |
| 164 | + console.log(`Native modules:`); |
| 165 | + for (const [k, v] of Object.entries(nativeNodePaths)) { |
| 166 | + console.log(` ${k}=${v}`); |
| 167 | + } |
| 168 | + |
| 169 | + // Use Bun.spawn with CLI because Bun.build({ outfile, compile: true }) |
| 170 | + // does not reliably place the output file on Windows. |
| 171 | + const result = Bun.spawnSync( |
| 172 | + [ |
| 173 | + bunExe, |
| 174 | + "build", |
| 175 | + "--compile", |
| 176 | + "--outfile=" + outfile, |
| 177 | + ...defineArgsWithBundled, |
| 178 | + ...featureArgs, |
| 179 | + "src/entrypoints/cli.tsx", |
| 180 | + ], |
| 181 | + { |
| 182 | + stdio: ["inherit", "inherit", "inherit"], |
| 183 | + env: compileEnv, |
| 184 | + }, |
| 185 | + ); |
| 186 | + |
| 187 | + if (result.exitCode !== 0) { |
| 188 | + console.error("Compile failed with exit code:", result.exitCode); |
| 189 | + exit(1); |
| 190 | + } |
| 191 | + |
| 192 | + console.log(`\nCompiled standalone executable: ${outfile}`); |
| 193 | + if (features.length > 0) { |
| 194 | + console.log(`Features enabled: ${features.join(", ")}`); |
| 195 | + } |
| 196 | +} |
| 197 | + |
| 198 | +run(); |
0 commit comments