Sistemas de build baseados em tarefas

Relatar um problema Conferir código-fonte Por noite · 7,3 · 7,2 · 7,1 · 7,0 · 6,5

Esta página aborda sistemas de build baseados em tarefas, como eles funcionam e alguns dos e complicações que podem ocorrer com sistemas baseados em tarefas. Depois dos scripts de shell, sistemas de compilação baseados em tarefas são a próxima evolução lógica do desenvolvimento.

Noções básicas sobre sistemas de build baseados em tarefas

Em um sistema de build baseado em tarefas, a unidade fundamental de trabalho é a tarefa. Cada é um script que pode executar qualquer tipo de lógica, e as tarefas especificam outros tarefas como dependências que precisam ser executadas antes delas. A maioria dos principais sistemas de build em uso atualmente, como Ant, Maven, Gradle, Grunt e Rake, são baseados em tarefas. Em vez de scripts de shell, a maioria dos sistemas de build modernos exige que os engenheiros criem arquivos de build que descrevem como executar o build.

Pegue este exemplo do Manual do Ant:

<project name="MyProject" default="dist" basedir=".">
   <description>
     simple example build file
   </description>
   <!-- set global properties for this build -->
   <property name="src" location="src"/>
   <property name="build" location="build"/>
   <property name="dist" location="dist"/>

   <target name="init">
     <!-- Create the time stamp -->
     <tstamp/>
     <!-- Create the build directory structure used by compile -->
     <mkdir dir="${build}"/>
   </target>
   <target name="compile" depends="init"
       description="compile the source">
     <!-- Compile the Java code from ${src} into ${build} -->
     <javac srcdir="${src}" destdir="${build}"/>
   </target>
   <target name="dist" depends="compile"
       description="generate the distribution">
     <!-- Create the distribution directory -->
     <mkdir dir="${dist}/lib"/>
     <!-- Put everything in ${build} into the MyProject-${DSTAMP}.jar file -->
     <jar jarfile="${dist}/lib/MyProject-${DSTAMP}.jar" basedir="${build}"/>
   </target>
   <target name="clean"
       description="clean up">
     <!-- Delete the ${build} and ${dist} directory trees -->
     <delete dir="${build}"/>
     <delete dir="${dist}"/>
   </target>
</project>

