Esta página aborda sistemas de build baseados em artefatos e a filosofia por trás deles criação. O Bazel é um sistema de build baseado em artefatos. Enquanto o build baseado em tarefas são um bom passo acima dos scripts de build, eles dão muito poder para engenheiros individuais, permitindo que definam as próprias tarefas.
Os sistemas de build baseados em artefatos têm um pequeno número de tarefas definidas pelo sistema
que os engenheiros podem configurar de maneira limitada. Os engenheiros ainda informam ao sistema
o que criar, mas o sistema de compilação determina como fazer isso. Assim como
os sistemas de build baseados em tarefas, os sistemas de build baseados em artefatos, como o Bazel, ainda
têm arquivos de build, mas o conteúdo deles é muito diferente. Em vez
de ser um conjunto imperativo de comandos em uma linguagem de script Turing-completo
que descreve como produzir uma saída, os arquivos de build no Bazel são um manifesto
declarativo que descreve um conjunto de artefatos a serem criados, as dependências deles e um
conjunto limitado de opções que afetam a maneira como eles são criados. Quando os engenheiros executam bazel
Na linha de comando, elas especificam um conjunto de destinos a serem criados (o quê) e
O Bazel é responsável por configurar, executar e programar a compilação.
etapas (o como). Como o sistema de build agora tem controle total sobre quais
ferramentas executar e quando, ele pode oferecer garantias muito mais fortes que permitem ser muito
mais eficiente, além de garantir a correção.
Uma perspectiva funcional
É fácil fazer uma analogia entre sistemas de build baseados em artefatos e programação funcional. Linguagens de programação imperativas tradicionais (como Java, C e Python) especificam listas de instruções a serem executadas uma após a outra, da mesma forma que os sistemas de build baseados em tarefas permitem que os programadores definam uma série de etapas para execução. Linguagens de programação funcionais (como Haskell e ML) são estruturados como uma série de equações matemáticas. Em linguagens funcionais, o programador descreve uma computação a ser executada, mas deixa os detalhes de quando e exatamente como esse cálculo é executado para o .
Isso corresponde à ideia de declarar um manifesto em um sistema de build baseado em artefatos e permitir que o sistema descubra como executar o build. Muitos problemas não podem ser facilmente expressos usando a programação funcional, mas aqueles que se beneficiam muito dela: a linguagem geralmente é capaz de paralelizar esses programas e fazer garantias fortes sobre a correção deles que seriam impossíveis em uma linguagem imperativa. Os problemas mais fáceis de expressar usando programação funcional são aquelas que simplesmente envolvem a transformação de uma parte de dados para outro usando uma série de regras ou funções. E é exatamente isso que um sistema de build é: todo o sistema é, na verdade, uma função matemática que usa arquivos de origem (e ferramentas como o compilador) como entradas e produz binários como saídas. Portanto, não surpreende que funcione bem para basear um em torno dos princípios da programação funcional.
Noções básicas sobre sistemas de build baseados em artefatos
O sistema de build do Google, o Blaze, foi o primeiro sistema de build baseado em artefatos. O Bazel é a versão de código aberto do Blaze.
Confira como é um arquivo de build (normalmente chamado de BUILD
) no Bazel:
java_binary(
name = "MyBinary",
srcs = ["MyBinary.java"],
deps = [
":mylib",
],
)
java_library(
name = "mylib",
srcs = ["MyLibrary.java", "MyHelper.java"],
visibility = ["//java/com/example/myproduct:__subpackages__"],
deps = [
"//java/com/example/common",
"//java/com/example/myproduct/otherlib",
],
)
No Bazel, os arquivos BUILD
definem destinos. Os dois tipos de destinos aqui são
java_binary
e java_library
. Cada alvo corresponde a um artefato
podem ser criadas pelo sistema: alvos binários produzem binários que podem ser
executada diretamente, e os destinos de biblioteca produzem bibliotecas que podem ser usadas
binários ou outras bibliotecas. Cada destino tem:
name
: como o destino é referenciado na linha de comando e por outros destinossrcs
: os arquivos de origem que serão compilados para criar o artefato do destino.deps
: outros destinos que precisam ser criados antes desse destino e vinculados a
As dependências podem estar dentro do mesmo pacote (como asMyBinary
dependência de :mylib
) ou em um pacote diferente na mesma hierarquia de origem
(como a dependência de mylib
em //java/com/example/common
).
Assim como nos sistemas de build baseados em tarefas, você realiza builds usando a ferramenta de linha de comando
do Bazel. Para criar o destino MyBinary
, execute bazel build :MyBinary
. Depois
de inserir esse comando pela primeira vez em um repositório limpo, o Bazel:
- Analisa cada arquivo
BUILD
no espaço de trabalho para criar um gráfico de dependências. entre os artefatos. - Usa o gráfico para determinar as dependências transitivas de
MyBinary
. que todos os destinos de queMyBinary
depende e todos os destinos dos quais os destinos dependem de maneira recursiva. - Cria cada uma dessas dependências, em ordem. O Bazel começa criando cada
destino que não tem outras dependências e rastreia quais dependências
ainda precisam ser criadas para cada destino. Assim que todas as dependências
de um destino são criadas, o Bazel começa a criar esse destino. Esse processo
continua até que cada uma das dependências transitivas de
MyBinary
seja construído. - Cria
MyBinary
para produzir um binário executável final que vincula todos os as dependências que foram criadas na etapa 3.
Basicamente, pode não parecer que o que está acontecendo aqui é muito diferente do que acontece ao usar um sistema de build baseado em tarefas. O resultado final é o mesmo binário, e o processo de produção envolve analisar várias etapas para encontrar dependências entre elas e, em seguida, executar essas etapas em ordem. Mas há diferenças importantes. A primeira aparece Na etapa 3: como o Bazel sabe que cada destino produz apenas uma biblioteca Java, ele sabe que tudo o que precisa fazer é executar o compilador Java em vez de um definido pelo usuário para que ele saiba que é seguro executar essas etapas em paralelo. Isso pode produzir uma melhoria de desempenho de ordem de magnitude em relação à criação é direcionado um de cada vez em uma máquina com vários núcleos, e isso só é possível porque baseada em artefatos deixa o sistema de build responsável pela própria execução para dar garantias mais sólidas sobre o paralelismo.
No entanto, os benefícios vão além do paralelismo. A próxima coisa que este
nos torna aparentes quando o desenvolvedor digita bazel
build :MyBinary
uma segunda vez sem fazer nenhuma mudança. O Bazel sai em menos
de um segundo com uma mensagem informando que a meta está atualizada. Isso é
possível devido ao paradigma de programação funcional sobre o qual falamos
anteriormente, o Bazel sabe que cada destino é o resultado apenas da execução de um
compilador e sabe que a saída do compilador Java depende apenas
suas entradas. Desde que elas não tenham mudado, a saída pode ser reutilizada.
E essa análise funciona em todos os níveis. Se MyBinary.java
mudar, o Bazel saberá
como reconstruir MyBinary
, mas reutilizar mylib
. Se um arquivo de origem
//java/com/example/common
mudar, o Bazel vai saber que precisa recriar essa biblioteca
mylib
e MyBinary
, mas reutilizam //java/com/example/myproduct/otherlib
.
Como o Bazel conhece as propriedades das ferramentas que executa em cada etapa,
ele é capaz de recriar apenas o conjunto mínimo de artefatos a cada vez, enquanto
garantindo que ele não produz
builds desatualizados.
Reformular o processo de build em termos de artefatos em vez de tarefas é sutil mas poderoso. Ao reduzir a flexibilidade exposta ao programador, o build o sistema saiba mais sobre o que está sendo feito em cada etapa do build. Ele pode usar esse conhecimento para tornar o build muito mais eficiente, paralelizando processos de build e reutilizando as saídas. Mas este é realmente apenas o primeiro passo, e esses elementos básicos de paralelismo e reutilização formam a base para um altamente escalonável e fácil de usar.
Outros truques incríveis do Bazel
Os sistemas de build baseados em artefatos resolvem fundamentalmente os problemas com paralelismo e reutilizar que são inerentes aos sistemas de build baseados em tarefas. Mas ainda há alguns problemas que surgiram anteriormente e que não foram resolvidos. O Bazel tem maneiras inteligentes de resolver cada um deles, e precisamos discutir isso antes de continuar.
Ferramentas como dependências
Um problema que encontramos anteriormente foi que as versões dependiam das ferramentas instaladas na nossa máquina. A reprodução de builds em vários sistemas pode ser difícil versões ou locais da ferramenta. O problema fica ainda mais difícil quando o projeto usa linguagens que exigem ferramentas diferentes com base na plataforma em que estão sendo criadas ou compiladas (como Windows versus Linux), e cada uma dessas plataformas exige um conjunto ligeiramente diferente de ferramentas para fazer a mesma tarefa.
O Bazel resolve a primeira parte desse problema tratando as ferramentas como dependências de
cada destino. Cada java_library
no espaço de trabalho depende implicitamente de um
que tem como padrão um compilador conhecido. Sempre que o Bazel cria um
java_library
, ele verifica se o compilador especificado está disponível
em um local conhecido. Assim como qualquer outra dependência, se o compilador Java
mudar, todos os artefatos que dependem dele serão recriados.
O Bazel resolve a segunda parte do problema, a independência da plataforma, definindo configurações do build. Em vez de depender diretamente das ferramentas, as metas dependem de tipos de configurações:
- Configuração do host: criação de ferramentas que são executadas durante o build
- Configuração de destino: criação do binário solicitado
Como estender o sistema de build
O Bazel vem com objetivos para várias linguagens de programação conhecidas, mas os engenheiros sempre vão querer fazer mais, parte do benefício da tecnologia de sistemas é a flexibilidade deles em dar suporte a qualquer tipo de processo de build e isso seria melhor não abrir mão disso em um sistema de build baseado em artefatos. Felizmente, o Bazel permite que os tipos de destino com suporte sejam estendidos adicionando regras personalizadas.
Para definir uma regra no Bazel, o autor declara as entradas que a regra
requer (na forma de atributos transmitidos no arquivo BUILD
) e o conjunto
fixo de saídas que a regra produz. O autor também define as ações
será gerado por essa regra. Cada ação declara as entradas e saídas,
executa um determinado executável ou grava uma determinada string em um arquivo, podendo ser
conectada a outras ações por meio de entradas e saídas. Isso significa que as ações
são a unidade combinável de nível mais baixo no sistema de build. Uma ação pode fazer
o que quiser, desde que use apenas as entradas e saídas declaradas, e
o Bazel cuida de programar ações e armazenar em cache os resultados conforme apropriado.
O sistema não é infalível, já que não há como impedir um desenvolvedor de ações ao introduzir um processo não determinista como parte sua ação. Mas isso não acontece com muita frequência na prática, e aumentar as possibilidades de abuso até o nível de ação diminui muito as oportunidades de erros. As regras que oferecem suporte a muitas linguagens e ferramentas comuns estão amplamente disponíveis on-line, e a maioria dos projetos nunca precisa definir as próprias regras. Mesmo para aqueles que têm, as definições de regras só precisam ser definidas em um local central no repositório, o que significa que a maioria dos engenheiros poderá usar essas regras sem precisar se preocupar com a implementação.
Como isolar o ambiente
Parece que as ações podem ter os mesmos problemas que as tarefas em outros sistemas. Ainda é possível gravar ações que gravam no mesmo arquivo e acabam entrando em conflito uma com a outra? Na verdade, o Bazel torna esses conflitos impossíveis usando o sandbox. Compatível cada ação é isolada das demais por um sistema de arquivos, sandbox Na prática, cada ação pode acessar apenas uma visualização restrita do sistema de arquivos que inclui as entradas declaradas e as saídas produzidas. Isso é aplicado por sistemas como LXC no Linux, a mesma tecnologia do Docker. Isso significa que é impossível que as ações entrem em conflito outro porque não podem ler arquivos que não declarem, e quaisquer que escrevem, mas não declaram, serão descartados quando a ação termina. Ele também usa sandboxes para restringir a comunicação de ações via na rede.
Como tornar as dependências externas deterministas
Ainda resta um problema: os sistemas de build geralmente precisam fazer o download
dependências (ferramentas ou bibliotecas) de fontes externas,
ao construí-los diretamente. Isso pode ser visto no exemplo pela dependência
@com_google_common_guava_guava//jar
, que faz o download de um arquivo JAR
do Maven.
Depender de arquivos fora do espaço de trabalho atual é arriscado. Esses arquivos podem mudar a qualquer momento, o que pode exigir que o sistema de build verifique constantemente se eles estão atualizados. Se um arquivo remoto mudar sem uma mudança correspondente no código-fonte do espaço de trabalho, isso também poderá levar a builds não reproduzíveis. Um build pode funcionar um dia e falhar no dia seguinte sem motivo aparente devido a uma mudança de dependência que passou despercebida. Por fim, uma dependência externa pode apresentar um grande risco de segurança quando é de propriedade de terceiros: se um invasor conseguir se infiltrar no servidor de terceiros, ele poderá substituir o arquivo de dependência por algo do próprio design, potencialmente dando a ele controle total sobre o ambiente de build e a saída dele.
O problema fundamental é que queremos que o sistema de build esteja ciente desses arquivos sem precisar verificar no controle de origem. Atualizar uma dependência deve ser uma escolha consciente, mas essa escolha precisa ser feita uma vez em um local centralizado, em vez de ser gerenciada por engenheiros individuais ou automaticamente pelo sistema. Isso porque mesmo com um modelo “Live at Head”, ainda queremos versões ser determinista, o que implica que, se você conferir um commit da última semana, você verá as dependências como estavam antes, e não como estão agora.
O Bazel e alguns outros sistemas de build resolvem esse problema exigindo um arquivo de manifesto em todo o espaço de trabalho que lista um hash criptográfico para cada dependência externa no espaço de trabalho. O hash é uma forma concisa de representar sem verificar todo o arquivo no controle de origem. Sempre que uma nova dependência externa é referenciada em um espaço de trabalho, o hash dessa dependência é adicionado ao manifesto, manualmente ou automaticamente. Quando o Bazel executa uma build, ele verifica o hash real da dependência armazenada em cache em relação ao valor definido no manifesto e fará o download novamente do arquivo somente se o hash for diferente.
Se o artefato que você fizer o download tiver um hash diferente do declarado no manifesto, o build vai falhar, a menos que o hash no manifesto seja atualizado. Isso pode ser feita automaticamente, mas essa mudança deve ser aprovada e verificada o controle de origem antes que a compilação aceite a nova dependência. Isso significa que sempre há um registro de quando uma dependência foi atualizada, e uma dependência externa não pode mudar sem uma mudança correspondente na origem do espaço de trabalho. Isso também significa que, ao conferir uma versão mais antiga do código-fonte, o build tem garantia de usar as mesmas dependências que estava usando no momento em que a versão foi verificada. Caso contrário, ele vai falhar se essas dependências não estiverem mais disponíveis.
Claro, isso ainda pode ser um problema se um servidor remoto ficar indisponível ou começa a disponibilizar dados corrompidos, o que pode fazer com que todos os seus builds apresentem falhas caso você não tenha outra cópia dessa dependência disponível. Para evitar esse problema, recomendamos que, para qualquer projeto não trivial, você espelhe todas as dependências em servidores ou serviços confiáveis e controlados. Caso contrário, sempre dependerão de terceiros para os componentes do seu sistema de build disponibilidade, mesmo que os hashes de check-in garantam sua segurança.