E' pratica comune spezzare i grossi programmi in parti più maneggevoli, di piccole dimensioni, contenute in singoli file e per i quali potrebbero essere richiesta una compilazione differenziata.
Il linguaggio C favorisce questa pratica con l'impiego degli header file, caratterizzati dall'estensione .h, dei file di codice sorgente, la cui estensione è .c oppure .C, e riferendo le opportune librerie, eventualmente generandone specifiche da utilizzarsi con l'applicazione in corso di implementazione.
Alcuni file sorgenti o librerie potrebbero essere modificati durante la vita del software e tale evenienza richiederebbe l'aggiornamento dell'applicazione senza, per questo, ricompilare tutti i sorgenti. Il meccanismo descritto nel seguito risolve egregiamente situazioni di questo tipo.
Il metodo più generale per mantenere, aggiornare e rigenerare programmi è quello di disporre, come accade per il sistema operativo UNIX/Linux, del comando
che esegua una lista di comandi di shell associata a ciascun target, tipicamente per creare o aggiornare un file dello stesso nome. Il file makefile contiene riferimenti ai target, che descrivono quali azioni intraprendere per aggiornarli rispetto a file e/o altri target da cui dipendono, chiamati dipendenze.
Un target è datato quando il file che lo descrive è stato omesso oppure quando una o più delle sue dipendenze ha una data di modifica che risulta essere più recente dello stesso target. In questo caso make analizza ricorsivamente la lista delle dipendenze per ciascun target al fine di generare una lista di target da controllare. La verifica avviene bottom-up, per ciascun target si considerano tutti i file da cui dipende per vedere se ne esiste qualcuno avente data di modifica meno recente. In caso affermativo make ricostruisce il target.
A tal fine make esegue una serie di comandi di shell associati ad esso, detti regole, le quali possono apparire esplicitamente nei punti d'ingresso del makefile oppure essere fornite implicitamente dallo stesso make.
Quindi, l'utilizzo del comando make richiede la preventiva implementazione del corrispondente file makefile per la quale si possono seguire le brevi indicazioni riportate di seguito.
Sebbene non sia strettamente necessaria la presenza di un file per ogni target che appare nel makefile, ogni dipendenza della lista deve essere il nome di un file oppure il nome di un altro target.
Se non c'è alcuna lista di dipendenza o di regole associate a quel target, allora make cerca di produrre un punto d'inizio selezionando una regola fra l'insieme delle sue regole implicite, altrimenti utilizza quella specificata nel target .DEFAULT, se questo appare nel makefile.
La definizione di un target nel makefile assume la seguente forma
target...:[dependency]...[; command]... [command] ...
dove la prima linea contiene il nome del target (oppure una lista di target separati da blank) che termina con il carattere : a cui può far seguito una lista di una o più dipendenze su cui viene fatto il controllo nell'ordine indicato. Tale lista può terminare con ; e, a sua volta, essere seguita da comandi della Bourne shell. Le righe successive, contenenti comandi di shell, devono necessariamente iniziare con un TAB. Tali comandi costituiscono una regola per costruire il target e sono eseguiti quando questo viene aggiornato dal make. Se un comando utilizza più righe allora è necessarie terminare le precedenti con il carattere \ di escape del terminatore di linea.
I seguenti caratteri hanno un significato speciale quando appaiono in un makefile.
Le linee di comandi vengono eseguite una alla volta, ciascuna nella propria shell. Nel caso si utilizzi il comando if è conveniente riferirsi al seguente schema
if expression ; \ then command ; \ command ; \ ... elif command ; \ ... else command ; \ fi
Se, invece, si utilizza il comando for della Bourne shell, allora lo schema consigliato è il seguente
for var in list ; do \ command ; \ ... done
Punti d'ingresso della forma
macro-name=value
definiscono una macro avente nome name e valore value, che può consistere di qualunque carattere diverso da # oppure newline, purchè non preceduto da \. I suddetti caratteri terminano la definizione della macro. Il suo nome è delimitato da SPACE, TAB oppure newline preceduto da \.
Ogni successivo riferimento alla macro nella forma $(name) oppure ${name} sarà sostituito con value. Le parentesi si possono omettere se il nome è costituito da un solo carattere.
La definizione di una macro può contenere riferimenti ad altre macro ma questi non saranno espansi immediatamente ma soltanto quando richiesto dai riferimenti alla macro stessa. Le sostituzioni nelle macro possono essere fatte nel modo seguente
dove str1 è un suffisso oppure una parola da sostituire nella definizione della macro, mentre str2 è la relativa sostituzione.
Quelle riportate di seguito sono alcune delle macro mantenute dinamicamente ed utili come abbreviazioni nella definizione delle regole
Il comando make fornisce alcune regole implicite per certi tipi di target per i quali non sono presenti regole esplicite nel relativo makefile. Per selezionare una di tali regole make si basa sull'associazione fra il target e un file nel proprio direttorio e la scelta deriva direttamente dalla relazione esistente fra il suffisso del target e quella dei file delle possibili dipendenze.
Consideriamo il programma More per la lettura su terminale di un file, realizzato con tre file sorgenti main.c, more.c e command.c, assieme ad un file di definizione di costanti def.h. Alla luce di quanto visto il Makefile che contiene le azioni per la compilazione dei sorgenti e la generazione dell'eseguibile deve essere organizzato in definizioni di macro e regole (implicite e/o esplicite) per generare target che concorrono alla formazione del target finale More.
# # makefile per costruire il file eseguibile More # # Seguono le definizioni di alcune macro # OBJ = main.o more.o command.o SRC = main.c more.c command.c HEADER = defs.h # La macro $@ prende il nome del target corrente More More: $(OBJ) $(CC) $(OBJ) -o $@ # La dipendenza da more.c e' implicita ed e' dettata dal suffisso .c more.o: defs.h command.o: defs.h # Per il target che segue la presenza del carattere speciale @ all'inizio # di riga, preceduto comunque da un TAB, serve a disabilitare la stampa # su stdout del comando printall: # non c'e alcuna dipendenza @echo Stampo tutti i listati. @for file in $(HEADER) $(SRC); \ do \ pr -n $$file; #$$ sta per $ \ done print: $(HEADER) $(SRC) @echo Stampo solo i modificati. @for file in $?; \ \ do \ pr -n $$file; \ done @-touch $@ readable: -for file in $(SRC) $(HEADER); \ \ do \ echo test -r $$file | sh ; \ done clean: /bin/rm -f $(OBJ) More *~
Si noti la presenza nel Makefile precedente dei seguenti target: More, printall, print, readable e clean, ciascuno dei quali esegue una funzione diversa.