Makefile - Řízení překladu a sestavení programu

Příma kompilace programu prostřednictvím kompilátoru a příkazové je vhodnou pro prvotní seznámení se základními přepínači kompilátoru a linkeru. Pro komplexníší programy, ale také pro často opakované akce je mnohem výhodnější použít vhodný nástroj pro řízení překladu. Mezi ty základní patří zcela jistě make nebo případě Linuxu tzv. GNU Make. Není to jediný způsobem mezi další používáné nástroje patří např. cmakecmake nebo třeba Ninja. Pro celou řadu případů a řešení úloh v PRP nám plně postačí make, který lze nalézt snad na všech distribucích Linuxu jako příkaz make, případně jako gmake v BSD* systémech.

Předpis jak program sestavit se zapisuje do tzv. Makefile, a při volání make program hledá v aktuálním pracovním adresáři právě soubor s názvem Makefile. Základní syntax není složitá, ale pokročilé Makefile, které zjednodušují zápis mohou být na první pohled relativně komplexní, neboť se používají zavedená jména proměnných. Nicméně vždy se jedná o deklarativní zápis pravidel jak vytvořit soubor (cíl) z jiných souborů (závislostí).

Cílem tohoto textu není nahradit manuál ani existující návody, jak používat make, ale poskytnou minimální prakticky orientované příklady pro zjednodušení práce na domácích úlohách a jejich odevzdávání. Podrobnější návody lze nalézt napříklady vyhledáváním “gnu make tutorial” nebo “gnu make manual”.

Základní zápis pravidla má tvar

cíl: závislosti
	akce
kde cíl je jméno souboru nebo symbolické jméno cíle, závislosti je jeden nebo více souborů (proměnných nebo symbolických cílů) a akce příkaz, který se má vykonat, aby se splnil (vytvořil) cíl. Zároveň je detekováno, zdali existují a jsou splněny závislosti a pokud ne, hledá se a použije se nejdříve pravidlo pro vytvoření závislostí a teprve poté se zavolá příslušná akce.

Řádek s akcí je odsazen jedním znakem tabulátor.

Make obsahuje jak impliciní pravidla, tak umožňuje zápis explicitních pravidla, na kterých si nejdříve ukážeme základní použití. Následně si zápis Makefile souborů zjednodušíme využitím tzv. “pattern rules” nebo implicitních pravidel.

První makefile s explicitním zápisem

Mějme program se zdrojovým souborem program.c, který můžeme zkompilovat do binárního spustitelného soubor a.out prostým voláním. clang program.c. Raději však explicitně určíme výstupní soubor program voláním:

% clang program.c -o program
případně ještě oddělíme překlad souboru a linkování do spustitelné podoby:
% clang -c program.c -o program.o
% clang program.o -o program
To je poměrné pracné a pokud je náš program složitější, budeme překládat a spouštět opakovaně. Napíšeme si proto jednodudchý předpis jak vytvořit program ze souboru program.c, který uložímě do souboru Makefile
program: program.c
	clang program.c -o program
Nyní stačí zavolat make (případně gmake), který načte Makefile v aktuálním pracovním adresáři a pokusí se sestavit první cíl, kterém je náš program závislý na program.c. Make si u závislostí (na souborech) všímá tzv. modification time a pokud nedošlo ke změně, akce se neprovede, neboť výsledný soubor by byl stejný.

% make
make: 'program' is up to date

To se hodí zejména v projektech s mnoha soubory, kde je pak výhodné kompilovat jednotlivé soubory zvláště do příslušných .o souborů a následně samostatně linkovat. Mimo jiné pak lze využít i paraleního překladu na strojích s více výpočetními jádry nebo procesory. Předpis pro překlad souboru a následné linkování může například vypadat následovně

program.o: program.c
	clang -c program.c -o program.o
 
program: program.o
	clang program.o -o program
Tento předpis má jako první cíl program.o, proto po volání make dojde pouze k vytvoření soubor program.o. Uvedeme-li však cíl program jako argument programu make dojde k sestavení cíle program, který je závislý na program.o a výstup může vypadat například
% make program
clang -c program.c -o program.o
clang program.o -o program

