cc-haha/desktop/scripts/build-sidecars.ts
程序员阿江(Relakkes) 3c5549e221 experiment(desktop): merge server + cli into one sidecar binary
Replaces the two ~65MB bun-compiled sidecar binaries with a single
~66MB merged binary. The bun runtime + shared dependency code (anthropic
SDK, MCP SDK, ws, undici, etc) was previously duplicated across both —
merging eliminates that duplication entirely.

Combined with the previous P0 commit (static-import inlining + drop
src/ + node_modules/ from Resources), this brings the macOS .app from
the original 435MB baseline down to 87MB (-80%), and the DMG from 113MB
to 37MB (-67%).

Final breakdown of the 87MB .app:
  Contents/MacOS/claude-sidecar         66MB   (was 57+57=114MB)
  Contents/MacOS/claude-code-desktop    18MB   (Tauri Rust main)
  Contents/Resources/icon.icns           2MB
  + plist + frameworks                  ~1MB

This is essentially the floor — bun runtime + Tauri main + minimum
overhead. Going lower would require swapping toolchains.

Implementation
==============

* desktop/sidecars/claude-sidecar.ts (new): single entrypoint that
  takes a positional mode argument ("server" or "cli") then dispatches
  via `await import('../../src/server/index.ts').startServer()` or
  `await import('../../src/entrypoints/cli.tsx')`. Same env / argv setup
  pattern as the old launchers.
* desktop/sidecars/server-launcher.ts + cli-launcher.ts: deleted.
* desktop/scripts/build-sidecars.ts: compiles only claude-sidecar now.
* desktop/src-tauri/tauri.conf.json: externalBin → ["binaries/claude-sidecar"]
* desktop/src-tauri/src/lib.rs: spawns sidecar with leading "server"
  positional arg.
* src/server/services/conversationService.ts resolveBundledCliPath /
  resolveCliArgs: when current process is claude-sidecar, reuses the
  same exe and spawns it with leading "cli" positional arg. Backward
  compat path for old claude-server / claude-cli pair preserved for the
  bin/claude-haha dev mode.

Verification
============

* claude-sidecar cli --version → 999.0.0-local (Claude Code) ✓
* claude-sidecar cli --help → full Commander spec ✓
* claude-sidecar server --port N → HTTP listening, CronScheduler running ✓
* All three above run in /tmp with no src/ or node_modules/ on disk
* bun test on src/ → 358 pass / 45 fail / 1 error, identical to baseline
  (44 fails are pre-existing on main, unrelated to this change)
* Full DMG round-trip via build-macos-arm64.sh succeeds; new .app
  installs cleanly in /Volumes/

Bundle size summary (vs original baseline)
==========================================

  metric              baseline   final      delta
  .app total          435 MB     87 MB      -348 MB (-80%)
  .dmg                113 MB     37 MB      -76 MB  (-67%)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 00:10:47 +08:00

