1 pontos por GN⁺ 2024-09-22 | 2 comentários | Compartilhar no WhatsApp
  • Mesmo com o surgimento constante de novas ferramentas de build, o Makefile continua útil como uma ferramenta básica e leve para reunir tarefas repetitivas de um projeto
  • Graças a comandos convencionais como make, make build, make install e make dev, é possível tentar rapidamente compilar, instalar e executar o servidor de desenvolvimento mesmo em um projeto desconhecido
  • Seja com Jekyll, Hugo, 11ty, frameworks web em Python, Docker ou gulp, o Makefile funciona como uma interface de execução comum independentemente da stack
  • Em projetos pessoais, muitas vezes bastam alguns alvos simples como dev, build e deploy, em vez de condicionais ou flags
  • O GNU Make é amplamente difundido e, mesmo quando não está disponível, é possível executar diretamente os comandos de shell do Makefile, o que reduz a carga em ambientes restritos

Reunindo comandos diferentes de cada projeto em um único ponto de entrada

  • O Makefile é usado para envolver projetos com formas diferentes de compilar, executar e fazer deploy em um conjunto familiar de comandos
  • Quando um projeto desconhecido tem um Makefile, em geral dá para começar tentando os seguintes comandos
    • make
    • make build
    • make install
  • Mesmo ao reabrir um projeto pessoal antigo, é possível configurar tudo para que apenas make dev execute de uma vez as etapas de build necessárias e o servidor de desenvolvimento
  • Mesmo que a stack tecnológica varie, como em Jekyll, Hugo, 11ty ou vários frameworks web em Python, é possível manter o mesmo ponto de entrada
  • Mesmo ao usar Docker ou gulp, o Makefile não substitui essas ferramentas, mas atua como uma camada de orquestração
    • Exemplo: make build compila as imagens Docker necessárias passando argumentos adicionais específicos do projeto

Uma forma simples de usar em projetos pessoais

  • O uso de Makefiles se concentra mais em agrupar comandos de shell do que em recursos complexos
    • Não usa recursos como condicionais ou flags
    • Tecnicamente, a unidade de trabalho do make é o target, mas na prática ele é tratado como uma tarefa
    • Isso também poderia ser feito com funções em scripts bash, mas o Makefile é visto como mais fácil e mais rápido
  • Em projetos pessoais, normalmente há 2 ou 3 tarefas usadas com frequência
    • dev: iniciar o servidor de desenvolvimento
    • build: compilar o projeto, quando necessário
    • deploy: fazer deploy ou publicar o projeto
    • watch: tarefa adicional usada para rerodar automaticamente o build quando arquivos-fonte mudam
  • O Makefile do blog atual tem apenas um alvo, dev
    • Em dev:, executa npm run dev
  • Em projetos pessoais mais complexos, as seguintes tarefas são reunidas no Makefile
    • dev: executar o servidor de desenvolvimento com bundle exec jekyll serve
    • build: compilar os assets com npm run gulp build
    • watch: monitorar uma pasta específica com npm run gulp watch
    • deploy: após compilar o Jekyll em ambiente de produção, executar make encrypt e netlify deploy --prod
    • encrypt: criptografar os arquivos HTML da pasta _site com npx staticrypt

Limitações e vantagens que vale conhecer

  • Nos exemplos, os phony targets foram omitidos, mas se existirem arquivos com nomes como dev, build, watch, deploy ou encrypt, o Makefile pode não funcionar como esperado
  • GNU Make é o software que executa makefiles; é bem provável que já esteja instalado no Linux e é tão comum em MacBooks que nem se lembra de precisar instalar separadamente
  • O Make não exige muitas dependências adicionais, então pode ser útil em ambientes restritos onde instalar pacotes é difícil ou impossível por motivos de segurança
  • Mesmo sem Make, é possível copiar e executar diretamente no shell os comandos contidos no Makefile
    • Em contraste, se o gulp não estiver no servidor, é difícil simplesmente colar código JavaScript no terminal e executá-lo
  • Mesmo que você descubra ferramentas de build novas, melhores e mais rápidas, o Make continua útil como uma camada comum de comandos para gerenciar ferramentas e configurações diferentes

2 comentários

 
kayws426 2024-09-22

