Krótkie wprowadzenie do GNU make – czyli jak pisać poprawne Makefile

Make jest programem automatyzującym proces kompilacji programów, na które składa się wiele zależnych od siebie plików. Make przetwarza plik reguł Makefile, analizuje daty aktualizacji poszczególnych plików i wywołuje kompilacje tylko tych plików, które są zależne od plików które uległy zmianie od ostatniej kompilacji, co zaoszczędza wiele czasu przy tworzeniu programu, dzięki czemu nie ma potrzeby kompilacji całego projektu co pozwala oszczędzić czas. Ponadto umiejetnie zastosowany upraszcza i przyśpiesza proces instalacji i sprawia iż jest on bardziej elastyczny

Istnieje kilka wersji make, m.in:

Format pliku reguł różni się w zależności od implementacji programu make, ale podstawowe reguły są takie same dla wszystkich odmian make.

Po uruchomieniu, make w bieżącym katalogu próbuje otworzyć plik o nazwie „Makefile„, a jeżeli go nie ma, to próbuje odczytać plik „makefile„. Jeżeli chcemy aby wywołał plik o innej nazwie, to podajemy ją korzystając z opcji -f:
make -f moja_nazwa_pliku

Zanim przejdziemy do tego jak pisać pliki Makefile, warto wspomnieć jeszcze, że jeżeli program jest złożony z jednego pliku plik.c, to wystarczy
make plik
aby go skompilować — make skorzysta z odpowiedniego szablonu przetwarzania, pamiętając o CFLAGS/CXXFLAGS i odpowiednim celu.

Należy też wspomnieć o parametrach -M-MM, kompilatora cc/gcc:
gcc -MM *.c
wydrukuje gotową listę zależności dla make’a
.

Składnia pliku makefile:

# komentarz...

cel : zależność1 zależność2 zależność3 ... zależnośćN
	komendy ...
	komendy ...
	...


Każda linia komend musi zaczynać się znakiem tabulacji. Dla ustalenia uwagi (dydaktyczny) przykładowy plik makefile mógł by wyglądać tak:

program: program.cpp nagluwek.h
	g++ program.cpp -o program.cpp


Jeśli chcemy aby napisany makefile był maksymalnie przenośny, elastyczny i zgodny dobrymi praktykami i wytycznymi, to Makefile nawet dla podstawowego jednoplikowego programu powinien być znacznie bardziej rozbudowany.

Szablon maksymalnie przenośnego, elastycznego i zgodnego z dobrymi praktykami i wytycznymi pliku makefile

Jeśli chcemy zaoszczędzić sobie lub innym czasu i problemów w przyszłości, to dobrze od początki i nawet dla najprostszych programów tworzyć pliki Makefile zgodne z ogólnie przyjętymi normami, standardami, wytycznymi i sprawdzonymi rozwiązaniami. W tym celu za bazę możecie przyjąć przygotowany przeze mnie szablon:

#
# Podpis i kontakt do autora, licencja
# Copyright (C) 2010-2014 Arkadiusz Ćwiek
#

SHELL = /bin/sh

### Start of configuration section ###

srcdir = .

prefix = /usr/local # ewentualnie /opt/nazwa-pakietu
exec_prefix = $(prefix)

bindir = $(exec_prefix)/bin
sbindir = $(exec_prefix)/sbin
libexecdir = $(exec_prefix)/libexec
libdir = $(exec_prefix)/libdir

sysconfdir = $(prefix)/etc
sharedstatedir = $(prefix)/com
localstatedir = $(prefix)/var
runstatedir = $(localstatedir)/run
includedir = $(prefix)/include

datarootdir = $(prefix)/share
datadir = $(datarootdir)
docdir = $(datarootdir)/doc/nazwa-pakietu
infodir = $(datarootdir)/info
localedir = $(datarootdir)/locale
mandir = $(datarootdir)/man


INSTALL = install -C
INSTALL_PROGRAM = $(INSTALL) -m 755
INSTALL_DATA = $(INSTALL) -m 644
INSTALL_MKDIR = $(INSTALL) -d -m 755
CD = cd
RMDIR = rmdir

CXXFLAGS += -O3 -Wall
LFLAGS +=


### End of configuration section ###


EXEC = program1 program2



compile: $(EXEC)


install:
        $(INSTALL_MKDIR) -d $(bindir)/   &&   $(INSTALL_PROGRAM) $(EXEC) $(bindir)/

uinstall:
        $(CD) $$(bindir) && $(RM) $(EXEC)

all: compile install


clean:
        $(RM) *~ *.bak *.o *.so *.a $(EXEC)

program1: ...
        ...

program2: ...
        ...

...

Szablon zawiera wszystkie standardowe zmienne które powinny być używane przy definiowaniu katalogów. Natomiast cześć z regułami została ograniczona do niezbędnego minimum. Uważam, że nazwy wszystkich zmiennych są na tyle klarowne, że nie wymagają dodatkowego komentarza.
Warto podkreślić, że tak jak widać w powyższym szablonie do instalacji programów należy używać programu install, a nie cp, co jest często występującym błędem.
Ponad żadnego z programów konsolowych tj. np. awk cat cmp cp diff echo egrep expr false grep install-info ln ls mkdir mv printf pwd rm rmdir sed sleep sort tar test touch tr true nie należy używać bezpośrednio. Zamiast tego należy utworzyć odpowiednią zmienną i używać zewnętrznych programów poprzez te zmienne tak jak to zrobiono w powyższym szablonie dla programów rmdir, cd, install. Ponadto dla części programów istnieją wbudowane reprezentujące je zmienne, np. RM która odpowiada rm -f.

Jak NIE pisać makefile!

Na koniec jedna fatalna konstrukcja którą kiedyś widziałem (przez grzeczność nie linkuje do autorów) i której powielać NIE należy — czyli jak NIE tworzyć katalogów i jak NIE instalować programów i bibliotek:

install: all
	sh -c "if [ ! -d $(DESTDIR) ] ; then mkdir $(DESTDIR) ; fi"
	cp $(EXEC) $(DESTDIR)/$(EXEC)


Niektórzy pewnie zapytają co w tym złego? Oto odpowiedzi:

  1. Do instalacji programów i bibliotek należy używać programu/polecenia install a nie cp! Więcej na ten temat napisałem tu.
  2. Jaki jest sens sprawdzać czy podana ścieżka jest katalogiem — przecież można kazać go po prostu utworzyć — jak istnieje to nic złego się nie stanie.
  3. Skoro sprawdzamy czy taki katalog przypadkiem nie istnieje przed próbą utworzenia to&dlaczego nie sprawdzamy czy istnieje plik o tej nazwie przecież to stanowiło by realny problem.
  4. I na koniec — jest sprawdzenie czy katalog istnieje przed utworzeniem (co jest zbędne) ale nie ma sprawdzenia czy został utworzony przed próbą kopiowania do niego pliku.

Podsumowując — czy można być bardziej nie konsekwentnym?
A więc pojawi się pytanie jak to zrobić właściwie? Oto odpowiedź:

install: compile
	install -D -m 755 $(EXEC) $(bindir)/$(EXEC)


lub jeżeli zmienna ${EXEC} zwiera więcej niż jedną nazwę pliku:

install: compile
	install -d -m 755 $(bindir)   &&   install -m 755 $(EXEC) $(bindir)

Dodaj komentarz