Środowisko programisty/Automatyzacja kompilacji - make: Różnice pomiędzy wersjami
Zmienne -> Zmienne i funkcje |
→Funkcje: Pierwsza wersja |
||
Linia 188: | Linia 188: | ||
=== Funkcje === | === Funkcje === | ||
<code>make</code> udostępnia szereg wbudowanych funkcji, aby ułatwić nieco życie. Szczegółowe zestaw dostępnych funkcji jest opisany w dokumentacji, my dla przykładu pokażemy zastosowanie funkcji <code>patsubst</code>. | |||
Wprowadziliśmy zmienną <code>objects</code>, która zawiera listę wszystkich plików wynikowych <code>*.o</code>, które wchodzą w skład programu. Jednakże jest tak, że każdemu plikowi źródłowemu o rozszerzeniu <code>.c</code> odpowiada dany plik wynikowy. Powinniśmy raczej zadeklarować zmienną | |||
sources=komunikat.c main.c test.c | |||
pamiętającą wszystkie pliki źródłowe, a na ich podstawie powinniśmy utworzyć zmienną <code>objects</code>. Możemy zastosować funkcję <code>patsubst</code>: | |||
objects=$(patsubst %.c,%.o,$(sources)) | |||
Składnia tej funkcji jest następująca: | |||
$(patsubst wzorzec_wejściowy,wzorzec_wynikowy,lista_wyrazów) | |||
We wzorcach znak <code>%</code> spełni podobną rolę jak <code>*</code> we wzorcach nazw plików. <code>patsubst</code> działa mniej więcej tak, że każdy wyraz z listy kojrzy ze wzorcem wejściowym i zamienia go tak, aby pasował do wzorca wyjściowego, pozostawiając niezmienione części, które zostały przypasowane do znaku <code>%</code>. | |||
W tym przypadku, gdy zamieniamy tylko sufiksy, a listą jest wartość zmiennej <code>patsubst</code> ma skróconą wersję: | |||
$(sources:.c=.o) | |||
Użyjemy właśnie jej. | |||
<pre> | |||
sources=komunikat.c main.c test.c | |||
objects=$(sources:.c=.o) | |||
program: $(objects) | |||
gcc -lm $(objects) -o program | |||
komunikat.o: komunikat.c komunikat.h | |||
gcc -Wall -c komunikat.c -o komunikat.o | |||
main.o: main.c test.h | |||
gcc -Wall -c main.c -o main.o | |||
test.o: test.c test.h komunikat.h | |||
gcc -Wall -c test.c -o test.o | |||
.PHONY: clean | |||
clean: | |||
rm -f program $(objects) | |||
</pre> | |||
Jeżeli byśmy wiedzieli, że w skład programu wchodzą wszystkie pliki <code>*.c</code> znajdujące się w aktualnym katalogu, to moglibyśmy zmienić deklarację zmiennej <code>sources</code> z użyciem funkcji <code>wildcard</code> | |||
sources=$(wildcard *.c) | |||
=== Zmienne automatyczne === | === Zmienne automatyczne === | ||
== Reguły schematy == | == Reguły schematy == |
Wersja z 16:35, 27 wrz 2006
Wstęp
Przypuśćmy, że piszemy program w C, który składa się z kilku plików, a mianowicie main.c
, komunikat.c
, komunikat.h
, test.c
, test.h
. Dla przykładu niech to będą bardzo proste źródła .
Kompilujemy je stopniowo. Wpierw tworzymy pliki *.o
z plików *.c
, a następnie je linkujemy. Czyli kolejne polecenia kompilacji wyglądają następująco:
gcc -Wall -c komunikat.c -o komunikat.o gcc -Wall -c main.c -o main.o gcc -Wall -c test.c -o test.o gcc -lm komunikat.o main.o test.o -o program
Dla ułatwienia kompilacji możemy sobie te polecenia zapisać do skryptu i uruchamiać ten skrypt za każdym razem, gdy zmodyfikujemy jakieś źródła. Skrypt ma jednak parę wad. Po pierwsze jeśli dodamy lub usuniemy pliki źródłowe C, to będziemy musieli przeedytować skrypt. Możemy z tym problemem sobie poradzić, jeśli w danym katalogu znajdują się tylko pliki źródłowe należące do danego programu. Wtedy wystarczy przerobić skrypt.
for f in *.c; do gcc -Wall -c $f -o ${f%c}o done gcc -lm *.o -o program
Druga wada skryptu jest taka, że kompilujemy za każdym razem wszystkie źródła, a przecież wystarczy skompilować tylko te co się zmieniły i powtórzyć linkowanie plików *.o
w jeden program. Taki skrypt jesteśmy w stanie napisać, ale zrobiłoby się to bardzo skomplikowane.
Po za tym co jak będziemy chcieli zmienić opcje kompilacji, linkowania, itp. Będziemy musieli rozbudować nasz skrypt znacznie. Tymczasem gotową automatyzację udostępnia program make
i skupimy się dalej na wykorzystaniu tego narzędzia do ułatwienia życia przy rekompilacji programu.
Pierwszy makefile
Program make
czyta co ma zrobić z pliku o nazwie makefile
, zatem cała sztuka użycia make
sprowadza się do umiejętności pisania pliku makefile
.
Najważnieszą składowa pliku makefile
są reguły. Reguła wygląda następująco:
cel: zależności polecenie
Reguła określa w jakis sposób należy budować cel. Znaczenie poszczególnych składników.
- cel
- nazwa pliku, który ma powstać z tej reguly,
- zależności
- lista plików od których zależy cel, tzn. zmiana któregokolwiek z tych plików oznacza, że cel też się zmieni,
- polecenie
- jest to polecenie za pomocą, którego ma zostać wytworzony cel.
Uwaga! Istotne jest, aby polecenie było wcięte za pomocą jednego znaku tabulacji, a nie przypadkiem spacji. Jest to dosyć kłopotliwe, ale cóż, każde narzędzie miewa swoje widzimisie.
Dla przykładu reguła
komunikat.o: komunikat.c komunikat.h gcc -Wall -c komunikat.c -o komunikat.o
mówi w jaki sposób tworzyć plik komunikat.o
. Zależy on od dwóch plików źródłowych komunikat.c
i komunikat.h
.
W jaki sposób znajdować pliki zależna? Można posłużyć się opcją -MM
polecenia gcc
.
gcc -MM plik.c
Powyższe polecenie wyrzucie linię
plik.o: plik.c inne pliki źródłowe
pokazującą jakie są jeszcze inne pliki źródłowe, od których zależy dany plik. Forma wyjścia nie jest przypadkowa. Jest taka, aby było można ją łatwo wstawić do pliku makefile
.
Z pomocą gcc -MM
lub bez tworzymy nasz pierwszy makefile
, który wygląda tak:
program: komunikat.o main.o test.o gcc -lm komunikat.o main.o test.o -o program komunikat.o: komunikat.c komunikat.h gcc -Wall -c komunikat.c -o komunikat.o main.o: main.c test.h gcc -Wall -c main.c -o main.o test.o: test.c test.h komunikat.h gcc -Wall -c test.c -o test.o
Teraz w wyniku wykonania polecenia make
mamy taki efekt:
$ make gcc -Wall -c komunikat.c -o komunikat.o gcc -Wall -c main.c -o main.o gcc -Wall -c test.c -o test.o gcc -lm komunikat.o main.o test.o -o program
Zostały utworzone odpowiednie pliki *.o
oraz program wykonywalny program
.
W jaki sposób zadziałał make
? Otóż jeśli nie podamy mu żadnego argumentu, to próbuje on utworzyć cel występujący w pierwszej regule pliku makefile
. W tym celu tworzy wpierw wszystkie pliki od których on zależy, jeśli takowe jeszcze nie istnieją. Do tworzenia plików *.o
używa dalszych reguł. Tworzenie celu kończy się porażką jeżeli zajdzie jeden z przypadków:
- plik, od którego zależy cel, nie zostanie znaleziony,
- nie będzie można znaleźć reguły, do utworzenia danego pliku,
- polecenie podane w regule skończy się porażką.
Ponadto make
potrafi stwierdzać, czy jest potrzeba ponownego wykonania poleceń kompilacji. Wykonajmy go jeszcze raz:
$ make -f simple.mak make: `program' jest aktualne.
co oznacza, że plik wykonywalny program
jest aktualny i nie trzeba nic uruchamiać.
Teraz zmodyfikujmy jakiś plik. Zasymulujemy to poleceniem touch
:
$ touch komunikat.h
Uruchamiamy make
jeszcze raz:
$ make -f simple.mak gcc -Wall -c komunikat.c -o komunikat.o gcc -Wall -c test.c -o test.o gcc -lm komunikat.o main.o test.o -o program
Tym razem ponownie zostały utworzone te pliki wynikowe, które zależały od komunikat.h
, a mianowicie są to komunikat.o
i test.o
. Oczywiście zostało też ponowione linkowanie.
Reguły jak polecenia
Dodamy regułę, która będzie czyścić niepotrzebne pliki:
program: komunikat.o main.o test.o gcc -lm komunikat.o main.o test.o -o program komunikat.o: komunikat.c komunikat.h gcc -Wall -c komunikat.c -o komunikat.o main.o: main.c test.h gcc -Wall -c main.c -o main.o test.o: test.c test.h komunikat.h gcc -Wall -c test.c -o test.o clean: rm -f program komunikat.o main.o test.o
Nowa reguła nie ma zależności, a jest jedynie cel, który w wyniku polecenia w tej regule i tak nie jest tworzony. Nie mniej takie reguły są przydatne. Są to takie reguły-polecenia. Żeby je wykonać należy wywołać polecenie make
z nazwą celu jako argument:
$ make clean rm -f program komunikat.o main.o test.o
Problem jest jednak taki, że jeśli będzie np. istniaj plik o nazwie clean
to taka reguła nie zostanie wykonana, gdyż cel będzie już istniał, a wszystkie pliki, od których on zależy (wszystkie, czyli żadne) są aktualne. Żeby wskazać, że dana reguła jest tak na prawdę tylko wywołaniem polecenia, należy dodać regułę z celem o specjalnej nazwie .PHONY
.
program: komunikat.o main.o test.o gcc -lm komunikat.o main.o test.o -o program komunikat.o: komunikat.c komunikat.h gcc -Wall -c komunikat.c -o komunikat.o main.o: main.c test.h gcc -Wall -c main.c -o main.o test.o: test.c test.h komunikat.h gcc -Wall -c test.c -o test.o .PHONY: clean clean: rm -f program komunikat.o main.o test.o
Zmienne i funkcje
Zmienne
Nasz makefile
jest na razie dosyć brzydki. Co jeśli będziemy chcieli dodać nowy plik wynikowy o rozszerzeniu .o
? Będziemy musieli go dodać w kilku miejscach, co jest jednak dosyć żmudne.
Z pomocą przychodzą zmienne. Jeśli zadeklarujemy zmienną objects
objects=komunikat.o main.o test.o
to będziemy mogli tej zmiennej użyć w dalszej części przez wywołanie $(objects)
. W ten sposób znacznie uprościmy makefile
:
objects=komunikat.o main.o test.o program: $(objects) gcc -lm $(objects) -o program komunikat.o: komunikat.c komunikat.h gcc -Wall -c komunikat.c -o komunikat.o main.o: main.c test.h gcc -Wall -c main.c -o main.o test.o: test.c test.h komunikat.h gcc -Wall -c test.c -o test.o .PHONY: clean clean: rm -f program $(objects)
Funkcje
make
udostępnia szereg wbudowanych funkcji, aby ułatwić nieco życie. Szczegółowe zestaw dostępnych funkcji jest opisany w dokumentacji, my dla przykładu pokażemy zastosowanie funkcji patsubst
.
Wprowadziliśmy zmienną objects
, która zawiera listę wszystkich plików wynikowych *.o
, które wchodzą w skład programu. Jednakże jest tak, że każdemu plikowi źródłowemu o rozszerzeniu .c
odpowiada dany plik wynikowy. Powinniśmy raczej zadeklarować zmienną
sources=komunikat.c main.c test.c
pamiętającą wszystkie pliki źródłowe, a na ich podstawie powinniśmy utworzyć zmienną objects
. Możemy zastosować funkcję patsubst
:
objects=$(patsubst %.c,%.o,$(sources))
Składnia tej funkcji jest następująca:
$(patsubst wzorzec_wejściowy,wzorzec_wynikowy,lista_wyrazów)
We wzorcach znak %
spełni podobną rolę jak *
we wzorcach nazw plików. patsubst
działa mniej więcej tak, że każdy wyraz z listy kojrzy ze wzorcem wejściowym i zamienia go tak, aby pasował do wzorca wyjściowego, pozostawiając niezmienione części, które zostały przypasowane do znaku %
.
W tym przypadku, gdy zamieniamy tylko sufiksy, a listą jest wartość zmiennej patsubst
ma skróconą wersję:
$(sources:.c=.o)
Użyjemy właśnie jej.
sources=komunikat.c main.c test.c objects=$(sources:.c=.o) program: $(objects) gcc -lm $(objects) -o program komunikat.o: komunikat.c komunikat.h gcc -Wall -c komunikat.c -o komunikat.o main.o: main.c test.h gcc -Wall -c main.c -o main.o test.o: test.c test.h komunikat.h gcc -Wall -c test.c -o test.o .PHONY: clean clean: rm -f program $(objects)
Jeżeli byśmy wiedzieli, że w skład programu wchodzą wszystkie pliki *.c
znajdujące się w aktualnym katalogu, to moglibyśmy zmienić deklarację zmiennej sources
z użyciem funkcji wildcard
sources=$(wildcard *.c)