マクロを使ってカスタム動詞を作成する

Bazel の日常的な操作には、主に buildtestrun のいくつかのコマンドを使用します。ただし、パッケージをリポジトリに push したり、エンドユーザーにドキュメントを公開したり、Kubernetes でアプリケーションをデプロイしたりする必要がある場合があります。しかし、Bazel には publish コマンドや deploy コマンドがありません。これらのアクションをどこに配置するのでしょうか。

bazel run コマンド

Bazel は密閉性、再現性、インクリメンタリティを重視しているため、build コマンドと test コマンドは上記のタスクには役に立ちません。これらのアクションは、ネットワーク アクセスが制限されたサンドボックス内で実行される可能性があります。また、bazel build のたびに再実行されるとは限りません。

代わりに、副作用を発生させるタスクの主軸である bazel run を使用します。Bazel ユーザーは実行可能ファイルを作成するルールに慣れており、ルール作成者は共通のパターンセットに従ってこれを「カスタム動詞」に拡張できます。

実際の環境: rules_k8s

たとえば、Bazel に関する Kubernetes ルールである rules_k8s について考えてみましょう。次のターゲットがあるとします。

# BUILD file in //application/k8s
k8s_object(
    name = "staging",
    kind = "deployment",
    cluster = "testing",
    template = "deployment.yaml",
)

bazel buildstaging ターゲットで使用されると、k8s_object ルールにより、標準の Kubernetes YAML ファイルが作成されます。ただし、追加のターゲットは、staging.apply:staging.delete などの名前を持つ k8s_object マクロによっても作成されます。これらのビルド スクリプトは、このようなアクションを実行するもので、bazel run staging.apply で実行されると、独自の bazel k8s-apply コマンドや bazel k8s-delete コマンドのように動作します。

別の例: ts_api_guardian_test

このパターンは、Angular プロジェクトでも確認できます。ts_api_guardian_test マクロは 2 つのターゲットを生成します。1 つ目は標準の nodejs_test ターゲットで、生成された出力の一部を「ゴールデン」ファイル(想定される出力を含むファイル)と比較します。これは、通常の bazel test の呼び出しでビルドして実行できます。angular-cli では、bazel test //etc/api:angular_devkit_core_api を使用してそのようなターゲットの 1 つを実行できます。

時間の経過とともに、正当な理由でこのゴールデン ファイルの更新が必要になる可能性があります。これを手動で更新すると面倒でエラーが発生しやすいため、このマクロでは nodejs_binary ターゲットも提供します。ターゲット ファイルと比較するのではなく、ゴールデン ファイルを更新します。実質的には、同じテスト スクリプトを呼び出し方法に基づいて、「確認」モードまたは「承認」モードで実行するように記述できます。これは、すでに学習したのと同じパターンに従います。ネイティブの bazel test-accept コマンドはありませんが、bazel run //etc/api:angular_devkit_core_api.accept を使用して同じ効果が得られます。

このパターンは非常に強力であり、認識できれば、かなり一般的になります。

独自のルールを適用する

マクロはこのパターンの中核です。マクロはルールと同じように使用されますが、複数のターゲットを作成することもできます。通常は、プライマリ ビルド アクションを実行する指定された名前でターゲットを作成します。通常のバイナリ、Docker イメージ、ソースコードのアーカイブをビルドします。このパターンでは、追加のターゲットが作成され、プライマリ ターゲットの出力に基づいて副作用(結果のバイナリの公開、想定されるテスト出力の更新など)を実行するスクリプトが生成されます。

これを説明するために、ウェブサイトを生成する架空のルールを Sphinx でラップし、ユーザーが準備ができたらそれを公開できる追加のターゲットを作成します。Sphinx を使用してウェブサイトを生成するために、次の既存のルールがあるとします。

_sphinx_site = rule(
     implementation = _sphinx_impl,
     attrs = {"srcs": attr.label_list(allow_files = [".rst"])},
)

次に、次のようなルールについて考えてみましょう。このルールは、実行時にスクリプトを作成して、生成されたページを公開します。

_sphinx_publisher = rule(
    implementation = _publish_impl,
    attrs = {
        "site": attr.label(),
        "_publisher": attr.label(
            default = "//internal/sphinx:publisher",
            executable = True,
        ),
    },
    executable = True,
)

最後に、次のマクロを定義して、上記のルールの両方のターゲットを作成します。

def sphinx_site(name, srcs = [], **kwargs):
    # This creates the primary target, producing the Sphinx-generated HTML.
    _sphinx_site(name = name, srcs = srcs, **kwargs)
    # This creates the secondary target, which produces a script for publishing
    # the site generated above.
    _sphinx_publisher(name = "%s.publish" % name, site = name, **kwargs)

BUILD ファイルでは、プライマリ ターゲットを作成するだけの場合と同様に、このマクロを使用します。

sphinx_site(
    name = "docs",
    srcs = ["index.md", "providers.md"],
)

この例では、マクロが標準の単一の Bazel ルールであるかのように「ドキュメント」ターゲットが作成されます。ルールをビルドすると、構成が生成され、Sphinx が実行され、手動で検査できる HTML サイトが生成されます。ただし、追加の「docs.publish」ターゲットも作成され、サイトを公開するためのスクリプトが作成されます。プライマリ ターゲットの出力を確認したら、架空の bazel publish コマンドと同様に、bazel run :docs.publish を使用して公開できます。

_sphinx_publisher ルールの実装がどのようなものかは、すぐにはわかりません。多くの場合、このようなアクションは Launcher のシェル スクリプトを作成します。この方法では通常、ctx.actions.expand_template を使用して非常にシンプルなシェル スクリプトを作成します。この場合は、プライマリ ターゲットの出力へのパスを使用してパブリッシャー バイナリを呼び出します。このようにして、パブリッシャーの実装を汎用性のままにすることができ、_sphinx_site ルールで HTML を生成するだけで済み、この小さなスクリプトだけで 2 つを組み合わせることができます。

rules_k8s では、これが .apply の動作になります。expand_templateapply.sh.tpl に基づく非常に単純な Bash スクリプトを作成し、プライマリ ターゲットの出力を使用して kubectl を実行します。次に、bazel run :staging.apply を使用してこのスクリプトをビルドして実行することで、k8s_object ターゲットに k8s-apply コマンドを効果的に提供できます。