Spis treści
MAKE

Wielu z was zapewne spotkało się z czymś takim, jak 'makefile' lub 'make', natomiast nie każdy wie, do czego te pliki służą. W tym artykule opiszę używany przeze mnie program Make, wchodzący w skład Geek Gadgets. Wersja ta powinna być zgodna z odpowiednikami dostępnymi dla Linuxa, Unixa czy NetBSD.

      Program Make ma za zadanie usprawnić proces kompilowania programu. W jaki sposób objawia się owo usprawnienie? Otóż jeśli mamy program składający się z kilku lub kilkunastu plików, z których każdy podczas kompilacji wymaga wpisania kilku długich linijek, na przykład:

ppc-amigaos-g++ -O3 -Wall -o BlaBla.o -c BlaBla.cxx
ppc-amigaos-g++ -O3 -Wall -o Hello.o -c Hello.cxx
ppc-amigaos-g++ Hello.o BlaBla.o -o MyProg
chmod a+x MyProg

to wtedy wpisujemy owe linijki do specjalnego pliku, zwykle o nazwie 'makefile'. Wystarczy wtedy napisać 'make', by skompilowały się wszystkie potrzebne pliki, a dokładniej rzecz biorąc tylko te, które są nowsze (mają późniejszą datę) niż odpowiadający im plik wynikowy (zwykle z końcówką '.o').
      Załóżmy że mamy następujące pliki: "plik1.cxx", "plik1.h", "plik2.cxx", "plik2.h", "main.cxx". Zakładając że posiadamy odpowiedni makefile (jak on musi być skonstruowany, napiszę później), po wpisaniu komendy 'make' trzy razy wywołany zostanie kompilator (tutaj kompilator C++, choć może to być kompilator dowolnego języka) dla plików: "plik1.cxx", "plik2.cxx" i "main.cxx" (pozostałe pliki są tylko pomocnicze, więc nie należy ich oddzielnie kompilować) oraz jeszcze raz dodatkowo na zlinkowanie całego programu do jednego pliku.
      Mamy więc nowe pliki (trzy z końcówką ".o" i jeden główny), każdy oczywiście z datą późniejszą niż źródłówki (jest to dość ważne). Uruchamiamy nasz świeżo skompilowany program i powiedzmy, że chcemy zmienić plik "main.cxx", ponieważ coś w nim nam nie odpowiada. Poprawiamy błąd, zapisujemy na dysk i ponownie kompilujemy. I tu dochodzimy do najlepszej właściwości Make'a. Zastanówmy się, co właściwie musimy skompilować? Zmieniliśmy tylko plik "main.cxx", resztę pozostawiając bez zmian, tak więc wypadałoby skompilować tylko ten jeden plik, a inne zostawić w spokoju. I tak też postąpi Make. A po czym pozna który plik kompilować, a który nie? Po dacie. Należy zauważyć, że tylko "main.cxx" jest nowszy niż odpowiadający mu plik wynikowy (main.o), tak więc logiczne jest, że tylko ten plik zostanie skompilowany. Zatem po ponownym wywołaniu Make'a uruchomi on kompilator jednokrotnie (poprzednio trzykrotnie), aby wygenerować "main.o" i jeszcze raz, aby zlinkować cały program.
      Jak przekazać Make'owi informację, które pliki należy kompilować i w jaki sposób? Po wystartowaniu, Make szuka pliku o nazwie "makefile" (lub innej, jeśli taką podamy za pomocą "make -f inny_plik") i zaczyna go analizować. Makefile ma bardzo rozbudowaną gramatykę, która pozwala tworzyć skomplikowane skrypty, mimo że samych podstaw ich konstrukcji można nauczyć się w pięć minut.
      Podstawowym elementem pliku makefile jest "zasada" (z ang. "rule") i tą też nazwą będę się dalej posługiwał. Pojedyncza zasada określa sposób generowania pliku, czyli zawiera takie informacje, jak rodzaj kompilatora który będzie używany, przekazywane mu parametry i pliki od których jest on "zależny". Przykład:

