Make
GNU make je nástroj pro automatizované vytváření cílových souborů ze zdrojových souborů. Často se používá pro překlad zdrojových souborů (např. C/C++) na spustitelné binární soubory, lze jej však s výhodou použít pro všechny typy úloh kde se z nějakých souborů vytvářejí jiné soubory.
Make zpracuje předpis, kterému říkáme makefile a který je zpravidla uložen v
souboru s názvem Makefile
. Makefile
je souborem pravidel v následujícím
tvaru:
target: dependencies
commands
target
je název pravidla, který se zpravidla shoduje s názvem cílového souboru, který chceme vytvořit.dependencies
(závislosti) je nepovinný seznam souborů nebo jiných pravidel, která jsou potřeba pro aplikaci tohoto pravidla. Pokud tyto soubory neexistují, make se pokusí najít pravidla pro jejich vytvoření a rekurzivně je vykoná. Pokud některý soubor nelze ani vytvořit, celé pravidlo selže.commands
je sekvence příkazů (mohou být na více řádek), jejichž vykonáním dojde k provedení pravidla.
Příkazy v Makefile
musí být odsazeny tabulátorem! Pozor na editory které
při stisku tabulátoru vloží sekvenci mezer!
Make se při vykonávání pravidel chová velmi jednoduše: pokud jsou splněny
(existují) požadované dependencies, vykoná se sekvence příkazů. Přitom se
však nijak nekontroluje, že spuštěním uvedených příkazů se opravdu vytvoří
soubor target – mezi hlavičkou pravidla a příkazy není žádná implicitní vazba
(ta je na autoru Makefile
podle napsaných příkazů).
Hlavní výhodou make
je, že provádí pouze ta pravidla, která jsou nutná a
přeskakuje ta, která by se prováděla zbytečně. Typickým případem je situace,
kdy máme projekt sestávající z více zdrojových souborů, všechny soubory již
máme přeložené (předchozím spuštěním make
), provedeme změnu v jednom ze
souborů a chceme projekt znovu přeložit. V takovém případě make
spustí pouze
překlad změněných souborů a ostatní ponechá přeložené od minule. Aby však tato
funkcionalita pracovala správně, je nutné aby byl Makefile
napsán korektně,
což zahrnuje následující pravidla:
- jedno pravidlo provádí pouze jednu elementární operaci (typicky převádí právě jeden soubor na jiný)
- jméno pravidla které vytváří soubor se shoduje se jménem souboru
- pravidlo má korektně uvedené závislosti
Rozhodování o vykonání pravidla se provádí podle následujícího postupu:
- Pokud cílový soubor neexistuje, pravidlo se vždy provede.
- Pokud cílový soubor existuje, pravidlo se provede pokud je čas změny některého souboru v dependencies novější než čas poslední změny cílového souboru.
Je zřejmé, že pokud dojde k nekonzistenci časových značek souborů, make nemusí fungovat správně. V takových případech může být nejbezpečnější všechny vygenerované soubory smazat a nechat make vytvořit celý projekt znovu.
Spouštění make
Make se spouští příkazem make
v adresáři, ve kterém se nachází Makefile
.
Pokud jej spustíme bez parametrů, provádí se první pravidlo v souboru
Makefile
. Parametrem příkazu make
je možné zadat název jiného pravidla,
které se má vykonat.
Pravidla v závislostech se vykonávají podle potřeby rekurzivně. Pokud kterékoliv pravidlo selže (nejsou k dispozici závislosti, nebo některý příkaz skončí chybou), je zastaven celý proces a make vrací chybu.
Při běhu make vypisuje všechny prováděné příkazy, podle čehož lze snadno sledovat, která pravidla se provádějí.
V případě, že vše proběhne v pořádku, vrací make návratovou hodnotu a nemusí vypisovat žádné hlášení na výstup.
Triviální příklad
Mějme aplikaci sestávající ze tří souborů src1.c
, src2.c
a src3.c
,
jejichž překladem má vzniknout binární spustitelný soubor. Funkce main()
je v
jednom z nich. Součástí projektu může být i několik hlavičkových souborů
(.h
), které však z hlediska volání kompilátoru nejsou důležité. Sestavení
aplikace vyžaduje nejprve přeložení jednotlivých zdrojových souborů na binární
.o
soubory a jejich následné “slinkování” do spustitelného souboru
“aplikace”. Makefile
pro sestavení této aplikace pak může vypadat následovně:
aplikace: src1.o src2.o src3.o
gcc -o aplikace src1.o src2.o src3.o
src1.o: src1.c
gcc -c src1.c
src2.o: src2.c
gcc -c src2.c
src3.o: src3.c
gcc -c src3.c
Je zřejmé že pokud by se psal Makefile
takto (samostatné pravidlo pro každý
soubor), bylo by to poměrně pracné a náročné na údržbu v případě dalšího
vývoje. Berte to proto pouze jako ukázku pro vysvětlení základní funkcionality.
Průběh make:
- Po spuštění příkazu
make
se provede první pravidlo “aplikace”, neboť soubor s názvem “aplikace” zatím neexistuje. - Pravidlo “aplikace” je závislé na souborech
src1.o
–src3.o
, které také neexistují. Naleznou se tedy vhodná pravidla pro jejich vytvoření a ta se vykonají. - Pro každý neexistující
.o
soubor se spustí odpovídající pravidlo, které jej vytvoří z příslušného.c
souboru. - Po překladu všech
.c
souborů na.o
se může vykonat příkaz pravidla “aplikace”, který provede slinkování.
Pokročilé možnosti make
Generická pravidla
Pokud máme řadu souborů, které se zpracovávají stejným příkazem (např.
kompilace .c
souborů), není nutné psát pravidlo pro každý soubor zvlášť, ale
je možné využít generické pravidlo ve tvaru
%.o: %.c
gcc -c -o $@ $<
Toto pravidlo se aplikuje vždy, když potřebujeme vytvořit soubor .o
a
existuje soubor .c
se stejným jménem. $<
je proměnná make
, za kterou se
dosadí jméno vstupního .c
souboru, proměnná $@
má hodnotu cíle pravidla,
tedy .o
souboru. Toto pravidlo v sobě navíc implicitně zahrnuje závislost
.o
souboru na .c
souboru.
Proměnné v Makefile
Proměnné (též makra) v Makefile nám umožňují jej parametrizovat a pomáhají jeho udržitelnosti. Zvláště užitečné jsou proměnné pro uložení řetězců nebo seznamů, které se opakují na mnoha místech. Proměnné se zpravidla deklarují v úvodní části Makefile, ještě před prvním pravidlem, ve tvaru
PROMENNA=hodnota
Hodnotu proměnné pak mohu použít v příkazu, v závislostech, jménu pravidla, nebo při definici jiných proměnných zápisem
$(PROMENNA)
Příklad
Při překladu projektu v jazyce C potřebujeme často pracovat se seznamem všech
.o
souborů.
OBJ=main.o src1.o src2.o
bin: $(OBJ)
gcc -o bin $(OBJ)
clean:
rm $(OBJ)
Následující Makefile
slouží k překladu projektu v jazyce C. Pokud se
rozhodneme použití syntaxe jazyka C++ ve zdrojových kódech, nebo budeme chtít
změnit parametry kompilátoru, uděláme změnu pohodlně na jednom řádku, který
najdeme na začátku souboru Makefile
.
OBJ=...
CC=gcc
CFLAGS=-g -Wall -Iinclude
bin: $(OBJ)
$(CC) -o $@ $(OBJ)
%.o: %.c
$(CC) -o $@ -c $(CFLAGS) $<
specific.o: specific.c
$(CC) -o $@ -c $(CFLAGS) -Dspecificka_definice $<
Proměnná $@
, použitá v pravidlu bin se nahradí názvem pravidla (tj. v tomto
případě řetězcem “bin”). Pro vytvoření každého souboru se hledá
nejspecifičtější pravidlo – v tomto případě se tedy soubor specific.c
překládá jiným příkazem než ostatní .c
soubory).
Další možnosti make
Make nám nabízí řadu funkcí a maker pro ulehčení práce.
SRC=$(wildcard *.c)
Za makro wildcard make
dosadí seznam všech souborů odpovídajících dané
masce.
OBJ=$(patsubst %.c,%.o,$(SRC))
Makro patsubst umožňuje provést nahrazení části názvu v seznamu souborů. 3
parametry makra jsou vzor pro nalezení, vzor pro nahrazení a vstupní seznam
souborů. Uvedený příklad nahradí všechna jména .c
souborů v seznamu SRC
odpovídajícími jmény .o
souborů a uloží nový seznam do proměnné OBJ
.
Pokročilé řešení závislostí při překladu kódu v C/C++
Nikde ve výše uvedených příkladech překladu C/C++ kódu se nijak neřešila
závislost na header souborech vložených ve zdrojových souborech direktivou
#include
. Protože make nijak neanalyzuje prováděné příkazy, nemůže si být
této závislosti vědom a při změně některého .h
souboru automaticky nevyvolá
překlad .c
souborů které jej vkládají, jak bychom požadovali.
Toto chování je možné vynutit přidáním header souborů do závislostí pravidel pro jednotlivé soubory:
soubor1.o: soubor1.c header1.h header2.h
$(CC) -o $@ -c $(CFLAGS) $<
soubor2.o: soubor2.c header1.h header3.h header4.h
$(CC) -o $@ -c $(CFLAGS) $<
Tato pravidla mohou existovat i vedle stávajícího pravidla pro překlad (např.
%.o: %.c: ...
), není tedy nutné příkaz pro překlad u každého z nich
opakovat. Výše uvedený příklad by tedy šel přepsat následovně:
%.o: %.c
$(CC) -o $@ -c $(CFLAGS) $<
soubor1.o: header1.h header2.h
soubor2.o: header1.h header3.h header4.h
Uvedený způsob je však velmi náročný na údržbu v rámci vývoje, neboť při
přidání header souboru do .c
souboru je nutné upravit i Makefile
.
Tento problém je možné s využitím překladače gcc
vyřešit pomocí přepínače
-MM
, který umožňuje vygenerovat seznam závislostí na header soubory ve tvaru
kompatibilním s formátem Makefile
. Tento výstup můžete uložit do samostatného
souboru, který pak vložíte do vašeho Makefile
direktivou include
. Funkční
a efektivní řešení na tomto principu však není triviální a před jeho použitím
doporučuji se inspirovat vhodnými příklady.
Jinou možností jak překládat komplikované projekty s korektním uvažováním závislostí je použít sofistikovanější nástroj, určený specificky pro danou činnost (překlad C/C++ projektů), který tyto závislosti hlídá automaticky.
Takovým nástrojem je např. Meson Build system: http://mesonbuild.com/.