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 propósito simples: eles transformam 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 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 o 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 diferentes partes escritas em
várias linguagens de programação com redes de dependências entre elas,
o que significa que nenhum compilador de uma única linguagem pode criar o sistema inteiro.
Quando você lida com código de várias linguagens ou várias unidades de compilação, o processo de build 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 a 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 não ache que precisa de um sistema de build e possa automatizar as partes tediosas usando alguns scripts simples que cuidam da criação de coisas na ordem correta. Isso ajuda por um tempo, mas logo você começa a ter ainda mais problemas:
Isso se torna 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. Depurar scripts de shell é trabalhoso, 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 você o executa. Você pensa em adicionar alguma 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 lançar! É melhor descobrir todos os argumentos que você precisa transmitir ao comando jar para criar o 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...
Desastre! O 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 as bibliotecas que você fez o download? Você consegue encontrá-los todos novamente e verificar se eles têm a mesma versão que quando foram transferidos? Seus scripts provavelmente dependiam de ferramentas específicas instaladas em lugares específicos. Você pode restaurar esse mesmo ambiente para que os scripts funcionem novamente? E todas aquelas variáveis de ambiente que você definiu há muito tempo para fazer o compilador funcionar perfeitamente e depois 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 de outra, e cada vez leva algumas horas de caminhos de ferramentas de depuração ou versões de biblioteca para descobrir onde está a diferença.
Você decide que precisa automatizar o 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 processo de configuração, mas agora não tem o benefício de um cérebro humano detectar e resolver problemas menores. Agora, toda manhã, quando você entra no trabalho, percebe que o build da noite anterior falhou porque um desenvolvedor fez uma mudança que funcionou no sistema dele, mas não no sistema de build automatizado. Cada vez é uma correção simples, mas acontece com tanta frequência que você acaba gastando muito tempo todos os dias descobrindo e aplicando essas correções simples.
Os builds ficam mais lentos à medida que o projeto cresce. Um dia, enquanto espera a conclusão de um build, você olha com tristeza para o computador inativo do colega, que está de férias, e gostaria de ter uma maneira de aproveitar toda a capacidade computacional desperdiçada.
Você se deparou com um problema clássico de escala. Para um único desenvolvedor que trabalha em até 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 ajudar 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. Nesse ponto, essa abordagem simples falha e é hora de investir em um sistema de build real.