Bazel チュートリアル: Java プロジェクトを構築する

問題を報告する ソースを表示

このチュートリアルでは、Bazel を使用した Java アプリケーションの構築の基本について説明します。ワークスペースを設定し、ターゲットや BUILD ファイルなど、Bazel の重要なコンセプトを示すシンプルな Java プロジェクトを作成します。

完了までの推定時間: 30 分。

学習内容

このチュートリアルでは、次の方法を学習します。

  • ターゲットを作成する
  • プロジェクトの依存関係を可視化する
  • プロジェクトを複数のターゲットとパッケージに分割する
  • パッケージ全体のターゲットの公開設定を制御する
  • ラベルによるターゲットの参照
  • ターゲットをデプロイする

始める前に

Bazel をインストールする

チュートリアルの準備をするには、まず Bazel をインストールします(まだインストールしていない場合)。

JDK をインストールする

  1. Java JDK をインストールします(推奨バージョンは 11 ですが、8 ~ 15 のバージョンがサポートされています)。

  2. JDK を指すように JAVA_HOME 環境変数を設定します。

    • Linux/macOS の場合:

      export JAVA_HOME="$(dirname $(dirname $(realpath $(which javac))))"
      
    • Windows の場合:

      1. コントロール パネルを開きます。
      2. [システムとセキュリティ] > [システム] > [システムの詳細設定] > [詳細設定] タブ > [環境変数...] に移動します。.
      3. [ユーザー変数] リスト(一番上のリスト)で [新規...] をクリックします。
      4. [変数名] に「JAVA_HOME」と入力します。
      5. [ディレクトリを参照] をクリックします。
      6. JDK ディレクトリに移動します(例: C:\Program Files\Java\jdk1.8.0_152)。
      7. すべてのダイアログ ウィンドウで [OK] をクリックします。

サンプル プロジェクトを取得する

Bazel の GitHub リポジトリからサンプル プロジェクトを取得します。

git clone https://github.com/bazelbuild/examples

このチュートリアルのサンプル プロジェクトは examples/java-tutorial ディレクトリにあり、次のように構成されています。

java-tutorial
├── BUILD
├── src
│   └── main
│       └── java
│           └── com
│               └── example
│                   ├── cmdline
│                   │   ├── BUILD
│                   │   └── Runner.java
│                   ├── Greeting.java
│                   └── ProjectRunner.java
└── WORKSPACE

Bazel を使用したビルド

ワークスペースを設定する

プロジェクトを構築するには、まずワークスペースを設定する必要があります。ワークスペースは、プロジェクトのソースファイルと Bazel のビルド出力を保持するディレクトリです。また、Bazel が特別と認識しているファイルも含まれます。

  • ディレクトリとその内容は Bazel ワークスペースとして識別され、プロジェクトのディレクトリ構造のルートにある WORKSPACE ファイル。

  • 1 つ以上の BUILD ファイル。プロジェクトのさまざまな部分をどのようにビルドするかを Bazel に指示します。(BUILD ファイルを含むワークスペース内のディレクトリはパッケージです。パッケージについては、このチュートリアルの後半で説明します)。

Bazel ワークスペースとしてディレクトリを指定するには、そのディレクトリに WORKSPACE という名前の空のファイルを作成します。

Bazel でプロジェクトをビルドする場合、すべての入力と依存関係を同じワークスペースに配置する必要があります。異なるワークスペースに存在するファイルは、リンクされていない限り、互いに独立しています。このチュートリアルの範囲外です。

BUILD ファイルについて

BUILD ファイルには、Bazel に関するさまざまなタイプの手順が含まれています。最も重要なタイプはビルドルールです。このルールは、実行可能なバイナリやライブラリなど、目的の出力をビルドする方法を Bazel に指示します。BUILD ファイル内のビルドルールの各インスタンスはターゲットと呼ばれ、特定のソースファイルと依存関係のセットをポイントします。ターゲットは他のターゲットを指すこともできます。

java-tutorial/BUILD ファイルを確認します。

java_binary(
    name = "ProjectRunner",
    srcs = glob(["src/main/java/com/example/*.java"]),
)

この例では、ProjectRunner ターゲットは Bazel の組み込み java_binary ルールをインスタンス化します。このルールは、.jar ファイルとラッパー シェル スクリプト(どちらもターゲットに由来する名前)をビルドするように Bazel に指示します。

ターゲットの属性で依存関係とオプションが明示的に記述されている。 name 属性は必須ではありませんが、多くは省略可能です。たとえば、ProjectRunner ルール ターゲットで、name はターゲットの名前、srcs は Bazel がターゲットのビルドに使用するソースファイル、main_class は main メソッドを含むクラスを指定します。(この例では、ソースファイルを 1 つずつ一覧表示するのではなく、glob を使用して Bazel に渡しています)。

プロジェクトをビルドする

サンプル プロジェクトをビルドするには、java-tutorial ディレクトリに移動して次のコマンドを実行します。

