Este documento é uma descrição da base de código e de como o Bazel é estruturado. Ela destina-se a pessoas dispostas a contribuir com o Bazel, não a usuários finais.
Introdução
A base de código do Bazel é grande (cerca de 350 KLOC de código de produção e 260 KLOC de teste). e ninguém está familiarizado com o cenário inteiro: todos sabem vale muito bem, mas poucos sabem o que há sobre as colinas em cada direção
Para que as pessoas no meio da jornada não se encontrem em um floresta escurecida com o caminho simples sendo perdido, este documento tenta fornecer uma visão geral da base de código para que seja mais fácil começar a usar trabalhando nisso.
A versão pública do código-fonte do Bazel fica no GitHub em github.com/bazelbuild/bazel (link em inglês) Isso não é a "fonte da verdade"; são derivados de uma árvore de origem interna do Google que contenha funcionalidades adicionais que não sejam úteis fora do Google. A o objetivo de longo prazo é fazer do GitHub a fonte da verdade.
As contribuições são aceitas pelo mecanismo normal de solicitação de envio do GitHub. e importadas manualmente por um Googler para a árvore de origem interna e, em seguida, e reexportado para o GitHub.
Arquitetura de cliente/servidor
A maior parte do Bazel reside em um processo de servidor que permanece na RAM entre os builds. Isso permite que o Bazel mantenha o estado entre builds.
É por isso que a linha de comando do Bazel tem dois tipos de opções: inicialização e kubectl. Em uma linha de comando como esta:
bazel --host_jvm_args=-Xmx8G build -c opt //foo:bar
Algumas opções (--host_jvm_args=
) estão antes do nome do comando a ser executado
e outras depois de (-c opt
). o tipo anterior é chamado de "opção de inicialização" e
afeta o processo do servidor como um todo, enquanto o último tipo, o "comando
afeta apenas um comando.
Cada instância de servidor tem uma única árvore de origem associada ("espaço de trabalho") e cada o espaço de trabalho geralmente tem uma única instância ativa do servidor. Isso pode ser contornado especificando uma base de saída personalizada. Consulte a seção "Layout do diretório" para informações).
O Bazel é distribuído como um único executável ELF que também é um arquivo ZIP válido.
Quando você digita bazel
, o executável ELF acima foi implementado em C++ (o
"cliente") tem o controle. Ele configura um processo de servidor apropriado usando o
etapas a seguir:
- Verifica se ele já foi extraído. Se não, ele faz isso. Isso é de onde vem a implementação do servidor.
- Verifica se há uma instância ativa do servidor que funciona (em execução,
tem as opções de inicialização corretas e usa o diretório do espaço de trabalho correto. Ela
encontra o servidor em execução conferindo o diretório
$OUTPUT_BASE/server
em que há um arquivo de bloqueio com a porta em que o servidor está escutando. - Se necessário, encerra o processo do servidor antigo
- Se necessário, inicia um novo processo de servidor
Depois que um processo de servidor adequado estiver pronto, o comando que precisa ser executado será
comunicada a ele por uma interface gRPC, a saída do Bazel é enviada de volta
ao terminal. Apenas um comando pode estar em execução ao mesmo tempo. Isso é
implementado usando um mecanismo de bloqueio elaborado com peças em C++ e peças
Java. Há uma infraestrutura para executar vários comandos em paralelo,
já que a incapacidade de executar bazel version
em paralelo com outro comando
é um pouco constrangedor. O principal obstáculo é o ciclo de vida de BlazeModule
s
e algum estado em BlazeRuntime
.
Ao final de um comando, o servidor do Bazel transmite o código de saída que o cliente
deve retornar. Uma diferença interessante é a implementação de bazel run
: o
trabalho desse comando é executar algo que o Bazel acabou de criar, mas isso não é possível.
do processo do servidor porque ele não tem um terminal. Em vez disso, ele informa
ao cliente qual binário deve ujexec()
e com quais argumentos.
Quando alguém pressiona Ctrl-C, o cliente a converte em uma chamada Cancelar no gRPC que tenta encerrar o comando o mais rápido possível. Após o terceiro Ctrl-C, o cliente envia um SIGKILL ao servidor.
O código-fonte do cliente está em src/main/cpp
e o protocolo usado para
para se comunicar com o servidor é em src/main/protobuf/command_server.proto
.
O ponto de entrada principal do servidor é BlazeRuntime.main()
, e as chamadas gRPC
do cliente são processados por GrpcServerImpl.run()
.
Layout do diretório
O Bazel cria um conjunto de diretórios um pouco complicado durante um build. Um está disponível no Layout do diretório de saída.
O "espaço de trabalho" é a árvore de origem em que o Bazel é executado. Geralmente, corresponde a algo que você conferiu no controle de origem.
Ele coloca todos os dados na "raiz do usuário de saída". Isso geralmente é
$HOME/.cache/bazel/_bazel_${USER}
, mas pode ser substituído usando o
Opção de inicialização --output_user_root
.
A "base instalada" é para onde o Bazel é extraído. Isso é feito automaticamente
e cada versão do Bazel recebe um subdiretório com base na soma de verificação dele na
base instalada. Ele é $OUTPUT_USER_ROOT/install
por padrão e pode ser alterado
usando a opção de linha de comando --install_base
.
A "base de saída" é o lugar em que a instância do Bazel é anexada a uma instância
espaço de trabalho grava. Cada base de saída tem no máximo uma instância do servidor Bazel
em execução a qualquer momento. Geralmente é às $OUTPUT_USER_ROOT/<checksum of the path
to the workspace>
. Ele pode ser mudado usando a opção de inicialização --output_base
.
o que é, entre outras coisas, útil para contornar a limitação que só
uma instância do Bazel pode ser executada em qualquer espaço de trabalho a qualquer momento.
O diretório de saída contém, entre outras coisas:
- Os repositórios externos buscados em
$OUTPUT_BASE/external
. - A raiz exec, um diretório que contém links simbólicos para todos os
o código do build atual. Ele fica neste endereço:
$OUTPUT_BASE/execroot
. Durante o build, o diretório de trabalho será$EXECROOT/<name of main repository>
. Planejamos mudar isso para$EXECROOT
, embora seja uma de um plano de longo prazo, porque é uma mudança incompatível. - Arquivos criados durante a criação.
Processo de execução de um comando
Depois que o servidor do Bazel recebe o controle e é informado sobre um comando, precisa executada, acontece a seguinte sequência de eventos:
BlazeCommandDispatcher
é informado sobre a nova solicitação. Ele decide se o comando precisa de um espaço de trabalho para ser executado (quase todos os comandos, exceto para aqueles que não têm nada a ver com código-fonte, como versão ou help) e se outro comando está em execução.O comando certo é encontrado. Cada comando precisa implementar a interface
BlazeCommand
e precisam ter a anotação@Command
, que é um pouco seria bom se todos os metadados necessários para um comando fossem descrito por métodos emBlazeCommand
)As opções da linha de comando são analisadas. Cada comando tem uma linha de comando diferente, , que são descritas na anotação
@Command
.Um barramento de eventos é criado. O barramento de eventos é um fluxo de eventos durante o build. Algumas delas são exportadas para fora do Bazel sob o comando o protocolo de eventos de build para informar ao mundo como o build vai
O comando recebe o controle. Os comandos mais interessantes são os que executam uma build: criar, testar, executar, cobertura e assim por diante: essa funcionalidade é implementado por
BuildTool
.O conjunto de padrões de segmentação na linha de comando é analisado e caracteres curinga, como
//pkg:all
e//pkg/...
foram resolvidos. Isso é implementadoAnalysisPhaseRunner.evaluateTargetPatterns()
e reificado no Skyframe comoTargetPatternPhaseValue
.A fase de carregamento/análise é executada para produzir o gráfico de ações (um método gráfico acíclico de comandos que precisam ser executados para o build).
A fase de execução é executada. Isso significa executar todas as ações necessárias criar os destinos de nível superior solicitados são executados.
Opções de linha de comando
As opções de linha de comando para uma invocação do Bazel são descritas em
Objeto OptionsParsingResult
, que, por sua vez, contém um mapa da "opção"
aulas" aos valores das opções. Uma "classe de opção" é uma subclasse de
OptionsBase
e agrupa opções de linha de comando relacionadas a cada uma delas.
entre si. Exemplo:
- Opções relacionadas a uma linguagem de programação (
CppOptions
ouJavaOptions
). Elas precisam ser uma subclasse daFragmentOptions
e são encapsuladas em um objetoBuildOptions
. - Opções relacionadas à forma como o Bazel executa ações (
ExecutionOptions
)
Essas opções são projetadas para serem consumidas na fase de análise e (seja
usando RuleContext.getFragment()
em Java ou ctx.fragments
em Starlark).
Alguns deles (por exemplo, se é necessário fazer verificação com C++ ou não) são lidos
na fase de execução, mas isso sempre exige explicação explícita,
BuildConfiguration
não está disponível então. Para mais informações, consulte a
seção "Configurações".
AVISO:gostamos de fingir que as instâncias de OptionsBase
são imutáveis e
usá-los dessa maneira (como parte de SkyKeys
). Esse não é o caso e
modificá-las é uma boa maneira de quebrar o Bazel de maneiras sutis e difíceis
para depurar. Infelizmente, torná-los imutáveis é um grande esforço.
Modificar um FragmentOptions
imediatamente após a construção antes de qualquer outra pessoa
tenha a chance de manter uma referência a ele e antes que equals()
ou hashCode()
sejam
chamado, não tem problema.
O Bazel aprende sobre as classes de opções das seguintes maneiras:
- Algumas estão conectadas ao Bazel (
CommonCommandOptions
). - Na anotação
@Command
em cada comando do Bazel - De
ConfiguredRuleClassProvider
(são opções de linha de comando relacionadas a linguagens de programação individuais) - As regras do Starlark também podem definir as próprias opções (consulte aqui)
Cada opção (excluindo as opções definidas por Starlark) é uma variável membro de
Subclasse FragmentOptions
que tem a anotação @Option
, que especifica
o nome e o tipo da opção de linha de comando, além de um texto de ajuda.
O tipo Java do valor de uma opção de linha de comando é geralmente algo simples
(uma string, um número inteiro, um booleano, um rótulo etc.). No entanto, também oferecemos suporte
opções de tipos mais complicados, neste caso, o trabalho de converter do
string de linha de comando para o tipo de dados é uma implementação do
com.google.devtools.common.options.Converter
:
A árvore de origem, conforme visto pelo Bazel
O Bazel cria software, o que acontece ao ler e a interpretação do código-fonte. A totalidade do código-fonte em que o Bazel opera é chamado de "espaço de trabalho", e é estruturado em repositórios, pacotes e regras de firewall.
Repositórios
Um "repositório" é uma árvore de origem na qual um desenvolvedor trabalha; geralmente representa um único projeto. O ancestral do Bazel, o Blaze, operava em um monorepo, Ou seja, uma única árvore de origem que contém todo o código-fonte usado para executar o build. O Bazel, por outro lado, dá suporte a projetos com código-fonte que abrange vários repositórios. O repositório de onde o Bazel é invocado é chamado de repositório", os outros são chamados de "repositórios externos".
Um repositório é marcado por um arquivo chamado WORKSPACE
(ou WORKSPACE.bazel
) em
diretório raiz. Esse arquivo contém informações que são "globais" ao todo
build, por exemplo, o conjunto de repositórios externos disponíveis. Funciona como um
arquivo Starlark normal, o que significa que é possível load()
outros arquivos Starlark.
Isso é comumente usado para extrair repositórios necessários para um repositório
explicitamente referenciado (chamamos isso de "padrão deps.bzl
")
O código de repositórios externos tem um link simbólico ou é baixado em
$OUTPUT_BASE/external
:
Ao executar o build, toda a árvore de origem precisa ser reunida. este
é feito por SymlinkForest
, que vincula simbolicamente todos os pacotes no repositório principal.
para $EXECROOT
e cada repositório externo para $EXECROOT/external
ou
$EXECROOT/..
(o primeiro, é claro, impossibilita ter um pacote
chamado external
no repositório principal. é por isso que estamos migrando
ela)
Pacotes
Cada repositório é composto por pacotes, uma coleção de arquivos relacionados e
uma especificação das dependências. Elas são especificadas por um arquivo chamado
BUILD
ou BUILD.bazel
. Se ambos existirem, o Bazel vai preferir BUILD.bazel
. o motivo
por que arquivos BUILD
ainda são aceitos é que o ancestral do Bazel, o Blaze, usava esse
nome do arquivo. No entanto, ele acabou sendo um segmento de caminho usado com frequência, especialmente
no Windows, que não diferenciam maiúsculas de minúsculas.
Os pacotes são independentes um do outro: mudanças no arquivo BUILD
de um pacote.
não podem causar alterações em outros pacotes. Adição ou remoção de arquivos BUILD
_can _alterar outros pacotes, já que globs recursivos param nos limites de pacotes
e, portanto, a presença de um arquivo BUILD
interrompe a recursão.
A avaliação de um arquivo BUILD
é chamada de "carregamento de pacote". Está implementado
na classe PackageFactory
, chama o intérprete de Starlark e
requer conhecimento do conjunto de classes de regras disponíveis. O resultado do pacote
carregando é um objeto Package
. É basicamente um mapa de uma string (o nome de um
destino) ao próprio destino.
Uma grande parte da complexidade durante o carregamento do pacote é globular: o Bazel não tem
exigem que cada arquivo de origem seja listado explicitamente e, em vez disso, pode executar globs
(como glob(["**/*.java"])
). Ao contrário do shell, ele aceita globs recursivos que
vão para subdiretórios, mas não para subdiretórios. Isso requer acesso a
no sistema de arquivos e, como isso pode demorar, implementamos todos os tipos de truques para
executá-lo em paralelo e com a maior eficiência possível.
O globbing é implementado nas seguintes classes:
LegacyGlobber
, um planeta rápido e alegre que não reconhece o SkyframeSkyframeHybridGlobber
, uma versão que usa o Skyframe e é revertida para no globber legado para evitar "reinicializações do Skyframe" (descrito abaixo)
A própria classe Package
contém alguns membros que são usados exclusivamente para
analisar o arquivo WORKSPACE e quais não fazem sentido para pacotes reais. Isso é
uma falha de design porque os objetos que descrevem pacotes regulares não podem conter
campos que descrevem outra coisa. São eles:
- Os mapeamentos do repositório
- Os conjuntos de ferramentas registrados
- As plataformas de execução registradas
O ideal é que haja uma separação maior entre a análise do arquivo WORKSPACE
analisar pacotes regulares para que Package
não precise suprir as necessidades
de ambos. Infelizmente, isso é difícil de fazer porque os dois estão interligados
profundamente.
Rótulos, destinos e regras
Os pacotes são compostos de destinos, que têm os seguintes tipos:
- Arquivos:itens que são a entrada ou a saída do build. Em No linguajar do Bazel, eles são chamados de artefatos (discutidos em outro lugar). Nem todas os arquivos criados durante o build são destinos. é comum que uma saída de O Bazel não precisa ter um rótulo associado.
- Regras:descrevem as etapas para derivar as saídas das entradas. Eles
geralmente estão associados a uma linguagem de programação (como
cc_library
,java_library
oupy_library
), mas há alguns que não dependem de idioma. (comogenrule
oufilegroup
) - Grupos de pacotes:discutidos na seção Visibilidade.
O nome de um destino é chamado de Rótulo. A sintaxe dos rótulos é
@repo//pac/kage:name
, em que repo
é o nome do repositório do rótulo
pac/kage
é o diretório em que o arquivo BUILD
está e name
é o caminho do
o arquivo (se o rótulo se referir a um arquivo de origem) relativo ao diretório do
. Ao se referir a um destino na linha de comando, algumas partes do rótulo
podem ser omitidas:
- Se o repositório for omitido, o rótulo será considerado na pasta principal repositório de dados.
- Se a parte do pacote for omitida (como
name
ou:name
), o rótulo será usado para estar no pacote do diretório de trabalho atual (caminhos relativos que contenham referências de nível superior (..) não são permitidas)
Um tipo de regra (como "biblioteca C++") é chamado de "classe de regras". As classes de regras podem
ser implementados em Starlark (a função rule()
) ou em Java (assim chamado
"native rules", digite RuleClass
). A longo prazo, cada linguagem específica
será implementada no Starlark, mas algumas famílias de regras legadas (como
ou C++) ainda estão em Java por enquanto.
As classes de regra do Starlark precisam ser importadas no início dos arquivos BUILD
usando a instrução load()
, enquanto as classes de regra Java são "intencionais" conhecido por
Bazel, em virtude de estar registrado com ConfiguredRuleClassProvider
.
As classes de regra contêm informações como:
- Os atributos (como
srcs
,deps
): tipos, valores padrão, restrições etc. - As transições e os aspectos de configuração anexados a cada atributo, se houver
- A implementação da regra
- Os provedores de informações transitivas que a regra "geralmente" cria
Observação de terminologia:na base de código, geralmente usamos "Regra" significar o alvo
criados por uma classe de regra. Mas no Starlark e na documentação voltada ao usuário,
"Regra" deve ser usado exclusivamente para se referir à própria classe de regras; a meta
é apenas um "alvo". Observe também que, apesar de RuleClass
ter "class" na
nome, não há relacionamento de herança Java entre uma classe de regra e os destinos
desse tipo.
Skyframe
O framework de avaliação subjacente ao Bazel é chamado de Skyframe. Seu modelo é que tudo o que precisa ser construído durante um build é organizado em uma gráfico acíclico com bordas apontando de quaisquer dados para suas dependências, ou seja, outros dados que precisam ser conhecidos para construí-los.
Os nós no gráfico são chamados de SkyValue
s, e seus nomes são chamados
SkyKey
. Ambos são profundamente imutáveis; apenas objetos imutáveis devem ser
que estão acessíveis para eles. Essa invariante quase sempre se aplica e, caso não
(como nas classes de opções individuais BuildOptions
, que é membro da
BuildConfigurationValue
e a SkyKey
dele), fazemos o possível para não mudar
ou para mudá-los somente de maneiras que não são observáveis de fora.
A partir disso, tudo que é calculado no Skyframe (como
destinos configurados) também precisam ser imutáveis.
A maneira mais conveniente de observar o gráfico do Skyframe é executar bazel dump
--skyframe=deps
, que despeja o gráfico, um SkyValue
por linha. É melhor
em builds muito pequenos, já que podem ficar muito grandes.
O Skyframe fica no pacote com.google.devtools.build.skyframe
. A
o pacote com.google.devtools.build.lib.skyframe
com nome semelhante contém
implementação do Bazel sobre o Skyframe. Mais informações sobre o Skyframe estão
disponíveis aqui.
Para avaliar um determinado SkyKey
em um SkyValue
, o Skyframe invoca o
SkyFunction
correspondente ao tipo da chave. Durante o ciclo de vida
ele poderá solicitar outras dependências do Skyframe chamando o
várias sobrecargas de SkyFunction.Environment.getValue()
. Ele tem
efeito colateral de registrar essas dependências no gráfico interno do Skyframe, de modo que
que o Skyframe saiba que precisa reavaliar a função quando qualquer uma das dependências
mudar. Em outras palavras, o armazenamento em cache e a computação incremental do Skyframe funcionam
a granularidade de SkyFunction
s e SkyValue
s.
Sempre que um SkyFunction
solicita uma dependência indisponível, getValue()
retornará nulo. A função deve então retornar o controle ao Skyframe
retornando nulo. Em algum momento posterior, o Skyframe avaliará
indisponível, reinicie a função desde o início — somente esse
momento em que a chamada getValue()
será bem-sucedida com um resultado não nulo.
Uma consequência disso é que qualquer computação realizada dentro do SkyFunction
antes da reinicialização precisam ser repetidos. Mas isso não inclui o trabalho feito para
avaliar a dependência SkyValues
, que são armazenadas em cache. Por isso, normalmente trabalhamos
em torno desse problema ao:
- Declarando dependências em lotes (usando
getValuesAndExceptions()
) limitar o número de reinicializações. - Dividir um
SkyValue
em partes separadas, calculadas por diferentesSkyFunction
s, para que possam ser calculados e armazenados em cache de forma independente. Isso deve ser feito estrategicamente, pois tem o potencial de aumentar a memória uso. - Armazenamento do estado entre reinicializações, seja usando
SkyFunction.Environment.getState()
ou manter um cache estático ad hoc "atrás do Skyframe".
Essencialmente, precisamos desses tipos de soluções alternativas porque temos centenas de milhares de nós Skyframe em trânsito, e o Java não oferece suporte e encadeamentos leves.
Lark
Starlark é a linguagem específica de domínio que as pessoas usam para configurar e ampliar o Bazel Ela é concebida como um subconjunto restrito do Python que tem muito menos tipos, mais restrições no fluxo de controle e, o mais importante, forte imutabilidade para permitir leituras simultâneas. Ele não está completo de Turing, desencorajamos alguns (mas não todos) os usuários de tentar realizar tarefas de programação dentro da linguagem.
O Starlark é implementado no pacote net.starlark.java
.
Ele também tem uma implementação independente do Go
aqui. A linguagem Java
usada no Bazel é atualmente um intérprete.
O Starlark é usado em vários contextos, incluindo:
- O idioma
BUILD
. É aqui que as novas regras são definidas. Código Starlark em execução neste contexto só tem acesso ao conteúdo do arquivoBUILD
e os arquivos.bzl
carregados por ele. - Definições de regras. É assim que as novas regras (como o suporte a um novo linguagem) estão definidos. O código Starlark em execução nesse contexto tem acesso a a configuração e os dados fornecidos por suas dependências diretas (mais informações mais tarde).
- O arquivo WORKSPACE. É aqui que os repositórios externos (códigos na árvore de origem principal) estão definidos.
- Definições de regras de repositório. É aqui que novos tipos de repositórios externos estão definidos. O código Starlark em execução nesse contexto pode executar código arbitrário a máquina em que o Bazel está sendo executado e chegar fora do espaço de trabalho.
Os dialetos disponíveis para arquivos BUILD
e .bzl
são um pouco diferentes
porque expressam coisas diferentes. Uma lista de diferenças está disponível
aqui.
Mais informações sobre o Starlark estão disponíveis aqui.
Fase de carregamento/análise
Na fase de carregamento/análise, o Bazel determina quais ações são necessárias para criar uma regra específica. Sua unidade básica é um "objetivo configurado", ou seja, sensivelmente, um par (alvo, configuração).
Ela é chamada de "fase de carregamento/análise" porque ele pode ser dividido em dois partes distintas, que costumavam ser serializadas, mas agora podem se sobrepor no tempo:
- Carregar pacotes, ou seja, transformar arquivos
BUILD
em objetosPackage
que os representam - Analisar os destinos configurados, ou seja, executar a implementação do de regras para produzir o gráfico de ação
Cada destino configurado no fechamento transitivo dos destinos configurados solicitados na linha de comando precisam ser analisados de baixo para cima. ou seja, os nós de folhas e depois os que estão na linha de comando. As entradas para a análise de um único destino configurado são:
- A configuração. ("como" criar essa regra; por exemplo, o plataforma, mas também coisas como opções de linha de comando que o usuário deseja passados para o compilador C++)
- As dependências diretas. Os provedores de informações transitivas deles estão disponíveis à regra que está sendo analisada. Eles são chamados assim porque fornecem uma "roll-up" das informações no fechamento transitivo do fluxo destino, como todos os arquivos .jar no caminho de classe ou todos os arquivos .o que precisam ser vinculados a um binário C++)
- O próprio destino. Este é o resultado do carregamento do pacote de destino está. Para regras, isso inclui seus atributos, que geralmente são o que é importante.
- A implementação do destino configurado. Para regras, isso pode em Starlark ou Java. Todas as metas que não são configuradas por regras foram implementadas em Java.
A saída da análise de um destino configurado é:
- Os provedores de informações transitivas que configuraram destinos que dependem deles podem acesso
- os artefatos que ele pode criar e as ações que os produzem;
A API oferecida para as regras de Java é RuleContext
, que é equivalente à
Argumento ctx
das regras de Starlark. Sua API é mais eficiente, mas com o mesmo
é mais fácil fazer Bad ThingsTM, por exemplo, escrever código cuja hora ou
é quadrática (ou pior), para fazer o servidor Bazel falhar com uma
exceção Java ou para violar invariantes (como modificar acidentalmente um
instância Options
ou tornando um destino configurado mutável).
O algoritmo que determina as dependências diretas de um destino configurado
mora em DependencyResolver.dependentNodeMap()
.
Configurações
As configurações são o "como" de criar um público-alvo: para qual plataforma, com qual opções de linha de comando etc.
O mesmo destino pode ser criado para várias configurações no mesmo build. Isso é útil, por exemplo, quando o mesmo código é usado para uma ferramenta executada durante para o build e o código de destino e estamos fazendo compilação cruzada ou quando criar um aplicativo Android multiuso (que contenha código nativo para vários recursos de CPU arquiteturas de
Conceitualmente, a configuração é uma instância de BuildOptions
. No entanto, em
prática, BuildOptions
é encapsulada por BuildConfiguration
, que fornece
funcionalidades adicionais. Ela se propaga da parte superior
gráfico de dependência para a parte inferior. Se mudar, o build precisará ser
analisado novamente.
Isso resulta em anomalias como a necessidade de analisar novamente todo o build se, por exemplo, o número de execuções de teste solicitadas muda, mesmo que apenas afeta os destinos de teste (temos planos de "cortar" as configurações para que isso seja não é o caso, mas ele ainda não está pronto).
Quando a implementação de uma regra precisa de parte da configuração, é necessário declarar
na definição usando RuleClass.Builder.requiresConfigurationFragments()
, Isso serve tanto para evitar erros (como regras de Python usando o fragmento Java) e
para facilitar o corte de configuração para que, por exemplo, se as opções do Python mudarem, o C++
e os alvos não precisam ser analisados novamente.
A configuração de uma regra não é necessariamente a mesma do seu "pai" regra de firewall. O processo de alteração da configuração em uma borda de dependência é chamado de "transição de configuração". Isso pode acontecer em dois lugares:
- Em uma borda de dependência. Essas transições são especificadas
Attribute.Builder.cfg()
e são funções de umaRule
(em que o a transição acontecer) e umBuildOptions
(a configuração original) para um ou maisBuildOptions
(a configuração de saída). - Em qualquer borda de entrada para um destino configurado. Eles são especificados em
RuleClass.Builder.cfg()
:
As classes relevantes são TransitionFactory
e ConfigurationTransition
.
As transições de configuração são usadas, por exemplo:
- Para declarar que uma dependência específica é usada durante o build e ela precisa ser incorporada na arquitetura de execução
- Para declarar que uma dependência específica deve ser criada para vários arquiteturas (como para código nativo em APKs grandes do Android)
Se uma transição de configuração resultar em várias configurações, ela é chamada de transição dividida.
As transições de configuração também podem ser implementadas no Starlark (documentação aqui)
Provedores de informações transitivas
Provedores de informações transitivas são um caminho (e o _único _caminho) de destinos configurados para informar sobre outros destinos configurados que dependem deles. O motivo "transitivo" em seu nome é que isso geralmente é algum tipo de agrupamento de o fechamento transitivo de um destino configurado.
Geralmente, há uma correspondência individual entre provedores de informações transitivas do Java
e do Starlark (a exceção é DefaultInfo
, que é uma fusão das
FileProvider
, FilesToRunProvider
e RunfilesProvider
porque essa API foi
considerada mais Starlark do que a transliteração direta do Java).
A chave é uma das seguintes coisas:
- Um objeto de classe Java. Isso só está disponível para provedores que não
acessível pelo Starlark. Esses provedores são uma subclasse
TransitiveInfoProvider
: - Uma string. Isso é legado e altamente desencorajado, já que é suscetível a
entre em conflito. Esses provedores de informações transitivas são subclasses diretas de
build.lib.packages.Info
. - Símbolo de provedor. Ela pode ser criada no Starlark usando a
provider()
. e é a forma recomendada de criar provedores. O símbolo é representado por uma instânciaProvider.Key
em Java.
Novos provedores implementados em Java precisam ser implementados usando BuiltinProvider
.
O uso de NativeProvider
foi descontinuado (ainda não tivemos tempo de removê-lo) e
Não é possível acessar subclasses TransitiveInfoProvider
no Starlark.
Metas configuradas
As metas configuradas são implementadas como RuleConfiguredTargetFactory
. Há um
para cada classe de regra implementada em Java. Destinos configurados pelo Starlark
são criados usando StarlarkRuleConfiguredTargetUtil.buildRule()
.
As fábricas de destino configuradas precisam usar RuleConfiguredTargetBuilder
para
construa o valor de retorno. Ele consiste nos seguintes itens:
- O
filesToBuild
, o conceito confuso de "o conjunto de arquivos que essa regra representa". Esses são os arquivos criados quando o destino configurado está na linha de comando ou no srcs de uma genrule. - Os arquivos de execução, regulares e dados.
- Os grupos de saída deles. Estes são vários "outros conjuntos de arquivos" a regra pode
ser construído. Eles podem ser acessados usando o atributo output_group da
regra de grupo de arquivos em BUILD e usar o provedor
OutputGroupInfo
em Java.
Arquivos de execução
Alguns binários precisam de arquivos de dados para serem executados. Um exemplo famoso são os testes que precisam arquivos de entrada. Isso é representado no Bazel pelo conceito de "runfiles". Um "árvore do runfiles" é uma árvore de diretórios dos arquivos de dados de um binário específico. Ele é criado no sistema de arquivos como uma árvore de links simbólicos com links simbólicos individuais. apontando para os arquivos na origem das árvores de saída.
Um conjunto de arquivos de execução é representado como uma instância Runfiles
. Conceitualmente, é uma
do caminho de um arquivo na árvore de arquivos de execução para a instância Artifact
que
o representa. É um pouco mais complicado do que uma única Map
para dois
motivos:
- Na maioria das vezes, o caminho dos arquivos de execução de um arquivo é o mesmo do execpath. Usamos isso para economizar espaço na RAM.
- Há vários tipos legados de entradas em árvores do Runfiles, que também precisam que devem ser representados.
Os arquivos de execução são coletados usando RunfilesProvider
: uma instância dessa classe.
representa os arquivos de execução de um destino configurado (como uma biblioteca) e seus
necessidades de fechamento e são reunidas como um conjunto aninhado
implementado usando conjuntos aninhados ocultos: cada destino une os arquivos de execução.
das dependências, adiciona algumas das próprias e envia o conjunto resultante para cima,
no gráfico de dependência. Uma instância RunfilesProvider
contém duas Runfiles
uma para quando a regra depende dos "dados" e
uma para cada outro tipo de dependência de entrada. Isso ocorre porque uma meta
às vezes, apresenta arquivos de execução diferentes quando eles dependem de um atributo de dados.
do que de outra forma. Esse é um comportamento legado indesejado que não enfrentamos
já está removendo.
Os arquivos de execução de binários são representados como uma instância de RunfilesSupport
. Isso
é diferente de Runfiles
porque RunfilesSupport
tem a capacidade de
sendo criada, diferentemente de Runfiles
, que é apenas um mapeamento. Isso
precisa dos seguintes componentes adicionais:
- O manifesto dos arquivos de execução de entrada. É uma descrição serializada do árvore de arquivos de execução. É usado como um proxy para o conteúdo da árvore de arquivos de execução. e o Bazel pressupõe que a árvore de arquivos de execução muda somente se o conteúdo da mudança de manifesto.
- O manifesto dos arquivos de execução de saída. Ele é usado por bibliotecas de ambiente de execução que lidar com árvores de arquivos de execução, especialmente no Windows, que às vezes não oferecem suporte links simbólicos.
- O intermediário dos arquivos de execução. Para que uma árvore de arquivos de execução exista, é necessário para criar a árvore de links simbólicos e o artefato para o qual os links simbólicos apontam. Para para diminuir o número de arestas de dependência, o intermediário dos arquivos de execução pode ser costumava representar tudo isso.
- Argumentos de linha de comando para executar o binário cujos arquivos de execução o
objeto
RunfilesSupport
representa.
Aspectos
Os aspectos são uma forma de "propagar a computação pelo gráfico de dependências". São
descritos para os usuários do Bazel
aqui. Uma boa
um exemplo de motivação são os buffers de protocolo: uma regra proto_library
não deve saber
sobre qualquer linguagem específica, mas criar a implementação de um protocolo
(a "unidade básica" dos buffers de protocolo) em qualquer programação
deve ser acoplada à regra proto_library
para que, se dois destinos em
da mesma linguagem dependem do mesmo buffer de protocolo, ele é criado apenas uma vez.
Assim como os destinos configurados, eles são representados no Skyframe como um SkyValue
.
e a maneira como eles são construídos é muito semelhante à forma como os destinos configurados
criados: eles têm uma classe de fábrica chamada ConfiguredAspectFactory
, que tem
acesso a um RuleContext
, mas, diferentemente das fábricas de destino configuradas, ele também sabe
sobre o destino configurado ao qual está anexado e os provedores dele.
O conjunto de aspectos propagados pelo gráfico de dependência é especificado para cada
usando a função Attribute.Builder.aspects()
. Existem alguns
classes com nomes confusos que participam do processo:
AspectClass
é a implementação do aspecto. Pode ser em Java (nesse caso, é uma subclasse) ou em Starlark (nesse caso, é uma instância deStarlarkAspectClass
). É análogo aoRuleConfiguredTargetFactory
.AspectDefinition
é a definição do aspecto. ela inclui provedores necessários, os provedores que ela fornece e contém uma referência aos a implementação dele, como a instânciaAspectClass
apropriada. Está análoga aRuleClass
.AspectParameters
é uma forma de parametrizar um aspecto que é propagado para baixo. o gráfico de dependência. No momento, ele é um mapa de string a string. Um bom exemplo por que isso é útil são os buffers de protocolo: se uma linguagem tiver várias APIs, o informações sobre para qual API os buffers de protocolo devem ser criados propagadas pelo gráfico de dependências.Aspect
representa todos os dados necessários para computar um aspecto que se propaga pelo gráfico de dependências. Ele consiste na classe de aspecto, definição e seus parâmetros.RuleAspect
é a função que determina quais aspectos uma regra específica devem se propagar. É umRule
-> funçãoAspect
.
Uma complicação um pouco inesperada é que alguns aspectos podem se vincular a outros.
por exemplo, um aspecto que coleta o classpath de um Java IDE provavelmente
querem saber sobre todos os arquivos .jar no caminho de classe, mas alguns deles são
buffers de protocolo. Nesse caso, será necessário anexar o aspecto do ambiente de desenvolvimento integrado ao
(regra proto_library
+ aspecto do proto Java).
A complexidade dos aspectos dos aspectos é capturada na aula
AspectCollection
:
Plataformas e conjuntos de ferramentas
O Bazel oferece suporte a builds multiplataforma, ou seja, builds em que haja múltiplas arquiteturas em que as ações de compilação são executadas e múltiplas arquiteturas para qual código é criado. No Bazel, essas arquiteturas são chamadas de plataformas. linguagem (documentação completa) aqui)
Uma plataforma é descrita por um mapeamento de chave-valor das configurações de restrição (como
o conceito de "arquitetura de CPU") para valores de restrição (como uma CPU específica
como x86_64). Temos um "dicionário" das restrições mais usadas
configurações e valores no repositório @platforms
.
O conceito de conjunto de ferramentas vem do fato de que, dependendo de quais plataformas em que o build está sendo executado e em quais plataformas são segmentadas, pode ser necessário usar compiladores diferentes; por exemplo, um determinado conjunto de ferramentas C++ pode ser executado em uma para um sistema operacional específico e segmentar outros sistemas operacionais. O Bazel precisa determinar o código compilador que é usado com base na execução definida e na plataforma de destino (documentação sobre conjuntos de ferramentas aqui.
Para isso, os conjuntos de ferramentas são anotados com o conjunto de execução e restrições da plataforma de destino que suportam. Para fazer isso, a definição de um conjunto de ferramentas são divididos em duas partes:
- Uma regra
toolchain()
que descreva o conjunto de execução e destino de que um conjunto de ferramentas oferece suporte e informa o tipo (como C++ ou Java) de conjunto de ferramentas que é (o último é representado pela regratoolchain_type()
) - Uma regra específica da linguagem que descreve o conjunto de ferramentas real (como
cc_toolchain()
)
Isso é feito dessa forma porque precisamos conhecer as restrições para cada
para fazer a resolução do conjunto de ferramentas e configurações
As regras *_toolchain()
contêm muito mais informações do que isso, por isso levam mais
tempo de carregamento.
As plataformas de execução são especificadas de uma das seguintes maneiras:
- No arquivo ESPAÇO DE TRABALHO, usando a função
register_execution_platforms()
- Na linha de comando usando --extra_execution_platforms opção
O conjunto de plataformas de execução disponíveis é calculado em
RegisteredExecutionPlatformsFunction
.
A plataforma de destino para um destino configurado é determinada pela
PlatformOptions.computeTargetPlatform()
. É uma lista de plataformas porque
querem oferecer suporte a várias plataformas de destino, mas isso não foi implementado
ainda.
O conjunto de conjuntos de ferramentas a serem usados para um destino configurado é determinado pelos
ToolchainResolutionFunction
: É uma função de:
- O conjunto de conjuntos de ferramentas registrados (no arquivo WORKSPACE e o )
- A execução e as plataformas de destino desejadas (na configuração)
- O conjunto de tipos de conjunto de ferramentas exigido pelo destino configurado (em
UnloadedToolchainContextKey)
- O conjunto de restrições de plataforma de execução do destino configurado (o
exec_compatible_with
) e a configuração (--experimental_add_exec_constraints_to_targets
), emUnloadedToolchainContextKey
O resultado é um UnloadedToolchainContext
, que é essencialmente um mapa da
tipo de conjunto de ferramentas (representado como uma instância ToolchainTypeInfo
) para o rótulo de
o conjunto de ferramentas selecionado. Ele se chama "descarregado" porque ele não contém
em si, somente os rótulos.
Em seguida, os conjuntos de ferramentas são carregados usando ResolvedToolchainContext.load()
.
e usados pela implementação do destino configurado que as solicitou.
Também temos um sistema legado que depende da existência de um único "host"
e as configurações de destino representadas por várias
flags de configuração, como --cpu
. Estamos fazendo a transição gradual para as opções acima
sistema. Para lidar com casos em que as pessoas dependem da configuração legada
os valores de conversão, implementamos
mapeamentos de plataforma
para fazer a conversão entre as sinalizações legadas e as restrições da plataforma do novo estilo.
O código está em PlatformMappingFunction
e usa um "pouco não Starlark"
idioma".
Restrições
Às vezes, você quer designar um destino como compatível com apenas alguns plataformas. Infelizmente, o Bazel tem vários mecanismos para fazer isso:
- Restrições específicas de regras
environment_group()
/environment()
- Restrições da plataforma
Restrições específicas de regras são usadas principalmente no Google para regras Java. eles são
e eles não estão disponíveis no Bazel, mas o código-fonte pode
contêm referências a ele. O atributo que rege isso é chamado
constraints=
.
ambiente_group() e ambiente()
Essas regras são um mecanismo legado e não são amplamente utilizadas.
Todas as regras de build podem declarar quais "ambientes" para a qual eles podem ser construídos,
"ambiente" é uma instância da regra environment()
.
Há várias maneiras de especificar ambientes com suporte para uma regra:
- Pelo atributo
restricted_to=
. Essa é a forma mais direta de especificação declara o conjunto exato de ambientes com suporte para a regra para este grupo. - Pelo atributo
compatible_with=
. Isso declara aos ambientes uma regra além de "padrão", ambientes com suporte padrão. - Usando os atributos de nível de pacote
default_restricted_to=
edefault_compatible_with=
. - Por especificações padrão em regras
environment_group()
. Todas de rede pertence a um grupo de pares tematicamente relacionados (como arquiteturas de terceiros", "versões do JDK" ou "sistemas operacionais para dispositivos móveis"). A a definição de um grupo de ambientes inclui qual desses ambientes é compatível com "default" caso não seja especificado de outra forma pelo Atributosrestricted_to=
/environment()
. Uma regra sem essa herdam todos os padrões. - Por um padrão de classe de regra. Isso substitui os padrões globais de todas
instâncias da classe de regra especificada. Isso pode ser usado, por exemplo, para tornar
todas as regras
*_test
possam ser testadas sem que cada instância precise declarar essa capacidade.
environment()
é implementado como uma regra regular, enquanto environment_group()
é uma subclasse de Target
, mas não de Rule
(EnvironmentGroup
) e um
disponível por padrão no Starlark
(StarlarkLibrary.environmentGroup()
) que, por fim, cria um nome
alvo. Isso serve para evitar uma dependência cíclica que surgiria porque cada
ambiente precisa declarar o grupo a que pertence e cada
grupo de ambiente de execução precisa declarar seus ambientes padrão.
Um build pode ser restrito a um determinado ambiente com o
Opção de linha de comando --target_environment
.
A implementação da verificação de restrições está
RuleContextConstraintSemantics
e TopLevelConstraintSemantics
.
Restrições da plataforma
A posição "oficial" atual é uma forma de descrever em quais plataformas um público-alvo é compatível é usar as mesmas restrições usadas para descrever os conjuntos de ferramentas e as plataformas. Está em análise na solicitação de envio N.o 10945 (link em inglês).
Visibilidade
Se você trabalha em uma grande base de código com muitos desenvolvedores (como no Google), você evitar que outras pessoas dependam arbitrariamente das suas o código-fonte. Caso contrário, de acordo com a Lei de Hyrum, as pessoas vão confiar em comportamentos que você considerou serem implementados detalhes.
O Bazel oferece essa função pelo mecanismo visibilidade: é possível declarar que uma determinado segmento só pode depender do uso do visibilidade. Isso atributo é um pouco especial porque, embora contenha uma lista de marcadores, esses rótulos podem codificar um padrão sobre nomes de pacotes em vez de um ponteiro para qualquer um alvo específico. (Sim, isso é uma falha de design.)
Isso é implementado nos seguintes locais:
- A interface
RuleVisibility
representa uma declaração de visibilidade. Ela pode ser uma constante (totalmente pública ou totalmente particular) ou uma lista de rótulos. - Os rótulos podem se referir a grupos de pacotes (lista predefinida de pacotes)
pacotes diretamente (
//pkg:__pkg__
) ou subárvores de pacotes//pkg:__subpackages__
). Ela é diferente da sintaxe da linha de comando, que usa//pkg:*
ou//pkg/...
. - Os grupos de pacotes são implementados como o próprio destino (
PackageGroup
) e destino configurado (PackageGroupConfiguredTarget
). Poderíamos provavelmente e substituí-las por regras simples, se quiséssemos. A lógica deles é implementada com a ajuda dePackageSpecification
, que corresponde a uma padrão único, como//pkg/...
;PackageGroupContents
, que corresponde para um único atributopackages
depackage_group
. ePackageSpecificationProvider
, que agrega mais de umpackage_group
e seuincludes
transitivo. - A conversão de listas de rótulos de visibilidade para dependências é feita em
DependencyResolver.visitTargetVisibility
e mais alguns outros itens lugares. - A verificação real é feita
CommonPrerequisiteValidator.validateDirectPrerequisiteVisibility()
Conjuntos aninhados
Muitas vezes, um destino configurado agrega um conjunto de arquivos de suas dependências, adiciona seu próprio e encapsula o conjunto agregado em um provedor de informações transitivas para que os destinos configurados que dependem dele podem fazer o mesmo. Exemplos:
- Os arquivos principais C++ usados para um build
- Os arquivos de objeto que representam o fechamento transitivo de um
cc_library
- O conjunto de arquivos .jar que precisam estar no caminho de classe para que uma regra Java compilar ou executar
- O conjunto de arquivos Python no fechamento transitivo de uma regra do Python
Se fizéssemos isso de forma ingênua usando, por exemplo, List
ou Set
, acabaríamos com
Uso da memória quadrática: se houver uma cadeia de N regras e cada regra adicionar um
teríamos 1+2+...+N membros de coleção.
Para contornar esse problema, criamos o conceito de
NestedSet
: Ela é uma estrutura de dados composta por outros NestedSet
.
instâncias e alguns membros próprios, formando assim um gráfico acíclico dirigido
de conjuntos. Elas são imutáveis e os membros podem ser iterados. Nós definimos
ordem de iteração múltipla (NestedSet.Order
): pedido antecipado, pós-pedido, topológico
(um nó sempre vem depois de seus ancestrais) e "não se importa, mas esse
sempre iguais".
A mesma estrutura de dados é chamada de depset
no Starlark.
Artefatos e ações
O build real consiste em um conjunto de comandos que precisam ser executados para produzir
a saída que o usuário quer. Os comandos são representados como instâncias do
classe Action
, e os arquivos são representados como instâncias da classe
Artifact
. Elas estão dispostas em um gráfico acíclico dirigido, bipartito,
"gráfico de ações".
Há dois tipos de artefatos: artefatos de origem (que estão disponíveis antes do Bazel começar a ser executado) e artefatos derivados (aqueles que precisam ser construídos). Os artefatos derivados podem ser de vários tipos:
- **Artefatos regulares. **Elas são verificadas para garantir a atualização a soma de verificação, com mtime como atalho; não fazemos checksum do arquivo se ctime não mudou.
- Artefatos de link simbólico não resolvidos. Elas são verificadas pelo chame readlink(). Ao contrário dos artefatos normais, eles podem estar pendentes links simbólicos. Geralmente usado nos casos em que é preciso empacotar alguns arquivos em um algum tipo de arquivo.
- Artefatos de árvores. Não são arquivos únicos, mas árvores de diretórios. Eles
são verificadas pelo conjunto de arquivos e seus
conteúdo. Eles são representados como
TreeArtifact
. - Artefatos de metadados constantes. As mudanças nesses artefatos não acionam uma ser reconstruída. Isso é usado exclusivamente para informações de carimbo de data/hora da versão: não queremos reconstruir só porque o horário atual mudou.
Não há motivo fundamental para que os artefatos de origem não sejam artefatos de árvore ou
artefatos de link simbólico não resolvidos, mas ainda não o implementamos (estamos
deveria, no entanto, fazer referência a um diretório de origem em um arquivo BUILD
é uma das
alguns problemas de incorreção antigos conhecidos com o Bazel. temos um
implementação que funciona, e é possível
propriedade da JVM BAZEL_TRACK_SOURCE_DIRECTORIES=1
)
Um tipo notável de Artifact
são os intermediários. Elas são indicadas por Artifact
.
instâncias que são as saídas de MiddlemanAction
. Eles são usados para
alguns casos especiais:
- A agregação de intermediários é usada para agrupar artefatos. Isso é para que Se muitas ações usarem o mesmo grande conjunto de entradas, não teremos N*M bordas de dependência, somente N+M (que estão sendo substituídas por conjuntos aninhados)
- Intermediários de dependência de programação garantem que uma ação seja executada antes de outra.
Elas são usadas principalmente para inspeção, mas também para compilação em C++ (consulte
CcCompilationContext.createMiddleman()
para conferir uma explicação) - Os intermediários de arquivos de execução são usados para garantir a presença de uma árvore de arquivos de execução para que que você não precise depender separadamente do manifesto de saída e que cada único artefato referenciado pela árvore de arquivos de execução.
As ações são mais bem entendidas como um comando que precisa ser executado, que ele precisa e o conjunto de saídas que ele produz. Os itens a seguir são os principais componentes da descrição de uma ação:
- A linha de comando que precisa ser executada
- Os artefatos de entrada necessários
- As variáveis de ambiente que precisam ser definidas
- Anotações que descrevem o ambiente (como a plataforma) em que ele precisa ser executado .
Há também outros casos especiais, como gravar um arquivo cujo conteúdo é
conhecidos pelo Bazel. Elas são uma subclasse de AbstractAction
. A maioria das ações são
um SpawnAction
ou um StarlarkAction
(o mesmo, eles não deveriam ser
classes separadas), embora Java e C++ tenham os próprios tipos de ação
(JavaCompileAction
, CppCompileAction
e CppLinkAction
).
Eventualmente, queremos mover tudo para SpawnAction
. JavaCompileAction
é
mas o C++ é um caso especial devido à análise de arquivos .d e
incluem verificação.
Na maioria das vezes, o gráfico de ações é "incorporado" para o gráfico Skyframe: conceitualmente, a
a execução de uma ação é representada como uma invocação
ActionExecutionFunction
: O mapeamento de uma borda de dependência do gráfico de ações para um
A borda de dependência do Skyframe é descrita em
ActionExecutionFunction.getInputDeps()
e Artifact.key()
e tem alguns
para manter baixo o número de bordas de Skyframe:
- Os artefatos derivados não têm os próprios
SkyValue
s. Em vez disso,Artifact.getGeneratingActionKey()
é usado para descobrir a chave do que gera - Os conjuntos aninhados têm sua própria chave de Skyframe.
Ações compartilhadas
Algumas ações são geradas por vários destinos configurados. As regras Starlark são são mais limitadas, pois só podem colocar as ações derivadas determinado pela configuração e pelo pacote (mas mesmo assim, regras no mesmo pacote podem entrar em conflito), mas as regras implementadas em Java podem colocar os artefatos derivados em qualquer lugar.
Isso é considerado um erro de recurso, mas eliminar isso é muito difícil porque produz uma economia significativa no tempo de execução quando, por exemplo, arquivo de origem precisa ser processado de alguma forma e esse arquivo é referenciado por várias regras (handwave-handwave). Isso custa um pouco de RAM: cada instância de uma ação compartilhada precisa ser armazenada separadamente na memória.
Se duas ações gerarem o mesmo arquivo de saída, elas precisarão ser exatamente as mesmas:
têm as mesmas entradas, as mesmas saídas e executam a mesma linha de comando. Isso
relação de equivalência é implementada em Actions.canBeShared()
e é
verificados entre as fases de análise e execução, analisando cada ação.
Isso foi implementado em SkyframeActionExecutor.findAndStoreArtifactConflicts()
e é um dos únicos locais no Bazel que exigem uma configuração do
ser construído.
A fase de execução
É quando o Bazel começa a executar ações de compilação, como comandos que para produzir saídas.
A primeira coisa que o Bazel faz após a fase de análise é determinar o que
É preciso construir artefatos. A lógica para isso é codificada
TopLevelArtifactHelper
Em termos gerais, é o filesToBuild
do
os destinos configurados na linha de comando e o conteúdo de um bloco
grupo com a finalidade explícita de expressar "se este destino estiver no comando
crie esses artefatos".
A próxima etapa é criar a raiz de execução. Como o Bazel tem a opção de ler
pacotes de origem de diferentes locais no sistema de arquivos (--package_path
);
ele precisa fornecer ações executadas localmente com uma árvore de origem completa. Isso é
processado pela classe SymlinkForest
e trabalha anotando cada destino
usada na fase de análise e construir uma única árvore de diretórios que vincula
cada pacote com um destino usado a partir de seu local real. Uma alternativa seria
transmitir os caminhos corretos para os comandos, considerando o --package_path
.
Isso não é desejável, porque:
- Muda as linhas de comando de ação quando um pacote é movido de um caminho de pacote entrada para outra (uma ocorrência comum)
- Se uma ação for executada remotamente, o resultado serão linhas de comando diferentes ele é executado localmente
- Requer uma transformação de linha de comando específica para a ferramenta em uso (considere a diferença entre caminhos de classe do Java e caminhos de inclusão do C++)
- Alterar a linha de comando de uma ação invalida a entrada do cache de ações
- O uso de
--package_path
está sendo descontinuado de modo lento e contínuo
Em seguida, o Bazel começa a atravessar o gráfico de ações (o gráfico bipartite dirigido).
composta de ações e seus artefatos de entrada e saída) e ações em execução.
A execução de cada ação é representada por uma instância do SkyValue
.
classe ActionExecutionValue
.
Como a execução de uma ação é cara, temos algumas camadas de armazenamento em cache que podem ser atingido por trás do Skyframe:
- O arquivo
ActionExecutionFunction.stateMap
contém dados para reiniciar o Skyframe deActionExecutionFunction
baratos - O cache de ações locais contém dados sobre o estado do sistema de arquivos
- Os sistemas de execução remota geralmente também contêm o próprio cache
O cache de ações locais
Esse cache é outra camada que fica atrás do Skyframe. mesmo que uma ação seja for executada novamente no Skyframe, ela ainda poderá ser um hit no cache de ações locais. Ela representa o estado do sistema de arquivos local e é serializado no disco, o que Isso significa que, ao iniciar um novo servidor do Bazel, é possível acessar o cache de ações locais mesmo que o gráfico do Skyframe esteja vazio.
Esse cache é verificado quanto a ocorrências usando o método
ActionCacheChecker.getTokenIfNeedToExecute()
.
Ao contrário do nome, é um mapa do caminho de um artefato derivado para o que o emitiu. A ação é descrita como:
- O conjunto dos arquivos de entrada e saída e a soma de verificação deles
- Sua "chave de ação", que geralmente é a linha de comando que foi executada, mas
em geral, representa tudo o que não é capturado pela soma de verificação do
arquivos de entrada (como para
FileWriteAction
, é a soma de verificação dos dados que está escrito)
Há também um "cache de ação de cima para baixo" altamente experimental que ainda está no de desenvolvimento de software, que usa hashes transitivos para evitar que o maior número de vezes.
Descoberta e remoção de entradas
Algumas ações são mais complicadas do que apenas ter um conjunto de entradas. Mudanças no o conjunto de entradas de uma ação tem duas formas:
- Uma ação pode descobrir novas entradas antes da execução ou decidir que algumas
das entradas não são realmente necessárias. O exemplo canônico é C++,
em que é melhor adivinhar quais arquivos de cabeçalho um arquivo C++
usa a partir de seu fechamento transitivo para que não tenhamos cuidado para enviar cada
para executores remotos. Portanto, temos a opção de não registrar todos
como uma "entrada", mas verifique o arquivo de origem para
cabeçalhos incluídos e apenas marcar esses arquivos de cabeçalho como entradas que são
mencionado nas instruções
#include
(superestimamos para não precisar implementar um pré-processador C completo). Essa opção atualmente está conectada "falso" no Bazel e é usada apenas no Google. - Uma ação pode realizar que alguns arquivos não foram usados durante a execução. Em C++, isso é chamado de "arquivos .d": o compilador informa quais arquivos de cabeçalho foram usada após o fato e para evitar o constrangimento de ter a incrementabilidade do que o Make, o Bazel usa esse fato. Isso oferece uma abordagem do que o verificador de inclusão, porque depende do compilador.
Elas são implementadas usando métodos na ação:
Action.discoverInputs()
é chamado. Ele deve retornar um conjunto aninhado de Artefatos determinados como obrigatórios. Eles precisam ser artefatos de origem para que não haja arestas de dependência no gráfico de ações sem uma equivalente no gráfico de destino configurado.- A ação é executada chamando
Action.execute()
. - Ao final de
Action.execute()
, a ação pode chamarAction.updateInputs()
para informar ao Bazel que nem todas as entradas foram necessários. Isso pode resultar em builds incrementais incorretos se uma entrada usada for relatadas como não usadas.
Quando um cache de ações retorna uma ocorrência em uma nova instância de ação (como
após a reinicialização do servidor), o Bazel chama o próprio updateInputs()
para que o conjunto de
"inputs" reflete o resultado da descoberta e da remoção de entradas feitas anteriormente.
As ações do Starlark podem usar a instalação para declarar algumas entradas como não utilizadas
usando o argumento unused_inputs_list=
de
ctx.actions.run()
.
Várias maneiras de executar ações: estratégias/ActionContexts
Algumas ações podem ser executadas de maneiras diferentes. Por exemplo, uma linha de comando pode ser
executados localmente, localmente, mas em vários tipos de sandboxes, ou remotamente. A
conceito que incorpora isso é chamado de ActionContext
(ou Strategy
, já que
foi apenas até a metade com uma renomeação...)
O ciclo de vida de um contexto de ação é o seguinte:
- Quando a fase de execução é iniciada, as instâncias
BlazeModule
precisam informar o que contextos de ação que elas têm. Isso acontece no construtorExecutionTool
: Os tipos de contexto de ação são identificados por umClass
do Java. que se refere a uma subinterface deActionContext
e qual interface que o contexto de ação deve implementar. - O contexto de ação apropriado é selecionado entre os disponíveis e é
encaminhados para
ActionExecutionContext
eBlazeExecutor
. - Contextos de solicitação de ações usando
ActionExecutionContext.getContext()
eBlazeExecutor.getStrategy()
(deve haver apenas uma maneira de fazer ele...)
As estratégias são livres para chamar outras para que façam seu trabalho; ela é usada para exemplo, na estratégia dinâmica que inicia ações local e remotamente, e usa o que terminar primeiro.
Uma estratégia notável é a que implementa processos de worker persistentes,
(WorkerSpawnStrategy
). A ideia é que algumas ferramentas têm um tempo de inicialização longo
e, portanto, deve ser reutilizado entre ações, em vez de iniciar uma nova para
cada ação. Isso representa um possível problema de correção, já que o
na promessa do processo do worker de que ele não carrega
estado entre solicitações individuais)
Se a ferramenta mudar, o processo do worker precisará ser reiniciado. Se um worker
pode ser reutilizado é determinado pela computação de uma soma de verificação para a ferramenta usada usando
WorkerFilesHash
: Ela depende de saber quais entradas da ação representam
que representam entradas da ferramenta, Isso é determinado pelo criador
da ação: Spawn.getToolFiles()
e os arquivos de execução do Spawn
são
contados como partes da ferramenta.
Mais informações sobre estratégias (ou contextos de ação):
- Informações sobre várias estratégias para executar ações estão disponíveis aqui.
- Informações sobre a estratégia dinâmica, na qual realizamos uma ação tanto local e remotamente para conferir o que terminar primeiro aqui.
- Informações sobre as complexidades da execução local de ações estão disponíveis aqui.
O gerente de recursos local
O Bazel pode executar muitas ações em paralelo. O número de ações locais deve ser executado em paralelo difere de uma ação para outra: quanto mais recursos um ação exigir, menos instâncias devem estar em execução ao mesmo tempo para evitar sobrecarregando a máquina local.
Isso é implementado na classe ResourceManager
: cada ação precisa ser
anotado com uma estimativa dos recursos locais necessários na forma de um
ResourceSet
instância (CPU e RAM). Então, quando os contextos de ação fazem algo
que exige recursos locais, ela chama ResourceManager.acquireResources()
e ficam bloqueados até os recursos necessários ficarem disponíveis.
Uma descrição mais detalhada do gerenciamento de recursos locais está disponível aqui.
A estrutura do diretório de saída
Cada ação exige um local separado no diretório de saída, onde ela é colocada das saídas. O local dos artefatos derivados geralmente é o seguinte:
$EXECROOT/bazel-out/<configuration>/bin/<package>/<artifact name>
Como é o nome do diretório que está associado a um determinado configuração determinada? Há duas propriedades desejadas conflitantes:
- Se duas configurações puderem ocorrer no mesmo build, elas deverão ter diretórios diferentes para que ambos possam ter a própria versão do mesmo ação; caso contrário, se as duas configurações discordarem, linha de ação que produz o mesmo arquivo de saída, o Bazel não sabe qual ação escolher (um "conflito de ação")
- Se duas configurações representam aproximadamente a mesma coisa, elas deveriam ter com o mesmo nome para que as ações executadas em um possam ser reutilizadas no outro caso as linhas de comando corresponder: por exemplo, mudanças nas opções de linha de comando para o compilador Java não pode fazer com que as ações de compilação do C++ sejam executadas novamente.
Até agora, não encontramos uma maneira fundamentada de resolver esse problema, o que tem semelhanças com o problema de corte de configuração. Uma discussão mais longa está disponível aqui. As principais áreas problemáticas são as regras dos Starlark (cujos autores geralmente não são intimamente familiarizado com Bazel) e aspectos que adicionam outra dimensão à espaço de coisas que podem produzir "o mesmo" arquivo de saída.
A abordagem atual é que o segmento do caminho para a configuração seja
<CPU>-<compilation mode>
com vários sufixos adicionados para que a configuração
transições implementadas em Java não resultam em conflitos de ação. Além disso, um
o checksum do conjunto de transições de configuração do Starlark é adicionado para que os usuários
não podem causar conflitos de ação. Está longe de ser perfeito. Isso é implementado
OutputDirectories.buildMnemonic()
e depende de cada fragmento de configuração
adicionando a própria parte ao nome do diretório de saída.
Testes
O Bazel tem suporte avançado para a execução de testes. Ela aceita estas opções:
- Executar testes remotamente (se houver um back-end de execução remota disponível)
- Execução de testes várias vezes em paralelo (para redução da redução ou coleta de tempo) dados)
- Fragmentação de testes (dividir casos de teste no mesmo teste com vários processos) para saber mais sobre velocidade)
- Nova execução de testes instáveis
- Agrupar testes em conjuntos de testes
Testes são destinos regulares configurados que têm um TestProvider, que descreve como o teste deve ser executado:
- Os artefatos cuja criação resultou na execução do teste. Este é um cache
status" arquivo que contém uma mensagem
TestResultData
serializada - O número de vezes que o teste deve ser executado
- O número de fragmentos em que o teste deve ser dividido
- Alguns parâmetros sobre como o teste precisa ser executado (como o tempo limite do teste)
Como determinar quais testes serão executados
Determinar quais testes serão executados é um processo complexo.
Primeiro, durante a análise do padrão de destino, os conjuntos de testes são expandidos recursivamente. A
a expansão é implementada em TestsForTargetPatternFunction
. Um pouco
surpreendente é que, se um conjunto de testes não declara testes, ele se refere
every no pacote. Isso é implementado em Package.beforeBuild()
pela
adicionando um atributo implícito chamado $implicit_tests
para testar regras do pacote.
Em seguida, os testes são filtrados por tamanho, tags, tempo limite e idioma de acordo com o
opções da linha de comando. Isso é implementado em TestFilter
e chamado do
TargetPatternPhaseFunction.determineTests()
durante a análise de destino e o
resultado é colocado em TargetPatternPhaseValue.getTestsToRunLabels()
. O motivo
por que os atributos de regra que podem ser filtrados não são configuráveis é que este
ocorre antes da fase de análise, portanto, a configuração não é
disponíveis.
Em seguida, ele é processado mais em BuildView.createResult()
: destinos com
na análise são filtrados, e os testes são divididos em grupos exclusivos e
não exclusivos. Em seguida, ele é colocado em AnalysisResult
, que é assim
O ExecutionTool
sabe quais testes executar.
Para dar mais transparência a esse processo, o tests()
o operador de consulta (implementado em TestsFunction
) está disponível para informar quais testes
são executados quando um determinado destino é especificado na linha de comando. Está
uma reimplementação, então ele provavelmente vai se desviar do que foi descrito acima
de várias maneiras sutis.
Como executar testes
Os testes são executados solicitando artefatos de status de cache. Assim,
resulta na execução de uma TestRunnerAction
, que chama a função
O TestActionContext
escolhido pela opção de linha de comando --test_strategy
que
executa o teste da maneira solicitada.
Os testes são executados de acordo com um protocolo elaborado que usa variáveis de ambiente para informar aos testes o que é esperado deles. Uma descrição detalhada do que o Bazel o que espera dos testes e o que esperar do Bazel está disponível aqui. No mais simples: um código de saída 0 significa sucesso, enquanto qualquer outro significa falha.
Além do arquivo de status de cache, cada processo de teste emite vários outros
. Eles são colocados no "diretório de registros de teste" que é o subdiretório chamado
testlogs
do diretório de saída da configuração de destino:
test.xml
, um arquivo XML no estilo JUnit detalhando os casos de teste individuais em o fragmento de testetest.log
, a saída do console do teste. stdout e stderr não são separadas.test.outputs
, o "diretório de saídas não declaradas"; ela é usada por testes que querem gerar arquivos, além dos que são impressos no terminal.
Durante a execução do teste, podem ocorrer duas coisas que não podem durante Criar destinos regulares: execução de teste exclusiva e streaming de saída.
Alguns testes precisam ser executados no modo exclusivo, por exemplo, não em paralelo com
outros testes. Isso pode ocorrer ao adicionar tags=["exclusive"]
ao
regra de teste ou executar o teste com --test_strategy=exclusive
. Cada um
é executado por uma invocação de Skyframe separada, solicitando a execução da
após o "main" ser construído. Isso é implementado
SkyframeExecutor.runExclusiveTest()
:
Ao contrário das ações normais, cuja saída terminal é despejada quando a ação
terminar, o usuário pode solicitar que o resultado dos testes seja transmitido para que possa
saber o progresso de um teste de longa duração. Ela é especificada pela
A opção de linha de comando --test_output=streamed
implica em teste exclusivo
para que as saídas de diferentes testes não sejam intercaladas.
Isso é implementado na classe StreamedTestOutput
, devidamente nomeada e funciona da seguinte forma:
Sondagem de mudanças no arquivo test.log
do teste em questão e despesa nova
bytes ao terminal em que o Bazel rege.
Os resultados dos testes executados estão disponíveis no barramento de eventos observando
vários eventos (como TestAttempt
, TestResult
ou TestingCompleteEvent
).
Eles são despejados no protocolo de evento de build e emitidos para o console
por AggregatingTestListener
.
Coleta de cobertura
A cobertura é informada pelos testes no formato LCOV nos arquivos
bazel-testlogs/$PACKAGE/$TARGET/coverage.dat
.
Para coletar cobertura, cada execução de teste é encapsulada em um script chamado
collect_coverage.sh
.
Este script configura o ambiente de teste para ativar a coleta de cobertura e determinar onde os arquivos de cobertura são gravados pelos ambientes de execução de cobertura. Em seguida, ele executa o teste. Um teste pode executar vários subprocessos e consistir em de partes escritas em várias linguagens de programação diferentes (com partes ambientes de execução de coleta de cobertura). O script de wrapper é responsável pela conversão os arquivos resultantes no formato LCOV, se necessário, e os mescla em um único .
A interposição de collect_coverage.sh
é feita pelas estratégias de teste e
exige que collect_coverage.sh
esteja nas entradas do teste. Isso é
realizada pelo atributo implícito :coverage_support
, que é resolvido para
o valor da flag de configuração --coverage_support
(consulte
TestConfiguration.TestOptions.coverageSupport
)
Alguns idiomas fazem instrumentação off-line, ou seja, a cobertura é adicionada no momento da compilação (como C++) e outros fazem on-line ou seja, a instrumentação de cobertura é adicionada na execução tempo de resposta.
Outro conceito central é a cobertura dos valores de referência. Essa é a cobertura de uma biblioteca,
binário ou testar se nenhum código foi executado nele. O problema que ele resolve é que,
desejar computar a cobertura de teste para um binário, não será suficiente mesclar o
de todos os testes porque pode haver código no binário que não está
vinculado a qualquer teste. Portanto, o que fazemos é emitir um arquivo de cobertura para cada
Binário que contém apenas os arquivos para os quais coletamos informações, sem cobertura
linhas O arquivo de cobertura do valor de referência para uma meta está em
bazel-testlogs/$PACKAGE/$TARGET/baseline_coverage.dat
. Ele também é gerado
para binários e bibliotecas, além de testes, se você passar no
Sinalização --nobuild_tests_only
para o Bazel.
No momento, a cobertura do valor de referência está corrompida.
Acompanhamos dois grupos de arquivos para a coleta da cobertura de cada regra: o conjunto de instrumentados e o conjunto de arquivos de metadados de instrumentação.
O conjunto de arquivos instrumentados é exatamente isso, um conjunto de arquivos para instrumentar. Para ambientes de execução de cobertura on-line, pode ser usado no tempo de execução para decidir quais arquivos devem ser instrumento. Ele também é usado para implementar a cobertura do valor de referência.
O conjunto de arquivos de metadados de instrumentação é o conjunto de arquivos extras necessários para um teste para gerar os arquivos LCOV que o Bazel exige. Na prática, isso consiste em específicos do ambiente de execução. Por exemplo, o gcc emite arquivos .gcno durante a compilação. Elas serão adicionadas ao conjunto de entradas das ações de teste se o modo de cobertura for ativado.
A coleta ou não da cobertura é armazenada no
BuildConfiguration
: Isso é útil porque é uma maneira fácil de alterar o teste
e o gráfico de ação, dependendo deste bit, mas também significa que se
invertido, todos os alvos precisam ser reanalisados (algumas linguagens, como
O C++ exige diferentes opções de compilador para emitir código que possa coletar cobertura,
o que atenua um pouco esse problema, já que uma nova análise é necessária de qualquer forma).
Os arquivos de suporte de cobertura dependem dos marcadores em um campo implícito para que possam ser substituídos pela política de invocação, o que permite diferentes entre as versões do Bazel. Idealmente, essas seria removida, e padronizamos uma delas.
Também geramos um "relatório de cobertura" que mescla a cobertura coletada para
todos os testes em uma invocação do Bazel. Isso é feito pela
CoverageReportActionFactory
e é chamado em BuildView.createResult()
. Ela
tenha acesso às ferramentas necessárias no :coverage_report_generator
do primeiro teste que é executado.
O mecanismo de consulta
O Bazel tem pouco linguagem usada para fazer várias perguntas sobre diversos gráficos. Os seguintes tipos de consulta são fornecidos:
- O
bazel query
é usado para investigar o gráfico de destino. - O
bazel cquery
é usado para investigar o gráfico de destino configurado. - O
bazel aquery
é usado para investigar o gráfico de ações
Cada um deles é implementado com a subclasse AbstractBlazeQueryEnvironment
.
Outras funções de consulta podem ser feitas criando subclasses de QueryFunction
, Para permitir o streaming de resultados de consulta, em vez de coletá-los para alguns
estrutura de dados, um query2.engine.Callback
é transmitido para QueryFunction
, que
chama para resultados que quer retornar.
O resultado de uma consulta pode ser emitido de várias maneiras: rótulos, rótulos e regras
classes, XML, protobuf e assim por diante. Eles são implementados como subclasses
OutputFormatter
:
Um requisito sutil de alguns formatos de saída de consulta (proto, com certeza) é que O Bazel precisa emitir _todas__as informações fornecidas pelo carregamento do pacote para que é possível diferenciar a saída e determinar se um objetivo específico mudou. Como consequência, os valores dos atributos precisam ser serializáveis, e é por isso que não são poucos tipos de atributos sem nenhum atributo que tenha Starlark complexo e a distribuição dos valores dos dados. A solução alternativa comum é usar um rótulo e anexar o arquivo informações à regra com esse rótulo. Não é uma solução alternativa muito satisfatória e seria ótimo remover esse requisito.
O sistema de módulos
O Bazel pode ser estendido adicionando módulos a ele. Cada módulo precisa criar uma subclasse
BlazeModule
(o nome é uma relíquia da história do Bazel quando era
chamado Blaze) e obtém informações sobre vários eventos durante a execução de
um comando.
Eles são usados principalmente para implementar várias partes "não essenciais" funcionalidade que apenas algumas versões do Bazel (como a que usamos no Google) precisam de:
- Interfaces para sistemas de execução remota
- Novos comandos
O conjunto de pontos de extensão que o BlazeModule
oferece é um pouco perigoso. O que não fazer
usá-lo como um exemplo de bons princípios de design.
O ônibus de eventos
A principal maneira de o BlazeModules se comunicar com o restante do Bazel é por um barramento de eventos
(EventBus
): uma nova instância é criada para cada build, várias partes do Bazel.
pode postar eventos nele e os módulos podem registrar listeners para os eventos aos quais estão
interesse. Por exemplo, os seguintes itens são representados como eventos:
- A lista de destinos de build a serem compilados foi determinada
(
TargetParsingCompleteEvent
) - As configurações de nível superior foram determinadas
(
BuildConfigurationEvent
) - Um destino foi criado, com êxito ou não (
TargetCompleteEvent
) - Um teste foi executado (
TestAttempt
,TestSummary
)
Alguns desses eventos são representados fora do Bazel no
Protocolo de evento de build
(são BuildEvent
s). Isso permite não apenas BlazeModule
s, mas também itens
fora do processo do Bazel para observar o build. Eles são acessíveis como um
com mensagens de protocolo, ou o Bazel pode se conectar a um servidor (chamado
o serviço de evento de build) para transmitir eventos.
Isso é implementado nas APIs build.lib.buildeventservice
e
build.lib.buildeventstream
pacotes Java.
Repositórios externos
Já o Bazel foi originalmente projetado para ser usado em um monorepo (uma única fonte que contém tudo o que é preciso para criar), o Bazel vive em um mundo onde isso não é necessariamente verdade. "Repositórios externos" são uma abstração usada para unir esses dois mundos: eles representam o código necessário para o build, mas não está na árvore de origem principal.
O arquivo WORKSPACE
O conjunto de repositórios externos é determinado pela análise do arquivo do ESPAÇO DE TRABALHO. Por exemplo, uma declaração como esta:
local_repository(name="foo", path="/foo/bar")
O resultado no repositório chamado @foo
está disponível. Onde isso acontece
complicado é que se pode definir novas regras de repositório em arquivos Starlark, que
pode ser usado para carregar um novo código Starlark, que pode ser usado para definir novos
regras de repositório etc.
Para lidar com esse caso, a análise do arquivo WORKSPACE (em
WorkspaceFileFunction
) é dividida em blocos delineados por load()
.
declarações. O índice do bloco é indicado por WorkspaceFileKey.getIndex()
e
computar WorkspaceFileFunction
até que o índice X significa avaliá-lo até que
Xa instrução load()
.
Como buscar repositórios
Antes que o código do repositório seja disponibilizado para o Bazel, ele precisa ser
buscados. Com isso, o Bazel cria um diretório em
$OUTPUT_BASE/external/<repository name>
:
A busca do repositório acontece com estas etapas:
- O
PackageLookupFunction
percebe que precisa de um repositório e cria umRepositoryName
como umSkyKey
, que invocaRepositoryLoaderFunction
RepositoryLoaderFunction
encaminha a solicitação paraRepositoryDelegatorFunction
por motivos pouco claros (o código diz que evitar rebaixar os dados no caso de reinicializações do Skyframe, mas não raciocínio muito sólido)- O
RepositoryDelegatorFunction
descobre a regra de repositório solicitada busque iterando as partes do arquivo WORKSPACE até que o repositório for encontrado - Encontramos o
RepositoryFunction
apropriado que implementa o repositório fetching; é a implementação do repositório pelo Starlark mapa codificado para repositórios implementados em Java.
Há várias camadas de armazenamento em cache, já que buscar um repositório pode ser caras:
- Há um cache para arquivos baixados que são codificados pela soma de verificação deles.
(
RepositoryCache
). Isso exige que a soma de verificação esteja disponível no WORKSPACE, mas ainda assim é bom para hermética. Isto é compartilhado por todas as instâncias do servidor Bazel na mesma estação de trabalho, espaço de trabalho ou na base de saída em que estão sendo executados. - Um "arquivo de marcador" é gravado para cada repositório em
$OUTPUT_BASE/external
que contém uma soma de verificação da regra que foi usada para buscá-lo. Se o Bazel o servidor for reiniciado, mas a soma de verificação não mudar, ele não será buscado novamente. Isso é implementado emRepositoryDelegatorFunction.DigestWriter
. - A opção de linha de comando
--distdir
designa outro cache usado para procurar artefatos para download. Isso é útil em ambientes corporativos em que ele não deve buscar coisas aleatórias da Internet. Isso é implementado porDownloadManager
.
Após o download de um repositório, os artefatos dele são tratados como origem
artefatos. Isso pode ser um problema porque o Bazel geralmente verifica se há atualizações
de artefatos de origem chamando stat() neles, e esses artefatos também são
invalidado quando a definição do repositório em que ele se encontra muda. Assim,
As FileStateValue
s de um artefato em um repositório externo precisam depender de
repositório externo. Isso é gerenciado por ExternalFilesHelper
.
Diretórios gerenciados
Às vezes, os repositórios externos precisam modificar arquivos na raiz do espaço de trabalho (como um gerenciador de pacotes que hospeda os pacotes baixados em um subdiretório de a árvore de origem). Isso vai contra a suposição de que o Bazel faz essa fonte são modificados somente pelo usuário, e não por si só, e permite que os pacotes refere-se a todos os diretórios na raiz do espaço de trabalho. Para tornar esse tipo de trabalho de repositório externo, o Bazel faz duas coisas:
- Permite que o usuário especifique subdiretórios do espaço de trabalho em que o Bazel não é
podem alcançar. Elas estão listadas em um arquivo chamado
.bazelignore
e a funcionalidade é implementada emBlacklistedPackagePrefixesFunction
. - Codificamos o mapeamento do subdiretório do espaço de trabalho para o
repositório pelo qual ele é processado em
ManagedDirectoriesKnowledge
e processarFileStateValue
s que se referem a eles da mesma forma que os usuários repositórios externos.
Mapeamentos de repositório
Pode acontecer de vários repositórios quererem depender do mesmo repositório,
mas em versões diferentes (esta é uma instância do bloco de
problema). Por exemplo, se dois binários em repositórios separados no build
dependerem do Guava, ambos provavelmente se referirão ao Guava com rótulos
começando @guava//
e esperam que isso signifique versões diferentes dele.
Assim, o Bazel permite remapear rótulos de repositórios externos para que
A string @guava//
pode se referir a um repositório Guava (como @guava1//
) no
de um binário e outro repositório Guava (como @guava2//
) do
repositório do outro.
Como alternativa, isso também pode ser usado para mesclar diamantes. Se um repositório
depende de @guava1//
e outro depende de @guava2//
, o mapeamento de repositório
permite remapear os dois repositórios para usar um repositório @guava//
canônico.
O mapeamento é especificado no arquivo WORKSPACE como o atributo repo_mapping
.
de definições de repositório individuais. Em seguida, ele aparece no Skyframe como um membro
WorkspaceFileValue
, onde é responsável por:
Package.Builder.repositoryMapping
, que é usado para transformar os rótulos com valor atributos de regras no pacote porRuleClass.populateRuleAttributeValues()
Package.repositoryMapping
, que é usado na fase de análise (por resolver problemas como$(location)
, que não são analisados no carregamento fase)BzlLoadFunction
para resolver rótulos em instruções load()
Bits JNI
O servidor do Bazel é escrito principalmente em Java. A exceção são as partes que O Java não funciona sozinho ou não funciona sozinho quando o implementamos. Isso é limitado principalmente à interação com o sistema de arquivos, o controle de processo e várias outras coisas de baixo nível.
O código C++ reside em src/main/native e as classes Java com código nativo são:
NativePosixFiles
eNativePosixFileSystem
ProcessUtils
WindowsFileOperations
eWindowsFileProcesses
com.google.devtools.build.lib.platform
Saída do console
Emitir saída do console parece algo simples, mas a confluência de executar vários processos (às vezes remotamente), armazenamento em cache detalhado, a vontade de uma boa saída de terminal colorida e um servidor de longa duração não trivial.
Logo após a chamada RPC chegar do cliente, dois RpcOutputStream
instâncias são criadas (para stdout e stderr) que encaminham os dados impressos para
ao cliente. Eles são agrupados em OutErr
(stdout, stderr)
). Qualquer item que precise ser impresso no console passa por
córregos. Em seguida, esses streams são transferidos
BlazeCommandDispatcher.execExclusively()
:
A saída é impressa por padrão com sequências de escape ANSI. Quando estes não são
desejados (--color=no
), eles serão removidos por uma AnsiStrippingOutputStream
. Em
Além disso, System.out
e System.err
são redirecionados a esses streams de saída.
Dessa forma, as informações de depuração podem ser impressas usando
System.err.println()
e ainda vão para a saída do terminal do cliente
(diferente do código do servidor). É tomado o cuidado de que, se um processo
produz saída binária (como bazel query --output=proto
), sem mostrar stdout.
acontecer.
Mensagens curtas (erros, avisos e similares) são expressas por meio do
interface EventHandler
. Eles são diferentes daqueles que se publicam no
o EventBus
(isso é confuso). Cada Event
tem um EventKind
(erro,
aviso, informações e alguns outros) e podem ter um Location
(o lugar em
o código-fonte que causou o evento).
Algumas implementações de EventHandler
armazenam os eventos recebidos. Isso é usado
para reproduzir informações na interface do usuário causadas por vários tipos de processamento em cache,
como os avisos emitidos por um destino configurado em cache.
Algumas EventHandler
s também permitem postar eventos que, com o tempo, acabam se conectando
o barramento de eventos (os Event
s normais _não _aparecem lá). São
implementações de ExtendedEventHandler
, e o principal uso delas é repetir conteúdo armazenado em cache
EventBus
. Todos esses eventos EventBus
implementam Postable
, mas não
tudo o que é postado no EventBus
necessariamente implementa essa interface.
somente aqueles armazenados em cache por um ExtendedEventHandler
(seria bom
a maioria das coisas faz; mas não é aplicado)
A saída do terminal é principalmente emitida pelo UiEventHandler
, que é
responsável por toda a sofisticada formatação de saída e os relatórios de progresso do Bazel
faz. Ela tem duas entradas:
- O ônibus de eventos
- O fluxo do evento era direcionado pelo Repórter
A única conexão direta que o equipamento de execução de comando (por exemplo, o restante do
Bazel) tem para o stream de RPC para o cliente é por Reporter.getOutErr()
,
que permite acesso direto a eles. Ela só é usada quando um comando precisa
para fazer o despejo de grandes quantidades de dados binários possíveis (como bazel query
).
Como criar o perfil do Bazel
O Bazel é rápido. Ele também é lento, porque os builds tendem a crescer até que
o limite do que é tolerável. Por isso, o Bazel inclui um criador de perfil que pode ser
usada para criar o perfil de builds e do próprio Bazel. Ela é implementada em uma classe
chamado Profiler
. Ele é ativado por padrão, mas só grava
dados resumidos para que sua sobrecarga seja tolerável; A linha de comando
O --record_full_profiler_data
faz com que ele grave tudo o que pode.
Ele emite um perfil no formato Chrome Profiler. a melhor visualização dele no Chrome. Seu modelo de dados é o de pilhas de tarefas: é possível iniciar e encerrar tarefas e elas precisam estar bem aninhadas umas nas outras. Cada linha de execução Java recebe a própria pilha de tarefas. TODO: como isso funciona com ações e estilo de transmissão de continuação?
O criador de perfil é iniciado e interrompido em BlazeRuntime.initProfiler()
e
BlazeRuntime.afterCommand()
, respectivamente, e tenta permanecer ativo por tanto tempo
possível para que possamos criar o perfil de tudo. Para adicionar algo ao perfil,
chame Profiler.instance().profile()
. Ela retorna um Closeable
, com um fechamento
representa o fim da tarefa. É melhor usado com recursos "testar com recursos"
declarações.
Também fazemos perfis de memória rudimentares em MemoryProfiler
. Também está sempre ativado
e registra principalmente o tamanho máximo de heap e o comportamento do GC.
Como testar o Bazel
O Bazel tem dois tipos principais de testes: aqueles que o observam como uma "caixa preta". e aqueles que só executam a fase de análise. Chamamos os antigos de "testes de integração", e o último "teste de unidade", embora sejam mais como testes de integração que são menos integradas. Também temos alguns testes de unidade reais, nos quais eles necessários.
Há dois tipos de testes de integração:
- Aqueles implementados usando uma estrutura de teste bash muito elaborada em
src/test/shell
- Aquelas implementadas em Java. Eles são implementados como subclasses
BuildIntegrationTestCase
O BuildIntegrationTestCase
é o framework de teste de integração recomendado, já que
é bem equipado para a maioria dos cenários de teste. Por se tratar de um framework Java,
oferece capacidade de depuração e integração total com vários recursos comuns de
ferramentas de visualização. Há muitos exemplos de classes BuildIntegrationTestCase
Repositório do Bazel.
Os testes de análise são implementados como subclasses de BuildViewTestCase
. Há um
um sistema de arquivos de rascunho que você pode usar para escrever arquivos BUILD
, além de diversas funções
podem solicitar destinos configurados, alterar a configuração e declarar
várias coisas sobre o resultado da análise.