Googleが開発する最新ビルドツール「Bazel」を使ってみよう

ソフトウェアのビルドに古くから使われている定番ツールといえば「make」だが、昨今ではそれに変わる新たなビルドツールも登場している。今回紹介する「Bazel」は、Googleが社内で使用しているものをベースとして開発されたビルドツールだ。今回はこのBazelの概要や基本的な使い方を紹介する。

さまざまな可能性がある新ビルドツール

現代のソフトウェア開発現場においては、多数のコンポーネントを組み合わせてソフトウェアを構築するのが一般的だ。そのような場合、手動でそれそれをコンパイルするのは大きな手間になるため、ビルドツールを利用することになる。たとえばMac OS XのXcodeやWindowsのVisual StudioといったIDE(統合開発環境)では独自のビルドツールがIDEに組み込まれている。また、JavaではAntやMaven、RubyではRakeなど、言語ごとに特有のツールが使われることも多い。とはいえ、オープンソースソフトウェアの分野において最も多く使われているビルドツールはやはり「make」だろう。

makeはプラットフォームに依存せず、ほとんどすべてのプログラミング言語やツールに対応できる。また、記述ルールがシンプルであり学習しやすいというメリットもある。その半面、複雑なルールを記述しにくかったり、並列処理が苦手といったデメリットもある。そのためマクロツールや設定自動生成ツールなどと組み合わせて使われることが多いが、そうすると今度は一見しただけでは記述ルールが分かりにくくなってしまうことも少なくない。

こういった背景から、makeを置き換えるべくさまざまなビルドツールが登場している。今回取り上げる「Bazel」は、Googleが開発を主導しているビルドツールだ(図1)。

図1 BazelのWebサイト

Bazelは、Googleが社内で自社プロダクトのビルドに使っていたツール「Blaze」をベースとするビルドツールだ。現在はまだベータ版というステータスだが、今後はAndroid開発向けの標準ビルドツールになるのではとも言われている。

Bazelの特徴1:さまざまな言語/目的に対応

Bazelもmakeと同様、ビルドに必要なファイルとビルド後に生成されるファイルの組み合わせをテキストファイルに記述していくことでビルドルールを設定していくのだが、Bazelで特徴的なのは、AndroidやC/C++、Java、Objective-Cといった言語向けのルールがあらかじめ用意されている点だ。

記事執筆時点では表1の言語/環境向けルールが用意されており、これらを利用することで少ない記述量で容易にビルドルールを記述できる。もちろん、これ以外の言語やツール向けのビルドルールを記述することも可能だ。さらに、C/C++やJava、Python、シェルスクリプトなどについてはテストを実行するためのルールも用意されている。

表1 記事執筆時点でBazelがサポートしている言語/環境
言語/環境 ビルド テスト
Android
C/C++
Java
Objective-C
Python
シェルスクリプト
C#
Java App Engine
D
Dockerコンテナ
Grooby
Go
Closure
Jsonnet
tarball
Debianパッケージ(.deb)
Rust
Sass
Scala

Bazelの特徴2:ビルドによってディレクトリを汚染しない

Bazelでは、ソースコードやテストデータなどが格納されているディレクトリとは別のディレクトリでビルドやテストなどを行う仕組みになっている。makeコマンドでは意図的に設定や操作を行わない限りソースコードと生成物が同じディレクトリに混在する事態になることが多いが、Bazelではこういった問題が発生しない。

また、ビルドやテストはデフォルトではサンドボックス化された環境で行われるため、ビルドやテストがそれを実行しているシステムに影響を及ぼす可能性が最小限に抑えられている。

Bazelの特徴3:並列ビルド

大規模なソフトウェアではビルド対象が増えるため、ビルドにかかる時間も増える傾向がある。Bazelでは生成物どうしの依存性を自動的に把握し、可能な限り並列でビルドを実行する仕組みになっている。これにより、ビルド時間の短縮が期待できる。

