Extensões de módulo

<ph-0-0>

As extensões de módulo permitem que os usuários estendam o sistema de módulos lendo dados de entrada dos módulos no gráfico de dependência, executando a lógica necessária para resolver dependências e, por fim, criando repositórios chamando as regras de repo. Essas extensões têm recursos semelhantes às regras do repositório, o que permite que elas realizem E/S de arquivos, enviem solicitações de rede e assim por diante. Entre outras coisas, eles permitem que o Bazel interaja com outros sistemas de gerenciamento de pacotes e, ao mesmo tempo, respeita o gráfico de dependência criado com base nos módulos dele.

É possível definir extensões de módulos em arquivos .bzl, assim como as regras de repositório. Elas não são invocadas diretamente. Em vez disso, cada módulo especifica partes de dados chamadas tags para que as extensões leiam. O Bazel executa a resolução do módulo antes de avaliar qualquer extensão. A extensão lê todas as tags pertencentes a ela em todo o gráfico de dependência.

Uso da extensão

As extensões são hospedadas nos próprios módulos do Bazel. Para usar uma extensão em um módulo, primeiro adicione um bazel_dep no módulo que hospeda a extensão e chame a função integrada use_extension para incluí-la no escopo. Considere o seguinte exemplo: um snippet de um arquivo MODULE.bazel para usar a extensão "maven" definida no módulo rules_jvm_external:

bazel_dep(name = "rules_jvm_external", version = "4.5")
maven = use_extension("@rules_jvm_external//:extensions.bzl", "maven")

Isso vincula o valor de retorno de use_extension a uma variável, o que permite que o usuário use a sintaxe de ponto para especificar tags para a extensão. As tags precisam seguir o esquema definido pelas classes de tag correspondentes especificadas na definição de extensão. Veja um exemplo de especificação de algumas tags maven.install e maven.artifact:

maven.install(artifacts = ["org.junit:junit:4.13.2"])
maven.artifact(group = "com.google.guava",
               artifact = "guava",
               version = "27.0-jre",
               exclusions = ["com.google.j2objc:j2objc-annotations"])

Use a diretiva use_repo para colocar os repositórios gerados pela extensão no escopo do módulo atual.

use_repo(maven, "maven")

Os repositórios gerados por uma extensão fazem parte da API dela. Neste exemplo, a extensão do módulo "maven" promete gerar um repositório chamado maven. Com a declaração acima, a extensão resolve adequadamente rótulos como @maven//:org_junit_junit para apontar para o repositório gerado pela extensão "maven".

Definição de extensão

É possível definir extensões de módulo de maneira semelhante às regras de repositório, usando a função module_extension. No entanto, embora as regras de repo tenham vários atributos, as extensões de módulo têm tag_classes, cada um com vários atributos. As classes de tag definem os esquemas das tags usadas por essa extensão. Por exemplo, a extensão "maven" acima pode ser definida assim:

# @rules_jvm_external//:extensions.bzl

_install = tag_class(attrs = {"artifacts": attr.string_list(), ...})
_artifact = tag_class(attrs = {"group": attr.string(), "artifact": attr.string(), ...})
maven = module_extension(
  implementation = _maven_impl,
  tag_classes = {"install": _install, "artifact": _artifact},
)

Essas declarações mostram que as tags maven.install e maven.artifact podem ser especificadas usando o esquema de atributos especificado.

A função de implementação das extensões de módulo é semelhante às das regras de repo, exceto pelo fato de que elas recebem um objeto module_ctx, que concede acesso a todos os módulos usando a extensão e todas as tags pertinentes. Em seguida, a função de implementação chama as regras de repo para gerar repositórios.

# @rules_jvm_external//:extensions.bzl

load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_file")  # a repo rule
def _maven_impl(ctx):
  # This is a fake implementation for demonstration purposes only

  # collect artifacts from across the dependency graph
  artifacts = []
  for mod in ctx.modules:
    for install in mod.tags.install:
      artifacts += install.artifacts
    artifacts += [_to_artifact(artifact) for artifact in mod.tags.artifact]

  # call out to the coursier CLI tool to resolve dependencies
  output = ctx.execute(["coursier", "resolve", artifacts])
  repo_attrs = _process_coursier_output(output)

  # call repo rules to generate repos
  for attrs in repo_attrs:
    http_file(**attrs)
  _generate_hub_repo(name = "maven", repo_attrs)

