Extensões de módulo

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

É possível definir extensões de módulo em arquivos .bzl, assim como regras de repositório. Elas não são invocadas diretamente. Em vez disso, cada módulo especifica partes de dados chamadas tags para as extensões lerem. O Bazel executa a resolução de módulos 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 de extensões

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, em seguida, chame a função integrada use_extension para colocá-la no escopo. Considere o exemplo a seguir: um snippet de um MODULE.bazel arquivo para usar a extensão "maven" definida no rules_jvm_external módulo:

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 correspondidas especificadas na definição da extensão. Confira um exemplo que especifica 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 corretamente 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, enquanto as regras de repositório têm vários atributos, as extensões de módulo têm tag_classes, cada uma das quais tem vários atributos. As classes de tag definem esquemas para tags usadas por essa extensão. Por exemplo, a extensão "maven" acima pode ser definida desta forma:

# @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 atributo especificado.

A função de implementação de extensões de módulo é semelhante à das regras de repositório, exceto que elas recebem um module_ctx objeto, que concede acesso a todos os módulos que usam a extensão e todas as tags relevantes. A função de implementação chama regras de repositório 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 de 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")

A reexportação de uma extensão de um arquivo .bzl diferente confere a ela uma nova identidade. Se as duas versões da extensão forem usadas no gráfico de módulos transitivos, elas serão avaliadas separadamente e só vão mostrar as tags associadas a essa identidade específica.

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

Nomes e visibilidade do repositório

Os repositórios gerados por extensões têm nomes canônicos no formato module_repo_canonical_name+extension_name+repo_name. O formato de nome canônico não é uma API da qual você deve 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 de repositório". Duas extensões distintas podem definir um repositório com o mesmo nome sem correr o risco de conflitos. Isso também significa que repository_ctx.name informa o nome canônico do repositório, que não é o mesmo nome especificado na chamada da regra do repositório.

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

  • Um repositório de módulo do Bazel pode mostrar 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 mostrar 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 repositório como nomes aparentes).
    • Isso pode resultar em um conflito. Se o repositório do módulo puder mostrar 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 refere ao primeiro.
  • Da mesma forma, na função de implementação de uma extensão de módulo, os repositórios criados pela extensão podem se referir uns aos outros pelos nomes aparentes nos atributos, independentemente da ordem em que são criados.
    • Em caso de conflito com um repositório visível para o módulo, os rótulos transmitidos aos atributos da regra do repositório podem ser encapsulados em uma chamada para Label para garantir que eles se refiram ao repositório visível para o módulo em vez do repositório gerado pela extensão do mesmo nome.

Substituir e injetar repositórios de extensão de módulo

O módulo raiz pode usar override_repo e inject_repo para substituir ou injetar repositórios de extensão de módulo.

Exemplo: substituir java_tools de rules_java por uma cópia fornecida

# MODULE.bazel
local_repository = use_repo_rule("@bazel_tools//tools/build_defs/repo:local.bzl", "local_repository")
local_repository(
  name = "my_java_tools",
  path = "vendor/java_tools",
)

bazel_dep(name = "rules_java", version = "7.11.1")
java_toolchains = use_extension("@rules_java//java:extension.bzl", "toolchains")

override_repo(java_toolchains, remote_java_tools = "my_java_tools")

Exemplo: corrigir uma dependência do Go para depender de @zlib em vez do zlib do sistema

# MODULE.bazel
bazel_dep(name = "gazelle", version = "0.38.0")
bazel_dep(name = "zlib", version = "1.3.1.bcr.3")

go_deps = use_extension("@gazelle//:extensions.bzl", "go_deps")
go_deps.from_file(go_mod = "//:go.mod")
go_deps.module_override(
  patches = [
    "//patches:my_module_zlib.patch",
  ],
  path = "example.com/my_module",
)
use_repo(go_deps, ...)

inject_repo(go_deps, "zlib")
# patches/my_module_zlib.patch
--- a/BUILD.bazel
+++ b/BUILD.bazel
@@ -1,6 +1,6 @@
 go_binary(
     name = "my_module",
     importpath = "example.com/my_module",
     srcs = ["my_module.go"],
-    copts = ["-lz"],
+    cdeps = ["@zlib"],
 )

Práticas recomendadas

Esta seção descreve as práticas recomendadas ao escrever extensões para que elas sejam simples de usar, fáceis de manter e se adaptem 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, uma extensão pode carregar 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 identidade da extensão é baseada no arquivo dela. Portanto, mover a extensão para outro arquivo mais tarde muda sua API pública e é uma mudança incompatível com versões anteriores para seus usuários.

Especificar a capacidade de reprodução e usar fatos

Se a extensão sempre definir os mesmos repositórios com as mesmas entradas (tags de extensão, arquivos lidos etc.) e, em particular, não depender de nenhum download que não seja protegido por uma soma de verificação, considere retornar extension_metadata com reproducible = True. Isso permite que o Bazel pule essa extensão ao gravar no arquivo de bloqueio MODULE.bazel, o que ajuda a manter o arquivo de bloqueio pequeno e reduz a chance de conflitos de mesclagem. O Bazel ainda armazena em cache os resultados de extensões reproduzíveis de uma maneira que persiste nas reinicializações do servidor. Portanto, mesmo uma extensão de longa duração pode ser marcada como reproduzível sem uma penalidade de desempenho.

Se a extensão depender de dados efetivamente imutáveis obtidos de fora do build, mais comumente da rede, mas você não tiver uma soma de verificação disponível para proteger o download, considere usar o facts parâmetro de extension_metadata para registrar esses dados de forma persistente e, assim, permitir que a extensão se torne reproduzível. facts precisa ser um dicionário com chaves de string e valores Starlark arbitrários semelhantes a JSON que sempre são mantidos no arquivo de bloqueio e disponíveis para avaliações futuras da extensão pelo facts campo de module_ctx.

facts não são invalidados mesmo quando o código da extensão do módulo muda. Portanto, prepare-se para lidar com o caso em que a estrutura de facts muda. O Bazel também pressupõe que dois dicionários facts diferentes produzidos por duas avaliações diferentes da mesma extensão podem ser mesclados superficialmente (ou seja, como se usando o operador | em dois dicionários). Isso é parcialmente aplicado por module_ctx.facts que não oferece suporte à enumeração das entradas, apenas pesquisas por chave.

Um exemplo de uso de facts seria registrar um mapeamento de números de versão de algum SDK para um objeto que contém o URL de download e a soma de verificação dessa versão. Na primeira vez que a extensão é avaliada, ela pode buscar esse mapeamento da rede, mas em avaliações posteriores, ela pode usar o mapeamento de facts para evitar as solicitações de rede.

Especificar a dependência do sistema operacional e da 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 mudanças em qualquer um deles.

Como esse tipo de dependência do host dificulta a manutenção da entrada do arquivo de bloqueio para essa extensão, considere marcar a extensão como reproduzível, se possível.

Somente o módulo raiz pode afetar diretamente os nomes do repositório

Quando uma extensão cria repositórios, eles são criados no namespace da extensão. Isso significa que colisões podem ocorrer se módulos diferentes usarem a mesma extensão e acabarem criando um repositório com o mesmo nome. Isso geralmente se manifesta como uma tag_class de 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"), os dois vão tentar criar um repositório chamado foo no módulo mylang, e um erro vai ocorrer.

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. Não há problema em permitir que o módulo raiz tenha essa capacidade, porque nada vai depender dele. Portanto, não é preciso se preocupar com outro módulo que crie um nome conflitante.