2009-03-31

Makefiles

Tenho estado a trabalhar para outras cadeiras e não tenho tido muito tempo para acabar o gestor de memória. Como já tinha este post sobre Makefiles em draft há uns tempos aqui fica...


Já vimos anteriormente alguns usos de um Makefile e também como criar um de forma simples.

Também já mostrei um Makefile com macros e regras automáticas. Na altura não expliquei nada mas prometi que o fazia num post futuro. Vou fazer isso neste post.

Esse Makefile era assim:

OBJECT_FILES=prog.o mymalloc.o dict.o
CFLAGS=-g

prog : $(OBJECT_FILES)
$(CC) $(CFLAGS) -o $@ $(OBJECT_FILES)

.c.o :
$(CC) $(CFLAGS) -c -o $@ $<;

dict.o : dict.h
mymalloc.o : mymalloc.h

E escrito numa forma “simples” seria assim:

prog : prog.o dict.o mymalloc.o
gcc -g -o prog prog.o dict.o mymalloc.o

prog.o : prog.c
gcc -g -c prog.c

dict.o : dict.c dict.h
gcc -g -c dict.c

mymalloc.o : mymalloc.c mymalloc.h
gcc -g -c mymalloc.c

Vamos começar por este e gradualmente transformá-lo no outro.

Don’t Repeat Yourself

O Makefile “simples” tem alguns defeitos que vão contra o princípio DRY (Don’t Repeat Yourself). Logo na primeira regra podemos ver a repetição dos ficheiros prog.o, dict.o e mymalloc.o nas dependências e no comando.

As outras três regras são bastante semelhantes entre si:

  • Todas elas produzem um ficheiro .o.
  • Todas elas dependem de um ficheiro .c
  • Todas elas usam o mesmo comando para compilar, sendo a única diferença o nome do ficheiro .c.

Vamos então tornar isto “mais DRY” evitando toda esta repetição.

Nomes

Vamos começar pela primeira regra. Vamos dar um nome ao conjunto dos ficheiros prog.o, dict.o e mymalloc.o e depois vamos usar esse nome no lugar desses ficheiros. Num Makefile um nome (também chamado macro) define-se muito simplesmente assim:

NOME=VALOR

No nosso Makefile ficamos com:

OBJECT_FILES=prog.o dict.o mymalloc.o

O nome OBJECT_FILES não tem nada a ver com programação orientada a objectos. Os ficheiros .o são conhecidos como ficheiros-objecto, mas apenas por razões históricas, não por qualquer relação com os objectos de POO.

Agora vamos substituir todas as ocorrências de prog.o dict.o mymalloc.o pelo nosso nome. Para usar um nome num Makefile temos de rodear o nome por parêntesis e precedê-lo com um $, assim:

$(NOME)

A primeira regra fica então assim:

prog : $(OBJECT_FILES)
gcc -g -o prog $(OBJECT_FILES)

Regras implícitas

As regras para produzir os ficheiros prog.o, dict.o e mymalloc.o seguem o mesmo padrão. Podemos capturar esse padrão numa regra implícita. Uma regra implícita é uma regra para transformar qualquer ficheiro de um determinado tipo noutro. No nosso caso queremos transformar ficheiros .c em ficheiros .o. Para isso vamos escrever uma regra com o alvo .c.o, assim:

.c.o :
gcc -g –c ????

Isto define uma regra onde um ficheiro .o depende de um ficheiro .c com o mesmo nome. Para produzir esse ficheiro .o usa-se o comando especificado.

Mas falta uma coisa. Temos de passar como argumento para o gcc o nome do ficheiro .c! Mas como é que sabemos o nome, se esta regra é para todos? Existe uma macro pré-definida que serve para isso. Não é a escolha mais óbvia, mas há quem goste deste tipo de notação. Essa macro chama-se $<. Sempre que aparecer esta macro num comando ela é substituída pelo nome da primeira dependência.

No nosso caso, a primeira dependência, apesar de não ser explícita é o ficheiro .c. Podemos então escrever a regra assim:

.c.o :
gcc -g –c $<

Existe uma outra macro com um função semelhante: $@. Esta macro é substituída pelo nome do objectivo. No nosso caso seria o ficheiro .o. Usando a flag –o do gcc para explicitar o nome do ficheiro de saída, podemos escrever a regra implícita assim:

.c.o :
gcc -g –c –o $@ $<

Esta última alteração não era absolutamente necessária, mas serviu para mostrar o uso da macro $@. Também podemos usá-la na primeira regra:

