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

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

推定完了時間: 30 分

学習内容

このチュートリアルでは、次の方法について説明します。

  • ターゲットをビルドする
  • プロジェクトの依存関係を可視化する
  • プロジェクトを複数のターゲットとパッケージに分割する
  • パッケージ間でターゲットの公開設定を制御する
  • ラベルを使用してターゲットを参照する
  • ターゲットをデプロイする

始める前に

Bazel をインストールする

チュートリアルを開始する前に、Bazel がまだインストールされていない場合は、Bazel をインストールします

JDK をインストールする

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

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

    • Linux/macOS の場合:

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

    • コントロール パネルを開きます。

    • [システムとセキュリティ] > [システム] > [システムの詳細設定] > [詳細設定] タブ > [環境変数...]に移動します。

    • [ユーザー環境変数] リスト(上部)で [新規...] をクリックします。

    • ["Variable name"] フィールドに JAVA_HOME と入力します。

    • [ディレクトリの参照...] をクリックします。

    • JDK ディレクトリ(C:\Program Files\Java\jdk1.8.0_152 など)に移動します。

    • すべてのダイアログ ウィンドウで [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
└── MODULE.bazel

Bazel を使用したビルド

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

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

  • MODULE.bazel ファイル。ディレクトリとそのコンテンツを Bazel ワークスペースとして識別し、プロジェクトのディレクトリ 構造のルートに配置されます。

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

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

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 ルールをインスタンス化します。このルールは、 ファイルとラッパー シェル スクリプト(どちらも ターゲットと同じ名前)をビルドするように Bazel に指示します。.jar

ターゲットの属性は、その依存関係とオプションを明示的に示します。 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 ビルドを改善する

小規模なプロジェクトでは単一のターゲットで十分ですが、大規模なプロジェクトでは、高速な増分ビルド(変更されたものだけを再ビルドする)を可能にするため、また、プロジェクトの複数の部分を同時にビルドしてビルドを高速化するために、複数のターゲットとパッケージに分割することをおすすめします。

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

サンプル プロジェクトのビルドを 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 ターゲットは 1 つのソースファイルをビルドし、もう 1 つのターゲット(:greeter)に依存します。このターゲットは 1 つの追加のソースファイルをビルドします。

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

次に、プロジェクトを複数のパッケージに分割します。 src/main/java/com/example/cmdline ディレクトリを見ると、 a BUILD ファイルといくつかのソースファイルが含まれています。したがって、Bazel にとって、ワークスペースには //src/main/java/com/example/cmdline// の 2 つのパッケージが含まれています(ワークスペースのルートに BUILD ファイルがあるため)。

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」の依存関係グラフ

ただし、ビルドを成功させるには、runner ターゲット に //src/main/java/com/example/cmdline/BUILD のターゲットの公開設定を //BUILD を使用して明示的に付与する必要があります。visibilityこれは、デフォルトでは、ターゲット は同じ 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

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

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

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

//path/to/package:target-name

ターゲットがルール ターゲットの場合、path/to/packageBUILD ファイルを含む ディレクトリのパス、target-nameBUILD ファイルでターゲットに付けた名前(name 属性)です。ターゲットがファイル ターゲットの場合、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 ビルドルールは a .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

関連情報

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

ビルドをお楽しみください。