Bazelの欠点

このように利点の多いBazelであるが、もちろん欠点もある。Bazelでは言語毎にビルドルールを用意することで記述量を大幅に削減しているが、その副作用として実際にどのような処理が実行されるのかが分かりにくくなっている。そのため、トラブルが発生した際にその原因を突き止めることが難しくなる可能性がある。

また、BazelのデフォルトではサポートされていないPerlやRubyなどの言語では、ビルドルールを詳細に記述しなければならない。そのほか、ベータ版ということで機能が不足していたり、期待した動作をしてくれない可能性もある。

こういった状況であるため、現時点ではサポートされているC/C++やObjective-C、Java等の言語以外でBazelを活用するのはおすすめできない。逆にサポートされている言語であれば、ベータ版とはいえ十分採用を検討できるだろう。

Bazelのインストールと基礎

それでは、実際にBazelを使ってソフトウェアをビルドする例を紹介しよう。以下ではまずBazelを利用するための基本的なルールを解説し、続けて実際のルール記述例やコマンド実行例を紹介する。

Ubuntu/Debian環境でのBazelのインストール

まずはBazelのインストールだが、現在公式にサポート対象とされているのはUbuntu 15.10(Wily)および14.04(Trusty)、Mac OS X、Windowsだ。ただし、これ以外の環境でもJDK 7以降(8以降が推奨)が利用できる環境であれば利用できる。

Ubuntu/Debian環境向けにはapt-getコマンドでアクセスできるリポジトリをGoogleが提供しているため、これを利用できるように設定ファイルを追加することで、apt-getコマンドでBazelをインストールできる。具体的な手順としては下記のようになる。

まず、/etc/apt/sources.list.dディレクトリ内に「bazel.list」という名称のリポジトリ設定ファイルを作成する。

# vi /etc/apt/sources.list.d/bazel.list

このファイルの中身は以下のようになる。

deb [arch=amd64] http://storage.googleapis.com/bazel-apt stable jdk1.8

続いて、このリポジトリの配布物における署名検証に必要な公開鍵をダウンロードし、apt-keyコマンドでローカル環境に登録しておく。

# curl https://storage.googleapis.com/bazel-apt/doc/apt-key.pub.gpg | apt-key add -

以上でBazelの公開リポジトリが利用可能になるので、最後にapt-getコマンドで「bazel」パッケージをインストールすれば良い。

# apt-get update
# apt-get install bazel

ソースコードからBazelをビルドする

JDK 8が利用できる環境であれば、ソースコードからBazelをビルドしてインストールすることが可能だ。ソースコードはGitHubで公開されているので、このリポジトリからソースコードをクローンし、含まれる「compile.sh」スクリプトを実行すれば良い。

たとえばCentOS 7環境では、「java-1.8.0-openjdk-devel」パッケージでJDK 8がインストールできるのでまずこれをインストールする。

# yum install java-1.8.0-openjdk-devel

続いてソースコードをクローンし、compile.shを実行する。

$ git clone https://github.com/bazelbuild/bazel
$ cd bazel
$ ./compile.sh
 
 
INFO: Elapsed time: 215.448s, Critical Path: 209.39s

Build successful! Binary is here: /home/hylom/bazel/output/bazel

コンパイルされたバイナリはoutputディレクトリ以下に出力されるので、/usr/local/binなどのパスが通った適当なディレクトリにコピーしておこう。

cp output/bazel /usr/local/bin/

ちなみに、Bazelの初回起動時には~/.cache/bazel以下にキャッシュファイルを作成する処理が行われるため、やや時間がかかる点に注意したい。

$ bazel
Extracting Bazel installation...
............... [bazel release 0.3.1-2016-09-26 (@94b2c88)]

BazelのインストールドキュメントではJDK 8が標準では提供されていない環境にOracleのJDK 8をインストールする方法や、Mac OS XおよびWindows向けのインストール方法についても説明されている。必要に応じてこちらも参照してほしい。

