💾 Archived View for gemini.ctrl-c.club › ~fleg › gemlog › 2020-04-11-makefiles.gmi captured on 2023-01-29 at 04:09:13. Gemini links have been rewritten to link to archived content

View Raw

More Information

⬅️ Previous capture (2021-12-03)

-=-=-=-=-=-=-

Narzędzia niedoceniane: make - fleg gemlog

- Back to main page

Gemlog entries

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[1], 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Ł

`make: *** Brak reguł do zrobienia obiektu 'ŹRÓDŁO', wymaganego przez 'CEL'. Stop.`

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[2], a konkretnie z:

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]' < {body}amp;lt; > $@

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*)[3]. 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]' < {body}amp;lt; > $@

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]' < {body}amp;lt; > $@

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 {body}amp;lt; > $@

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 {body}amp;lt; > $@

%.upper.txt: %.mixed.txt
	tr '[A-Z]' '[a-z]' < {body}amp;lt; > $@

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:

Links

=> https://www.gnu.org/software/make/manual/html_node/Catalogue-of-Rules.html 1

=> https://www.gnu.org/software/make/manual/html_node/Automatic-Variables.html 2

=> https://www.gnu.org/software/make/manual/html_node/Pattern-Match.html 3

=> https://www.gnu.org/software/make/manual/html_node/ 4

=> https://tech.davis-hansson.com/p/make/ 5

- Back to main page