Bazel

Bazel ビルドシステム完全解説

Googleのモダンなビルドシステムの全容


目次

  1. Bazelとは
  2. Bazelの基本概念
  3. Bazelのアーキテクチャ
  4. ワークスペース構成
  5. BUILD ファイルの記述
  6. ルールとターゲット
  7. 依存関係管理
  8. 実行環境とクロスコンパイル
  9. キャッシング戦略
  10. 分散ビルド
  11. 実践的な設定例
  12. パフォーマンス最適化
  13. Bazelの利点と課題

Bazelとは

定義と背景

Bazelは、Google社が開発・公開した高性能なビルドシステムです。複雑な依存関係を持つ大規模プロジェクトの効率的なビルド、テスト実行、デプロイメントを実現します。

Bazelという名前は、Babelの綴りを変えたもので、「多くの言語が混在する」という意味が込められています。実際に、Java、Python、Go、C++、Rust、TypeScriptなど多数のプログラミング言語に対応しています。

開発背景

GoogleはMonorepoと呼ばれる単一の巨大なコードリポジトリを運用しており、数百万ファイル、数千ものプロジェクトが存在します。従来のビルドシステムでは、こうした規模のプロジェクトをサポートすることが困難でした。Bazelは、このような課題に対応するために開発されました。

主な特徴

  1. マルチ言語対応:複数のプログラミング言語を同一ビルドシステムで扱える
  2. 高速・増分ビルド:変更があった部分のみを再ビルド
  3. 再現可能性:同じ入力から常に同じ出力が得られる
  4. スケーラビリティ:数百万ファイルのプロジェクトに対応
  5. マルチプラットフォーム対応:Windows、macOS、Linuxをサポート

Bazelの基本概念

ワークスペース(Workspace)

ワークスペースは、Bazelプロジェクトの最上位ディレクトリです。WORKSPACEまたはWORKSPACE.bazelファイルで定義されます。

my-project/
├── WORKSPACE
├── BUILD
├── src/
│   ├── main/
│   │   ├── BUILD
│   │   └── app.py
│   └── test/
│       ├── BUILD
│       └── app_test.py
└── third_party/
    └── BUILD

パッケージ(Package)

パッケージはBUILDファイルを含むディレクトリです。各パッケージは独立したビルドユニットとして扱われます。

ターゲット(Target)

ターゲットはビルド対象の最小単位です。実行ファイル、ライブラリ、テストなどがターゲットになります。

//path/to/package:target_name

このように//で始まるパスで表記します。

ラベル(Label)

ラベルはターゲットを一意に識別する方法です。

@workspace_name//package:target_name

ルール(Rule)

ルールはターゲットを作成するための定義です。Bazelは多数の組み込みルールを提供し、カスタムルールも作成可能です。


Bazelのアーキテクチャ

処理フロー

Bazelのビルド処理は以下のステップで進行します:

1. Loading Phase
   └─ BUILD ファイルの読み込みと解析

2. Analysis Phase
   └─ 依存関係グラフの構築
   └─ ルールの実行前処理

3. Execution Phase
   └─ アクションの実行
   └─ ターゲットの生成

4. Verification Phase
   └─ テスト実行
   └─ 検証

Starlark言語

BUILDファイルやルール定義はStarlark言語で記述されます。Starlarkはカスタマイズ可能で読みやすい言語で、Pythonに似た構文を持ちます。

def my_rule_impl(ctx):
    # ルールの実装
    output = ctx.actions.declare_file("output.txt")
    ctx.actions.run(
        inputs = ctx.files.srcs,
        outputs = [output],
        executable = ctx.executable.tool,
        arguments = [output.path],
    )
    return [DefaultInfo(files = depset([output]))]

my_rule = rule(
    implementation = my_rule_impl,
    attrs = {
        "srcs": attr.label_list(allow_files = True),
        "tool": attr.label(executable = True, cfg = "exec"),
    },
)

依存関係グラフ

