risorse | dipendenze automatiche con make

Dipendenze automatiche con make

Introduzione

Quel che segue è uno studio circa la gestione automatica delle dipendenze di compilazione nei makefile con l'ausilio del compilatore C++ di Visual Studio 2013 Express for Windows Desktop.

Il problema

Supponiamo di dover compilare un file sorgente (main.cpp) che include due file d'intestazione (a.h, b.h), il secondo dei quali ne include un terzo (c.h):

[main.cpp]
#include <a.h>
#include <b.h>

#include <iostream>

int main() {
  std::cout << "Hello, World!" << std::endl;
}
[a.h]
// empty
[b.h]
#include <c.h>
[c.h]
// empty

Un makefile minimale per la compilazione del file sorgente è il seguente:

[makefile]
all : main.exe

%.exe : %.obj
	@link "$<" /nologo /out:"$@"

%.obj : %.cpp
	@cl /I. /c /EHsc /nologo "$<" /Fo"$@"

clean :
	@del *.obj

Supponendo che i file sorgente e il makefile siano salvati nella stessa cartella, la compilazione avviene invocando make da un Prompt dei comandi degli strumenti di VS2013:

C:\…>make
main.cpp
rm main.obj

C:\…>main
Hello, World!

Il file oggetto è stato quindi compilato e l'eseguibile linkato. Chiamare nuovamente make non produce alcun effetto, com'è lecito attendersi:

C:\…>make
make: Nothing to be done for 'all'.

Se il file main.cpp viene modificato, la successiva invocazione di make produce la nuova versione dell'eseguibile:

C:\…>rem modifica di main.cpp
C:\…>make
main.cpp
rm main.obj

Nulla accade però se si modifica uno qualunque dei file di intestazione:

C:\…>rem modifica di a.h, b.h o c.h
C:\…>make
make: Nothing to be done for 'all'.

Per fare in modo che il file oggetto venga ricompilato anche in seguito alla modifica di uno dei file di intestazione è necessario elencare anch'essi tra i prerequisiti:

[makefile]
...

clean :
	@del *.obj

main.obj : main.cpp a.h b.h c.h

Con i nuovi prerequisiti, il file oggetto ora viene ricompilato:

C:\…>make
main.cpp

Non è pratico gestire manualmente questo tipo di dipendenze; sarebbe più comodo determinarle automaticamente.

Determinazione delle dipendenze di compilazione

Il compilatore C++ del Visual Studio 2013 è in grado di produrre l'elenco dei file inclusi direttamente e indirettamente da un file C++[7]:

C:\…>cl /I. /Zs /EHsc /nologo /showIncludes main.cpp
Includes main.cpp
main.cpp
Note: including file: .\a.h
Note: including file: .\b.h
Note: including file:  .\c.h
Note: including file: %VCINSTALLDIR%\INCLUDE\iostream
Note: including file:  %VCINSTALLDIR%\INCLUDE\istream
Note: including file:   %VCINSTALLDIR%\INCLUDE\ostream
…

È dunque possibile ottenere automaticamente l'elenco delle inclusioni di un file sorgente; resta da capire come utilizzare questa informazione per raggiungere l'obiettivo di ricompilare il file oggetto anche in seguito alla modifica di uno qualunque dei file di intestazione.

Generazione automatica dei prerequisiti

Nella sezione 4.14 del manuale di make, intitolata “Generating Prerequisites Automatically”, si legge:

The practice we recommend for automatic prerequisite generation is to have one makefile corresponding to each source file. For each source file name.c there is a makefile name.d which lists what files the object file name.o depends on. That way only the source files that have changed need to be rescanned to produce the new prerequisites.

Introduciamo quindi un nuovo makefile main.d che specifica i prerequisiti del relativo file oggetto:

[main.d]
main.obj main.d : main.cpp a.h b.h c.h

La creazione di questo makefile ausiliario sarà demandato ad uno script apposito di cui ci si curerà più avanti; per il momento ci accontentiamo di una soluzione ad hoc:

[make-d-file.cmd]
@echo main.obj main.d : main.cpp a.h b.h c.h>main.d

La presenza del target main.d garantisce che in caso di modifiche al file sorgente – o a una qualunque sua dipendenza – non solo il file oggetto viene ricompilato, ma viene rigenerato pure il makefile delle dipendenze. Il makefile principale diventa:

[makefile]
...

main.obj : main.cpp a.h b.h c.h

%.d : %.cpp
	@make-d-file.cmd

clean :
	@del *.obj *.d

include main.d

Questa nuova configurazione è equivalente alla precedente, ed è immediato verificare che il file oggetto viene ricompilato anche in seguito alla modifica di uno qualunque dei file di intestazione:

C:\…>rem modifica di c.h
C:\…>make
makefile:15: main.d: No such file or directory
main.cpp

Il messaggio d'errore è dovuto all'assenza del file main.d alla prima invocazione di make. Nei build successivi l'anomalia non si presenta più:

C:\…>rem modifica di a.h
C:\…>make
main.cpp

Nel caso venga modificato l'albero delle inclusioni del file main.cpp, per esempio per l'introduzione di una nuova direttiva #include nel file a.h, avendo make già letto – tramite l'istruzione include main.d – l'elenco non aggiornato delle dipendenze dal makefile main.d, non c'è il rischio che il file oggetto non venga aggiornato? No: make rivaluta da capo il makefile principale se anche uno solo dei makefile inclusi risulta aggiornato (cfr. sezione 3.5 del manuale di make, intitolata “How Makefiles Are Remade”):

To this end, after reading in all makefiles, make will consider each as a goal target and attempt to update it. If a makefile has a rule which says how to update it (found either in that very makefile or in another one) or if an implicit rule applies to it (see Using Implicit Rules), it will be updated if necessary. After all makefiles have been checked, if any have actually been changed, make starts with a clean slate and reads all the makefiles over again. (It will also attempt to update each of them over again, but normally this will not change them again, since they are already up to date.)

Per verificare che l'aggiornamento del makefile delle dipendenze causa la rivalutazione dell'intero makefile è sufficiente inserire una chiamata $(warning… ) a inizio file:

[makefile]
$(warning Running makefile...)
...

C:\…>make
makefile:1: Running makefile...
make: Nothing to be done for 'all'.

C:\…>rem modifica di c.h
C:\…>make
makefile:1: Running makefile...
makefile:1: Running makefile...
main.cpp

Se l'esecuzione del makefile impiega tempo o risorse non trascurabili, questo comportamento potrebbe rivelarsi fastidioso, se non innaccettabile. Non solo: in caso di cancellazione o rinomina di un prerequisito, make termina con un errore fatale:

C:\…>ren c.h c1.h
C:\…>rem modifica di b.h di conseguenza
C:\…>rem modifica di make-d-file.cmd di conseguenza
C:\…>make
makefile:1: Running makefile...
make: *** No rule to make target 'c.h', needed by 'main.d'.  Stop.

Per uscire dall'empasse non c'è altro da fare che cancellare il file delle dipendenze e rilanciare make:

C:\…>del main.d
C:\…>make
makefile:1: Running makefile...
makefile:17: main.d: No such file or directory
makefile:1: Running makefile...
main.cpp

Generazione “furba” dei prerequisiti

I due problemi appena evidenziati sono stati brillantemente risolti da Tom Tromey, il cui metodo è alla base del meccanismo di generazione delle dipendenze di automake. L'idea è di ribaltare il ruolo file delle dipendenze main.d: anziché considerarlo come l'elenco delle dipendenze del file oggetto sulla base del quale decidere se avviare una nuova compilazione – e in quanto tale da creare prima della compilazione –, lo si può considerare come un'istantanea delle dipendenze scattata nell'istante della compilazione – e quindi prodotto in concomitanza con la compilazione.

Se uno o più prerequisiti sono cambiati, di fatto è inutile ricalcolare il nuovo elenco delle dipendenze: proprio per il fatto che almeno una di esse è stata modificata, il file oggetto deve essere ricompilato, e poco importa sapere a questo punto quali sono le dipendenze effettivamente in gioco. Vale invece la pena conservare l'elenco delle nuove dipendenze per la prossima invocazione di make.

Con la nuova tecnica, main.d non è più un target, ma un semplice sottoprodotto della compilazione; la regola %.d: %.cpp è quindi superflua. Per la stessa ragione, l'istruzione include non causa più la rivalutazione del makefile principale, ed è quindi lecito ignorare l'errore emesso in caso di file non disponibile:

[make-d-file.cmd]
@echo main.obj main.d : main.cpp a.h b.h c1.h>main.d
[makefile]
...

%.d : %.cpp
	@make-d-file.cmd

%.obj : %.cpp
	@cl /I. /c /EHsc /nologo "$<" /Fo"$@"
	@make-d-file.cmd

clean :
	@del *.obj *.d

-include main.d

Il nuovo makefile non presenta più problema della doppia esecuzione:

C:\…>rem modifica di a.h
C:\…>make
makefile:1: Running makefile...
main.cpp

L'assenza del file delle dipendenze non genera più la condizione d'errore vista prima:

C:\…>del main.d main.obj main.exe
C:\…>make
makefile:1: Running makefile...
main.cpp
rm main.obj

Resta il problema dell'errore fatale nel caso uno dei prerequisiti di main.d non sia disponibile:

C:\…>ren c1.h c.h
C:\…>rem modifica b.h di conseguenza
C:\…>rem modifica make-d-file.cmd di conseguenza
C:\…>make
makefile:1: Running makefile...
make: *** No rule to make target 'c1.h', needed by 'main.d'.  Stop.

Anche questo problema è di facile soluzione, grazie ad una peculiarità di make (cfr. sezione 4.7 del manuale di make, intitolata “Rules without Recipes or Prerequisites”):

If a rule has no prerequisites or recipe, and the target of the rule is a nonexistent file, then make imagines this target to have been updated whenever its rule is run. This implies that all targets depending on this one will always have their recipe run.

È dunque sufficiente aggiungere, per ogni dipendenza, una regola senza prerequisiti nè comandi:

[make-d-file.cmd]
@echo main.obj main.d : main.cpp a.h b.h c.h>main.d
@echo.>>main.d
@echo a.h :>>main.d
@echo.>>main.d
@echo b.h :>>main.d
@echo.>>main.d
@echo c.h :>>main.d
@echo.>>main.d
C:\…>make-d-file
C:\…>type main.d
main.obj: main.cpp a.h b.h c.h

a.h:

b.h:

c.h:

C:\…>make
makefile:1: Running makefile...
main.cpp

C:\…>ren c.h c2.h
C:\…>rem modifica b.h di conseguenza
C:\…>rem modifica make-d-file.cmd di conseguenza
C:\…>make
makefile:1: Running makefile...
main.cpp

Poiché ora il file delle dipendenze viene generato all'atto della compilazione, in caso di cancellazione accidentale le dipendenze del file sorgente associato non verranno ricalcolate – e per tale motivo non saranno rispettate – fino alla prossima compilazione del file oggetto:

C:\…>del main.d
C:\…>rem modifica di a.h -- dovrebbe causare la ricompilazione di main.obj
C:\…>make
makefile:1: Running makefile...
make: Nothing to be done for 'all'.

Rigenerazione delle dipendenze mancanti

Per forzare la rigenerazione del file delle dipendenze è sufficiente inserire esso stesso tra i prerequisiti del file oggetto; in questo modo la sua assenza causerà la ricompilazione di main.obj che produrrà, come effetto collaterale, il file mancante:

[makefile]
...

%.obj : %.cpp %.d
	@cl /I. /c /EHsc /nologo /showIncludes "$<" /Fo"$@" | %PYTHON% make-d-file.py "$@" "$<" -o "$*.d"

%.d: ;
...

Vale la pena notare la sintassi delle regole implicite prive di comandi (cfr. sezione 5.9 del manuale di make, intitolata “Using Empty Recipes”). È immediato verficare che ora i file delle dipendenze non disponibili vengono generati alla prima esecuzione del makefile:

C:\…>del main.d
C:\…>make
makefile:1: Running makefile...
main.cpp

Generazione automatica del file delle dipendenze

Grazie al flag /showIncludes è facile approntare uno script python che, ricevuto in ingresso l'output del compilatore, produce il file *.d associato al sorgente in fase di compilazione:

[make-d-file.py]
import optparse
import os.path
import re
import subprocess
import sys

INCLUDE_MARK = "Note: including file:" # warning, localized text!
INCLUDE_MARK_LEN = len(INCLUDE_MARK)

SYS_INCLUDE_DIRS = [os.getenv(x) for x in ['WindowsSdkDir', 'VCINSTALLDIR']]

def to_unix_path(path):
    return path.replace("\\", "/").replace(" ", "\\ ")

def is_sys_include(path):
    for dir in SYS_INCLUDE_DIRS:
        if path.startswith(dir):
            return True
    return False

if __name__ == "__main__":

    parser = optparse.OptionParser(
        usage="usage: %prog <object> <source> [options]")
    parser.add_option("-a", "", dest="list_all_header_files",
        action="store_true", help="list system header files too")
    parser.add_option("-o", "", dest="file", default="",
        help="write to <FILE> instead of STDOUT")
    (options, args) = parser.parse_args()

    if len(args) != 2:
        parser.error("incorrect number of arguments")

    target = args[0]
    source = args[1]

    paths = []
    for line in sys.stdin:
        if line.startswith(INCLUDE_MARK):
            path = line[INCLUDE_MARK_LEN:].strip()
            if options.list_all_header_files or not is_sys_include(path):
                paths.append(to_unix_path(path))
        else:
            sys.stdout.write(line)

    if options.file:
        output = open(options.file, "w")
    else:
        output = sys.stdout

    output.write("{0} : {1}".format(target, source))
    for path in paths:
        output.write(" \\\n\t%s" % path)
    output.write("\n\n")
    for path in paths:
        output.write("%s :\n\n" % path)

Il makefile diventa:

[makefile]
...

%.obj : %.cpp %.d
	@cl /I. /c /EHsc /nologo /showIncludes "$<" /Fo"$@" | %PYTHON% make-d-file.py "$@" "$<" -o "$*.d"
	@make-d-file.cmd
...
C:\…>del main.d main.obj main.exe
C:\…>make
makefile:1: Running makefile...
main.cpp
rm main.obj

C:\…>type main.d
main.obj : main.cpp \
        ./a.h \
        ./b.h \
        ./c2.h

./a.h :

./b.h :

./c2.h :

Riferimenti

  1. McPeak, S. “Autodependencies with GNU make”. scottmcpeak.com. <http://scottmcpeak.com/autodepend/autodepend.html>. Visitato il 18/09/2014.
  2. Smith, P. D. “Advanced Auto-Dependency Generation”. mad-scientist.net. <http://mad-scientist.net/make/autodep.html>. Visitato il 18/09/2014.
  3. “GNU `make'”. GNU Operating System. <http://www.gnu.org/software/make/manual/make.html>. Visitato il 18/09/2014.
  4. “How to Get Dependencies from /showIncludes”. The Conifer Systems Blog. <http://www.conifersystems.com/2008/10/09/dependencies-from-showincludes/>. Visitato il 18/09/2014.
  5. “Solving the Dependency Problem in Software Build”. Electric Cloud. <http://electric-cloud.com/wp-content/uploads/2014/06/Solving-the-Dep-Prob-Sftw-Blds.pdf>. Visitato il 18/09/2014.
  6. “/showIncludes (List Include Files)”. MSDN. <http://msdn.microsoft.com/en-us/library/hdkef6tk.aspx>. Visitato il 18/09/2014.

Pagina modificata il 25/09/2014