Nei titoli e nei testi troverete qualche rimando cinematografico (ebbene si, sono un cinefilo). Se non vi interessano fate finta di non vederli, già che non sono fondamentali per la comprensione dei post...

Di questo blog ho mandato avanti, fino a Settembre 2018, anche una versione in Spagnolo. Potete trovarla su El arte de la programación en C. Buona lettura.

sabato 18 febbraio 2017

Prendi il makefile e scappa
come scrivere un makefile universale

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.

...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. Organizziamo i nostri file in una maniera canonica, in tre directory: pluto, lib e include. Ovviamente scriveremo il codice in C e piazzeremo i file in maniera logica (evidentemente il file con il main() andrà nella directory pluto). I file sono tanti e  e ogni volta che ricompiliamo non vogliamo riscrivere il comando a mano e vogliamo ricompilare solo quello che serve (solo i sorgenti modificati) soddisfacendo automaticamente le dipendenze dagli header (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, al tempo stesso, super funzionale e, soprattutto, generico e universale? Se la risposta è NO questo è il vostro post (e se la risposta è SI allora Ciao e al prossimo post!).

Bando alle ciance: se state leggendo questa riga avete risposto NO alla domanda precedente, e quindi vai con l'esempio!
# variabili
CC = gcc
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)

# creazione del target file eseguibile
pluto: $(OBJS) $(OBJS_LIB)
$(CC) $^ -o $@ -lcurl

# creazione degli object files
%.o: %.c
$(CC) -MMD -MP -I../include -c $< -o $@ -g -Wall -std=c11 -D SIMULATION

# direttive phony
.PHONY: clean

# pulizia progetto ($(RM) è di default "rm -f")
clean:
$(RM) $(OBJS) $(OBJS_LIB) $(DEPS) $(DEPS_LIB)

# creazione dipendenze
-include $(DEPS) $(DEPS_LIB)
Come vedete il makefile presentato è veramente semplice. Però è anche veramente 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 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ù?

Qualche piccolo dettaglio sui blocchi (commentati) che compongono il makefile:

# variabili
Qua si mettono le variabili che vengono usate nel resto del makefile. In particolare la variabile CC indica il compilatore da usare: nel nostro caso è gcc, ma potrebbe essere, per esempio, g++ (per il C++). Ovviamente in questo caso i sorgenti sarebbero dei .cpp o .cc, quindi bisogna ricordarsi di modificare anche le altre variabili che fanno riferimento ai .c.

# creazione del target file eseguibile
Qua si mette il comando per linkare i file oggetto creati e produrre il file eseguibile finale. Se usiamo qualche libreria esterna il riferimento si aggiunge qui (nell'esempio si linka la libcurl usando -lcurl).

# creazione degli object files
Qua si mette il comando per compilare ogni sorgente e creare il file oggetto corrispondente, attivando tutte le opzioni del compilatore che ci servono. Se usiamo qualche #ifdef particolare (come quelle viste la) la attivazione si mette qui (nell'esempio si attiva una define SIMULATION usata nei sorgenti).

# direttive phony
Qua si mettono tutte le direttive phony (è un po' lungo da spiegare: guardate il link, che è chiarissimo).

# pulizia progetto ($(RM) è di default "rm -f")
Qua si mette il comando di cancellazione degli oggetti per, eventualmente, forzare una successiva ricompilazione completa.

# creazione dipendenze
Qua si mette il comando per generare i file di dipendenza che ci permettono di ricompilare solo quello che serve quando modifichiamo un header file.

Il makefile presentato è un esempio reale, pronto all'uso. Ovviamente le direttive -lcurl e -D SIMULATION sono state aggiunte come esempio per indicare come estendere le funzionalità del makefile: se non ci servono possiamo toglierle senza problemi (e aggiungeremo quelle che ci servono usando lo stessa sintassi).

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 tools come Autotools o CMake... ma vi assicuro che il metodo rapido e vecchia-scuola che ho descritto è usabile sempre e senza limitazioni. Sono soddisfazioni...

Ciao, e al prossimo post!

Nessun commento:

Posta un commento