Gerenciar dependências externas com o Bzlmod

Informar um problema Mostrar fonte Por noite · 7,3 · 7,2 · 7,1 · 7,0 · 6,5

Bzlmod é o codinome do novo sistema de dependência externa. introduzido no Bazel 5.0. Ele foi introduzido para resolver vários aspectos problemáticos da sistema antigo que não podia ser corrigido de forma incremental, consulte Seção "Definição do problema" do documento de design original para mais detalhes.

No Bazel 5.0, o Bzlmod não está ativado por padrão. a bandeira --experimental_enable_bzlmod precisa ser especificado para que o seguinte aconteça efeito Como o nome da sinalização sugere, no momento, esse recurso é experimental. As APIs e os comportamentos podem mudar até o lançamento oficial do recurso.

Para migrar seu projeto para o Bzlmod, siga o Guia de migração do Bzlmod. Você também pode encontrar exemplos de usos do Bzlmod no repositório examples.

Módulos do Bazel

O antigo sistema de dependência externa baseado em WORKSPACE é centralizado na repositories (ou repos), criados por meio de regras de repositório (ou regras de repositório). Embora os repositórios ainda sejam um conceito importante no novo sistema, os módulos são a unidades centrais de dependência.

Um módulo é essencialmente um projeto do Bazel que pode ter várias versões, cada uma que publica metadados sobre outros módulos de que depende. Isso é análogo a conceitos conhecidos em outros sistemas de gerenciamento de dependências: um artifact, um pacote do npm, uma caixa do Cargo, um módulo Go etc.

Um módulo simplesmente especifica as próprias dependências usando pares name e version. em vez de URLs específicos em WORKSPACE. Em seguida, as dependências são pesquisadas Um registro do Bazel por padrão, os Bazel Central Registry (em inglês). No seu espaço de trabalho, cada é transformado em um repositório.

MODULE.bazel

Cada versão de cada módulo tem um arquivo MODULE.bazel que declara dependências e outros metadados. Este é um exemplo básico:

module(
    name = "my-module",
    version = "1.0",
)

bazel_dep(name = "rules_cc", version = "0.0.1")
bazel_dep(name = "protobuf", version = "3.19.0")

O arquivo MODULE.bazel precisa estar localizado na raiz do diretório do espaço de trabalho. (ao lado do arquivo WORKSPACE). Diferentemente do arquivo WORKSPACE, não é necessário para especificar suas dependências transitivas. você só deve especificar dependências diretas, e os arquivos MODULE.bazel das dependências são para descobrir dependências transitivas automaticamente.

O arquivo MODULE.bazel é semelhante aos arquivos BUILD, porque não tem suporte forma de fluxo de controle, também proíbe instruções load. As diretivas Os arquivos MODULE.bazel compatíveis são:

Formato da versão

O Bazel tem um ecossistema diverso, e os projetos usam vários esquemas de controle de versões. A o mais popular é o SemVer, mas há também projetos de destaque usando diferentes esquemas, Abseil, cuja são baseadas em data, por exemplo, 20210324.2).

Por esse motivo, o Bzlmod adota uma versão mais descontraída das especificações do SemVer. A as diferenças incluem:

  • O SemVer determina que a "versão" da versão deve consistir em 3 segmentos: MAJOR.MINOR.PATCH. No Bazel, esse requisito é atenuado para que que qualquer número de segmentos é permitido.
  • No SemVer, cada um dos segmentos do "lançamento" deve ter apenas dígitos. No Bazel, isso é flexibilizado para permitir letras também, e a comparação a semântica corresponde aos "identifiers" no "pré-lançamento", parte.
  • Além disso, a semântica dos aumentos principais, secundários e de versão de patch são é aplicada. No entanto, consulte o nível de compatibilidade para detalhes sobre como indicamos a compatibilidade com versões anteriores.

Qualquer versão válida do SemVer é uma versão válida do módulo do Bazel. Além disso, dois As versões a e b do SemVer comparam a < b se as mesmas forem mantidas quando forem em comparação às versões do módulo do Bazel.

Resolução da versão

O problema da dependência losango é um item básico da dependência com controle de versões. de gerenciamento de projetos. Suponha que você tenha o seguinte gráfico de dependência:

       A 1.0
      /     \
   B 1.0    C 1.1
     |        |
   D 1.0    D 1.1

Qual versão de D deve ser usada? Para resolver essa questão, o Bzlmod usa o Seleção de versão mínima (MVS) introduzido no sistema de módulos Go. O MVS presume que todos os novos de um módulo são compatíveis com versões anteriores e, assim, escolhe a especificada por qualquer dependente (D 1.1 no nosso exemplo). É chamado de "mínimo" porque, aqui, o D 1.1 é a versão mínima que pode atender aos nossos requisitos. mesmo que haja D 1.2 ou mais recente, não os selecionamos. Isso tem o benefício extra que a seleção da versão seja de alta fidelidade e reprodutível.

