Soluții - PC Magazine Romania, Mai 2002
Make, un utilitar nu doar pentru programatori
Iulian Radu
Deși în prima pagină a manualului programului GNU make se specifică faptul
că: "utilitarul determină automat care parte dintr-un program mare trebuie recompilată
și execută comenzile necesare recompilării", acest program poate fi folosit
ori de câte ori avem de gestionat fișiere al căror conținut trebuie actualizat
când conținutul altor fișiere s-a modificat. Soluțiile oferite în acest articol
nu sunt aplicabile doar cu GNU make, ci sunt valabile pentru orice altă variantă
a programului make.
Cum lucrează programul make
Programul make execută o serie de comenzi aflate într-un fișier text pe baza
argumentelor furnizate în linia de comandă. De aceea, make are nevoie să știe
două lucruri: care este numele scopului pentru care trebuie executate comenzi
și în ce fișier găsește descris acest scop. Dacă nu este furnizat nici un nume
de fișier, se caută în directorul curent un fișier care se numește GNUmakefile
sau makefile sau Makefile (căutarea se face în această ordine; oricare din aceste
fișiere sau cel/cele specificat/e implicit cu -f îl voi denumi în continuare
Makefile). Când a fost găsit unul dintre ele, se întrerupe căutarea. Dacă se
specifică mai multe fișiere cu ajutorul argumentului -f, atunci toate acestea
vor fi privite ca un singur fișier ce conține reguli. În lipsa specificării
unui scop, se va considera primul scop care nu începe cu punct, găsit în fișierul
Makefile. În secțiunea "Structura unui fișier Makefile" veți găsi explicat cum
arată conținutul unui astfel de fișier.
Argumentele cele mai utilizate
În acest articol nu vom face o prezentare completă a tuturor argumentelor pe
care le poate primi programul make și nici a tuturor aspectelor legate de el.
Pentru astfel de informații cea mai bună sursă rămâne documentația în format
.info aflată în kit-ul programului (se poate vizualiza cu "info make" după instalarea
pachetului). Iată o listă cu cele mai utilizate argumente:
1 -C nume_director (înainte de a face orice altceva make schimbă directorul
la cel specificat)
2 -f nume_fișier_makefile (poate apărea de mai multe ori pentru a crea în mod
transparent un singur fișier Makefile din mai multe fișiere reale)
3 -k (continuă executarea scopurilor chiar dacă a apărut eroare la actualizarea
unuia)
4 -i (se ignoră erorile produse la executarea comenzilor definite în cadrul
regulilor; se consideră că un program nu s-a terminat cu succes în cazul în
care codul de terminare a lui a fost diferit de 0)
5 -n (nu se execută nici o comandă, doar le afișează; util pentru a vedea ce
va executa make în realitate)
6 -q (nu execută nimic, doar întoarce codul de terminare care ne arată starea
în care se află targetul: este sau nu nevoie să-l actualizăm)
7 -r (dezactivează regulile implicite)
8 -R (dezactivează variabilele implicite)
9 -s (nu afișează comenzile pe care le execută sau orice alt mesaj, cu excepția
celor de eroare)
10 -t (actualizează timpul ultimei modificări a fișierelor în loc să le actualizeze
conținutul).
Structura unui fișier Makefile
Deoarece un model va fi mai simplu de înțeles decât o prezentare în cuvinte,
vom da un exemplu pe care apoi îl vom analiza. Acesta este făcut special pentru
a putea fi prezentate anumite caracteristici ale fișierelor Makefile, nefiind
un model luat din lumea reală.
1. .PHONY all compile-all do-exe instal-all test clean
2. .SILENT clean test
3.
4. include"common.mak"
5.
6. VPATH = src man doc tests
7.
8. prefix = /usr/local
9. exec_prefix = $(prefix)
10. bindir = $(prefix)/bin
11. libdir = $(prefix)/lib
12. includedir = $(prefix)/include
13.
14. objects = foo.o bar.o
15. files := $(wildcard *_exe)
16. files += $(objects)
17.
18. all: compile-all install-all
19. compile-all: $(objects) mylib.a do-exe
20. $(objects): %.o : %c
21. $(CC) -c $(CFLAGS) $< -o $@
22. mylib.a: mylib.c mylib.h
23. @$(CC) -c $(CFLAGS) $^ -o $(patsubst %.c,%.o,$^)
24. ar -r $@ $(patsubst %.c,%.o,$^)
25. do-exe: $(filter %_exe,$(files))
26. +echo "Executie terminata !"
27. $(filter %_exe,$(files)): %_exe: %
28. ifneq(,$(findstring "/bin",$PATH))
29. $*.sh
30. else
31. $*.exe
32. endif
33.
34. instal-all: $(filter %_unix.exe,$(objects))
35. $(MAKE) -C $(addsuffix ".dir",$(dir "/usr/local/lib/mylib.so"))
36. $(shell echo "Instalarea s-a facut cu succes!")
37.
38. clean:
39. -rm -rf $(files)
40.
41. test: $(wildcard tests/*.c)
42. $(MAKE) -C test $(word $(words $<)-1,$<)
În linia 1, folosind directiva .PHONY, anunțăm programul make că scopurile all,
compile-all, do-exe, install-all, test și clean nu sunt fișiere reale și deci
scopurile și comenzile asociate lor trebuie executate de fiecare dată când sunt
evaluate. În linia 2, folosind directiva .SILENT, se precizează că la executarea
comenzilor asociate scopurilor clean și test programul make să nu afișeze comenzile
pe care le execută. Implicit, make afișează comenzile pe care le execută și
intrările/ ieșirile din directoare. În linia 4, folosind directiva include,
se intercalează conținutul fișierului common.mak în cadrul fișierului Makefile
curent. Fișierul common.mak trebuie să respecte structura unui fișier de tip
Makefile. Programul make știe să caute automat un fișier aflat într-un scop
atât în directorul curent cât și în directoarele specificate în variabila VPATH.
În linia 6, indicăm programului make să caute fișierele și în directoarele ./src,
.-/man, ./doc și ./tests dacă nu le găsește în directorul curent (./.). În liniile
8-12 definim 5 variabile, din care 4 depind de valoarea primeia. Este o practică
comună să se definească o variabilă denumită prefix în care să se memoreze rădăcina
directoarelor implicate în prelucrarea datelor. Astfel, dacă se dorește prelucrarea
unor date aflate în locuri diferite dar care respectă aceeași structură de directoare
și fișiere se poate apela make cu diferite valori ale variabilei prefix (valoarea
variabilei este stabilită în cadrul shellului înainte de a apela programul make).
În linia 14 este definită o variabilă denumită objects care primește numele
a două fișiere. Folosirea semnului egal (=) pentru atribuire înseamnă că variabila
primește exact valoarea din partea dreaptă a egalului. Orice expandare va avea
loc la folosirea variabilei. În acest fel, este posibil să definim variabile
care depind de valorile altor variabile, valori care se pot modifica de-a lungul
execuției programului make și deci care vor modifica în mod dinamic și valoarea
primelor variabile. În linia 15 definim o variabilă denumită files și care primește
valoarea funcției $(wildcard *_exe). Această funcție generează o listă a tuturor
fișierelor din directorul curent care respectă modelul dat, în cazul nostru
*_exe. Apoi, în linia 16 îi adăugam valoarea $(objects) care va fi expandată
la folosirea valorii variabilei files. În linia 18 definim primul scop din acest
fișier Makefile. Numele lui este all și depinde de alte două scopuri denumite
compile-all și install-all, dar nu are definită nici o comandă. În linia 19
definim scopul compile-all necesar scopului all. Scopul compile-all depinde
de valoarea variabilei $(objects), care după expandare reprezintă timpul modificării
fișierelor foo.o și bar.o, a timpului modifcării fișierului mylib.a și a rezultatului
evaluării scopului do-exe. Când este evaluat un scop, întâi se evaluează dependențele
lui (în cazul nostru fișierele foo.o, bar.o și mylib.a și rezultatul scopului
do-exe) și apoi comenzile asociate lui (dacă este necesară actualizarea). În
linia 20 definim un scop generic care spune că pentru fiecare cuvânt din $(objects)
se aplică modelul %.o și apoi transformarea %.c (% înseamnă orice număr de caractere).
Se consideră cuvânt orice succesiune de caractere diferite de spațiile albe
(spațiu, tab, linie nouă etc.). Traducerea acestui scop generic este: fiecărui
cuvânt din $(objects) i se schimbă extensia din .o în .c și se generează câte
un scop pentru generarea fișierelor cu extensie .o din fișierele corespunzătoare
cu extensia .c. În linia 21 avem comanda asociată scopului generic. Aceasta
face apel la variabilele definite implicit de către programul make: $(CC) și
$(CFLAGS). De asemenea, apar variabilele speciale $<, care semnifică lista
tuturor dependențelor (în cazul nostru, fișierul cu extensia .c), și $@, care
semnifică numele scopului (în cazul nostru fișierul cu extensia .o). În linia
22 este definit un scop care indică necesitatea actualizării fișierului mylib.a
dacă fișierul mylib.c sau mylib.h este mai nou decât el. Linia 23, care este
comanda asociată scopului mylib.a, începe cu caracterul @ ce indică programului
make să nu afișeze comanda pe care o execută (cea care urmează după @), așa
cum face în mod implicit dacă din linia de comandă lipsește argumentul -s sau
acel scop apare în cadrul unui scop .SILENT. În linia 23 întâlnim variabila
specială $^, care semnifică primul cuvânt din lista de dependențe (în cazul
nostru mylib.c), și funcția $(patsubst %.c, %.o,$^), care în această formă semnifică
înlocuirea extensiei .o a primului argument din lista de dependențe cu .c. În
linia 24 valoarea variabilei speciale $@ este mylib.a. În linia 25 apare funcția
$(filter %_exe,$(files)) care are ca efect, în acest context, extragerea din
valoarea variabilei $(files) doar a cuvintelor care au sufixul _exe. Comanda
din linia 26 apare cu un semn plus (+) înaintea ei, ceea ce semnifică faptul
că această comandă va fi executată chiar dacă programul make a fost apelat cu
unul din următoarele argumente: -n, -q sau -t. În linia 27 este definit un scop
generic care are o comandă ce depinde de mediul în care este lansat programul
make. Funcția $(findstring "/bin",$PATH), apelată astfel, întoarce șirul vid
dacă nu există șirul "/bin" în valoarea variabilei de sistem $PATH și "/bin"
altfel.
Astfel, dacă există șirul "/bin" în valoarea lui $PATH atunci se va executa
ramura cu $*.sh, altfel se va executa ramura cu $*.exe. Se observă prezența
variabilei speciale $* a cărei valoare este înlocuită cu partea din cuvânt care
corespunde în model lui %. În cazul nostru, partea din numele fișierelor care
se află înaintea sufixului _exe. În linia 35 este apelat programul make prin
intermediul variabilei speciale $(MAKE) urmat de argumentul -C și numele directorului
în care se află un fișier denumit în una din cele trei forme indicate la începutul
acestui articol. Funcția specială $(dir "/usr/local/ lib/mylib.so") extrage
partea de director din șirul furnizat ca argument, în cazul nostru "/usr/local/lib",
iar funcția $(addsuffix ".dir","cale") adaugă sufixul ".dir" la șirul cale,
în cazul nostru obținem "/usr/ local/lib.dir". Există și funcțiile: $(notdir
"path/file"), care extrag din șir partea care corespunde numelui fișierului,
$(basename "nume"), opusul funcției $(suffix), și $(addprefix prefix,șir), care
adaugă un prefix în fața lui șir. În linia 36 este lansat un shell folosindu-se
funcția $(shell comandă). Linia 38 conține un exemplu de scop fără dependințe.
Comanda din linia 39 este precedată de semnul minus (-) pentru a indica programului
make să continue execuția următoarelor comenzi asociate scopului curent chiar
dacă această comandă se termină cu eroare
(de exemplu, dacă vrem să ștergem niște fișiere este mai simplu să punem semnul
minus în fața comenzii care va șterge fișierele decât să testăm dacă acestea
există pe disc). În linia 42 apar funcțiile $(words $<), care întorc numărul
de cuvinte conținute
(în cazul nostru numărul de dependințe), și $(word poziție,$<), care întorc
poziția cuvântului (indexul primului cuvânt este 1). În cazul nostru este extras
penultimul nume de fișier care are extensia .c din directorul tests.
Pentru o listă completă de funcții puteți consulta nodul "Quick reference"
din documentația în format .info a programului GNU make.
|