ビルドルールの作成

Bazelでは、主に「WORKSPACE」と「BUILD」という2つのファイルを使ってビルドルールを定義する。

まずWORKSPACEファイルだが、これはソースコードや各種データファイルなどを含むディレクトリの最も最上位のディレクトリに作成する。Bazelではこのファイルが置かれたディレクトリ以下を「ワークスペース(Workspace)」と呼び、ワークスペース内のファイルのみがBazelでの管理対象となる。

WORKSPACEファイルでは、ビルド全般に関連する設定と、外部依存性の解決に必要な設定を記述できる。「外部依存性の解決」というと分かりにくいが、具体的にはビルド時に必要となるファイルやリポジトリなどをワークスペース外からコピーしたりダウンロードする、といった処理をここに記述する。

WORKSPACEファイル内に記述できる命令はWorkspace Rulesドキュメントにまとめられている。特に外部依存性がない場合はファイル内に何も記述しなくても良いが、ファイル自体は必要となるので、その場合は空のWORKSPACEファイルを作成しておこう。

いっぽうのBUILDファイルはソースコードなどビルドに必要なファイルやビルド方法、出力されるファイルなどの情報を記述するファイルで、通常はソースコードなどの素材となるファイルが配置されているディレクトリに作成する。BUILDファイルが配置されたディレクトリをBazelでは「パッケージ(Package)」と呼び、Workspaceは複数のパッケージを含むことができる。また、パッケージ内に別のパッケージを格納する(BUILDファイルが置かれたディレクトリのサブディレクトリ内にBUILDファイルを置く)ことも可能だ(図2)。

図2 Bazelの「ワークスペース」と「パッケージ」

BUILDファイル内では、次のようなルールでビルドルールを記述する。

<使用するルール>(
  name = "ターゲット名",
  <パラメータ1> = <値>,
  <パラメータ2> = <値>,
  <パラメータ3> = <値>,
  
  
)

「使用するルール」部分には、定義されているビルドルールのうちいずれかを指定する。たとえばC/C++コードをビルドして実行ファイルを作成するには「cc_binary」、Javaコードをビルドして実行ファイルを作成するには「java_binary」を指定する。

定義されているビルドルールについての詳細はBazel BUILD Encyclopedia of Functionsドキュメントにまとめられている。ビルドルールは各言語ごとに用意されているが、言語に依存しない汎用ルール(General Rules)もある。また、ルールの定義内で利用できる関数(Functions)も用意されている。

値には次のような形で文字列、リスト、ハッシュなどを指定できる。

ダブルクォーテーションで囲むと文字列として扱われる
"文字列"

角括弧で囲み、カンマで並べるとリストとして扱われる
["値1","値2","値3",]

角括弧内での改行も許される
[ "値1",
  "値2",
  "値3",]

波括弧で囲み、「キー = 値,」を並べるとハッシュとして扱われる
{"キー1" = "値1", "キー2" = "値2", "キー3" = "値3", }

波括弧内での改行も許される
{"キー1": "値1",
 "キー2": "値2",
 "キー3": "値3", }

BUILDファイルはWORKSPACEファイルと同じディレクトリ(ワークスペースのトップディレクトリ)に配置しても良いが、一般的にはワークスペースのトップディレクトリ内に子ディレクトリを作り、そこにソースコードなどを格納するケースが多い。たとえば「hello」というディレクトリ内に「hello.c」というソースコードを配置した場合、WORKSPACEファイルとBUILDファイルの配置は次のようになる。

(ルートディレクトリ)/WORKSPACE
                     /hello/BUILD                         
                     /hello/hello.c

この場合、「/hello/BUILD」ファイルには同じhelloディレクトリ内にあるhello.cファイルをコンパイルするビルドルールを記述する。また、外部依存性はないので、WORKSPACEファイルは空で良い。

この例では、/hello/BUILDファイルの中身は以下のようになる。

