Una guida per principianti per comprendere i makefile in Linux
Come principianti, la maggior parte dei programmatori C/C++ compila i propri programmi sulla riga di comando di Linux eseguendo i comandi del compilatore gcc o g++. Ma man mano che i loro progetti iniziano a crescere, la compilazione con un singolo comando non rimane facile ed efficiente.
Come soluzione a tutti i problemi di compilazione, è necessario comprendere il concetto di makefile. Questo argomento sarà trattato in una serie di articoli e in questa prima parte discuteremo le basi dei makefile in Linux.
Problemi con il metodo dell’ordine singolo
Supponiamo che il tuo progetto sia costituito dai seguenti tre file conservati in una directory:
test.c
anotherTest.c
test.h
Come principiante, eseguirai ripetutamente il seguente comando per compilare il tuo progetto:
gcc -Wall test.c anotherTest.c -o test -I.
Questo va bene fintanto che il tuo progetto contiene solo pochi file. Ora supponiamo che il tuo progetto cresca e ora contenga altri 10 file sorgente e di intestazione. Cosa farai allora?
Molti sostengono che estenderanno il comando esistente aggiungendo i nomi di quei nuovi file. Ma cosa succede se in qualche modo perdi questo comando o passi a un altro sistema? Digiterai di nuovo il comando lungo?
Supponiamo inoltre che il tuo progetto diventi un progetto di grandi dimensioni che contiene centinaia di file e richiede 5-10 minuti per essere compilato. Supponiamo ora di aggiungere un semplice Stampa debug in uno dei file .c, ma usa un solo comando per ricompilare. Non sarebbe inefficiente per un programma impiegare 10 minuti per la compilazione anche se una singola riga viene modificata/aggiunta a uno dei file sorgente?
Bene, la soluzione è usare i Makefile.
Makefile
Un makefile è un file speciale (chiamato semplicemente “Makefile”) che consiste di obiettivi, dipendenze e comandi, strutturati in modo da rendere facile per un programmatore compilare il programma.
In parole povere, ecco una sintassi di makefile di base:
target: dependencies
[tab] command
Quindi, se vogliamo creare un makefile molto semplice per il nostro progetto di esempio (elencato nell’ultima sezione), sarebbe qualcosa del tipo:
all: test
test: test.o anotherTest.o
gcc -Wall test.c anotherTest.c -o test -I.
test.o: test.c
gcc -Wall -c test.c -I.
anotherTest.o: anothertest.c
gcc -Wall -c anotherTest.c -I.
Questo può sembrare un po’ complicato per un principiante, ma se guardi da vicino, contiene solo gruppi di obiettivi, dipendenze e comandi.
- Il target “all” non è altro che un target makefile predefinito.
- Il “test” di destinazione dipende da test.o e anotherTest.o e può essere prodotto dal comando indicato di seguito.
- Allo stesso modo, i target “test.o” e “anotherTest.o” dipendono entrambi dai file .c corrispondenti e possono essere prodotti dai rispettivi comandi menzionati di seguito.
Ora, verrebbe da chiedersi, come utilizzare questi obiettivi? Bene, sono usati dal comando “make”. Questo comando accetta un target come argomento e cerca un file chiamato “Makefile” per capire come costruire quel target. Ad esempio, se specifichi il seguente comando:
make test
Quindi questo comando eseguirà la scansione del Makefile, creando (se necessario) le sue dipendenze Test e quindi eseguire il comando per costruire Test di per sé.
Allo stesso modo, se vuoi rimuovere tutti gli oggetti spazzatura o gli eseguibili prima di ogni ciclo di compilazione, puoi aggiungere un obiettivo pulire al Makefile.
Ecco un esempio:
all: test
test: test.o anotherTest.o
gcc -Wall test.c anotherTest.c -o test -I.
test.o: test.c
gcc -Wall -c test.c -I.
anotherTest.o: anothertest.c
gcc -Wall -c anotherTest.c -I.
clean:
rm -rf *o test
Quindi puoi semplicemente emettere il seguente comando:
make clean
per ripulire tutti i file oggetto aggiuntivi e Test file eseguibile, prima di avviare un ciclo di compilazione.
La bellezza di questo comando è che sa quali file sono stati modificati dall’ultima volta che è stato eseguito e crea solo le dipendenze necessarie. Quindi la combinazione di fare command e Makefile non solo rende più facile per il programmatore compilare il progetto, ma riduce anche al minimo il tempo complessivo di compilazione.