Makefile

O programu make

Nemůžete programovat v Linuxu a neznat program make. Program make vám pomůže zjednodušit proces kompilace, urychlit ho a zautomatizovat.

Program make čte soubor (obvykle nazvaný) Makefile. Do něj se zapisují instrukce, jak se má váš program přeložit a případně nainstalovat.

Při kompilaci obvykle vytváříte nějaké dočasné soubory (.o, .a). Ty se vytvářejí ze souborů .c a .h. Program make z popisu v Makefile dokáže poznat, které soubory jste změnili od poslední kompilace a přeloží jen ty soubory, které je potřeba aktualizovat. Tím překlad při vývoji značně urychluje. No a v neposlední řadě se do Makefile dají napsat instrukce, jak program nainstalovat. To zase pomáhá uživatelům vaší aplikace, kterým by mělo stačit pro překlad a instalaci vašeho programu spustit tzv. svatou trojici:

$ ./configure
$ make
$ make install

Tohle je nejběžnější způsob, jak se instalují programy ze zdrojových souborů v Linuxu.

Skript configure je obvykle skript napsaný v bashi (a generovaný pomocí autotools). Obvykle se stará o nastavení správných cest k hlavičkovým souborům a knihovnám, zjišťuje, jestli je nainstalováno vše co je potřeba pro překlad, dává uživateli na výběr různé volby (například jazyk aplikace, kam se bude instalovat) atp. Tímto skriptem se zabývat v této kapitole nebudu.

V této kapitole se soustředím na to, jak napsat správný Makefile pro překlad a instalaci programů v jazyce C/C++.

Cíle (target)

Všichni máme v životě nějaké cíle a ne jinak je to i s programem make :). Ten načte své cíle ze souboru Makefile. A jak název napovídá, makefile = vytvoř soubor, obvykle je cílem nějaký soubor.

Název takového souboru se napíše na samostatnou řádku a zakončí se dvojtečkou. Za dvojtečku se píšou subory, na kterých je cíl závislý. Například:

hello: hello.c

Tohle zapíšete do souboru Makefile. A program make už má svůj cíl. Ví, že chcete vytvořit soubor hello na základě souboru hello.c

A teď pozor, přijde kouzlo! Pokud soubor hello už existuje a jeho čas poslední modifikace je novější než čas modifikace souboru, na kterém zavísí, program make neudělá nic! To je celé kouzlo programu make.

Ještě by se ale hodilo programu make říct, jak soubor hello ze suboru hello.c vytvoří. Všechny k tomu potřebné příkazy se píší na řádky za řádkem s cílem (a jeho závislostmi). Všechny příkazy musí začínat tabulátorem.

hello: hello.c
        gcc -o hello hello.c

Do souboru Makefile můžete vkládat komentáře. Každá řádka, která začíná znakem # je považována za komentář.

#
# Makefile.1
#
hello: hello.c
        gcc -o hello hello.c

Program make defaultně načítá soubor Makefile, ale argumentem -f soubor ho můžete přimět načítat jiný soubor. Protože budu v této kapitole ukazovat více příkladů Makefile, pojmenuji je Makefile.1 atp. a vy je můžete spustit takto:

$ make -f Makefile.1
gcc -o hello hello.c

Vidíte, že se spustil příkaz gcc -o hello hello.c. Pokud byste náhodou zkopírovali obsah z této stránky, pak budete mít příkaz odsazený 8 mezerami místo tabulátoru a dočkáte se tohoto:

$ make -f Makefile.1
Makefile.1:5: *** missing separator (did you mean TAB instead of 8 spaces?).  Stop.

Když už se program hello vygeneroval, zkuste spustit make znovu:

$ make -f Makefile.1
make: `hello' is up to date.

Vidíte, make poznal, že je hello aktuální a žádné příkazy nespustil. Zkuste ale upravit soubor hello.c, nebo stačí jenom aktualizovat jeho čas modifikace příkazem touch:

$ touch hello.c
$ make -f Makefile.1
gcc -o hello hello.c

Cílů může být v Makefile více a mohou se řetězit, tj. jeden cíl může být závislostí jiného cíle. Např:

#
# Makefile.2
#
hello: hello.o
        gcc -o hello hello.o

hello.o: hello.c
        gcc -c hello.c
$ make -f Makefile.2
gcc -c hello.c
gcc -o hello hello.o

Program make si zase vybral za cíl hello (protože je to první cíl v souboru Makefile). Tento cíl závisí na souboru hello.o, který neexistuje. Takže make hledá, jestli náhodou nemá nějaký cíl hello.o a ten se pokusí vytvořit …

Pokud předáte programu make jako argument název cíle, pak se pokusí vytvořit tento cíl (ne ten první).

$ make -f Makefile.2  hello.o
make: `hello.o' is up to date.
$ rm hello.o
$ make -f Makefile.2  hello.o
gcc -c hello.c

Teď už by vám mělo dávat smysl make; make install. První make spustí defaultní cíl (což bývá vytvoření programu) a druhé volání make spustí cíl install. Nojo, ale copak chceme vytvořit soubor install?

.PHONY

Cílem nemusí být soubor. Pokud požádáte o vytvoření nějakého cíle a soubor se jménem cíle neexistuje, make spustí všechny příkazy pod tímto cílem. Třeba cíl install se používá pro instalaci vytvořeného programu, což bývá příkaz pro zkopírování programu někam. Protože install není soubor, spustí se příkazy vždy. Co kdyby ale nááááhodou nějaký soubor se jménem install existoval?

Od toho je tady právě .PHONY: cíl cíl.

Tímto se označují cíle, které nejsou soubory. Jeho použití není povinné. Dá se předpokládat, že ve svém adresáři s projektem (a souborem Makefile) nebudete mít soubor install, ale taky se může stát, že někdo jednou soubor install vytvoří (bude v něm popisovat instalaci). Jistota je jistota a proto je lepší .PHONY použít.

Dalším obvyklým cílem, který není soubor, je clean. Ten se používá pro vyčištění projektu od všecho generovaného (.o, .a, výsledného programu atp.), prostě uvádí projekt do stavu před spuštěním kompilace.

A poslední, o kterém vám zatím řeknu, je cíl all. Je pravda, že se obvykle spoléhá na to, že make spouští první nalezený cíl, ale bývá zvykem jako první cíl použít all. Takže příkazy make a make all by měly dělat to samé.

#
# Makefile.3
#
.PHONY: all clean install

all: hello

hello: hello.o
        gcc -o hello hello.o

hello.o: hello.c
        gcc -c hello.c

clean:
        rm -f hello *.o

install: hello
        cp ./hello ~/bin/

Všiměte si použití volby -f u příkazu rm. Díky této volbě příkaz rm neselže, pokud žádný soubor "*.o" neexistuje. Pokud někdy nějaký příkaz selže, make vypíše chybovou hlášku a zastaví se.

Taky si všiměte, že cíl install je závislý na cíli hello. Dokud neexistuje soubor hello, tak přeci není co instalovat … Taky si všiměte, že cíl all má jen závislost, ale nemá žádné další příkazy k provedení. Také k čemu? Cíl all jenom chce, aby se provedl cíl hello.

