Uso de macros para criar verbos personalizados

Informar um problema Ver código-fonte Nightly · 7.4 . 7.3 · 7.2 · 7.1 · 7.0 · 6.5

A interação diária com o Bazel acontece principalmente por alguns comandos: build, test e run. No entanto, às vezes, elas podem parecer limitadas: 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 refeitas com cada bazel build.

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

No mundo real: rules_k8s

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

# 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 são criados para realizar 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 "ideal" (ou seja, um arquivo que contém a saída esperada). Isso pode ser criado e executado com uma invocação bazel test normal. Em angular-cli, é possível executar um desses alvos com bazel test //etc/api:angular_devkit_core_api.

Com o tempo, esse arquivo de ouro pode precisar ser atualizado por motivos legítimos. Atualizar isso manualmente é tedioso e propenso a erros. Portanto, essa macro também fornece um destino nodejs_binary que atualiza o arquivo de ouro, em vez de comparar com ele. Na prática, o mesmo script de teste pode ser escrito para ser executado no modo "verificar" ou "aceitar", com base na forma 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 poderoso e se tornar muito comum depois que você aprender a reconhecê-lo.

Como adaptar suas regras

As macros são o centro desse padrão. As macros são usadas como regras, mas podem criar várias segmentações. Normalmente, eles criam um alvo com o nome especificado que executa a ação de build principal: talvez ele crie um binário normal, uma imagem Docker ou um arquivo de código-fonte. Nesse padrão, outros destinos são criados para produzir scripts que executam efeitos secundários 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, use uma regra imaginária que gera um site com o Sphinx com uma macro para criar um destino adicional que permite que o usuário publique 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 metas 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 criasse apenas 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 padrão do Bazel. Quando criada, a regra gera uma configuração 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, você pode usar bazel run :docs.publish para publicá-la para consumo público, assim como um comando bazel publish imaginário.

Não é imediatamente óbvio como a implementação da regra _sphinx_publisher pode ser feita. Muitas vezes, ações como essa gravam um script de shell do iniciador. Esse método geralmente envolve o uso de ctx.actions.expand_template para escrever um script de shell muito simples. Nesse caso, o binário do editor é invocado 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 produzir apenas HTML, e esse pequeno script é tudo o que é necessário para combinar os dois juntos.

Em rules_k8s, isso é o 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 um comando k8s-apply para destinos k8s_object.