prog : $(OBJECT_FILES)
gcc –g -o $@ $(OBJECT_FILES)

Mas as três regras que seguem este padrão são ligeiramente diferentes! As duas últimas dependem não só dum ficheiro .c mas também dum ficheiro .h. Felizmente podemos definir dependências adicionais separadamente, assim:

dict.o : dict.h
mymalloc.o : mymalloc.h

Desta forma se alterarmos o ficheiro dict.h o make vai refazer dict.o. E para isso vai usar a regra implícita que definimos. Se alterarmos o ficheiro dict.c, dict.o será recriado na mesma, devida à dependência implícita de dict.o e dict.c definida na regra implícita.

Macros pré-definidas

Uma outra diferença entre o nosso Makefile “simples” e o outro é o uso explicito do gcc. Apesar do gcc ser bastante massificado, podemos querer usar um outro compilador. Para isso teríamos de alterar todo o Makefile e substituir todas as ocorrências de gcc. Ou então usamos a macro pré-definida $(CC):

prog : $(OBJECT_FILES)
$(CC) -g -o $@ $(OBJECT_FILES)

.c.o :
$(CC) -g -c -o $@ $<

Assim podemos ter outro compilador instalado e usar o mesmo Makefile. Também podemos definir a variável de ambiente CC e o make usa esse compilador em lugar do gcc:

$ CC=othercc make

Existem outras macros pré-definidas para outros programas. Podem ver uma lista dessas macros com:

$ make –p

Ainda mais DRY

Depois de todas estas alterações temos o seguinte:

OBJECT_FILES=prog.o mymalloc.o dict.o

prog : $(OBJECT_FILES)
$(CC) -g -o $@ $(OBJECT_FILES)

.c.o :
$(CC) -g -c $<

dict.o : dict.h
mymalloc.o : mymalloc.h

Ainda podemos ver um outro padrão que se repete: a flag –g. Se quisermos compilar sem informação de debug, temos de retirar a flag de todos os comandos. Vamos então capturar este padrão com uma macro que vamos chamar CFLAGS, conforme é convencional.

OBJECT_FILES=prog.o mymalloc.o dict.o
CFLAGS=-g

prog : $(OBJECT_FILES)
$(CC) $(CFLAGS) -o $@ $(OBJECT_FILES)

.c.o :
$(CC) $(CFLAGS) -c $<

dict.o : dict.h
mymalloc.o : mymalloc.h

Agora, se quisermos compilar com optimizações em vez de informações de debug, podemos simplesmente mudar CFLAGS para:

CFLAGS=-O2

Targets tradicionais

Os Makefiles podem ser usados para mais do que simples compilação. Vamos definir um alvo clássico. Serve para apagar ficheiros temporários, ficheiros objecto, e ficheiros executáveis. Depois de correr este alvo apenas o código fonte deve restar.

clean :
$(RM) prog
$(RM) *.o
$(RM) *~
$(RM) *.bak

Aqui podemos ver que podemos definir vários comandos para uma só regra. Os comandos são executados por ordem. Se um deles falhar o make pára a execução.

Para correr este alvo basta fazer:

$ make clean

A macro $(RM) é, em UNIX, substituída por rm –f. Por isso, muito cuidado! Não escrevam nunca isto:

clean :
$(RM) *.c

Por razões óbvias.

Agora já é DRY

O Makefile final fica assim:

OBJECT_FILES=prog.o mymalloc.o dict.o
CFLAGS=-g

prog : $(OBJECT_FILES)
$(CC) $(CFLAGS) -o $@ $(OBJECT_FILES)

.c.o :
$(CC) $(CFLAGS) -c -o $@ $<;

dict.o : dict.h
mymalloc.o : mymalloc.h

clean :
$(RM) prog
$(RM) *.o
$(RM) *~
$(RM) *.bak

3 commentários:

Anónimo disse...

Obrigado pela explicação, estava em falta e estava com algumas dúvidas em alguns parametros. Ainda procurei na net e encontrei alguns documentos mas que explicavam mal.

Fico à espera de mais desenvolvimentos nos guiões de SO a ver se é desta que faço a cadeira, mas compreendo que tenhas outras cadeiras com que te preocupar.

Anónimo disse...

Muito bom mesmo este post, deu para alargar horizontes. Força e continua.

Martinho Fernandes disse...

Corrigi um erro neste post: A regra implícita para fazer ficheiros .o a partir de .c deve chamar-se .c.o e não .o.c como estava. Por vezes sou demasiado humano...

Enviar um comentário