C++ Make
Na aula Compilar Vários Arquivos, a Ana descobriu que dá para recompilar só o arquivo que mudou e depois ligar de novo. Só que fazer isso à mão é chato e arriscado: qual arquivo mudou? Quem inclui aquele .h? Esqueceu um, e o programa sai com um pedaço velho. O Make é um programa que faz essa conta por você: você escreve uma vez, num arquivo chamado Makefile, o que depende de quê e como fazer cada coisa, e daí em diante só digita make.
- Que problema o Make resolve.
- Como escrever um
Makefile: alvo, dependências e receita. - Por que a receita tem que começar com Tab, e o erro
missing separator("faltando o separador"). - Como o Make decide o que recompilar, testando com
touch. - Como usar variáveis (
CXX,CXXFLAGS),make cleane.PHONY.
Todos os comandos desta aula foram executados na máquina fictícia da Ana (usuária ana, computador notebook), com o GNU Make 4.4.1 e o g++ 15.2, no projeto da lojinha: main.cpp, calculos.cpp e calculos.h, os mesmos de Arquivos .h e .cpp. O Make vem junto com o g++ no pacote build-essential (veja Instalar no Linux); para conferir:
$ make --version | head -1 GNU Make 4.4.1
A ideia: a lista de tarefas de um bolo
Pense numa receita de bolo com cobertura. O bolo pronto depende da massa assada e da cobertura. A massa depende de farinha, ovos e leite. Se você trocar o chocolate da cobertura, não precisa assar a massa de novo: basta refazer a cobertura e montar o bolo.
O Make funciona com essa mesma lógica, escrita em regras:
| Parte da regra | No bolo | Na lojinha |
|---|---|---|
| Alvo (target): o que vai ser produzido | a cobertura | calculos.o |
| Dependências (prerequisites): do que ele é feito | chocolate, creme de leite | calculos.cpp, calculos.h |
| Receita (recipe): o comando que produz o alvo | "derreta e misture" | g++ … -c calculos.cpp |
E como o Make sabe se "o chocolate mudou"? Pela data de modificação dos arquivos, a mesma que aparece no ls -l. A regra de ouro do Make é uma só:
Se o alvo nem existe, ele também precisa ser feito.
1. O primeiro Makefile, linha por linha
Na pasta do projeto, a Ana criou um arquivo de texto chamado exatamente Makefile (com M maiúsculo e sem extensão) e escreveu, num editor como o nano (veja Editores e IDEs):
loja: main.o calculos.o g++ main.o calculos.o -o loja main.o: main.cpp calculos.h g++ -std=c++23 -Wall -Wextra -c main.cpp calculos.o: calculos.cpp calculos.h g++ -std=c++23 -Wall -Wextra -c calculos.cpp
- loja:
- O alvo, seguido de dois-pontos. A primeira regra do arquivo é a que o
makefaz quando você não diz nada, por isso o programa final vem primeiro. - main.o calculos.o
- As dependências do
loja, separadas por espaço: para ligar o programa, primeiro é preciso ter os dois.o. - (TAB) g++ main.o calculos.o -o loja
- A receita: o comando que faz o alvo, exatamente como você digitaria no terminal. A linha tem que começar com um caractere TAB (a tecla Tab), não com espaços.
- main.o: main.cpp calculos.h
- O
main.odepende domain.cppe também docalculos.h, porque omain.cppfaz#include "calculos.h". Esquecer um.haqui é o erro mais comum de Makefile: o Make não recompila quando o cabeçalho muda. - linhas em branco
- Só para separar as regras e facilitar a leitura.
As dependências formam uma árvore. O make começa pelo alvo de cima e desce até os arquivos que você escreveu:
| Alvo | Depende de | Receita |
|---|---|---|
loja | main.o, calculos.o | ligar |
↳ main.o | main.cpp, calculos.h | compilar main.cpp |
↳ calculos.o | calculos.cpp, calculos.h | compilar calculos.cpp |
2. Rodando o make
Na primeira vez, nenhum alvo existe, e o make faz tudo. Ele mostra cada comando antes de rodá-lo:
$ cd ~/cpp/make1 $ make g++ -std=c++23 -Wall -Wextra -c main.cpp g++ -std=c++23 -Wall -Wextra -c calculos.cpp g++ main.o calculos.o -o loja $ ./loja Total: R$ 10 Com 10% de desconto: R$ 9
Repare na ordem: primeiro os .o, depois a ligação, mesmo que no arquivo a regra do loja esteja em cima. O Make segue as dependências, não a ordem das linhas. Agora rode de novo, sem mudar nada:
$ make make: 'loja' está atualizado. $ echo $? 0
"‘loja’ está atualizado": nenhuma dependência é mais nova que o seu alvo, então não há nada a fazer. O código de saída 0 quer dizer sucesso. (O Make tem tradução para o português; em inglês, a mensagem é make: 'loja' is up to date.)
Só recompila o que mudou: o teste do touch
O comando touch ("tocar") atualiza a data de modificação de um arquivo para agora, sem mexer no conteúdo. É o jeito de fingir que você editou um arquivo:
$ touch calculos.cpp $ make g++ -std=c++23 -Wall -Wextra -c calculos.cpp « só o calculos.cpp g++ main.o calculos.o -o loja « e a ligação
O calculos.cpp ficou mais novo que o calculos.o, então o calculos.o foi refeito. Aí o calculos.o ficou mais novo que o loja, e a ligação rodou. O main.o não foi tocado. Agora mude o cabeçalho, que os dois .cpp incluem:
$ touch calculos.h $ make g++ -std=c++23 -Wall -Wextra -c main.cpp g++ -std=c++23 -Wall -Wextra -c calculos.cpp g++ main.o calculos.o -o loja
Os dois foram recompilados, porque os dois dependem do calculos.h. É exatamente a conta que a Ana teria de fazer de cabeça.
Ver sem fazer: make -n
A opção -n (de no-op, "não execute") mostra o que seria feito, sem rodar nada. Ótima para conferir um Makefile:
$ touch main.cpp $ make -n g++ -std=c++23 -Wall -Wextra -c main.cpp g++ main.o calculos.o -o loja
Pedir um alvo específico
Escreva o nome do alvo depois de make. Sem nome, vale o primeiro do arquivo:
$ make calculos.o make: 'calculos.o' está atualizado.
Experimente: o editor do guia não roda o make, mas dá para ver o raciocínio dele num programa C++. Abaixo, cada arquivo tem uma "hora" de modificação (número maior = mais novo), e a função make() aplica a regra de ouro. Rode. Depois troque o touch("calculos.cpp") por touch("calculos.h") e por touch("main.cpp"), e compare com as saídas reais acima. (O std::map é uma tabela "nome → valor", que você vai estudar em std::map.)
#include <iostream>
#include <map>
#include <string>
#include <vector>
// "hora" da última modificação de cada arquivo: maior = mais novo
std::map<std::string, int> hora = {
{"main.cpp", 10}, {"calculos.cpp", 10}, {"calculos.h", 10},
{"main.o", 20}, {"calculos.o", 20}, {"loja", 30},
};
int relogio = 100;
struct Regra {
std::string alvo;
std::vector<std::string> dependencias;
std::string receita;
};
// em ordem: primeiro os .o, depois o programa (o make de verdade descobre a ordem sozinho)
std::vector<Regra> regras = {
{"main.o", {"main.cpp", "calculos.h"}, "g++ -std=c++23 -Wall -Wextra -c main.cpp"},
{"calculos.o", {"calculos.cpp", "calculos.h"}, "g++ -std=c++23 -Wall -Wextra -c calculos.cpp"},
{"loja", {"main.o", "calculos.o"}, "g++ main.o calculos.o -o loja"},
};
void touch(const std::string& arquivo) {
hora[arquivo] = ++relogio;
}
void make() {
bool fezAlgo = false;
for (const Regra& r : regras) {
bool refazer = false;
for (const std::string& d : r.dependencias) {
if (hora[d] > hora[r.alvo]) {
refazer = true; // a dependência é mais nova que o alvo
}
}
if (refazer) {
std::cout << r.receita << '\n';
touch(r.alvo); // o alvo acabou de ser refeito
fezAlgo = true;
}
}
if (!fezAlgo) {
std::cout << "make: 'loja' está atualizado.\n";
}
}
int main() {
std::cout << "$ make\n";
make();
std::cout << "$ touch calculos.cpp\n";
touch("calculos.cpp");
std::cout << "$ make\n";
make();
}
3. TAB, não espaços: missing separator
A receita de cada regra precisa começar com o caractere TAB. Espaços, mesmo que pareçam iguais na tela, não servem. Muitos editores trocam o Tab por espaços sem avisar, e o resultado é este:
missing separator)
$ make Makefile:2: *** faltando o separador. Pare. $ LANG=C make Makefile:2: *** missing separator. Stop.
O que significa: na linha 2 do Makefile, o Make esperava um "separador" e não achou: a linha não é uma regra (não tem :) e não começa com TAB, então ele não sabe o que ela é. Pare (Stop): nada foi feito.
Como confirmar: o cat -A mostra os caracteres invisíveis. TAB aparece como ^I, e o fim de cada linha, como $. Compare o arquivo errado com o certo:
$ cat -A Makefile | head -2
loja: main.o calculos.o$
g++ main.o calculos.o -o loja$ « quatro espaços: errado
$ cat -A Makefile | head -2
loja: main.o calculos.o$
^Ig++ main.o calculos.o -o loja$ « ^I é o TAB: certo
Se forem exatamente 8 espaços, o Make 4.4 até adivinha o problema:
$ make -f Makefile.8esp Makefile.8esp:2: *** faltando separador (você pensou em TAB ao invés de 8 espaços?). Pare.
(A opção -f escolhe outro arquivo no lugar do Makefile.)
Como resolver: apague os espaços do começo da receita e aperte Tab. No nano, o Tab grava um TAB de verdade. No VS Code, abra o arquivo com o nome exato Makefile: ele reconhece o tipo e passa a usar TAB nesse arquivo.
$ make make: *** Nenhum alvo indicado e nenhum arquivo make encontrado. Pare.
O que significa: não há Makefile na pasta atual (em inglês: No targets specified and no makefile found).
Como resolver: entre na pasta do projeto com cd e confira o nome do arquivo: Makefile ou makefile, sem .txt no fim.
make
A Ana apagou sem querer o ; de uma linha do calculos.cpp:
$ make
g++ -std=c++23 -Wall -Wextra -c calculos.cpp
calculos.cpp: In function ‘double calcularTotal(double, int)’:
calculos.cpp:5:30: error: expected ‘;’ before ‘}’ token
5 | return preco * quantidade
| ^
| ;
6 | }
| ~
make: *** [Makefile:12: calculos.o] Error 1
$ echo $?
2
O que significa: as primeiras linhas são o erro do g++, do jeito de sempre (veja Lendo Erros do Compilador). A última é do Make: "a receita da linha 12 do Makefile, que faz o calculos.o, terminou com o código 1". O Make para no primeiro erro e não tenta ligar, então o loja antigo continua lá, sem a mudança.
Como resolver: conserte o código e rode make de novo. Só o que falhou será refeito.
$ make make: *** Sem regra para processar o alvo 'calculos.h', necessário por 'main.o'. Pare.
O que significa: o main.o depende de calculos.h, o arquivo não existe, e não há regra que o produza (em inglês: No rule to make target). Aqui, a Ana tinha renomeado o arquivo.
Como resolver: confira o nome com ls. Se o arquivo mudou de nome, corrija o Makefile (e os #include).
4. Variáveis: CXX e CXXFLAGS
No primeiro Makefile, -std=c++23 -Wall -Wextra aparece repetido. Para trocar uma opção, seria preciso mexer em todas as linhas. As variáveis do Make resolvem isso:
# Makefile da lojinha da Ana CXX = g++ CXXFLAGS = -std=c++23 -Wall -Wextra loja: main.o calculos.o $(CXX) main.o calculos.o -o loja main.o: main.cpp calculos.h $(CXX) $(CXXFLAGS) -c main.cpp calculos.o: calculos.cpp calculos.h $(CXX) $(CXXFLAGS) -c calculos.cpp clean: rm -f loja *.o .PHONY: clean
- # Makefile da lojinha da Ana
- Comentário: no Makefile, começa com
#e vai até o fim da linha. - CXX = g++
- Cria a variável
CXXcom o valorg++. O nome é uma convenção do Make:CXXé "o compilador de C++" (oXXsão os++deitados). - CXXFLAGS = -std=c++23 -Wall -Wextra
- As opções (flags) de compilação de C++. Outro nome de convenção.
- $(CXX), $(CXXFLAGS)
- Usa o valor da variável: o Make troca
$(CXX)porg++antes de rodar o comando. - clean:
- Uma regra sem dependências, cujo alvo não é um arquivo: é uma tarefa. A receita apaga o programa e os
.o. - .PHONY: clean
- Avisa que
cleané um alvo "de mentira" (phony), explicado na seção 5.
$ cd ~/cpp/make2 $ make g++ -std=c++23 -Wall -Wextra -c main.cpp g++ -std=c++23 -Wall -Wextra -c calculos.cpp g++ main.o calculos.o -o loja
Uma vantagem extra: dá para trocar o valor de uma variável na linha de comando, sem editar o arquivo. Por exemplo, para ligar as otimizações (-O2, veja Compilar com g++):
$ make clean rm -f loja *.o $ make CXXFLAGS="-std=c++23 -Wall -Wextra -O2" g++ -std=c++23 -Wall -Wextra -O2 -c main.cpp g++ -std=c++23 -Wall -Wextra -O2 -c calculos.cpp g++ main.o calculos.o -o loja
Do mesmo jeito, make CXX=clang++ compila tudo com o Clang.
5. make clean e .PHONY
make clean ("limpar") apaga tudo o que o Make gerou, para recomeçar do zero. Todo Makefile costuma ter essa regra:
$ make clean rm -f loja *.o
O -f do rm evita erro quando os arquivos já não existem. E para que serve o .PHONY? Lembre que o Make pensa em arquivos: para ele, clean é o nome de um arquivo que a receita deveria criar. Veja o que acontece se, por acaso, existir um arquivo chamado clean na pasta e o Makefile não tiver o .PHONY:
$ touch clean $ make clean make: 'clean' está atualizado. « não apagou nada!
O arquivo clean existe e não tem dependências, então para o Make ele "está atualizado", e a receita não roda. Com a linha .PHONY: clean no Makefile, o Make sabe que clean é uma tarefa, não um arquivo, e roda a receita sempre:
$ make clean rm -f loja *.o
Ponha no .PHONY todo alvo que não é um arquivo. É comum ter também all (tudo), run (compilar e rodar) e test (rodar os testes): .PHONY: all clean run test.
6. Formas mais curtas (para reconhecer)
Em projetos maiores, escrever uma regra por .cpp cansa. O Make tem atalhos que você vai encontrar em Makefiles alheios:
CXX = g++ CXXFLAGS = -std=c++23 -Wall -Wextra OBJS = main.o calculos.o loja: $(OBJS) $(CXX) $^ -o $@ %.o: %.cpp calculos.h $(CXX) $(CXXFLAGS) -c $< -o $@ clean: rm -f loja $(OBJS) .PHONY: clean
| Escrita | Quer dizer | No exemplo |
|---|---|---|
%.o: %.cpp | regra-modelo: "qualquer X.o é feito de X.cpp" | vale para main.o e calculos.o |
$@ | o alvo da regra | loja, ou main.o |
$^ | todas as dependências | main.o calculos.o |
$< | só a primeira dependência | main.cpp |
$ make g++ -std=c++23 -Wall -Wextra -c main.cpp -o main.o g++ -std=c++23 -Wall -Wextra -c calculos.cpp -o calculos.o g++ main.o calculos.o -o loja
O resultado é o mesmo. Use a forma explícita, uma regra por arquivo, enquanto estiver aprendendo: é mais fácil de ler e de achar erros. E quando o projeto crescer de verdade, a próxima ferramenta, o CMake, escreve esse tipo de arquivo por você, inclusive descobrindo sozinho quais .h cada .cpp inclui.
Referência
| Comando | O que faz |
|---|---|
make | faz o primeiro alvo do Makefile (e o que ele precisar) |
make alvo | faz um alvo específico (make calculos.o, make clean) |
make -n | mostra o que faria, sem fazer |
make -f arquivo | usa outro arquivo no lugar do Makefile |
make VAR=valor | troca o valor de uma variável só desta vez |
make -j4 | roda até 4 receitas ao mesmo tempo (mais rápido em projetos grandes) |
touch arquivo | marca o arquivo como modificado agora |
cat -A Makefile | mostra TABs (^I) e fins de linha ($) |
| No Makefile | O que é |
|---|---|
alvo: dep1 dep2 | regra: o alvo depende dessas coisas |
(TAB)comando | receita: começa obrigatoriamente com TAB |
NOME = valor e $(NOME) | variável |
CXX, CXXFLAGS | compilador de C++ e suas opções (nomes de convenção) |
.PHONY: clean | alvos que não são arquivos |
# | comentário |
Exercícios
1. A regra do estoque
O estoque.cpp inclui o estoque.h. Complete a linha de dependências da regra do estoque.o:
2. Usar a variável
Complete a receita usando as variáveis de convenção (o TAB já está lá):
3. Com o Makefile da seção 1 e tudo compilado, a Ana roda touch main.cpp e depois make. Quais comandos aparecem?
4. O make respondeu Makefile:5: *** faltando o separador. Pare.. Qual é a causa mais provável?
5. Para que serve .PHONY: clean?
Missão final: escreva a regra de ouro do Make
Missão: o mini-Make abaixo está quase pronto, mas a função precisaRefazer sempre responde false. Escreva o corpo dela: devolva true se alguma dependência da regra tiver hora maior que a do alvo. O main roda make (nada a fazer), depois touch calculos.h e make de novo. A saída deve ser igual à do Make de verdade:
$ make make: 'loja' está atualizado. $ touch calculos.h $ make g++ -std=c++23 -Wall -Wextra -c main.cpp g++ -std=c++23 -Wall -Wextra -c calculos.cpp g++ main.o calculos.o -o loja
#include <iostream>
#include <map>
#include <string>
#include <vector>
std::map<std::string, int> hora = {
{"main.cpp", 10}, {"calculos.cpp", 10}, {"calculos.h", 10},
{"main.o", 20}, {"calculos.o", 20}, {"loja", 30},
};
int relogio = 100;
struct Regra {
std::string alvo;
std::vector<std::string> dependencias;
std::string receita;
};
std::vector<Regra> regras = {
{"main.o", {"main.cpp", "calculos.h"}, "g++ -std=c++23 -Wall -Wextra -c main.cpp"},
{"calculos.o", {"calculos.cpp", "calculos.h"}, "g++ -std=c++23 -Wall -Wextra -c calculos.cpp"},
{"loja", {"main.o", "calculos.o"}, "g++ main.o calculos.o -o loja"},
};
void touch(const std::string& arquivo) {
hora[arquivo] = ++relogio;
}
bool precisaRefazer(const Regra& r) {
// escreva aqui: percorra r.dependencias e compare hora[d] com hora[r.alvo]
(void)r; // (esta linha só cala o aviso de parâmetro não usado; pode apagar)
return false;
}
void make() {
bool fezAlgo = false;
for (const Regra& r : regras) {
if (precisaRefazer(r)) {
std::cout << r.receita << '\n';
touch(r.alvo);
fezAlgo = true;
}
}
if (!fezAlgo) {
std::cout << "make: 'loja' está atualizado.\n";
}
}
int main() {
std::cout << "$ make\n";
make();
std::cout << "$ touch calculos.h\n";
touch("calculos.h");
std::cout << "$ make\n";
make();
}
#include <iostream>
#include <map>
#include <string>
#include <vector>
std::map<std::string, int> hora = {
{"main.cpp", 10}, {"calculos.cpp", 10}, {"calculos.h", 10},
{"main.o", 20}, {"calculos.o", 20}, {"loja", 30},
};
int relogio = 100;
struct Regra {
std::string alvo;
std::vector<std::string> dependencias;
std::string receita;
};
std::vector<Regra> regras = {
{"main.o", {"main.cpp", "calculos.h"}, "g++ -std=c++23 -Wall -Wextra -c main.cpp"},
{"calculos.o", {"calculos.cpp", "calculos.h"}, "g++ -std=c++23 -Wall -Wextra -c calculos.cpp"},
{"loja", {"main.o", "calculos.o"}, "g++ main.o calculos.o -o loja"},
};
void touch(const std::string& arquivo) {
hora[arquivo] = ++relogio;
}
bool precisaRefazer(const Regra& r) {
for (const std::string& d : r.dependencias) {
if (hora[d] > hora[r.alvo]) {
return true;
}
}
return false;
}
void make() {
bool fezAlgo = false;
for (const Regra& r : regras) {
if (precisaRefazer(r)) {
std::cout << r.receita << '\n';
touch(r.alvo);
fezAlgo = true;
}
}
if (!fezAlgo) {
std::cout << "make: 'loja' está atualizado.\n";
}
}
int main() {
std::cout << "$ make\n";
make();
std::cout << "$ touch calculos.h\n";
touch("calculos.h");
std::cout << "$ make\n";
make();
}
Resumo
- O Make recompila só o que mudou, comparando datas: se uma dependência é mais nova que o alvo (ou o alvo não existe), a receita roda.
- Uma regra é
alvo: dependênciase, na linha de baixo, a receita, que começa obrigatoriamente com TAB. Com espaços:missing separator("faltando o separador"). Confira comcat -A(^I= TAB). - Liste os
.hnas dependências dos.o, senão mudar um cabeçalho não recompila quem o inclui. makefaz o primeiro alvo;make -nmostra sem fazer;touchsimula uma edição.- Variáveis:
CXX = g++,CXXFLAGS = -std=c++23 -Wall -Wextra, usadas com$(CXX); troque na hora commake CXXFLAGS="…". make cleanapaga o que foi gerado;.PHONY: cleangarante que ele rode mesmo com um arquivo chamadocleanna pasta.