$ make -f Makefile.3  clean
rm -f hello *.o
$ make -f Makefile.3  all
gcc -c hello.c
gcc -o hello hello.o
$ make -f Makefile.3
make: Nothing to be done for `all'.
$ make -f Makefile.3 install
cp ./hello ~/bin/

Příkazy

Příkazy se spouštějí v shellu. V jakém? Obvykle /bin/sh. Shell můžete změnit například takto: SHELL = /bin/bash, ale to se nedoporučuje.

Naopak se doporučuje, abyste používali sh (kvůli přenositelnosti). A co víc, aby uživatel neměl náhodou přenastavený shell na něco neočekávaného, doporučuje se nastavit /bin/sh explicintě.

Příkazy se před provedením nejdříve vypíší na obrazovku, takže máte poměrně přehled o tom, co se děje. Ale občas se to naopak nehodí. Proto můžete na každý řádek s příkazem napsat na začátek @, který zabrání vypsání příkazu.

#
# Makefile.4
#
SHELL = /bin/sh
.PHONY: all clean install

all: hello

hello: hello.o
        gcc -o hello hello.o

hello.o: hello.c
        gcc -c hello.c

clean:
        @rm -f hello *.o
        @echo 'Projekt byl vyčištěn'

install: hello
        cp ./hello ~/bin/
$ make -f Makefile.4 clean
Projekt byl vyčištěn

Pozor:

Každý řádek s příkazem se spouští ve vlastním shellu!

Je to možná divné, ale je to tak. Má to své neblahé důsledky. Pokud například na jednom řádku změníte adresář, na druhém řádku jste zase tam, kde jste byli. Nebo když na jednom řádku nastasvíte nějakou proměnnou, na druhém řádku už zase neexistuje. Dá se to obejít jenom tak, že spojíte příkazy do jednoho dlouhého příkazu. Příkaz naštěstí můžete rozdělit na více řádků pomocí zpětného lomítka před koncem řádku.

V shellu musíte zdvojovat $, protože jedním $ se uvozují proměnné programu make (o těch bude řeč coby dup).

#
# Makefile.5
#

SHELL = /bin/sh
.PHONY: id wrong1 ok1 ok2

id:
        @echo Id shellu je $${$$}
        @echo Id shellu je $${$$}
        @echo Id shellu je $$$$

wrong1:
        ID=${$}
        echo Id shellu je $${$$}
        echo Id shellu je $$ID

ok1:
        @ID=$$$$; echo Id shellu je $$$$; echo Id shellu je $$ID

ok2:
        @ID=$$$$;\
        echo Id shellu je $$$$;\
        echo Id shellu je $$ID
$ make -f Makefile.5 id
Id shellu je 4484
Id shellu je 4485
Id shellu je 4486
$ make -f Makefile.5 wrong1
ID=${$}
echo Id shellu je ${$}
Id shellu je 4489
echo Id shellu je $ID
Id shellu je
$ make -f Makefile.5 ok1
Id shellu je 4493
Id shellu je 4493
$ make -f Makefile.5 ok2
Id shellu je 4495
Id shellu je 4495

Proměnné

V Makefile si můžete vytvořit proměnnou a pak ji používat v příkazech. Můžete si tím zjednodušit a zpřehlednit psaní Makefile.

#
# Makefile.6
#

CC = gcc
CXX = g++
CFLAGS = -g -Wall
CPPFLAGS = -g -Wall
TARGETS = hello hellocpp

all: $(TARGETS)

hello: hello.o
        $(CC) -o hello hello.o

hello.o: hello.c
        $(CC) $(CFLAGS) -c hello.c

hellocpp: hellocpp.o
        $(CXX) -o hellocpp hellocpp.o

hellocpp.o: hello.cpp
        $(CXX) $(CPPFLAGS) -c hello.cpp -o hellocpp.o
       
clean:
        rm -f *.o $(TARGETS)

V příkladu jsem vytvořil a použil hned 5 proměnných. Všiměte si, že „make proměnné“ se v příkazech uvozují jen jedním dolarem a jsou v kulatých závorkách (na rozdíl od proměnných bashe, jejichž jména jsou v složených závorkách {}). Tyto závorky nejsou povinné, ale doporučuji je používat kvůli čitelnosti.

$ make -f Makefile.6 clean
rm -f *.o hello hellocpp
$ make -f Makefile.6 all
gcc -g -Wall -c hello.c
gcc -o hello hello.o
g++ -g -Wall -c hello.cpp -o hellocpp.o
g++ -o hellocpp hellocpp.o

S proměnnými se dají dělat různé věci. Jedna z nich je přidat k proměnné další hodnotu (pomocí +=):

CPPFLAGS = -g
CPPFLAGS += -Wall

V makefile lze používat i různé řídící struktury (podobně jako if v C). To už je ale nad rámec této kapitoly.

Implicitní proměnné

Make definuje některé implicitní proměnné. Tj proměnné, které mají přednastavenou hodnotu a používají se v implicitních pravidlech (o těch bude řeč hned v zápětí).

Tabulka těch nejdůležitějších:

AR Archivační program, defaulntě 'ar'
CC Program pro kompilaci jazyka C, defaulntě 'cc'
CXX Program pro kompilaci jazyka C++, defaultně 'g++'
CFLAGS Extra volby pro jazyk C
CXXFLAGS Extra volby pro jazyk C++
LDLIBS Přepínače knihoven (např. -lm pro knihovnu libm).
MAKE Program make. (Na některých systémech existuje make a gmake (GNU make), což jsou různé klony tohoto programu. $(MAKE) se odkazuje na tu verzi programu, kterou uživatel spustil.)
RM Program pro mazání souborů. Defaultně rm -f.

Příklad Makefile.6 můžu upravit tak, aby používal pro překlad implicitní hodnoty jednoduše tak, že smažu řádky, kde jsem změnil hodnotu CC a CXX.

#
# Makefile.7
#

CFLAGS = -g -Wall
CPPFLAGS = -g -Wall
TARGETS = hello hellocpp

all: $(TARGETS)

hello: hello.o
        $(CC) -o hello hello.o

hello.o: hello.c
        $(CC) $(CFLAGS) -c hello.c

hellocpp: hellocpp.o
        $(CXX) -o hellocpp hellocpp.o

hellocpp.o: hello.cpp
        $(CXX) $(CPPFLAGS) -c hello.cpp -o hellocpp.o

clean:
        $(RM) *.o $(TARGETS)
$ make -f Makefile.7 clean
rm -f *.o hello hellocpp
$ make -f Makefile.7 all
cc -g -Wall -c hello.c
cc -o hello hello.o
g++ -g -Wall -c hello.cpp -o hellocpp.o
g++ -o hellocpp hellocpp.o

Proměnné můžete nastavovat nejen v souboru Makefile, ale můžete je i předat přímo programu make jako argument. (To platí nejen pro implicitní proměnné, ale pro jakékoliv si vytvoříte).

$ make -f Makefile.7 clean
rm -f *.o hello hellocpp
$ make -f Makefile.7 all CC=clang CXX=clang++
clang -g -Wall -c hello.c
clang -o hello hello.o
clang++ -g -Wall -c hello.cpp -o hellocpp.o
clang++ -o hellocpp hellocpp.o

all je první cíl v Makefile, takže jeho uvedení nebylo nutné.

Automatické proměnné

V příkazech můžete použít několik speciálních proměnných.

$@ Obsahuje cíl, pro který se příkaz spouští.
$^ Obsahuje všechny závislosti cíle.
$< Obsahuje jméno první závislosti.

Makefile.7 lze takto zjednodušit:

#
# Makefile.8
#

CFLAGS = -g -Wall
CPPFLAGS = -g -Wall
TARGETS = hello hellocpp

all: $(TARGETS)

hello: hello.o
        $(CC) -o $@ $^

hello.o: hello.c
        $(CC) $(CFLAGS) -c $^

hellocpp: hellocpp.o
        $(CXX) -o $@ $^

hellocpp.o: hello.cpp
        $(CXX) $(CPPFLAGS) -c $^ -o $@
       
clean:
        $(RM) *.o $(TARGETS)

Implicitní pravidla

Program make má zabudovaná některá implicitní pravidla, jak vytvořit cíle na základě přípon souborů. Třeba k vytvoření objektového souboru .o ze souboru .c použije příkaz $(CC) $(CFLAGS) -c. Program make vyhledá implicitní pravidla i podle souborů, na kterých je cíl závislý.

Bohužel, implicitní pravidla předpokládají stejné názvy souborů cíle a závislosti (kromě přípony), takže se nevztahují na vytvoření hellocpp.o z hello.cpp. A u .o souborů se předpokládá, že jsou z jazyka C (a ne C++).

Příklad Makefile.8 se tak dá zjednodušit (pouze) takto:

#
# Makefile.9
#

CFLAGS = -g -Wall
CPPFLAGS = -g -Wall
TARGETS = hello hellocpp

all: $(TARGETS)

hello: hello.o

hello.o: hello.c

hellocpp: hellocpp.o
        $(CXX) -o $@ $^

hellocpp.o: hello.cpp
        $(CXX) $(CPPFLAGS) -c $^ -o $@

clean:
        $(RM) *.o $(TARGETS)
$ make -f Makefile.8  clean
rm -f *.o hello hellocpp
$ make -f Makefile.8
cc -g -Wall -g -Wall  -c -o hello.o hello.c
cc   hello.o   -o hello
g++ -g -Wall -c hello.cpp -o hellocpp.o
g++ -o hellocpp hellocpp.o

Vlastně se to dá zjednodušit ještě více. Můžete klidně smazat řádek hello.o: hello.c. Ale pozor, pokud vytvoříte nějaký hlavičkový soubor, musíte ho uvést mezi závislostmi (aby se při jeho změně překompiloval příslušný objektový soubor). Takže zase tak často si s implicitními pravidli moc nepomůžete.

Podadresáře

Rozsáhlejší projekty obvykle rozdělují zdrojové soubory do podadresářů. V každém takovém podadresáři můžete mít samostatný Makefile, který se spustí z nadřazeného Makefile, viz následující ukázka:

#
# Makefile.top
#
.PHONY: clean src/substrc.a

TARGETS= subhello

all: $(TARGETS)

subhello: subhello.o src/libsrc.a

subhello.o: subhello.c src/printhello.h

src/libsrc.a:
        cd src; $(MAKE)

clean:
        $(RM) *.o $(TARGETS)
        cd src; $(MAKE) clean

Příkazy cd src; $(MAKE) se musí spouštět v rámci jednoho shellu (jak už víte).

Cíl src/libsrc.a je označen jako „PHONY“. Na ničem nezávisí, takže pokud soubor src/libsrc.a existuje, make si může myslet, že je aktuální a žádné příkazy nespustí. Tím, že je tento cíl „PHONY“, se příkaz se spustí vždy – o další už se stará Makefile v adresáři src/.

#
# src/Makefile
#
.PHONY: clean

all: libsrc.a

libsrc.a: printhello.o
        $(AR) -r $@ $^

printhello.o: printhello.c printhello.h

clean:
        $(RM) *.a *.o

V src/ se příkazy pro libsrc.a spustí jen tehdy, pokud libsrc.a neexistuje, nebo je starší než printhello.o. Makefile se tedy postará o to, že se bude překládat, jen když to bude nutné.

Za domácí úkol si vytvořte nějaký další objektový soubor, který přidáte do libsrc.a. Všiměte si, že v Makefile.top nebudete muset nic měnit, změny budou probíhat čistě jen v adresáři src/. Není to super?

Příklad

Na závěr ukáži příklad Makefile z kaptioly o statických a sdílených knihovnách. Ve zdrojových souborech ke stažení máte ke každému projektu Makefile, takže máte spousty materiálu ke studiu :-)

include ../makefile.in

TARGETS = main smain libnumber.a libsnumber.so

all: $(TARGETS)

main: libnumber.a main.o
        $(CC) -o $@ main.o -lnumber -L.

libnumber.a: number1.o number2.o
        $(AR) -r $@ number1.o number2.o

main.o: number1.h number2.h
number1.o: number1.c number1.h
number2.o: number2.c number2.h


smain: libsnumber.so main.o
        $(CC) -o $@ main.o -lsnumber -L.

libsnumber.so: snumber1.o snumber2.o
        $(CC) -shared -o $@ $^ $(CFLAGS)

snumber1.o: number1.c number1.h
        $(CC) -c -o $@ number1.c -fpic $(CFLAGS)
snumber2.o: number2.c number2.h
        $(CC) -c -o $@ $< -fpic $(CFLAGS)

clean:
        $(RM) *.o $(TARGETS)

Někde používám automatické proměnné $@, $^ a $< (a někde ne, to jen tak pro příklad. Nehledejte v tom logiku).

V příkladu jsem použil direktivu include, která jakoby vloží do Makefile obsah souboru. Kdybyste místo direktivy include ../makefile.in vložili obsah souboru ../makefile.in, vyšlo by to na stejno.

Soubor ../makefile.in obsahuje toto:

.PHONY: all clean

CFLAGS = -g -Wall

Další použití

Makefile nemusíte používat jen při programování. Pokud máte jakékoliv cíle, které se dají vyřešit pomocí příkazů shellu, make je váš kamarád.

Tak například když budu chtít vytvořit distribuční balíček programu, než abych si pořád vzpomínal na to, jaké příkazy a v jakém pořadí je potřeba spustit, udělám si na to další cíl v Makefile.top:

#...
PACKAGE=subhello
VERSION=1.0
DISTDIR=$(PACKAGE)-$(VERSION)

dist: clean
        mkdir $(DISTDIR)
        cp -r src subhello.c Makefile.top $(DISTDIR)
        tar cz $(DISTDIR) > $(DISTDIR).tgz
        $(RM) -r $(DISTDIR)

Teď mi stačí příkaz make -f Makefile.top dist k vytvoření .tgz balíčku se všemi potřebnými soubory (a díky závislosti na clean bez generovaných souborů).

Závěr

O programu make by se toho dalo napsat ještě hodně, ale myslím, že s tím, co jsem tu popsal, si na nějakou dobu vystačíte. Pokud si budete chtít rozšířit obzory, začněte studiem Makefile Convetions.

Komentář Hlášení chyby
Vytvořeno: 22.8.2014
Naposledy upraveno: 4.9.2017
Tato stánka používá ke svému běhu cookies, díky kterým je možné monitorovat, co tu provádíte (ne že bych to bez cookies nezvládl). Také vás tu bude špehovat google analytics. Jestli si myslíte, že je to problém, vypněte si cookies ve vašem prohlížeči, nebo odejděte a už se nevracejte :-). Prohlížením tohoto webu souhlasíte s používáním cookies. Dozvědět se více..