Skip to content

Introdução ao make

by em 31/07/2015

Sempre que usamos uma IDE para criar um software, normalmente um conjunto de regras é gerado para que os códigos-fonte sejam compilados em um binário. Mas e quando não queremos depender de uma IDE específica? Neste caso, podemos usar o comando make!

Como obter

O comando make está disponível principalmente para Linux (se não estiver incluso por padrão, deve ser facilmente encontrado no gerenciador de pacotes utilizado pela sua distribuição),Mac OS X (fica disponível com a instalação do Xcode usando a App Store) e Windows (http://gnuwin32.sourceforge.net/packages/make.htm).

Funcionamento

Uma vez instalado, executar o comando make em uma pasta onde não haja um arquivo makefile nos retornará algo parecido com isso:

make: *** No targets specified and no makefile found.  Stop.

Isso é sinal de que ele está corretamente instalado e pronto para o uso.

Para que o comando make execute alguma ação, necessitamos de um arquivo com o nome makefile ou Makefile (nomes diferentes podem ser usados, mas não serão encontrados automaticamente e deverão ser referenciados com a opção -f).

Este arquivo contém a seguinte estrutura básica:

target: [prerequisite ...]
<Tab>commands;
(É importante que todas as linhas de commands sejam indentadas com um caracter de tabulação, ou o arquivo não vai funcionar como esperado)

target é o nome do arquivo ou uma lista de arquivos que serão atualizados quando esta regra for referenciada. Caso target não corresponda a um nome de arquivo, esta regra será executada sempre que for referenciada.

prerequisite é uma lista de arquivos e/ou regras separados por espaço que serão checadas com o target:

  • Se prerequisite for um arquivo, a data de modificação deste arquivo é comparada com a data de modificação do arquivo referenciado por target, e será executada se target for mais antigo que prerequisite.
  • Se prerequisite for outro target, esta regra será executada se o outro target for executado.

Muito confuso? Vamos usar um exemplo:

Suponha que na sua pasta atual contenha os arquivos main.c, code.c e code.h.

O código que usaríamos para gerar um executável com estes códigos-fonte provavelmente seria:

gcc -o main -I. main.c code.c

Um makefile equivalente a isso seria feito dessa forma:

all:
gcc -o main -I. main.c code.c;

(Chamar o comando make sem especificar um target faz com que o primeiro seja executado)

Agora, podemos simplesmente, na linha de comando, chamar o comando make e nosso código será compilado como esperado!

Neste exemplo, nosso makefile tem um target chamado all, que não possui prerequisites. Ou seja, sempre que make ou make all for executado, a linha gcc -o main -I. main.c code.c; será executada, uma vez que não temos prerequisites que impeçam a regra de ser executada.

Checando arquivos

Vamos agora supor que o nossos códigos-fonte sejam arquivos muito complexos, e que suas compilações sejam bastante demoradas. Podemos melhorar nosso makefile:

all: main.o code.o main

clean:
    rm *.o main;

main: main.o code.o
    gcc main.o code.o -o main;

main.o: main.c
    gcc -I. main.c;

code.o: code.c
    gcc -I. code.c;

Agora, nosso makefile tem 5 regras com targets diferentes: all, clean, main, main.o e code.o. Vamos analisar cada regra separadamente, na ordem inversa:

code.o: code.c e main.o: main.c

No primeiro caso, nosso target é o arquivo code.o, o prerequisite é o arquivo code.c e o comando a ser executado é gcc -I. code.c;. Esta regra só será executada:

  • se o arquivo code.o não existir ou
  • se o arquivo code.c tiver uma data de modificação mais recente que code.o.

Neste caso, nosso makefile só vai compilar code.c em code.o se for realmente necessário.

O segundo caso funciona de forma similar ao primeiro, com a diferença de que os arquivos tratados serão main.o e main.c

main: main.o code.o

Nesta regra, temos 2 pré-requisitos: main.o e code.o. Quando esta regra for chamada:

  • se os arquivos main.o e/ou code.o não existirem, a regra de cada um que não existir será chamada;
  • se os arquivos main.o e/ou code.o existirem mas forem mais novos que `main`, esta regra será executada;

Se pelo menos 1 dos casos for verdade, esta regra será executada. Caso contrário, ela será ignorada (o que implica que o nosso main está atualizado)

clean:

Este target é o que chamamos de “Phony Target”, pois este não referencia um arquivo, e seu comando não vai gerar um arquivo com este nome. Portanto, sempre que este target for chamado, ele será executado (neste caso, vai apagar todos os arquivos que terminarem com .o e nosso binário main).

all: main.o code.o main

Esta regra não tem comandos, apenas pré-requisitos. Mas isso não significa que esta regra seja inútil, muito pelo contrário: uma vez que não temos um arquivo com o nome all, todos os prerequisites serão analisados e, quando necessário, atualizados um a um. Se todas as regras forem ignoradas, nada será executado, e nosso ambiente permanecerá do mesmo jeito.

Simplificando nosso makefile

Legal, nosso makefile funciona bem, mas cá entre nós, esse é um exemplo bem simples, com poucos arquivos em um projeto. Mas e se tivermos vários arquivos? Vamos ter que criar uma regra pra cada um deles? E se esquecermos alguma regra?

Para resolver esses problemas, vamos simplificar e generalizar nosso makefile:

EXECUTABLE=main
OBJS=main.o code.o
CFLAGS=-I.
LFLAGS=

all: $(OBJS) $(EXECUTABLE)

clean:
    rm $(OBJS) $(EXECUTABLE);

$(EXECUTABLE): $(OBJS)
    gcc $(LFLAGS) $^ -o $@;

%.o: %.c
    gcc $(CFLAGS) $^;

Nossa, quanta diferença! Mas calma, não é tão complicado assim… Vamos, novamente, por partes:

Variáveis

Neste exemplo, declaramos 4 variáveis: EXECUTABLE, OBJS, CFLAGS e LFLAGS. Variáveis contém textos (strings) que podem ser substituídos nas regras. Por exemplo, nosso target all agora tem como dependências o conteúdo das variáveis OBJS e EXECUTABLE. Da mesma forma, clean apaga todos os arquivos listados em OBJS e EXECUTABLE.

Variáveis são referenciadas por $(VARNAME) e podem ser declaradas de 3 formas diferentes:

VARNAME=VALUE

Esta forma de declaração vai ser atualizada toda vez que for referenciada. No exemplo:

VAR1=$(VAR2)
VAR2=foo

A variável VAR1 vai ter o mesmo valor de VAR2 (neste caso, foo, mas pode ter um valor diferente caso VAR2 seja modificada posteriormente).

O único problema que esta forma implica é que uma linha que contenha VAR1=$(VAR1) foo vai resultar em uma recursão infinita, pois ela vai sempre se referenciar e se atualizar. Além disso, se referenciarmos uma variável que pode se alterar ao longo da execução, o valor de VAR1 pode se tornar instável. Para este caso, temos a próxima forma de declaração.

VARNAME:=VALUE ou VARNAME::=VALUE

As duas formas possuem o mesmo comportamento, e as referências de variáveis na declaração serão resolvidas apenas no momento da declaração. No exemplo:

VAR1=foo
VAR2:=$(VAR1)
VAR1=bar

VAR2 vai conter o valor foo, independente do momento que for referenciada.

VARNAME?=VALUE

Com esta forma, VARNAME só receberá VALUE se esta ainda não existir. Caso contrário, a linha será ignorada.

Variáveis Automáticas ($@ e $^)

Estas variáveis são automaticamente preenchidas e simplificam bastante a escrita de uma regra. No nosso caso, usamos duas delas: $@, que referencia o target e $^, que referencia as dependências. Há várias outras variáveis automáticas, com as mais diversas finalidades.

Wildcards

Vamos falar agora da última mudança que fizemos no nosso exemplo, a regra que utiliza os sinais %. O conceito é bem simples: ele funciona exatamente como um wildcard, e vai conter os mesmos valores tanto no target quanto no prerequisite. Isso significa que essa regra vai ser executada toda vez que uma regra de um arquivo que termine com .o seja chamada, e usará como prerequisite o arquivo de mesmo nome com a terminação .c. Assim, evitamos a necessidade de referenciar todos os arquivos que precisamos para poder compilar nosso executável.

Considerações Finais

Este não é nem de longe um guia exaustivo do comando make. Apenas algumas noções básicas presentes em quase todo makefile foram apresentadas. Mas com isso já podemos criar nossas próprias regras e tornar nossa rotina de compilação muito mais fácil e simples.

Se tiver alguma dúvida, crítica ou sugestão, por favor, comente!😀

Como referência, cito os seguintes links:

Deixe um comentário

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: