Compilarea din mai multe surse. Utilitarul make

În lucrul cu proiecte mari este necesară compilarea din mai multe surse. Deoarece acest lucru este destul de greu de realizat manual, au fost create diferite utilitare pentru automatizarea sarcinilor. Un astfel de utilitar este GNU Make având ca executabil asociat make. Make rezolvă problema compilării din mai multe surse pe baza relaţiilor de dependenţă dintre acestea, relaţii descrise într-un fişier special numit Makefile.

Sintaxa Makefile

Makefile reprezintă fişierul în care sunt descrise relaţiile de dependenţă dintre sursele proiectului. Acesta trebuie să se numească ‘Makefile’ sau 'makefile' şi are urmatoare sintaxă:

Se obişnuieşte ca numele targetului să coincidă cu numele fişierului rezultat, excepţie fiind acele target-uri care sunt .PHONY numite target-uri virtuale (şi în urma cărora nu se generează un fişier concret).
Lista de dependenţe reprezintă dependenţele necesare pentru a se executa comanda. De obicei sunt fişierele din care se obtine target-ul.
O greseală frecventă este ca în loc de <tab> să fie spaţii. Acest lucru va duce la un mesaj de eroare în momentul rulării make.
Un exemplu de Makefile este:

Acesta nu este cel mai bun mod de utilizare al make deoarece nu se descrie nicio dependenţă, deci de fiecare dată când va fi rulat make va duce la executarea comenzii gcc ana.c are.c mere.c -o exec, chiar dacă nu au fost modificate sursele respective. O mai bună utilizare este exemplul urmator:

În acest caz target-ul exec se va executa doar dacă una din surse s-a modificat. Nici acest caz nu exploatează la maxim facilităţile pe care le oferă make deoarece modificarea unei singure surse duce la compilarea tuturor surselor existente. Un fişier Makefile ideal descrie dependenţele până la cel mai jos nivel posibil, in cazul nostru acesta fiind fişierele obiect:

Modul de funcţionare al unui fisier Makefile

Un anumit target se execută rulând comanda make target. În cazul in care nu există niciun argument atunci se va executa comanda corespunzătoare primului target. Pentru obtinerea unui target trebuie satisfăcute toate dependenţele acetuia.
Pentru exemplul nostru, target-ul exec se obţine numai dupa ce au fost obţinute target-urile ana.o, are.o, mere.o, care la rândul lor sunt condiţionate de ana.c, are.c, mere.c.

Variabile

În fişierele Makefile se pot declara variabile cu scopul de a înlocui secvenţe des utilizate sau care se modificâ frecvent.
Valoarea variabilelor se obţine utilizând caracterul ‘$’: $(nume_variabilă).
Pentru exemplul de mai sus, să presupunem că unul din fişierele sursă foloseşte funcţii din math.h. Vom declara o variabilă care are rolul de a specifica acest lucru pentru link-editare:

Make oferă şi câteva variabile predefinite, dintre care cele mai importante sunt:

  • $@ - numele target-ului
  • $^ - lista de dependenţe
  • $< - prima dependenţă

Makefile-ul de mai sus poate fi rescris mai simplu astfel:

Variabilele dintr-un Makefile pot proveni şi din mediul în care este rulată comanda make. Fiecare variabilă de mediu existentă în momentul rulării este transformată într-o variabilă locală, proprie Makefile-ului respectiv, cu acelaşi nume si aceeaşi valoare. Astfel, atribuind o anumită valoare variabilei LDFLAGS din exemplul de mai sus se pot produce schimbări la compilarea oricărei surse C.
Pentru a transforma o variabilă locală intr-o variabilă de mediu în scopul de a o utiliza şi în alte fişiere Makefile se foloseşte directiva export:

Transformarea inversă se face utilizând unexport:

Target-ul .PHONY

În cazul în care ne dorim ca un target să fie marcat în permanenţă ca neactualizat vom folosi target-ul .PHONY.
Să luăm ca exemplu existenţa unui target pack care creează o arhivă cu sursele proiectului. In cazul în care una din surse se numeşte pack şi aceasta nu se modifică comanda asociată acestui target nu va fi executată. Pentru aceasta folosim .PHONY.

De asemenea, în mod convenţional toate fişierele Makefile conţin un target .PHONY numit 'clean' pentru a şterge fişierele obtinute in urma compilarii sau rularii programului.

Reguli implicite

De foarte multe ori make oferă posibilitatea simplificării sintaxei prin neprecizarea comenzii care trebuie rulată, aceasta fiind detectată implicit.

O alta regulă implicită este ca rularea comenzii make file să ducă la compilarea fişierului file.c chiar dacă nu există niciun Makefile.
Regulile implicite se folosesc de variabile de mediu. Astfel, exemplul considerat de noi este echivalent cu:

Deoarece regulile implicite folosesc variabile de mediu, comportamentul acestora este uşor de modificat.

Tips

Se recomandă ca primul target dintr-un Makefile să fie cel care compilează sursele pentru a nu preciza la fiecare rularea a make target-ul respectiv. Acest lucru este util deoarece compilarea surselor este, in general, cea mai des utilizată comandă.

Adăugând aceste modificări la exemplul nostru obtinem un Makefile complet: