CubicLouve

Spring_MTの技術ブログです。https://github.com/SpringMT (http://spring-mt.tumblr.com/ からの移転)

BazelでC++をビルドする

以前Bazelの入門の記事をかきましたが、今回はC++をビルドする環境を作成してみます。 spring-mt.hatenablog.com

下記のチュートリアル通りにやっていきます。 Build Tutorial - C++ - Bazel

まずはexampleをcloneしてやっていくのですが、前回やった残りのプロジェクトもあるのでサクッと自分でやっていきます。

【1】単一パッケージ、単一ターゲットでビルド

workspaceのセットアップを行います。

workspaceにはソースファイルとビルド成果物が含まれます。

Bazelのowrkspaceとして、トップディレクトリにWORKSPACEファイルを置きます。

今回は依存関係がないので、空ファイルで置いておきます。

プロジェクトをビルドするために、全てのインプットと依存関係を同一workspaceに含める必要があります。

異なるワークスペースに存在するファイルは、リンクしていなければ互いに独立しています。

BUILDファイルはソースコードなどビルドに必要なファイルやビルド方法、出力されるファイルなどの設定を記述するファイルです。

BUILDファイルを含むワークスペース内のディレクトリはパッケージという単位になります。

BUILDファイルにはBazelのさまざまな種類の設定を記述しますが、一番重要な設定はビルドルールです。

ビルドルールは、実行可能なバイナリやライブラリなど、どのような成果物を作成するかを記述します。

BUILDファイル内のビルドルールのそれぞれの実体はターゲットと呼ばれ、ソース・ファイルと依存関係の特定のセットを指します。ターゲットは他のターゲット郡を指すこともできます。

最初に下記構成にしてみました。

.
├── WORKSPACE
└── main
    ├── BUILD
    └── hello-world.cc

まずは単純なBUILDファイルを見ていきます。

cc_binary(
    name = "hello-world",
    srcs = ["hello-world.cc"],
)

nameで指定したパラメータがターゲット名になります。(nameの指定は必須です。)

hello-worldターゲットはcc_binaryルールをインスタンス化します。 このルールでは依存関係のないhello-world.ccソースファイルから自己完結型の実行可能バイナリをビルドします。

ではビルドしてみます。

ビルドコマンドは

bazel build //main:hello-world

//main: はworkspaseのルートディレクトリからのBUILDファイルが配置されてる相対パスを示しています。hello-worldBUILDファイル内で指定したnameです。

% bazel build //main:hello-world
.....................
INFO: Found 1 target...
Target //main:hello-world up-to-date:
  bazel-bin/main/hello-world
INFO: Elapsed time: 12.816s, Critical Path: 3.27s
% bazel build //main:hello-world
INFO: Found 1 target...
Target //main:hello-world up-to-date:
  bazel-bin/main/hello-world
INFO: Elapsed time: 0.165s, Critical Path: 0.00s

(二回目はキャッシュが使われて高速にビルドできています)

ビルドが完了すると、workspaceのルート直下のbazel-binに成果物ができています。

 % bazel-bin/main/hello-world
Hello world
Fri Aug  4 01:11:51 2017

依存性の確認

BUILDファイルにはビルドの依存関係がすべて明示的に記述されています。

Bazelはこれらのステートメントを使用してプロジェクトの依存関係グラフを作成し、正確なインクリメンタルビルドを可能にします。

前述のプロジェクトの依存性のグラフは下記コマンドで生成できます。

% bazel query --nohost_deps --noimplicit_deps 'deps(//main:hello-world)' --output graph
digraph mygraph {
  node [shape=box];
"//main:hello-world"
"//main:hello-world" -> "//main:hello-world.cc"
"//main:hello-world.cc"
}

グラフ構造はGraphViz で可視化できます。

【2】単一のパッケージ、複数のターゲット

大きなプロジェクトを複数のターゲットとパッケージに分割することで、変更した部分の再ビルドだけの高速なインクリメンタルビルドができ、分割されたプロジェクトを一度にビルドするとことでビルド時間の短縮ができます。

【1】プロジェクトを2つのターゲットに分けます。

diff --git a/CppExamples/main/BUILD b/CppExamples/main/BUILD
index 20c6f47..43b22a4 100644
--- a/CppExamples/main/BUILD
+++ b/CppExamples/main/BUILD
@@ -1,4 +1,13 @@
+cc_library(
+    name = "hello-greet",
+    srcs = ["hello-greet.cc"],
+    hdrs = ["hello-greet.h"],
+)
+
 cc_binary(
     name = "hello-world",
     srcs = ["hello-world.cc"],
+    deps = [
+        ":hello-greet",
+    ],
 )

hello-worldのリンクしたいライブラリのラベルをdepsに指定します。

こうすると、hello-worldをビルドする前に、hello-greetのビルドが実行されます。

% bazel build //main:hello-world
INFO: Found 1 target...
Target //main:hello-world up-to-date:
  bazel-bin/main/hello-world
INFO: Elapsed time: 5.813s, Critical Path: 1.16s

% bazel-bin/main/hello-world
Hello world
Sun Aug  6 15:23:47 2017

hello-greet.ccが修正されたら、hello-greet.ccのみコンパイルが行われます。

【3】複数のパッケージ、複数のターゲット

BUILDファイルを含む新しいディレクトリを作成します。 (BUILDファイルを含むワークスペース内のディレクトリはパッケージという単位)

cc_library(
    name = "hello-time",
    srcs = ["hello-time.cc"],
    hdrs = ["hello-time.h"],
    visibility = ["//main:__pkg__"],
)

srcsでソースファイルを、hdrsでヘッダーファイルを指定します。

lib/BUILDのなかで指定した// lib:hello-timeターゲットをmain/ BUILDから明示的に見えるようにするためにvisibility//main:__pkg__を指定しています。

デフォルトでは、ターゲットは同じBUILDファイル内の他のターゲットに対してのみだけに公開されています。

Bazelは、ターゲットの可視性を利用して、ライブラリの実装の詳細がパブリックAPIに漏れることを防いでいます。

Common Definitions - Bazel

ビルドをしてみます。

% bazel build //main:hello-world
INFO: Found 1 target...
Target //main:hello-world up-to-date:
  bazel-bin/main/hello-world
INFO: Elapsed time: 0.365s, Critical Path: 0.00s
% bazel-bin/main/hello-world
Hello world
Sun Aug  6 22:01:01 2017

ラベルの使い方

BUILDファイルやコマンドライン上では、ラベルからターゲットを参照します。

シンタックスは下記の通り。

//path/to/package:target-name

ターゲットがruleのターゲットの場合、path/to/packageはBUILDファイルを含むディレクトリへのパスで、target-nameはBUILDファイルのターゲット(name)の名前です。

ターゲットがファイルである場合、path/to/packageはパッケージのrootからのパスであり、target-nameはターゲットファイル名です。

同じパッケージ内のターゲットを参照するときは、パッケージパスを省略して//:target-nameとだけで使えます。

同じBUILDファイル内のターゲットを参照する場合は、//のworkspaceのルート識別子をスキップして、:target-nameだけを使用することができます。