Skip to content

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_VERSIONOPENCODE_MIGRATIONSOPENCODE_CHANNEL 等をバイナリに焼き込むことで、ランタイムの設定ファイル読み込みや動的解決を排除し、単一バイナリの自己完結性を高める。

  • 段階的なビルド依存で並列性を確保する: Turborepo の dependsOn を最小限に設定し(build タスクは dependsOn: [])、不要な依存を排除して並列ビルドを最大化する。テストだけが ^build に依存する設計。

  • 配布チャネルの統一抽象: @opencode-ai/script パッケージが channel(latest/beta/dev 等)、versionrelease フラグを一元管理し、すべてのビルド・公開スクリプトがこの共通抽象を参照する。ブランチ名から自動でチャネルを決定する仕組みにより、preview/stable の配布を同一パイプラインで処理する。

実例と分析

Bun.build() によるスタンドアロンバイナリ生成

ビルドスクリプトは Bun.build()compile オプションを活用して、Node.js や Bun のインストールが不要なスタンドアロンバイナリを生成する。全 11 ターゲット(linux/darwin/win32 x arm64/x64 + baseline/musl バリアント)に対応し、--single フラグで現在のプラットフォームのみビルドする開発モードも用意されている。

typescript
// 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 globaldeclare const で型宣言し、存在しない場合のフォールバックも用意する。これにより開発時(ビルドなし)でもアプリが動作する。

typescript
// 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";
typescript
// packages/opencode/src/storage/db.ts:16
declare const OPENCODE_MIGRATIONS: { sql: string; timestamp: number; }[] | undefined;

マイグレーション SQL をビルド時にバイナリへ埋め込むことで、バイナリ単体でデータベースマイグレーションを実行できる。ファイルシステム上のマイグレーションディレクトリへの依存を排除している。

ビルド時のデータプリフェッチ

ビルドスクリプトはビルド前に外部 API からモデル定義をフェッチし、TypeScript ファイルとして書き出す。これにより、ランタイムのネットワーク依存を排除する。

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.jsonworkspaces.catalog でモノレポ全体の依存バージョンを一元定義し、各パッケージは "catalog:" で参照する。これにより Renovate 等のバージョン管理ツールが単一箇所を更新するだけで済む。

json
// 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 がインストール環境を検出してバイナリをシンボリックリンクする。

typescript
// 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.nixbun install --frozen-lockfile を FOD(Fixed-Output Derivation)として実行し、outputHashAlgo = "sha256" でハッシュ検証する。hashes.json で各プラットフォームのハッシュを管理し、node_modules_updater でハッシュ更新を自動化する。

nix
-- 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 環境でのランタイムクラッシュを防止する。

yaml
# .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"
    fi

Script パッケージによるバージョン・チャネル抽象

packages/script がビルドパイプライン全体の共通ユーティリティとして機能する。バージョン計算ロジック(npm registry から最新版を取得 → bump)、チャネル判定(ブランチ名 or 環境変数)、リリースフラグを一元化する。

typescript
// 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-120allTargets 配列でターゲットを宣言的に定義し、ループでビルド実行
    • 注意点: --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.tspublish.tsprocess.chdir() を使用しているが、これは暗黙のグローバル状態変更であり、スクリプトの合成可能性を損なう。publish.tsawait 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-193typeof OPENCODE_VERSION === "string" ? OPENCODE_VERSION : "local" とすることで、ビルドなし開発時にもアプリが動作する
  • [MUST] マルチプラットフォームバイナリ配布では、ターゲットマトリクスを宣言的データ構造として定義し、--single 相当のフィルタで開発時のビルドを高速化する

    • 根拠: packages/opencode/script/build.ts:63-141 で 11 ターゲットを配列定義し、--single フラグで現在プラットフォームのみにフィルタリング
  • [SHOULD] モノレポのビルドオーケストレーションでは、タスク間依存を最小限に宣言し、テスト以外のビルドは独立並列実行可能にする

    • 根拠: turbo.jsonbuild.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 + ハッシュ検証パターンで管理する