Abychom nemuseli explicitně uvádět cíl při každém volání make můžeme jako první dát implicitní cíl. V našem případě to uděláme vytvořením cíle bin, který má jako závislost 'program'.

bin: program
Dále vytvoříme cíl clean, ve kterém smažeme všechny soubory, vytvořené při překladu. Celý Makefile pak může vypadat následovně
bin: program

program.o: program.c
	clang -c program.c -o program.o

program: program.o
	clang program.o -o program

clean:
	rm -rf program program.o

Použití standardních proměnných

Výše uvedené příklady Makefile souborů lze jistě použít, ale vyžadují poměrné rozsáhlou editaci a uvedení jmen souborů na vícero místech. Mnohem praktičtější je využití vzorů pro překlad soubor, např. jak vytvořit soubory .o ze souborů .c

%.o: %.c
	clang -c $< -o $@
kde je použita proměnná astupující soubor vyhovující pravé straně pravidla, tj. náš program.c, a proměnná definující výstup (levou část pravidla), tj. program.o.

Dále není nutné uvádět explicitně kompilátor, ale je možné využít základních proměnných CC a také parametrů pro preprocessor (CPPFLAGS) a kompilátor CFLAGS.

%.o: %.c
	$(CC) $(CPPFLAGS) $(CFLAGS) -c $< -o $@
Což v podstatě odpovídá výchozímu implicitnímu pravidlu pro kompilací Cčkových souborů. Výhoda použití zavedených proměnných je, že můžeme ovlinit překlad, aniž bychom muse-li měnit samotný Makefile. Například komplilaci s optimizací -O2 můžeme realizovat
$ CFLAGS=-O2 gmake
clang  -O2 -c program.c -o program.o
clang program.o -o program
Nebo použití kompilátoru gcc
% CC=gcc make
gcc -O2 -pipe -c program.c -o program.o
clang program.o -o program

Komplexnější Makefile, ve kterém je nutné pouze specifikovat jméno programu, který se má sestavit ze zdrojových souborů .c (resp. .o) pak může vypadat například

TARGET = program
OBJS = $(patsubst %.c,%.o,$(wildcard *.c))
CFLAGS +=-std=c99 -pedantic -Wall
CFLAGS += -O2

bin: $(TARGET)

$(OBJS):  %.o: %.c
	$(CC) $(CPPFLAGS) $(CFLAGS) -c $< -o $@

$(TARGET): $(OBJS) 
	$(CC) $(OBJS) $(LDFLAGS) -o $@

clean:
	rm -rf $(OBJS) $(TARGET)

Jméno programu nastavuje na první řádku hodnotou proměnné TARGET. Na druhém řádku jsou jména všech souborů v pracovním adresáři končící na .c uložena do proměnné OBJS, ale nejdříve je nahrazen výskyt .c za .o, čímž získáme seznam všech object souborů, ze kterých sestavíme program. Následně specifikujeme nastavení kompilátoru a jeho verzi a to tak, aniž bychom přepsali nějaký původní obsah proměnné CFLAGS, například specifikovaný proměnnou prostředí. Podobně přidáme mezi parametry kompilátoru optimizalizaci -O2. Proměnnou TARGET použijeme jako závislost prvního cíle bin, ale také jako cíl závislý na přeložených souborech $(OBJS), které jsou přeloženy specifikovaných pravidlem se dvěma :, které specifikuje jak individuálně vytvořit soubory uvedené v OBJS ze souborů .c.

Využití hvězdičkové konvence *.c skrývý úskalí při linkování programu, neboť při něm záleží na pořadí uvedených souborů. Proto můžeme být vhodnější explicitně uvést seznam souborů.
Při sestavování programů s více souborů, je vhodné pamatovat jakým způsobem funguje make a jak detekuje změnu v souborech. Pokud nejsou v seznamu závislostí uvedený hlavičkové soubory (což se např. nedělá z důvodu snížení počtu přístup na souborový systém), tak při změně hlavičky funkce není automaticky přelože příslušný .o soubor a výsledný program může vykazovat nepředvídatelné chování. V takovém případě je řešením volat make clean a případně využít ccache nebo změnit Makefile či nástroj pro sestavení programu.
courses/b3b36prg/tutorials/make.txt · Last modified: 2019/03/26 13:43 by faiglj