Aulas de C

Aprendizado continuo. Linguagem antiga e moderna

Utilitários Make, Makefiles e sua importância

OK…
Então continuaremos sem programar C real. Essa é a má notícia da “aula” de hoje.

A boa é que terminaremos o tópico que começamos na “aula” passada, quando falamos sobre o conceito de projeto, separação de códigos fonte e compilação individual.
Como vimos, é possível dividir um programa em arquivos fontes individuais (que formam, em conjunto, um projeto) e compilar os mesmos individualmente, de modo que no caso de uma modificação pontual não seja necessário recompilar totalmente os fontes para obter-se o executável. Isso deve-se ao fato de os compiladores modernos na verdade executarem duas funções simultaneamente: a compilação (transformação de códigos fontes em códigos objetos) e a linkedição ou ligação (a união de vários códigos objetos em binários que possam ser executados pela máquina).
Até aqui nenhuma novidade.
Mas lembremos novamente do que mostramos na aula passada. Programas “reais”, como pacotes Office e navegadores possuem milhões de linhas de código, que, por sua vez, podem estar espalhadas em milhares de arquivos de código fonte. Mesmo com essa divisão, a tarefa de gerar um novo código-objeto para cada fonte alterado e ligar todos os objetos em um executável seria MUITO enfadonha e propensa a erros.
Para resolver esse problema, antigamente usavam-se scripts específicos para cada plataforma de desenvolvimento e uso. Porém, isso ainda assim era ineficiente, pois a adição ou remoção de novos arquivos e a mudança na estrutura do projeto demandava a total modificação dos scripts, sendo que os próprios scripts tinham que ser mantidos, e eram enfadonhos de se manter.
Em 1977, porém, Stuart Feldman criou o primeiro sistema de automação para a compilação de programas, o make. A função do make é, construir todas as dependências descritas em um arquivo especial chamado Makefile. Makefiles seguem um padrão razoavelmente simples de construção. Embora o formato Makefile original seja um “padrão de facto“, muitos compiladores trazem consigo o seu próprio make, e IDEs, como a Code::Blocks, o Eclipse e o Netbeans também possuem suas próprias regras e mecanismos, usando ou não e baseado ou não no make UNIX.
Para explicarmos o conceito geral e demonstrarmos o funcionamento, utilizaremos o GNU Make. GNU Make é parte dos utilitários incluídos no GCC (GNU Compiler Chain), que é incluído em quase todas as distribuições Linux e está disponível em várias plataformas, como Windows, MacOS/X, etc…

Um Makefile simples:

Sem muitas delongas, vamos mostrar como o Make trabalha e como criar um Makefile:
Basicamente, make funciona em um sistema de alvos e dependências. Ou seja, make precisa saber quais são os arquivos que ele irá processar (dependências) para realizar alguma tarefa e obter alguma outra coisa (alvo). Por exemplo, vamos fazer um Makefile simples.

all:
    echo “Hello, Make!”

Aqui dizemos que queremos obter “all“, ou seja, tudo (é o default do GNU Make. Se não encontrado, executa o primeiro alvo de cima para baixo dentro do Makefile). Depois dos dois-pontos (:) indicaríamos qualquer dependência que precisássemos. Porém, como não temos nenhuma, deixamos em branco mesmo.
Em seguida, colocamos a tarefa a ser executada. Ela pode ser quaisquer seqüências de comandos válidos para o sistema operacional em questão. No nosso caso, utilizamos um comando echo “Hello, Make!”, que é usado no Linux para emitir uma mensagem na tela (no Windows também funciona). Uma coisa importante: os comandos da tarefa devem ser espaçados do início da linha por uma tabulação (tecla TAB). Embora algumas ferramentas make modernas consiga reconhecer espaços no lugar do TAB para efeito de indicação da tarefa, é melhor manter o padrão para não incorrer em problemas em outras plataformas.
Bem, digitado esse arquivo, salve-o com o nome de Makefile. Esse nome é o nome default que o GNU make (e a maioria dos demais) irá procurar. Em várias ferramentas make, é possível que você defina, por meio de uma opção de linha de comando, qual o Makefile a ser usado. Consulte o manual de sua ferramenta Make para maiores informações.
Bem, voltando: uma vez salvo o mesmo (no momento não interessa onde você irá o jogar), digite o comando “make” na linha de comandos de seu sistema operacional, no diretório onde você salvou o seu arquivo Makefile. A saída resultante será algo mais ou menos como a seguinte:

