Compilarea
Proiectelor in UNIX.
Utilizarea
Makefile.
De
la Codul-Sursa la Programul Executabil
-
Preprocesare
In
general, programele sursa trec printr-o faza de precompilare in care se
fac cateva modificari simple asupra textului programului, rezultand un
fisier care contine un text in acelasi limbaj ca si cel initial.
Principalele operatii in timpul preprocesarii sunt:
-
expandarea
macrodefinitiilor (#define)
-
includerea
de fisiere. (#include)
-
Compilarea
Compilare
transforma un program scris intr-un limbaj de programare compilabil (ex.
C, C++) in cod obiect. Pentru limabjul C++ intrarea procesului de
compilare este un fisier C++, iar iesirea este un fisier cu reprezentand
codul obiect generat, intr-un format pregatit pentru
faza de
editare a legaturilor. Compilarea realizeaza implicit si preprocesarea.
De obicei, in UNIX fisierele care contin codul-obiect au acelasi nume cu
fisierul compilat dar au extensia ".o"
-
Linkeditarea
Are
rolul de a "lega" intre ele mai multe fisiere-obiect si de a crea pe baza
lor un fisier executabil. Faza de linkeditare este impusa de
facilitatea
limbajului C++ de compilare separata.
In afara de fisierele care contin codul realizat de utilizator, mai este
uneori includerea unor biblioteci de obiecte utilizate de program. Asadar
intrarile etapei de likeditare sunt fisierele-obiect obtinute din etapa
de compilare si bibliotecile externe.
Exemple
de Compilare cu gcc/g++
Compilatorul
gcc
este la origine unul dintre cele mai bune compilatoare de C existente.
El a fost extins pentru a putea compila si programe C++, si aceasta
varianta a sa a fost denumita g++
. modul sau de utilizarea fiind insa consistent gcc-ul.
Nu ne propunem in acest paragraf sa discutam in detaliu utilizarea compilatorului
gcc/g++ , ci doar sa oferim cateva exemple care sa ilustreze modul uzual
de utilizare a sa. Pentru detalii consultati pagina de manual (man
gcc).
-
Compilarea
unui fisier-sursa unic. In acest caz compilarea si faza de linkeditare
pot fi contopite, faza de linkeditare desfasurandu-se transparent pentru
utilizator. Astfel printr comanda:
obtinem
din fisierul sursa C++ foo.cc executabilul
foo.
-
Compilarea
unui program cu fisiere-sursa multiple. Sa consideram in acest scop
compilarea programului prezentat ca exemplu in lucrarea
2, format din fisierele sursa stack.cc
si main.cc.
Pentru
a ajunga la programul executabil, trebuie mai intai compilate cele fisiere:
g++
stack.cc -c stack.o; g++ main.cc -c ;
Observati
din exemplul de mai sus precizarea numelui fisierului obiect nu este obligatorie,
implicit aplicandu-se regula mentionata mai sus. Odata compilate, cele
doua fisiere-obiect trebuie astfel:
g++
stack.o main.o -o stack;
-
Compilarea
unui program bazat pe biblioteci externe. Sa presupunem ca extinzand
programul de mai sus folosim pentru afisarea stivei functii ale bibliotecii
ncurses.
Considerand ca avem deja compilate cele doua fisiere sursa ca mai sus,
linkeditarea se va face astfel:
g++
stack.o main.o -I/usr/lib/libncurses.a -o stack;
Necesitate
Compilarii Separate. Dependente.
Dezvoltarea
unor programe de dimensiuni mai mari intr-un limbaj de programare presupune
o metodologie de lucru diferita de cazul programelor mici. La programele
simple este suficienta crearea unui fisier care contine codul si apoi,
printr-o singura comanda la nivelul procesorului de comenzi sau in cadrul
unui mediu integrat, se obtine fisierul executabil.
La
programele mai mari lucrurile nu sunt atat de simple: daca am avea un singur
fisier programul ar fi foarte greu de inteles si in plus trecerea la programul
executabil ar necesita un timp mare, intrucat modificarea celui mai mic
detaliu ar impune recompilare intregului cod. nefolosindu-se facilitatea
de compilare separata. In plus, proiectele mari sunt intotdeauna rezultatul
muncii unei intregi echipe, ceea ce face imperios necesara impartirea intr-un
numar mare de fisiere (numite module) codul-sursa si de a compila independent
aceste fisiere. Aceasta facilitate se numeste compilare separata.
Desi
distincte si compilabile separat, diferitele module apartin aceluiasi proiect
si in mod normal nu sunt total independente intre ele. Numim aceste relatii
intre diferitele module, dependente. Procesul
de modularizare are ca scop tocmai detectarea si minimizarea
acestor dependente intre module. Aceste dependente au un impact evident
asupra procesului de compilarea si anume: atunci
cand a fost modificat un modul, recompilarea sa implica si recompilarea
tuturor modulelor care depind de el.
Utilitarul
make.Fisierele
Makefile
Utilitarul
make
determina automat care sunt bucatile unui proiect care trebuie recompilate
ca urmare a operarii unor modificari si declanseaza comenzile necesare
pentru recompilarea lor.
Pentru
a putea utiliza make, trebuie sa scrieti un fisier numit Makefile
(sau makefile)
care descrie relatiile de de dependenta intre diferitele
fisiere din care se compune programul, si specifica regulile de actualizare
pentru fiecare fisier in parte. In mod normal, intr-un program fisierul
executabil este actualizat (recompilat) pe baza fisierelor-obiect, care
la randul lor sunt obtinute prin compilarea fisierelor sursa.
Odata
creat acest fisier Makefile, de fiecare data cand operati vreo modificare
in fisierele sursa, este suficient sa tastati in linia de comanda:
make
pentru ca toate recompilarile necesare
sa fie efectuate. Programul make utilizeaza fisierul Makefile ca
baza de date si pe baza timpilor ultimei modificari a fisierelor din Makefile
decide care sunt fisierele care trebuie actualizate. Pentru fiecare din
aceste fisiere, declanseaza comenzile precizate in Makefile.
Un
Exemplu de Makefile
Vom
prezenta in continuare ca un exemplu
de Makefile scris pentru compilarea programului prezentat ca exemplu
in lucrarea 2,:
| #
Declaratiile de variabile
CC
= g++
CCFLAGS
= -Wall
SRC
= stack.cc \
main.cc
OBJ
= stack.o \
main.o
PROGRAM
= stack
#
Regulile de compilare
$(PROGRAM)
: $(OBJ)
$(CC)
$(OBJ) -o $(PROGRAM)
stack.o
: stack.cc stack.h
$(CC)
stack.cc -c $(CCFLAGS)
main.o
: main.cc stack.h
$(CC)
main.cc -c $(CCFLAGS)
#
Regulile de ... curatenie
.PHONY
: clean
clean
:
rm
-f $(PROGRAM) $(OBJ) core *~ |
Structura
unui Makefile
Un
Makefile se poate compune din urmatoarele elemente:
-
Reguli
. O regula defineste cand si cum trebuie refacute
unul sau mai multe fisiere denumite obiectivele regulii.
O regula enumera fisierele de care depind obiectivele regulii, numite dependente
si defineste comenzile ce trebuie executate pentru crearea
sau actualizarea obiectivelor.
-
Declaratii
de Variabile. Sunt linii care atribuie unui nume (variabila) un
sir de caractere, nume ce va putea fi folosit in continuare pentru a subsitui
respectivul text.
-
Directivele
sunt comenzi pentru programul make prin care i se comunica acestui
sa face ceva special in timp ce citeste fisierul Makefile, de exemplu sa
includa un alt Makefile sau sa decida bazat pe valoarea unei variabile
daca sa igonore sau nu o anumita parte a fisierului Makefile. Nu tratam
acest aspect aici.
-
Comentarii.
O linie care incepe cu caracterul diez (`#') este o linie de comentariu,
continutul ei fiind ignorat.
Observatie:
Intr-un Makefile o linie tine pana la intalnirea caracterului CR ('\n').
Totusi o linie poate fi "continuata" si dupa caracterul CR ('\n') prin
utilizarea caracterului backslash (\) ca in exemplul de mai sus.
Structure
unei Reguli
Un
Makefile simpul se compune din reguli de urmatoarea forma:
OBIECTIV
. . . : DEPENDENTA
. . .
COMANDA
. . .
. . .
Un obiectiv
(taget) este in mod normal numele unui fisier generat de catre un
program, de exemplu fisiere-obiect sau fisiere executabile. Un obiectiv
poate fi si numele unei actiuni care trebuie executata, cum este obiectivul
clean
din exemplul nostru. (vezi Obiective Phony)
O
dependenta este un fisier utilizat ca intrarea pentru a crea
un obiectiv. De obieci un obiectiv depinde de mai multe fisiere.
O
comanda este o actiune pe care o executa make pentru o anumita
regula. Pentru o regula se pot executa mai multe comenzi.
Atentie:
Fiecare comanda trebuie plasata
pe o alta linie in fisierul Makefile.
Este obligatoriu ca fiecare linie ce contine o comanda sa inceapa cu un
tab.
De
obicei o comanda apare intr-o regula cu dependente si serveste la crearea
unui fisier-obiectiv daca vreunul din fisierele dependenta sunt modificate.
Cu toate acestea, regula care specifica comenzi pentru un obiectiv nu trebuie
sa aiba neaparat dependente, cum este cazul regula clean
in care se sterg anumite fisiere nu are nici o dependenta, comezile executandu-se
intotdeauna cand acesta regula este invocata.
Obiective
Phony
Spre
deosebire de celelate obiective, un obiectiv phony
nu reprezinta numele unui fisier. Este mai degraba un nume pentru un set
de comenzi care trebuie executate cand regula este apelata explicit. Exista
doua motive pentru care declaram un obiectiv ca fiind phony:
-
evitarea
unui conflict cu un fisier cu acelasi nume.
-
cresterea
performantei (evitandu-se cautarea regulilor implicite)
Daca scrieti
o regula a carei comenzi nu vor crea fisierul-obiectiv comenzile vor fi
executate de fiecare data cand regula este invocata. Regulile phony vor
inceta insa sa functioneze corect daca se creaza in acel director un fisier
cu acelasi nume cu cel al obiectuvului phony
(in
exemplul nostru, daca s-ar crea un fisier cu numele clean).
Intrucat regula nu are dependente, "fisierul" clean va fi considerat in
permanenta actualizat, iar comenzile sale nu se vor executa. Pentru a evita
aceasta problema, puteti declara explicit un obiectiv ca fiind phony, utilizand
regula speciala ".PHONY"
(vezi exemplul). Prin aceasta, comanda "make
clean"
va executa comenzile indiferent daca exista sau nu un fisier numit "clean".
Executia
unui Makefile
Intrucat
un Makefile contine mai multe reguli, intrebarea care se ridica este: care
dintre reguli se va executa la apelul lui make
fara nici un parametru? In acest caz se va executa intotdeauna prima
regula intalnita in Makefile. Din acet motiv la compilarea unui proiect
se obisnuieste declararea prima data a regulii prin care se obtine executabilul.
Daca se doreste executarea unei anumite reguli se apeleaza make
sub forma: make
numele_regulii
;
Bibliografie
Probabil
cea mai cuprinzatoare si bine structurata documentatie on-line pentru utilizarea
Makefile-urilor este cea disponibila prin comanda info
in aproape toate sisteme UNIX. Aceasta se poate apela fie sub forma:
info -f /usr/info/make , dar se recomanda
utilizarea utilitarului info din Emacs (ESC-x info) .