Bazel
Bazel ビルドシステム完全解説
Googleのモダンなビルドシステムの全容
目次
- Bazelとは
- Bazelの基本概念
- Bazelのアーキテクチャ
- ワークスペース構成
- BUILD ファイルの記述
- ルールとターゲット
- 依存関係管理
- 実行環境とクロスコンパイル
- キャッシング戦略
- 分散ビルド
- 実践的な設定例
- パフォーマンス最適化
- Bazelの利点と課題
Bazelとは
定義と背景
Bazelは、Google社が開発・公開した高性能なビルドシステムです。複雑な依存関係を持つ大規模プロジェクトの効率的なビルド、テスト実行、デプロイメントを実現します。
Bazelという名前は、Babelの綴りを変えたもので、「多くの言語が混在する」という意味が込められています。実際に、Java、Python、Go、C++、Rust、TypeScriptなど多数のプログラミング言語に対応しています。
開発背景
GoogleはMonorepoと呼ばれる単一の巨大なコードリポジトリを運用しており、数百万ファイル、数千ものプロジェクトが存在します。従来のビルドシステムでは、こうした規模のプロジェクトをサポートすることが困難でした。Bazelは、このような課題に対応するために開発されました。
主な特徴
- マルチ言語対応:複数のプログラミング言語を同一ビルドシステムで扱える
- 高速・増分ビルド:変更があった部分のみを再ビルド
- 再現可能性:同じ入力から常に同じ出力が得られる
- スケーラビリティ:数百万ファイルのプロジェクトに対応
- マルチプラットフォーム対応: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は複数のレベルでキャッシングを実施します:
- ローカルキャッシュ:ビルド済みアーティファクトをディスクに保存
- リモートキャッシュ:複数マシン間でアーティファクトを共有
- アクションキャッシュ:入力ハッシュに基づいてアクションの実行結果をキャッシュ
ワークスペース構成
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の利点と課題
利点
-
高速・増分ビルド
- 変更があった部分のみを再ビルド
- キャッシングにより大幅な時間短縮
-
再現可能性
- 同じ入力から常に同じ出力が得られる
- 異なるマシン間での一貫性を保証
-
スケーラビリティ
- 数百万ファイルのプロジェクトに対応
- 並列ビルド、分散ビルドをサポート
-
マルチ言語対応
- Java、Python、Go、C++などをサポート
- 同一ビルドシステムで複数言語を扱える
-
柔軟な依存関係管理
- 外部パッケージの管理が容易
- セマンティクバージョニングの自動管理
課題
-
学習曲線の急峻さ
- Starlark言語の習得が必要
- 複雑な依存関係の管理が難しい
-
セットアップの複雑さ
- 初期設定に多くの時間がかかる
- 既存プロジェクトからの移行が困難
-
デバッグの難しさ
- ビルドエラーの原因特定が困難
- 分散ビルド時の問題診断が複雑
-
IDE統合の限定性
- IDEサポートが限定的
- リアルタイムのフィードバックが得られにくい
-
外部ツールとの統合
- 既存の自動化ツールとの統合が難しい場合がある
- 標準化されていないツールの対応
ベストプラクティス
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"],
# ...
)
ビルドの最適化
- 段階的なビルド
# 高速なビルド
bazel build -c fastbuild //...
# デバッグビルド
bazel build -c dbg //...
# 最適化されたビルド
bazel build -c opt //...
- ターゲットの選別
# 特定のターゲットのみビルド
bazel build //backend:app
# 複数の関連ターゲットをビルド
bazel build //backend/...
- リモートキャッシュの活用
# リモートキャッシュを使用してビルド
bazel build --config=remote //...
# キャッシュに上載
bazel build --config=remote --remote_upload_local_results //...
まとめ
Bazelは、複雑なプロジェクトの効率的なビルド、テスト、デプロイメントを実現するモダンなビルドシステムです。高速な増分ビルド、再現可能性、スケーラビリティを備えており、大規模Monorepoプロジェクトに最適です。
しかし、学習曲線が急峻で、セットアップが複雑というデメリットもあります。プロジェクトの規模や要件に応じて、Bazelの導入を検討する価値があります。
次のステップ
- 公式ドキュメント:https://bazel.build/
- Bazel Tutorialの実行
- 小規模プロジェクトでの試験導入
- チームのトレーニング
- 徐々にプロジェクトへの適用を拡大
Bazelを活用することで、開発生産性の向上とビルド時間の短縮が期待できます。