$ make
echo “Hello, Make!”
Hello, Make!

Não muito útil, mas já mostra resultados. Uma vez que você disparou o comando, ele procurou um alvo-padrão (all) e verificou se suas dependências estavam resolvidas (no momento nenhuma, então ok). Estando tudo OK, ele executou a tarefa determinada (no caso, o comando echo “Hello, Make!”) e se encerrou.
Exemplo bobo…

Um Makefile útil

Bem, pelo menos sabemos como ele funciona. Agora vamos fazer algo realmente útil:
Vá ao diretório onde você guardou o seu projeto do Roletrando que mostramos na “aula” passada. Vamos criar um Makefile um pouco mais interessante:

#
# Primeira tentativa de Makefile
#

all: roletrando
   
roletrando: main.o regras.o letras.o
    gcc -o roletrando main.o regras.o letras.o

main.o: main.c roletrando.h
    gcc -o main.o -c main.c

regras.o: regras.c roletrando.h
    gcc -o regras.o -c regras.c

letras.o: letras.c roletrando.h
    gcc -o letras.o -c letras.c

Agora temos um Makefile realmente útil. Primeira coisa que você deve ter notado é que, de certa forma, colocamos todos os comandos que usaríamos em uma compilação parcial normal, como vimos anteriormente:
Sobre arquivos de código-fonte, arquivos de cabeçalhos e projetos « Aulas de C
No nosso caso, utilizaremos primeiro os comandos:

gcc -o letras.o -c letras.c
gcc -o regras.o -c regras.c
gcc -o main.o -c main.c

Para gerarmos os arquivos .o (os códigos-objeto) de cada um dos código-fonte e, após isso, utilizaremos o comando:

gcc -o roletrando letras.o regras.o main.o

Para fazermos o link das funções e obtermos o executável.

Agora, precisamos entender o que estamos fazendo.
As primeiras linhas de nosso Makefile:

#
# Primeira tentativa de Makefile
#

São apenas comentários. No caso do make, os comentários utilizam o símbolo sustenido, ou sharp, ou qualquer outro nome que você já ouviu falar (vale até lasanha… :D). Como no C e em qualquer outra linguagem ou ferramenta, os comentários são simplesmente ignorados.

A linha seguinte:

all: roletrando

Indica que, para o make atingir o alvo all, ele tem como dependência que executar o alvo roletrando. Importante notar que, no make, uma dependência pode ser quaisquer arquivos E outros alvos. Isso é importante e veremos abaixo o por que.

Outra coisa: repare que esse alvo não possui tarefas. O que fizemos aqui é criar uma espécie de alvo “nulo”, ou phony. Simplesmente fizemos isso para “chamar a atenção” do make para cá. Normalmente ele executa o primeiro alvo de cima para baixo dentro do Makefile por padrão se ele não encontrar um alvo all. Em alguns casos, porém, você pode querer que o make gere vários alvos ao mesmo tempo (por exemplo, se você desenvolver um sistema composto por vários programas). Nesse caso, basta listar outros alvos como dependências em all.
Seguindo adiante, vemos uma entrada “completa” de um alvo (chamado também de regra):
roletrando: main.o regras.o letras.o
    gcc -o roletrando main.o regras.o letras.o

Uma regra é composta pelo nome da regra (o alvo), uma ou mais dependências, e comandos que permita ao sistema obter essa dependência. O make precisa dessa informação para fazer suas tarefas. No caso acima:
  • O nome da regra (alvo) é roletrando;
  • As dependências são main.o regras.o letras.o;
  • O comando a ser executado é gcc -o roletrando main.o regras.o letras.o;

Perceba como a coisa está construída: quando usamos várias dependências, elas são separadas por espaço. Além disso, normalmente o nome da regra (ou alvo) é o nome do arquivo final a ser obtido por aquela regra (no nosso caso, o binário roletrando). O comando (ou comandos) devem seguir após a linha com a descrição do alvo e das dependências. Uma regra é separada da outra por uma linha em branco.
OK… Aqui vemos que o make sabe que, para obter o arquivo roletrando, ele precisa ter as dependência (ou seja, os arquivos) main.o, regras.o e letras.o. Além disso, ele sabe que, tendo esses arquivos, ele precisará executar o comando gcc -o roletrando main.o regras.o letras.o para obter o seu alvo e, portanto, cumprir a regra.
Mas até aqui, o Makefile não sabe como obter nenhuma das dependências em questão.
Para isso, incluímos as linhas seguintes:

