Author: Blau Araujo
Date: 0101-01-01 00:00:00+00:00

Introdução ao GNU Make

Table of Contents

- 1. Sistemas de construção

- 1.1. Um exemplo de construção com scripts

- 1.1.1. O que conseguimos

- 1.1.2. Por que não é suficiente

- 2. O GNU Make

- 2.1. Principais características

- 2.2. Por que (ou quando) escolher o Make

- 3. Uma pequena demonstração do GNU Make

- 3.1. Estrutura, escrita e uso do arquivo de construção

- 3.1.1. Seções do arquivo

- 3.1.2. Regras de escrita

- 3.1.3. Regras expressas com padrões (pattern rules)

- 3.1.4. Variáveis
- 3.1.5. Substituições de sufixos
- 3.1.6. Alvos 'phony'
- 3.1.7. Ordem de processamento das regras
- 3.1.8. Boas ideias

- 3.2. Executando o arquivo de construção

- 4. Primeiro mão na massa: cada coisa em seu lugar

- 4.1. Dica 1: ordem dos pré-requisitos

- 4.1.1. Observação sobre o controle das modificações

- 4.2. Dica 2: comando 'mkdir'

- 4.3. Dica 3: comando 'rm'

- 5. Segundo mão na massa: vamos evitar a fadiga

- 5.1. Dica: funções 'wildcard' e 'shell'

- 5.1.1. Função 'wildcard'

- 5.1.2. Função 'shell'

- 6. Declarações de dependências

- 7. Dependências automáticas com 'include' e GCC

- 8. Automatizando instalações

- 8.1. Construção e instalação

- 9. Do que mais precisamos saber

- 9.1. Variáveis frequentemente associadas a diretórios padrão

- 9.2. Variáveis automáticas

- 9.3. Variáveis simples e recursivas

- 9.3.1. Atribuição recursiva (=)

- 9.3.2. Atribuição simples (:=)

- 9.4. Modificadores de comandos

- 9.4.1. Modificador @ (arroba)
- 9.4.2. Modificador - (menos)
- 9.4.3. Modificador + (mais)

- 9.5. Alguns modos de execução

- 10. Exemplos para inspiração

- 10.1. Pequenos projetos em NASM (x86-64/Linux)

- 10.2. Instalação de scripts em Bash
- 10.3. Pequenos projetos em C
- 10.4. União de arquivos de texto

O objetivo deste curso é apresentar os recursos e funcionalidades do GNU Make que podem nos ajudar a automatizar a construção e instalação de programas em, pelo menos, 90% dos casos de uso. O foco das demonstrações será a construção de programas em C, mas os conceitos trabalhados poderão ser facilmente aplicados ao desenvolvimento em outras linguagens ou até na construção de documentos em vários formatos (a partir de fontes em LaTeX, por exemplo).

1. Sistemas de construção

Mais conhecidos como build systems, os sistemad de construção tem que cumprir três papéis básicos na construção de um projeto de software:

- Organizar e padronizar o processo de construção e instalação;

- Durante o desenvolvimento, reconstruir apenas o que for necessário;

- Facilitar a manutenção do processo de construção.

1.1. Um exemplo de construção com scripts

Consideremos um projeto em C composto por três arquivos:

- Fonte principal: main.c
- Cabeçalho de declarações: utils.h
- Implementações: utils.c

Arquivo: main.c

#include <stdio.h>
#include <utils.h>

int result(void) {
return soma(13);
}

int main(void) {
printf("%d\n", result());
return 0;
}

Arquivo: utils.h

#ifndef UTILS_H
#define UTILS_H

#define XCONST 29

int soma(int num);

#endif

Arquivo: utils.c

#include <utils.h>

int soma(int num) {
return num + XCONST;
}

Digamos, também, que o processo de construção deveria seguir estes passos:

- Compilar o arquivo main.c como objeto;

- Compilar o arquivo utils.c como objeto;

- Compilar os objetos no executável final (teste);

- Remover os objetos.

No script (build.sh), refletindo o que faríamos no terminal, nós poderíamos escrever:

#!/bin/bash

set -x # Ativa 'xtrace' para exibir os comandos executados...
gcc -Wall -Wextra -I. -g -c main.c || exit 1
gcc -Wall -Wextra -I. -g -c utils.c || exit 2
gcc utils.o main.o -o teste || exit 3
rm *.o
set +x # Desativa 'xtrace'...

echo 'Construção terminada com sucesso!'

Vamos tornar nosso script executável:

chmod +x build.sh

Para construir o executável e testar:

:~$ ./build.sh
+ gcc -Wall -Wextra -I. -g -c main.c
+ gcc -Wall -Wextra -I. -g -c utils.c
+ gcc utils.o main.o -o teste
+ rm main.o utils.o
+ set +x
Construção terminada com sucesso!
:~$ ls
build.sh main.c teste utils.c utils.h
:~$ ./teste
42

1.1.1. O que conseguimos

