Por que um sistema de compilação?

Esta página discute o que são sistemas de build, o que eles fazem, por que usar um sistema de build e por que compiladores e scripts de build não são a melhor escolha quando sua organização começa a escalonar. Ele é destinado a desenvolvedores que não têm muita experiência com um sistema de build.

O que é um sistema de build?

Basicamente, todos os sistemas de build têm um objetivo simples: transformar o código-fonte escrito por engenheiros em binários executáveis que podem ser lidos pelas máquinas. Os sistemas de build não servem apenas para código criado por humanos. Eles também permitem que as máquinas criem builds automaticamente, seja para testes ou lançamentos para produção. Em uma organização com milhares de engenheiros, é comum que a maioria dos builds seja acionada automaticamente em vez de diretamente por engenheiros.

Não posso só usar um compilador?

A necessidade de um sistema de build pode não ser óbvia de imediato. A maioria dos engenheiros não usa um sistema de build para aprender a programar: a maioria começa invocando ferramentas como gcc ou javac diretamente na linha de comando ou o equivalente em um ambiente de desenvolvimento integrado (IDE). Contanto que todo o código-fonte esteja no mesmo diretório, um comando como este funciona bem:

javac *.java

Isso instrui o compilador Java a transformar todos os arquivos de origem Java no diretório atual em um arquivo de classe binária. No caso mais simples, isso é tudo que você precisa.

No entanto, assim que o código se expande, as complicações começam. javac é inteligente o suficiente para procurar nos subdiretórios do diretório atual um código a ser importado. No entanto, ela não oferece como encontrar o código armazenado em outras partes do sistema de arquivos (talvez uma biblioteca compartilhada por vários projetos). Ela também só sabe como criar código Java. Sistemas grandes geralmente envolvem partes diferentes escritas em várias linguagens de programação com redes de dependências entre essas partes. Ou seja, nenhum compilador para uma única linguagem pode criar todo o sistema.

Ao lidar com código de várias linguagens ou várias unidades de compilação, a criação do código não é mais um processo de uma etapa. Agora você precisa avaliar do que o código depende e criar essas partes na ordem correta, possivelmente usando um conjunto diferente de ferramentas para cada uma. Se alguma dependência mudar, repita esse processo para evitar a dependência de binários desatualizados. Para uma base de código de tamanho moderado, esse processo se torna rapidamente tedioso e propenso a erros.

O compilador também não sabe nada sobre como lidar com dependências externas, como arquivos JAR de terceiros em Java. Sem um sistema de build, é possível gerenciar isso fazendo o download da dependência da Internet, colocando-a em uma pasta lib no disco rígido e configurando o compilador para ler bibliotecas nesse diretório. Com o tempo, é difícil manter as atualizações, versões e origem dessas dependências externas.

E os scripts de shell?

Suponha que seu projeto hobby comece simples o suficiente para que você possa criá-lo usando apenas um compilador, mas comece a se deparar com alguns dos problemas descritos anteriormente. Talvez você ainda ache que não precisa de um sistema de compilação e possa automatizar as partes tediosas usando alguns scripts de shell simples que cuidam da criação das coisas na ordem correta. Isso ajuda por um tempo, mas logo você começa a ter ainda mais problemas:

  • Isso se torna entediante. À medida que o sistema fica mais complexo, você começa a gastar quase tanto tempo nos scripts de build quanto no código real. Depurar scripts de shell é complicado, com cada vez mais invasões sendo sobrepostas umas às outras.

  • É lento. Para garantir que você não dependa acidentalmente de bibliotecas desatualizadas, o script de build cria todas as dependências em ordem sempre que é executado. Você pensa em adicionar lógica para detectar quais partes precisam ser reconstruídas, mas isso parece muito complexo e propenso a erros para um script. Ou você pensa em especificar quais peças precisam ser reconstruídas a cada vez, mas depois volta ao primeiro ponto.

  • Boas notícias: é hora de um lançamento! É melhor descobrir todos os argumentos necessários para o comando jar para criar seu build final. E lembre-se de como fazer o upload e enviá-lo para o repositório central. Por fim, criar e enviar as atualizações da documentação e enviar uma notificação aos usuários. Hmm, talvez isso precise de outro script...

  • É um desastre! O disco rígido falha e agora você precisa recriar todo o sistema. Você foi inteligente o bastante para manter todos os arquivos de origem no controle de versão, mas e as bibliotecas que você transferiu por download? É possível encontrá-los todos novamente e garantir que eles tenham a mesma versão de quando você fez o download deles pela primeira vez? Seus scripts provavelmente dependiam de ferramentas específicas que estavam sendo instaladas em locais específicos. É possível restaurar o mesmo ambiente para que os scripts funcionem novamente? E todas as variáveis de ambiente que você definiu há muito tempo para fazer o compilador funcionar corretamente e depois se esqueceu?

  • Apesar dos problemas, seu projeto foi bem-sucedido o suficiente para começar a contratar mais engenheiros. Agora, você percebe que não precisa ser um desastre para que os problemas anteriores surjam: é preciso passar pelo mesmo complicado processo de bootstrapping toda vez que um novo desenvolvedor se juntar à sua equipe. E, apesar de todos os esforços, ainda há pequenas diferenças no sistema de cada pessoa. Muitas vezes, o que funciona na máquina de uma pessoa não funciona na outra, e cada vez são necessárias algumas horas de caminhos de ferramentas ou versões de bibliotecas para descobrir a diferença.

  • Você decide automatizar seu sistema de build. Teoricamente, isso é tão simples quanto conseguir um novo computador e configurá-lo para executar o script de compilação todas as noites usando o cron. Você ainda precisa passar pelo difícil processo de configuração, mas agora não tem o benefício de um cérebro humano ser capaz de detectar e resolver pequenos problemas. Todas as manhãs, ao entrar, você vê que o build da noite passada falhou porque ontem um desenvolvedor fez uma mudança que funcionou no sistema, mas não funcionou no sistema de build automatizado. Cada vez que uma solução é simples, mas ela acontece com tanta frequência que você acaba gastando muito tempo todos os dias para descobrir e aplicar essas correções simples.

  • Os builds ficam mais lentos à medida que o projeto cresce. Um dia, enquanto aguarda a conclusão de uma versão, você olha tristemente para a área de trabalho ociosa de um colega de trabalho, que está de férias, e gostaria que houvesse uma maneira de aproveitar todo o poder computacional desperdiçado.

Você se deparou com um problema clássico de escalonamento. Para um único desenvolvedor que trabalha em no máximo algumas centenas de linhas de código por no máximo uma ou duas semanas (o que pode ter sido toda a experiência até agora de um desenvolvedor júnior que acabou de se formar na universidade), um compilador é tudo o que você precisa. Os scripts podem levar você um pouco mais. Mas assim que você precisa coordenar vários desenvolvedores e as máquinas deles, nem mesmo um script de compilação perfeito não é suficiente, porque se torna muito difícil considerar as pequenas diferenças nessas máquinas. Neste ponto, essa abordagem simples é dividida e é hora de investir em um sistema de build real.