Dependències automàtiques en un Makefile

Una eina molt utilitzada per compilar programari és GNU Make. Aquest programa, cridat amb la comanda make, llegeix un fitxer on es descriuen els arxius necessaris per generar un executable a més dels executables a generar en el procès de compilació. Aquesta llista de fitxers i executables són els objectius que GNU Make ha de construir.

Per determinar si un objectiu s'ha de construir o actualitzar GNU Make determina si aquest existeix o està antiquat. Si no existeix llavors s'ha de generar. Si ja existeix, comprova la data de l'executable o fitxer objecte amb la data dels fitxers de codi font que figuren com a requisit. Si el codi font té una data de modificació posterior al objectiu GNU Make invocarà la recepta de generació de l'objectiu per actualitzar-lo.

Aquest sistema per tant funcionarà bé sempre que totes les dependències estiguin especificades. En programes amb codi font escrit en C o C++ això significa especificar també les capçaleres on es declaren les classes i tipus de dades. Això pot significar tenir moltes dependències que cal ficar en les llistes de requisits de cada recepta de generació de fitxers. Per no crear receptes i regles que dificulten la genericitat i automatització d'un Makefile, el compilador GCC proporciona unes opcions que permeten generar les llistes de requisits automàticament. Aquestes llistes de requisits no són més que regles de Make i es poden escriure en fitxers, que seran fitxers de Make o makefiles.

Començem definint el directori on guardar l'especificació de dependències i les opcions del compilador per generar les dependències:

DEPDIR := .deps
$(shell mkdir -p $(DEPDIR) > /dev/null)
# '=' en comptes de ':=' per a una expansió retardada

DEPFLAGS = -MMD -MP -MT $@ -MF $(DEPDIR)/$*.Td
DEPRENAME = @mv -f $(DEPDIR)/$*.Td $(DEPDIR)/$*.d && touch $@


El directori és .deps, el creem si no existeix. Les opcions per a le generació de automàtica de dependències són:
  • -MMD: Generar durant el preprocès del fitxer les seves dependències i escriure-les en un fitxer d'extensió «.d». És equivalent a -M -MF però sense l'opció -E i sense afegir les capçaleres del sistema operatiu, només les del programa en qüestió. -E només executa el preprocessador, sense desprès compilar el codi preprocessat.
  • -MP: (Make Phony?). Afegeix objectius PHONY per a les capçaleres autogenerades.
  • -MT: (Make Target?). Especifica com a objectiu autogenerat la variable $@, que correspon a l'objectiu ja existent.
  • -MF: (Make File?). Fitxer on escriure les dependències autogenerades. Sobreescriu el fitxer de sortida que per defecte utilitza l'opció -MMD.
En la variable DEPRENAME fiquem una comanda per renombrar l'arxiu de dependències amb el nom final acaban en «*.d» i actualitzem la data de modificació per assegurar-nos que quedi actualitzat. Alguna versió antiga de GCC pot deixar l'arxiu objectiu amb una data anterior a l'arxiu de dependències, provocant reconstruccions innecessàries.

Un cop tenim les variables necessàries definides, passem a especificar les regles i receptes del Makefile:

CXXSRCS:=$(shell find . -name \*.cpp) 
CXXOBJS:=$(CXXSRCS:.cpp=.o)
 
.PRECIOUS: $(DEPDIR)/%.d:
$(DEPDIR)/%.d: ;

%.o: %.c
%.o: %.c $(DEPDIR)/%.d
    $(CC) $(DEPFLAGS) $(CXXFLAGS) -c -o $@ $<
    $(DEPRENAME)
%.o: %.cpp
%.o: %.cpp $(DEPDIR)/%.d
    $(CXX) $(DEPFLAGS) $(CXXFLAGS) -c -o $@ $<
    $(DEPRENAME) 
 
program: $(CXXOBJS)
    $(CXX) $(CXXFLAGS) -o $@ $^
 
include $(wildcard $(patsubst %,$(DEPDIR)/%.d,$(basename $(CXXSRCS)))

Les regles són els patrons de coincidència amb noms abans dels dos punts «:», com per exemple %.o, que coincideix amb tots els fitxers acabats en «.o». Seguidament trobem els requisits de la regla que corresponen al fitxer coincident amb la regla. Les receptes son les comandes sota la línia de la regla (patró i requisits). Han d'anar tabulades (caràcter «\t», invisible en un editor de text).

Especifiquem com a objectiu .PRECIOUS el fitxer de dependències. PRECIOUS indica a GNU Make que no l'esborri com faria amb qualsevol altre fitxer intermedi.
Seguidament fiquem una regla buida per als fitxers de dependències. Això evita que GNU Make s'aturi al no trobar el fitxer de dependències quan executi una recepta que el tingui com a requisit. Si no el troba, recursivament busca una regla de noms coincident per construir el requisit. La trobarà, no farà res i continuarà amb el procès de construcció. GNU Make considerarà l'objectiu de la recepta inicial antiquat i l'actualitzarà. El punt i coma, «;», delimita la recepta.

Desprès, a cada objectiu del Makefile afegim com a requisit el fitxer de dependències. A la recepta afegim la variable amb les opcions del compilador que permeten generar les dependències així com la comanda per reanomenar el fitxer que les conté. Cada cop que es refaci un objectiu s'actualitzaran també tots els fitxers de prerequisits. Per als fitxers «*.o» sobreescrivim la regla per defecte de GNU Make afegint una regla buida i desprès introduïm la regla personalitzada amb les dependències. Això aconsegueix l'efecte de cancel·lar la regla implícita per defecte. En cas contrari GNU Make trobaria el nom coincident primer amb la regla interna (regla implícita) que té en comptes de la regla escrita manualment.

Finalment, apareix una directiva per incloure els fitxers de dependències existents. Un fitxer de dependències és un Makefile amb les regles que permeten mantenir un seguiment de les mateixes, és a dir, dels fitxers indicats per aquestes. Així doncs, incloem aquests fitxers Make al principal. Els que faltin ja es construiran. Si no existeixen es perquè cal reconstruir l'objectiu analitzat. S'executarà la recepta de l'objectiu inicial que té com a requisit la dependència i generarà el fitxer amb les dependències de l'objectiu per a futures execucions de GNU Make, comprovant la data de modificació i reconstruint l'objectiu si alguna dependència (codi font o capçalera) és més recent que el fitxer objectiu.

Al manual de GNU Make s'indica un mètode diferent per implementar la generació automàtica de dependències. És un mètode més universal que no utilitza funcionalitat específica de GCC que pot no estar disponible en altres compiladors. Veure «Generating pre-requisites automatically». Utilitza l'opció -M i la comanda sed. -M és universalment suportat i sed és una comanda disponible en qualsevol entorn GNU, BSD o Windows amb Cygwin (o més recentment, WSL).

%.d: %.c
 @set -e; rm -f $@; \
 $(CC) -M $(CPPFLAGS) $< > $@.$$$$; \
 sed 's,\($*\)\.o[ :]*,\1.o $@ : ,g' < $@.$$$$ > $@; \
 rm -f $@.$$$$ 

El problema d'aquesta regla de generació és que fa dos passades per cada fitxer font: una per generar els prerequisits i altra per compilar el fitxer de codi font. A més, cal eliminar manualment el fitxer de dependències (el «*.d») si esborrem les capçaleres o fitxers que es corresponen amb ell.

Comentaris