main.o: main.c roletrando.h
    gcc -o main.o -c main.c

regras.o: regras.c roletrando.h
    gcc -o regras.o -c regras.c

letras.o: letras.c roletrando.h
    gcc -o letras.o -c letras.c

Aqui, vemos que colocamos as diversas regras para gerar cada um dos arquivos .o (objetos) necessários como dependência em roletrando. Perceba que as dependências são os nomes dos arquivos de fonte .c e (no nosso caso), o arquivo de cabeçalho roletrando.h. Isso visa garantir que o arquivo de cabeçalho exista e, no caso de ele ser modificado (por exemplo devido a uma função modificada) o código seja reconstruído de maneira adequada. Claramente isso vai depender de como funciona seu código fonte: haverá situações onde adicionar muitos arquivos fonte ou cabeçalhos gerará inconvenientes (como compilações desnecessárias). Mas isso não vem ao caso agora.
O make checará se (1) os arquivos indicados nas dependências existem ou se (2) eles são alvos resolvidos por outras regras. Se ambas as coisas não forem possíveis, ele irá dar uma mensagem de erro como a seguinte (No caso, renomeei o roletrando.h, exigido por main.o, para outro nome qualquer):

make: *** Sem regra para processar o alvo `roletrando.h’, necessário por `main.o’.  Pare.

Estando tudo OK, ao rodar-se o comando make (por via das dúvidas, antes apague todos os arquivos .o e o arquivo roletrando), ele irá mostrar algo como abaixo:

$ make
gcc -o main.o -c main.c
gcc -o regras.o -c regras.c
gcc -o letras.o -c letras.c
gcc -o roletrando main.o regras.o letras.o

Vamos descrever o que aconteceu:

  1. make foi executado, procurou a regra default all e a encontrou. Viu que ela demandava um alvo chamado roletrando. Então foi para a regra roletrando;
  2. Em roletrando, ele viu que precisava dos arquivos (ou alvos) main.o, regras.o e letras.o. Antes de fazer qualquer coisa, ele irá verificar se ele tem uma regra que explique como obter esses arquivos.
  3. O make percebe que existe uma regra para main.o. Ela demanda os arquivos main.c e roletrando.h. Verificando que os dois existem, ele verifica se o arquivo indicado no alvo existe e é mais novo que os arquivos de dependência (o que indica que nenhum deles foi alterado). Caso contrário, ele irá executar as regras para obter-se uma nova versão do arquivo main.o (ou obtê-lo, caso não exista).
  4. Se houver alguma falha (por exemplo, um erro de sintaxe), o make irá abortar sua execução, não gerando nenhum binário e não criando o roletrando.
  5. Em caso de sucesso no passo 3 para main.o, o sistema repetirá o processo dos passos 3 e 4 para os demais arquivos de dependência regras.o e letras.o. Se algum deles exigisse um outro arquivo gerado por um outro alvo, os passos 3 e 4 seriam repetidos para tal arquivo e assim sucessivamente. make realiza uma pesquisa recursiva para ver se cada alvo necessáriao como dependência de cada outro alvo (incluindo all) foi obtido com sucesso.
  6. O passo 5 tendo sido realizado com sucesso para TODOS os alvos e suas dependências (incluindo outros alvos), o make irá executar os comandos indicados na regra roletrando para obter o arquivo roletrando, resolvendo a dependência all;
  7. Resolvida todas as dependências de all (roletrando e suas dependências), make irá se encerrar com sucesso;
Agora, após rodar make, você terá o arquivo roletrando. Se tentar rodar make novamente, verá que ele não fará absolutamente NADA, pois ele sabe que nenhuma das dependências de roletrando foi modificada (nehum dos .o), pois todas existem e são mais atuais que qualquer modificação em quaisquer um dos arquivos .c.
Vamos simular uma alteração em um dos arquivos .c (no caso, o arquivo regras.c). Para isso:
  • em Windows, abra o arquivo em um editor e o salve sem alterar nada;
  • no Linux, na linha de comando, dê o comando touch regras.c;

Execute então o comando make. Sua saída deve ser como a seguinte:

$ make
gcc -o regras.o -c regras.c

gcc -o roletrando main.o regras.o letras.o