O buildfile é escrito em XML e define alguns metadados simples sobre o build. com uma lista de tarefas (as tags <target> no XML). (Ant usa a palavra target para representar uma tarefa e usar a palavra tarefa para se referir a comandos. Cada tarefa executa uma lista de possíveis comandos definidos pelo Ant, que aqui incluem a criação e a exclusão de diretórios, a execução de javac e criar um arquivo JAR. Esse conjunto de comandos pode ser estendido por comandos plug-ins para abranger qualquer tipo de lógica. Cada tarefa também pode definir as tarefas depende do atributo "depends". Essas dependências formam um gráfico acíclico, como mostrado na Figura 1.

Gráfico acrílico mostrando dependências

Figura 1. Gráfico acíclico mostrando dependências

Os usuários realizam builds fornecendo tarefas para a ferramenta de linha de comando do Ant. Por exemplo: quando um usuário digita ant dist, o Ant segue as seguintes etapas:

  1. Carrega um arquivo chamado build.xml no diretório atual e o analisa para crie a estrutura do gráfico mostrado na figura 1.
  2. Procura a tarefa dist fornecida na linha de comando e descobre que tem uma dependência na tarefa chamada compile.
  3. Procura a tarefa chamada compile e descobre que ela tem uma dependência a tarefa chamada init.
  4. Procura a tarefa chamada init e descobre que ela não tem dependências.
  5. Executa os comandos definidos na tarefa init.
  6. Executa os comandos definidos na tarefa compile, já que tudo isso dependências da tarefa tenham sido executadas.
  7. Executa os comandos definidos na tarefa dist, já que tudo isso dependências da tarefa tenham sido executadas.

No final, o código executado pelo Ant ao executar a tarefa dist é equivalente no seguinte script de shell:

./createTimestamp.sh
mkdir build/
javac src/* -d build/
mkdir -p dist/lib/
jar cf dist/lib/MyProject-$(date --iso-8601).jar build/*

Quando a sintaxe é removida, o arquivo de build e o script de compilação, na verdade, não são muito diferentes. Mas já ganhamos muito fazendo isso. Podemos criar novos arquivos de build em outros diretórios e vinculá-los. Podemos facilmente adicionar novas tarefas que dependem de tarefas existentes de maneiras arbitrárias e complexas. Qa você só precisa transmitir o nome de uma única tarefa para a ferramenta de linha de comando ant, e ela determina tudo que precisa ser executado.

Ant é um software antigo, originalmente lançado em 2000. Outras ferramentas, como O Maven e o Gradle melhoraram o Ant nos anos seguintes e essencialmente e o substituiu por recursos como o gerenciamento automático dependências e uma sintaxe mais limpa sem nenhum XML. Mas a natureza desses novos permanece os mesmos: eles permitem que os engenheiros escrevam scripts de build em um com princípios e modulares como tarefas e fornecem ferramentas para a execução dessas tarefas e gerenciar dependências entre eles.

O lado sombrio dos sistemas de build baseados em tarefas

Como essas ferramentas permitem que os engenheiros definam qualquer script como uma tarefa, eles são extremamente poderosos, permitindo que você faça quase tudo o que pode imaginar com eles. Mas esse poder tem desvantagens, e sistemas de compilação baseados em tarefas podem à medida que os scripts de build ficam mais complexos. A algum problema com esses sistemas é que eles acabam concedendo poder demais engenheiros e não há energia suficiente para o sistema. Como o sistema não tem ideia o que os scripts estão fazendo, o desempenho é prejudicado, pois deve ser muito conservador na forma como ele programa e executa as etapas de build. E não há como o sistema para confirmar que cada script está fazendo o que deveria, então os scripts tendem a crescer complexidade e acabar sendo outra coisa que precisa de depuração.

Dificuldade de carregar as etapas de compilação em paralelo

As estações de trabalho de desenvolvimento moderno são bastante potentes, com vários núcleos que são capaz de executar várias etapas de compilação em paralelo. Mas sistemas baseados em tarefas muitas vezes não conseguem paralelizar a execução de tarefas, mesmo quando parece que deveriam ser capazes de fazer. Suponha que a tarefa A dependa das tarefas B e C. Como as tarefas B e C não dependem uns dos outros, é seguro executá-los ao mesmo tempo para que o sistema possa chegar mais rapidamente à tarefa A? Talvez, se eles não tocarem dos mesmos recursos. Mas talvez não; talvez ambos usem o mesmo arquivo para acompanhar os status e executá-los ao mesmo tempo causa um conflito. Não há maneira em geral para o sistema saber, então ele tem que arriscar esses conflitos (levando a problemas de compilação raros, mas muito difíceis de depurar), ou precisa restringir todo o build para execução em uma única linha de execução em um único processo. Isso pode ser um grande desperdício de uma poderosa máquina de desenvolvedor exclui a possibilidade de distribuir o build em várias máquinas.

Dificuldade em executar builds incrementais

Um bom sistema de build permite que os engenheiros executem builds incrementais confiáveis, como para que uma pequena mudança não exija reconstrução de toda a base de código zero. Isso é ainda mais importante se o sistema de build for lento e não conseguir paralelize as etapas de build pelos motivos mencionados. Mas, infelizmente, sistemas de compilação baseados em tarefas também têm dificuldades aqui. Como as tarefas podem fazer qualquer coisa, em geral, não há como verificar se elas já foram concluídas. Muitas tarefas basta executar um compilador para criar um conjunto de arquivos-fonte binários; Assim, eles não precisam ser executados novamente se os arquivos de origem não mudaram. Mas, sem informações adicionais, o sistema não pode dizer isso com certeza. Talvez a tarefa faça o download de um arquivo que poderia ter sido alterado, ou talvez grava um carimbo de data/hora que pode ser diferente em cada execução. Para garantir precisão, o sistema normalmente precisa executar novamente todas as tarefas durante cada compilação. Algumas os sistemas de build tentam ativar builds incrementais permitindo que os engenheiros especifiquem condições sob as quais uma tarefa precisa ser executada novamente. Às vezes isso é viável, mas muitas vezes é um problema muito mais complicado do que parece. Por exemplo, em linguagens como C++, que permitem a inclusão direta de arquivos por outros arquivos, é impossível determinar todo o conjunto de arquivos que devem ser monitorados quanto a alterações sem analisar as origens de entrada. Os engenheiros muitas vezes acabam usando atalhos esses atalhos podem levar a problemas raros e frustrantes em que o resultado de uma tarefa reutilizados mesmo quando não deveriam ser. Quando isso acontece com frequência, os engenheiros o hábito de fazer a limpeza antes de cada build para obter um novo estado, anulando completamente o propósito de ter um build incremental na lugar Descobrir quando uma tarefa precisa ser executada novamente é surpreendentemente sutil e é trabalhos mais bem administrados por máquinas do que por humanos.

Dificuldade em manter e depurar scripts

Por fim, os scripts de compilação impostos pelos sistemas de compilação baseados em tarefas geralmente são apenas difíceis de trabalhar. Embora muitas vezes recebam menos escrutínio, crie scripts são código, assim como o sistema que está sendo criado, e são lugares fáceis para os bugs se esconderem. Aqui estão alguns exemplos de bugs que são muito comuns ao trabalhar com um sistema de build baseado em tarefas:

  • A Tarefa A depende da tarefa B para produzir um arquivo específico como saída. O proprietário da tarefa B não percebe que outras tarefas dependem dela, então muda para em um local diferente. Ele só será detectado se alguém tenta executar a tarefa A e descobre que ela falha.
  • A Tarefa A depende da tarefa B, que depende da tarefa C, que está produzindo uma arquivo específico como saída que é necessária para a tarefa A. O proprietário da tarefa B decide que não precisa mais depender da tarefa C, o que faz com que a tarefa A falha de A, mesmo que a tarefa B não se importe com a tarefa C!
  • O desenvolvedor de uma nova tarefa acidentalmente faz uma suposição máquina que executa a tarefa, como a localização de uma ferramenta ou o valor variáveis de ambiente específicas. A tarefa funciona na máquina, mas falha sempre que outro desenvolvedor testar.
  • Uma tarefa contém um componente não determinista, como o download de um arquivo da Internet ou adicionar um carimbo de data/hora a um build. Agora, as pessoas ficam resultados potencialmente diferentes cada vez que o build é executado, ou seja, os engenheiros nem sempre conseguem reproduzir e corrigir as falhas uns dos outros ou falhas que ocorrem em um sistema de build automatizado.
  • Tarefas com várias dependências podem criar disputas. Se a tarefa A depende da tarefa B e da tarefa C, e as tarefas B e C modificam a mesma arquivo, a tarefa A obtém um resultado diferente dependendo de qual das tarefas B e C terminar primeiro.

Não há uma maneira de uso geral para resolver esses problemas de desempenho, problemas de manutenção no framework baseado em tarefas disposto aqui. Até logo já que os engenheiros podem escrever códigos arbitrários para execução durante o build, o sistema pode não ter informações suficientes para executar builds de forma rápida e corretamente. Para resolver o problema, precisamos tirar energia das mãos de para engenheiros e colocá-los de volta nas mãos do sistema e reconcedê-los função do sistema não como execução de tarefas, mas como produção de artefatos.

Essa abordagem levou à criação de sistemas de build baseados em artefatos, como o Blaze e o Bazel.