bazel build //:ProjectRunner

ターゲット ラベルにおいて、// の部分はワークスペースのルート(この場合はルート自体)に対する BUILD ファイルの場所であり、ProjectRunnerBUILD ファイル内のターゲット名です。(ターゲット ラベルについては、このチュートリアルの最後で詳しく説明します)。

Bazel によって、次のような出力が生成されます。

   INFO: Found 1 target...
   Target //:ProjectRunner up-to-date:
      bazel-bin/ProjectRunner.jar
      bazel-bin/ProjectRunner
   INFO: Elapsed time: 1.021s, Critical Path: 0.83s

これで、最初の Bazel ターゲットが作成されました。Bazel は、ビルドの出力をワークスペースのルートの bazel-bin ディレクトリに配置します。その内容を参照して、Bazel の出力構造を把握できます。

新たにビルドしたバイナリをテストします。

bazel-bin/ProjectRunner

依存関係グラフを確認する

Bazel では、BUILD ファイルでビルドの依存関係を明示的に宣言する必要があります。Bazel はこれらのステートメントを使用して、プロジェクトの依存関係グラフを作成します。これにより、正確な増分ビルドが可能になります。

サンプル プロジェクトの依存関係を可視化するには、ワークスペース ルートで次のコマンドを実行して、依存関係グラフのテキスト表現を生成します。

bazel query  --notool_deps --noimplicit_deps "deps(//:ProjectRunner)" --output graph

上記のコマンドは、ターゲット //:ProjectRunner のすべての依存関係(ホストと暗黙的な依存関係を除く)を探して、出力をグラフとしてフォーマットするよう Bazel に指示します。

次に、テキストを GraphViz に貼り付けます。

ご覧のように、プロジェクトには単一の依存関係があり、追加の依存関係なしで 2 つのソースファイルを作成しています。

ターゲット「ProjectRunner」の依存関係グラフ

ワークスペースを設定し、プロジェクトをビルドして依存関係を検証したら、さらに複雑にすることができます。

Bazel ビルドの改良

小規模なプロジェクトでは 1 つのターゲットで十分ですが、大規模なプロジェクトを複数のターゲットとパッケージに分割して、迅速な増分ビルド(つまり、変更されたプロジェクトの再ビルドのみ)や、プロジェクトの複数の部分を一度にビルドすることでビルドを高速化することもできます。

複数のビルド ターゲットを指定する

サンプル プロジェクトのビルドは 2 つのターゲットに分割できます。java-tutorial/BUILD ファイルの内容を次のように置き換えます。

java_binary(
    name = "ProjectRunner",
    srcs = ["src/main/java/com/example/ProjectRunner.java"],
    main_class = "com.example.ProjectRunner",
    deps = [":greeter"],
)

java_library(
    name = "greeter",
    srcs = ["src/main/java/com/example/Greeting.java"],
)

この構成では、Bazel が最初に greeter ライブラリをビルドし、次に ProjectRunner バイナリをビルドします。java_binarydeps 属性は、ProjectRunner バイナリをビルドするために greeter ライブラリが必要であることを Bazel に伝えます。

プロジェクトのこの新しいバージョンをビルドするには、次のコマンドを実行します。

bazel build //:ProjectRunner

Bazel によって、次のような出力が生成されます。

INFO: Found 1 target...
Target //:ProjectRunner up-to-date:
  bazel-bin/ProjectRunner.jar
  bazel-bin/ProjectRunner
INFO: Elapsed time: 2.454s, Critical Path: 1.58s

新たにビルドしたバイナリをテストします。

bazel-bin/ProjectRunner

ここで ProjectRunner.java を変更してプロジェクトを再ビルドした場合、Bazel はそのファイルのみを再コンパイルします。

依存関係グラフを見ると、ProjectRunner が以前と同じ入力に依存していることがわかりますが、ビルドの構造は異なります。

依存関係を追加した後のターゲット「ProjectRunner」の依存関係グラフ

これで、2 つのターゲットを含むプロジェクトがビルドされました。ProjectRunner ターゲットは 2 つのソースファイルを作成し、もう 1 つのターゲット(:greeter)に依存します。これにより、追加のソースファイルが 1 つビルドされます。

複数のパッケージを使用する

プロジェクトを複数のパッケージに分割しましょう。src/main/java/com/example/cmdline ディレクトリを見ると、BUILD ファイルといくつかのソースファイルも含まれていることがわかります。そのため、Bazel ではワークスペースに BUILD ファイルが 1 つあるため、//src/main/java/com/example/cmdline// の 2 つのパッケージが含まれるようになりました。

src/main/java/com/example/cmdline/BUILD ファイルを確認します。

java_binary(
    name = "runner",
    srcs = ["Runner.java"],
    main_class = "com.example.cmdline.Runner",
    deps = ["//:greeter"],
)

