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

問題を報告 ソースを表示 Nightly · 8.3 · 8.2 · 8.1 · 8.0 · 7.6

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",
)

k8s_object ルールは、staging ターゲットで bazel build が使用されている場合に、標準の Kubernetes YAML ファイルをビルドします。ただし、追加のターゲットも k8s_object マクロによって作成され、staging.apply:staging.delete などの名前が付けられます。これらのアクションを実行するビルドスクリプトを作成し、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,
)

最後に、上記の 2 つのルール両方のターゲットをまとめて作成するために、次のシンボリック マクロ(Bazel 8 以降で使用可能)を定義します。

def _sphinx_site_impl(name, visibility, srcs, **kwargs):
    # This creates the primary target, producing the Sphinx-generated HTML. We
    # set `visibility = visibility` to make it visible to callers of the
    # macro.
    _sphinx_site(name = name, visibility = visibility, srcs = srcs, **kwargs)
    # This creates the secondary target, which produces a script for publishing
    # the site generated above. We don't want it to be visible to callers of
    # our macro, so we omit visibility for it.
    _sphinx_publisher(name = "%s.publish" % name, site = name, **kwargs)

sphinx_site = macro(
    implementation = _sphinx_site_impl,
    attrs = {"srcs": attr.label_list(allow_files = [".rst"])},
    # Inherit common attributes like tags and testonly
    inherit_attrs = "common",
)

または、Bazel 8 より前の Bazel リリースをサポートする必要がある場合は、代わりにレガシー マクロを定義します。

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 ルールであるかのように、「docs」ターゲットが作成されます。ビルドすると、ルールによって構成が生成され、Sphinx が実行されて HTML サイトが生成されます。このサイトは手動で検査できます。ただし、サイトを公開するためのスクリプトをビルドする「docs.publish」ターゲットも作成されます。プライマリ ターゲットの出力を確認したら、bazel run :docs.publish を使用して、仮想の bazel publish コマンドのように一般公開できます。

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

rules_k8s では、.apply は実際に次の処理を行います。expand_template は、apply.sh.tpl に基づいて非常にシンプルな Bash スクリプトを作成し、プライマリ ターゲットの出力で kubectl を実行します。このスクリプトは bazel run :staging.apply でビルドして実行できるため、k8s_object ターゲットに k8s-apply コマンドを効果的に提供できます。