V tomto článku sa pokúsim objasniť základy make – nástroja pre prácu s väčšími (a nielen väčšími projektmi). K napísaniu tohoto krátkeho úvodu ma inšpirovali občasné zúfalé volania o pomoc v rôznych konferenciách. Mojim cieľom nie je ukázať ako vytvoriť „My First Makefile“, skôr ide o to ukázať najčastejšie používané konštrukcie a napomôcť pochopeniu existujúcich projektov.
Čo je a čo nie je make
Veľmi zjednodušene povedané, make je nástroj, ktorý dokáže vytvoriť ľubovoľný súbor pomocou špecifikovaných príkazov. Tieto príkazy sa vykonajú v prípade, že cieľový súbor neexistuje, alebo cieľový súbor je starší ako ktorýkoľvek z vymenovaných súborov, od ktorých je cieľový súbor závislý. Pri tom všetkom sa make vôbec nezaujíma o obsah súborov – jediné, čo ho zaujíma je dátum modifikácie súboru, prípadne jeho existencia.
Make nie je ani editor, ani IDE (integrované vývojové prostredie), ani kompilátor, ani debuger. Je to jednoduchý, ale veľmi silný nástroj, ktorý dokáže prekompilovať program, ak sa zmenili zdrojové súbory, dokáže vygenerovať novú dokumentáciu ak sa zmenili napr. obrázky, dokáže vytvoriť nový inštalačný balík, ak sa zmenilo čokoľvek.
Pravidlá pre make
Činnosť make sa riadi na základe pravidiel (rules). Pravidlá pre make sa nachadzajú v súbore, tzv. makefile. Obvyklý názov je Makefile (s veľkým prvým písmenom) – tento súbor sa použije, ak nie je špecifikovaný iný názov súboru (prepínačom -f). Makefile obsahuje pravidlá, definície premenných, direktívy a poznámky.
Pravidlo je predpis, ako vytvoriť cieľový súbor. Základný formát je jednoduchý:
ciel: zavislosti príkaz príkaz # poznámka
- cieľ (target) je súbor, ktorý sa má vytvoriť
- závislosti (prerequisities) je zoznam súborov; ak je ktorýkoľvek z nich novší ako cieľový súbor, prípadne cieľový súbor neexistuje, príkazy sa vykonajú
- príkaz (command) je príkaz, ktorého vykonanie prispeje k vytvoreniu cieľového súboru. Na vykonanie príkazov sa obvykle používa štandardný shell (/bin/sh), pod windows command.com. POZOR! Každý riadok s príkazom MUSÍ začínať tabelátorom.
- poznámka (comment) začína znakom # a končí na konci riadku
Pozrime sa na jednoduchý príklad:
test: test.c test.h gcc -o test test.c
Toto pravidlo vytvorí program zvaný test v prípade, že aspoň jeden zo zdrojových súborov je novší ako test. K tomu použije uvedený príkaz pre gcc.
Ak v niektorom pravidle existuje súbor, ktorý je definovaný ako cieľ v inom pravidle, make sa postará najprv o tento súbor a ak treba, tak ho vyrobí. Pozrime sa na konkrétny príklad:
test: testa.o testb.o ld -o test testa.o testb.o testa.o: testa.c test.h gcc -c -o testa.o testa.c testb.o: testb.c test.h gcc -c -o testb.o testb.c
Prvé pravidlo hovorí o tom, že program test sa skladá z modulov testa.o a testb.o. ýalšie dve pravidlá hovoria, ako skompilovať tieto moduly zo zdrojových súborov.
Make začne spracovávať prvé pravidlo. Avšak pretože existujú pravidlá pre jednotlivé moduly, najprv spracuje tieto pravidlá a až potom sa vráti k prvému pravidlu.
V praxi: ak sa zmení súbor testb.c, najprv sa skompiluje modul testb.o (tretie pravidlo) a potom sa zlinkuje program test (prvé pravidlo). Druhé pravidlo sa neaplikuje, keďže ani testa.c a ani test.h sa nezmenili.
Na záver spomeňme rôzne varianty zápisu pravidiel:
- viacnásobný cieľ – ak v pravidle uvedieme viac cieľov, je to to isté ako keby sme zopakovali úplne rovnaký zápis pravidla pre každý cieľ zvlášť
- jeden cieľ vo viacerých pravidlách – v tomto prípade môže existovať iba jedno pravidlo s príkazmi pre tento cieľ. Ostatné pravidlá pre tento cieľ nesmú obsahovať príkazy a slúžia na špecifikovanie dodatočných závislostí
- pravidlá bez príkazov – ako sme už naznačili, tieto pravidlá slúia na definovanie dodatočných závislostí. Ich hlavný význam pochopíme pri implicitných pravidlách
- kombinácia uvedeného– pozrime sa na príklad:
test1.o: test1.h test2.o: test2.h test1.o test2.o: test.h príkazy na kompiláciu
Týmto možnosti pravidiel nekončia. ýalej si rozoberieme špeciálne prípady pravidiel, implicitné pravidlá a použitie premenných. Avšak najprv sa pozrime, ako sa make spúšťa.
Spustenie make
Make sa spúšťa príkazom make. Ako voliteľný parameter môžme použiť meno cieľa. Ak si vezmeme náš príklad a použijeme príkaz make testa.o
, make skompiluje modul testa.o, ale program nebude linkovať. Ak použijeme príkaz make bez špecifikovania cieľa, make spracuje prvý cieľ ktorý v súbore nájde.
Ak chceme použiť iný súbor pravidiel ako štandardné Makefile, môžme špecifikovať názov súboru pomocou prepínača -f.
Vykonávacie pravidlá
Vykonávacie pravidlá sa vyznačujú tým, že ich príkazy sa vykonávajú vždy. Toto sa dosiahne tým, že cieľový súbor sa nikdy nevytvorí. Takýto „cieľový súbor“ slúži v podstate na vykonanie príkazov (phony target). Najlepšie na príklade:
clean: -rm -f *.o test
V tomto príklade po spustení príkazu make clean
sa vymažú uvedené súbory. (Všimnite si znak „-“ na začiatku príkazu: to znamená, že sa chyby pri vykonávaní príkazu ignorujú).
Položme si ale otázku, čo by sa stalo, ak by niekto zlomyseľne alebo omylom vytvoril súbor clean. Stalo by sa to, že príkazy by sa nikdy nevykonali – cieľ existuje, neexistujú žiadne závislosti, takže niet dôvodu čokoľvek robiť. Našťastie existuje spôsob, ako tomu zabrániť. Stačí vymenovať všetky ciele vykonávacích pravidiel v direktíve .PHONY:
clean: -rm -f *.o test .PHONY: clean
Príkazy pre ciele, vymenované v direktíve .PHONY sa budú vykonávať vždy, nezávisle od existencie súboru s rovnakým menom.
Zvlátnym prípadom vykonávacieho pravidla je pravidlo s časovou značkou (timestamp). Toto pravidlo síce produkuje výstupný súbor, avšak jeho obsah je nepodstatný – dôležitý je čas zmeny. Opäť príklad:
print: testa.c testb.c test.h lpr -p $? touch print
Toto pravidlo nám vytlačí všetky súbory, ktoré sa zmenili od poslednej tlače. Kombinácia znakov $? je takzvaná automatická premenná a pre túto chvíľu sa uspokojíme s vysvetlením, že táto premenná obsahuje zoznam všetkých závislostí, ktoré sú novšie ako cieľ.
Tento druh pravidla sa nesmie uvádzať v direktíve .PHONY.
Premenné
V podstate rozlišujeme dve skupiny premenných.
Prvou skupinou sú takzvané automatické premenné, ktorých obsah je priamo odvodený z pravidla a dajú sa použiť len v príkazoch pravidla. Hlavný význam automatických premenných je v tzv. implicitných pravidlách (zmienime sa ďalej). Najdôležitejšie automatické premenné môžme vidieť v tabuľke:
Premenná | Popis |
---|---|
$@ | Meno cieľa |
$< | Meno prvej zo závislosti |
$? | Zoznam závislostí, novších ako cieľ |
$^ | Zoznam všetkých závislostí |
Použitie automatických premenných budeme ilustrovať trochu neskôr, v časti o implicitných pravidlách.
Druhou skupinou sú užívateľsky definované premenné. Takýmto premenným môžme priradiť hodnotu, môžme hodnotu premennej použiť pri definícii inej premennej, a – a to je to hlavné – môžme jej hodnotu použiť v príkazoch. Pozrime sa ako:
VAR1=abc def VAR2=$(VAR1) ghi VAR3=$(VAR1) VAR3+= ghi echo $(VAR2) echo $(VAR3) echo $(VAR3:hi=xx) echo $(word 2,$(VAR3))
V prvom riadku priradíme premennej VAR1 hodnotu abc def. V druhom riadku premennej VAR2 priradíme hodnotu abc def ghi, pričom abc def vezmeme z premennej VAR1.
Tretí a štvrtý riadok nám ukazujú, ako pripojiť hodnotu k existujúcej premennej.
V príkazoch echo môžme vidieť použitie premenných. v prvých dvoch príkazoch sa jednoducho referencia na premennú nahradí hodnotou premennej a oba príkazy vypíšu hodnotu abc def ghi.
Tretie echo je príkladom substitúcie – v každom slove v premennej VAR3, končiacom na hi, sa táto koncovka nahradí textom xx. Substitúcia sa používa hlavne na náhradu koncoviek v mene súboru. Posledné echo je príkladom funkcie – v tomto prípade sa zobrazí druhé slovo z obsahu premennej VAR3. Substitúcie a funkcie sú však nad rámec tohoto úvodu. Zmieňujem sa o nich len pre ilustráciu možností práce s premennými.
Existuje množstvo preddefinovaných premenných. Ich význam je hlavne v implicitných pravidlách, ktoré sú zabudované v make – tzv. katalógové pravidlá. Tieto premenné obvykle predstavujú názvy programov a sady prepínačov. Uveďme si aspoň niekoľko z nich, používaných pri práci s C:
Premenná | Popis |
---|---|
MAKE | Názov programu make |
CC | Názov C kompilátora |
CFLAGS | Prepínače pre C kompilátor |
CPPFLAGS | Prepínače pre C preprocesor |
CPP | Názov C preprocesora |
AS | Názov assemblera |
ASFLAGS | Prepínače pre assembler |
LD | Názov linkera |
LDFLAGS | Prepínače pre linker |
SHELL | Názov shellu, ktorý sa použije na vykonanie príkazov |
Pár príkladov, ako vyzerajú príkazy s použitím premenných:
$(CC) -c $(CPPFLAGS) $(CFLAGS) -o test.o test.c $(CC) $(LDFLAGS) -o test test.o $(AS) $(ASFLAGS) -o test.o test.s
Odporúča sa používať tieto názvy premenných aj v prípade, že si budete vytvárať vlastné pravidlá. Zjednodušíte život každému, kto sa bude pokúšať pochopiť Vaše Makefile.
Implicitné pravidlá
Implicitné pravidlo (implicit rule) určuje, ako vytvoriť cieľový súbor určitého typu bez toho, aby sme museli špecifikovať pravidlo pre každý jeden súbor. Typickým príkladom je získanie objektového súboru zo zdrojového textu.
Existuje množstvo zabudovaných implicitných pravidiel. Avšak každopádne je najlepšie definovať tieto pravidlá znova (aj keď ich opíšete od slova do slova) – nikdy nevieme, aké modifikáciu môže obsahovať tá „vaša“ verzia make. Zoznam týchto tzv. katalógových pravidiel nájdete v dokumentácii k make.
Impicitné pravidlá sa vytvárajú pomocou šablónových pravidiel (pattern rules). A toto je jednoduchá definícia implicitného pravidla:
%.o : %.c $(CC) -c $(CPPFLAGS) $(CFLAGS) -o $@ $<
Toto implicitné pravidlo môžme čítať ako: súbor s koncovkou .o získame zo súboru s koncovkou .c pomocou príkazov …
A ako to funguje v praxi: Ak potrebujeme získať napr. súbor testa.o a neexistuje preňho žiadne iné použiteľné pravidlo, príde ku slovu naše implicitné pravidlo. Znak % sa nahradí menom súboru bez koncovky (zjednodušene povedané), automatické premenné sa nahradia menom cieľa a menom zdrojového súboru. Výsledok je, ako keby sme napísali pravidlo v znení:
testa.o : testa.c $(CC) -c $(CPPFLAGS) $(CFLAGS) -o testa.o testa.c
Implicitné pravidlo sa použije, ak pre súbor, ktorý potrebujeme získať neexistuje žiadne pravidlo, alebo pravidlo pre tento súbor existuje, avšak nemá žiadne príkazy. Táto vlastnosť umožňuje špecifikovať dodatočné závislosti (ako napríklad .h súbory), ktoré by sme inak nevedeli špecifikovať.
test1.o: test.h test1.h test2.o test4.o: test.h test2.h test3.o test4.o: test.h test3.h %.o : %.c $(CC) -c $(CPPFLAGS) $(CFLAGS) -o $@ $<
Všimnime si, že v prvých troch pravidlách nie je vymenovaný samotný zdrojový súbor. Ten nie je potrebný, pretože make si ho dokže vydedukovať.
Aby sme lepšie pochopili, ako táto dedukcia funguje, predstavme si situáciu, keď máme viac implicitných pravidiel (táto situácia je viac ako reálna, pretože existujú zabudované implicitné pravidlá):
%.o : %.c $(CC) -c $(CPPFLAGS) $(CFLAGS) -o $@ $< %.o : %.s $(AS) $(ASFLAGS) -o $@ $<
Predstavme si, že potrebujeme získať súbor testa.o. Ktoré pravidlo sa bude aplikovať? V prvom rade sa berie do úvahy, či daný zdrojový súbor existuje. Ak existuje buď testa.c alebo testa.s (avšak nie obidva naraz), použije sa príslušné pravidlo.
Ak však existujú obidva zdrojové súbory testa.c a [/i]testa.s[/i], ktoré pravidlo sa aplikuje v tomto prípade?
K určeniu poradia hľadania implicitných pravidiel slúži direktíva .SUFFIXES, obsahujúca zoznam koncoviek súborov. Preddefinovaný obsah je taký, že .c je pred .s. Takže sa použije súbor testa.c. Keby sme predefinovali poradie (dvojriadkový zápis je potrebný, prvá direktíva ruší existujúci zoznam, druhá direktíva pridáva k zoznamu):
%.o : %.c $(CC) -c $(CPPFLAGS) $(CFLAGS) -o $@ $< %.o : %.s $(AS) $(ASFLAGS) -o $@ $< .SUFFIXES: .SUFFIXES: .o .s .c
použil by sa zdrojový súbor testa.s. A toto je vyššie zmienený spôsob, ako make dokáže vydedukovať zdrojový súbor, keď nie je uvedený ako závislosť v pravidle.
V starších projektoch môžme nájsť starý spôsob zápisu implicitných pravidiel pomocou takzvaných koncovkových pravidiel. Tento zápis sa neodporúča používať, a je plne nahraditeľný šablónovými pravidlami. Naše pravidlo pre kompiláciu C by vyzeralo takto (všimnime si obrátené poradie koncoviek):
.c.o: $(CC) -c $(CPPFLAGS) $(CFLAGS) -o $@ $<
Tieto pravidlá nesmú obsahovať závislosti a všetky koncovky musia byť vymenované v direktíve .SUFFIXES.
Ešte sa zmienime o špeciálnom prípade šablónových pravidiel – o statických šablónových pravidlách (static pattern rules). Tieto pravidlá definujú šablónové pravidlo pre vymenovaný zoznam cieľových súborov. Spôsob zápisu je zrejmý z príkladu:
testc.o testd.o: %.o : %.c $(CC) -c $(CPPFLAGS) $(CFLAGS) -o $@ $<
Príklad na záver
Na záver jednoduché, ale komplexné Makefile.
CC=gcc CFLAGS+=-g -O2 OBJ1=testa.o testb.o OBJ2=testa.o testc.o all: test1 test2 testb.o: test1.h testc.o: test2.h testa.o testb.o testc.o: test.h test1: $(OBJ1) $(CC) $(CFLAGS) $(LDFLAGS) -o $@ $^ test2: $(OBJ2) $(CC) $(CFLAGS) $(LDFLAGS) -o $@ $^ %.o : %.c $(CC) -c $(CPPFLAGS) $(CFLAGS) -o $@ $< .PHONY: all clean print clean: -rm -f *.o test1 test2 print: testa.c testb.c testc.c test1.h test2.h test.h lpr -p $?
Hneď na prvý pohľad je jasné, že tento príklad popisuje, ako skompilovať a zlinkovať dva programy: test1 a test2.
Pritom test1 sa skladá zo zdrojových súborov testa.c a testb.c a test2 sa skladá zo zdrojových súborov testa.c a testc.c. Všetky zdrojové súbory závisia od hlavičkového súboru test.h, a niektoré majú ešte svoje vlastné závislosti.
Vykonávacie pravidlá clean a print nevyžadujú zvláštny komentár.
Zhrnutie
Verím, že tento stručný úvod pomohol pochopiť silu (kráčajcu ruku v ruke s jednoduchosťou) nástroja make.
Ak chcete vedieť viac, pozrite si originálnu dokumentáciu k GNU verzii tohoto programu, ktorá sa nachádza na http://www.gnu.org/software/make/manual/make.pdf. Stojí za to. V žiadnom prípade neodporúčam publikácie z produkcie slovenských a českých vydavateľstiev. Tie väčšinou slúžia na znechutenie a odradenie.