Make

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.osrc3.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/.