Dość często zdarza mi się pisać różne, małe projekty w ramach przedmiotów na wydziale. Teraz jestem w trakcie pisania “kolejnego programu do katalogowania muzyki” na JPR222. Projekt powstaje w odgórnie narzuconym C++.
C++ w miare znam, napisałem ileś tam hello worldów, nigdy nie przypadł mi do gustu na tyle bym klepał w nim coś większego. Dlatego aby nadać projektowi jakiś sens dydaktyczny postanowiłem samemu zająć się stworzeniem Makefile’a. Zadanie stosunkowo proste, łatwe i przyjemne :)
Naturalnie odpowiednie polecenia można wpisać w terminalu, jendak jest dość męczące. Drugie możliwe rozwiązanie to stworzenie skryptu kompilującego program. To już jest dość proste w obsłudze rozwiązanie, jednak nie pozbawione wad. Zakładając, że projekt zawiera klikadziesiąt plików, których kompilacja zajmuje trochę czasu, skrypt będzie bezmyślnie kompilował wszystkie pliki źródłowe mimo, że w większości przypadków rekompilacji wymagają tylko modyfikowane pliki i zależne od nich. Ponadto tworzenie skryptów kompilujących nie jest w żaden sposób ustandaryzowane, osoby kompilujące program będą zmuszone do przeczytania dokumentacji lub, w przypadku braku odpowiednich informacji, analizowania źródeł skryptu.
W tym momencie na scene wkracza make. Narzędzie to służy do automatyzacji takich zadań jak kompilacja, choć tak naprawdę jest na tyle uniwersalne, że można za jego pomocą zarządzać całym projektem. make ma możliwość wykrywania zmian w plikach i definiowania zależności między nimi, dzięki czemu można zaoszczędzić sporo czasu na kompilacji. Ponadto, w połączeniu z gcc, można zautomatyzować wykrywanie zależności między plikami, dzięki czemu obsługa make’a stanie się banalna.
Aby umożliwić make’owi działanie, konieczne jest utworzenie pliku o nazwie Makefile bądź makefile. Osobiście preferuje pierwszą wersję, w momencie wyświetlania zawartości katalogu Makefile znajduje się w okolicach początku listy. Makefile zawiera opis projektu, określa działania jakie make musi podjąć, aby skompilować program.
Ogólnie reguła wygląda tak:
cel: wymagania polecenie ...
cel i wymagania to nazwy plików, mogą zawierać metaznaki ? i *, które działają tak samo jak w powłoce. cel to plik lub pliki wyjściowe, wymagania wzbogacają make’a o wiedzę, od jakich plików pliki celu są zależne. W momencie, gdy którykolwiek plik wymagany jest nowszy niż docelowy, zostaną wykonane polecenia zawarte w dalszych liniach.
Polecenia wcięte są znakami tabulacji (wcięcia są konieczne do prawidłowej pracy make’a). Warto pamiętać, że make uruchamia osobną powłokę dla kazdego polecenia, próby kombinowania ze zmiennymi środowiska są zapominane między kolejnymi linijkami.
Powszechne jest tworzenie złożonych zależności między plikami w Makefile’u. Złożonych w sensie określania reguł dla plików wymaganych w innych regułach, np.:
amp3lib: amp3lib.o blablabla.o gcc -o amp3lib amp3lib.o blablabla.o amp3lib.o: amp3lib.c amp3lib.h gcc -c amp3lib.c blablabla.o: blablabla.c blablabla.h gcc -c blablabla.c
make analizuje cały Makefile i na podstawie tego ustala zależności między plikami. W powyższym przykladzie make skonsoliduje pliki amp3lib.o i blablabla.o pod warunkiem, że są nowsze od amp3lib. Wcześniej jednak zostanie sprawdzony wiek plików amp3lib.o i blablabla.o i porównany z wiekiem odpowiednich plików źródłowych i nagłowkowych. Jeżeli którykolwiek z tych plików okaże się starszy, nastąpi jego kompilacja. Wszystkie cele zależne od niego staną się automatycznie nieaktualne, więc make wykona polecenia określone dla celu amp3lib.
Powyższy Makefile jest kompletnym działającym przykladem. Użycie ogranicza się do jednego polecenia w terminalu:
ruanda@hestia:~$ make amp3lib gcc -c amp3lib.c gcc -c blablabla.c gcc -o amp3lib amp3lib.c blablabla.c
W razie potrzeby, można również wykonać jeden z celów amp3lib.o lub blablabla.o. Poniższe polecenia są również prawidłowe:
ruanda@hestia:~$ make amp3lib.o gcc -c amp3lib.c ruanda@hestia:~$ make blablabla.o gcc -c blablabla.c
Po ponownym wywołaniu make’a widać, że kompilacja nie jest już wykonywana:
ruanda@hestia:~$ make amp3lib ruanda@hestia:~$
Uzyskanie takiego efektu skryptem w bashu nie jest nie możliwe, ale z całą pewnoscią dużo bardziej czasochłonne.
Powyższe cele nie produkują bezpośrednio żadnych plików, dlatego powinno się to jasno określić w Makefile’u przy użyciu reguły .PHONY:
.PHONY: all clean install all: amp3lib clean: -rm -f *.o install: cp amp3lib /usr/local/bin amp3lib: amp3lib.o blablabla.o gcc -o amp3lib amp3lib.o blablabla.o amp3lib.o: amp3lib.c amp3lib.h gcc -c amp3lib.c blablabla.o: blablabla.c blablabla.h gcc -c blablabla.c
Powyższy Makefile zadziałałby prawidłowo również po usunięciu reguły .PHONY, jednak według dokumentacji, cele nieprodukujące plików działają wydajniej w momencie odpowiedniego opisania, dlatego warto stosować reguły .PHONY.
Znak minusa w poleceniu wykonującym cel clean jest pewnym zabezpieczeniem przed ewentualnymi błędami wywoływanego polecenie. rm -f *.o usunie wszystkie pliki obiektowe w aktualnym katalogu, jednak, jeżeli żaden plik nie zostanie usunięty, zwrócony zostanie błąd. Błędy zwracane przez polecenia zatrzymują działanie make’a. Takie działanie nie jest jednak zbyt pożadane w momencie czyszczenia plików.
Oczywiście make install wykonany musi być jako root z uwagi na ograniczenia możliwości zapisu w katalogu /usr/local/bin.
amp3lib: amp3lib.o blablabla.o
wystarczy aby make skompilował kolejno pliki amp3lib.c i blablabla.c, a następnie przeprowadził ich konsolidację. Dzięki domyślnym regułom przetwarzania plików można znacząco skrócić Makefile.
W razie konieczności domyślne reguły można nadpisać.
%.o: %.c gcc -c $< -o $@
% w nazwie celu oznacza dowolny ciąg znaków. %.o oznacza więc wszystkie pliki obiektowe. W liście wymagań % ma nieco inne znaczenie. Pozwala on zdefiniować relację wymagania do celu, tzn. % jest zastępowany przez tekst jaki został dopasowany w określeniu celu. Jeżeli wzorzec %.o pasuje do plików amp3lib.o i blablabla.o, to nasz Makefile działa identycznie do następującego:
amp3lib.o: amp3lib.c gcc -c $< -o $@ blablabla.o: blablabla.c gcc -c $< -o $@
$< zawsze zawiera listę plików wymaganych przez aktualną regułę, $@ zawiera listę plików docelowych.