Por que isso aconteceu?
Make analisa o timestamp do arquivo (o registro de quando o mesmo foi alterado pela última vez) de todas as dependências necessárias e, caso o timestamp de uma dependência seja maior que o do arquivo do alvo (ou seja, a dependência em questão foi alterada), ele executa novamente a regra para ele e para todas as regras que tenha o alvo como dependência e assim sucessivamente. Portanto:

  1. O sistema identificou que a dependência regras.c de regras.o mudou e reconstruiu regras.o;
  2. Então o make percebeu que a dependência regras.o de roletrando mudou e, portanto, reconstruiu roletrando;

Ou seja, você não precisa se preocupar com re-executar manualmente os comandos em questão. Estando tudo OK, o próprio make irá executar e processar os arquivos adequados para gerar um binário.

Varíaveis e Macros no Makefile

Bem, agora temos um Makefile útil…
Mas agora pense em uma coisa…
Aqui colocamos regras, alvos e dependências para CADA arquivo fonte. Isso em sistemas pequenos é bem útil, mas conforme o sistema aumenta de tamanho e funcionalidades, obviamente aumenta o número de arquivos de código fonte. Ou seja, teríamos que fazer mais entradas de regras, mais dependências…
Além disso, imagine que você deixa de usar o gcc como compilador. Você provavelmente teria que modificar todos os alvos e regras para usar os padrões do novo compilador, o que seria um pesadelo!
O make, porém, salva a nossa vida, oferecendo variáveis e macros que nos ajudam a fazer com que o make realize tarefas “genéricas” (como gerar um objeto .o a partir de um fonte .c), além de embutir alguma inteligência para que ele descubra se existem novos arquivos .c no local e coisas do gênero. No caso, digite o seguinte Makefile e o salve no diretório do roletrando com o nome Makefile-1:

#
# Segunda tentativa de Makefile
#

C_COMP=gcc
FONTES=$(wildcard *.c)    # equivale a dizer FONTES=main.c regras.c letras.c
HEADERS=$(wildcard *.h)

all: roletrando
   
roletrando: $(FONTES:.c=.o)
    $(C_COMP) -o $@ $^

%.o: %.c $(HEADERS)
    $(C_COMP) -c $< -o $@

Perceba que ele parece muito mais complexo do que o nosso Makefile anterior. Mas não se preocupe que iremos descrevê-lo aos poucos.
Primeiro vamos pegar as primeiras três linhas:

C_COMP=gcc

FONTES=$(wildcard *.c)    # “equivale” a dizer FONTES=main.c regras.c letras.c


HEADERS=$(wildcard *.h)

Aqui estamos criando três variáveis: C_COMP, FONTES e HEADERS. A primeira variável, C_COMP, indica o compilador que estamos usando. O nome poderia ser qualquer um (à exceção de alguns nomes que o make reserva para uso próprio, que não detalharemos aqui), e abaixo veremos o porque. No nosso caso atual, definimos que C_COMP=gcc, o que lembra uma atribuição em C. Estamos atribuindo à variável do Makefile C_COMP esse valor.
A variável seguinte, FONTES, é atribuída com $(wildcard *.c).
O que isso quer dizer, afinal de contas?“.
Quando você usa o símbolo de $, quer dizer que você está usando uma macro do make. As macros são certos recursos embutidos no make que estão disponíveis para facilitar sua vida na criação de um Makefile. Quando usamos $() cercando algo, estamos indicando uma macro de expansão, ou seja, vamos fazer com que o make entenda que o valor que está ali equivale ao de um comando ou variável previamente definido. No nosso caso, usamos o comando wildcard para pedir que o make realize uma busca no diretório onde o Makefile se encontra e localize todos os arquivos que seguem o padrão indicado. No nosso caso, $(wildcard *.c), pode ser lido como “procure todos os arquivos .c no diretório onde você está e os dê como valor onde você está”. Colocamos em seguida um comentário:
# “equivale” a dizer FONTES=main.c regras.c letras.c

para dizer o nosso resultante. No fim das contas, usar:


FONTES=$(wildcard *.c)

equivale a dizer, na situação atual

FONTES=main.c regras.c letras.c

