9.
Make
Vývoj vanilkové příchuti Linuxu: Knihy kouzel byl 1. března 2025
ukončen. Tento text je zachován jako historický, ale chyby již nejsou
opravovány. Odnože projektu pod kompatibilní licencí jsou vítány.
1. Úvod
GNU make je nástroj k automatizaci procesu sestavování určitých cílových souborů ze souborů zdrojových. Vykonává podobnou úlohu jako např. skript napsaný v bashi, ale na rozdíl od něj umí:
- Kompilovat pouze ty části programu, které jsou potřeba (jejichž cílové soubory neexistují nebo se jejich zdrojové soubory změnily).
- Spouštět procesy paralelně, aniž by to narušilo definované závislosti.
Vztahy mezi zdrojovými a cílovými soubory a kompilační příkazy jsou popsány v tzv. Makefilu (souboru jménem „Makefile“), jemuž se bude věnovat většina této kapitoly.
2. Definice
- Pravidlo je definice v Makefilu, která instruuje program make, na kterých dalších souborech určitý soubor závisí a jakými příkazy jej z nich vytvořit či aktualizovat.
- Cíl je název souboru k vytvoření či akce k vykonání.
- Zdroj (často nazýváný „závislost“) je název souboru, na kterém určitý cíl závisí. Příkazy stanovené pravidlem se vykonají jen tehdy, pokud cíl neexistuje nebo je alespoň jeden jeho z jeho zdrojů novější. (Vychází se z času poslední změny souborů.) Zdrojem může být také název akce.
- Slovo je posloupnost nebílých znaků v řetězci. Jednotlivá slova v řetězci jsou od sebe oddělena bílými znaky, nejčastěji jednotlivou mezerou.
- %-vzor je řetězec sloužící k filtrování, vyhledávání a nahrazování slov. Jde o velice praktickou věc. Může obsahovat nejvýše jeden znak %, který slouží jako náhrada za libovolné množství znaků (včetně lomítek oddělujících adresáře). (Např. %-vzoru a% odpovídají právě ta slova, která začínají malým písmenem a.) Slouží-li %-vzor k záměně, slova, která mu neodpovídají, projdou záměnou nezměněna. %-vzor nemusí obsahovat znak %; v takovém případě mu odpovídají pouze slova, která se s ním přesně shodují.
3. Zaklínadla: V souboru Makefile
3/1 Nastavení proměnných
@nastavit proměnnou (expandovat v místě definice)#1
NÁZEV_PROMĚNNÉ := hodnota včetně mezer
@připojit obsah na konec proměnné (expanze stejně jako v původní definici)#2
NÁZEV_PROMĚNNÉ += hodnota včetně mezer
@nastavit proměnnou (expandovat v každém místě použití)#3
NÁZEV_PROMĚNNÉ = hodnota včetně mezer
@nastavit proměnnou, pokud ještě nebyla nastavena (expandovat v místě definice)#4
ifeq ($(origin NÁZEV_PROMĚNNÉ, undefined)
NÁZEV_PROMĚNNÉ := hodnota včetně mezer
endif
@nastavit proměnnou, pokud ještě nebyla nastavena (expandovat v každém místě použití)#5
NÁZEV_PROMĚNNÉ ?= hodnota včetně mezer
@přiřadit do proměnné mezeru (trik)#6
PRAZDNA :=
NÁZEV_PROMĚNNÉ := $(PRAZDNA) $(PRAZDNA)
@nastavit proměnnou prostředí pro všechny podprocesy#8
export NÁZEV_PROMĚNNÉ := hodnota včetně mezer
3/2 Rozvoj proměnných
@rozvinout proměnnou#1
$(NÁZEV_PROMĚNNÉ)
$(NÁZEV_PROMĚNNÉ:[původní-prefix]%=[nový-prefix]%)
$(NÁZEV_PROMĚNNÉ:%[původní-suffix]=%[nový-suffix])
$(NÁZEV_PROMĚNNÉ:[původní-prefix]%[původní-suffix]=[nový-prefix]%[nový-suffix])
@rozvinout proměnnou prostředí či příkazového interpretu (ne proměnnou Makefilu)(alternativy)#3
$$NÁZEV_PROMĚNNÉ
$${NÁZEV_PROMĚNNÉ}
@rozvinout proměnnou, jejíž název je uložený v jiné proměnné#4
$($(NÁZEV_PROMĚNNÉ))
@příklad záměny při rozvoji – vypíše: bbeceda.cpp hlavicka.h bbakus.cpp ostatni.cc#5
TEST := abeceda.cc hlavicka.h abakus.cc ostatni.cc
all:
@echo $(TEST:a%.cc=b%.cpp)
3/3 Automatické a předdefinované proměnné
@cíl pravidla (alternativy)#1
$@
$(@)
@první zdroj pravidla (alternativy)#2
$<
$(<)
@všechny zdroje (alternativy)#3
$^
$(^)
@mazání souborů (typicky „rm -f“)#4
$(RM)
@program make#5
$(MAKE)
3/4 Obecný tvar pravidel
@normální (pevné) pravidlo#1
cíle oddělené mezerami: [zdroje oddělené mezerami] [;příkaz]
[ [prefix-příkazu]příkaz]
@zobecněné (generované) pravidlo (zdroje lze odvodit od cíle)#2
cíle oddělené mezerami: %-vzor-pro-cíle: cesta-nebo-%-vzor-zdroje [;příkaz]
[ [prefix-příkazu]příkaz]
%-vzor cíle: cesta-nebo-%-vzor-zdroje [;příkaz]
[ [prefix-příkazu]příkaz]
@označit, že určité cíle jsou akce, ne soubory#4
.PHONY: [akce oddělené mezerami]
@přeložit soubory uvedené v proměnné ZDROJE, které se nacházejí v adresáři „kod“ a jeho podadresářích a mají příponu „.cc“, na objektové soubory do adresáře obj#5 (4)
$(patsubst kod/%.cc,obj/%.o,$(filter kod/%.cc,$(ZDROJE))): obj/%.o: kod/%.cc
$(CXX) $(CXXFLAGS) -c -o $@ $<
cíle oddělené mezerami: $(wildcard nepovinný-zdroj)
[ [prefix-příkazu]příkaz]
@v případě chyby odstranit cílový soubor#7
.DELETE_ON_ERROR: cíl [další-cíl]
@v případě chyby pokračovat dál, jako by nenastala#8
.IGNORE: cíl [další-cíl]
3/5 Textové funkce
$(addprefix text-před,řetězec slov)
$(addsuffix text-za,řetězec slov)
$(proměnná:%=text-před%text-za)
@provést náhradu (záměnu) ve slovech pomocí %-vzoru#2
$(patsubst co-nahradit-%-vzor,čím-nahradit-%-vzor,řetězec slov)
@vybrat slova odpovídající/neodpovídající kterémukoliv ze zadaných %-vzorů#3
$(filter %-vzory oddělené mezerou,řetězec slov)
$(filter-out %-vzory oddělené mezerou,řetězec slov)
@nahradit všechny výskyty podřetězce#4
$(subst co nahradit,čím nahradit,původní text)
@normalizovat bílé znaky (posloupnosti nahradit jednou mezerou, na začátku a konci odstranit)#5
$(strip řetězec)
@získat počet slov v řetězci#6
$(words řetězec)
@první/poslední/n-té slovo z řetězce#7
$(firstword řetězec)
$(lastword řetězec)
$(word n,řetězec)
@předposlední/před-předposlední slovo z řetězce#8
$(if $(word 2,řetězec),$(word $(shell expr $(words řetězec ) - 1),řetězec),)
$(if $(word 3,řetězec),$(word $(shell expr $(words řetězec ) - 2),řetězec),)
@seřadit slova a odstranit duplicity#9
$(sort řetězec)
@obrátit pořadí slov v řetězci#10
$(shell printf %s\\n '$(strip řetězec slov)' | tr ' ' \\n | tac)
3/6 Analýza adresářových cest (pro každé slovo zvlášť)
@získat samotný název souboru včetně přípony/bez přípony (např. „b.o“, resp. „b“)#2
$(notdir řetězec slov)
$(basename $(notdir řetězec slov))
@získat adresářovou cestu (je-li uvedena) + název souboru bez přípony (např. „../a/b“)#4
$(basename řetězec slov)
@získat úplnou kanonickou cestu existujících souborů a adresářů (např. „/home/elli/test/a/b.o“)#5
$(realpath řetězec slov)
$(wildcard vzorek)
3/7 Příkazy v pravidlech
@vykonat příkaz bez vypsání#1
@příkaz
@vykonat příkaz a ignorovat návratovou hodnotu#2
-příkaz
@u určitých cílů nevypisovat příkazy před vykonáním#3
.SILENT: cíl [další-cíl]
3/8 Logické funkce
@podmíněný výraz#1
$(if podmínkový řetězec,je-li neprázdný[,jinak])
@získat první neprázdný řetězec#2
$(or řetězec[,další řetězec])
@získat první prázdný řetězec, nebo jsou-li všechny neprázdné, poslední z nich#3
$(and řetězec[,další řetězec])
@pro každé slovo ze seznamu toto slovo nastavit do proměnné a rozvinout podvýraz#4
$(foreach proměnná,seznam,podvýraz)
@vrátí podřetězec, pokud se vyskytuje v řetězci; jinak vrátí prázdný řetězec#5
$(findstring podřetězec,řetězec)
@příklad – vrátí: aa bb cc cc bb aa#6
$(foreach PROM,a b c c b a,$(PROM)$(PROM))
3/9 Podmíněný překlad a include
ifeq "výraz 1" "výraz 2"
první alternativa
[else ifeq "výraz 1" "výraz 2"
další alternativa]
[else
poslední alternativa]
endif
@načíst kód Makefilu z jiných souborů, jako by byl zde#2
include soubory
3/10 Ostatní funkce
@vykonat příkaz a rozvinout se na jeho výstup; konce řádků se nahradí mezerami#1
$(shell příkaz shellu)
4. Parametry příkazů
@make#1
make [parametry] [cíl]
$(MAKE) [parametry] [cíl]
- -j počet :: umožní paralelní běh více úloh najednou
- -C adresář :: před děláním čehokoliv (dokonce i hledání Makefilu) vstoupí do zadaného adresáře
- -n :: nespouští příkazy, pouze je vypíše
- -s :: nevypisuje příkazy, pouze je spouští
- Není-li cíl zadán, použije se první cíl v Makefile (tradičně akce „all“).
5. Instalace na Ubuntu
sudo apt-get install make
6. Ukázka
@/home/elli/test/Makefile:#1
# Komentář
SHELL := /bin/sh
CXX := g++
CXXFLAGS := -Wall \
-pedantic -DHOME=\"$$HOME\"
OBJS := main.o second.o
OBJS += hello.o
SOURCES := $(OBJS:%.o=%.cc)
.PHONY: all clean
all: main
clean:
$(MAKE) -C lib clean
$(RM) $(OBJS)
main: $(OBJS)
@echo "Koncové sestavení:"
$(CXX) $(CXXFLAGS) -o $@ $^
main.o second.o: %.o: %.cc lib/hello.h
$(CXX) $(CXXFLAGS) -c -o $@ $<
hello.o:
$(MAKE) -C lib hello.o
cp lib/hello.o $@
@/home/elli/test/lib/Makefile:#2
.PHONY: clean
hello.o: hello.cc hello.h
g++ -o $@ -c -Wall -pedantic hello.cc
clean:
$(RM) hello.o
7. Tipy a zkušenosti
- Dlouhé řádky Makefilu můžete rozdělit, pokud před každý konec řádku, který má make ignorovat, vložíte zpětné lomítko. Rozdělíte-li řádek s příkazem, make toto rozdělení předá volanému interpretu příkazové řádky, což však u běžně používaných „sh“ a „bash“ nezpůsobí problémy.
- Nebojte se definovat více cílů v jednom pravidle. Funguje to stejně jako definovat stejné pravidlo pro každý uvedený cíl zvlášť a ušetří vám to spoustu práce s údržbou. Ze stejného důvodu se vyplatí naučit se syntaxi pravidla s %-vzorem.
- Pro jeden cíl můžete definovat více pravidel, pokud nejvýše jedno z nich bude deklarovat příkazy; v takovém případě se automaticky sloučí zdroje ze všech odpovídajících pravidel.
- Program make v příkazech pravidel interpretuje znak $. Má-li se předat shellu, je třeba jej zdvojit, např. $$PATH nebo $$$$. To platí i při uzavření do apostrofů.
- Některé textové editory mohou v závislosti na svém nastavení nahrazovat tabulátory mezerami či naopak. Pokud takovým editorem upravíte Makefile, přestane fungovat, protože na začátku každého příkazu v pravidle musí být tabulátor, ne posloupnost mezer.
- Obvyklé názvy akcí jsou např.: all, clean, install.
- Každý příkaz pravidla se při kompilaci spouští ve vlastní instanci interpretu příkazové řádky!
- Použitý interpret v příkazech a volání funkce $(shell) určuje proměnná SHELL. Kvůli přenositelnosti se doporučuje ji na začátku Makefile výslovně nastavit: SHELL := /bin/sh nebo SHELL := /bin/bash.
- Akce může mít jako zdroje soubory a další akce; ty budou přeloženy před vykonáním vlastní akce.
8. Další zdroje informací
- make --help
- Makefile na sallyx.org
- Stránka na Wikipedii
- Rychlo-školička pro Makefile
- Správa projektů pomocí programu Make
- Oficiální manuál GNU make (anglicky)
- Manuálová stránka (anglicky)
- Balíček Ubuntu (anglicky)
- Oficiální stránka GNU make (anglicky)
- TL;DR stránka „make“ (anglicky)
9. Zákulisí kapitoly
V této verzi kapitoly chybí:
- nic
Tato kapitola záměrně nepokrývá:
- nic
1 „endef“ musí být na samostatném řádku; operátor může být „=“, „:=“ nebo „+=“. Jeden znak konce řádky bezprostředně před začátkem víceřádkového obsahu a jeden znak konce řádku za jeho koncem jsou vypuštěny, ostatní znaky konce řádky jsou ponechány.
2 Třetí uvedená varianta provede náhradu jen ve slovech, kde odpovídá prefix i suffix. Slova, u kterých odpovídá jen prefix nebo jen suffix, projdou bez náhrady.
3 Obecné pravidlo má nižší prioritu než všechna pevná a generovaná pravidla. Navíc je tiše ignorováno, pokud chybí nekterý ze zdrojů. Má-li uvedeno víc cílů, považují se po jeho provedení všechny uvedené cíle za vygenerované, a tedy se pravidlo nevolá pro překlad dalších zdrojů znovu.
4 Uvedený příklad předpokládá předdefinované proměnné CXX a CXXFLAGS, které GNU make předdefinovává, takže je nemusíte sami nastavovat.
5 Poznámka: nepovinné zdroje se zadávají jako vzorky interpretu oddělené mezerami, což znamená, že „?“, „*“ a „ “ jsou v nich znaky se speciální významem.
6 Při použití poslední uvedené varianty nesmějí text-před a text-za obsahovat znak %.
7 Neobsahuje-li slovo žádné „/“, vrací pro něj $(dir) „./“.
8 Pozor! Slova, která takovou příponu neobsahují, budou touto funkcí vynechána bez náhrady, což sníží počet slov ve výsledném řetězci.
9 Vzorek příkazového interpretu může obsahovat znaky ? a * s významem obvyklým v bashi. Pokud vzorku neodpovídá žádný soubor ani adresář, vzorek se potichu přeskočí. Toho je možno použít k vynechání neexistujících souborů z proměnné.
10 Vybere první z alternativ, kde jsou si uvedené výrazy po rozvinutí rovny. Doporučuji alternativy v Makefilu odsadit, ale není to vyžadováno.
11 Tip: Popis chyby v parametru funkce $(error) může obsahovat rozvoj proměnných a volání funkcí.
12 Výsledkem expanze funkcí $(warning) a $(info) je prázdný řetězec.