Se o makefile não define dependências, substituí-lo por um justfile oferece uma usabilidade melhor.

 
GN⁺ 2024-09-22
Opiniões no Hacker News
  • Não há motivo para ficar desanimado com quem diz nesta thread que você “está usando make errado”. Um dos motivos pelos quais Make é uma boa ferramenta é que ela permite começar de forma mais simples do que parece
    Não usar .PHONY pode causar problemas, mas, em um projeto pequeno, talvez seja uma armadilha em que você só caia daqui a um ano e, mesmo então, é bem provável que baste coçar a cabeça por uma horinha para resolver. Em 99% dos casos, você pode simplesmente começar sem se preocupar com o “jeito certo”, e o Make só exige complexidade suficiente para não desmoronar

    • Um dos piores aspectos do Make é que ele parece simples por fora
      O Make recebe arquivos de entrada e dependências e cria exatamente um arquivo de saída. Regras que não geram saída, como install, all e clean, ou todos os alvos do texto, precisam depender de um alvo mágico especial como .PHONY, mas .PHONY não existia no POSIX de 2017 (IEEE Std 1003.1-2017, https://pubs.opengroup.org/onlinepubs/9699919799/utilities/m...) e só entrou no padrão atual (IEEE Std 1003.1-2024, https://pubs.opengroup.org/onlinepubs/9799919799/utilities/m...). Se você quiser gerar vários arquivos, como arquivos objeto, módulos e headers pré-compilados, precisa criar por conta própria gambiarras frágeis. Nem todo Make é GNU Make, e variantes nix como BSD e Solaris/Illumos ainda existem
      Isso não quer dizer que o Make seja inútil. Ele é útil para projetos que já são complexos o bastante, mas ainda não a ponto de precisarem de um sistema de build “melhor”. O problema é que esses projetos vão ficando mais complexos conforme o código cresce e, no fim, acabam tendo scripts/programas que geram o Makefile ou partes dele, criando um meta-sistema de build improvisado
      O problema maior não é usá-lo, mas propô-lo como solução para “todo mundo”. E, no momento em que aparece no diretório raiz do projeto um diretório ou arquivo chamado build ou dev, aquele Makefile para de funcionar
    • Um exemplo de Makefile um pouco mais complexo que uso ao iniciar um novo projeto TypeScript está aqui: https://github.com/borisovg/node-ts-template/blob/main/Makef...
      Hoje em dia eu até mudo para pnpm, mas ainda assim não considero tão complexo. É preciso conhecer um pouco da sintaxe, mas é menos difícil do que fazer outras ferramentas funcionarem direito desde o início
    • Usei make bem por mais de 20 anos, em muitos projetos e várias linguagens, e nunca tive problemas com essas tarefas .PHONY com que as pessoas tanto se preocupam
      É simples, fácil de ler, modificável, combinável e já vem instalado em todo lugar. Faz o que está escrito na superfície e quase nada além disso
      Aliás, quando preciso lidar em outros projetos com sistemas de build da moda ou sistemas de build de pesadelo, eu os envolvo com make
    • Concordo totalmente. É melhor usar a ferramenta adequada para a tarefa e, mesmo sem ser especialista, isso é muito melhor do que escolher a ferramenta errada para evitar críticas perfeccionistas
      Todo mundo precisa começar em algum lugar e, uma vez que algo começa a rodar, melhorias incrementais podem ser adicionadas facilmente
    • Toda receita de makefile deve produzir exatamente um artefato de saída, $@. O Makefile inteiro pode produzir um número arbitrário de artefatos de saída, já que regras podem depender de outras regras
      Daí vem uma boa regra prática para alvos phony. Se uma receita não mexe apenas em $@, então $@ deve ser marcado como phony. Gerenciar alvos phony em uma lista torna tudo muito mais fácil
      phonies :=
      phonies += something
      something:
      ./do-something
      phonies += something-else
      something-else: something
      ./do-something-else
      # touches $@ and thus does not need to be phony
      create-file:
      ./generate-some-output > $@
      .PHONY: $(phonies)
  • Makefile é uma tecnologia péssima. Só que é um pouco menos ruim do que a maioria dos outros sistemas de build que criamos, então se torna útil num sentido masoquista
    Sistemas de build geralmente são básicos demais, virando uma confusão quando se tenta construir algo além de um brinquedo; ou complexos demais, exigindo conhecimento prévio, burocracia, sincronização e boilerplate em excesso; ou não têm uma biblioteca-padrão, fazendo os mesmos padrões de build se espalharem em 10 mil implementações incompatíveis; ou são restritivos demais, obrigando uma migração quando os requisitos crescem; ou têm magia demais; ou a sintaxe é obscura e inconsistente

    • Na minha opinião, Makefile é uma tecnologia excelente; muita gente é que não o aprendeu direito ou não o usa do modo pretendido
      Mais da metade do software que uso só usa Makefiles comuns e, às vezes, um script configure. Sem autotools, basta executar ./configure opcionalmente e depois make, make install, e simplesmente funciona. Isso não é de modo algum uma configuração de brinquedo; é composto por pequenos programas que fazem bem uma coisa só
      Make e a forma como ele funciona são fáceis de entender, pelo menos para mim. Acho que as experiências ruins das pessoas muitas vezes vêm mais de configurações de build ruins em projetos complexos, ou da falta do conhecimento necessário, do que do Make em si
      A ausência de uma biblioteca-padrão é uma intenção de independência de ferramentas, e por isso ele se aplica de forma universal a várias ferramentas, linguagens e usos. Vejo isso como recurso, não como defeito. A camada simples é rastreamento de dependências, e os alvos e dependências também são claros, então é difícil dizer que seja mágico. A sintaxe certamente poderia melhorar
    • Os piores sistemas de build são os feitos em torno de uma linguagem de programação específica. Se houver N>>1 linguagens, haverá N>>1 sistemas de build, e a carga cognitiva fica grande demais para escalar
      Os únicos sistemas de build gerais que abrangem várias linguagens são sistemas que miram o make, como o make ou o CMake. O motivo de isso não ser ótimo é que o make não é ótimo. make é realmente difícil de usar corretamente, tem problemas como make recursivo e não recursivo, muitas variantes incompatíveis como Unix/POSIX make, BSD make, GNU make e Windows nmake, e é bem feio
      Ainda assim, um make bem usado é bem bom. É realmente uma sorte termos o make como mínimo denominador comum
    • Concordo. Muitas alternativas também são focadas em linguagens específicas, não em uma ferramenta genérica de “executar alguma coisa” como o Make
      É bem absurdo que o Make nem consiga lidar de forma limpa com subdiretórios. Fico curioso se existe algo melhor que o Make. Existe o Ninja, mas ele não foi projetado para ser escrito diretamente por humanos
    • Dá para dizer que essa confusão, “de forma muito hipotética”, não tem absolutamente nada a ver com compiladores ou linguagens sem módulos?
      Quanto você acha que contribui? Uns 20%, 40%, 70% não parece correto?
    • Se for “um ou mais”, há bastante margem. Minha estimativa é esta: Makefile não é básico demais. Pode ficar complexo demais, mas depende de como você o constrói. Quanto à biblioteca-padrão, há funções embutidas. Nunca achei restritivo demais, então diria que não. Como os alvos e dependências são claros, também não há magia demais. Mas a sintaxe certamente poderia melhorar, e acho que até um arquivo JSON comum já seria melhor
  • Gosto muito de Make. Nos últimos 20 anos, escrevi pelo menos um para todo trabalho ou projeto em que mexi
    Não há nada inteligente; é só uma colagem de trechos com algumas variáveis. Coisas como make run, make test, make lint
    make recent faz lint e depois executa o script modificado mais recentemente. Dá para fazer isso com Bash ou outro shell, mas aí você é sugado para o mundo do desenvolvedor, só aumentando a complexidade sem valor adicional. Make é apenas uma linguagem específica de domínio para “este tipo de arquivo é feito a partir deste tipo de arquivo e, nesse momento, execute este ou dois comandos”. Por isso é extremamente poderoso

    • Gosto da expressão “este tipo de arquivo é feito a partir deste tipo de arquivo e, nesse momento, execute este ou dois comandos”
      Décadas atrás, criei um framework de testes em Java que permitia especificar testes e classes dependentes com uma sintaxe parecida com make. Havia um conjunto de classes de teste que definia um “pacote base de testes”, e outras camadas de classes de teste dependentes dele só eram executadas se a etapa anterior tivesse sucesso
      Não sei por que hoje tudo precisa ser tão complicado. Sempre aconselhei a manter as ferramentas Unix padrão e sua forma de fazer as coisas, ou seja, métodos comprovados pelo tempo. A menos que seja uma situação em que isso seja absolutamente impossível. O tempo é finito, então quero gastá-lo no que é central para resolver o problema: projeto de sistemas/programas, modelagem, estrutura, padrões etc.; o resto é secundário
    • Todos os Makefiles que encontrei na prática faziam muito mais do que “só isso”
    • Comigo é parecido. No passado eu não usava muito, mas no meu emprego atual quase todos os repositórios têm um Makefile
      É um grande alívio saber que, independentemente da stack, da linguagem ou do repositório, executar make test funciona
    • O problema do Make não é o Make; é que as coisas que o Make chama já não se comportam mais segundo esse modelo
      O Makefile do meu projeto anterior tinha quatro comandos principais: build, test, frontend, deploy. build e test chamavam maven, frontend chamava npm, e deploy chamava docker e aws
      Todas essas ferramentas têm seu próprio rastreamento de estado, cache e processamento incremental, mas não informam o que fizeram. Por isso não dá para escrever regras em nível molecular como “implante apenas se o build tiver sido atualizado”. Porque maven/cargo/dotnet/npm/go não expõem essa informação
  • O autor nem sequer usa rastreamento de dependências baseado em mtime. Os alvos também deveriam ser .PHONY, mas não estão marcados assim. Isso poderia ter sido substituído por um script shell que lê $1, faz o matching e decide o que fazer

    • Ou poderia ser substituído por um executor simples de comandos, como o just
      https://just.systems/
    • Há uma história de terror de uma linha tirada de um projeto real em andamento: .PHONY: $(MAKECMDGOALS)
      ./build.sh dev é menos claro que make dev. Outro motivo para usar Make, mesmo sem nenhuma etapa não phony, é poder adicionar essas etapas depois, se necessário. Claro que concordo que o autor deveria marcar {dev,build,deploy} como phony
    • Tecnicamente está certo, mas o ponto principal que tirei do texto é que se ganha consistência de uso não só nos próprios projetos, mas também em projetos de terceiros
      O Makefile dele poderia ser tecnicamente melhorado, mas o mais importante é que a experiência de entrar em um projeto e “começar” seja consistente
    • Todos nós já fomos iniciantes um dia. Para aprender uma ferramenta, ajuda usá-la de verdade, mesmo que seja de um jeito imperfeito de iniciante. Dá para melhorar gradualmente enquanto se aprende
    • A beleza do make e do shell está em seguirem o princípio Unix de serem simples e fazerem uma coisa bem
      As pessoas querem que isso faça muitas outras coisas, como linguagem de script, rastreador de dependências etc., e acabam trazendo ferramentas inchadas. Novo não quer dizer necessariamente melhor. Autoconf e automake não são make
  • Tecnicamente, todos esses alvos do make procuram arquivos com o mesmo nome do alvo. Cada alvo deveria, de fato, ser definido como .PHONY
    Antigamente eu também usava Makefiles assim o tempo todo, mas nos últimos anos migrei para just e justfile. Esse comportamento é o padrão lá, e no geral é mais fácil de usar. Coisas como parâmetros também são mais simples
    https://github.com/casey/just

    • Esses sistemas parecidos com make são bem bons, mas têm um problema. O Make já está em todos os Linux e Macs, e também é bem fácil de conseguir no Windows. É uma pena mesmo que não venha incluído no Git Bash. Vejo o fato de usar o mínimo denominador comum como uma grande vantagem do Make
    • O grande erro do Make foi misturar alvos phony e alvos de arquivo no mesmo namespace. Por exemplo, alvos phony deveriam começar com :, para que fosse possível distingui-los só pelo nome
      Claro que agora é tarde demais
    • O just é realmente bom, mas é meio chato porque normalmente não vem instalado
      Acho que já passou da hora de aparecer algo como uma nova distribuição de coreutils que agrupe ferramentas novas e úteis, como just e ripgrep, e seja instalada em qualquer lugar onde você queira usá-las
  • É curioso que make provoque debates tão acalorados. Lembra as antigas discussões vi versus emacs, quase guerras religiosas
    Concordo totalmente com o post original e, em especial, acho inteligente encapsular ferramentas de build mais sofisticadas com um Makefile de nível superior. A vantagem é a padronização. Sem precisar lembrar de nada: se é um projeto meu, sei que basta digitar make e vai funcionar
    Por exemplo, alguém que usa C e quer compilar um projeto Rust não precisa procurar como usar cargo; basta digitar make ou gmake. Tento seguir POSIX sem usar recursos específicos do GNU, mas também é verdade que quase 100% dos makes reais são gmake
    A sugestão de usar o atemporal make como uma espécie de acionador de sistema de build de nível superior provavelmente ainda vai funcionar daqui a 250 anos

    • É engraçado que um título tão simples tenha iniciado uma flame war. O texto em si é um caso de uso absurdamente simples de make, e fala de usar gulp em 2024, mas claramente ninguém parece ter lido
    • cargo é um mau exemplo. Porque universalmente se usa cargo build
      O Make em si é excelente, mas, na maioria das vezes em que trabalhei com projetos C, era uma combinação de cmake/autotools e instalação global de pacotes, e isso eu frequentemente precisava consultar
    • Talvez não, se bazel/blaze dominar antes disso. Se duvida, veja Chrome versus Firefox, ou Kubernetes versus docker-compose
  • Concordo com o sentimento daqui, mas estou reescrevendo muita coisa como Justfile
    https://github.com/casey/just
    Dá para evitar muitas esquisitices específicas de Makefile

    • O GPT de fato ajudou a escrever Makefiles e deu uma segunda vida ao Makefile, retardando sua morte
    • Eu também. O fato de o mesmo arquivo funcionar no Windows é um bônus
  • Sim, Make é excelente. Uso para muita coisa. É uma boa forma de automatizar tarefas
    Por exemplo, no meu site pessoal, um Makefile chama scripts bash para recriar as páginas atualizadas, faz git push para o servidor, e então um git hook no servidor chama o Make para fazer o deploy. Só que não quero colocar no repositório Git blobs binários que mudam com frequência, como PDFs de material de aula. Tudo bem. O Makefile tem um alvo uploads que envia para o servidor apenas os PDFs modificados, e esse alvo é dependência do alvo deploy, que faz o git push, então não preciso me preocupar com isso
    Além disso, PDFs atualizados de material de aula são colocados automaticamente na árvore de código-fonte do site por outro Makefile. Com esse Makefile, posso gerar a partir do código-fonte LaTeX o PDF que uso, ou versões alternativas para alunos a partir da mesma fonte, por exemplo uma versão sem respostas dos exercícios, publicá-las automaticamente na versão local do site e, quando eu quiser, enviá-las para o site atualizado
    É uma espécie de cadeia infinita de Makefiles. Gosto de Makefiles

  • Nesse caso, o que o autor realmente precisa não é make, é just
    https://just.systems/man/en/

    • just é só mais uma dependência. make está em todo lugar
  • Como executor de tarefas, o make não é ruim, mas hoje em dia há alternativas melhores, como just, conforme outros comentários
    Como sistema de build, o make é ok até você esbarrar nas desvantagens. make/Makefile não é padronizado, por isso existe o automake. Então você não escreve um Makefile diretamente; escreve um template e faz com que o makefile real seja gerado. Se você for dono de toda a toolchain, tudo bem, mas a maioria não é, então isso é feito para garantir um Makefile portável
    O make não faz nenhum tipo de resolução de dependências e presume que o necessário está exatamente ali. Por isso surgiram os scripts configure; e, como scripts configure também não são um padrão, você acaba gerando o script configure com autoconf/autoreconf antes de executar o alvo do make
    O make e ferramentas adjacentes como automake/autoconf/autoreconf usam mtime para decidir se as entradas estão desatualizadas. É possível cair numa situação em que, mesmo executando autoconf/autoreconf/automake/configure, as entradas continuam permanentemente desatualizadas e nada pode ser buildado. A propósito, muitos sistemas de build ficam bem usando mtime, desde que haja rastreamento de dependências adequado
    No geral, a falha fundamental de projeto do make é que ele segue a filosofia Unix, isto é, “faça uma coisa e faça-a bem”. Essa uma coisa é “rebuildar o alvo se a entrada estiver desatualizada”, mas isso é uma ferramenta muito limitada, então sistemas de build modernos precisam empilhar muita coisa em cima dela para torná-la útil além de um executor básico de tarefas

    • make não resolve dependências?
      dependency:
      ...
      target: dependency
      ...
    • Pode ser uma pergunta ignorante, mas resolução de dependências não é o ponto central do make? O que exatamente se quer dizer aqui?
    • A convenção antiga nesse caso é make configure. Isso configura o que é necessário para make [build]
    • O que realmente faz falta no make é apenas a capacidade de interpretar mtime por algum critério diferente de mtime
      Por isso acabamos usando touchfiles; é bagunçado, mas ainda funciona melhor do que muitas outras coisas. Estou falando especialmente do cache de docker build