A resolução de versão é realizada localmente na sua máquina, não pelo registro.

Nível de compatibilidade

A suposição do MVS sobre a compatibilidade com versões anteriores é viável porque trata as versões incompatíveis com versões anteriores de um módulo como um módulo separado. Em relação ao SemVer, isso significa que A 1.x e A 2.x são considerados módulos distintos, e podem coexistir no gráfico de dependências resolvida. Isso é, por sua vez, feito possível porque a versão principal está codificada no caminho do pacote no Vá para que não haja conflitos de tempo de compilação ou vinculação.

No Bazel, não temos essas garantias. Portanto, precisamos de uma maneira de denotar a "maioria versão" para detectar versões incompatíveis com versões anteriores. Este número é chamada de nível de compatibilidade e é especificada por cada versão de módulo em à diretiva module(). Com essas informações em mãos, podemos lançar um erro quando detectamos que as versões do mesmo módulo com compatibilidade existem no gráfico de dependências resolvidas.

Nomes de repositório

No Bazel, cada dependência externa tem um nome de repositório. Às vezes, o mesmo pode ser usada com diferentes nomes de repositório (por exemplo, ambos Média de @io_bazel_skylib e @bazel_skylib Bazel skylib) ou a mesma de repositório de dados pode ser usado para dependências diferentes em projetos distintos.

No Bzlmod, os repositórios podem ser gerados por módulos do Bazel e extensões de módulo. Para resolver conflitos de nome de repositório, estamos adotando o mapeamento de repositório no novo sistema. Aqui estão dois conceitos importantes:

  • Nome do repositório canônico: o nome do repositório globalmente exclusivo para cada repositório de dados. Esse será o nome do diretório em que o repositório está.
    Ele é construído da seguinte forma. Aviso: o formato de nome canônico é não é uma API de que você deveria depender, ela está sujeita a alterações a qualquer momento):

    • Para repositórios do módulo do Bazel: module_name~version
      (Exemplo: @bazel_skylib~1.0.3)
    • Para repositórios de extensão de módulo: module_name~version~extension_name~repo_name
      (Exemplo: @rules_cc~0.0.1~cc_configure~local_config_cc)
  • Nome do repositório aparente: o nome do repositório que será usado nas APIs BUILD e .bzl em um repositório. A mesma dependência pode ter valores aparentes diferentes em repositórios diferentes.
    É determinado da seguinte maneira:

    • Para repositórios do módulo do Bazel: module_name por padrão ou o nome especificado pelo atributo repo_name no bazel_dep.
    • Para repositórios de extensão de módulo: nome do repositório introduzido por meio de use_repo

Cada repositório tem um dicionário de mapeamento de repositório de suas dependências diretas, que é um mapa do nome do repositório aparente para o nome do repositório canônico. Usamos o mapeamento do repositório para resolver o nome do repositório ao construir um rótulo. Observe que não há conflito entre os nomes dos repositórios canônicos e os os usos de nomes de repositórios aparentes podem ser descobertos analisando o MODULE.bazel arquivo, portanto os conflitos podem ser facilmente detectados e resolvidos sem afetar outras dependências.

Dependências estritas

O novo formato de especificação de dependência nos permite realizar verificações mais rigorosas. Em particular, agora vamos impor que um módulo só pode usar repositórios criados a partir dependências diretas. Isso ajuda a evitar falhas acidentais e difíceis de depurar quando algo no gráfico de dependências transitivas muda.

As dependências estritas são implementadas com base mapeamento do repositório. Basicamente, a o mapeamento do repositório de cada repo contém todas as suas dependências diretas, quaisquer outro repositório não fica visível. As dependências visíveis para cada repositório são determinado da seguinte forma:

  • Um repositório de módulo do Bazel pode conferir todos os repositórios introduzidos no arquivo MODULE.bazel. via bazel_dep e use_repo
  • Um repositório de extensão de módulos pode conferir todas as dependências visíveis do módulo que fornece a extensão, além de todos os outros repositórios gerados pelo mesmo módulo. .

Registros

O Bzlmod descobre dependências solicitando as informações dele ao Bazel registros. Um registro do Bazel é simplesmente um banco de dados de módulos do Bazel. O único com suporte é um registro de índice, que é um diretório local ou um servidor HTTP estático seguindo um formato específico. Na no futuro, planejamos adicionar suporte para registros de módulo único, que são simplesmente Repositórios Git que contêm a origem e o histórico de um projeto.

Registro do índice

Um registro de índice é um diretório local ou um servidor HTTP estático que contém informações sobre uma lista de módulos, incluindo sua página inicial, mantenedores, o MODULE.bazel de cada versão e como buscar a origem de cada uma para a versão anterior. Ele não precisa disponibilizar os arquivos de origem em si.