Em seguida, criamos uma varíavel HEADERS, com o mesmo tipo de conteúdo de FONTES.
Uma coisa importante de dizer aqui, que não foi dita, é que as variáveis podem conter um valor só (como em C_COMP), ou vários valores separados entre si por espaço (como no caso da “expansão” de FONTES). Isso é importante em alguns casos, como veremos adiante.
Após estipularmos o alvo all, com dependência roletrando (igual ao nosso primeiro Makefile), definimos nosso alvo roletrando.
roletrando: $(FONTES:.c=.o)
    $(C_COMP) -o $@ $^

“Que coisa maluca é essa pelamordeus?!”
Calma. Aqui estamos usando várias macros para resumir nosso serviço.
Primeiro, vamos olhar a parte das dependências: $(FONTES:.c=.o)
O que isso quer dizer?
O make possui alguma inteligência para saber que determinados arquivos geram outros determinados arquivos. Por isso, ele é capaz de “traduzir” nomes por substituição simples se corretamente indicado. No caso, estamos usando o :.c=.o na frente de FONTES. Isso indica ao make para entender que, ao expandir FONTES ali, ele deve substituir todas as extensões .c para .o. Ou seja, ele será capaz de montar uma lista dos arquivos objetos necessários a partir da lista de arquivos fontes (que colocamos na variável FONTES, lembra).
Em seguida, temos a regra para obtermos os alvos: $(C_COMP) -o $@ $^
Temos a expansão de C_COMP, o que já deve ser claro, e um -o que é parâmetro do gcc (aqui fica uma sugestão: o ideal aqui é que outra variável contivesse todos os parâmetros da compilação, uma vez que eles podem mudar de compilador para compilador). Em seguida temos duas macros novas: $@ e $^. No caso, $@ deve ser lido como “o alvo a ser alcançado” e $^ indica “a lista de dependências passadas”.
Como tudo isso então se comporta, no fim das contas?

  1. make irá expandir a lista de dependências. Para isso, irá olhar o valor de FONTES, pegar qualquer valor listado em FONTES que termine com .c e modificará o valor dele nessa expansão para .o. Considerando que FONTES equivale, nesse momento a main.c regras.c letras.c, ele expadirá FONTES como main.o regras.o letras.o nas dependências de roletrando;
  2. Em seguida, ele irá montar a regra para atingir-se o alvo roletrando. Primeiro irá expandir a variável C_COMP (valendo gcc), colocará o -o na frente do mesmo (que, como não é uma macro ou variável é deixado como está), após isso inserindo o nome do alvo (no caso, roletrando) e a lista de dependências do mesmo! O valor final para a regra expandida será gcc -o roletrando main.o regras.o letras.o (ou valor similar: na realidade normalmente será gcc -o roletrando letras.o main.o regras.o, pois a expansão com wildcard em FONTES gera um lista em ordem alfabética dos arquivos cujo nome casem com o padrão desejado);

OK, então temos o comando para gerar o executável a partir dos objetos… Mas e quanto a geração dos objetos a partir dos fontes?
O make tem outra boa inteligência que é permitir que um alvo seja estabelecido a partir de um padrão, ou melhor, que haja uma regra padrão para alvos de um determinado tipo. Um exemplo de alvo assim está no final do nosso novo Makefile:

%.o: %.c $(HEADERS)
    $(C_COMP) -c $< -o $@

O símbolo de porcento (%) serve para indicar um padrão genérico que pode ser usado na regra como um todo. Colocando no alvo, ele diz ao make que ele sabe o que fazer para qualquer arquivo que obedeça o padrão em questão. No nosso caso, o alvo passa a ser “qualquer arquivo que termine em .o“. O bom é que esse valor passa a ser “salvo” e pode ser usado na dependência, como fizemos aqui: nossa dependência é %.c $(HEADERS), ou seja, “qualquer arquivo que tenha o mesmo nome do alvo, mas termine em .c” e o valor da variável HEADERS (que é um wildcard de arquivos .h).
A linha de regra tem algumas similaridades com o que temos na linha de regra para roletrando: primeiro expandimos C_COMP para gcc e temos um -c que é mantido como está (lembrando que -c apenas compila o arquivo oferecido como entrada, sem fazer link, gerando um arquivo objeto).
Em seguida, temos a macro $<. Essa macro pega o primeiro valor listado na lista de dependências e o usa como valor. Perceba que, se analisarmos a lista de dependências, teremos normalmente o arquivo .c e um ou mais header files. Por isso, usamos apenas o primeiro item da lista (é bom deixar o header file como dependência, para o caso de alteração, mas o que precisamos usar mesmo é o arquivo de fonte .c) como parâmetro aqui. Em seguida, temos a montagem do resto da regra, que é idêntica a como fizemos no alvo roletrando: -o como está e o $@ sendo expandido para o nome do alvo.
Como então se dá a execução passo a passo do Makefile desde que mandamos o comando make, nesse caso?
Ele irá expandir a regra roletrando e ver se todos os arquivos .o existem. Caso algum deles não exista ou seja mais antigo que o seu  .c, ele perceberá isso e, usando o alvo genérico %.o, irá gerar o .o necessário a partir do .c. Em seguida irá gerar o alvo roletrando.
Apague todos os arquivos .o e mande executar nosso Makefile com make -f Makefile-1. Sua saída provavelmente será algo como abaixo:
gcc -c letras.c -o letras.o
gcc -c main.c -o main.o
gcc -c regras.c -o regras.o
gcc -o roletrando letras.o main.o regras.o

