Módulos do Bazel

Informar um problema Acessar a origem

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

Um módulo precisa ter um arquivo MODULE.bazel na raiz do repositório (ao lado do arquivo WORKSPACE). Esse arquivo é o manifesto do módulo, declarando o nome, a versão, a lista de dependências diretas e outras informações. Veja 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")

Veja a lista completa de diretivas disponíveis em arquivos MODULE.bazel.

Para executar a resolução do módulo, o Bazel começa lendo o arquivo MODULE.bazel do módulo raiz e solicita repetidamente o arquivo MODULE.bazel de qualquer dependência de um registro do Bazel até descobrir todo o gráfico de dependência.

Por padrão, o Bazel seleciona uma versão de cada módulo para usar. Ele representa cada módulo com um repositório e consulta o registro novamente para saber como definir cada um dos repositórios.

Formato da versão

O Bazel tem um ecossistema diversificado e os projetos usam vários esquemas de controle de versões. O SemVer é o mais conhecido, mas também há projetos em destaque que usam esquemas diferentes, como Abseil, com versões baseadas em data. Por exemplo, 20210324.2.

Por esse motivo, o Bzlmod adota uma versão mais flexível da especificação SemVer. As diferenças incluem:

  • O SemVer determina que a parte de "lançamento" da versão precisa consistir em três segmentos: MAJOR.MINOR.PATCH. No Bazel, esse requisito é reduzido para que qualquer número de segmentos seja permitido.
  • No SemVer, cada um dos segmentos na parte "release" deve ter apenas dígitos. No Bazel, isso é reduzido para permitir letras também, e a semântica de comparação corresponde aos "identificadores" na parte "pré-lançamento".
  • Além disso, a semântica de aumentos de versões principais, secundárias e de patch nã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, duas versões do SemVer a e b comparam a < b somente se o mesmo ocorrer quando elas forem comparadas com versões de módulo do Bazel.

Escolher a versão

Considere o problema de dependência diamante, um elemento básico no espaço de gerenciamento de dependências com controle de versões. Suponha que você tenha o gráfico de dependências:

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

Qual versão do D precisa ser usada? Para resolver essa questão, o Bzlmod usa o algoritmo de seleção mínima de versão (MVS, na sigla em inglês) introduzido no sistema de módulos Go. O MVS pressupõe que todas as novas versões de um módulo são compatíveis com versões anteriores e, portanto, escolhe a versão mais recente especificada por qualquer dependente (D 1.1 no nosso exemplo). Ela é chamada de "mínima" porque a D 1.1 é a versão mais antiga que pode atender aos nossos requisitos. Mesmo que haja uma versão D 1.2 ou mais recente, ela não é selecionada. Usar o MVS cria um processo de seleção de versão que é de alta fidelidade e reproduzível.

Versões iaqueadas

O registro pode declarar determinadas versões como yanked caso precisem ser evitadas (por exemplo, devido a vulnerabilidades de segurança). O Bazel gera um erro ao selecionar uma versão puxada de um módulo. Para corrigir esse erro, faça upgrade para uma versão mais recente não puxada ou use a sinalização --allow_yanked_versions para permitir explicitamente essa versão.

Nível de compatibilidade

Em Go, a suposição do MVS sobre compatibilidade com versões anteriores funciona porque trata as versões incompatíveis com versões anteriores de um módulo como um módulo separado. Em termos do 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ência resolvido. Isso é possível, por sua vez, codificando a versão principal no caminho do pacote em Go, para que não haja conflitos de tempo de compilação ou vinculação.

No entanto, o Bazel não oferece essas garantias. Portanto, ele precisa do número da "versão principal" para detectar versões incompatíveis com versões anteriores. Esse número é chamado de nível de compatibilidade e é especificado por cada versão do módulo na diretiva module(). Com essas informações, o Bazel pode gerar um erro ao detectar que versões do mesmo módulo com diferentes níveis de compatibilidade existem no gráfico de dependência resolvido.

Modifica

Especifique substituições no arquivo MODULE.bazel para alterar o comportamento da resolução do módulo do Bazel. Somente as substituições do módulo raiz entram em vigor. Se um módulo é usado como uma dependência, as substituições dele são ignoradas.

Cada substituição é especificada para um determinado nome de módulo, afetando todas as versões dele no gráfico de dependência. Embora apenas as substituições do módulo raiz entrem em vigor, elas podem ser para dependências transitivas de que o módulo raiz não depende diretamente.

Substituição de uma única versão

O single_version_override tem várias finalidades:

  • Com o atributo version, é possível fixar uma dependência em uma versão específica, independentemente de quais versões da dependência são solicitadas no gráfico de dependência.
  • Com o atributo registry, é possível forçar essa dependência a vir de um registro específico, em vez de seguir o processo normal de seleção de registro.
  • Com os atributos patch*, é possível especificar um conjunto de patches para aplicar ao módulo transferido por download.

Esses atributos são opcionais e podem ser combinados entre si.

Substituição de várias versões

Um multiple_version_override pode ser especificado para permitir que várias versões do mesmo módulo coexistam no gráfico de dependência resolvido.

É possível especificar uma lista explícita de versões permitidas para o módulo, que precisam estar presentes no gráfico de dependência antes da resolução. É necessário que exista alguma dependência transitiva dependendo de cada versão permitida. Após a resolução, apenas as versões permitidas do módulo permanecem, enquanto o Bazel faz upgrade de outras versões do módulo para a versão mais alta permitida mais próxima no mesmo nível de compatibilidade. Se não houver uma versão mais recente permitida com o mesmo nível de compatibilidade, o Bazel vai gerar um erro.

Por exemplo, se as versões 1.1, 1.3, 1.5, 1.7 e 2.0 existirem no gráfico de dependência antes da resolução e a versão principal for o nível de compatibilidade:

  • Uma substituição de várias versões que permite 1.3, 1.7 e 2.0 resulta no upgrade de 1.1 para 1.3, de 1.5 no upgrade para 1.7 e outras versões permanecem as mesmas.
  • Uma substituição de várias versões que permite 1.5 e 2.0 resulta em um erro, já que 1.7 não tem uma versão mais recente no mesmo nível de compatibilidade para fazer upgrade.
  • Uma substituição de várias versões que permite 1.9 e 2.0 resulta em erro, já que 1.9 não está presente no gráfico de dependências antes da resolução.

Além disso, os usuários também podem substituir o registro usando o atributo registry, de maneira semelhante às substituições de versão única.

Substituições que não são de registro

As substituições que não são do registro removem completamente um módulo da resolução da versão. O Bazel não solicita esses arquivos MODULE.bazel de um registro, mas do próprio repositório.

O Bazel é compatível com as seguintes substituições não relacionadas ao registro:

Nomes de repositório e dependências restritas

O nome canônico de um repositório que apoia um módulo é module_name~version (por exemplo, bazel_skylib~1.0.3). Para módulos com uma substituição que não é de registro, substitua a parte version pela string override. Observe que o formato de nome canônico não é uma API de que você precisa e está sujeito a mudanças a qualquer momento.

O nome aparente de um repositório que faz backup de um módulo para os dependentes diretos é o nome do módulo, a menos que o atributo repo_name da diretiva bazel_dep dire o contrário. Observe que isso significa que um módulo só pode encontrar as próprias dependências diretas. Isso ajuda a evitar interrupções acidentais devido a mudanças em dependências transitivas.

As extensões de módulo também podem introduzir repositórios adicionais no escopo visível de um módulo.