- Uma forma simples e objetiva de construir projetos e instalar programas;

- Funcionalidades mais complexas também poderiam ser implementadas;

- Seria possível até implementar compilações e montagens apenas de módulos alterados.

1.1.2. Por que não é suficiente

- Scripts simples, como este, não lidam com todos os requisitos de um sistema de construção;

- Implementações mais complexas seriam quase o mesmo que escrever um sistema de construção completo do zero.

2. O GNU Make

O GNU Make é um entre os vários sistemas de construção disponíveis, como o Autoconf, o Ninja, o CMake e o Meson. Aliás, desde que cumpra com os papeis esperados, até scripts em shell podem ser utilizados como sistemas de construção.

2.1. Principais características

Sintaxe simples e minimalista

Um Makefile básico pode ser criado em poucas linhas, sem necessidade de aprender linguagens extras ou configurar camadas intermediárias.

Baseado em descrições de dependências

No Makefile, nós escrevemos regras de construção baseadas na descrição das dependências de cada componente que deverá ser montado.

Configurável e extensível

É possível usar variáveis, funções internas e até comandos de shell embutidos, o que dá grande flexibilidade para adaptar o processo de construção e instalação.

Integração com o shell (GNU/Unix)

O corpo das regras de construção podem conter comandos do shell, o que possibilita a integração com scripts e evita a duplicação da lógica de construção.

Portabilidade

Está disponível praticamente em qualquer sistema Unix-like (e até no Windows via MSYS/MinGW), o que garante que um mesmo Makefile possa rodar em ambientes diferentes sem necessidade de ferramentas adicionais.

2.2. Por que (ou quando) escolher o Make

Não existe uma fronteira natural delimitando até onde o Make deve ser adotado em detrimento de outros sistemas de construção. Geralmente, ele é escolhido em projetos de pequeno e médio porte, mas até projetos gigantescos (como o do kernel Linux, por exemplo) utiliza o Make como principal sistema de construção. Contudo, projetos muito grandes quase sempre criam infraestruturas próprias sobre o Make, ao passo que ferramentas mais modernas já embutem muitas dessas soluções.

O projeto Linux desenvolveu o Kbuild, que é uma camada de convenções e macros escritas sobre o GNU Make, tornando a manutenção mais organizada sem abandonar a base confiável do Make.

3. Uma pequena demonstração do GNU Make

Para esta demonstração, utilizaremos os mesmos arquivos fonte do exemplo de construção com scripts:

- main.c
- utils.h
- utils.c

Desta vez, porém, nós criaremos o arquivo de construção Makefile, onde definimos os alvos da construção, suas dependências e os comandos necessários para gerá-los.

Arquivo: Makefile

# Definições de variáveis...
CC=gcc
INCDIRS=-I.
OPT=-O0
CFLAGS=-Wall -Wextra -g $(INCDIRS) $(OPT)

CFILES=main.c utils.c
OBJECTS=$(CFILES:.c=.o)

TARGET=teste

# Alvo 'all': agrupa todos os alvos...
all: $(TARGET)

# ALVO:DEPENDÊNCIAS -> Para gerar TARGET, é preciso gerar OBJECTS...
$(TARGET): $(OBJECTS)
$(CC) -o $@ $^

# Para gerar OBJECTS, é preciso compilar cada fonte em C...
%.o: %.c
$(CC) $(CFLAGS) -c -o $@ $^

# O alvo 'clean' precisará ser invocada explicitamente...
clean:
rm -f $(TARGET) $(OBJECTS)

# Aqui, avisamos que 'clean' não é um arquivo (PHONY=fictício)...
.PHONY: clean all

3.1. Estrutura, escrita e uso do arquivo de construção

3.1.1. Seções do arquivo

Um Makefile geralmente tem quatro seções principais:

- Definição de variáveis;
- Regras principais (targets);
- Regras genéricas (pattern rules);
- Alvos auxiliares ou phony (como clean).

3.1.2. Regras de escrita

- Linhas de comandos devem começar com uma tabulação;

- O dois-pontos (:) separa o alvo das suas dependências;

- Um espaço depois do ':' é recomendado para legibilidade;

- Não pode haver espaços antes do alvo.

3.1.3. Regras expressas com padrões (pattern rules)

- %: Curinga para regras genéricas
- $@: Nomes dos alvos
- $^: Nomes das dependências
- $<: Primeira dependência

3.1.4. Variáveis

- São nomes associados a listas de itens;

- Definir nomes de arquivos, comandos, argumentos, diretórios, etc;

- Sintaxe de acesso (expansão): $(VAR);

3.1.5. Substituições de sufixos

Com a sintaxe:

$(VAR:OLD=NEW)

Para cada item da lista associada a VAR, substituir as ocorrências de OLD por NEW.

Exemplo:

CFILES = main.c utils.c
OBJECTS = $(CFILES:.c=.o)

3.1.6. Alvos 'phony'

