Środowisko programisty/Automatyzacja kompilacji - make: Różnice pomiędzy wersjami
→Reguły schematy: Pierwsza wersja |
|||
Linia 256: | Linia 256: | ||
== Reguły schematy == | == Reguły schematy == | ||
Zauważmy, że po ostatnich zmianach polecenia do kompilacja programu <code>.c</code> w plik wynikowy <code>.o</code> wyglądają identycznie. Zatem może da się je jakoś raz zadeklarować. Otóż tak, z pomocą przychodzą reguły schematy. | |||
=== Deklarowanie reguły schematu === | === Deklarowanie reguły schematu === | ||
=== Użycie wbudowanych reguł schematów === | === Użycie wbudowanych reguł schematów === |
Wersja z 16:59, 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)
Zmienne automatyczne
Innym udogodnienie są zmienne automatyczne o specjalnych nazwach, które są dostępne w regułach, a dokłdnie w treści polecenia dotyczącego danej reguły. Oto przykładowe trzy:
- $@
- daje nazwę celu
- $<
- daje nazwę pierwszej zależność
- $^
- daje nazwy wszystkich zależności
Z użyciem tych zmiennych można trochę skróć nasz makefile
:
sources=komunikat.c main.c test.c objects=$(sources:.c=.o) program: $(objects) gcc -lm $^ -o $@ komunikat.o: komunikat.c komunikat.h gcc -Wall -c $< -o $@ main.o: main.c test.h gcc -Wall -c $< -o $@ test.o: test.c test.h komunikat.h gcc -Wall -c $< -o $@ .PHONY: clean clean: rm -f program $(objects)
Reguły schematy
Zauważmy, że po ostatnich zmianach polecenia do kompilacja programu .c
w plik wynikowy .o
wyglądają identycznie. Zatem może da się je jakoś raz zadeklarować. Otóż tak, z pomocą przychodzą reguły schematy.