Bazelは各ターゲットの依存関係をDAG(有向非環グラフ)として管理します。このグラフにより、ターゲット間の依存関係を明確に把握し、並列ビルドが可能になります。

app
├── lib_a
│   └── lib_base
├── lib_b
│   └── lib_base
└── config

キャッシング戦略

Bazelは複数のレベルでキャッシングを実施します:

  1. ローカルキャッシュ:ビルド済みアーティファクトをディスクに保存
  2. リモートキャッシュ:複数マシン間でアーティファクトを共有
  3. アクションキャッシュ:入力ハッシュに基づいてアクションの実行結果をキャッシュ

ワークスペース構成

WORKSPACE ファイル

ワークスペースの定義ファイルです:

workspace(name = "my_project")

# リモート依存関係の定義
http_archive(
    name = "com_google_protobuf",
    sha256 = "abc123...",
    strip_prefix = "protobuf-3.21.0",
    urls = ["https://github.com/protocolbuffers/protobuf/archive/v3.21.0.tar.gz"],
)

# Gitリポジトリからの依存関係
git_repository(
    name = "com_example_lib",
    remote = "https://github.com/example/lib.git",
    branch = "main",
)

# ローカルリポジトリ
local_repository(
    name = "local_lib",
    path = "../local_lib",
)

プロジェクトレイアウト

my-project/
├── WORKSPACE
├── WORKSPACE.bazel
├── BUILD
├── bazelrc
├── .bazelignore
│
├── src/
│   ├── BUILD
│   ├── main.py
│   └── module.py
│
├── lib/
│   ├── BUILD
│   ├── util.go
│   └── util_test.go
│
├── tests/
│   ├── BUILD
│   └── integration_test.py
│
└── third_party/
    └── BUILD

.bazelignore ファイル

Bazelが無視するディレクトリを指定します:

node_modules
.git
vendor
build_output

BUILD ファイルの記述

基本構造

BUILD ファイルは、そのディレクトリで利用可能なターゲットを定義します:

# コメント

# Pythonライブラリ
py_library(
    name = "my_lib",
    srcs = ["lib.py"],
    deps = [
        "//common:helpers",
        "@third_party_lib//:core",
    ],
)

# Pythonバイナリ
py_binary(
    name = "my_app",
    srcs = ["main.py"],
    deps = [":my_lib"],
    main = "main.py",
)

# テスト
py_test(
    name = "lib_test",
    srcs = ["lib_test.py"],
    deps = [":my_lib"],
)

属性の指定

各ルールは属性(attributes)を持ちます:

cc_library(
    name = "mylib",
    # ラベルリスト属性:ソースファイル
    srcs = [
        "src/util.cc",
        "src/helper.cc",
    ],
    # ラベルリスト属性:ヘッダーファイル
    hdrs = [
        "include/util.h",
        "include/helper.h",
    ],
    # 依存関係
    deps = [
        "//third_party:boost",
        "//common:config",
    ],
    # コンパイルオプション
    copts = [
        "-std=c++17",
        "-Wall",
    ],
    # リンクオプション
    linkopts = [
        "-lpthread",
    ],
    # 可視性
    visibility = ["//visibility:public"],
)

グロブパターン

ファイルを動的に指定できます:

py_library(
    name = "all_tests",
    srcs = glob(["*.py"], exclude = ["*_test.py"]),
)

cc_library(
    name = "src",
    srcs = glob(
        ["src/**/*.cc"],
        exclude = ["src/generated/**"],
    ),
    hdrs = glob(["include/**/*.h"]),
)

ルールとターゲット

組み込みルール

C++ルール

# C++ライブラリ
cc_library(
    name = "mylib",
    srcs = ["lib.cc"],
    hdrs = ["lib.h"],
)

# C++バイナリ
cc_binary(
    name = "app",
    srcs = ["main.cc"],
    deps = [":mylib"],
)