cc_binary(
  name = "hello",
    srcs = [
      "hello.c",
    ],
    copts = ["-O2"],
)

ここでは、C/C++コードをビルドするための「cc_binary」というルールを使用している。詳しくはドキュメントを参照して欲しいが、cc_binaryルールでは「srcs」パラメータでソースファイルを、「copts」パラメータでコンパイルオプションを指定できる。この例の場合、「hello.c」というファイルを「-O2」オプション付きでコンパイルすることになる。また、「name」パラメータで指定したターゲット名はこのルールを参照するために使われるだけでなく、出力ファイルの名前としても使われる。

ビルドの実行

bazelでは、「bazel build <ターゲット>」コマンドでビルドを実行できる。「<ターゲット>」は、以下のような形式で指定できる。

<ターゲット名>
:<ターゲット名>
//<パッケージ名>
//<パッケージ名>:<ターゲット名>

「ターゲット名」は、ルールの「name」パラメータで定義したものだ。また、パッケージ名はBUILDファイルが格納されているディレクトリのワークスペーストップディレクトリ(WORKSPACEファイルが格納されているディレクトリ)からの相対パスとなる。

パッケージ名が省略された場合、カレントディレクトリがパッケージ名として指定されたものとみなされる。また、パッケージ名のみを指定した場合は、ターゲット名としてそのパッケージに対応するディレクトリ名が指定されたものとみなされる。

たとえば先のhelloディレクトリ内にあるBUILDファイルに記述された「hello」という名前のターゲットをビルドする場合、次のように実行すれば良い。

$ bazel build //hello:hello

この場合、ディレクトリ名とターゲット名が同じなので以下のようにターゲット名を省略することも可能だ。

$ bazel build //hello

また、「hello」ディレクトリ内であれば、次のようにパッケージ名を省略できる。

$ bazel build hello
(もしくは)
$ bazel build :hello

このようなターゲット名表記は「ラベル(label)」と呼ばれており、WORKSPACEファイルやBUILDファイル内でビルドルールを参照する際にも使用される。

さて、「bazel build」コマンドでビルドを実行すると、次のように進捗が表示され、問題が無ければ経過時間が表示されてビルドが終了する。このとき、生成物の出力先も表示される。たとえば次の例では、「bazel-bin/hello/hello」が生成物となる。

$ bazel build //hello:hello
INFO: Found 1 target...
Target //hello:hello up-to-date:
  bazel-bin/hello/hello
INFO: Elapsed time: 1.266s, Critical Path: 0.09s

Bazelではmakeなどと同様に依存関係をチェックしており、前回のビルド時からファイルの生成に必要なファイル(この例ではhello.c)が更新されている場合のみ、実際のビルドを実行するようになっている。

また、Bazelではビルドの際の中間生成物や最終生成物はワークスペースとは別の場所に出力される。これらの出力先は通常は「~/.cache/bazel」ディレクトリとなり、そのディレクトリへのリンクが「bazel-bin」や「bazel-out」といった名称でワークスペースのトップディレクトリ内に作成される。たとえば今回の例では、次のようにリンクが作成されている。

$ ls -l
合計 24
-rw-rw-r-- 1 hylom hylom    0  9月 26 19:29 WORKSPACE
lrwxrwxrwx 1 hylom hylom  116  9月 30 00:15 bazel-bin -> /home/hylom/.cache/bazel/_bazel_hylom/0ab1861957d682bb4de0a7de3c0ad538/execroot/test01/bazel-out/local-fastbuild/bin
lrwxrwxrwx 1 hylom hylom  121  9月 30 00:15 bazel-genfiles -> /home/hylom/.cache/bazel/_bazel_hylom/0ab1861957d682bb4de0a7de3c0ad538/execroot/test01/bazel-out/local-fastbuild/genfiles
lrwxrwxrwx 1 hylom hylom   98  9月 30 00:15 bazel-out -> /home/hylom/.cache/bazel/_bazel_hylom/0ab1861957d682bb4de0a7de3c0ad538/execroot/__main__/bazel-out
lrwxrwxrwx 1 hylom hylom   88  9月 30 00:15 bazel-test01 -> /home/hylom/.cache/bazel/_bazel_hylom/0ab1861957d682bb4de0a7de3c0ad538/execroot/__main__
lrwxrwxrwx 1 hylom hylom  121  9月 30 00:15 bazel-testlogs -> /home/hylom/.cache/bazel/_bazel_hylom/0ab1861957d682bb4de0a7de3c0ad538/execroot/test01/bazel-out/local-fastbuild/testlogs
drwxrwxr-x 2 hylom hylom 4096  9月 29 23:29 hello