main.o: main.cxx plik1.h plik2.h
ppc-amigaos-g++ main.cxx -O3 -c -Wall -o main.o

      W pierwszej linijce znajduje się nazwa pliku docelowego (ang. "target"), potem dwukropek, a następnie nazwy wszystkich plików, od których ten docelowy jest zależny (zmiana któregoś z nich wymusza rekompilację pliku docelowego). Sposób kompilacji określony jest w drugiej linii.
      Przy tej zasadzie należy zwrócić uwagę na dwie rzeczy: na początku drugiej linijki mamy tabulator i jest on tam absolutnie konieczny (pod GoldED-em uzyskuje się go przy pomocy kombinacji klawiszy CTRL+i)! Po tabulatorze Make poznaje, czy dana linijka jest nową zasadą, czy też kolejną komendą do poprzedniej zasady. Oto przykład:

program: main.o plik1.o plik2.o
ppc-amigaos-gcc main.o plik1.o plik2.o -o program
chmod a+x program
format drive="Dh0:"

      Jeśli mamy kilka zasad w jednym makefile'u (w naszym przykładzie będą to zasady do plików: "program", "main.o", "plik1.o" i "plik2.o"), a chcemy wywołać tylko jedną z nich, na przykład taką:

clear: rm *.o program
echo "Wywolano 'make clear' ! Czas: `date`" >>Log

to w takim wypadku należy wykonać "make clear" i wywołana zostanie tylko zasada clear (oraz ewentualnie zasady od których jest ona uzależniona). Jak to będzie wyglądało? Przyjmijmy że właśnie daliśmy "make clear", co spowodowało usunięcie wszystkich plików z końcówką ".o", i teraz wykonujemy "make program". Jak widać, aby stworzyć plik o nazwie "program", należy najpierw zaktualizować pliki, od których jest on zależny (są to: "main.o", "plik1.o" i "plik2.o", co wynika z zasady "program"). Czyli najpierw wywołane zostaną zasady dla tych właśnie plików (w kolejności występowania), a dopiero potem zasada dla pliku "program". Należy pamiętać, że gdy nie podamy żadnej zasady przy wywołaniu (czyli napiszemy po prostu "make"), to jako domyślna zasada zostanie przyjęta pierwsza znaleziona. Tkwi to mały haczyk, gdyż co się stanie, gdy pierwsza z brzegu zasada będzie wyglądała tak:

małe_bum:
format drive="dh0:"
reset

      Chcielibyśmy się pewnie uchronić przed przypadkowym uruchomieniem tej zasady. Otóż istnieje operator, który nam w tym pomoże. Należy tylko gdzieś przed tą zasadą napisać:

.PHONY: małe_bum

      Od teraz zasada ta będzie wywołana tylko wtedy, gdy zostanie wyraźnie wskazana poprzez "make małe_bum" albo uruchomiona przez inną "uzależnioną" od niej zasadę.

      Każdą komendę w danej zasadzie można potraktować czymś co nazwałem modyfikatorem. Trzeba wiedzieć, że Make komentuje w oknie każdą komendę którą wykonuje, a czasami jest to niezbyt pożądane:

plik1.o: plik1.cxx plik1.h
ppc-amigaos-g++ -O3 -Wall -c plik1.cxx -o plik1.o
echo "Właśnie skompilowałem plik 'plik1.cxx'!"

Uruchamiając tą zasadę ujrzymy co następuje:

ppc-amigaos-g++ -O3 -Wall -c plik1.cxx -o plik1.o
echo "Właśnie skompilowałem plik 'plik1.cxx'!"
Właśnie skompilowałem plik 'plik1.cxx'!

Jak temu zaradzić? Otóż przed niechcianą komendą wystarczy postawić znak '@':

@echo "Właśnie skompilowałem plik 'plik1.cxx'!"

      Oprócz tego, że każda komenda jest wypisywana na ekran, Make ma jeszcze jedną ciekawą cechę: po każdym błędzie cały proces analizy makefile'a jest przerywany i Make wychodzi do shella zgłaszając błąd. To również nie zawsze jest pożądane, gdyż np. po dwukrotnym wywołaniu "make clear" dostaniemy komunikat o błędzie (nie ma pliku "*.o"). Aby temu zapobiec, przed komendą dajemy znak '-', tak więc teraz mamy:

.PHONY: clear

