5. Introducere în utilitarul Make și fișiere Makefile

În secțiunile anterioare, am compilat fișiere cod sursă C folosind compilatorul GCC. Dezvoltarea unui program este un proces continuu, nu scriem tot codul dintr-o singură iterație și de multe ori ajungem să îl modificăm pe parcurs. Vrem să testăm schimbările aduse în program. Pentru aceasta trebuie să recompilăm fișierele pe care le-am modificat și să creăm un nou executabil.

Automatizarea procesului de compilare ne ajută să fim eficienți atunci când dezvoltăm un proiect: * În loc să dăm de fiecare dată toate comenzile pentru recompilarea fișierelor, putem să dăm o singură comandă care să le facă pe toate. * Putem să evităm compilarea unor fișiere pe care nu le-am modificat de la ultima compilare, astfel salvând mult timp. Acest proces se numește build automation. Există mai multe soluții de build automation [1]. În această carte vom folosi utilitarul Make împreună cu fișiere Makefile ca să automatizăm procesul de compilare.

În secțiunile următoare vom vedea cum funcționează utilitarul make și cum arată un fișier Makefile. După, vom crea un fișier Makefile pentru un proiect dat.

5.1. Folosirea unui Makefile existent

În această secțiune vom compila programul Hangman folosind un fișier Makefile.

Întrăm în directorul ~/support/simple-make folosind comanda cd:

student@uso:~$ cd ~/support/simple-make
student@uso:~/support/simple-make$ ls
hangman.c  Makefile

Avem în director un fișier cod sursă C, hangman.c, și un fișier Makefile existent, astfel că putem utiliza comanda make. Aceasta va rula scriptul scris în Makefile, care, în cazul nostru, urmărește compilarea fișierului hangman.c.

student@uso:~/support/simple-make$ make
gcc -c hangman.c
gcc -o hangman hangman.o
student@uso:~/support/simple-make$ ls
hangman  hangman.c  hangman.o  Makefile

Comanda make a rulat, de fapt, 2 comenzi:

  1. De compilare: gcc -c hangman.c, comandă prin care am creat fișierul obiect hangman.o

  2. De legare: gcc -o hangman hangman.o, comandă prin care am creat executabilul hangman.

Practic, scriind doar comanda make, am trecut fișierul hangman.c prin toate etapele compilării și am obținut executabilul final, așa cum am făcut în secțiunea Compilarea programului din surse multiple pentru fișierele main.c și algorithms.c.

Rulăm executabilul hangman ca să vedem că funcționează, ca în imaginea de mai jos:

Rularea programului ``hangman``

5.2. Înțelegerea formatului Makefile

În secțiunea anterioară, Folosirea unui Makefile existent, am folosit fișierul Makefile ca să compilăm programul Hangman. Ca să putem crea un Makefile pentru un proiect al nostru, trebuie să înțelegem formatul fișierului Makefile. În această secțiune vom folosi fișierul Makefile pe care l-am folosit anterior ca suport.

Fișierul Makefile folosit la programul Hangman are următorul conținut:

all: hangman

hangman: hangman.o
    gcc -o hangman hangman.o

hangman.o: hangman.c
    gcc -c hangman.c

clean:
    rm -rf *.o hangman

Liniile din fișier sunt de două tipuri:

#. Regulă, care are formatul regulă: <dependență> (all: hangman sau clean:). Regula trebuie să existe, dependența este opțională. Vom explica ce este o dependentă în secțiunea app_dev_dependency_makefile O regulă din fișierul Makefile este, de fapt, un nume asociat unui unei comenzi.

student@uso:~/support/simple-make$ make clean
rm *.o hangman
student@uso:~/support/simple-make$ ls
hangman.c   Makefile

#. Observăm deci că putem rula make + <nume regulă>, dar și make simplu. Utilizând prima formă, din fișierul Makefile se va executa doar regula dorită, în cazul nostru fiind codul specific regulii ‘clean’. Utilizând a doua formă, simplă, se va apela implicit prima regulă găsită din fișierul Makefile, în cazul nostru regula ‘all’. Aceasta la rândul ei poate trimite către una sau mai multe reguli care să fie executate în succesiune, în cazul nostru urmând regula ‘hangman’.

5.3. Crearea primului Makefile

În această secțiune vom crea un Makefile pentru programul TODO. Scopul final este să avem un target pentru compilare și unul pentru curățarea directorului sursă.

5.3.1. Adăugarea targetului all

În directorul TODO avem fișierul cod sursă TODO:

student@uso: cd TODO
student@uso: ls
TODO.c

Creăm un fișier numit Makefile în care vom scrie primul target: all care trebuie să compileze codul sursă:

student@uso: touch Makefile
student@uso: cat Makefile
all:
    gcc -o exec TODO.c

Ne asigurăm că Makefile-ul funcționează corect:

student@uso: make all # sau doar simplu 'make', pentru că va chema prima regulă
gcc -o exec TODO.c

student@uso: ./exec
"Hello World!"