Ou seja, ele fará todo o serviço necessário sem você criar regras individuais e aumentar muito a complexidade do seu Makefile.
Um efeito colateral importante do uso de wildcards é que qualquer arquivo pego pelo padrão é usado como valor de variável (e como dependência, em geral). Isso pode acarretar problemas. Por exemplo, mantendo nosso Makefile como está, crie um arquivo .c em branco no diretório do roletrando. Não interessa o nome, apenas crie (para usuários de Linux, use o comando touch o_nome_do_meu_arquivo_aleatório.c), sem conteúdo nenhum. Após isso, execute o Makefile-1 como fizemos anteriormente. Você perceberá que ele irá achar o arquivo random.c na lista de fontes e colocará o arquivo random.o como parte das dependência de roletrando (devido à expansão), compilando random.c com a regra genérica.
Isso parece bobagem, uma vez que random.c está vazio… Mas tente, por exemplo, copiar um arquivo que tenha uma função main() para dentro de roletrando. Ao usar wildcards, você pode incorrer no risco de, cedo ou tarde, provocar erros de compilação e ligação devido à inclusão inadvertidade de um arquivo .c que contenha um código para uma função similar a uma que já exista em seu código. Tome muito cuidado quanto a isso.
Uma sugestão seria não utilizar as expansões com wildcards e apenas incluir os nomes de arquivo fontes, separados por espaço, em FONTES. Ainda demandaria alguma edição no Makefile após a inclusão de um novo arquivo, mas isso permitiria uma melhor administração dos fontes a serem compilados. Além disso, o resto das regras e macros do Makegile continuariam com sua utilidade mantida.
No caso de a pessoa tentar isso, ela não poderia normalmente dar ENTER e em seguida colocar o nome de um arquivo (considera-se que o valor da variável termina de ser lido em um caracter nova-linha). Porém, é possível “enganar” o sistema para que ele ache que um ENTER não é o iniício de uma nova linha. Para isso, coloque “\” antes de dar ENTER e siga em frente. O ENTER dado não será lido pelo make (será “escapado”) e o valor na linha de baixo será acrescentado aos demais normalmente. Por exemplo,

FONTES=fonte1.c fonte2.c fonte3.c
       fonte4.c

Faria com que você recebesse a mensagem de erro “Makefile:2: *** faltando o separador.  Pare.“. Mas:

FONTES=fonte1.c fonte2.c fonte3.c \
       fonte4.c

Seria correto e retornaria a expansão para fonte1.c fonte2.c fonte3.c fonte4.c.

Bem, estamos quase acabando esse assunto chato de Makefiles. Vamos apenas falar de um último tópico.

Phony targets (Alvos nulos)

Lá no início, quando falamos do alvo defaultall“, mencionamos o fato de ele ser um phony target, um alvo nulo…

