Build and Tooling
リポジトリ: anomalyco/opencode 分析日: 2026-03-05
概要
Bun + Turborepo をベースとしたモノレポのビルドパイプラインを分析する。特に Bun の compile オプションによるスタンドアロンバイナリ生成、define による静的値埋め込み、Nix flake による再現可能ビルド、そしてマルチプラットフォーム配布(npm + Homebrew + AUR + Docker + Tauri/Electron デスクトップ)を統合する仕組みに注目する。CLI ツールのビルドから配布までを一気通貫で TypeScript スクリプトとして記述するプラクティスが特徴的である。
背景にある原則
ビルドスクリプトをアプリケーションと同じ言語で書く: シェルスクリプトや Makefile ではなく、TypeScript(Bun)でビルドスクリプトを記述することで、型安全性と IDE 支援を享受しつつ、アプリケーションコードと同じツールチェーンで保守できる。
script/publish.tsがバージョン計算・ビルド・npm publish・AUR/Homebrew 更新・Docker push を一つの言語で統合している。コンパイル時定数で環境差を吸収する:
Bun.build()のdefineオプションでOPENCODE_VERSION、OPENCODE_MIGRATIONS、OPENCODE_CHANNEL等をバイナリに焼き込むことで、ランタイムの設定ファイル読み込みや動的解決を排除し、単一バイナリの自己完結性を高める。段階的なビルド依存で並列性を確保する: Turborepo の
dependsOnを最小限に設定し(buildタスクはdependsOn: [])、不要な依存を排除して並列ビルドを最大化する。テストだけが^buildに依存する設計。配布チャネルの統一抽象:
@opencode-ai/scriptパッケージがchannel(latest/beta/dev 等)、version、releaseフラグを一元管理し、すべてのビルド・公開スクリプトがこの共通抽象を参照する。ブランチ名から自動でチャネルを決定する仕組みにより、preview/stable の配布を同一パイプラインで処理する。
実例と分析
Bun.build() によるスタンドアロンバイナリ生成
ビルドスクリプトは Bun.build() の compile オプションを活用して、Node.js や Bun のインストールが不要なスタンドアロンバイナリを生成する。全 11 ターゲット(linux/darwin/win32 x arm64/x64 + baseline/musl バリアント)に対応し、--single フラグで現在のプラットフォームのみビルドする開発モードも用意されている。
// packages/opencode/script/build.ts:171-195
await Bun.build({
conditions: ["browser"],
tsconfig: "./tsconfig.json",
plugins: [solidPlugin],
sourcemap: "external",
compile: {
autoloadBunfig: false,
autoloadDotenv: false,
autoloadTsconfig: true,
autoloadPackageJson: true,
target: name.replace(pkg.name, "bun") as any,
outfile: `dist/${name}/bin/opencode`,
execArgv: [`--user-agent=opencode/${Script.version}`, "--use-system-ca", "--"],
windows: {},
},
entrypoints: ["./src/index.ts", parserWorker, workerPath],
define: {
OPENCODE_VERSION: `'${Script.version}'`,
OPENCODE_MIGRATIONS: JSON.stringify(migrations),
OTUI_TREE_SITTER_WORKER_PATH: bunfsRoot + workerRelativePath,
OPENCODE_WORKER_PATH: workerPath,
OPENCODE_CHANNEL: `'${Script.channel}'`,
OPENCODE_LIBC: item.os === "linux" ? `'${item.abi ?? "glibc"}'` : "",
},
});define による静的値埋め込みと型安全な参照
define で埋め込んだ値は、アプリケーション側で declare global や declare const で型宣言し、存在しない場合のフォールバックも用意する。これにより開発時(ビルドなし)でもアプリが動作する。
// packages/opencode/src/installation/index.ts:10-13
declare global {
const OPENCODE_VERSION: string;
const OPENCODE_CHANNEL: string;
}
// packages/opencode/src/installation/index.ts:192-193
export const VERSION = typeof OPENCODE_VERSION === "string" ? OPENCODE_VERSION : "local";
export const CHANNEL = typeof OPENCODE_CHANNEL === "string" ? OPENCODE_CHANNEL : "local";// packages/opencode/src/storage/db.ts:16
declare const OPENCODE_MIGRATIONS: { sql: string; timestamp: number; }[] | undefined;マイグレーション SQL をビルド時にバイナリへ埋め込むことで、バイナリ単体でデータベースマイグレーションを実行できる。ファイルシステム上のマイグレーションディレクトリへの依存を排除している。
ビルド時のデータプリフェッチ
ビルドスクリプトはビルド前に外部 API からモデル定義をフェッチし、TypeScript ファイルとして書き出す。これにより、ランタイムのネットワーク依存を排除する。
// packages/opencode/script/build.ts:18-27
const modelsUrl = process.env.OPENCODE_MODELS_URL || "https://models.dev";
const modelsData = process.env.MODELS_DEV_API_JSON
? await Bun.file(process.env.MODELS_DEV_API_JSON).text()
: await fetch(`${modelsUrl}/api.json`).then((x) => x.text());
await Bun.write(
path.join(dir, "src/provider/models-snapshot.ts"),
`// Auto-generated by build.ts - do not edit\nexport const snapshot = ${modelsData} as const\n`,
);環境変数 MODELS_DEV_API_JSON でローカルファイルからのオーバーライドも可能であり、Nix ビルドではこれを利用してネットワークアクセスなしでビルドする(env.MODELS_DEV_API_JSON = "${models-dev}/dist/_api.json")。
Bun Workspaces の catalog パターン
package.json の workspaces.catalog でモノレポ全体の依存バージョンを一元定義し、各パッケージは "catalog:" で参照する。これにより Renovate 等のバージョン管理ツールが単一箇所を更新するだけで済む。
// package.json:19-68 (抜粋)
"workspaces": {
"packages": ["packages/*", "packages/console/*", ...],
"catalog": {
"typescript": "5.8.2",
"zod": "4.1.8",
"hono": "4.10.7",
...
}
}npm 配布のアーキテクチャ別パッケージ分離
npm publish 時にプラットフォーム別バイナリを個別パッケージとして公開し、メインパッケージの optionalDependencies で参照する。postinstall.mjs がインストール環境を検出してバイナリをシンボリックリンクする。
// packages/opencode/script/publish.ts:23-40
await Bun.file(`./dist/${pkg.name}/package.json`).write(
JSON.stringify(
{
name: pkg.name + "-ai",
bin: { [pkg.name]: `./bin/${pkg.name}` },
scripts: {
postinstall: "bun ./postinstall.mjs || node ./postinstall.mjs",
},
version: version,
license: pkg.license,
optionalDependencies: binaries,
},
null,
2,
),
);Nix による再現可能ビルド
flake.nix + nix/*.nix で完全な再現可能ビルドを実現する。node_modules.nix は bun install --frozen-lockfile を FOD(Fixed-Output Derivation)として実行し、outputHashAlgo = "sha256" でハッシュ検証する。hashes.json で各プラットフォームのハッシュを管理し、node_modules_updater でハッシュ更新を自動化する。
-- nix/node_modules.nix:48-58 (抜粋)
buildPhase = ''
export BUN_INSTALL_CACHE_DIR=$(mktemp -d)
bun install \
--cpu="${bunCpu}" \
--os="${bunOs}" \
--filter '!./' \
--filter './packages/opencode' \
--filter './packages/desktop' \
--frozen-lockfile \
--ignore-scripts \
--no-progress
'';CI 最適化: baseline バイナリの自動選択
GitHub Actions で x64 ランナーが AVX2 非対応の場合に対応するため、setup-bun action が x64 ランナーでは baseline バイナリを自動ダウンロードする。これにより CI 環境でのランタイムクラッシュを防止する。
# .github/actions/setup-bun/action.yml:17-25
- name: Get baseline download URL
id: bun-url
shell: bash
run: |
if [ "$RUNNER_ARCH" = "X64" ]; then
V=$(node -p "require('./package.json').packageManager.split('@')[1]")
...
echo "url=...bun-${OS}-x64-baseline.zip" >> "$GITHUB_OUTPUT"
fiScript パッケージによるバージョン・チャネル抽象
packages/script がビルドパイプライン全体の共通ユーティリティとして機能する。バージョン計算ロジック(npm registry から最新版を取得 → bump)、チャネル判定(ブランチ名 or 環境変数)、リリースフラグを一元化する。
// packages/script/src/index.ts:25-47
const CHANNEL = await (async () => {
if (env.OPENCODE_CHANNEL) return env.OPENCODE_CHANNEL;
if (env.OPENCODE_BUMP) return "latest";
if (env.OPENCODE_VERSION && !env.OPENCODE_VERSION.startsWith("0.0.0-")) return "latest";
return await $`git branch --show-current`.text().then((x) => x.trim());
})();
const IS_PREVIEW = CHANNEL !== "latest";
const VERSION = await (async () => {
if (env.OPENCODE_VERSION) return env.OPENCODE_VERSION;
if (IS_PREVIEW) return `0.0.0-${CHANNEL}-${new Date().toISOString().slice(0, 16).replace(/[-:T]/g, "")}`;
const version = await fetch("https://registry.npmjs.org/opencode-ai/latest")
.then((res) => res.json())
.then((data: any) => data.version);
// ... bump logic
})();パターンカタログ
Builder Pattern (生成)
- 解決する問題: マルチプラットフォームバイナリの宣言的ビルド構成
- 適用条件: ビルドターゲットの組み合わせが多く、各ターゲットの差分が少ない場合
- コード例:
packages/opencode/script/build.ts:63-120—allTargets配列でターゲットを宣言的に定義し、ループでビルド実行 - 注意点:
--singleフラグによるターゲット絞り込みで開発時のビルド時間を短縮
Strategy Pattern (振る舞い)
- 解決する問題: 環境(CI/ローカル/Nix)ごとに異なるデータ供給方法
- 適用条件: 同一のビルド結果を異なる入力ソースから生成する場合
- コード例:
packages/opencode/script/build.ts:18-22— 環境変数でローカルファイル or API フェッチを切り替え - 注意点: フォールバック先が明示されていることが重要
Good Patterns
ビルドスクリプトの TypeScript 化 + shebangs:
#!/usr/bin/env bunで始まるスクリプトを直接実行可能にしつつ、@opencode-ai/scriptとして共通ロジックをワークスペースパッケージ化している。シェルスクリプトの脆弱なエラーハンドリングを回避し、Bun.$のテンプレートリテラルで型安全なコマンド実行を実現する。declare + typeof ガードによるビルド時定数の安全な参照:
declare global { const OPENCODE_VERSION: string }で型を宣言し、typeof OPENCODE_VERSION === "string" ? OPENCODE_VERSION : "local"でフォールバック。ビルド済みバイナリと開発時のbun runの両方で動作する。Turborepo の最小依存宣言:
buildタスクのdependsOn: []で不要な依存を排除し、テストのみ^build依存とすることで、ビルド並列性を最大化しつつテストの前提条件だけ保証する。Nix の FOD で node_modules を再現可能に管理:
bun installを Fixed-Output Derivation として実行し、ハッシュで検証。hashes.json+node_modules_updaterパターンで更新フローも整備。
Anti-Patterns / 注意点
postinstall.mjs のフォールバックチェーン:
"bun ./postinstall.mjs || node ./postinstall.mjs"は Bun がインストールされていない環境で Node.js へフォールバックする実用的なパターンだが、2つのランタイムの挙動差異によるバグの温床になりうる。javascript// Bad: ランタイム固有 API を使った postinstall import { $ } from "bun"; await $`chmod 755 ./bin/opencode`; // Better: Node.js 標準 API のみ使用(現在の実装) import fs from "fs"; fs.chmodSync(target, 0o755);ビルドスクリプト内での process.chdir:
build.tsやpublish.tsがprocess.chdir()を使用しているが、これは暗黙のグローバル状態変更であり、スクリプトの合成可能性を損なう。publish.tsでawait import(...)によるスクリプト連鎖実行時に、CWD の状態が予測しにくくなる。typescript// Bad: process.chdir でグローバル状態を変更 process.chdir(dir); await $`bun install`; // Better: コマンド単位で CWD を指定 await $`bun install`.cwd(dir);
導出ルール
[MUST]コンパイル時定数をdefineで埋め込む場合、アプリケーション側でtypeofガード付きフォールバックを必ず用意する- 根拠:
packages/opencode/src/installation/index.ts:192-193でtypeof OPENCODE_VERSION === "string" ? OPENCODE_VERSION : "local"とすることで、ビルドなし開発時にもアプリが動作する
- 根拠:
[MUST]マルチプラットフォームバイナリ配布では、ターゲットマトリクスを宣言的データ構造として定義し、--single相当のフィルタで開発時のビルドを高速化する- 根拠:
packages/opencode/script/build.ts:63-141で 11 ターゲットを配列定義し、--singleフラグで現在プラットフォームのみにフィルタリング
- 根拠:
[SHOULD]モノレポのビルドオーケストレーションでは、タスク間依存を最小限に宣言し、テスト以外のビルドは独立並列実行可能にする- 根拠:
turbo.jsonでbuild.dependsOn: []とし、testのみ^buildに依存させることで並列性を最大化
- 根拠:
[SHOULD]バイナリに必要なランタイムデータ(マイグレーション SQL、外部 API スナップショット等)はビルド時にフェッチ・埋め込みし、実行時のネットワーク/ファイルシステム依存を排除する- 根拠:
build.ts:19-27でモデル API をプリフェッチし、build.ts:39-56でマイグレーション SQL をバイナリに埋め込み
- 根拠:
[SHOULD]ビルド・リリースの共通ロジック(バージョン計算、チャネル判定、チームメンバー一覧)は専用のワークスペースパッケージに集約する- 根拠:
packages/scriptが全スクリプトの共通抽象としてScript.version,Script.channel,Script.releaseを提供
- 根拠:
[AVOID]ビルドスクリプト内でprocess.chdir()を使ってグローバルな CWD を変更する — 代わりにBun.$().cwd(dir)やコマンド単位の CWD 指定を使う- 根拠:
script/publish.tsでスクリプト連鎖実行(await import(...))時に CWD の状態追跡が困難になる
- 根拠:
適用チェックリスト
- [ ] CLI ツールのビルドに
Bun.build({ compile: ... })を検討し、Node.js/Bun ランタイム不要のスタンドアロンバイナリを配布可能にする - [ ] ビルド時に
defineで埋め込む定数には、アプリケーション側にtypeofガード付きフォールバックを実装する - [ ] Turborepo や類似ツールのタスク依存グラフを見直し、不要な
dependsOnを削除して並列性を最大化する - [ ] ランタイムに必要な外部データ(API スナップショット、マイグレーション等)をビルド時にプリフェッチ・埋め込みするパターンを検討する
- [ ] モノレポ内の依存バージョンを workspace catalog(Bun)や類似機能で一元管理し、Renovate 等の更新対象を単一箇所にする
- [ ] ビルド・リリーススクリプトの共通ロジックを専用パッケージに集約し、各スクリプトはその抽象を参照するだけにする
- [ ] Nix flake による再現可能ビルドが必要な場合、node_modules を FOD + ハッシュ検証パターンで管理する