中間生成物や最終生成物は、「bazel clean」コマンドを実行することで削除できる。

$ bazel clean

今回使用した「cc_build」のような実行可能ファイルを生成するルールの場合、次のように「bazel run」コマンドで生成物を実行できる。

$ bazel run //hello
INFO: Found 1 target...
Target //hello:hello up-to-date:
  bazel-bin/hello/hello
INFO: Elapsed time: 0.070s, Critical Path: 0.00s

INFO: Running command line: bazel-bin/hello/hello
hello, world!

今回は定義していないが、Bazelではテスト用のルールも用意されており、テスト用ルールが定義されていれば「bazel test」コマンドでテストを実行できる。

汎用ルールを使ったビルドルールの記述

前節ではC/C++向けに用意されたビルドルールを使ってビルドを行ったが、「genrule」という汎用ルールを使って同様の記述を行うことも可能だ。

genrule(
  name = "hello",
  srcs = [
    "hello.c",
  ],
  outs = [
    "hello",
  ],
  executable = 1,
  output_to_bindir = 1,
  cmd = "gcc $< -O2 -o $@",
)

ここで使われている「genrule」ルールは、「cmd」で指定したコマンドを実行するというものだ。「srcs」で入力とするファイル、「outs」で出力されるファイルを指定する。また、「executable」は出力されるファイルが実行ファイルであることを指定するパラメータで、「output_to_bindir」は「bazel-bin」ディレクトリに生成されたファイルを格納することを指定するフラグとなる。これらを指定することで、「bazel run」コマンドでビルドした生成物を実行できるようになる。

「cmd」パラメータでは、変数を使って入力ファイルや出力ファイルを指定できる。たとえば「$<」は「srcs」で指定したファイルに、「$@」は「outs」で指定したファイルに置き換えられてコマンドが実行される。そのほか利用できる変数については、"Make" Variableドキュメントに記載されている。

Bazelを使ってアプリケーションのパッケージングを自動化する

Bazelではアプリケーションのビルドだけでなく、配布用のパッケージ作成やDockerイメージの作成を行うルールも用意されている。ただし、どちらも微妙に癖のある仕様となっており、現時点では作成できるパッケージはtar/tar.gz/tar.bz2形式(いわゆるtarball)とdeb(Debianパッケージ)形式のみで、またDockerイメージについてはDockerfileを使った細かい処理には対応せず、単純にtar形式のディスクイメージを作成する機能しか用意されていない。そのため利用には工夫がいるものの、うまく活用すればパッケージやイメージ作成を効率化できる。

tarパッケージの作成

Bazelでは、「pkg_tar」および「pkg_deb」というルールでtarballやdebファイルの作成を行える(「Packaging for Bazel」ドキュメント)。

まずpkg_tarルールだが、これは指定したファイルをtarコマンドでアーカイブ化するものだ。次の例は、先に紹介した「hello.c」ファイルをビルドするBUILDファイルにこのルールを追加したものだ。

load("@bazel_tools//tools/build_defs/pkg:pkg.bzl", "pkg_tar", "pkg_deb")