- Usados para ações que não correspondem a arquivos reais (ex.: clean, all);

- Evita confusão se existirem arquivos com mesmos nomes do alvo;

- Declarados com .PHONY.

Exemplo:

.PHONY: clean all

Os alvos phony podem ser utilizados como argumentos da invocação do make:

:~$ make clean

Isso faz com que apenas o alvo clean seja processado.

3.1.7. Ordem de processamento das regras

O make constrói o primeiro alvo listado (geralmente all). As dependências e regras devem ser listadas de forma lógica, mas o make resolve a ordem automaticamente com base nas dependências.

3.1.8. Boas ideias

- Utilizar variáveis para diretórios, listas de arquivos, compilador e flags;

- Utilizar pattern rules sempre que possível;

- Separar código-fonte, objetos e binários em diretórios diferentes;

- Declarar alvos auxiliaras com .PHONY;

- Utilizar a opção -f do comando rm no clean para não gerar erros se os arquivos não existirem mais;

- Documentar Makefile com comentários claros (#);

- Evitar misturar construção e limpeza automática, para não quebrar a compilação incremental.

3.2. Executando o arquivo de construção

Quando invocado sem argumentos, o make executará o conteúdo do primeiro arquivo de construção que encontrar no diretório corrente que tenha qualquer um desses nomes (nesta ordem):

- GNUmakefile
- makefile
- Makefile

Embora seja o último nome a ser procurado, o manual recomenda que se use Makefile, para que o arquivo de construção se destaque na listagem do diretório do projeto.

Ainda é possível determinar o arquivo de construção a ser utilizado com a opção de linha de comando -f, por exemplo:

:~$ make -f AltMakefile

Para esta demonstração, nós seguimos a recomendação do manual:

:~$ ls
main.c Makefile utils.c utils.h

Sendo assim, vamos construir o programa teste:

:~$ make
gcc -Wall -Wextra -g -I. -O0 -c -o main.o main.c
gcc -Wall -Wextra -g -I. -O0 -c -o utils.o utils.c
gcc -o teste main.o utils.o

Depois do make, nosso diretório está assim:

:~$ ls
main.c main.o Makefile teste utils.c utils.h utils.o

Observe que, desta vez, os arquivos objeto (*.o) não foram removidos. Em um projeto real, eles provavelmente seriam gerados em um diretório separado (geralmente, de nome build), assim como o executável (que normalmente iria para um diretório chamado bin). De todo modo, é importante que seja assim, porque a ideia é que, na continuidade do desenvolvimento, o make gere apenas os módulos que tiverem seus códigos-fonte alterados.

Por exemplo, se nós removermos o objeto utils.o, apenas ele será gerado novamente antes da compilação do executável:

:~$ rm utils.o
:~$ ls
main.c main.o Makefile teste utils.c utils.h
:~$ make
gcc -Wall -Wextra -g -I. -O0 -c -o utils.o utils.c
gcc -o teste main.o utils.o
:~$ ls
main.c main.o Makefile teste utils.c utils.h utils.o

O mesmo aconteceria se modificássemos o código em main.c, por exemplo:

:~$ cat main.c
#include <stdio.h>
#include <utils.h>

int result(void) {
return soma(23); // Modificado de 13 para 23!
}

int main(void) {
printf("%d\n", result());
return 0;
}
:~$ make
gcc -Wall -Wextra -g -I. -O0 -c -o main.o main.c
gcc -o teste main.o utils.o
:~$ ./teste
52

Contudo, nada aconteceria se modificássemos o arquivo utils.h:

:~$ cat utils.h
#ifndef UTILS_H
#define UTILS_H

#define XCONST 19 // Modificado de 29 para 19!

int soma(int num);

#endif
:~$ make
make: Nada a ser feito para 'all'.

Neste caso, uma das possibilidades seria processar o alvo clean:

:~$ make clean
rm -f teste main.o utils.o
:~$ make
gcc -Wall -Wextra -g -I. -O0 -c -o main.o main.c
gcc -Wall -Wextra -g -I. -O0 -c -o utils.o utils.c
gcc -o teste main.o utils.o
:~$ ./teste
42

Alternativamente, poderíamos utilizar a opção -B (ou --always-make):

:~$ make -B
gcc -Wall -Wextra -g -I. -O0 -c -o main.o main.c
gcc -Wall -Wextra -g -I. -O0 -c -o utils.o utils.c
gcc -o teste main.o utils.o

Existem estratégias avançadas para lidar com alterações em arquivos de cabeçalho, mas elas vão muito além do escopo deste primeiro contato com o GNU Make. Mais adiante, porém, nós veremos como eles podem ser declarados como dependências.

4. Primeiro mão na massa: cada coisa em seu lugar

Altere o Makefile do exemplo para que a construção do projeto resulte na árvore de diretórios abaixo:

src
├── bin
│ └── teste
├── build
│ ├── main.o
│ └── utils.o
├── main.c
├── Makefile
├── utils.c
└── utils.h

4.1. Dica 1: ordem dos pré-requisitos

No Makefile, utiliza-se o caractere | como uma sintaxe especial na linha das dependências:

ALVO: DEPENDÊNCIA1 | DEPENDÊNCIA2

Isso se chama order-only prerequisites (dependência apenas para efeito de ordem), e serve para especificar que DEPENDÊNCIA2 só tem que existir antes do ALVO ser construído, mas não força a reconstrução do ALVO se apenas DEPENDÊNCIA2 mudar. A DEPENDÊNCIA1, por outro lado, é uma dependência normal: ou seja, se ela mudar, a reconstrução do ALVO será obrigatória.

Como a decisão sobre a reconstrução dos alvos é tomada com base na data e hora da última alteração dos arquivos (timestamp), pode acontecer, por exemplo, de um diretório sofrer alguma alteração mas seu conteúdo ainda ser o mesmo. Neste caso…

Sem a barra | DEPENDÊNCIA1 seria reconstruída todas as vezes que o alvo tivesse que ser gerado.
Com a barra | DEPENDÊNCIA1 só será reconstruída se, de fato, tiver mudado.

4.1.1. Observação sobre o controle das modificações

O GNU Make não mantém um registro do timestamp dos arquivos. Em vez disso, para cada regra no Makefile, ele compara as informações de data e hora da ultima modificação dos alvos com as das suas dependências. Então, por exemplo, se o objeto main.o foi modificado às 10:00 e o binário teste às 9:50, o make recompilará teste.

4.2. Dica 2: comando 'mkdir'

Para que o utilitário mkdir não pare com erro no caso do diretório que se pretende criar já existir, nós utilizamos a opção -p (--parents):

mkdir -p DIRETÓRIO

4.3. Dica 3: comando 'rm'

Além da opção -f, utilizada para que o rm não reclame da remoção de arquivos inexistentes, a opção -r (-R ou --recursive) é obrigatória para remoção de diretórios:

rm -rf ARQUIVOS DIRETÓRIOS

5. Segundo mão na massa: vamos evitar a fadiga

No nosso Makefile, a lista dos arquivos fonte foi definida explicitamente:

CFILES = main.c utils.c

Mas, se o projeto crescer (ou diminuir) ao longo do desenvolvimento, isso nos obrigaria a voltar ao arquivo de construção várias vezes para atualizar a lista de fontes manualmente – o que, além de cansativo, é altamente suscetível a erros.

Sendo assim, não seria bom se pudéssemos utilizar os curingas do shell (*, ? e [...]) em vez disso?

5.1. Dica: funções 'wildcard' e 'shell'

O GNU Make tem algumas funções que podem ser chamadas para expandir e transformar listas. Entre elas, duas podem nos ajudar neste desafio:

5.1.1. Função 'wildcard'

VAR = $(wildcard PADRÃO)

Onde PADRÃO é uma descrição de nomes de arquivos utilizando os mesmos curingas do shell.

5.1.2. Função 'shell'

VAR = $(shell COMANDO)

Onde COMANDO pode ser a invocação de qualquer comando do shell que resulte na lista desejada.

6. Declarações de dependências

Como vimos quando alteramos o cabeçalho do nosso projeto, o GNU Make não detecta a necessidade de reconstruir o alvo. Contudo, se for necessário, nós também podemos declará-los como dependências da construção dos objetos.

Para demonstrar como isso pode ser feito, vamos alterar um pouco o projeto, que agora terá os seguintes fontes:

- main.c: sem alterações e depende de utils.h;

- utils.c: agora depende de utils.h e macros.h;

- utils.h: agora, só declara a função soma;

- macros.h: novo cabeçalho com a constante simbólica XCONST.

No novo Makefile, portanto, nós declararemos explicitamente as dependências dos cabeçalhos para a construção dos objetos:

# Compilador e flags
CC = gcc
INCDIRS = -I.
OPT = -O0
CFLAGS = -Wall -Wextra -g $(INCDIRS) $(OPT)

# Diretórios
BUILDDIR = build
BINDIR = bin

# Fontes e objetos
CFILES = $(wildcard *.c)
OBJECTS = $(CFILES:%.c=$(BUILDDIR)/%.o)

TARGET = $(BINDIR)/teste

# Alvo padrão
all: $(TARGET)

# Como gerar o executável
$(TARGET): $(OBJECTS) | $(BINDIR)
$(CC) -o $@ $^

# Como gerar objetos a partir dos fontes
$(BUILDDIR)/%.o: %.c | $(BUILDDIR)
$(CC) $(CFLAGS) -c -o $@ $<

# Dependências (explícitas) de cabeçalhos
$(BUILDDIR)/main.o: utils.h
$(BUILDDIR)/utils.o: utils.h macros.h

# Criação dos diretórios
$(BUILDDIR):
mkdir -p $@

$(BINDIR):
mkdir -p $@

# Limpeza
clean:
rm -rf $(BUILDDIR) $(BINDIR)

.PHONY: all clean

Nele, podemos observar que as dependências foram declaradas depois da regra da receita de construção dos objetos:

# Como gerar objetos a partir dos fontes
$(BUILDDIR)/%.o: %.c | $(BUILDDIR)
$(CC) $(CFLAGS) -c -o $@ $<

# Dependências (explícitas) de cabeçalhos
$(BUILDDIR)/main.o: utils.h
$(BUILDDIR)/utils.o: utils.h macros.h

Resumidamente, podemos dizer que regras com comandos, como aquela que descreve a geração dos objetos, é uma regra de receita. Na ausência de comandos, o Make interpreta as regras como informações adicionais de dependências. Deste modo, mudanças nos cabeçalhos acionam a recompilação dos objetos que dependem deles, mesmo sem comandos nessas regras.

Para testar, vamos construir o nosso novo projeto:

:~$ ls
macros.h main.c Makefile utils.c utils.h
:~$ make
mkdir -p build
gcc -Wall -Wextra -g -I. -O0 -c -o build/main.o main.c
gcc -Wall -Wextra -g -I. -O0 -c -o build/utils.o utils.c
mkdir -p bin
gcc -o bin/teste build/main.o build/utils.o
$ tree
.
├── bin
│ └── teste
├── build
│ ├── main.o
│ └── utils.o
├── macros.h
├── main.c
├── Makefile
├── utils.c
└── utils.h

3 directories, 8 files

Para simplificar, vamos alterar o timestamp de macros.h com o utilitário touch (é para isso que ele serve, não para criar arquivos):

:~$ touch macros.h
:~$ make
gcc -Wall -Wextra -g -I. -O0 -c -o build/utils.o utils.c
gcc -o bin/teste build/main.o build/utils.o

Note que apenas utils.o precisou ser gerado novamente. Contudo, se alterarmos o timestamp de utils.h…

:~$ touch utils.h
:~$ make
gcc -Wall -Wextra -g -I. -O0 -c -o build/main.o main.c
gcc -Wall -Wextra -g -I. -O0 -c -o build/utils.o utils.c
gcc -o bin/teste build/main.o build/utils.o

Desta vez, todos os objetos foram reconstruídos para compilar o binário.

7. Dependências automáticas com 'include' e GCC

Nos fontes anteriores, as inclusões foram feitas com:

#include <CABEÇALHO>

Aproveitando que a opção -I. faria com que o GCC os encontrasse no diretório corrente. Mas o GCC tem outra opção muito útil (e muito utilizada) para gerar arquivos de dependências de cabeçalhos para uso no Makefile: a opção -MMD, que escreve a saída do pré-processamento para um arquivo de extensão .d incluindo apenas os cabeçalhos de usuário, cujos arquivos são escritos entre aspas ("ARQUIVO").

Por este motivo, vamos alterar os includes nos nossos fontes…

Arquivo: main.c

#include <stdio.h>
#include "utils.h"

Arquivo: utils.c

#include "utils.h"
#include "macros.h"

O Makefile também terá que ser alterado na parte em que descrevemos a receita para a geração dos objetos e declaramos suas dependências:

# Compilador e flags
CC = gcc
INCDIRS = -I.
OPT = -O0
CFLAGS = -Wall -Wextra -g $(INCDIRS) $(OPT)

# Diretórios
BUILDDIR = build
BINDIR = bin

# Fontes e objetos
CFILES = $(wildcard *.c)
OBJECTS = $(CFILES:%.c=$(BUILDDIR)/%.o)

TARGET = $(BINDIR)/teste

# Alvo padrão
all: $(TARGET)

# Como gerar o executável
$(TARGET): $(OBJECTS) | $(BINDIR)
$(CC) -o $@ $^

# Como gerar objetos a partir dos fontes e gerar dependências
$(BUILDDIR)/%.o: %.c | $(BUILDDIR)
$(CC) $(CFLAGS) -MMD -c -o $@ $<

# Incluir automaticamente os arquivos de dependência (.d)
-include $(OBJECTS:.o=.d)

# Criação dos diretórios
$(BUILDDIR):
mkdir -p $@

$(BINDIR):
mkdir -p $@

# Limpeza
clean:
rm -rf $(BUILDDIR) $(BINDIR)

.PHONY: all clean

Agora, a receita tem a opção -MMD no GCC e a inclusão dos arquivos de dependências gerados é feita com a palavra-chave do Make include:

# Como gerar objetos a partir dos fontes e gerar dependências
$(BUILDDIR)/%.o: %.c | $(BUILDDIR)
$(CC) $(CFLAGS) -MMD -c -o $@ $<

# Incluir automaticamente os arquivos de dependência (.d)
-include $(OBJECTS:.o=.d)

O traço, antes de include, serve para que a construção não seja interrompida com erro no caso dos arquivos de dependências ainda não terem sido gerados.

Vamos construir nosso projeto e dar uma espiada no conteúdo dos arquivos .d:

$ make
mkdir -p build
gcc -Wall -Wextra -g -I. -O0 -MMD -c -o build/main.o main.c
gcc -Wall -Wextra -g -I. -O0 -MMD -c -o build/utils.o utils.c
mkdir -p bin
gcc -o bin/teste build/main.o build/utils.o
:~$ tree
.
├── bin
│ └── teste
├── build
│ ├── main.d
│ ├── main.o
│ ├── utils.d
│ └── utils.o
├── macros.h
├── main.c
├── Makefile
├── utils.c
└── utils.h

3 directories, 10 files
:~$ cat build/*.d
build/main.o: main.c utils.h
build/utils.o: utils.c utils.h macros.h

Repare que o conteúdo dos arquivos .d tem exatamente o formato utilizado para declarar as regras das dependências no nosso Makefile anterior. Ou seja, com o -include, o resultado foi:

# Como gerar objetos a partir dos fontes e gerar dependências
$(BUILDDIR)/%.o: %.c | $(BUILDDIR)
$(CC) $(CFLAGS) -MMD -c -o $@ $<

# Incluir automaticamente os arquivos de dependência (.d)
build/main.o: main.c utils.h
build/utils.o: utils.c utils.h macros.h

8. Automatizando instalações

Pode parecer algo muito complicado, mas o verbo instalar, em "instalar um programa", tem o sentido de "acomodar", que remete à ideia de "colocar em algum lugar". Na prática, significa que os binários executáveis e os outros arquivos necessários ao funcionamento do programa serão copiados para seus diretórios de destino no sistema operacional.

No Makefile, para o nosso projeto de exemplo, isso é tão simples como:

install: $(TARGET) | $(BINDIR_INSTALL)
cp $(TARGET) $(BINDIR_INSTALL)

O alvo nomeado como install, assim como all e clean, é apenas uma convenção adotada pela comunidade. Eles aparecem no manual do GNU Make como exemplos de alvos phony: ou seja, alvos que não são arquivos, mas identificadores de receitas que poderão ser processadas quando incluídas como argumentos do make na linha de comandos.

Por isso mesmo, é importante que eles sejam listados na regra com o alvo builtin .PHONY:

.PHONY: all clean install

Outra prática comum, quando se trata de automatizar instalações, é separar os diretórios de destino em caminhos que se complementam a partir de um diretório raiz. Neste nosso exemplo, a raiz da instalação será o diretório ./usr (para simular o /usr de sistemas Unix-like) e ele será associado a uma variável chamada DESTDIR no nosso Makefile:

DESTDIR = ./usr

Na sequência, o subdiretório de instalações é definido numa variável de nome PREFIX que, em sistemas GNU e Unix, seria /usr/local, por padrão. No nosso exemplo, porém será:

PREFIX = $(DESTDIR)/local

Desta forma, em cenários reais, bastaria trocar ./usr por /usr (em DESTDIR) para que PREFIX expandisse o diretório padrão para instalações feitas por usuários.

Na prática, PREFIX aponta para o diretório real de instalação (por padrão /usr/local), enquanto DESTDIR é usado como um prefixo temporário, muito útil para fins didáticos, de testes e para empacotamento de software.

O diretório padrão para instalações geralmente diz respeito à instalação de executáveis e bibliotecas. Arquivos de configurações, dados e documentos podem ir para outros diretórios: como /etc (configurações globais) e /usr/share (documentos de acesso global), embora a hierarquia do sistema de arquivos preveja o uso de /usr/local/etc e /usr/local/share para instalações feitas pelo usuário.

Essas práticas seguem a convenção documentada no GNU Coding Standards e no Filesystem Hierarchy Standard (FHS), que organizam de forma previsível onde programas, bibliotecas, configurações e dados devem ser instalados em sistemas Unix-like.

O nosso projeto só terá um binário executável a ser instalado, cujo caminho definiremos desta forma:

BINDIR_INSTALL = $(PREFIX)/bin

Por fim, vamos escrever nosso Makefile, aproveitando para organizar melhor as coisas:

# Compilador e flags
CC = gcc
INCDIRS = -I.
OPT = -O0
CFLAGS = -Wall -Wextra -g $(INCDIRS) $(OPT)

# Diretórios
BUILDDIR = build
BINDIR = bin
DESTDIR = ./usr
PREFIX = $(DESTDIR)/local
BINDIR_INSTALL = $(PREFIX)/bin

# Fontes e objetos
CFILES = $(wildcard *.c)
OBJECTS = $(CFILES:%.c=$(BUILDDIR)/%.o)
DEPS = $(OBJECTS:.o=.d)

TARGET = $(BINDIR)/teste

# Alvo padrão
all: $(TARGET)

# Como gerar o executável
$(TARGET): $(OBJECTS) | $(BINDIR)
$(CC) -o $@ $^

# Como gerar objetos e dependências
$(BUILDDIR)/%.o: %.c | $(BUILDDIR)
$(CC) $(CFLAGS) -MMD -c -o $@ $<

# Criação dos diretórios
$(BUILDDIR) $(BINDIR) $(BINDIR_INSTALL):
mkdir -p $@

# Instalação
install: $(TARGET) | $(BINDIR_INSTALL)
cp $(TARGET) $(BINDIR_INSTALL)

# Limpeza
clean:
rm -rf $(BUILDDIR) $(BINDIR) $(DESTDIR)

.PHONY: all clean install

# Inclusão automática de dependências
-include $(DEPS)

8.1. Construção e instalação

Antes da instalação, obviamente, o projeto precisa ser construído:

:~$ ls
macros.h main.c Makefile utils.c utils.h
:~$ make
mkdir -p build
gcc -Wall -Wextra -g -I. -O0 -MMD -c -o build/main.o main.c
gcc -Wall -Wextra -g -I. -O0 -MMD -c -o build/utils.o utils.c
mkdir -p bin
gcc -o bin/teste build/main.o build/utils.o
:~$ tree
.
├── bin
│ └── teste
├── build
│ ├── main.d
│ ├── main.o
│ ├── utils.d
│ └── utils.o
├── macros.h
├── main.c
├── Makefile
├── utils.c
└── utils.h

3 directories, 10 files

Para instalar:

:~$ make install
mkdir -p usr/local/bin
cp bin/teste ./usr/local/bin
:~$ tree
.
├── bin
│ └── teste
├── build
│ ├── main.d
│ ├── main.o
│ ├── utils.d
│ └── utils.o
├── usr
│ └── local
│ └── bin
│ └── teste
├── macros.h
├── main.c
├── Makefile
├── utils.c
└── utils.h

6 directories, 11 files

Numa instalação real, make install teria que ser executado com privilégios administrativos (com sudo ou como usuário root).

É exatamente por isso que, tradicionalmente, os programas no GNU/Linux são instalados com a sequência de comandos:

./configure
make
sudo make install

O script configure, também bastante comum, é gerado por uma ferramenta chamada autotools, que não faz parte do Make, mas é muito utilizada para gerar um Makefile de acordo com as opções de instalação passadas como argumento. Uma das opções mais comuns é justamente a definição do diretório de instalação associado à variável PREFIX, por exemplo:

:~$ ./configure PREFIX=/usr

9. Do que mais precisamos saber

Aqui estão mais algumas informações úteis e importantes, mas que não teremos espaço (e tempo) suficiente para abordar completamente nesta introdução.

9.1. Variáveis frequentemente associadas a diretórios padrão

Essas variáveis não são mágicas do Make: elas fazem parte de uma convenção seguida por projetos GNU e Unix, documentada no GNU Coding Standards e no Filesystem Hierarchy Standard (FHS).

Variável | Padrão | Uso típico
PREFIX | /usr/local | Raiz da instalação.
DESTDIR | (opcional) | Prefixo temporário (testes, empacotamento).
BINDIR | $(PREFIX)/bin | Diretório de executáveis.
LIBDIR | $(PREFIX)/lib | Diretório de bibliotecas.
INCLUDEDIR | $(PREFIX)/include | Diretório de cabeçalhos.
DATADIR | $(PREFIX)/share | Arquivos de dados independentes da arquitetura.
SYSCONFDIR | /etc | Arquivos de configuração globais.
MANDIR | $(DATADIR)/man | Páginas de manual.
DOCDIR | $(DATADIR)/doc | Documentação adicional.

9.2. Variáveis automáticas

O GNU Make disponibiliza algumas variáveis especiais para representar alvos e dependências nas definições de regras:

Variável | Expande
$@ | Nome do alvo da regra (por exemplo, o executável ou objeto a ser gerado).
$< | Primeira dependência listada (muito usado em regras de compilação).
$^ | Todas as dependências, sem repetições (útil para linkar vários objetos).
$? | Dependências mais novas que o alvo (serve para recompilar só o necessário).
$* | Nome base do arquivo, sem extensão (por exemplo, para transformar main.c em main.o, o $* vale main).

Essas variáveis tornam os makefiles mais curtos e genéricos.

9.3. Variáveis simples e recursivas

As variáveis podem ser definidas de duas formas principais no Make:

9.3.1. Atribuição recursiva (=)

O valor é expandido apenas quando usado. Isso significa que, se a variável referenciada mudar depois, a expansão refletirá a mudança.

Exemplo:

A = $(B)
B = valor
all:
@echo $(A) # imprime "valor"

Este tipo de atribuição é utilizado quando queremos adiar expansões de valores.

9.3.2. Atribuição simples (:=)

O valor é expandido no momento da definição e não muda depois, mesmo que variáveis expandidas na sua definição sejam alteradas.

Exemplo:

A := $(B)
B = valor
all:
@echo $(A) # imprime vazio

Este tipo de atribuição é utilizado quando queremos fixar valores logo no momento da atribuição.

9.4. Modificadores de comandos

Os modificadores de comandos são símbolos utilizados antes de comandos para modificar seu comportamento.

Os modificadores podem ser combinados (por exemplo, -@).

9.4.1. Modificador @ (arroba)

Suprime a impressão do comando antes de executá-lo. Sem ele, o Make ecoará a linha executada no terminal.

Exemplo:

all:
@echo "Compilando…" # imprime só a mensagem, sem 'echo ...'

9.4.2. Modificador - (menos)

Instrui o Make a ignorar erros na execução do comando. Normalmente, se um comando retornar erro (estado de término diferente de 0), o Make interromperá a execução.

clean:
-rm *.o # mesmo que não existam arquivos, não dá erro

9.4.3. Modificador + (mais)

Força a execução do comando mesmo quando o Make está sendo executado com a opção -n (ou --just-print), que apenas imprime os comandos sem executá-los (dry-run).

É útil, por exemplo, para invocar outro Make dentro de uma receita:

rebuild:
+make clean all

9.5. Alguns modos de execução

Opção | Efeito
make -n | Imprime os comandos que seriam executados sem executar nada (dry-run).
make -d | Exibe mensagens de depuração sobre decisões de dependência e regras.
make -p | Lista todas as variáveis e regras conhecidas (inclui internas).
make -q | Só verifica se há algo a ser reconstruído (retorna 0 se atualizado).

Esses modos são muito interessantes para explorar o funcionamento do Make de forma segura.

10. Exemplos para inspiração

10.1. Pequenos projetos em NASM (x86-64/Linux)

Com regras para compilar, ligar, executar e limpar:

# Arquivos
SRC = main.asm
OBJ = $(SRC:.asm=.o)
BIN = prog

# Alvo padrão
all: $(BIN)

# Montagem com NASM
%.o: %.asm
nasm -f elf64 -g -o $@ $<

# Linkagem
$(BIN): $(OBJ)
ld -o $@ $^

# Executar
run: $(BIN)
./$(BIN)

# Limpar
clean:
rm -f $(OBJ) $(BIN)

.PHONY: all run clean

10.2. Instalação de scripts em Bash

Instala o script em $(PREFIX)/bin, configurações em $(SYSCONFDIR) e documentação em $(DOCDIR):

PREFIX = /usr/local
BINDIR = $(PREFIX)/bin
SYSCONFDIR = /etc
DOCDIR = $(PREFIX)/share/doc/myscript

TARGET = myscript.sh

all:
@echo "Nada para compilar, apenas instalar."

install: $(TARGET)
install -Dm755 $(TARGET) $(BINDIR)/myscript
install -Dm644 myscript.conf $(SYSCONFDIR)/myscript.conf
install -Dm644 README.md $(DOCDIR)/README.md

clean:
@echo "Nada para limpar."

.PHONY: all install clean

10.3. Pequenos projetos em C

Apenas para compilar e executar:

CC = gcc
CFLAGS = -Wall -Wextra -g
SRC = main.c
BIN = app

all: $(BIN)

$(BIN): $(SRC)
$(CC) $(CFLAGS) -o $@ $^

run: $(BIN)
./$(BIN)

clean:
rm -f $(BIN)

.PHONY: all run clean

10.4. União de arquivos de texto

Concatena arquivos de texto e conta as linhas:

SRC = $(wildcard *.txt)
OUT = all.txt

# Junta todos os arquivos .txt
concat: $(OUT)

$(OUT): $(SRC)
cat $^ > $@

# Conta linhas
count:
wc -l $(SRC)

# Limpa
clean:
rm -f $(OUT)

.PHONY: concat count clean

Date: 13 de setembro de 2025 às 14h (UTC-3)

Author: Blau Araujo

Created: 2025-09-13 sáb 08:41

Validate

Table of Contents

- Sistemas de construção

- Um exemplo de construção com scripts

- O GNU Make

- Principais características

- Por que (ou quando) escolher o Make

- Uma pequena demonstração do GNU Make

- Estrutura, escrita e uso do arquivo de construção

- Executando o arquivo de construção

- Primeiro mão na massa: cada coisa em seu lugar

- Dica 1: ordem dos pré-requisitos
- Dica 2: comando 'mkdir'
- Dica 3: comando 'rm'

- Segundo mão na massa: vamos evitar a fadiga

- Dica: funções 'wildcard' e 'shell'

- Declarações de dependências

- Dependências automáticas com 'include' e GCC

- Automatizando instalações

- Construção e instalação

- Do que mais precisamos saber

- Variáveis frequentemente associadas a diretórios padrão

- Variáveis automáticas
- Variáveis simples e recursivas
- Modificadores de comandos
- Alguns modos de execução

- Exemplos para inspiração

- Pequenos projetos em NASM (x86-64/Linux)
- Instalação de scripts em Bash
- Pequenos projetos em C
- União de arquivos de texto