Módulos do Bazel

Um módulo do Bazel é um projeto do Bazel que pode ter várias versões, cada uma delas publicando 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. Confira 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")

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

Por padrão, o Bazel então seleciona uma versão de cada módulo a ser usada. O Bazel 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 mais popular é o SemVer, mas também há projetos importantes que usam esquemas diferentes, como o Abseil, cujas versões são baseadas em datas, 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 prescreve que a parte "lançamento" da versão precisa consistir em três segmentos: MAJOR.MINOR.PATCH. No Bazel, esse requisito é flexibilizado para que qualquer número de segmentos seja permitido.
  • No SemVer, cada um dos segmentos na parte "lançamento" precisa ser apenas dígitos. No Bazel, isso é flexibilizado 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 das versões principal, secundária e de patch não é aplicada. No entanto, consulte o nível de compatibilidade para mais detalhes sobre como denotamos a compatibilidade com versões anteriores.

Qualquer versão SemVer válida é uma versão de módulo do Bazel válida. Além disso, duas versões SemVer a e b comparam a < b se e somente se o mesmo for válido quando comparadas como versões de módulo do Bazel.

Seleção da versão

Considere o problema de dependência de diamante, um elemento básico no espaço de gerenciamento de dependências com versões. Suponha que você tenha o 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 algoritmo de seleção de versão mínima (MVS, na sigla em inglês) introduzido no sistema de módulos do Go. O MVS pressupõe que todas as novas versões de um módulo sejam compatíveis com versões anteriores e, portanto, escolhe a versão mais alta especificada por qualquer dependente (D 1.1 no nosso exemplo). Ele é chamado de "mínimo" porque D 1.1 é a versão mais antiga que pode atender aos nossos requisitos. Mesmo que D 1.2 ou mais recente exista, não a selecionamos. O uso do MVS cria um processo de seleção de versão de alta fidelidade e reproduzível.

Versões retiradas

O registro pode declarar determinadas versões como retiradas se elas precisarem ser evitadas (como vulnerabilidades de segurança). O Bazel gera um erro ao selecionar uma versão retirada de um módulo. Para corrigir esse erro, faça upgrade para uma versão mais recente, não retirada ou use a --allow_yanked_versions flag para permitir explicitamente a versão retirada.

Nível de compatibilidade

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

No entanto, o Bazel não pode fornecer 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 module() diretiva. Com essas informações, o Bazel pode gerar um erro quando detecta que versões do mesmo módulo com níveis de compatibilidade diferentes existem no gráfico de dependência resolvido.

Modificações

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

Cada modificação é especificada para um determinado nome de módulo, afetando todas as versões dele no gráfico de dependência. Embora apenas as modificaçõ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.

Modificação de versão única

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 sejam 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 a serem aplicados ao módulo transferido por download.

Todos esses atributos são opcionais e podem ser combinados.

Modificação de várias versões

Uma multiple_version_override pode ser especificada 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 haja 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 permitida mais próxima no mesmo nível de compatibilidade. Se não houver uma versão permitida mais alta no 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 modificação de várias versões que permite 1.3, 1.7 e 2.0 resulta no upgrade de 1.1 para 1.3, 1.5 para 1.7 e outras versões permanecem as mesmas.
  • Uma modificaçã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 alta no mesmo nível de compatibilidade para fazer upgrade.
  • Uma modificação de várias versões que permite 1.9 e 2.0 resulta em um erro, já que 1.9 não está presente no gráfico de dependência antes da resolução.

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

Modificações não registradas

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

O Bazel oferece suporte às seguintes modificações não registradas:

Nomes de repositório e dependências estritas

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

O nome aparente de um repositório que faz backup de um módulo para seus dependentes diretos é o nome do módulo, a menos que o repo_name atributo da bazel_dep diretiva diga o contrário. Isso significa que um módulo só pode encontrar as 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 outros repositórios no escopo visível de um módulo.