cc_binary(
  name = "hello",
  srcs = [
    "hello.c",
  ],
  deps = [],
  copts = ["-O2"],
)

pkg_tar(
  name = "archive",
  extension = "tar.gz",
  files = [":hello"],
  mode = "0755",
  package_dir = "usr/bin",
)

pkg_tarルールおよびpkg_debルールを利用する場合、まずBUILDファイル内でこのルールを利用することを明示する必要がある。この例の1行目「load("@bazel_tools//tools/build_defs/pkg:pkg.bzl", "pkg_tar", "pkg_deb")」が、これらのルールを利用するための宣言となる。

pkg_tarルールで指定できるパラメータについて詳しくはドキュメントを参照してほしいが、アーカイブに格納するファイルは「files」パラメータで指定している。ここは「:hello」を指定し、同じパッケージ内の「hello」というターゲット名を持つビルドルールによって生成される生成物を対象とするよう指定している。また「mode」は格納時のパーミッションを、「package_dir」はアーカイブ内におけるファイルの格納先を、「extension」は出力されるアーカイブの形式を指定するパラメータだ。

BUILDファイルをこのように書き換えた後、次のように「bazel build」コマンドを実行すると、ワークスペースのトップディレクトリ内のbazel-bin/helloディレクトリ以下にアーカイブファイルが出力される。

$ bazel build //hello:archive
INFO: Found 1 target...
Target //hello:archive up-to-date:
  bazel-bin/hello/archive.tar.gz
INFO: Elapsed time: 1.559s, Critical Path: 0.20s

出力されたアーカイブの中身は下記のようになり、「package_dir」で指定した「usr/bin」ディレクトリ内に「755」というパーミッションで「hello」ファイルが格納されていることが分かる。

$ tar tvzf bazel-bin/hello/archive.tar.gz
drwxr-xr-x 0/0               0 1970-01-01 09:00 ./
drwxr-xr-x 0/0               0 1970-01-01 09:00 ./usr/
drwxr-xr-x 0/0               0 1970-01-01 09:00 ./usr/bin/
-rwxr-xr-x 0/0            8503 1970-01-01 09:00 ./usr/bin/hello

debパッケージの作成

続いてはpkg_debルールを使ったdebパッケージの作成だが、こちらはpkg_tarルールを使って作成したtarballをそのままdebパッケージ化するという処理を行う。次の例は、先の例で作成したtarballを元にdebパッケージファイルを作成するものだ。

load("@bazel_tools//tools/build_defs/pkg:pkg.bzl", "pkg_tar", "pkg_deb")

cc_binary(
        name = "hello",
        srcs = [
                "hello.c",
        ],
        deps = [],
        copts = ["-O2"],
)

pkg_tar(
        name = "archive",
        extension = "tar.gz",
        files = [":hello"],
        mode = "0755",
        package_dir = "usr/bin",
)

pkg_deb(
  name = "deb",
  data = ":archive",
  package = "hello",
  architecture = "amd64",
  maintainer = "Hiromichi Matsushima <hylom@example.com>",
  version = "0.1.0",
  description = "hello, world!"
)

こちらについても詳細は公式ドキュメントを参照してほしいが、「data」パラメータでパッケージの作成元とするtarファイルを、「package」パラメータでパッケージ名を指定している。「architecture」や「maintainer」、「version」、「description」などのパラメータは、それぞれパッケージの情報を示すものだ。

この「deb」ルールをターゲットに指定してビルドを実行すると、debファイルが生成される。

$ bazel build //hello:deb
INFO: Found 1 target...
Target //hello:deb up-to-date:
  bazel-bin/hello/hello_0.1.0_amd64.changes
  bazel-bin/hello/hello_0.1.0_amd64.deb
  bazel-bin/hello/deb.deb
INFO: Elapsed time: 0.284s, Critical Path: 0.19s

dpkgコマンドで作成されたパッケージの情報を確認してみると、「maintainer」や「description」パラメータで指定された情報がパッケージ情報として使われていることが確認できる。