5.3.2. Adăugarea targetului clean

Targetul clean este folositor pentru a șterge fișierele generate în urma procesului de compilare. Aceste fișiere sunt de obicei executabile și fișiere obiect, dar nu se limitează la acestea. Adăugăm targetul clean la fișierul Makefile creat anterior.

student@uso: cat Makefile
all:
    gcc -o exec TODO.c

clean:
    rm exec

Testăm că regula funcționează corect.

student@uso: ls
exec
Makefile
TODO.c
student@uso: make clean
rm exec
student@uso:  ls
Makefile
TODO.c

5.4. Fișiere Makefile cu alte nume

Fișierele Makefile au în general numele Makefile, însă, într-un proiect mai mare pot exista mai multe fișiere Makefile în același loc, și deci vrem să le diferențiem. Facem asta dându-le câte un nume unic. Utilitarul make se uită, predefinit, după fișiere numite GNUmakefile, makefile, și Makefile. Pentru a putea avea un fișier Makefile cu nume propriu, folosim opțiunea make -f.

Redenumim fișierul Makefile anterior în Makefile.TODO și compilăm din nou codul sursă:

student@uso: mv Makefile Makefile.TODO
student@uso: make -f Makefile.TODO
gcc -o exec TODO.c
student@uso: ./exec
"Hello World!"

Observăm că efectul compilării este același.

5.5. Adăugarea de targeturi pentru crearea fișierelor obiect

Am văzut în secțiunea Folosirea unui Makefile existent că atunci când am compilat codul sursă pentru jocul Hangman, acesta a trecut mai întâi prin etapa de cod obiect și abia după am obținut fișierul executabil.

Pentru a face acest lucru pentru proiectul TODO, vom crea mai întâi targeturi pentru crearea fișierelor obiect în fișierul Makefile. Creăm targetul TODO.o care are scopul de a compila fișierul TODO.c până în stadiul de fișier obiect. La fel facem și pentru celelalte fișiere cod sursă din proiect.

student@uso: cat Makefile
all:
    gcc -o exec TODO.c

TODO.o: TODO.c
    gcc -c TODO.c

clean:
    rm exec

Creăm fișierele obiect pentru fiecare cod sursă în parte:

student@uso: make TODO.o
gcc -c TODO.c

5.5.1. Modificarea targetului de creare a executabilului

Acum că avem targeturi pentru creearea fișierelor obiect, trebuie să modificăm targetul pentru compilarea executabilului final. Acesta trebuie să folosească acum fișierele obiect în loc de fișierele cod sursă pentru a compila programul.

student@uso: cat Makefile
all: make_exec

make_exec: TODO.o
    gcc -o exec TODO.c

TODO.o: TODO.c
    gcc -c TODO.c
clean:
    rm exec

5.5.2. Modificarea targetului clean

Fișierele obiect obținute prin targeturile intermediare sunt fișiere generate care nu ne trebuie pentru a putea rula programul final. Acestea sunt fișiere pe care nu vrem să le păstrăm în proiectul nostru mereu deoarece le vom regenera de fiecare dată când avem nevoie. Modificăm targetul clean astfel încât acesta să șteargă și fișierele obiect generate pe parcursul compilării.

student@uso: cat Makefile
all: make_exec

make_exec: TODO.o
    gcc -o exec TODO.c

TODO.o: TODO.c
    gcc -c TODO.c
clean:
    rm exec
    rm *.o

5.5.3. Testarea fișierului Makefile

În secțiunile anterioare, am creat un fișier Makefile care trece codul sursă al programului TODO mai întâi prin etapa de cod obiect, după care în aduce în starea finală de executabil. Testăm Makefile-ul pe care l-am făcut. Rulăm comanda make în terminal. În urma rulării ei obținem executabilul TODO.

student@uso: make
gcc -c TODO.c
gcc -o exec TODO.c
student@uso: ls
exec
Makefile
TODO.c
TODO.o

Ștergem toate fișierele generate (obiect și executabil) folosind targetul clean:

student@uso: make clean
rm exec
rm *.o

Anterior, când am rulat targetul make, acesta a putut să creeze fișierul executabil TODO din fișierele obiect TODO deoarece acestea erau deja generate. Acum, avem un director curat, fără fișiere generate. Compilăm încă o dată tot proiectul:

Mai întâi generăm fișierele obiect, după care generăm fișierul executabil:

student@uso: make TODO.O
gcc -c TODO.c
student@uso: make TODO
gcc -o exec TODO.c

Faptul că trebuie să dăm 2 comenzi în terminal pentru a compila un program nu este ideal. Ce ne facem dacă avem mai multe fișiere? În secțiunea următoare, app_dev_advanced_makefile, vom vedea cum adăugăm dependențe pentru reguli astfel încât în final, să folosim doar comanda make în terminal pentru a trece fișierul cod sursă prin toate etapele compilării.

Ștergem fișierele generate folosind comanda make clean:

student@uso: make clean
rm exec
rm *.o

Note de subsol