"In particolare ricordo una volta che rubò una penna. Non volevo umiliarlo. Sa, noi maestri sappiamo come comportarci in tali casi, così dissi alla classe: 'Ora chiuderemo tutti gli occhi e colui che ha preso la penna pensi a restituirla'. Allora, mentre avevamo gli occhi chiusi lui restituì la penna, ma ne approfittò per tastare il culo alla sua compagna di banco. ...in tv si può dire 'tastare'?" [Mrs. Dorothy Lowry, maestra di Virgil Starkwell, intervistata].
(...una premessa: questo post è un remake di un mio vecchio post (parte 1 di 2). Ma, anche se tratta lo stesso argomento, amplia e perfeziona un po' il discorso è mi è sembrato il caso di riproporlo. Leggete e mi direte...)
Questo è un post veloce. E non è neanche propriamente un post sul C. Il consiglio è di prendere l'informazione, scappare e conservarla gelosamente per il futuro, perché potrebbe tornare molto utile. E non fatevi prendere, se no potreste fare la fine di Virgil Starkwell, il protagonista del divertentissimo e bel mockumentary Prendi i soldi e scappa del Maestro Woody Allen. Oggi parleremo di makefile!
![]() |
...faccia da "ma ho solo rubato un makefile!"... |
Allora, supponiamo che dobbiamo fare un progetto (che chiameremo, per esempio, pluto) e, per vari motivi, non vogliamo (siamo della vecchia scuola) o non possiamo (non ce n'è uno adatto) usare un IDE. Quindi organizziamo (a mano) i nostri file in una maniera canonica, in tre directory: pluto, lib e include. Ovviamente scriveremo il codice in C e piazziamo i file in maniera logica (evidentemente il file con il main va nella directory pluto). I file sono tanti e ogni volta che ricompiliamo non vogliamo riscrivere il tutto il comando e, soprattutto, vogliamo ricompilare solo quello che serve (solo i sorgenti modificati) soddisfacendo automaticamente anche le dipendenze dagli header (ovvero: ricompilare solo i sorgenti che dipendono da un header modificato)... Ma ci serve un makefile!
Ok, tutti voi sapete già cosa è un makefile, ma... sapete scriverne uno veramente semplice e, allo stesso tempo, super-funzionale, generico e universale? Se la risposta è NO questo è l'articolo che fa per voi (e se la risposta è SI... Ciao e al prossimo post!).
Bando alle ciance: se state leggendo questa riga avete risposto NO alla domanda precedente, e quindi possiamo procedere con l'esempio!
# variabiliSRCS = $(wildcard *.c)SRCS_LIB = $(wildcard ../lib/*.c)OBJS = $(SRCS:.c=.o)OBJS_LIB = $(SRCS_LIB:.c=.o)DEPS = $(SRCS:.c=.d)DEPS_LIB = $(SRCS_LIB:.c=.d)# compilatore e linker (normalmente si usa gcc anche per il link)CC = gccLD = gcc # NOTA: usualmente si omette e si usa solo CC# flag per il preprocessore di CC durante la creazione dei file oggettoCPPFLAGS = -I../include -g -O2 -Wall -pedantic -pthread -DUNA_MIA_DEFINE -MMD -MP# flag per il compilatore CC durante la creazione dei file oggettoCFLAGS = -std=c11 # NOTA: CFLAGS usualmente si omette e va tutto in CPPFLAGS# flag per il linker LD durante la creazione del programma eseguibileLDFLAGS = -Lpath_delle_librerie -pthread# librerie che il linker LD deve collegareLDLIBS = -lcurl # NOTA: LDLIBS usualmente si omette e va tutto in LDFLAGS# creazione del target file eseguibilepluto: $(OBJS) $(OBJS_LIB) # target (il file pluto) e da chi dipende (i file .o)$(LD) $(LDFLAGS) $(LDLIBS) $^ -o $@# creazione degli object files%.o: %.c # target (i file .o) e da chi dipende (i file .c)$(CC) $(CPPFLAGS) $(CFLAGS) -c $< -o $@# direttive phony.PHONY: clean# pulizia progetto ($(RM) è di default "rm -f")clean:$(RM) $(OBJS) $(OBJS_LIB) $(DEPS) $(DEPS_LIB) pluto# creazione dipendenze-include $(DEPS) $(DEPS_LIB)
Come vedete il makefile presentato è veramente semplice. Però è anche molto completo: fa tutto quello che serve, compresa la generazione dei file di dipendenza dagli header, e possiamo usarlo per qualsiasi progetto, indipendentemente dal numero di file (le directory pluto, lib e include potrebbero essere vuote oppure contenere centinaia di file). Possiamo aggiungere e togliere sorgenti e header e ricompilare senza modificare una sola linea del makefile, perché lui si adatta automaticamente a quello che trova nelle tre directory del progetto: cosa vogliamo di più?
Come avrete già notato il makefile proposto è stra-commentato, perché le buone usanze dei commenti del codice si devono estendere (e perché no?) anche ai makefile. Comunque per questo articolo è il caso di aggiungere qualche dettaglio in più sui blocchi che compongono l'esempio (titolati con gli stessi commenti che introducono i blocchi), ricordando che molti dei nomi che vedrete da qui in avanti sono predefiniti dal manuale del GNU make nel paragrafo Implicit Rules. Vai con la lista!
- # variabili
Qui si mettono le variabili locali del nostro makefile. In questo caso, per il semplice progetto proposto, avremo solo tre famiglie di variabili: sorgenti SRCS, oggetti OBJS e dipendenze DEPS, e ognuna di queste tre famiglie è divisa in due parti: normali (senza suffisso) e di libreria (con suffisso _LIB) visto che il nostro progetto ha una directory base e una per una libreria (uhm, e a cosa serve? Potrebbe essere una parte più generica che è in comune con altri progetti, quindi la manteniamo separata dalla directory SRCS che contiene il progetto vero e proprio con la sua personalità base). - # compilatore e linker (normalmente si usa gcc anche per il link)
Qui si mettono le variabili che descrivono il compilatore e il linker da usare: di solito è gcc per entrambi e quindi, normalmente, si usa solo la variabile CC (e LD si omette): LD serve per alcuni casi particolari come quando in un progetto C si vuole linkare una libreria C++ e diventa necessario usare g++ come linker. Notare che per un progetto C++ invece di CC si usa la variabile CXX (vedi di nuovo in Implicit Rules). - # flag per il preprocessore di CC durante la creazione dei file oggetto
Qui si mettono i flag extra da assegnare al preprocessore C e ai programmi che lo utilizzano (i compilatori C, C++ e Fortran, per esempio). Ho messo un po' di flag di uso frequente a caso (-Ipath_degli_include, -g, -O2, etc.) che sono ben spiegati nel manuale di gcc (usare di volta in volta solo quelli che servono, eh!). Mi preme solo aggiungere due dettagli:
- Il flag -DUNA_MIA_DEFINE serve a dire al preprocessore: compila anche quello che, nel codice, è incluso in una #ifdef UNA_MIA_DEFINE. Si tratta, quindi, di compilazione condizionale, e si possono usare multiple define usando più flag -D sulla stessa linea.
- I flag speciali -MMD -MP sono indispensabili in un makefile come questo, perché servono a gestire automaticamente le dipendenze dagli header, come premesso all'inizio.
- # flag per il compilatore CC durante la creazione dei file oggetto
Qui si mettono i flag aggiuntivi da fornire al compilatore C. Per semplificare si puó omettere CFLAGS e mettere tutto in CPPFLAGS, ma la forma estesa presentata qui ha dei vantaggi: ad esempio in un makefile misto (C e C++) mettendo in CFLAGS il flag -std=c11 sono sicuro che non verrà usato per i sorgenti C++ (che, automaticamente, saltano CFLAGS e usano, se disponibile, il flag corrispondente del C++ che è CXXFLAGS). - # flag per il linker LD durante la creazione del programma eseguibile
Qui si mettono i flag aggiuntivi da dare al compilatore quando deve invocare il linker ld, come il flag -Lpath_delle_librerie (nell'esempio è -L../include e si possono aggiungere più path a piacere). Le librerie (e.g.: -lcurl) dovrebbero invece essere aggiunte alla variabile LDLIBS (vedi il prossimo punto). - # librerie che il linker LD deve collegare
Qui si mettono i nomi delle librerie forniti al compilatore quando deve invocare il linker LD. I flag che non si riferiscono a librerie, come -Lpath_delle_librerie, vanno invece inseriti nella variabile LDFLAGS (come visto nel punto precedente). Per semplificare si può omettere LDLIBS e mettere tutto in LDFLAGS, ma la forma estesa presentata qui può avere dei vantaggi in alcuni casi particolari. Nell'esempio ho linkato la libcurl (e si possono aggiungere più librerie a piacere), ma se il progetto non usa nessuna libreria esterna LDLIBS si può lasciare vuoto. - # creazione del target file eseguibile
Qui si mette il nome dell'eseguibile da creare e da chi dipende e, nella linea successiva, il comando per linkare i file oggetto creati e produrre il file eseguibile finale. Si usa un comando generico che usa le variabili LD e LDFLAGS (e, se usata, anche LDLIBS) e richiama automaticamente tutti gli oggetti compilati (è quel'espressione speciale $^ -o $@ che si espande, più o meno, in "prendi tutti gli oggetti ($^ ) e genera il target corrispondente (-o $@)"). - # creazione degli object files
Qui si mette nome (i file .o) e dipendenza (i file .c) dei file oggetto, e, nella linea successiva, il comando generico per compilare ogni sorgente e creare il l'oggetto corrispondente. Si usa un comando generico che usa le variabili CPPFLAGS (e, se usata, anche CFLAGS) e richiama automaticamente tutti i sorgenti da compilare (è quell'espressione speciale $< -o $@ che si espande, più o meno, in "prendi tutti i sorgenti ($<) e genera gli oggetti corrispondenti (-o $@)"). - # direttive phony
Qui si mettono tutte le direttive phony (uhm, questo è un po' lungo da spiegare: aprite il link, che è chiarissimo). - # pulizia progetto ($(RM) è di default "rm -f")
Qui si mette il comando di cancellazione degli oggetti e dell'eseguibile per, eventualmente, forzare una successiva ricompilazione completa. - # creazione dipendenze
Qui si mette il comando per generare i file di dipendenza che ci permettono di ricompilare solo quello che serve quando modifichiamo un header file.
Che ne dite? L'obbiettivo non era di spiegare cosa è un makefile e come si scrive (uff, c'è in rete una documentazione enorme sull'argomento). E neppure era di spiegare i segreti della sintassi (che permette anche soluzioni complesse). L'obbiettivo era di fornire un makefile basico e completo allo stesso tempo, un makefile universale per (quasi) qualsiasi progetto. Io direi che l'obbiettivo è compiuto... poi, se dobbiamo fare progetti complessi e portabili, con auto-installatori, ecc. magari ci troveremo più comodi usando un IDE di buona qualità oppure usando a mano strumenti come Autotools o CMake... ma vi assicuro che il metodo rapido e vecchia-scuola che ho descritto è usabile sempre e senza limitazioni (io l'ho usato in progetti di produzione, giuro!). Sono soddisfazioni...
Nella prossima e seconda parte del post prenderò lo stesso makefile e lo adatterò per creare una shared-library: so che è un argomento molto interessante, ma non trattenete il respiro nell'attesa! (può nuocere gravemente alla salute...).
Ciao e al prossimo post!