$ dpkg -I bazel-bin/hello/hello_0.1.0_amd64.deb
 新形式 debian パッケージ、バージョン 2.0。
 サイズ 2976 バイト: コントロールアーカイブ = 269 バイト。
     193 バイト、    8 行      control
 Package: hello
 Version: 0.1.0
 Section: contrib/devel
 Priority: optional
 Architecture: amd64
 Maintainer: Hiromichi Matsushima <hylom@example.com>
 Description: hello, world!
 Built-Using: Bazel

また、パッケージ内のファイルはtarballに含まれるものと一致する。

$ dpkg --contents bazel-bin/hello/hello_0.1.0_amd64.deb
drwxr-xr-x 0/0               0 1970-01-01 09:00 ./
drwxr-xr-x 0/0               0 1970-01-01 09:00 ./usr/
drwxr-xr-x 0/0               0 1970-01-01 09:00 ./usr/bin/
-rwxr-xr-x 0/0            8503 1970-01-01 09:00 ./usr/bin/hello

Dockerイメージを作成する

BazelではDockerイメージを作成することもできるが、次のような制約がある。

  • ベースとするDockerイメージはあらかじめtar形式のファイルとして用意しておく必要がある
  • 作成されたDockerイメージはtar形式のファイルとして出力される
  • イメージの作成時にコンテナ内で処理を実行することはできない
  • 可能な操作はイメージへのファイルの追加およびローカルにあるdeb形式パッケージのインストールのみ

そのため、既存のイメージをベースにイメージを作成したい場合、あらかじめgenruleルールなどを使ってベースとするイメージをtar形式で用意しておく必要がある。たとえば、Dockerの公式リポジトリで公開されている「debian:jessie」イメージをベースに先ほど作成した「hello」パッケージをインストールしたイメージを作成するためのルールは以下のようになる。

load("@bazel_tools//tools/build_defs/pkg:pkg.bzl", "pkg_tar", "pkg_deb")
load("@bazel_tools//tools/build_defs/docker:docker.bzl", "docker_build")

cc_binary(
  name = "hello",
  srcs = [
    "hello.c",
  ],
  deps = [],
  copts = ["-O2"],
)

pkg_tar(
  name = "archive",
  extension = "tar.gz",
  files = [":hello"],
  mode = "0755",
  package_dir = "usr/bin",
)

pkg_deb(
  name = "deb",
  data = ":archive",
  package = "hello",
  architecture = "amd64",
  maintainer = "Hiromichi Matsushima <hylom@users.osdn.me>",
  version = "0.1.0",
  description = "hello, world!"
)

genrule(
  name = "container_base",
  outs = ["container_base.tar"],
  cmd = "docker pull debian:jessie && docker save debian:jessie > $@",
  local = 1,
)

docker_build(
  name = "container",
  base = ":container_base",
  debs = [":deb"],
  repository = "osdn",
)

「pkg_tar」や「pkg_tar」ルールの場合と同様、「docker_build」ルールを使用する際には事前の宣言が必要だ。2行目の「load("@bazel_tools//tools/build_defs/docker:docker.bzl", "docker_build")」がそのための宣言となる。

ここで追加したルールは2つで、それぞれ「genrule」ルールと「docker_build」ルールを使用している。まずgenruleルールを使った「container_base」ターゲットだが、こちらは「container_base.tar」というファイルを出力とするルールだ。ここでは「docker pull」コマンドを実行して「debian:jessie」イメージをダウンロードし、これを「docker save」コマンドでファイルに出力している。また、ここでは「local = 1」というパラメータを指定しているが、これはBazelのサンドボックス機能を使用しないよう指定するパラメータだ。Bazelでは前述のとおり、デフォルトではビルドコマンドをサンドボックス環境で実行する。サンドボックス環境ではDockerデーモンにアクセスできない(正確に言うとDockerデーモンにアクセスするための/var/run/docker.sockファイルにアクセスできない)ため、「local」パラメータに1を指定し、このターゲットのビルド時にはサンドボックスを使用しないように指定している。

