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.

Você vai aprender
  • 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 clean e .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 regraNo boloNa lojinha
Alvo (target): o que vai ser produzidoa coberturacalculos.o
Dependências (prerequisites): do que ele é feitochocolate, creme de leitecalculos.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 alguma dependência é mais nova que o alvo, o alvo está velho: rode a receita.

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 make faz 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.o depende do main.cpp e também do calculos.h, porque o main.cpp faz #include "calculos.h". Esquecer um .h aqui é 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:

AlvoDepende deReceita
lojamain.o, calculos.oligar
  ↳ main.omain.cpp, calculos.hcompilar main.cpp
  ↳ calculos.ocalculos.cpp, calculos.hcompilar 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:

"*** faltando o separador. Pare." (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.

"Nenhum alvo indicado e nenhum arquivo make encontrado"
$ 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.

Erro de compilação no meio do 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.

"Sem regra para processar o alvo ‘calculos.h’"
$ 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 CXX com o valor g++. O nome é uma convenção do Make: CXX é "o compilador de C++" (o XX sã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) por g++ 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
EscritaQuer dizerNo exemplo
%.o: %.cppregra-modelo: "qualquer X.o é feito de X.cpp"vale para main.o e calculos.o
$@o alvo da regraloja, ou main.o
$^todas as dependênciasmain.o calculos.o
$<só a primeira dependênciamain.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

ComandoO que faz
makefaz o primeiro alvo do Makefile (e o que ele precisar)
make alvofaz um alvo específico (make calculos.o, make clean)
make -nmostra o que faria, sem fazer
make -f arquivousa outro arquivo no lugar do Makefile
make VAR=valortroca o valor de uma variável só desta vez
make -j4roda até 4 receitas ao mesmo tempo (mais rápido em projetos grandes)
touch arquivomarca o arquivo como modificado agora
cat -A Makefilemostra TABs (^I) e fins de linha ($)
No MakefileO que é
alvo: dep1 dep2regra: o alvo depende dessas coisas
(TAB)comandoreceita: começa obrigatoriamente com TAB
NOME = valor e $(NOME)variável
CXX, CXXFLAGScompilador de C++ e suas opções (nomes de convenção)
.PHONY: cleanalvos 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:

estoque.o:

2. Usar a variável

Complete a receita usando as variáveis de convenção (o TAB já está lá):

     -c estoque.cpp

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ências e, na linha de baixo, a receita, que começa obrigatoriamente com TAB. Com espaços: missing separator ("faltando o separador"). Confira com cat -A (^I = TAB).
  • Liste os .h nas dependências dos .o, senão mudar um cabeçalho não recompila quem o inclui.
  • make faz o primeiro alvo; make -n mostra sem fazer; touch simula uma edição.
  • Variáveis: CXX = g++, CXXFLAGS = -std=c++23 -Wall -Wextra, usadas com $(CXX); troque na hora com make CXXFLAGS="…".
  • make clean apaga o que foi gerado; .PHONY: clean garante que ele rode mesmo com um arquivo chamado clean na pasta.