Sistemas de compilação baseados em artefatos

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

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 eles 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 acontece com sistemas de build baseados em tarefas, sistemas de build baseados em artefatos, como Bazel, ainda têm arquivos de build, mas o conteúdo deles é muito diferente. Em vez do que ser um conjunto imperativo de comandos em uma linguagem de script de Turing completa que descrevem como produzir uma saída, os arquivos de build no Bazel são uma que descreve um conjunto de artefatos a serem criados, suas dependências e um um conjunto limitado de opções que afetam 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 para execução, pode oferecer garantias muito mais sólidas mais eficiente e ainda garantir a correção.

Uma perspectiva funcional

É fácil fazer uma analogia entre sistemas de build baseados em artefatos e programação. Linguagens de programação imperativas tradicionais (como Java, C e Python) especificam listas de instruções a serem executadas uma após a outra, no Da mesma forma que os sistemas de compilação baseados em tarefas permitem que os programadores definam uma série de etapas executar. 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 mapeia a ideia de declarar um manifesto em um sistema de build baseado em artefatos. e permitindo que o sistema descubra como executar o build. Muitos problemas não podem podem ser expressas facilmente usando programação funcional, mas aquelas que se beneficiam é muito grande: a linguagem costuma ser capaz de carregar trivialmente essas programas e dar garantias sobre sua correção, que seriam impossível 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. Isso é exatamente o que é um sistema de build: todo o sistema é efetivamente 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. Júlio é 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. Toda meta tem:

  • name: como o destino é referenciado na linha de comando e por outros destinos
  • srcs: 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ê executa builds usando a linha de comando do Bazel. . Para criar o destino MyBinary, execute bazel build :MyBinary. Depois digitando esse comando pela primeira vez em um repositório limpo, o Bazel:

  1. Analisa cada arquivo BUILD no espaço de trabalho para criar um gráfico de dependências. entre os artefatos.
  2. Usa o gráfico para determinar as dependências transitivas de MyBinary. que todos os destinos de que MyBinary depende e todos os destinos dos quais os destinos dependem de maneira recursiva.
  3. Cria cada uma dessas dependências, em ordem. O Bazel começa criando destino que não tem outras dependências e que monitora quais dependências ainda precisam ser construídos para cada destino. Assim que todos os são criadas, o Bazel começa a criar o destino. Esse processo continua até que cada uma das dependências transitivas de MyBinary seja construído.
  4. Cria MyBinary para produzir um binário executável final que vincula todos os as dependências que foram criadas na etapa 3.

Pode não parecer que o que está acontecendo aqui diferente do que acontecia com um sistema de build baseado em tarefas. De fato, a o resultado final é o mesmo binário, e o processo de produção envolveu analisar várias etapas para encontrar dependências entre elas e executar essas etapas em ordem. Mas existem diferenças críticas. 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á para recriar 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. Ela pode usar esse conhecimento para tornar o build muito mais eficiente carregando o build em paralelo e reutilizar os resultados. Mas este é realmente apenas o primeiro passo, e esses elementos básicos de paralelismo e reutilização formam a base de um modelo 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 antes que não abordamos. O Bazel tem recursos inteligentes maneiras de resolver cada uma delas, e devemos discuti-las antes de prosseguir.

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 seu projeto usa linguagens que exigem ferramentas diferentes com base em quais plataforma em que o conteúdo está sendo desenvolvido ou para a qual ele está sendo compilado (por exemplo, Windows versus Linux), e cada uma dessas plataformas requer um conjunto de ferramentas um pouco diferente para fazer a mesmo job.

O Bazel resolve a primeira parte desse problema tratando as ferramentas como dependências para cada alvo. 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 uma 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 alterações, cada artefato que depende dele é reconstruído.

O Bazel resolve a segunda parte do problema, a independência da plataforma, definindo configurações do build. Em vez de de acordo com as ferramentas, eles dependem dos 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 como adicionar regras personalizadas.

Para definir uma regra no Bazel, o autor da regra declara as entradas que ela exige (na forma de atributos transmitidos no arquivo BUILD) e o de saída 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 realizar o que quiser, desde que use apenas as entradas e saídas declaradas, e Ele programa ações e armazena os resultados em cache 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 frequência, na prática, e forçar os as possibilidades de abuso até o nível de ação diminuem muito oportunidades para erros. As regras compatíveis com muitas linguagens e ferramentas comuns são amplamente disponível on-line, e a maioria dos projetos nunca precisará definir seus próprios regras de firewall. Mesmo para aqueles que o fazem, as definições de regra só precisam ser definidas em um um lugar centralizado no repositório, o que significa que a maioria dos engenheiros poderá usar essas regras sem se preocupar com a implementação.

Como isolar o ambiente

Parece que as ações podem ter os mesmos problemas que as tarefas de outros sistemas. Ainda não é possível gravar ações que ambos gravam no mesmo e acabam em conflito entre si? Na verdade, o Bazel faz essas impossíveis devido ao sandbox. Compatível cada ação é isolada das demais por um sistema de arquivos, sandbox Efetivamente, cada ação pode ter apenas uma visualização restrita do sistema de arquivos que inclui as entradas declaradas e as saídas produzidos. 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 pelo 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 ser alterado a qualquer momento, possivelmente exigindo que o sistema de compilação verifique constantemente se eles são recentes. Se um arquivo remoto for alterado sem uma alteração correspondente no código-fonte do espaço de trabalho, isso pode resultar em builds pode funcionar em um dia e falhar no outro sem motivo óbvio devido a uma mudança de dependência. Por fim, uma dependência externa pode introduzir um risco enorme de propriedade de terceiros: se um invasor for capaz de se infiltrar servidor de terceiros, é possível substituir o arquivo de dependência por algo o próprio design, o que pode dar a eles controle total sobre o build do ambiente de execução e da saída dele.

O problema fundamental é que queremos que o sistema de build esteja ciente desses sem precisar verificar o código-fonte. Como atualizar uma dependência deve ser uma escolha consciente, mas ela deve ser feita apenas uma vez em vez de gerenciados por engenheiros individuais ou automaticamente sistema. Isso porque mesmo com um modelo “Live at Head”, ainda queremos que os builds 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 uma arquivo de manifesto de todo o espaço de trabalho que lista um hash criptográfico para cada no espaço de trabalho. O hash é uma forma concisa de representar sem verificar todo o arquivo no controle de origem. Sempre que um novo dependência externa é referenciada em um espaço de trabalho, o hash dessa dependência é adicionados ao manifesto, seja de forma manual ou automática. 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 do qual fazemos o download tiver um hash diferente daquele declarado no o build 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 há sempre um registro de quando uma dependência foi atualizada e um dependência não pode mudar sem uma alteração correspondente na origem do espaço de trabalho. Isso também significa que, ao verificar uma versão mais antiga do código-fonte, a o build usa as mesmas dependências que usava no momento durante o check-in da versão. Caso contrário, ela falhará se essas dependências forem não estão 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 isso, problema, recomendamos que, para qualquer projeto não trivial, você espelhe todos os dependências em servidores ou serviços em que você confia e controla. 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.