# C++テスト
cc_test(
    name = "lib_test",
    srcs = ["lib_test.cc"],
    deps = [
        ":mylib",
        "@com_google_googletest//:gtest_main",
    ],
)

Javaルール

# Javaライブラリ
java_library(
    name = "my_lib",
    srcs = glob(["src/**/*.java"]),
    deps = [
        "//common:config",
        "@maven//:com_google_guava_guava",
    ],
)

# Javaバイナリ
java_binary(
    name = "app",
    srcs = ["Main.java"],
    main_class = "com.example.Main",
    deps = [":my_lib"],
)

Pythonルール

# Pythonライブラリ
py_library(
    name = "mylib",
    srcs = ["lib.py"],
    imports = [""],
)

# Pythonバイナリ
py_binary(
    name = "app",
    srcs = ["main.py"],
    main = "main.py",
    deps = [":mylib"],
)

# Pythonテスト
py_test(
    name = "lib_test",
    srcs = ["lib_test.py"],
    deps = [":mylib"],
)

Goルール

# Goライブラリ
go_library(
    name = "mylib",
    srcs = ["lib.go"],
    importpath = "github.com/example/mylib",
)

# Goバイナリ
go_binary(
    name = "app",
    srcs = ["main.go"],
    deps = [":mylib"],
)

# Goテスト
go_test(
    name = "lib_test",
    srcs = ["lib_test.go"],
    embed = [":mylib"],
)

カスタムルールの作成

def _my_custom_rule_impl(ctx):
    """カスタムルールの実装"""
    
    # 出力ファイルを宣言
    output = ctx.actions.declare_file(ctx.attr.name + ".out")
    
    # アクションを実行
    ctx.actions.run(
        inputs = ctx.files.srcs,
        outputs = [output],
        executable = ctx.executable.tool,
        arguments = [
            ctx.files.srcs[0].path,
            output.path,
        ] + ctx.attr.args,
    )
    
    # プロバイダーを返却
    return [
        DefaultInfo(files = depset([output])),
        OutputGroupInfo(report = depset([output])),
    ]

my_custom_rule = rule(
    implementation = _my_custom_rule_impl,
    attrs = {
        "srcs": attr.label_list(
            allow_files = True,
            doc = "Input files",
        ),
        "tool": attr.label(
            executable = True,
            cfg = "exec",
            mandatory = True,
        ),
        "args": attr.string_list(
            doc = "Additional arguments",
        ),
    },
    doc = "Custom transformation rule",
)

依存関係管理

外部依存関係の定義

HTTP Archive

http_archive(
    name = "com_google_protobuf",
    sha256 = "abc123def456...",
    strip_prefix = "protobuf-3.21.0",
    urls = [
        "https://github.com/protocolbuffers/protobuf/archive/v3.21.0.tar.gz",
        "https://mirror.example.com/protobuf-3.21.0.tar.gz",
    ],
)

Git Repository

git_repository(
    name = "com_example_lib",
    remote = "https://github.com/example/lib.git",
    tag = "v1.0.0",
    shallow_since = "2023-01-01 00:00:00 +0000",
)

ローカルリポジトリ

local_repository(
    name = "local_lib",
    path = "../local_lib",
)

new_local_repository(
    name = "system_boost",
    path = "/usr/local",
    build_file = "//third_party:boost.BUILD",
)

Maven依存関係

load("@bazel_tools//tools/build_defs/repo:java.bzl", "java_import_external")

java_import_external(
    name = "com_google_guava",
    jar_sha256 = "abc123...",
    jar_urls = [
        "https://repo1.maven.org/maven2/com/google/guava/guava/31.1-jre/guava-31.1-jre.jar",
    ],
    licenses = ["notice"],
    neverlink = False,
    deps = [
        "@com_google_code_findbugs_jsr305",
    ],
)

バージョン管理戦略

# 特定のコミットを使用
git_repository(
    name = "com_example_lib",
    remote = "https://github.com/example/lib.git",
    commit = "abc123def456...",
)