161 lines
5.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { mkdir } from 'node:fs/promises'
import path from 'node:path'
const desktopRoot = path.resolve(import.meta.dir, '..')
const repoRoot = path.resolve(desktopRoot, '..')
const binariesDir = path.join(desktopRoot, 'src-tauri', 'binaries')
const targetTriple =
process.env.TAURI_ENV_TARGET_TRIPLE ||
process.env.CARGO_BUILD_TARGET ||
(await detectHostTriple())
const bunTarget = mapTargetTripleToBun(targetTriple)
// 编译前先扫一遍 src/ 把所有缺失的 ant-internal 模块在磁盘上 stub 出来。
// 见 desktop/scripts/scan-missing-imports.ts。
console.log('[build-sidecars] scanning for missing imports...')
const scanProc = Bun.spawn(
['bun', 'run', path.join(desktopRoot, 'scripts/scan-missing-imports.ts')],
{ cwd: repoRoot, stdout: 'inherit', stderr: 'inherit' },
)
const scanExit = await scanProc.exited
if (scanExit !== 0) {
throw new Error(`[build-sidecars] scan-missing-imports failed (exit ${scanExit})`)
}
await mkdir(binariesDir, { recursive: true })
// 单一合并 sidecarserver / cli 共享一份 bun runtime + 共享依赖代码。
// 调用方Tauri lib.rs / conversationService通过第一个 positional 参数
// 选择 'server' 或 'cli' 模式,详见 desktop/sidecars/claude-sidecar.ts。
await compileExecutable({
entrypoint: path.join(desktopRoot, 'sidecars/claude-sidecar.ts'),
outfileBase: path.join(binariesDir, `claude-sidecar-${targetTriple}`),
productName: 'Claude Code Sidecar',
bunTarget,
})
console.log(`[build-sidecars] Built desktop sidecar for ${targetTriple} (${bunTarget})`)
async function detectHostTriple() {
const proc = Bun.spawn(['rustc', '-vV'], {
cwd: repoRoot,
stdout: 'pipe',
stderr: 'pipe',
})
const stdout = await new Response(proc.stdout).text()
const stderr = await new Response(proc.stderr).text()
const exitCode = await proc.exited
if (exitCode !== 0) {
throw new Error(`[build-sidecars] rustc -vV failed: ${stderr || stdout}`)
}
const hostLine = stdout
.split('\n')
.map((line) => line.trim())
.find((line) => line.startsWith('host: '))
if (!hostLine) {
throw new Error('[build-sidecars] Could not detect Rust host triple')
}
return hostLine.replace('host: ', '')
}
function mapTargetTripleToBun(triple: string) {
switch (triple) {
case 'aarch64-apple-darwin':
return 'bun-darwin-arm64'
case 'x86_64-apple-darwin':
return 'bun-darwin-x64'
case 'x86_64-pc-windows-msvc':
return 'bun-windows-x64'
case 'aarch64-pc-windows-msvc':
return 'bun-windows-arm64'
case 'x86_64-unknown-linux-gnu':
return 'bun-linux-x64-baseline'
case 'aarch64-unknown-linux-gnu':
return 'bun-linux-arm64'
case 'x86_64-unknown-linux-musl':
return 'bun-linux-x64-musl'
case 'aarch64-unknown-linux-musl':
return 'bun-linux-arm64-musl'
default:
throw new Error(`[build-sidecars] Unsupported target triple: ${triple}`)
}
}
async function compileExecutable({
entrypoint,
outfileBase,
productName,
bunTarget,
}: {
entrypoint: string
outfileBase: string
productName: string
bunTarget: string
}) {
const result = await Bun.build({
entrypoints: [entrypoint],
// minify whitespace + identifiers + dead-code 大概能省 5-15% 的二进制大小,
// 代价是 stack trace 里的函数名变成短名 —— 终端用户场景可接受。
minify: { whitespace: true, identifiers: true, syntax: true },
sourcemap: 'none',
target: 'bun',
// 可选 npm 包:开 telemetry / 用 sharp 图像 / 用 Bedrock/Vertex 等
// 替代 provider 时才需要,全部不在顶层 package.json 里。标 external
// 让 bun build 跳过解析;运行时 import 在没装时自然失败,由 try/catch
// 或 feature() gate 兜底。
external: [
// OpenTelemetry exporters开 OTEL_* env 时才加载)
'@opentelemetry/exporter-trace-otlp-grpc',
'@opentelemetry/exporter-trace-otlp-http',
'@opentelemetry/exporter-trace-otlp-proto',
'@opentelemetry/exporter-logs-otlp-grpc',
'@opentelemetry/exporter-logs-otlp-http',
'@opentelemetry/exporter-logs-otlp-proto',
'@opentelemetry/exporter-metrics-otlp-grpc',
'@opentelemetry/exporter-metrics-otlp-http',
'@opentelemetry/exporter-metrics-otlp-proto',
'@opentelemetry/exporter-prometheus',
// 替代 LLM provider —— 默认不用,用户自装
'@aws-sdk/client-bedrock',
'@aws-sdk/client-sts',
'@anthropic-ai/bedrock-sdk',
'@anthropic-ai/foundry-sdk',
'@anthropic-ai/vertex-sdk',
'@azure/identity',
// ant-internal / 可选工具
'@anthropic-ai/mcpb',
'fflate',
'turndown',
'sharp',
'react-devtools-core',
],
compile: {
target: bunTarget,
outfile: outfileBase,
autoloadTsconfig: true,
autoloadPackageJson: true,
windows: {
title: productName,
publisher: 'Claude Code',
description: productName,
hideConsole: true,
},
},
})
if (!result.success) {
const logs = result.logs.map((log) => log.message).join('\n')
throw new Error(`[build-sidecars] Failed to compile ${productName}:\n${logs}`)
}
const outputPath = result.outputs[0]?.path
console.log(`[build-sidecars] ${productName} -> ${outputPath ?? outfileBase}`)
}