runner ターゲットは // パッケージの greeter ターゲットに依存します(したがってターゲット ラベル //:greeter です)。Bazel は deps 属性でこれを認識します。依存関係グラフを見てみましょう。

ターゲット「runner」の依存関係グラフ

ただし、ビルドを正常に行うには、visibility 属性を使用して、//src/main/java/com/example/cmdline/BUILD 内の runner ターゲットを //BUILD 内のターゲットに明示的に指定する必要があります。これは、デフォルトでは、同じ BUILD ファイル内の他のターゲットにのみターゲットが表示されるためです。(Bazel はターゲットの公開設定を使用して、実装の詳細を含むライブラリが公開 API にリークするなどの問題を防止しています)。

これを行うには、次のように java-tutorial/BUILDgreeter ターゲットに visibility 属性を追加します。

java_library(
    name = "greeter",
    srcs = ["src/main/java/com/example/Greeting.java"],
    visibility = ["//src/main/java/com/example/cmdline:__pkg__"],
)

これで、ワークスペースのルートで以下のコマンドを実行して、新しいパッケージをビルドできます。

bazel build //src/main/java/com/example/cmdline:runner

Bazel によって、次のような出力が生成されます。

INFO: Found 1 target...
Target //src/main/java/com/example/cmdline:runner up-to-date:
  bazel-bin/src/main/java/com/example/cmdline/runner.jar
  bazel-bin/src/main/java/com/example/cmdline/runner
  INFO: Elapsed time: 1.576s, Critical Path: 0.81s

新たにビルドしたバイナリをテストします。

./bazel-bin/src/main/java/com/example/cmdline/runner

ここでは、1 つのターゲットを含む 2 つのパッケージとしてビルドするようにプロジェクトを変更し、各パッケージの依存関係を理解しました。

ラベルを使用してターゲットを参照する

Bazel は、BUILD ファイルとコマンドラインでターゲット ラベルを使用してターゲットを参照します(例: //:ProjectRunner//src/main/java/com/example/cmdline:runner)。構文は次のとおりです。

//path/to/package:target-name

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

リポジトリ ルートでターゲットを参照する場合、パッケージパスは空です。//:target-name を使用してください。同じ BUILD ファイル内でターゲットを参照する場合は、// ワークスペースのルート識別子をスキップして、:target-name のみを使用することもできます。

たとえば、java-tutorial/BUILD ファイルのターゲットの場合、ワークスペースのルートはパッケージ(//)であり、2 つのターゲット ラベルは単に //:ProjectRunner//:greeter であるため、パッケージのパスを指定する必要はありません。

ただし、//src/main/java/com/example/cmdline/BUILD ファイル内のターゲットについては、//src/main/java/com/example/cmdline の完全なパッケージパスを指定する必要があり、ターゲット ラベルは //src/main/java/com/example/cmdline:runner でした。

デプロイ用の Java ターゲットのパッケージ化

次に、すべてのランタイム依存関係を含むバイナリをビルドすることで、デプロイ用の Java ターゲットをパッケージ化します。これにより、開発環境の外部でバイナリを実行できます。

すでに説明したように、java_binary ビルドルールは .jar とラッパー シェル スクリプトを生成します。次のコマンドを使用して、runner.jar の内容を確認します。

jar tf bazel-bin/src/main/java/com/example/cmdline/runner.jar

内容は次のとおりです。

META-INF/
META-INF/MANIFEST.MF
com/
com/example/
com/example/cmdline/
com/example/cmdline/Runner.class

ご覧のように、runner.jar には Runner.class が含まれていますが、その依存関係 Greeting.class は含まれていません。Bazel が生成する runner スクリプトは greeter.jar をクラスパスに追加します。このままでおくと、ローカルで実行されますが、別のマシン上でスタンドアロンで実行されることはありません。幸いなことに、java_binary ルールを使用すると、自己完結型のデプロイ可能なバイナリをビルドできます。ビルドするには、ターゲット名に _deploy.jar を追加します。

bazel build //src/main/java/com/example/cmdline:runner_deploy.jar

Bazel によって、次のような出力が生成されます。

INFO: Found 1 target...
Target //src/main/java/com/example/cmdline:runner_deploy.jar up-to-date:
  bazel-bin/src/main/java/com/example/cmdline/runner_deploy.jar
INFO: Elapsed time: 1.700s, Critical Path: 0.23s

これで runner_deploy.jar がビルドされました。必要なランタイム依存関係が含まれているため、開発環境からスタンドアロンで実行できます。前と同じコマンドを使用して、このスタンドアロン JAR の内容を確認します。

jar tf bazel-bin/src/main/java/com/example/cmdline/runner_deploy.jar

コンテンツには、実行に必要なすべてのクラスが含まれています。

META-INF/
META-INF/MANIFEST.MF
build-data.properties
com/
com/example/
com/example/cmdline/
com/example/cmdline/Runner.class
com/example/Greeting.class

参考資料

詳しくは以下をご覧ください。

今後ともよろしくお願いいたします。