# タグを使用
git_repository(
    name = "com_example_lib",
    remote = "https://github.com/example/lib.git",
    tag = "v1.0.0",
)

# ブランチを使用(推奨されない)
git_repository(
    name = "com_example_lib",
    remote = "https://github.com/example/lib.git",
    branch = "main",
)

実行環境とクロスコンパイル

ターゲットプラットフォーム

cc_binary(
    name = "app",
    srcs = ["main.cc"],
    # 実行環境を指定
    target_compatible_with = [
        "@platforms//os:linux",
        "@platforms//cpu:x86_64",
    ],
)

クロスコンパイル設定

# .bazelrc
build --platforms=//platforms:android_arm64

# 複数プラットフォーム用
platforms/
├── BUILD
├── android_arm64.bzl
└── linux_x86_64.bzl

プラットフォーム定義

# platforms/BUILD
platform(
    name = "android_arm64",
    constraint_values = [
        "@platforms//os:android",
        "@platforms//cpu:arm64",
    ],
)

platform(
    name = "ios_arm64",
    constraint_values = [
        "@platforms//os:ios",
        "@platforms//cpu:arm64",
    ],
)

キャッシング戦略

ローカルキャッシュ設定

# ~/.bazelrc
build --disk_cache=/path/to/cache
build --repository_cache=/path/to/repo_cache

リモートキャッシュ設定

# .bazelrc
build:remote --remote_cache=grpcs://cache.example.com
build:remote --remote_timeout=3600
build:remote --google_credentials=/path/to/credentials.json

キャッシュキーの計算

Bazelは入力ファイルのハッシュ値とアクション情報からキャッシュキーを生成します:

key = hash(
    action_command,
    input_hashes,
    environment_variables,
    platform_constraints,
)

キャッシュの検証

# キャッシュをクリア
bazel clean --expunge

# キャッシュをクリア(出力ベースのみ)
bazel clean

# キャッシュの統計情報を表示
bazel info

分散ビルド

RBE (Remote Build Execution)

Bazelは分散ビルド実行をサポートしており、Google Cloud Buildなどのサービスと統合できます。

# .bazelrc
build:remote --remote_executor=grpcs://remote-execution.example.com
build:remote --remote_instance_name=projects/my-project/instances/default_instance

Buildfarm

オンプレミス環境向けの分散ビルドシステム:

# buildfarm設定
build --remote_executor=grpcs://buildfarm.example.com:443
build --remote_instance_name=default_instance

ローカル並列ビルド

# .bazelrc
build --jobs=16
build --local_cpu_resources=16
build --local_ram_resources=32

実践的な設定例

マルチ言語プロジェクトの例

プロジェクト構成:

monorepo/
├── WORKSPACE
├── .bazelrc
├── BUILD
│
├── backend/
│   ├── BUILD
│   ├── src/
│   │   ├── BUILD
│   │   ├── main.go
│   │   └── handler.go
│   └── tests/
│       ├── BUILD
│       └── handler_test.go
│
├── frontend/
│   ├── BUILD
│   ├── src/
│   │   ├── BUILD
│   │   ├── main.ts
│   │   └── app.tsx
│   └── tests/
│       ├── BUILD
│       └── app.test.tsx
│
├── shared/
│   ├── BUILD
│   ├── proto/
│   │   ├── BUILD
│   │   └── api.proto
│   └── config/
│       ├── BUILD
│       └── config.yaml
│
└── third_party/
    └── BUILD

WORKSPACE ファイル

workspace(name = "monorepo")

# Go依存関係
http_archive(
    name = "io_bazel_rules_go",
    sha256 = "abc123...",
    urls = ["https://github.com/bazelbuild/rules_go/releases/download/v0.39.1/rules_go-v0.39.1.zip"],
)

load("@io_bazel_rules_go//go:deps.bzl", "go_rules_dependencies", "go_register_toolchains")
go_rules_dependencies()
go_register_toolchains(version = "1.20")