Na verdade, phony targets são alvos que não vão gerar algum arquivo de saída. Um alvo assim tem como objetivo facilitar atividades que o desenvolvedor necessita fazer, como limpar os arquivos objeto antigos, empacotar um tarball (um arquivo compactado .tar.gz ou .tar.bz2), entre outros.
Uma coisa importante é que o mesmo não irá gerar arquivos, mas é bom por via das dúvidas indicar que esses alvos são Phony Targets para que o make não exija um arquivo com o nome do Phony Target ou para evitar que a existência de um arquivo com o mesmo nome do alvo faça com que a regra pare de funcionar. Para isso, utilizamos o alvo especial .PHONY para isolar todos os Phony Targets por meio dele. Caso contrário, podemos receber mensagens de erros como a seguinte, ao utilizar uma Phony Target clean, por exemplo:
make: *** Sem regra para processar o alvo `clean’.  Pare.

Abra seu arquivo Makefile-1 e adicione as seguintes linhas ao final do mesmo:

clean:
    rm -f *.o roletrando *~

tar:
    tar cvjf roletrando.tar.bz2 $(FONTES) $(HEADERS)

Seu arquivo Makefile-1 deverá estar parecendo algo assim:

#
# Segunda tentativa de Makefile
#

C_COMP=gcc
FONTES=$(wildcard *.c)    # equivale a dizer FONTES=main.c regras.c letras.c
HEADERS=$(wildcard *.h)

all: roletrando
   
roletrando: $(FONTES:.c=.o)
    $(C_COMP) -o $@ $^

%.o: %.c $(HEADERS)
    $(C_COMP) -c $< -o $@

clean:
    rm -f *.o roletrando *~

tar:
    tar cvjf roletrando.tar.bz2 $(FONTES) $(HEADERS)

Perceba que ainda não definimos clean e tar como Phony Thargets. Agora, vamos definir nossas Phony Thargets. Antes de all, inclua a seguinte regra:
.PHONY: all clean tar

Perceba que agora definimos eles como Phony Targets, perceba que incluimos all. Essa é uma boa prática, incluir all como Phony Target para evitar problema maiores no caso da existência de uma arquivo all. Teste as regras clean e tar, lembrando de usar -f Makefile-1 para indicar o arquivo de Makefile desejado (se você colocar essas regras no arquivo Makefile padrão, não precisará do -f). Muitos softwares livres incluem em seus makefiles Phony Targets como clean, install, package, etc… Tente criar vários Phony Targets para fazer operações de instalação, remoção, limpeza dos objetos antigos, compactação, e por aí afora.
O nosso Makefile “genérico” deve ficar como o abaixo. Esse é um ótimo Makefile padrão para ser adotado no dia a dia:

C_COMP=gcc
FONTES=$(wildcard *.c) 
HEADERS=$(wildcard *.h)

.PHONY: all clean tar

all: roletrando
   
roletrando: $(FONTES:.c=.o)
    $(C_COMP) -o $@ $^

%.o: %.c $(HEADERS)
    $(C_COMP) -c $< -o $@

clean:
    rm -f *.o roletrando *~

tar:
    tar cvjf roletrando.tar.bz2 $(FONTES) $(HEADERS)

Para saber mais

Não pretendo falar por um bom tempo mais sobre Makefiles. Essa introdução deve ser o suficiente para o dia a dia e para o que precisaremos por um LONGO TEMPO no nosso “curso”. Se você desejar maiores informações, abaixo alguns links úteis:

Além dos seguintes tutoriais, onde me baseei para escrever esse artigo:

Bem, agora chega de falarmos de ferramentas. Prometo na próxima “aula” voltar à programação em C, com estruturas e tipos do usuário. Até lá!

4 Respostas para “Utilitários Make, Makefiles e sua importância

  1. Pingback: Tipos de Dados Personalizados em C – Estruturas « Aulas de C

  2. Railson Ramés 05/08/2013 às 15:43

    Gostaria primeiramente de agradecer pelo excelentíssimo conteúdo disponibilizado, e deixar a mensagem de que foi de grande valia para a minha massificação do makefile, sou aluno de Segurança da Informação aqui em Brasília e em estrutura de dados foi aderida a utilização do make nas nossas aplicações, com toda certeza vou levar informações extras obtidas aqui para a sala de aula.

  3. douglaszs 31/08/2014 às 0:04

    Muito obrigado! Finalmente consegui entender um Makefile graças a sua aula! Obrigado!

  4. Az 11/10/2015 às 17:39

    Muito obrigado, com sua explicação finalmente entendi. \o/

Deixe uma resposta

Preencha os seus dados abaixo ou clique em um ícone para log in:

Logotipo do WordPress.com

Você está comentando utilizando sua conta WordPress.com. Sair / Alterar )

Imagem do Twitter

Você está comentando utilizando sua conta Twitter. Sair / Alterar )

Foto do Facebook

Você está comentando utilizando sua conta Facebook. Sair / Alterar )

Foto do Google+

Você está comentando utilizando sua conta Google+. Sair / Alterar )

Conectando a %s

%d blogueiros gostam disto: