Quando si deve lavorare con un progetto molto esteso, formato da decine di file sorgente, la compilazione ed il collegamento dei vari moduli può diventare un'operazione molto laboriosa, specialmente se si vuole evitare che siano ricompilati i file sorgenti che non sono stati modificati. Questo articolo mostra l'utilizzo del programma di utilità make che consente di gestire agevolmente questo genere di situazioni.
Premessa
Lo sviluppo di applicazioni reali comporta la gestione di un numero elevato di file; in questi casi può essere conveniente suddividere il programma in un insieme di moduli funzionali, ciascuno implementato in un file sorgente separato. Ciascun file sorgente conterà il codice relativo alla funzionalità che si vuole gestire con quel modulo, mentre la funzione main() dovrà essere in un solo file. Organizzando il progetto in questo modo sarà possibile riutilizzare le funzionalità implementate in altri programmi semplicemente includendo i sorgenti relativi in fase di compilazione.
L'utility make
make è un programma di utilità, sviluppato originariamente per l'ambiente UNIX e successivamente portato anche su altre piattaforme, per gestire gruppi di file sorgenti. Più precisamente, nel caso di applicazioni suddivise in più moduli, potrebbe essere necessario ricompilare solamente i file modificati dopo una certa data, oppure scrivere complesse stringhe di comando per collegare assieme i moduli che costituiscono l'applicazione. L'utility make garantisce una compilazione esente da errori e solo dei moduli di cui non esiste il file oggetto aggiornato.
In realtà make può essere utilizzato anche per compilare un solo file sorgente: in questo caso tenta di determinare l'operazione più adatta da compiere, sulla base dell'estensione del file ricevuto in ingresso. Ad esempio se nella directory corrente esistesse il file prova.c, allora l'istruzione:
$ make prova.o[Invio]
è tradotto automaticamente nel comando seguente:
cc -o prova prova.c[Invio]
quindi nel caso in cui nella directory corrente esista il file prova.c, il comando andrà a buon fine.
Più comunemente questa utility riceve in ingresso un altro genere di file, denominato makefile, che specifica le dipendenze esistenti tra i moduli che compongono il progetto e le operazioni da compiere per aggiornarli.
Per eseguire un makefile è sufficiente digitare il comando make dalla shell; il programma cercherà automaticamente un file di nome "makefile" da eseguire. Nel caso in cui avessimo salvato il makefile con un nome diverso (ad esempio "compila_tutto"), possiamo specificare il file corretto aggiungendo l'opzione “-f”:
# make -f backup_all
Ovviamente esistono altre opzioni disponibili per il comando make, che possono essere scoperte tramite i comandi:
$ make --help$ man make
Il makefile
Il makefile è un semplice file di testo contenente definizioni di macro, regole e commenti. Sebbene il makefile contenga uno script che generalmente è utilizzato per automatizzare la compilazione ed il collegamento di un insieme di moduli sorgenti, è bene notare che questo non rappresenta l'unico utilizzo: le azioni che possono essere specificate all'interno di un makefile, infatti, non sono limitate all'invocazione del compilatore o del linker, ma può essere richiamato qualunque comando della shell.
Le regole del makefile
Le regole si suddividono in regole di dipendenza e regole di interpretazione; generalmente ad una regola di dipendenza è associata una o più regole di interpretazione.
Una regola di dipendenza è formata di due parti separate dal carattere ":".
target : dipendenze
La parte di sinistra specifica il nome di un target che deve essere creato, mentre quella di destra definisce i file da cui dipende il target. Di solito target rappresenta il nome dell'eseguibile o del file oggetto da ricompilare, ma può anche rappresentare una sorta di identificatore delle azioni da compiere, specificate dalle regole di interpretazione associate.
Se, durante l'esecuzione del makefile, il target risulta non aggiornato rispetto ai file da cui dipende (cioè la sua data di creazione è antecedente a quella dei file specificati nella parte di destra), allora sono eseguite le regole di interpretazione associate alla regola di dipendenza.
Quindi più in generale le regole del makefile avranno l'aspetto seguente:
target : dipendenze ...comando...
Affinché il makefile sia valido è necessario che tutte le regole di interpretazione (i comandi) inizino con un carattere di tabulazione.
# Un esempio di Makefile con dipendenzeone:
@echo UNO!two:
one
@echo DUE!three:
one two
@echo E TRE!all:
one two three
@echo TUTTI E TRE!
Listato 1: esempio di makefile con dipendenze
L'esempio precedente (Listato 1) mostra un makefile che definisce quattro target e per ognuno di essi stampa sulla shell una stringa diversa. Poiché i target "three" e "all" dipendono dai target precedenti invocando il comando:
$make all
si otterrà l'output seguente:
UNO!
DUE!
E TRE!
TUTTI E TRE!
Il comando indicato in una regola, può proseguire su più righe successive, basta concludere la riga, prima del codice di interruzione di riga, con una barra obliqua inversa (vedi il Listato 2). Quello che conta è che le righe aggiuntive inizino sempre dopo un carattere di tabulazione.
Inoltre, come si può osservare dal Listato 1, il comando di una regola può iniziare con un prefisso particolare (vedi Tabella 1) che modifica il comportamento del programma make durante l'esecuzione del comando stesso.
| Prefisso |
Significato |
| - |
fa in modo che gli errori vengano ignorati; |
| + |
fa in modo che il comando venga eseguito sempre; |
| @ |
fa in modo che il testo del comando non venga mostrato. |
Tabella 1: Elenco dei prefissi applicabili ai comandi delle regole
Le macro del makefile
Le macro sono simili alle variabili di ambiente definite negli script della shell e si definiscono semplicemente assegnando una stringa ad un nome:
nome = stringa
La stringa non deve essere delimitata., L'esempio seguente definisce la macro prefix, che da quel punto del makefile in poi equivale a /usr/local:
prefix=/usr/local
La sostituzione del nome di una macro con il valore corrispondente si indica attraverso due possibili notazioni:
$(nome)
oppure:
${nome}
Esistono alcune macro predefinite il cui contenuto può anche essere modificato. Le più importanti sono elencate nella Tabella 2.
| Nome |
Definizione |
| MAKE |
make |
| AR |
ar |
| ARFLAGS |
rw |
| YACC |
yacc |
| YFLAGS |
|
| LEX |
lex |
| LFLAGS |
|
| LDFLAGS |
|
| CC |
cc |
| CFLAGS |
|
| FC |
f77 |
| FFLAGS |
|
Tabella 2: Macro predefinite
Il Listato 2 rappresenta un semplice makefile che consente di mostrare sullo schermo il contenuto delle macro elencate nella Tabella 2.
flags:@echo MAKE $(MAKE) ; \
echo AR $(AR) ; \
echo ARFLAGS $(ARFLAGS) ; \
echo YACC $(YACC) ; \
echo YFLAGS $(YFLAGS) ; \
echo LEX $(LEX) ; \
echo LFLAGS $(LFLAGS) ; \
echo LDFLAGS $(LDFLAGS) ; \
echo CC $(CC) ; \
echo CFLAGS $(CFLAGS) ; \
echo FC $(FC) ; \
echo FFLAGS $(FFLAGS)
Listato 2: makefile che visualizza il contenuto delle macro
Un altro insieme di macro molto importante, il cui significato sarà più chiaro nei prossimi paragrafi, è il seguente (vedi Tabella 3).
| Nome |
Significato |
| $< |
La prima dipendenza della regola. |
| $* |
Il nome del target senza suffisso. |
| $@ |
Il target della regola specificata. |
Tabella 3: Altre macro predefinite
Le regole deduttive
make prevede alcune regole predefinite, o deduttive, riferite ai suffissi dei file indicati come target. Ad esempio, make è consapevole che per creare un file con il suffisso “.o”, è necessario invocare il comando cc -c sul corrispondente file “.c”; oppure se nelle dipendenze è specificato solamente un file “.h”, l'utility riterrà il target dipendente anche dal corrispondente file con estensione “.c”. Queste regole possono essere sfruttare per rendere più corti i makefile.
Si distingue tra due tipi di regole deduttive: a suffisso singolo e a suffisso doppio. Quindi, a seconda del suffisso specificato nel target, make esegue il comando predefinito più opportuno. La Tabella 4 ne riporta alcune per chiarire il concetto.
| Target |
Comando corrispondente |
| .c |
$(CC) $(CFLAGS) $(LDFLAGS) -o $@ $< |
| .f |
$(FC) $(FFLAGS) $(LDFLAGS) -o $@ $< |
| .c.o |
$(CC) $(CFLAGS) -c $< |
| .f.o |
$(FC) $(CFLAGS) -c $< |
Tabella 4: Alcune regole deduttive
Ad esempio la terza regola della Tabella 4 significa: "prendi tutti i file .c e trasformali in file con estensione .o eseguendo il comando cc su tutti i file .c (espresso con il simbolo $<)”.]
L'esistenza delle regole deduttive consente al programma make di eseguire il makefile seguente (vedi Listato 3), nonostante che per il target dtmain non sia specificato alcun comando.
dt: dtmain.o dtproc.occ -o dt dtmain.o dtproc.odtmain.o: dtmain.c dt.hdtproc.o: dtproc.c dt.hdatecc -c dtproc.c
Listato 3: makefile che fa uso delle regole deduttive
Conclusione
In questo articolo sono state descritte le principali funzionalità del programma make e quali vantaggi porta per la compilazione ed il collegamento di progetti software organizzati in più file.
E' pur vero che la scrittura di makefile per la compilazione e installazione di pacchetti portabili e complessi, può diventare un compito molto arduo. In questi casi è preferibile l'utilizzo di strumenti più sofisticati come autoconf, automake e libtool che generano makefile in modo automatico, partendo da specifiche di livello più alto.