Prendi il makefile e scappa – come scrivere un makefile universale – pt.1

  ICT, Rassegna Stampa
image_pdfimage_print
"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].

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!

prendi il makefile e scappa
…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!

# variabili
SRCS = $(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 = gcc
LD = gcc # NOTA: usualmente si omette e si usa solo CC # flag per il preprocessore di CC durante la creazione dei file oggetto
CPPFLAGS = -I../include -g -O2 -Wall -pedantic -pthread -DUNA_MIA_DEFINE -MMD -MP # flag per il compilatore CC durante la creazione dei file oggetto
CFLAGS = -std=c11 # NOTA: CFLAGS usualmente si omette e va tutto in CPPFLAGS # flag per il linker LD durante la creazione del programma eseguibile
LDFLAGS = -Lpath_delle_librerie -pthread # librerie che il linker LD deve collegare
LDLIBS = -lcurl # NOTA: LDLIBS usualmente si omette e va tutto in LDFLAGS # creazione del target file eseguibile
pluto: $(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!

https://italiancoders.it/prendi-il-makefile-e-scappa-come-scrivere-un-makefile-universale-pt-1/