# Protocol Buffers
http_archive(
    name = "com_google_protobuf",
    sha256 = "def456...",
    strip_prefix = "protobuf-3.21.0",
    urls = ["https://github.com/protocolbuffers/protobuf/archive/v3.21.0.tar.gz"],
)

load("@com_google_protobuf//:protobuf_deps.bzl", "protobuf_deps")
protobuf_deps()

# TypeScript
http_archive(
    name = "build_bazel_rules_typescript",
    sha256 = "ghi789...",
    urls = ["https://github.com/bazelbuild/rules_typescript/releases/download/5.1.0/rules_typescript-5.1.0.tar.gz"],
)

load("@build_bazel_rules_typescript//:index.bzl", "ts_setup_workspace")
ts_setup_workspace()

.bazelrc ファイル

# 共通オプション
common --enable_platform_specific_config

# Linux設定
build:linux --platforms=@local_config_platform//:host
build:linux --crosstool_top=@local_config_cc//:toolchain

# macOS設定
build:macos --apple_platform_type=macos
build:macos --cpu=darwin_x86_64

# ローカルビルド
build:local --jobs=auto
build:local --local_ram_resources=32

# リモートビルド
build:remote --remote_cache=grpcs://cache.example.com
build:remote --remote_executor=grpcs://executor.example.com
build:remote --jobs=100

# テスト設定
test --test_output=errors
test --flaky_test_attempts=2

# 開発設定
build:dev --compilation_mode=dbg
build:dev --strip=never

# リリース設定
build:release --compilation_mode=opt
build:release --strip=always

Backend BUILD ファイル(Go)

load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_binary", "go_test")

go_library(
    name = "handler",
    srcs = ["handler.go"],
    importpath = "example.com/backend/handler",
    deps = [
        "//shared/proto:api_go_proto",
        "@com_github_gorilla_mux//:mux",
    ],
)

go_binary(
    name = "app",
    srcs = ["main.go"],
    deps = [":handler"],
    visibility = ["//visibility:public"],
)

go_test(
    name = "handler_test",
    srcs = ["handler_test.go"],
    embed = [":handler"],
    deps = ["@com_github_stretchr_testify//assert"],
)

Frontend BUILD ファイル(TypeScript)

load("@build_bazel_rules_typescript//:index.bzl", "ts_library", "ts_proto_library")
load("@npm//:defs.bzl", "npm_link_all_packages")

npm_link_all_packages(name = "node_modules")

ts_library(
    name = "app",
    srcs = ["app.tsx"],
    deps = [
        ":node_modules/react",
        ":node_modules/react-dom",
    ],
)

ts_library(
    name = "app_test",
    srcs = ["app.test.tsx"],
    deps = [
        ":app",
        ":node_modules/@testing-library/react",
    ],
)

Protocol Buffers BUILD ファイル

load("@com_google_protobuf//:protobuf.bzl", "proto_library")
load("@io_bazel_rules_go//proto:def.bzl", "go_proto_library")
load("@build_bazel_rules_typescript//proto:index.bzl", "ts_proto_library")

proto_library(
    name = "api_proto",
    srcs = ["api.proto"],
    visibility = ["//visibility:public"],
)

go_proto_library(
    name = "api_go_proto",
    proto = ":api_proto",
    visibility = ["//visibility:public"],
)

ts_proto_library(
    name = "api_ts_proto",
    proto = ":api_proto",
    visibility = ["//visibility:public"],
)

パフォーマンス最適化

ビルド時間の削減

1. 依存関係の最小化

# 悪い例:不要な依存関係
cc_library(
    name = "mylib",
    srcs = ["lib.cc"],
    deps = [
        "//unrelated:foo",
        "//unused:bar",
    ],
)

# 良い例:最小限の依存関係
cc_library(
    name = "mylib",
    srcs = ["lib.cc"],
    deps = [
        "//core:base",
    ],
)

2. アクションの最小化