Um registro de índice precisa seguir o formato abaixo:

  • /bazel_registry.json: um arquivo JSON contendo metadados do registro, como:
    • mirrors, especificando a lista de espelhos a serem usados para arquivos de origem.
    • module_base_path, especificando o caminho base para módulos com local_repository no arquivo source.json.
  • /modules: um diretório que contém um subdiretório para cada módulo no de registros.
  • /modules/$MODULE: um diretório contendo um subdiretório para cada versão deste módulo, além do seguinte arquivo:
    • metadata.json: um arquivo JSON contendo informações sobre o módulo. pelos seguintes campos:
      • homepage: o URL da página inicial do projeto.
      • maintainers: uma lista de objetos JSON, cada um correspondendo ao as informações de um administrador do módulo no registro. Observe que não é necessariamente o mesmo que os autores da projeto.
      • versions: uma lista de todas as versões deste módulo que podem ser encontradas em neste registro.
      • yanked_versions: uma lista de versões ianizadas deste módulo. Isso atualmente é um ambiente autônomo, mas, no futuro, versões puxadas serão pulou ou produziu um erro.
  • /modules/$MODULE/$VERSION: um diretório que contém os seguintes arquivos:
    • MODULE.bazel: o arquivo MODULE.bazel dessa versão do módulo.
    • source.json: um arquivo JSON contendo informações sobre como buscar o origem dessa versão do módulo.
      • O tipo padrão é "arquivo" pelos seguintes campos:
        • url: o URL do arquivo de origem.
        • integrity: o Integridade de sub-recursos checksum do arquivo.
        • strip_prefix: um prefixo de diretório a ser removido ao extrair o arquivo de origem.
        • patches: uma lista de strings, e cada uma nomeia um arquivo de patch para aplicar ao arquivo extraído. Os arquivos de patch estão localizados em diretório /modules/$MODULE/$VERSION/patches.
        • patch_strip: igual ao argumento --strip do patch do Unix.
      • O tipo pode ser alterado para usar um caminho local com estes campos:
        • type: local_path
        • path: o caminho local para o repo, calculado da seguinte maneira:
          • Se o caminho for absoluto, será usado do jeito que está.
          • Se o caminho for relativo e module_base_path for absoluto, o caminho está resolvido para <module_base_path>/<path>
          • Se o caminho e module_base_path forem ambos caminhos relativos, o caminho será resolvido para <registry_path>/<module_base_path>/<path>. O registro precisa ser hospedado localmente e usado por --registry=file://<registry_path>. Caso contrário, o Bazel vai gerar um erro.
    • patches/: um diretório opcional que contém arquivos de patch, usado somente quando source.json tem "archive". não é válido.

Registro central do Bazel

O Bazel Central Registry (BCR) é um registro de índice localizado em bcr.bazel.build. Seu conteúdo têm o suporte do repositório do GitHub bazelbuild/bazel-central-registry

O BCR é mantido pela comunidade do Bazel. os colaboradores podem enviar solicitações de envio. Consulte Políticas e procedimentos de registro central do Bazel.

Além de seguir o formato de um registro de índice normal, o BCR exige um arquivo presubmit.yml para cada versão do módulo /modules/$MODULE/$VERSION/presubmit.yml). Esse arquivo especifica alguns elementos destinos de build e teste que podem ser usados para verificar a integridade do módulo e é usada pelos pipelines de CI do BCR para garantir a interoperabilidade entre os módulos no BCR.

Seleção de registros

A flag repetível --registry do Bazel pode ser usada para especificar a lista de registros dos quais solicitar módulos. Assim, você pode configurar seu projeto para buscar dependências de um registro interno ou de terceiros. Registros anteriores levam precedência. Por conveniência, você pode colocar uma lista de sinalizações --registry no .bazelrc do seu projeto.

Extensões do módulo

As extensões de módulo permitem ampliar o sistema de módulos lendo dados de entrada. dos módulos em todo o gráfico de dependências, executando a lógica necessária para resolver dependências e, por fim, criar repositórios chamando regras de repo. Elas são semelhantes função às macros WORKSPACE atuais, mas são mais adequadas ao mundo módulos e dependências transitivas.

As extensões do módulo são definidas em arquivos .bzl, assim como as regras de repositório ou WORKSPACE macros. Elas não são invocadas diretamente. Em vez disso, cada módulo pode especificar partes de dados chamadas tags para que as extensões leiam. Depois do módulo, a resolução da versão seja concluída, as extensões do módulo serão executadas. Cada extensão é executada uma vez após a resolução do módulo (ainda antes de qualquer compilação realmente acontecer) e consegue ler todas as tags pertencentes a ele em todo o gráfico de dependências.

          [ A 1.1                ]
          [   * maven.dep(X 2.1) ]
          [   * maven.pom(...)   ]
              /              \
   bazel_dep /                \ bazel_dep
            /                  \
