main.c, komunikat.c, komunikat.h, test.c, test.h.
Dla przykładu niech to będą bardzo proste źródła.
main.c: #include "test.h" int main() { test(); return 0; }
komunikat.c
:
#include "komunikat.[geshifilter-code]h[/geshifilter-code]" const char *komunikat = "test";
komunikat.h
:
#ifndef KOMUNIKAT_H #define KOMUNIKAT_H extern const char *komunikat; #endif
test.c
:
#include <stdio.h> #include <math.h> #include "test.h" #include "komunikat.h" void test() { printf("%s\n", komunikat); printf("sin(2)=%f\n", sin(2)); }
test.h
:
#ifndef TEST_H #define TEST_H extern void test(); #endif
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.
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 code>makefile.M
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:
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.
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
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)
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)
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)
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.
Reguły schematy używane są do podowania sposobu kompilacji z jednego typu w drugi typ. W naszym przypadku mamy jeden sposób na tworzenie pliku wynikowego .o
z pliku .c
. Można w tym celu użyć reguły schematu, która jako cel i zależności ma wzorce.
%.o: %.c gcc -Wall -c $< -o $@
Zauważmy, że przy deklarowaniu takich reguł, zmienne automatyczne stają się niezbędne. Moglibyśmy teraz usunąć te trzy reguły, które służą nam do tworzenia pliku *.o i zastąpić je powyższą regułą. Jednak wtedy stracimy zależności. Otóż można zrobić tak. Zostawić same zależności bez poleceń, które są i tak zadane przez regułę schemat.
sources=komunikat.c main.c test.c objects=$(sources:.c=.o) program: $(objects) gcc -lm $^ -o $@ komunikat.o: komunikat.c komunikat.h main.o: main.c test.h test.o: test.c test.h komunikat.h %.o: %.c gcc -Wall -c $< -o $@ .PHONY: clean clean: rm -f program $(objects)
Takie kompilowanie programów napisanych w C wydaje się być standardowe. Okazuje się, że dla takich czynności make ma już wbudowane reguły schematy i nie trzeba już ich deklarować. Usuńmy nasz schemat i zobatrzmy co zrobi make dla takiego pliku makefile
:
sources=komunikat.c main.c test.c objects=$(sources:.c=.o) program: $(objects) gcc -lm $^ -o $@ komunikat.o: komunikat.c komunikat.h main.o: main.c test.h test.o: test.c test.h komunikat.h .PHONY: clean clean: rm -f program $(objects)
wykonujemy:
$ make clean rm -f program komunikat.o main.o test.o $ make cc -c -o komunikat.o komunikat.c cc -c -o main.o main.c cc -c -o test.o test.c gcc -lm komunikat.o main.o test.o -o program
No nie do końca otrzymaliśmy to o co nam chodziło. Został użyty kompilator cc
, a nie gcc
. Pondto znikła opcja -Wall
. Można jednak konfigurować wbudowaną regułę za pomocą zmiennych. W dokumentacji make możemy przeczytać, że wbudowana jest reguła
%.o: %.c $(CC) $(CFLAGS) $(CPPFLAGS) -c -o $@ $<
Zmienna CC
oznacza nazwę kompilatora programów napisanych w C, zmienna CFLAGS
oznacza opcję tego kompilatora, a CPPFLAGS
oznacza opcje preprocesora. Wystarczy nadać tym zmiennym odpowiednią wartość, aby uzyskać porządany efekt.
CC=gcc CFLAGS=-Wall sources=komunikat.c main.c test.c objects=$(sources:.c=.o) program: $(objects) gcc -lm $^ -o $@ komunikat.o: komunikat.c komunikat.h main.o: main.c test.h test.o: test.c test.h komunikat.h .PHONY: clean clean: rm -f program $(objects)