clear:
-rm *.o program
@echo "Wywolano 'make clear' ! Czas: `date`" >>Log

      Makefile ma bardzo rozbudowaną gramatykę, której poszczególne instrukcje można wiązać przy pomocy zmiennych. Przykładowa deklaracja zmiennych:

CC = ppc-amigaos-gcc
CCPLUS = ppc-amigaos-g++
CCFLAGS = -O3 -c -Wall

      Zmienne można tworzyć także przy wywołaniu, gdzie zamiast "make" można napisać "make CC=ppc-amigaos-gcc". W takim przypadku wartość przekazana z zewnątrz przesłania tą określoną w skrypcie. Zmienne można łączyć:

OBJS = main.o plik1.o plik2.o
HEADRES = plik1.h plik2.h
SOURCES = main.cxx plik1.cxx plik2.cxx
ALL_FILES = $(HEADERS) $(SOURCES) $(OBJS)

      W powyższym przykładzie deklaracja ALL_FILES jest równoważna deklaracji:

ALL_FILES = plik1.h plik2.h main.cxx plik1.cxx plik2.cxx main.o plik1.o plik2.o

      Ciekawą przypadłością zmiennych jest to, że ich wartość jest liczona dopiero przy odwołaniu się do określonej z nich:

CC = gcc
CCFLAGS = -O3 -c -Wall
CC2 = $(CC) $(CCFLAGS)
CC = ppc-amigaos-gcc
main.o: main.cxx
$(CC2) main.cxx -o main.o

W powyższym przykładzie wywołane zostanie 'ppc-amigaos-gcc', a nie, jak by się mogło wydawać, 'gcc'.

      Istnieją też zmienne specjalne. Oto niektóre z nich:

$@ - plik docelowy w danej zasadzie
$< - pierwszy plik "uzależniający" daną zasadę (tzn. ten tuż po dwukropku)
$^ - wszystkie pliki "uzależniające" (nazwy powtarzających się plików zostaną pominięte)
$+ - jak wyżej, z tym że bez eliminacji powtórzeń
$? - wszystkie pliki "uzależniające" daną zasadę, które są nowsze od pliku docelowego

Przykład:

main.o: main.cxx main.h
gcc $< -o $@
# równoważne: gcc main.cxx -o main.o

      Istnieją też tzw. "zasady domyślne" (ang. implicit rules). Są one używane wtedy, gdy dana zasada nie zawiera żadnych komend, na przykład:

main.o: main.cxx

      Zasady domyślne są zdefiniowane dla wielu języków. Dla C++ jest to:

$(CXX) -c $(CPPFLAGS) $(CXXFLAGS)

a dla C:

$(CC) -c $(CPPFLAGS) $(CCFLAGS)

      Można też defniować własne zasady tego typu, ale nie jest to zalecane jako metoda przestarzała. Warto jednak poznać mechanizm tworzenia takich zasad, gdyż podczas analizy starszych makefile'ów niejednokrotnie można spotkać właśnie taki typ definicji. Oto jak definiuje się zasadę przerabiania plików *.c na pliki *.o :

.c.o:
$(CC) $< -c $(CCFLAGS)

      Pomysłowość twórcy makefile'a wiąże się z umiejętnością korzystania ze zmiennych. Make traktuje wszystkie zmienne jako ciągi tekstowe, umożliwiając wykonywanie na nich takich operacji, jak zamiana tekstu pasującego do wzorca (używane przy zamianie '.cxx' na '.o'), dodawanie innych ciągów tekstowych, usuwanie fragmentów, znajdowanie wzorca czy sortowanie, a na samej nazwie pliku: zmiana, dodanie lub usunięcie ścieżki dostępu bądź usunięcie lub dodanie prefixa/suffixa. Można łączyć nazwy ($(join a b,.c .o) da nam a.c i b.o), wykorzystywać funkcję zwracającą co n-te słowo lub dowolną ilość słów (na przykład pięć pierwszych). Istnieją też znane wszystkim instrukcje tworzące pętle i zapisy warunkowe: for i if, a także możliwość używania komend shella:

SRC = $(shell echo *.c)

      Na tym kończę zarys możliwości Make'a, życząc wszystkim długich makefile'ów i jeszcze dłuższych źródłówek.

Kamil Burzyński
Spis treści