Identidade da extensão

As extensões do módulo são identificadas pelo nome e pelo arquivo .bzl que aparece na chamada para use_extension. No exemplo a seguir, a extensão maven é identificada pelo arquivo .bzl @rules_jvm_external//:extension.bzl e pelo nome maven:

maven = use_extension("@rules_jvm_external//:extensions.bzl", "maven")

Exportar novamente uma extensão de um arquivo .bzl diferente dá a ela uma nova identidade. Se as duas versões da extensão forem usadas no gráfico do módulo transitivo, elas serão avaliadas separadamente e verão apenas as tags associadas a essa identidade específica.

Como autor de extensão, você precisa garantir que os usuários usem sua extensão de módulo apenas de um único arquivo .bzl.

Nomes e visibilidade dos repositórios

Os repositórios gerados por extensões têm nomes canônicos no formato de module_repo_canonical_name~extension_name~repo_name. Para extensões hospedadas no módulo raiz, a parte module_repo_canonical_name é substituída pela string _main. Observe que o formato de nome canônico não é uma API de que você precisa depender. Ele está sujeito a mudanças a qualquer momento.

Essa política de nomenclatura significa que cada extensão tem seu próprio "namespace repo". Duas extensões distintas podem definir um repositório com o mesmo nome sem o risco de conflitos. Isso também significa que repository_ctx.name informa o nome canônico do repo, que não é o mesmo nome especificado na chamada de regra de repo.

Considerando os repositórios gerados por extensões de módulo, há várias regras de visibilidade do repo:

  • Um repositório de módulo do Bazel pode conferir todos os repositórios introduzidos no arquivo MODULE.bazel usando bazel_dep e use_repo.
  • Um repositório gerado por uma extensão de módulo pode ver todos os repositórios visíveis para o módulo que hospeda a extensão, além de todos os outros repositórios gerados pela mesma extensão de módulo (usando os nomes especificados nas chamadas de regra de repo como os nomes aparentes deles).
    • Isso pode resultar em um conflito. Se o repositório do módulo puder ver um repositório com o nome aparente foo e a extensão gerar um repositório com o nome especificado foo, então, para todos os repositórios gerados por essa extensão, foo se referirá ao primeiro.

Práticas recomendadas

Esta seção descreve as práticas recomendadas ao criar extensões para que sejam fáceis de usar, manter e se adaptar bem às mudanças ao longo do tempo.

Colocar cada extensão em um arquivo separado

Quando as extensões estão em arquivos diferentes, elas permitem que uma extensão carregue repositórios gerados por outra. Mesmo que você não use essa funcionalidade, é melhor colocá-las em arquivos separados, caso precise dela mais tarde. Isso ocorre porque a identificação da extensão é baseada no arquivo dela. Portanto, mover a extensão para outro arquivo posteriormente altera sua API pública e é uma alteração incompatível com versões anteriores para seus usuários.

Especificar o sistema operacional e a arquitetura

Se a extensão depender do sistema operacional ou do tipo de arquitetura, indique isso na definição da extensão usando os atributos booleanos os_dependent e arch_dependent. Isso garante que o Bazel reconheça a necessidade de reavaliação se houver alterações em qualquer uma delas.

Somente o módulo raiz afeta diretamente os nomes dos repositórios

Lembre-se de que, quando uma extensão cria repositórios, eles são criados dentro do namespace da extensão. Isso significa que podem ocorrer colisões se módulos diferentes usarem a mesma extensão e acabarem criando um repositório com o mesmo nome. Isso geralmente se manifesta como o tag_class de uma extensão de módulo com um argumento name que é transmitido como o valor name de uma regra de repositório.

Por exemplo, digamos que o módulo raiz, A, dependa do módulo B. Os dois módulos dependem do módulo mylang. Se A e B chamarem mylang.toolchain(name="foo"), ambos tentarão criar um repositório chamado foo no módulo mylang e ocorrerá um erro.

Para evitar isso, remova a capacidade de definir o nome do repositório diretamente ou permita que apenas o módulo raiz faça isso. Você pode permitir essa capacidade ao módulo raiz, porque nada dependerá dele. Portanto, ele não precisa se preocupar com outro módulo criando um nome conflitante.