Narzędzia niedoceniane: make
2020-04-11
Czym jest make
? Jest to narzędzie wykonujące reguły. Takie reguły mogą zależeć od plików lub on innych reguł, i make
analizuje je, sprawdzając, czy pliki, od których dana reguła zależy zostały zmienione (a, co za tym idzie, regułę trzeba uruchomić ponownie), albo te pliki nie istnieją, ale make
zna inną regułę, która potrafi je stworzyć.
Dla programistów dobre będzie określenie, że make
to dość prymitywny system budowania, który nie zakłada wykorzystywania żadnego języka programowania - chociaż kilka reguł jest domyślnie dostarczonych, to tak naprawdę siła make
polega na tym, że te reguły najczęściej musimy napisać sami.
Czy uważam, że make
to najlepszy wybór dla dużych, skomplikowanych projektów? Nie, w takich przypadkach należy rozejrzeć się za innymi build systemami, które zrobią za nas więcej. Jednak do prostych zastosowań, zwłaszcza, kiedy chcemy zrobić coś, do czego nikt nie stworzył wygodnego systemu budowania, make
jest niezastąpione.
Istnieje wiele implementacji make
, ale my będziemy korzystać z GNU Make. Jest to implementacja dostępna dla większości systemów operacyjnych i można ją znaleźć w większości dystrybucji Linuksa.
Niepraktyczny, wymyślony przykład
Załóżmy, że często łączymy dwa pliki, po czym zamieniamy w pliku wynikowym takiego złączenia wszystkie litery na małe. Używamy do tego następujących dwóch poleceń:
$ cat wejscie1.txt wejscie2.txt > wyjscie.mixed.txt
$ tr '[A-Z]' '[a-z]' < wyjscie.mixed.txt > wyjscie.upper.txt
Oczywiście, maszyny zostały stworzone po to, żebyśmy nie musieli ciągle powtarzać tego samego - one są w tym o wiele lepsze. Spróbujmy więc zautomatyzować naszą pracę.
Pozornie najłatwiejsza opcja: skrypt bashowy
Zakładając, że pliki wejściowe wejscie1.txt
i wejscie2.txt
znajdują się w aktualnym katalogu, to wszystko, czego potrzebujemy to następujący skrypt:
#!/usr/bin/env bash
cat wejscie1.txt wejscie2.txt > wyjscie.mixed.txt
tr '[A-Z]' '[a-z]' < wyjscie.mixed.txt > wyjscie.upper.txt
Jakie są minusy takiego rozwiązania? Przede wszystkim, jeśli mamy więcej niż jeden plik wejściowy, to będziemy musieli zrobić dużo kopiuj-wklej. Dodatkowo, zdecydowanym problemem w takim podejściu jest to, że skrypt bashowy nie sprawdza, które pliki się zmieniły, i które trzeba ponownie wygenerować.
Dla przykładu, jeśli wyjscie.upper.txt
zostało usunięte, to aby ponownie wygenerować ten plik skrypt bashowy będzie musiał wykonać też wcześniejszy krok - nawet, jeśli wyjscie.mixed.txt
jest aktualny i wystarczyłby tylko drugi krok. Spróbujmy więc napisać prosty Makefile
, który zajmie się tym za nas.
Makefile: opcja moim zdaniem przyjemniejsza
Polecenie make
domyślnie przyjmuje reguły zapisane w pliku o nazwie Makefile
(wielkość liter ma znaczenie). Pliki Makefile
mają dość prostą budowę:
CEL: ŹRÓDŁO1 ŹRODŁO2
polecenie lub polecenia generujące CEL na podstawie ŹRÓDEŁ
CEL
jest nazwą celu, który tworzy dana reguła. Zwykle jest to nazwa pliku, który zostanie stworzony, ale jest też możliwość tworzenia celów nie tworzących żadnych plików (a jedynie np. uruchamiających jakiś program albo czyszczących środowisko).ŹRODŁO1
iŹRÓDŁO2
to nazwy plików, od których zależy wygenerowanieCEL
u. Pliki te muszą albo istnieć, albo musi być zdefiniowana reguła, która je tworzy - w innym wypadku dostaniemy błąd:make: *** Brak reguł do zrobienia obiektu 'ŹRÓDŁO', wymaganego przez 'CEL'. Stop.
- W następnej linijce, po tabulatorze podane są polecenia generujące
CEL
, np. wywołanie kompilatora.- Ważne: Przed poleceniem musi znajdować się jeden tabulator. Jeśli zamiast tabulatora znajdą się tam spacje, reguła nie będzie działać.
- Można podać więcej niż jedną komendę, kolejne komendy są podawane w następnych linijkach. Ważne: każda taka linijka jest wykonywana w osobnym procesie. Dlatego np. zmienne ustawione w jednej linijce nie będą dostępne w następnej.
Stwórzmy więc na tej podstawie taki prosty plik odpowiadający skryptowi bashowemu napisanemu wcześniej:
wyjscie.upper.txt: wyjscie.mixed.txt
tr '[A-Z]' '[a-z]' < wyjscie.mixed.txt > wyjscie.upper.txt
wyjscie.mixed.txt: wejscie1.txt wejscie2.txt
cat wejscie1.txt wejscie2.txt > wyjscie.mixed.txt
W tym pliku widzimy zdefiniowane dwa cele: wyjscie.upper.txt
i wyjscie.mixed.txt
, z czego ten pierwszy zależy od drugiego, a drugi zależy od dwóch kolejnych plików. Jeśli teraz wywołamy make
w linii poleceń, to make
spróbuje stworzyć pierwszy cel zdefiniowany w pliku Makefile
za pomocą dostępnych reguł. Co prawda wyjscie.mixed.txt
nie istnieje, ale make
wie, jak je stworzyć, po czym tworzy wyjscie.upper.txt
. Jeśli skasujemy wyjscie.upper.txt
i wywołamy ponownie make
, to polecenia służące do generowania wyjscie.mixed.txt
nie zostaną wywołane - bo nie ma takiej potrzeby (o czym make
dowiaduje się porównując czas modyfikacji plików).
Co jednak dla mnie ważne, w pliku Makefile
– w odróżnieniu od skryptu bashowego – wyraźnie widzimy poszczególne kroki, które należy podjąć przy budowaniu naszego pliku.
Upraszczamy Makefile stosując wbudowane zmienne
W naszym przykładzie dość często się powtarzamy. Skorzystajmy ze zmiennych automatycznie dostarczanych przez make, a konkretnie z:
$@
- nazwaCEL
u$<
- nazwa pierwszegoŹRÓDŁA
$^
- nazwy wszystkichŹRÓDEŁ
, ze spacjami pomiędzy nimi
Posiadając tę wiedzę, możemy uprościć nasz Makefile
w następujący sposób:
wyjscie.upper.txt: wyjscie.mixed.txt
tr '[A-Z]' '[a-z]' < $< > $@
wyjscie.mixed.txt: wejscie1.txt wejscie2.txt
cat $^ > $@
Z pewnością nie są to najbardziej przyjazne nazwy zmiennych, ale niezaprzeczalnie są przydatne. Dzięki nim nie musimy się powtarzać, ale daje nam to jeszcze jedną ważną rzecz - reguły są w stanie działać nawet wtedy, kiedy nazwy CEL
ów albo ŹRóDEŁ
się zmieniają.
Dodajemy trochę generyczności do naszego Makefile
Tutaj wykorzystamy kolejną użyteczną funkcję make
- dopasowywanie wzorców (pattern matching). Dzięki niej możemy napisać regułę, której make
będzie w stanie automatycznie, gdy będzie potrzebował pliku, który (jeszcze) nie istnieje.
make
dopasowując wzorzec wykorzystuje znak %
otoczony stałym prefiksem i/lub sufiksem. Podczas analizy pliku Makefile
próbuje znaleźć najdokładniejszy wzorzec, czyli taki, gdzie za %
może być podstawiona najmniejsza liczba znaków.
Spróbujmy więc tego użyć:
%.upper.txt: %.mixed.txt
tr '[A-Z]' '[a-z]' < $< > $@
wyjscie.mixed.txt: wejscie1.txt wejscie2.txt
cat $^ > $@
Jednak po uruchomieniu make
widzimy, że stworzony został plik wyjscie.mixed.txt
, ale nie wyjscie.upper.txt
. Nic dziwnego, w końcu make
nie wie na podstawie takiego Makefile
jaki plik jest nam potrzebny, a pierwsza reguła, która to określa buduje właśnie wyjscie.mixed.txt
.
Dajmy więc znać w naszym Makefile
, że to właśnie tego pliku potrzebujemy.
Cele bez plików wynikowych
Aby móc skorzystać z generycznej reguły napisanej przed chwilą, musimy w jakiejś innej regule wymagać pliku, który ona mogłaby stworzyć. W tym celu możemy stworzyć regułę z celem, który nie jest plikiem:
.PHONY: all
all: wyjscie.upper.txt
%.upper.txt: %.mixed.txt
tr '[A-Z]' '[a-z]' < $< > $@
wyjscie.mixed.txt: wejscie1.txt wejscie2.txt
cat $^ > $@
W tym wypadku all
nie jest nazwą pliku. Aby jednak make
dobrze poradził sobie w takiej sytuacji, należy dodać taki cel do zmiennej .PHONY
- dzięki temu make
wie, że nie należy spodziewać pliku wynikowego, ale jeśli taki plik kiedykolwiek się pojawi - to go zignoruje.
Dzięki temu, jeśli będziemy chcieli stworzyć więcej niż jeden cel w taki sam sposób, możemy dodawać do zależności all
kolejne pliki pasujące do wzorca %.upper.txt
, a make
będzie w stanie automatycznie domyśleć się, w jaki sposób taki plik zbudować, pod warunkiem, że wszystkie zależności są spełnione.
Drabinka zależności
make
bez problemu potrafi samodzielnie przeanalizować dostępne reguły i wybrać te, które są potrzebne do wygenerowania potrzebnego pliku. Dodajmy nową regułę do naszego Makefile
:
%.hello.txt: %.upper.txt
echo "Hello, " > $@
cat $< > $@
Jeśli teraz zamienimy cel all
tak, aby wymagał %.hello.txt
zamiast %.upper.txt
, to make
będzie znał wszystkie kroki, od wejscie1.txt
i wejscie2.txt
(które istnieją), aż do wyjscie.hello.txt
, którego potrzebujemy.
Sprzątanie po sobie
Przydałaby się jeszcze reguła, która sprząta wszystkie automatycznie wygenerowane pliki. Niestety, make
nie jest w stanie wygenerować takiej reguły dla nas - musimy napisać ją samodzielnie.
.PHONY: all clean
all: wyjscie.hello.txt
%.hello.txt: %.upper.txt
echo "Hello, " > $@
cat $< > $@
%.upper.txt: %.mixed.txt
tr '[A-Z]' '[a-z]' < $< > $@
wyjscie.mixed.txt: wejscie1.txt wejscie2.txt
cat $^ > $@
clean:
rm wyjscie.mixed.txt
rm wyjscie.upper.txt
rm wyjscie.hello.txt
Jak widać, clean
jest regułą, która nie tworzy żadnych plików, jak również żadnych nie wymaga. Aby ją uruchomić, należy podać make
w linii poleceń nazwę celu, który chcemy zbudować - inaczej taki cel nigdy nie zostanie wywołany:
$ make clean
Więcej informacji
Na początek to jest wystarczające, żeby napisać proste pliki Makefile
, które dość często przydają się w codziennym życiu. Jeśli potrzebujesz więcej informacji, polecam:
- Manual GNU Make zawiera bardzo dużo informacji, a jednocześnie jest zaskakująco przystępny,
- Your Makefiles are Wrong Jacoba Davis-Hanssona zawiera dość dużo ciekawych hacków.
- Samodzielnie nie ze wszystkich korzystam - zmiana tabulacji na coś innego psuje kolorowanie składni, ale chociażby to, co zostało tam nazwane jako sentinel files - tego używam regularnie.