Uso de macros para criar verbos personalizados

Informar um problema Ver fonte Nightly · 8.3 · 8.2 · 8.1 · 8.0 · 7.6

A interação diária com o Bazel acontece principalmente por alguns comandos: build, test e run. No entanto, às vezes, esses recursos podem parecer limitados: talvez você queira enviar pacotes para um repositório, publicar documentação para usuários finais ou implantar um aplicativo com o Kubernetes. Mas o Bazel não tem um comando publish ou deploy. Onde essas ações se encaixam?

O comando "bazel run"

O foco do Bazel em hermeticidade, reprodutibilidade e incrementalidade significa que os comandos build e test não são úteis para as tarefas acima. Essas ações podem ser executadas em um sandbox, com acesso limitado à rede, e não há garantia de que serão executadas novamente a cada bazel build.

Em vez disso, use bazel run, o cavalo de batalha para tarefas que você quer ter efeitos colaterais. Os usuários do Bazel estão acostumados a regras que criam executáveis, e os autores de regras podem seguir um conjunto comum de padrões para estender isso a "verbos personalizados".

Na natureza: rules_k8s

Por exemplo, considere rules_k8s, as regras do Kubernetes para Bazel. Suponha que você tenha a seguinte meta:

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

A regra k8s_object cria um arquivo YAML padrão do Kubernetes quando bazel build é usado no destino staging. No entanto, os destinos adicionais também são criados pela macro k8s_object com nomes como staging.apply e :staging.delete. Esses scripts de build realizam essas ações e, quando executados com bazel run staging.apply, se comportam como nossos próprios comandos bazel k8s-apply ou bazel k8s-delete.

Outro exemplo: ts_api_guardian_test

Esse padrão também pode ser visto no projeto Angular. A macro ts_api_guardian_test produz dois destinos. O primeiro é um destino nodejs_test padrão que compara algumas saídas geradas com um arquivo "ouro", ou seja, um arquivo que contém a saída esperada. Ele pode ser criado e executado com uma invocação normal de bazel test. Em angular-cli, é possível executar um desses destinos com bazel test //etc/api:angular_devkit_core_api.

Com o tempo, esse arquivo dourado pode precisar ser atualizado por motivos legítimos. Atualizar isso manualmente é cansativo e propenso a erros. Por isso, essa macro também fornece um destino nodejs_binary que atualiza o arquivo de referência, em vez de comparar com ele. Na prática, o mesmo script de teste pode ser escrito para ser executado no modo "verify" ou "accept", com base em como ele é invocado. Isso segue o mesmo padrão que você já aprendeu: não há um comando bazel test-accept nativo, mas o mesmo efeito pode ser alcançado com bazel run //etc/api:angular_devkit_core_api.accept.

Esse padrão pode ser muito eficiente e se torna bastante comum quando você aprende a reconhecê-lo.

Adaptar suas próprias regras

As macros são a base desse padrão. As macros são usadas como regras, mas podem criar vários destinos. Normalmente, eles criam um destino com o nome especificado que executa a ação de build principal. Talvez ele crie um binário normal, uma imagem do Docker ou um arquivo de código-fonte. Nesse padrão, outros destinos são criados para produzir scripts que realizam efeitos colaterais com base na saída do destino principal, como publicar o binário resultante ou atualizar a saída de teste esperada.

Para ilustrar isso, encapsule uma regra imaginária que gera um site com Sphinx com uma macro para criar um destino adicional que permita ao usuário publicar quando estiver pronto. Considere a seguinte regra para gerar um site com o Sphinx:

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

Em seguida, considere uma regra como a seguinte, que cria um script que, quando executado, publica as páginas geradas:

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

Por fim, defina a seguinte macro simbólica (disponível no Bazel 8 ou mais recente) para criar destinos para as duas regras acima juntas:

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

Ou, se você precisar oferecer suporte a versões do Bazel anteriores à 8, defina uma macro legada:

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)

Nos arquivos BUILD, use a macro como se ela apenas criasse o destino principal:

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

Neste exemplo, um destino "docs" é criado, como se a macro fosse uma regra única e padrão do Bazel. Quando criada, a regra gera algumas configurações e executa o Sphinx para produzir um site HTML pronto para inspeção manual. No entanto, um destino "docs.publish" adicional também é criado, o que gera um script para publicar o site. Depois de verificar a saída do destino principal, use bazel run :docs.publish para publicar o conteúdo para consumo público, assim como um comando bazel publish imaginário.

Não é imediatamente óbvio como seria a implementação da regra _sphinx_publisher. Muitas vezes, ações como essa gravam um script shell de iniciador. Esse método normalmente envolve o uso de ctx.actions.expand_template para gravar um script shell muito simples, neste caso, invocando o binário do editor com um caminho para a saída do destino principal. Dessa forma, a implementação do editor pode permanecer genérica, a regra _sphinx_site pode apenas produzir HTML, e esse pequeno script é tudo o que é necessário para combinar os dois juntos.

Em rules_k8s, é isso que .apply faz: expand_template grava um script Bash muito simples, com base em apply.sh.tpl, que executa kubectl com a saída do destino principal. Esse script pode ser criado e executado com bazel run :staging.apply, fornecendo efetivamente um comando k8s-apply para destinos k8s_object.