Por que um sistema de compilação?

Esta página discute o que são sistemas de build, o que eles fazem, por que eles são necessários e por que compiladores e scripts de build não são as melhores opções à medida que sua organização começa a escalonar. É destinado a desenvolvedores que não têm muita experiência com um sistema de build.

O que é um sistema de build?

Essencialmente, todos os sistemas de compilação têm um objetivo direto: transformar o código-fonte escrito por engenheiros em binários executáveis que podem ser lidos por máquinas. Os sistemas de build não são 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, e não diretamente por engenheiros.

Não posso só usar um compilador?

A necessidade de um sistema de build pode não ser óbvia. A maioria dos engenheiros não usa um sistema de build enquanto aprende 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, na sigla em inglês). 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 cada arquivo de origem Java do diretório atual em um arquivo de classe binário. 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 em subdiretórios do diretório atual e encontrar código a ser importado. Mas ele não tem 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 teias de dependências entre elas, o que significa que nenhum compilador para uma única linguagem poderá criar todo o sistema.

Quando você estiver lidando com código de várias linguagens ou unidades de compilação, criar o código não será mais um processo de uma etapa. Agora você precisa avaliar do que seu código depende e criar essas peças na ordem adequada, possivelmente usando um conjunto diferente de ferramentas para cada parte. Se alguma dependência mudar, você precisará repetir esse processo para evitar a dependência de binários desatualizados. Para uma base de código até mesmo de tamanho moderado, esse processo se torna rapidamente tedioso e propenso a erros.

O compilador também não sabe nada sobre como processar 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, mantendo-a em uma pasta lib no disco rígido e configurando o compilador para ler bibliotecas desse diretório. Com o tempo, é difícil manter as atualizações, as versões e a origem dessas dependências externas.

E os scripts de shell?

Suponha que seu projeto de hobby comece simples o suficiente para ser criado usando apenas um compilador, mas você 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 é tedioso. À medida que o sistema fica mais complexo, você começa a gastar quase tanto tempo trabalhando nos scripts de build quanto no código real. A depuração de scripts de shell é difícil, com cada vez mais invasões sendo colocadas umas sobre as 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 ele é executado. Pense em adicionar lógica para detectar quais partes precisam ser recriadas, mas isso soa muito complexo e propenso a erros para um script. Ou você pensa em especificar quais partes precisam ser recriadas a cada vez, mas volta ao ponto principal.

  • Boas notícias: é hora de um lançamento! É melhor descobrir todos os argumentos que você precisa transmitir ao comando jar para criar o build final. Lembre-se de como fazer o upload e enviá-lo para o repositório central. E criar e enviar as atualizações da documentação por push e enviar uma notificação aos usuários. Hum, talvez isso precise de outro script...

  • É um desastre! Seu disco rígido falha, e agora você precisa recriar todo o sistema. Você foi inteligente o suficiente para manter todos os arquivos de origem no controle de versão, mas e quanto às bibliotecas que transferiu por download? Consegue encontrar 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 instaladas em locais específicos. É possível restaurar o mesmo ambiente para que os scripts funcionem novamente? E quanto a todas aquelas variáveis de ambiente que você definiu há muito tempo para fazer o compilador funcionar bem e depois esqueceu?

  • Apesar dos problemas, seu projeto tem sucesso o suficiente para que você comece a contratar mais engenheiros. Agora você percebe que os problemas anteriores não precisam de desastre. É preciso passar pelo mesmo processo dolorido de bootstrap toda vez que um novo desenvolvedor entra na equipe. Apesar de todos os nossos esforços, ainda existem pequenas diferenças no sistema de cada pessoa. Muitas vezes, o que funciona na máquina de uma pessoa não funciona na máquina de outra e, toda vez, leva algumas horas de caminhos da ferramenta de depuração ou versões de biblioteca para descobrir a diferença.

  • Você decide que precisa automatizar seu sistema de compilação. Teoricamente, isso é tão simples quanto adquirir um novo computador e configurá-lo para executar o script de compilação todas as noites usando o cron. Você ainda precisa passar pelo complicado processo de configuração, mas agora não tem a vantagem de um cérebro humano ser capaz de detectar e resolver problemas menores. Agora, 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, é uma solução simples, mas acontece com tanta frequência que você acaba gastando muito tempo todos os dias descobrindo e aplicando essas correções simples.

  • As versões ficam mais lentas e mais lentas à medida que o projeto cresce. Um dia, enquanto aguarda a conclusão de uma criação, você olha com tristeza para a área de trabalho inativa do seu colega de trabalho, que está de férias, e gostaria de poder aproveitar toda essa capacidade computacional desperdiçada.

Você se deparou com um problema clássico de escala. 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 que você precisa. Os scripts podem levá-lo um pouco mais longe. Mas, assim que você precisa coordenar vários desenvolvedores e as máquinas deles, mesmo um script de compilação perfeito não é suficiente, porque fica muito difícil explicar as pequenas diferenças nessas máquinas. Neste ponto, essa abordagem simples é detalhada, e é hora de investir em um sistema de build real.