# 複数の小さなアクションを1つに結合
genrule(
    name = "process",
    srcs = ["input.txt"],
    outs = ["output.txt"],
    cmd = """
        cat $(SRCS) | \
        sed 's/foo/bar/g' | \
        sort | \
        uniq > $(OUTS)
    """,
)

3. ビルドモードの最適化

# 開発時は高速ビルドを優先
bazel build -c dbg //...

# リリース時は最適化を優先
bazel build -c opt --config=release //...

メモリ使用の最適化

# .bazelrc
build --ram_utilization_factor=50
build --local_ram_resources=16
build --jobs=8

キャッシュの活用

# 最初のビルド(キャッシュなし)
time bazel build //...

# 2回目のビルド(キャッシュあり)
bazel clean
time bazel build //...

Bazelの利点と課題

利点

  1. 高速・増分ビルド

    • 変更があった部分のみを再ビルド
    • キャッシングにより大幅な時間短縮
  2. 再現可能性

    • 同じ入力から常に同じ出力が得られる
    • 異なるマシン間での一貫性を保証
  3. スケーラビリティ

    • 数百万ファイルのプロジェクトに対応
    • 並列ビルド、分散ビルドをサポート
  4. マルチ言語対応

    • Java、Python、Go、C++などをサポート
    • 同一ビルドシステムで複数言語を扱える
  5. 柔軟な依存関係管理

    • 外部パッケージの管理が容易
    • セマンティクバージョニングの自動管理

課題

  1. 学習曲線の急峻さ

    • Starlark言語の習得が必要
    • 複雑な依存関係の管理が難しい
  2. セットアップの複雑さ

    • 初期設定に多くの時間がかかる
    • 既存プロジェクトからの移行が困難
  3. デバッグの難しさ

    • ビルドエラーの原因特定が困難
    • 分散ビルド時の問題診断が複雑
  4. IDE統合の限定性

    • IDEサポートが限定的
    • リアルタイムのフィードバックが得られにくい
  5. 外部ツールとの統合

    • 既存の自動化ツールとの統合が難しい場合がある
    • 標準化されていないツールの対応

ベストプラクティス

BUILD ファイルの構成

# ファイルの先頭にローカルプロバイダーを定義
load(":providers.bzl", "MyProvider")

# 依存関係のロード
load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_binary")

# ライブラリターゲット
go_library(
    name = "lib",
    srcs = ["lib.go"],
    # ...
)

# バイナリターゲット
go_binary(
    name = "bin",
    srcs = ["main.go"],
    deps = [":lib"],
    # ...
)

# テストターゲット
go_test(
    name = "lib_test",
    srcs = ["lib_test.go"],
    embed = [":lib"],
    # ...
)

ビルドの最適化

  1. 段階的なビルド
# 高速なビルド
bazel build -c fastbuild //...

# デバッグビルド
bazel build -c dbg //...

# 最適化されたビルド
bazel build -c opt //...
  1. ターゲットの選別
# 特定のターゲットのみビルド
bazel build //backend:app

# 複数の関連ターゲットをビルド
bazel build //backend/...
  1. リモートキャッシュの活用
# リモートキャッシュを使用してビルド
bazel build --config=remote //...

# キャッシュに上載
bazel build --config=remote --remote_upload_local_results //...

まとめ

Bazelは、複雑なプロジェクトの効率的なビルド、テスト、デプロイメントを実現するモダンなビルドシステムです。高速な増分ビルド、再現可能性、スケーラビリティを備えており、大規模Monorepoプロジェクトに最適です。

しかし、学習曲線が急峻で、セットアップが複雑というデメリットもあります。プロジェクトの規模や要件に応じて、Bazelの導入を検討する価値があります。

次のステップ

  1. 公式ドキュメントhttps://bazel.build/
  2. Bazel Tutorialの実行
  3. 小規模プロジェクトでの試験導入
  4. チームのトレーニング
  5. 徐々にプロジェクトへの適用を拡大

Bazelを活用することで、開発生産性の向上とビルド時間の短縮が期待できます。