次のdocker_buildルールを使った「container」ターゲットでは、「container_base」ターゲットの出力結果(debian:jessieイメージをファイルに出力したもの)をベースとし、「deb」ターゲットの出力結果(「hello」バイナリを含むdebパッケージ)をイメージ内にインストールするという処理を指定している。指定しているパラメータについてはドキュメントを参照して欲しいが、「base」パラメータはベースとするイメージが格納されたtarファイルを、「debs」パラメータはインストールするdebパッケージを、「repository」パラメータはリポジトリ名を指定するものとなる。

これらをBUILDファイルに追記した後、「bazel build」コマンドでビルドを実行すると、以下のように「bazel-bin」ディレクトリ以下にtar形式でイメージが作成される。

$ bazel build container
INFO: Found 1 target...
INFO: From DockerLayer hello/container.layer:
Duplicate file in archive: ./usr/bin/hello, picking first occurrence
INFO: From Executing genrule //hello:container_base:
Trying to pull repository docker.io/library/debian ... jessie: Pulling from library/debian
89aec0449e93: Pulling fs layer
ae85c48b369c: Pulling fs layer
ae85c48b369c: Verifying Checksum
ae85c48b369c: Download complete
89aec0449e93: Verifying Checksum
89aec0449e93: Download complete
89aec0449e93: Pull complete
ae85c48b369c: Pull complete
Digest: sha256:fb657a90c9790812f617053c670185a64d02ae5da394354a4f12fda0f01f620b
Status: Downloaded newer image for docker.io/debian:jessie

Target //hello:container up-to-date:
  bazel-bin/hello/container-layer.tar
INFO: Elapsed time: 49.227s, Critical Path: 48.95s

続いて次のように「bazel run」コマンドを実行すると、作成したイメージがDockerで利用できるようになる。

$ bazel run container
INFO: Found 1 target...
Target //hello:container up-to-date:
  bazel-bin/hello/container-layer.tar
INFO: Elapsed time: 0.128s, Critical Path: 0.00s

INFO: Running command line: bazel-bin/hello/container
Loading 82bc4b768227fb888b419e529fab3b260a8becda7349059f6c75446af646135a...
Tagging 82bc4b768227fb888b419e529fab3b260a8becda7349059f6c75446af646135a as osdn/hello:container

この場合のコンテナ名は「<指定したリポジトリ名>/<パッケージ名>:<ターゲット名>」となる。このコンテナを実行してみると、コンテナ内に作成した「hello」コマンドがインストールされており、実行可能になっていることが分かる。

$ docker run -ti osdn/hello:container bash
root@e0cb848a82ff:/# hello
hello, world!

まとめ:利用方法は限られるものの、簡潔にルールを記述できる点には期待

さて、ここまで主としてCで記述されたコードを例にBazelの使い方を紹介してきた。このように、Bazelでは比較的シンプルな記述でコンパイルからパッケージング、Dockerイメージの作成までを行うことが可能だ。特に、簡単に配布パッケージを作成できる点は便利だろう。

ただ、現状ではいくつかの懸念点が存在する。まず、作成できるパッケージがdebパッケージのみで、Red Hat系のLinuxで使われているrpmパッケージは作成できない。また、PerlやRubyといった言語については公式にはまだサポートされていない。今回は紹介していないが、Bazelでは独自のルールを自作する機能もあるため、これらの問題はルールを自作すれば解決でき、また今後のバージョンでサポートが拡大される可能性もある。

現状ではWebサービスの構築やテストで利用するにはコツが必要だが、makeよりも簡潔にビルドルールを記述できる点は大きなメリットだ。現時点でもC/C++やJavaなどで実装したコードのビルドにおいては十分に利用できるため、今後のサポート拡大に期待したい。