Esta página discute o que são os sistemas de build, o que eles fazem, por que você deve usar um sistema de build e por que os compiladores e scripts de build não são a melhor escolha à medida que sua organização começa a crescer. 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 são apenas para códigos criados por humanos. Eles também permitem que as máquinas criem builds automaticamente, seja para testes ou para lançamentos em produção. Em uma organização com milhares de engenheiros, é comum que a maioria dos builds seja acionada automaticamente, e não diretamente pelos engenheiros.
Não posso 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 ao aprender a programar: a maioria começa invocando ferramentas
como gcc
ou javac
diretamente da linha de comando ou o equivalente em um
ambiente de desenvolvimento integrado (IDE). Desde 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 pegar todos os arquivos de origem Java no diretório atual e transformá-los em um arquivo de classe binário. 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 em subdiretórios do diretório atual para encontrar o código a ser
importado. Mas não há como encontrar o código armazenado em outras partes do
sistema de arquivos (talvez uma biblioteca compartilhada por vários projetos). Ele 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 em que o código depende e criar essas peças na ordem correta, possivelmente usando um conjunto diferente de ferramentas para cada peça. Se alguma dependência mudar, você precisará repetir esse processo para evitar depender de binários desatualizados. Para uma base de código de tamanho moderado, esse processo rapidamente se torna tedioso e propenso a erros.
O compilador também não sabe como processar dependências
externas, como arquivos JAR
de terceiros em Java. Sem um sistema de build,
é possível fazer 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 desse 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 de hobby comece simples o suficiente para que você possa criá-lo usando apenas um compilador, mas você começa a encontrar 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 de shell simples que cuidam de criar 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. A depuração de scripts de shell é dolorosa, com cada vez mais hacks sendo sobrepostos uns aos outros.
É 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 partes precisam ser reconstruídas a cada vez, mas volta para a estaca zero.
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 fazer upload e enviar para o repositório central. E crie e envie as atualizações da documentação e envie uma notificação aos usuários. Talvez seja necessário outro script...
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ões, 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 corretamente e depois esqueceu?
Apesar dos problemas, seu projeto é bem-sucedido o suficiente para que você possa contratar mais engenheiros. Agora você sabe que não é preciso um desastre para que os problemas anteriores surjam. Você precisa passar pelo mesmo processo de inicialização doloroso sempre que um novo desenvolvedor entra na 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. Em teoria, isso é tão simples quanto comprar um computador novo e configurá-lo para executar o script de build toda noite 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.
As builds ficam mais lentas à 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ê precisar coordenar vários desenvolvedores e as máquinas deles, nem mesmo um script de build perfeito será suficiente, porque fica muito difícil explicar as pequenas diferenças nessas máquinas. Nesse ponto, essa abordagem simples falha e é hora de investir em um sistema de build real.