[ B 1.2                ]     [ C 1.0                ]
[   * maven.dep(X 1.2) ]     [   * maven.dep(X 2.1) ]
[   * maven.dep(Y 1.3) ]     [   * cargo.dep(P 1.1) ]
            \                  /
   bazel_dep \                / bazel_dep
              \              /
          [ D 1.4                ]
          [   * maven.dep(Z 1.4) ]
          [   * cargo.dep(Q 1.1) ]

No exemplo de gráfico de dependência acima, A 1.1, B 1.2 etc. são módulos do Bazel. Pense em cada um como um arquivo MODULE.bazel. Cada módulo pode especificar para extensões de módulo. alguns são especificados para a extensão "maven", e outros são especificados para "cargo". Quando esse gráfico de dependência é finalizado (por exemplo, talvez o B 1.2 realmente tenha um bazel_dep em D 1.3, mas foi atualizado para D 1.4 devido a C), a extensão "maven" é executado e lê todos os Tags maven.*, usando as informações contidas neles para decidir quais repositórios criar. Da mesma forma, para a tag "cargo", .

Uso da extensão

As extensões são hospedadas nos próprios módulos do Bazel. Portanto, para usar uma extensão em seu módulo, primeiro você precisa adicionar um bazel_dep a esse módulo e, em seguida, chamar o use_extension integrado para colocá-la no escopo. Considere o exemplo a seguir, um snippet um arquivo MODULE.bazel para usar um "maven" hipotético extensão definida no Módulo rules_jvm_external:

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

Depois de colocar a extensão no escopo, você poderá usar a sintaxe de ponto para especificar tags para ele. As tags precisam seguir o esquema definido pelo classes de tag correspondentes (consulte a definição de extensão abaixo). Veja um exemplo que especifica algumas tags maven.dep e maven.pom.

maven.dep(coord="org.junit:junit:3.0")
maven.dep(coord="com.google.guava:guava:1.2")
maven.pom(pom_xml="//:pom.xml")

Se a extensão gerar repositórios que você quer usar no seu módulo, use o Diretiva use_repo a ser declarada para resolvê-los com rapidez. Isso serve para atender à condição de dependências rígidas e evitar o nome do repositório local conflitos.

use_repo(
    maven,
    "org_junit_junit",
    guava="com_google_guava_guava",
)

Os repositórios gerados por uma extensão fazem parte da API dela. Portanto, a partir das tags que você especificado, o "maven" gera uma "org_junit_junit" e outro chamado "com_google_guava_guava". Com use_repo, como alternativa, é possível renomeá-los no escopo do módulo, como em "guava" aqui.

Definição de extensão

As extensões do módulo são definidas de forma semelhante às regras do repositório, usando a função module_extension. Ambos têm uma função de implementação; mas, embora as regras de repositório tenham uma série de atributos, as extensões de módulo têm várias tag_classes, cada um com um número de atributos. As classes de tag definem os esquemas das tags usadas por este . Continuando com nosso exemplo do "maven" hipotético acima:

# @rules_jvm_external//:extensions.bzl
maven_dep = tag_class(attrs = {"coord": attr.string()})
maven_pom = tag_class(attrs = {"pom_xml": attr.label()})
maven = module_extension(
    implementation=_maven_impl,
    tag_classes={"dep": maven_dep, "pom": maven_pom},
)

Essas declarações deixam claro que as tags maven.dep e maven.pom podem ser especificado, usando o esquema de atributos definido acima.

A função de implementação é semelhante a uma macro WORKSPACE, só que recebe um objeto module_ctx, que concede o acesso ao gráfico de dependências e a todas as tags pertinentes. A implementação precisa chamar as regras de repo para gerar repos:

# @rules_jvm_external//:extensions.bzl
load("//:repo_rules.bzl", "maven_single_jar")
def _maven_impl(ctx):
  coords = []
  for mod in ctx.modules:
    coords += [dep.coord for dep in mod.tags.dep]
  output = ctx.execute(["coursier", "resolve", coords])  # hypothetical call
  repo_attrs = process_coursier(output)
  [maven_single_jar(**attrs) for attrs in repo_attrs]

No exemplo acima, analisamos todos os módulos no gráfico de dependências. (ctx.modules), cada uma sendo uma Objeto bazel_module com um campo tags expõe todas as tags maven.* no módulo. Em seguida, invocamos o utilitário CLI Coursier para entrar em contato com o Maven e executar a resolução. Por fim, usamos a resolução resultado para criar vários repositórios, usando o maven_single_jar hipotético regra repo.