Zalety plików make w porównaniu do automatycznego budowania przez IDE

…Czyli dlaczego pragmatyczni programiści mieli rację. Na przykładzie.

W swojej książce "Pragmatyczny Programista", Andy Hunt i Dave Thomas wysuwają tezę, że zautomatyzowane budowanie za pomocą zintegrowanych środowisk programistycznych jest gorszym rozwiązaniem, niż własnoręczne pisanie plików make. Uzasadniają to większymi możliwościami ingerencji w proces budowania i automatyzacji wielu czynności, które normalnie trzeba by było wykonać ręcznie.

Na podstawie krótkiego banalnego przykładu postaram się uzasadnić, że jest to na ogół właściwe podejście. Nie jest to artykuł instruktażowy na temat "jak się pisze dobre pliki make", ani nawet "jak się pisze jakiekolwiek pliki make". Jego rola jest raczej poglądowa, także nie będę tłumaczył szczegółowo zmian wprowadzanych w poszczególnych rozdziałach, a tylko przedstawię koncepcje.

Materiał do przykładu

Zacznę od pokazania kodu, z którym będziemy mieli do czynienia. Mój projekt składa się z 6 plików:

  1. Main.cc - główny plik programu, zawierający funkcję main().
  2. Test.cc - zawiera kod testujący moje klasy (najczęściej testy robi się w inny sposób, ale dla prostoty i dobra przykładu posłużyłem się tym najbardziej prymitywnym).
  3. ClassOne.hh i ClassOne.cc - zawierają opis klasy ClassOne
  4. ClassTwo.hh i ClassTwo.cc - analogicznie do powyższych dwóch plików.

Kod zawarty w poszczególnych plikach przedstawia się następująco:

ClassOne.hh

#ifndef __CLASS_ONE_HH__
#define __CLASS_ONE_HH__
class ClassOne
{
public:
    ClassOne();
};
#endif // __CLASS_ONE_HH__

ClassOne.cc

#include"ClassOne.hh"
 
ClassOne::ClassOne()
{
    //EMPTY BODY
}

ClassTwo.hh

#ifndef __CLASS_TWO_HH__
#define __CLASS_TWO_HH__
class ClassTwo
{
public:
    ClassTwo();
};
#endif // __CLASS_TWO_HH__

ClassTwo.cc

#include"ClassTwo.hh"
 
ClassTwo::ClassTwo()
{
    //EMPTY BODY
}

Main.cc

#include"ClassOne.hh"
#include"ClassTwo.hh"
 
int main()
{
    return 0;
}

Test.cc

#include"ClassOne.hh"
#include"ClassTwo.hh"
#include<iostream>
 
//UWAGA: ten test jest skrajnie idiotyczny!!
int main()
{
    if (&ClassOne() != 0)
    {
        std::cerr << "Wszystkie testy przeszły!!!" << std::endl;
        return 0;
    }
    else
    {
        std::cerr << "Coś się skopało, Ziomen..." << std::endl;
        return 1;
    }
    return 0;
}

Pierwszy Makefile.

Zacznę od czegoś prostego, co pozwoli zbudować program oraz testy. Najbardziej niewyszukany plik make jaki można napisać do naszego kodu mógłby wyglądać następująco:

all: test binary

ClassOne.o: ClassOne.cc
    g++ -c ClassOne.cc -o ClassOne.o

ClassTwo.o: ClassTwo.cc
    g++ -c ClassTwo.cc -o ClassTwo.o

Main.o: Main.cc
    g++ -c Main.cc -o Main.o    

Test.o: Test.cc
    g++ -c Test.cc -o Test.o

binary: Main.o ClassOne.o ClassTwo.o
    g++ Main.o ClassOne.o ClassTwo.o -o binary

tests: Test.o ClassOne.o ClassTwo.o
    g++ Test.o ClassOne.o ClassTwo.o -o tests

.PHONY : clean

clean:
    rm -f *.o binary tests
    rm -f *~ *.*~

Już w tym pliku widać pewną koncepcję - buduję dwa pliki wykonywalne, raz linkując funkcję main() z pliku Test.cc, a raz z Test.cc. Tym samym mam od razu wersję wykonywalną, ale też mogę sobie puścić testy, żeby zobaczyć, jakie będą rezultaty. Makefile automatyzuje za mnie proces budowania dwóch różnie linkowanych plików binarnych.

Po posprzątaniu i użyciu kilku bardziej zaawansowanych mechanizmów, takich jak zmienne automatyczne (nie będę tego tutaj omawiać, gdyż nie jest to kurs narzędzia make), mój plik make wygląda następująco:

# system commands
CXX = g++
CXXFLAGS = -Wall -pedantic
RM = rm -f

# source files names
COMMON_SRC = ClassOne.cc ClassTwo.cc
TEST_SRC = Test.cc
MAIN_SRC = Main.cc

# object files names
COMMON_OBJ = $(COMMON_SRC:.cc=.o)
TEST_OBJ = $(TEST_SRC:.cc=.o)
MAIN_OBJ = $(MAIN_SRC:.cc=.o)

# binary files names
TEST_BIN = $(TEST_SRC:.cc=)
MAIN_BIN = $(MAIN_SRC:.cc=)

# all objects and binaries names gathered (used in 'clean' rule)
OBJ = $(COMMON_OBJ) $(TEST_OBJ) $(MAIN_OBJ)
BIN = $(TEST_BIN) $(MAIN_BIN)

# 'all' rule is the default one 
.DEFAULT: all

# rules:
all: $(BIN)

.cc.o:
    $(CXX) -c $< -o $@

$(MAIN_BIN): $(MAIN_OBJ) $(COMMON_OBJ)
    $(CXX) $^ $(CXXFLAGS) -o $@

$(TEST_BIN): $(TEST_OBJ) $(COMMON_OBJ)
    $(CXX) $^ $(CXXFLAGS) -o $@

.PHONY : clean

clean:
    $(RM) $(OBJ) $(BIN)
    $(RM) *~ *.*~

Automatyzacja dodatkowych działań.

Drukowanie kodu źródłowego.

Często zdarza się, że kod, który się napisze, należy potem poddać inspekcji w koleżeńskim gronie. Wtedy przydają się wydruki kodu źródłowego. Można wtedy oczywiście otwierać każdy plik po kolei, można skorzystać z wtyczek albo możliwości niektórych zintegrowanych środowisk programistycznych, ale można też zautomatyzować to całkowicie i być pewnym, że aktualny kod zawsze zostanie wydrukowany zaraz po zbudowaniu aplikacji.

Do drukowania kodu źródłowego posłużę się narzędziami a2ps (konwertuje plik tekstowy do formatu postscript) oraz ps2pdf (konwertuje format postscript do formatu pdf).

Pojedyncza linijka, która drukowałaby w moim przypadku wszystkie interesujące mnie pliki do odpowiednio sformatowanego pliku postscript (nazwę go sobie CodeHandout.ps) wyglądać może następująco (ie wyjaśniam tutaj co oznaczają poszczególne flagi. Można to sprawdzić wpisując 'man a2ps' albo jeszcze lepiej - 'info a2ps'):

a2ps -M A4 -R --columns=1 --line-numbers=1 --left-footer='Version from: $D $C printed on: %C %D' 
    --left-title=''  --right-footer='$p./%p#' -E -g *.hh *.cc Makefile -o CodeHandout.ps

Można ją umieścić bezpośrednio w pliku make pod odpowiednią regułą, ale, jako że nie lubię konkretów w regułach pliku make, najpierw zdefiniuję szereg zmiennych:

A2PS = a2ps
A2PS_COLUMNS = 1
A2PS_PRINT_MODE = -R
A2PS_MEDIUM = A4
A2PS_LINE_NUMBERS_STEP = 1
A2PS_LEFT_FOOTER_TEXT = 'Version from: $$D $$C printed on: %C %D'
A2PS_LEFT_TITLE_TEXT = '' 
A2PS_RIGHT_FOOTER_TEXT = '$$p./%p\#'
A2PS_INPUT_FILES = *.hh *.cc Makefile
A2PS_OUTPUT_FILE = CodeHandout.ps
A2PS_PRINT_FLAGS = -E -g

Teraz z tych zmiennych poskładam wszystkie argumenty dodawane z linii poleceń:

A2PS_ARGUMENTS = -M $(A2PS_MEDIUM) \
        $(A2PS_PRINT_MODE) \
        --columns=$(A2PS_COLUMNS) \
        --line-numbers=$(A2PS_LINE_NUMBERS_STEP) \
        --left-footer=$(A2PS_LEFT_FOOTER_TEXT) \
        --left-title=$(A2PS_LEFT_TITLE_TEXT) \
        --right-footer=$(A2PS_RIGHT_FOOTER_TEXT) \
        $(A2PS_PRINT_FLAGS) \
        $(A2PS_INPUT_FILES) \
        -o $(A2PS_OUTPUT_FILE)

Na koniec dodam regułę, która wykona polecenie a2ps:
$(A2PS_OUTPUT_FILE):
    $(A2PS) $(A2PS_ARGUMENTS)

Podobnie z ps2pdf. Tu jednak zadeklaruję tylko zmienne:

PS2PDF = ps2pdf
PS2PDF_OUTPUT_FILE = $(A2PS_OUTPUT_FILE:.ps=.pdf)

Oraz regułę:

$(PS2PDF_OUTPUT_FILE): $(A2PS_OUTPUT_FILE)
    $(PS2PDF) $(A2PS_OUTPUT_FILE)

Zmodyfikuję także reguły: all (żeby przy każdym wywołaniu sprawdzała i aktualizowała wydruki) oraz clean (żeby usuwała wydruki). Pełen plik make ma teraz następującą postać:

# system commands
CXX = g++
RM = rm -f
A2PS = a2ps
PS2PDF = ps2pdf

#compiler flags
CXXFLAGS = -Wall -pedantic

#a2ps specific flags
A2PS_COLUMNS = 1
A2PS_PRINT_MODE = -R
A2PS_MEDIUM = A4
A2PS_LINE_NUMBERS_STEP = 1
A2PS_LEFT_FOOTER_TEXT = 'Version from: $$D $$C printed on: %C %D'
A2PS_LEFT_TITLE_TEXT = '' 
A2PS_RIGHT_FOOTER_TEXT = '$$p./%p\#'
A2PS_INPUT_FILES = *.hh *.cc Makefile
A2PS_OUTPUT_FILE = CodeHandout.ps
A2PS_PRINT_FLAGS = -E -g

A2PS_ARGUMENTS = -M $(A2PS_MEDIUM) \
        $(A2PS_PRINT_MODE) \
        --columns=$(A2PS_COLUMNS) \
        --line-numbers=$(A2PS_LINE_NUMBERS_STEP) \
        --left-footer=$(A2PS_LEFT_FOOTER_TEXT) \
        --left-title=$(A2PS_LEFT_TITLE_TEXT) \
        --right-footer=$(A2PS_RIGHT_FOOTER_TEXT) \
        $(A2PS_PRINT_FLAGS) \
        $(A2PS_INPUT_FILES) \
        -o $(A2PS_OUTPUT_FILE)

# ps2pdf specific flags
PS2PDF_OUTPUT_FILE = $(A2PS_OUTPUT_FILE:.ps=.pdf)

# source files names
COMMON_SRC = ClassOne.cc ClassTwo.cc
TEST_SRC = Test.cc
MAIN_SRC = Main.cc

# object files names
COMMON_OBJ = $(COMMON_SRC:.cc=.o)
TEST_OBJ = $(TEST_SRC:.cc=.o)
MAIN_OBJ = $(MAIN_SRC:.cc=.o)

# binary files names
TEST_BIN = $(TEST_SRC:.cc=)
MAIN_BIN = $(MAIN_SRC:.cc=)

# all objects and binaries names gathered (used in 'clean' rule)
OBJ = $(COMMON_OBJ) $(TEST_OBJ) $(MAIN_OBJ)
BIN = $(TEST_BIN) $(MAIN_BIN)

# 'all' rule is the default one 
.DEFAULT: all

# rules:
all: $(BIN) $(A2PS_OUTPUT_FILE) $(PS2PDF_OUTPUT_FILE)

.cc.o:
    $(CXX) -c $< -o $@

$(MAIN_BIN): $(MAIN_OBJ) $(COMMON_OBJ)
    $(CXX) $^ $(CXXFLAGS) -o $@

$(TEST_BIN): $(TEST_OBJ) $(COMMON_OBJ)
    $(CXX) $^ $(CXXFLAGS) -o $@

$(A2PS_OUTPUT_FILE):
    $(A2PS) $(A2PS_ARGUMENTS)

$(PS2PDF_OUTPUT_FILE): $(A2PS_OUTPUT_FILE)
    $(PS2PDF) $(A2PS_OUTPUT_FILE)

.PHONY : clean

clean:
    $(RM) $(OBJ) $(BIN) $(A2PS_OUTPUT_FILE) $(PS2PDF_OUTPUT_FILE)
    $(RM) *~ *.*~

Teraz możesz w każdej chwili zgarnąć pliki CodeHandout.ps i CodeHandout.pdf i spokojnie puścić je na drukarkę mając pewność, że są aktualne i nie martwić się o to, kto i kiedy je wyprodukował.

Generowanie dokumentacji.

Jeden z największych problemów z dokumentacją jest taki, że rzadko kiedy jest aktualna. Najgorzej jest, kiedy powstaje ona niezależnie od kodu źródłowego - wtedy ryzyko rozsynchronizowania jest większe. Na szczęście są do dyspozycji generatory dokumentacji, które tworzą dokumentację w oparciu o kod źródłowy i tam też pozwalają nam trzymać wszelkie informacje dotyczące kodu. Żeby mieć pewność, że dokumentacja zawsze dotyczy ostatnio budowanych plików binarnych, warto wciągnąć proces generowania dokumentacji do pliku make.

Posłużę się narzędziem doxygen, które jest chyba najpopularniejszym z narzędzi do generowania dokumentacji kodu w C++.

Doxygen wymaga pliku konfiguracyjnego, który zawiera wszystkie opcje dotyczące generowania dokumentacji dla danego projektu. Szablon takiego pliku można w każdej chwili wygenerować przy użyciu doxygena. W pliku make wezmę pod uwagę przypadek, w którym plik konfiguracyjny nie został jeszcze utworzony - wtedy będzie generowany. Natomiast nie będzie on usuwany podczas wywołania reguły clean, ponieważ po utworzeniu wymaga on najczęściej wielu ręcznych modyfikacji, by dopasować go do własnych potrzeb.

Najpierw utworzę zmienne przechowujące nazwę doxygena oraz pliku z opcjami:

DOC_GENERATOR = doxygen
DOXYFILE = Doxyfile
GENERATED_DOC_DIRS= html latex

Warto zauważyć, że zmienna GENERATED_DOC_DIRS przechowuje nazwy katalogów, które będą wygenerowane przez doxygena, żeby móc je w razie czego zniszczyć. Jest to nieeleganckie rozwiązanie, ponieważ jeśli niektóre opcje zostaną zmienione w pliku konfiguracyjnym (Doxyfile), to tutaj trzeba będzie to uwzględnić. W jednym z następnych rozdziałów naprawię tę niedoskonałość. Tymczasem dodam jeszcze dwie reguły: jedną tworzącą plik konfiguracyjny, drugą generującą dokumentację:

$(DOXYFILE):
    @ if [ ! -f $(DOXYFILE) ]; then \
        $(PRINT) "-> $(DOXYFILE) not found. Generating fresh one."; \
        $(DOC_GENERATOR) -g $(DOXYFILE); \
    fi

docs: $(DOXYFILE)
    $(DOC_GENERATOR) $(DOXYFILE)

Teraz pozostaje odpowiednio zmodyfikować reguły 'all', 'clean' oraz .PHONY (żeby uwzględnić nazwę docs, która została użyta jako nazwa reguły) i dostajemy następujący plik:

# system commands
CXX = g++
RM = rm -f
A2PS = a2ps
PS2PDF = ps2pdf
DOC_GENERATOR = doxygen

#compiler flags
CXXFLAGS = -Wall -pedantic

#a2ps specific flags
A2PS_COLUMNS = 1
A2PS_PRINT_MODE = -R
A2PS_MEDIUM = A4
A2PS_LINE_NUMBERS_STEP = 1
A2PS_LEFT_FOOTER_TEXT = 'Version from: $$D $$C printed on: %C %D'
A2PS_LEFT_TITLE_TEXT = '' 
A2PS_RIGHT_FOOTER_TEXT = '$$p./%p\#'
A2PS_INPUT_FILES = *.hh *.cc Makefile
A2PS_OUTPUT_FILE = CodeHandout.ps
A2PS_PRINT_FLAGS = -E -g

A2PS_ARGUMENTS = -M $(A2PS_MEDIUM) \
        $(A2PS_PRINT_MODE) \
        --columns=$(A2PS_COLUMNS) \
        --line-numbers=$(A2PS_LINE_NUMBERS_STEP) \
        --left-footer=$(A2PS_LEFT_FOOTER_TEXT) \
        --left-title=$(A2PS_LEFT_TITLE_TEXT) \
        --right-footer=$(A2PS_RIGHT_FOOTER_TEXT) \
        $(A2PS_PRINT_FLAGS) \
        $(A2PS_INPUT_FILES) \
        -o $(A2PS_OUTPUT_FILE)

# ps2pdf specific flags
PS2PDF_OUTPUT_FILE = $(A2PS_OUTPUT_FILE:.ps=.pdf)

# doxygen specific options
DOXYFILE = Doxyfile
GENERATED_DOC_DIRS= html latex

# source files names
COMMON_SRC = ClassOne.cc ClassTwo.cc
TEST_SRC = Test.cc
MAIN_SRC = Main.cc

# object files names
COMMON_OBJ = $(COMMON_SRC:.cc=.o)
TEST_OBJ = $(TEST_SRC:.cc=.o)
MAIN_OBJ = $(MAIN_SRC:.cc=.o)

# binary files names
TEST_BIN = $(TEST_SRC:.cc=)
MAIN_BIN = $(MAIN_SRC:.cc=)

# all objects and binaries names gathered (used in 'clean' rule)
OBJ = $(COMMON_OBJ) $(TEST_OBJ) $(MAIN_OBJ)
BIN = $(TEST_BIN) $(MAIN_BIN)

# 'all' rule is the default one 
.DEFAULT: all

# rules:
all: $(BIN) $(A2PS_OUTPUT_FILE) $(PS2PDF_OUTPUT_FILE) docs

.cc.o:
    $(CXX) -c $< -o $@

$(MAIN_BIN): $(MAIN_OBJ) $(COMMON_OBJ)
    $(CXX) $^ $(CXXFLAGS) -o $@

$(TEST_BIN): $(TEST_OBJ) $(COMMON_OBJ)
    $(CXX) $^ $(CXXFLAGS) -o $@

$(A2PS_OUTPUT_FILE):
    $(A2PS) $(A2PS_ARGUMENTS)

$(PS2PDF_OUTPUT_FILE): $(A2PS_OUTPUT_FILE)
    $(PS2PDF) $(A2PS_OUTPUT_FILE)

$(DOXYFILE):
    @ if [ ! -f $(DOXYFILE) ]; then \
        $(PRINT) "-> $(DOXYFILE) not found. Generating fresh one."; \
        $(DOC_GENERATOR) -g $(DOXYFILE); \
    fi

docs: $(DOXYFILE)
    $(DOC_GENERATOR) $(DOXYFILE)

.PHONY : clean docs

clean:
    $(RM) $(OBJ) $(BIN) $(A2PS_OUTPUT_FILE) $(PS2PDF_OUTPUT_FILE)
    $(RM) -r -d $(GENERATED_DOC_DIRS)
    $(RM) *~ *.*~

Moc powłoki

W tym rozdziale wykorzystam powłokę, żeby dodać troszkę logiki do mojego pliku make.

Reakcja na zawodzące testowanie.

Po pierwsze - nie podoba mi się to, że testy nie są automatycznie zapuszczane przy każdym budowaniu. To sprzyja lenistwu, które każe człowiekowi w końcu omijać testowanie jako czasochłonny proces. Przez to wykrycie błędu może zdarzyć się na tyle późno, że szukanie go będzie niczym szukanie igły w stogu siana. Dlatego też dopiszę do mojego pliku make wykonywanie testów za każdym razem, gdy program jest budowany. Zrobię nawet więcej - dopóki testy nie przejdą - żadnej dokumentacji, żadnego wydruku. Oczywiście każdy będzie mógł sobie je wygenerować wywołując jawnie odpowiednie reguły pliku make, zatem nie zatykam komuś całkowicie (jak wiadomo, "Nie istnieją decyzje ostateczne").

W celu dodania wspomnianej logiki, muszę najpierw rozbić regułę all w następujący sposób:

all: 
    $(MAKE) $(TEST_BIN)
    ./$(TEST_BIN)
    $(MAKE) $(MAIN_BIN) 
    $(MAKE) $(A2PS_OUTPUT_FILE) 
    $(MAKE) $(PS2PDF_OUTPUT_FILE) 
    $(MAKE) docs

… gdzie:

MAKE = make

W powyższym fragmencie widać, że dodałem także wykonanie testów za każdym wywołaniem budowania, za pomocą linijki ./$(TEST_BIN). Nigdzie nie sprawdzam jednak, czy testy się powiodły, czy też zawiodły. Do tego potrzebna będzie powierzchowna znajomość powłoki sh.

Jeśli spojrzy się na zawartość pliku Test.cc, to widać, że w zależności od tego, czy testy przeszły, czy też nie, program zwraca inną wartość. Tę wartość można wykorzystać z poziomu powłoki i w zależności od spełnienia warunku odpowiednio zareagować. W moim przypadku zwrócenie czegokolwiek innego niż 0 oznacza niepowodzenie:

    @ echo "Executing tests..."    
    ./$(TEST_BIN)
    @ if [ "$$?" -ne "0" ]; then \
        echo "Tests Failed, further building stopped."; \
        exit 1; \
    fi

Do zrozumienia powyższego skryptu wystarczy wiedzieć, że znak '@' anuluje wyświetlanie komendy na ekranie przed jej wykonaniem, zmienna środowiskowa '$?' przechowuje wartość zwróconą przez ostatnio wykonany program, a '-ne' oznacza "różny".

Po wyżej opisanych krokach oraz kilku innych kosmetycznych poprawkach, mój plik budujący wygląda tak:

# system commands
CXX = g++
RM = rm -f
A2PS = a2ps
PS2PDF = ps2pdf
DOC_GENERATOR = doxygen
MAKE = make
PRINT = echo

#compiler flags
CXXFLAGS = -Wall -pedantic

#a2ps specific flags
A2PS_COLUMNS = 1
A2PS_PRINT_MODE = -R
A2PS_MEDIUM = A4
A2PS_LINE_NUMBERS_STEP = 1
A2PS_LEFT_FOOTER_TEXT = 'Version from: $$D $$C printed on: %C %D'
A2PS_LEFT_TITLE_TEXT = '' 
A2PS_RIGHT_FOOTER_TEXT = '$$p./%p\#'
A2PS_INPUT_FILES = *.hh *.cc Makefile
A2PS_OUTPUT_FILE = CodeHandout.ps
A2PS_PRINT_FLAGS = -E -g

A2PS_ARGUMENTS = -M $(A2PS_MEDIUM) \
        $(A2PS_PRINT_MODE) \
        --columns=$(A2PS_COLUMNS) \
        --line-numbers=$(A2PS_LINE_NUMBERS_STEP) \
        --left-footer=$(A2PS_LEFT_FOOTER_TEXT) \
        --left-title=$(A2PS_LEFT_TITLE_TEXT) \
        --right-footer=$(A2PS_RIGHT_FOOTER_TEXT) \
        $(A2PS_PRINT_FLAGS) \
        $(A2PS_INPUT_FILES) \
        -o $(A2PS_OUTPUT_FILE)

# ps2pdf specific flags
PS2PDF_OUTPUT_FILE = $(A2PS_OUTPUT_FILE:.ps=.pdf)

# doxygen specific options
DOXYFILE = Doxyfile
GENERATED_DOC_DIRS= html latex

# source files names
COMMON_SRC = ClassOne.cc ClassTwo.cc
TEST_SRC = Test.cc
MAIN_SRC = Main.cc

# object files names
COMMON_OBJ = $(COMMON_SRC:.cc=.o)
TEST_OBJ = $(TEST_SRC:.cc=.o)
MAIN_OBJ = $(MAIN_SRC:.cc=.o)

# binary files names
TEST_BIN = $(TEST_SRC:.cc=)
MAIN_BIN = $(MAIN_SRC:.cc=)

# all objects and binaries names gathered (used in 'clean' rule)
OBJ = $(COMMON_OBJ) $(TEST_OBJ) $(MAIN_OBJ)
BIN = $(TEST_BIN) $(MAIN_BIN)

# 'all' rule is the default one 
.DEFAULT: all

# rules:
all: 
    @ $(MAKE) $(TEST_BIN)
    @ $(PRINT) "Executing tests..."
    ./$(TEST_BIN)
    @ if [ "$$?" -ne "0" ]; then \
        $(PRINT) "Tests Failed, further building stopped."; \
        exit 1; \
    fi
    @ $(PRINT) "Tests passed, building main binary..."
    @ $(MAKE) $(MAIN_BIN) 
    @ $(MAKE) $(A2PS_OUTPUT_FILE) 
    @ $(MAKE) $(PS2PDF_OUTPUT_FILE) 
    @ $(MAKE) docs

.cc.o:
    $(CXX) -c $< -o $@

$(MAIN_BIN): $(MAIN_OBJ) $(COMMON_OBJ)
    $(CXX) $^ $(CXXFLAGS) -o $@

$(TEST_BIN): $(TEST_OBJ) $(COMMON_OBJ)
    $(CXX) $^ $(CXXFLAGS) -o $@

$(A2PS_OUTPUT_FILE):
    $(A2PS) $(A2PS_ARGUMENTS)

$(PS2PDF_OUTPUT_FILE): $(A2PS_OUTPUT_FILE)
    $(PS2PDF) $(A2PS_OUTPUT_FILE)

$(DOXYFILE):
    @ if [ ! -f $(DOXYFILE) ]; then \
        $(PRINT) "-> $(DOXYFILE) not found. Generating fresh one."; \
        $(DOC_GENERATOR) -g $(DOXYFILE); \
    fi

docs: $(DOXYFILE)
    $(DOC_GENERATOR) $(DOXYFILE)

.PHONY : clean docs

clean:
    $(RM) $(OBJ) $(BIN) $(A2PS_OUTPUT_FILE) $(PS2PDF_OUTPUT_FILE)
    $(RM) -r -d $(GENERATED_DOC_DIRS)
    $(RM) *~ *.*~

Porządkowanie plików w katalogach.

Tutaj nie ma za wiele filozofii - wymyślę sobie jakąś strukturę katalogów, następnie będę je tworzyć i niektóre usuwać w razie potrzeby.

Chciałbym, żeby moje drzewo projektu wyglądało następująco:

  • [ROOT]
    • Makefile
    • Doxyfile
    • src
      • ClassOne.cc
      • ClassOne.hh
      • ClassTwo.cc
      • ClassTwo.hh
      • Main.cc
      • Test.cc
    • bin
      • Main
      • Test
    • doc
      • (wszystko, co wygenerował doxygen)
    • audit
      • CodeHandout.ps
      • CodeHandout.pdf

W tym celu wprowadzę szereg zmiennych dotyczących ścieżek:

PROJECT_ROOT = /home/astral/workspace/PlaygroundForCcLinux

SRC_DIR = src
OBJ_DIR = obj
BIN_DIR = bin
DOC_DIR = doc
AUDIT_DIR = audit

SRC_PATH = $(PROJECT_ROOT)/$(SRC_DIR)
OBJ_PATH = $(PROJECT_ROOT)/$(OBJ_DIR)
BIN_PATH = $(PROJECT_ROOT)/$(BIN_DIR)
DOC_PATH = $(PROJECT_ROOT)/$(DOC_DIR)
AUDIT_PATH = $(PROJECT_ROOT)/$(AUDIT_DIR)

Następnie zmienię reguły zastępowania napisów w ścieżkach, np.:

A2PS_INPUT_FILES = $(SRC_PATH)/*.hh $(SRC_PATH)/*.cc Makefile
A2PS_OUTPUT_FILE = $(AUDIT_PATH)/CodeHandout.ps

# doxygen specific options
DOXYFILE = $(PROJECT_ROOT)/Doxyfile

COMMON_SRC = $(SRC_PATH)/ClassOne.cc $(SRC_PATH)/ClassTwo.cc
TEST_SRC = $(SRC_PATH)/Test.cc
MAIN_SRC = $(SRC_PATH)/Main.cc

COMMON_OBJ = $(COMMON_SRC:$(SRC_PATH)/%.cc=$(OBJ_PATH)/%.o)
TEST_OBJ = $(TEST_SRC:$(SRC_PATH)/%.cc=$(OBJ_PATH)/%.o)
MAIN_OBJ = $(MAIN_SRC:$(SRC_PATH)/%.cc=$(OBJ_PATH)/%.o)

TEST_BIN = $(TEST_SRC:$(SRC_PATH)/%.cc=$(BIN_PATH)/%)
MAIN_BIN = $(MAIN_SRC:$(SRC_PATH)/%.cc=$(BIN_PATH)/%)

Zmianie musi ulec także reguła budująca pliki .o z plików .cc, z:

.cc.o

na:

$(OBJ_PATH)/%.o: $(SRC_PATH)/%.cc

W całość zawartości katalogów doc, bin, audit, obj jest generowana, zatem na codzień nie potrzebne mi są także same katalogi. Dlatego ustawię w regule 'clean', żeby niszczyła je z całą zawartością, natomiast wywołania odpowiednich reguł generujących pliki będą same sprawdzać, czy odpowiedni katalog istnieje i w razie potrzeby go tworzyć, np. dla dokumentacji będzie tworzony katalog docs w następujący sposób:

docs: $(DOXYFILE)
    @ if [ ! -d $(DOC_PATH) ]; then \
        $(PRINT) "Creating $(DOC_DIR) directory..."; \
        mkdir $(DOC_PATH); \
    fi
    $(CD) $(DOC_PATH) && $(DOC_GENERATOR) $(DOXYFILE)

W powyższej regule zmieniłem także sposób wywoływania doxygena, poniewać teraz dokumentacja nie będzie generowana do katalogu, w który znajduje się plik konfiguracyjny. Zatem najpierw wchodzę do katalogu, w którym chcę mieć dokumentację, a potem wywołuję generator z plikiem konfiguracyjnym umieszczonym w korzeniu drzewa mojego projektu (jak widać na jednym z wcześniej zamieszczonych fragmentów kodu, zmieniłem zmienną DOXYFILE tak, by zawierała pełną ścieżkę do pliku konfiguracyjnego).

Takie potraktowanie sprawy - że wszystko co wygeneruję wędruje do osobnych katalogów - umożliwia mi uproszczenie reguły clean - teraz będzie ona po prostu usuwać te katalogi. Pozwoli mi to także na eliminację nieeleganckiej zmiennej GENERATED_DOC_DIRS.

Jako że włączanie kodu sprawdzającego w każdej regule istnienie katalogu jest uciążliwe i pogarsza czytelność, zdecydowałem się na wyodrębnienie tych fragmentów kodu w postaci reguł.

Warto też zauważyć, że od teraz trzeba będzie przeszukiwać całe drzewo projektu w poszukiwaniu plików tymczasowych - nie będą tylko w katalogu głównym. W tym celu wprowadzę zmienną:

FIND_IN_PROJECT_ROOT = find $(PROJECT_ROOT) -name

którą wykorzystam w regule 'clean' w następujący sposób:

@ $(RM) `$(FIND_IN_PROJECT_ROOT) *~`

Po zastosowaniu omówionych koncepcji oraz dodaniu większej ilości opisów tego, co w danym momencie się dzieje, mój plik make wygląda tak:

# paths
PROJECT_ROOT = /home/astral/workspace/PlaygroundForCcLinux

SRC_DIR = src
OBJ_DIR = obj
BIN_DIR = bin
DOC_DIR = doc
AUDIT_DIR = audit

SRC_PATH = $(PROJECT_ROOT)/$(SRC_DIR)
OBJ_PATH = $(PROJECT_ROOT)/$(OBJ_DIR)
BIN_PATH = $(PROJECT_ROOT)/$(BIN_DIR)
DOC_PATH = $(PROJECT_ROOT)/$(DOC_DIR)
AUDIT_PATH = $(PROJECT_ROOT)/$(AUDIT_DIR)

# system commands
CXX = g++
RM = rm -f
A2PS = a2ps
PS2PDF = ps2pdf
DOC_GENERATOR = doxygen
MAKE = make
PRINT = echo
CD = cd
MKDIR = mkdir
FIND_IN_PROJECT_ROOT = find $(PROJECT_ROOT) -name 

#compiler flags
CXXFLAGS = -Wall -pedantic

#a2ps specific flags
A2PS_COLUMNS = 1
A2PS_PRINT_MODE = -R
A2PS_MEDIUM = A4
A2PS_LINE_NUMBERS_STEP = 1
A2PS_LEFT_FOOTER_TEXT = 'Version from: $$D $$C printed on: %C %D'
A2PS_LEFT_TITLE_TEXT = '' 
A2PS_RIGHT_FOOTER_TEXT = '$$p./%p\#'
A2PS_INPUT_FILES = $(SRC_PATH)/*.hh $(SRC_PATH)/*.cc Makefile
A2PS_OUTPUT_FILE = $(AUDIT_PATH)/CodeHandout.ps
A2PS_PRINT_FLAGS = -E -g

A2PS_ARGUMENTS = -M $(A2PS_MEDIUM) \
        $(A2PS_PRINT_MODE) \
        --columns=$(A2PS_COLUMNS) \
        --line-numbers=$(A2PS_LINE_NUMBERS_STEP) \
        --left-footer=$(A2PS_LEFT_FOOTER_TEXT) \
        --left-title=$(A2PS_LEFT_TITLE_TEXT) \
        --right-footer=$(A2PS_RIGHT_FOOTER_TEXT) \
        $(A2PS_PRINT_FLAGS) \
        $(A2PS_INPUT_FILES) \
        -o $(A2PS_OUTPUT_FILE)

# ps2pdf specific flags
PS2PDF_OUTPUT_FILE = $(A2PS_OUTPUT_FILE:.ps=.pdf)

# doxygen specific options
DOXYFILE = $(PROJECT_ROOT)/Doxyfile

# source files names
COMMON_SRC = $(SRC_PATH)/ClassOne.cc $(SRC_PATH)/ClassTwo.cc
TEST_SRC = $(SRC_PATH)/Test.cc
MAIN_SRC = $(SRC_PATH)/Main.cc

# object files names
COMMON_OBJ = $(COMMON_SRC:$(SRC_PATH)/%.cc=$(OBJ_PATH)/%.o)
TEST_OBJ = $(TEST_SRC:$(SRC_PATH)/%.cc=$(OBJ_PATH)/%.o)
MAIN_OBJ = $(MAIN_SRC:$(SRC_PATH)/%.cc=$(OBJ_PATH)/%.o)

# binary files names
TEST_BIN = $(TEST_SRC:$(SRC_PATH)/%.cc=$(BIN_PATH)/%)
MAIN_BIN = $(MAIN_SRC:$(SRC_PATH)/%.cc=$(BIN_PATH)/%)

# 'all' rule is the default one 
.DEFAULT: all

# phony targets 
.PHONY : clean docs obj_path doc_path audit_path bin_path

# rules:
all: 
    @ $(RM) *.log
    @ $(PRINT) $(COMMON_SRC)
    @ $(PRINT) $(COMMON_OBJ)
    @ $(PRINT) "-> Building test binary..."
    @ $(MAKE) $(TEST_BIN)
    @ $(PRINT) "-> Executing tests..."
    $(TEST_BIN)
    @ if [ "$$?" -ne "0" ]; then \
        $(PRINT) "Tests Failed, further building stopped."; \
        exit 1; \
    fi
    @ $(PRINT) "-> Tests passed."
    @ $(MAKE) $(MAIN_BIN)
    @ $(MAKE) $(A2PS_OUTPUT_FILE) 
    @ $(MAKE) $(PS2PDF_OUTPUT_FILE)
    @ $(MAKE) docs
    @ $(PRINT) "-> Build finished successfully"

$(OBJ_PATH)/%.o: $(SRC_PATH)/%.cc obj_path
    @ $(PRINT) "-> creating $@ out of $<" 
    @ $(CXX) -c $< -o $@

$(MAIN_BIN): $(MAIN_OBJ) $(COMMON_OBJ) bin_path
    @ $(PRINT) "-> linking $(TEST_OBJ) $(COMMON_OBJ) into $@"
    @ $(CXX) $(MAIN_OBJ) $(COMMON_OBJ) $(CXXFLAGS) -o $@

$(TEST_BIN): $(TEST_OBJ) $(COMMON_OBJ) bin_path
    @ $(PRINT) "-> linking $(TEST_OBJ) $(COMMON_OBJ) into $@"
    @ $(CXX) $(TEST_OBJ) $(COMMON_OBJ) $(CXXFLAGS) -o $@

$(A2PS_OUTPUT_FILE): audit_path
    @ $(PRINT) "-> Generating postscript file: $(A2PS_OUTPUT_FILE)"
    @ $(A2PS) $(A2PS_ARGUMENTS)

$(PS2PDF_OUTPUT_FILE): $(A2PS_OUTPUT_FILE)
    @ $(PRINT) "-> Generating pdf file: $(PS2PDF_OUTPUT_FILE)"
    @ $(PS2PDF) $(A2PS_OUTPUT_FILE) $(PS2PDF_OUTPUT_FILE)

$(DOXYFILE):
    @ if [ ! -f $(DOXYFILE) ]; then \
        $(PRINT) "-> $(DOXYFILE) not found. Generating fresh one."; \
        $(DOC_GENERATOR) -g $(DOXYFILE); \
    fi

docs: $(DOXYFILE) doc_path
    @ $(PRINT) "-> Generating documentation out of configuration file: $(DOXYFILE)"
    @ $(CD) $(DOC_PATH) && $(DOC_GENERATOR) $(DOXYFILE)

obj_path: 
    @ if [ ! -d $(OBJ_PATH) ]; then \
        $(PRINT) "-> Creating $(OBJ_DIR) directory..."; \
        $(MKDIR) $(OBJ_PATH); \
    fi

doc_path:
    @ if [ ! -d $(DOC_PATH) ]; then \
        $(PRINT) "-> Creating $(DOC_DIR) directory..."; \
        $(MKDIR) $(DOC_PATH); \
    fi

audit_path:
    @ if [ ! -d $(AUDIT_PATH) ]; then \
        $(PRINT) "-> Creating $(AUDIT_DIR) directory..."; \
        $(MKDIR) $(AUDIT_PATH); \
    fi

bin_path:
    @ if [ ! -d $(BIN_PATH) ]; then \
        $(PRINT) "-> Creating $(BIN_DIR) directory..."; \
        $(MKDIR) $(BIN_PATH); \
    fi

clean:
    @ $(PRINT) "-> Cleaning object files..."
    @ $(RM) -r -d $(OBJ_PATH)
    @ $(PRINT) "-> Done."  
    @ $(PRINT) "-> Cleaning binary files..."
    @ $(RM) -r -d $(BIN_PATH)
    @ $(PRINT) "-> Done."
    @ $(PRINT) "-> Cleaning auto-generated documentation..."
    @ $(RM) -r -d $(DOC_PATH)
    @ $(PRINT) "-> Done."
    @ $(PRINT) "-> Cleaning code printed for audit..."
    @ $(RM) -r -d $(DOC_PATH) $(AUDIT_PATH)
    @ $(PRINT) "-> Done."
    @ $(PRINT) "-> Cleaning temporary files..."
    @ $(RM) `$(FIND_IN_PROJECT_ROOT) *~` 
    @ $(PRINT) "-> Done."

Wykonywanie warunkowe.

Czasami użytkownik, który odpala plik make, nie ma u siebie zainstalowanego odpowiedniego oprogramowania, żeby zrealizować wszystkie zależności. Na szczęście można dać mu o tym znać, a nawet nie przetwarzać fragmentu, dla którego nie są zainstalowane odpowiednie narzędzia.

W moim pliku make wprowadzę regułę, która będzie sprawdzać, czy odpowiednie narzędzia są zainstalowane. Sprawdzę to za pomocą narzędzia which (za które podstawiona będzie zmienna $(WHICH)), oraz przeszukam ścieżkę lokalną. Zrobię to za pomocą reguły:

check_tools:
    @ if $(WHICH) $(CXX) > /dev/null || [ -f $(CXX) ]; then \
      $(PRINT) "Standard compiler $(CXX) found."; \
    else \
      $(PRINT) "Standard compiler $(CXX) not found."; \
    fi

    @ if $(WHICH) $(A2PS) > /dev/null || [ -f $(A2PS) ]; then \
      $(PRINT) "Code to postscript converter $(A2PS) found."; \
    else \
      $(PRINT) "Code to postscript converter $(A2PS) not found."; \
    fi

    @ if $(WHICH) $(PS2PDF) > /dev/null || [ -f $(PS2PDF) ]; then \
      $(PRINT) "Postscript to pdf converter $(PS2PDF) found."; \
    else \
      $(PRINT) "Postscript to pdf converter $(PS2PDF) not found."; \
    fi

    @ if $(WHICH) $(DOC_GENERATOR) > /dev/null || [ -f $(DOC_GENERATOR) ]; then \
      $(PRINT) "Documentation generator $(DOC_GENERATOR) found."; \
    else \
      $(PRINT) "Documentation generator $(DOC_GENERATOR) not found."; \
    fi

W podobny sposób postawię warunki w regule all. Zależy mi na takim przepływie sterowania:

->jeśli znajdziesz kompilator, to kompiluj
-> jeśli znajdziesz a2ps, to
    ->wygeneruj plik postscript
    -> jeśli znajdziesz ps2pdf to wygeneruj plik pdf
-> jeśli znajdziesz doxygena, to wygeneruj dokumentację.

Dla wygody przedstawię każdy z tych warunków jako wyodrębniony fragment (tak samo z resztą jak z reguły check_tools). Pozwoli mi to na większą przejrzystość w samych regułach. Kompletny plik make po tych zmianach wygląda następująco:

# paths
PROJECT_ROOT = /home/astral/workspace/PlaygroundForCcLinux

SRC_DIR = src
OBJ_DIR = obj
BIN_DIR = bin
DOC_DIR = doc
AUDIT_DIR = audit

SRC_PATH = $(PROJECT_ROOT)/$(SRC_DIR)
OBJ_PATH = $(PROJECT_ROOT)/$(OBJ_DIR)
BIN_PATH = $(PROJECT_ROOT)/$(BIN_DIR)
DOC_PATH = $(PROJECT_ROOT)/$(DOC_DIR)
AUDIT_PATH = $(PROJECT_ROOT)/$(AUDIT_DIR)

# system commands
CXX = g++
RM = rm -f
A2PS = a2ps
PS2PDF = ps2pdf
DOC_GENERATOR = doxygen
MAKE = make
PRINT = echo
CD = cd
MKDIR = mkdir
WHICH = which
FIND_IN_PROJECT_ROOT = find $(PROJECT_ROOT) -name 

#compiler flags
CXXFLAGS = -Wall -pedantic

#a2ps specific flags
A2PS_COLUMNS = 1
A2PS_PRINT_MODE = -R
A2PS_MEDIUM = A4
A2PS_LINE_NUMBERS_STEP = 1
A2PS_LEFT_FOOTER_TEXT = 'Version from: $$D $$C printed on: %C %D'
A2PS_LEFT_TITLE_TEXT = '' 
A2PS_RIGHT_FOOTER_TEXT = '$$p./%p\#'
A2PS_INPUT_FILES = $(SRC_PATH)/*.hh $(SRC_PATH)/*.cc Makefile
A2PS_OUTPUT_FILE = $(AUDIT_PATH)/CodeHandout.ps
A2PS_PRINT_FLAGS = -E -g

A2PS_ARGUMENTS = -M $(A2PS_MEDIUM) \
        $(A2PS_PRINT_MODE) \
        --columns=$(A2PS_COLUMNS) \
        --line-numbers=$(A2PS_LINE_NUMBERS_STEP) \
        --left-footer=$(A2PS_LEFT_FOOTER_TEXT) \
        --left-title=$(A2PS_LEFT_TITLE_TEXT) \
        --right-footer=$(A2PS_RIGHT_FOOTER_TEXT) \
        $(A2PS_PRINT_FLAGS) \
        $(A2PS_INPUT_FILES) \
        -o $(A2PS_OUTPUT_FILE)

# ps2pdf specific flags
PS2PDF_OUTPUT_FILE = $(A2PS_OUTPUT_FILE:.ps=.pdf)

# doxygen specific options
DOXYFILE = $(PROJECT_ROOT)/Doxyfile

# source files names
COMMON_SRC = $(SRC_PATH)/ClassOne.cc $(SRC_PATH)/ClassTwo.cc
TEST_SRC = $(SRC_PATH)/Test.cc
MAIN_SRC = $(SRC_PATH)/Main.cc

# object files names
COMMON_OBJ = $(COMMON_SRC:$(SRC_PATH)/%.cc=$(OBJ_PATH)/%.o)
TEST_OBJ = $(TEST_SRC:$(SRC_PATH)/%.cc=$(OBJ_PATH)/%.o)
MAIN_OBJ = $(MAIN_SRC:$(SRC_PATH)/%.cc=$(OBJ_PATH)/%.o)

# binary files names
TEST_BIN = $(TEST_SRC:$(SRC_PATH)/%.cc=$(BIN_PATH)/%)
MAIN_BIN = $(MAIN_SRC:$(SRC_PATH)/%.cc=$(BIN_PATH)/%)

# 'all' rule is the default one 
.DEFAULT: all

# phony targets 
.PHONY : clean docs obj_path doc_path audit_path bin_path

#defined routines:

define check-tools
    @ if $(WHICH) $(CXX) > /dev/null || [ -f $(CXX) ]; then \
      $(PRINT) "Standard compiler $(CXX) found."; \
    else \
      $(PRINT) "Standard compiler $(CXX) not found."; \
    fi

    @ if $(WHICH) $(A2PS) > /dev/null || [ -f $(A2PS) ]; then \
      $(PRINT) "Code to postscript converter $(A2PS) found."; \
    else \
      $(PRINT) "Code to postscript converter $(A2PS) not found."; \
    fi

    @ if $(WHICH) $(PS2PDF) > /dev/null || [ -f $(PS2PDF) ]; then \
      $(PRINT) "Postscript to pdf converter $(PS2PDF) found."; \
    else \
      $(PRINT) "Postscript to pdf converter $(PS2PDF) not found."; \
    fi

    @ if $(WHICH) $(DOC_GENERATOR) > /dev/null || [ -f $(DOC_GENERATOR) ]; then \
      $(PRINT) "Documentation generator $(DOC_GENERATOR) found."; \
    else \
      $(PRINT) "Documentation generator $(DOC_GENERATOR) not found."; \
    fi
endef

define compile-bin-if-posssible
    @ if $(WHICH) $(CXX) > /dev/null || [ -f $(CXX) ]; then \
        $(PRINT) "-> Building test binary..."; \
        $(MAKE) $(TEST_BIN); \
        $(PRINT) "-> Executing tests..."; \
        if ! $(TEST_BIN); then \
            $(PRINT) "Tests Failed, further building stopped. $$?"; \
            exit 1; \
        fi; \
        $(PRINT) "-> Tests passed."; \
        $(MAKE) $(MAIN_BIN); \
    else \
        $(PRINT) "Since $(CXX) wasn't found, skipping build."; \
    fi
endef

define generate-audit-materials-if-possible
    @ if $(WHICH) $(A2PS) > /dev/null || [ -f $(A2PS) ]; then \
        $(MAKE) $(A2PS_OUTPUT_FILE); \
        if $(WHICH) $(PS2PDF) > /dev/null || [ -f $(PS2PDF) ]; then \
          $(MAKE) $(PS2PDF_OUTPUT_FILE); \
        else \
          $(PRINT) "$(PS2PDF) not found. Skipping PDF generation."; \
        fi; \
    else \
        $(PRINT) "$(A2PS) wasn't found, so neither $(A2PS), nor $(PS2PDF) \
        can be used"; \
    fi
endef

define generate-documentation-if-possible
    @ if $(WHICH) $(DOC_GENERATOR) > /dev/null || [ -f $(DOC_GENERATOR) ]; then \
      $(MAKE) docs; \
    else \
      $(PRINT) "$(DOC_GENERATOR) not found. Skipping documentation\
       generation"; \
    fi
endef

# rules:
all: check_tools
    $(compile-bin-if-posssible)
    $(generate-audit-materials-if-possible)    
    $(generate-documentation-if-possible)    
    @ $(PRINT) "-> Build finished successfully"

$(OBJ_PATH)/%.o: $(SRC_PATH)/%.cc obj_path
    @ $(PRINT) "-> creating $@ out of $<" 
    @ $(CXX) -c $< -o $@

$(MAIN_BIN): $(MAIN_OBJ) $(COMMON_OBJ) bin_path
    @ $(PRINT) "-> linking $(MAIN_OBJ) $(COMMON_OBJ) into $@"
    @ $(CXX) $(TEST_OBJ) $(COMMON_OBJ) $(CXXFLAGS) -o $@

$(TEST_BIN): $(TEST_OBJ) $(COMMON_OBJ) bin_path
    @ $(PRINT) "-> linking $(TEST_OBJ) $(COMMON_OBJ) into $@"
    @ $(CXX) $(TEST_OBJ) $(COMMON_OBJ) $(CXXFLAGS) -o $@

$(A2PS_OUTPUT_FILE): audit_path
    @ $(PRINT) "-> Generating postscript file: $(A2PS_OUTPUT_FILE)"
    @ $(A2PS) $(A2PS_ARGUMENTS)

$(PS2PDF_OUTPUT_FILE): $(A2PS_OUTPUT_FILE)
    @ $(PRINT) "-> Generating pdf file: $(PS2PDF_OUTPUT_FILE)"
    @ $(PS2PDF) $(A2PS_OUTPUT_FILE) $(PS2PDF_OUTPUT_FILE)

$(DOXYFILE):
    @ if [ ! -f $(DOXYFILE) ]; then \
        $(PRINT) "-> $(DOXYFILE) not found. Generating fresh one."; \
        $(DOC_GENERATOR) -g $(DOXYFILE); \
    fi

docs: $(DOXYFILE) doc_path
    @ $(PRINT) "-> Generating documentation out of configuration file: $(DOXYFILE)"
    @ $(CD) $(DOC_PATH) && $(DOC_GENERATOR) $(DOXYFILE)

obj_path: 
    @ if [ ! -d $(OBJ_PATH) ]; then \
        $(PRINT) "-> Creating $(OBJ_DIR) directory..."; \
        $(MKDIR) $(OBJ_PATH); \
    fi

doc_path:
    @ if [ ! -d $(DOC_PATH) ]; then \
        $(PRINT) "-> Creating $(DOC_DIR) directory..."; \
        $(MKDIR) $(DOC_PATH); \
    fi

audit_path:
    @ if [ ! -d $(AUDIT_PATH) ]; then \
        $(PRINT) "-> Creating $(AUDIT_DIR) directory..."; \
        $(MKDIR) $(AUDIT_PATH); \
    fi

bin_path:
    @ if [ ! -d $(BIN_PATH) ]; then \
        $(PRINT) "-> Creating $(BIN_DIR) directory..."; \
        $(MKDIR) $(BIN_PATH); \
    fi

check_tools:
    $(check-tools)

clean:
    @ $(PRINT) "-> Cleaning object files..."
    @ $(RM) -r -d $(OBJ_PATH)
    @ $(PRINT) "-> Done."  
    @ $(PRINT) "-> Cleaning binary files..."
    @ $(RM) -r -d $(BIN_PATH)
    @ $(PRINT) "-> Done."
    @ $(PRINT) "-> Cleaning auto-generated documentation..."
    @ $(RM) -r -d $(DOC_PATH)
    @ $(PRINT) "-> Done."
    @ $(PRINT) "-> Cleaning code printed for audit..."
    @ $(RM) -r -d $(DOC_PATH) $(AUDIT_PATH)
    @ $(PRINT) "-> Done."
    @ $(PRINT) "-> Cleaning temporary files..."
    @ $(RM) `$(FIND_IN_PROJECT_ROOT) *~` 
    @ $(PRINT) "-> Done."

Logowanie

Po odpaleniu powyższego pliku make można mieć wrażenie, że to co pojawia się na ekranie, to jede wielki śmietnik, gdzie przeplatają się komunikaty wypluwane przez make z komunikatami wywoływanych narzędzi. Wygodniej byłoby, gdyby wszystkie szczegóły były logowane do pliku, natomiast sama konsola pokazywała ogólne sprawozdanie z tego co się dzieje.

Żeby dodać logowanie do mojego pliku make, posłużę się najprostszym przekierowaniem strumieni.

Najpierw zdefiniuję ścieżki, które będą potrzebne:

LOG_PATH = $(PROJECT_ROOT)/$(LOG_DIR)

COMPILATION_LOG = $(LOG_PATH)/Compilation.log
TEST_LOG = $(LOG_PATH)/Test.log
AUDIT_LOG = $(LOG_PATH)/Audit.log
DOC_GEN_LOG = $(LOG_PATH)/Documentation.log
SUMMARY_LOG = $(LOG_PATH)/Summary.log

a także przekierowania strumieni, dla większej czytelności:

#stream redirects
COMPILATION_STREAM_REDIRECT = 1>>$(COMPILATION_LOG) 2>&1
TEST_STREAM_REDIRECT = 1>>$(TEST_LOG) 2>&1
AUDIT_STREAM_REDIRECT = 1>>$(AUDIT_LOG) 2>&1
DOC_GEN_STREAM_REDIRECT = 1>>$(DOC_GEN_LOG) 2>&1

Dodam też sprawdzanie wartości zwracanych przez kompilator, generatory i inne narzędzia, żeby w razie czego wypisać przyjazny komunikat, że wszystko się posypało, oraz dopiszę przekierowania do komend, np.

    @ if $(CXX) $(MAIN_OBJ) $(COMMON_OBJ) $(CXXFLAGS) -o $@ \
    $(COMPILATION_STREAM_REDIRECT); then \
        $(PRINT) "-> Linking successful."; \
    else \
        $(PRINT) "-> Linking failed. See $(COMPILATION_LOG) for details"; \
        exit 1; \
    fi

Tworzenie katalogu zamknę w osobnej regule:

log_path:
    @ if [ ! -d $(LOG_PATH) ]; then \
        $(PRINT) "-> Creating $(LOG_DIR) directory..."; \
        $(MKDIR) $(LOG_PATH); \
    fi

i dopiszę ją do listy zależności dla każdej reguły, która wymagać będzie logowania.

Utworzę także log zbiorowy, który przechowywać będzie połączoną zawartość pozostałych logów, oraz dodam regułę czyszczącą logi, która będzie się zawsze wykonywać przed 'all'.

Ostateczny efekt wygląda tak:

# paths
PROJECT_ROOT = /home/astral/workspace/PlaygroundForCcLinux

SRC_DIR = src
OBJ_DIR = obj
BIN_DIR = bin
DOC_DIR = doc
AUDIT_DIR = audit
LOG_DIR = log

SRC_PATH = $(PROJECT_ROOT)/$(SRC_DIR)
OBJ_PATH = $(PROJECT_ROOT)/$(OBJ_DIR)
BIN_PATH = $(PROJECT_ROOT)/$(BIN_DIR)
DOC_PATH = $(PROJECT_ROOT)/$(DOC_DIR)
AUDIT_PATH = $(PROJECT_ROOT)/$(AUDIT_DIR)
LOG_PATH = $(PROJECT_ROOT)/$(LOG_DIR)

COMPILATION_LOG = $(LOG_PATH)/Compilation.log
TEST_LOG = $(LOG_PATH)/Test.log
AUDIT_LOG = $(LOG_PATH)/Audit.log
DOC_GEN_LOG = $(LOG_PATH)/Documentation.log
SUMMARY_LOG = $(LOG_PATH)/Summary.log

# system commands
SHELL = sh
CXX = g++
RM = rm -f
A2PS = a2ps
PS2PDF = ps2pdf
DOC_GENERATOR = doxygen
MAKE = make
PRINT = echo
CD = cd
MKDIR = mkdir
WHICH = which
FIND_IN_PROJECT_ROOT = find $(PROJECT_ROOT) -name 
CAT = cat

#stream redirects
COMPILATION_STREAM_REDIRECT = 1>>$(COMPILATION_LOG) 2>&1
TEST_STREAM_REDIRECT = 1>>$(TEST_LOG) 2>&1
AUDIT_STREAM_REDIRECT = 1>>$(AUDIT_LOG) 2>&1
DOC_GEN_STREAM_REDIRECT = 1>>$(DOC_GEN_LOG) 2>&1

#compiler flags
CXXFLAGS = -Wall -pedantic

#a2ps specific flags
A2PS_COLUMNS = 1
A2PS_PRINT_MODE = -R
A2PS_MEDIUM = A4
A2PS_LINE_NUMBERS_STEP = 1
A2PS_LEFT_FOOTER_TEXT = 'Version from: $$D $$C printed on: %C %D'
A2PS_LEFT_TITLE_TEXT = '' 
A2PS_RIGHT_FOOTER_TEXT = '$$p./%p\#'
A2PS_INPUT_FILES = $(SRC_PATH)/*.hh $(SRC_PATH)/*.cc Makefile
A2PS_OUTPUT_FILE = $(AUDIT_PATH)/CodeHandout.ps
A2PS_PRINT_FLAGS = -E -g

A2PS_ARGUMENTS = -M $(A2PS_MEDIUM) \
        $(A2PS_PRINT_MODE) \
        --columns=$(A2PS_COLUMNS) \
        --line-numbers=$(A2PS_LINE_NUMBERS_STEP) \
        --left-footer=$(A2PS_LEFT_FOOTER_TEXT) \
        --left-title=$(A2PS_LEFT_TITLE_TEXT) \
        --right-footer=$(A2PS_RIGHT_FOOTER_TEXT) \
        $(A2PS_PRINT_FLAGS) \
        $(A2PS_INPUT_FILES) \
        -o $(A2PS_OUTPUT_FILE)

# ps2pdf specific flags
PS2PDF_OUTPUT_FILE = $(A2PS_OUTPUT_FILE:.ps=.pdf)

# doxygen specific options
DOXYFILE = $(PROJECT_ROOT)/Doxyfile

# source files names
COMMON_SRC = $(SRC_PATH)/ClassOne.cc $(SRC_PATH)/ClassTwo.cc
TEST_SRC = $(SRC_PATH)/Test.cc
MAIN_SRC = $(SRC_PATH)/Main.cc

# object files names
COMMON_OBJ = $(COMMON_SRC:$(SRC_PATH)/%.cc=$(OBJ_PATH)/%.o)
TEST_OBJ = $(TEST_SRC:$(SRC_PATH)/%.cc=$(OBJ_PATH)/%.o)
MAIN_OBJ = $(MAIN_SRC:$(SRC_PATH)/%.cc=$(OBJ_PATH)/%.o)

# binary files names
TEST_BIN = $(TEST_SRC:$(SRC_PATH)/%.cc=$(BIN_PATH)/%)
MAIN_BIN = $(MAIN_SRC:$(SRC_PATH)/%.cc=$(BIN_PATH)/%)

# 'all' rule is the default one 
.DEFAULT: all

# phony targets 
.PHONY : clean docs obj_path doc_path audit_path bin_path log_path clean_log

#defined routines:
define check-tools
    @ if $(WHICH) $(CXX) > /dev/null || [ -f $(CXX) ]; then \
      $(PRINT) "-> Standard compiler $(CXX) found."; \
    else \
      $(PRINT) "-> Standard compiler $(CXX) not found."; \
    fi

    @ if $(WHICH) $(A2PS) > /dev/null || [ -f $(A2PS) ]; then \
      $(PRINT) "-> Code to postscript converter $(A2PS) found."; \
    else \
      $(PRINT) "-> Code to postscript converter $(A2PS) not found."; \
    fi

    @ if $(WHICH) $(PS2PDF) > /dev/null || [ -f $(PS2PDF) ]; then \
      $(PRINT) "-> Postscript to pdf converter $(PS2PDF) found."; \
    else \
      $(PRINT) "-> Postscript to pdf converter $(PS2PDF) not found."; \
    fi

    @ if $(WHICH) $(DOC_GENERATOR) > /dev/null || [ -f $(DOC_GENERATOR) ]; then \
      $(PRINT) "-> Documentation generator $(DOC_GENERATOR) found."; \
    else \
      $(PRINT) "-> Documentation generator $(DOC_GENERATOR) not found."; \
    fi
endef

define compile-bin-if-posssible
    @ if $(WHICH) $(CXX) > /dev/null || [ -f $(CXX) ]; then \
        $(PRINT) "-> Building test binary..."; \
        $(MAKE) -s $(TEST_BIN); \
        $(PRINT) "-> Executing tests..."; \
        if ! $(TEST_BIN) $(TEST_STREAM_REDIRECT); then \
            $(PRINT) "Tests Failed, further building stopped. $$?"; \
            exit 1; \
        fi; \
        $(PRINT) "-> Tests passed."; \
        $(MAKE) -s $(MAIN_BIN); \
    else \
        $(PRINT) "Since $(CXX) wasn't found, skipping build."; \
    fi
endef

define generate-audit-materials-if-possible
    @ if $(WHICH) $(A2PS) > /dev/null || [ -f $(A2PS) ]; then \
        if $(MAKE) -s $(A2PS_OUTPUT_FILE); then \
            if $(WHICH) $(PS2PDF) > /dev/null || [ -f $(PS2PDF) ]; then \
              $(MAKE) -s $(PS2PDF_OUTPUT_FILE); \
            else \
              $(PRINT) "$(PS2PDF) not found. Skipping PDF generation."; \
            fi; \
        fi; \
    else \
        $(PRINT) "$(A2PS) wasn't found, so neither $(A2PS), nor $(PS2PDF) \
        can be used"; \
    fi
endef

define generate-documentation-if-possible
    @ if $(WHICH) $(DOC_GENERATOR) > /dev/null || [ -f $(DOC_GENERATOR) ]; then \
      $(MAKE) -s docs; \
    else \
      $(PRINT) "$(DOC_GENERATOR) not found. Skipping documentation\
       generation"; \
    fi
endef

# rules:
all:
    @ $(PRINT) "------------------------------------------------"
    @ $(PRINT) "------------- AVAILABLE TOOLS INFO -------------"
    @ $(PRINT) "------------------------------------------------"
    @ $(MAKE) -s check_tools
    @ $(PRINT) "------------------------------------------------"
    @ $(PRINT) "-------------- PREPARATIONS INFO ---------------"
    @ $(PRINT) "------------------------------------------------"
    @ $(MAKE) -s clean_log
    @ $(MAKE) -s log_path
    @ $(PRINT) "------------------------------------------------"
    @ $(PRINT) "-------------- BINARY BUILD INFO ---------------"
    @ $(PRINT) "------------------------------------------------"
    $(compile-bin-if-posssible)
    @ $(PRINT) "------------------------------------------------"
    @ $(PRINT) "------------- AUDIT MATERIALS INFO -------------"
    @ $(PRINT) "------------------------------------------------"
    $(generate-audit-materials-if-possible)    
    @ $(PRINT) "------------------------------------------------"
    @ $(PRINT) "------------- DOCS GENERATION INFO -------------"
    @ $(PRINT) "------------------------------------------------"
    $(generate-documentation-if-possible)
    @ $(PRINT) "Summary Build log" > $(SUMMARY_LOG)
    @ $(PRINT) "Started: `date`" >> $(SUMMARY_LOG)
    @ $(PRINT) "COMPILATION LOG" >> $(SUMMARY_LOG)
    @ $(CAT) $(COMPILATION_LOG) >> $(SUMMARY_LOG)
    @ $(PRINT) "AUDIT MATERIALS GENERATION LOG" >> $(SUMMARY_LOG)
    @ $(CAT) $(AUDIT_LOG) >> $(SUMMARY_LOG)
    @ $(PRINT) "DOCUMENTATION GENERATION LOG" >> $(SUMMARY_LOG)
    @ $(CAT) $(DOC_GEN_LOG) >> $(SUMMARY_LOG)
    @ $(PRINT) "------------------------------------------------"
    @ $(PRINT) "------------------- RESULT ---------------------"
    @ $(PRINT) "------------------------------------------------"
    @ $(PRINT) "-> Build finished successfully."
    @ $(PRINT) "-> Check $(SUMMARY_LOG) for details."
    @ $(PRINT) "------------------------------------------------"
    @ $(PRINT) "-------------------- END -----------------------"
    @ $(PRINT) "------------------------------------------------"

$(OBJ_PATH)/%.o: $(SRC_PATH)/%.cc obj_path log_path
    @ $(PRINT) "-> Creating: \n$@ \nout of: \n$<" 
    @ if $(CXX) -c $< -o $@ $(COMPILATION_STREAM_REDIRECT); then \
        $(PRINT) "-> Compilation successful."; \
    else \
        $(PRINT) "-> Compilation failed. See $(COMPILATION_LOG) for details"; \
        exit 1; \
    fi

$(MAIN_BIN): $(MAIN_OBJ) $(COMMON_OBJ) bin_path log_path
    @ $(PRINT) "-> Linking: \n$(TEST_OBJ) $(COMMON_OBJ) \ninto: \n$@"
    @ if $(CXX) $(MAIN_OBJ) $(COMMON_OBJ) $(CXXFLAGS) -o $@ \
    $(COMPILATION_STREAM_REDIRECT); then \
        $(PRINT) "-> Linking successful."; \
    else \
        $(PRINT) "-> Linking failed. See $(COMPILATION_LOG) for details"; \
        exit 1; \
    fi

$(TEST_BIN): $(TEST_OBJ) $(COMMON_OBJ) bin_path log_path
    @ $(PRINT) "-> Linking: \n$(TEST_OBJ) $(COMMON_OBJ) \ninto: \n$@"
    @ if $(CXX) $(TEST_OBJ) $(COMMON_OBJ) $(CXXFLAGS) -o $@ \
    $(COMPILATION_STREAM_REDIRECT); then \
        $(PRINT) "-> Linking successful."; \
    else \
        $(PRINT) "-> Linking failed. See $(COMPILATION_LOG) for details"; \
        exit 1; \
    fi

$(A2PS_OUTPUT_FILE): audit_path log_path
    @ $(PRINT) "-> Generating postscript file: $(A2PS_OUTPUT_FILE)..."
    @ if $(A2PS) $(A2PS_ARGUMENTS) $(AUDIT_STREAM_REDIRECT); then \
        $(PRINT) "-> Postscript generation successful."; \
    else \
        $(PRINT) "-> Couldn't generate postscript file."; \
        $(PRINT) "-> Check $(AUDIT_LOG) for details"; \
        exit 1; \
    fi

$(PS2PDF_OUTPUT_FILE): log_path
    @ $(PRINT) "-> Generating pdf file: $(PS2PDF_OUTPUT_FILE)"
    @ if $(PS2PDF) $(A2PS_OUTPUT_FILE) $(PS2PDF_OUTPUT_FILE) \
    $(AUDIT_STREAM_REDIRECT); then \
        $(PRINT) "-> PDF generation successful."; \
    else \
        $(PRINT) "-> Couldn't generate PDF file."; \
        $(PRINT) "-> Check $(AUDIT_LOG) for details"; \
        exit 1; \
    fi

$(DOXYFILE): log_path
    @ if [ ! -f $(DOXYFILE) ]; then \
        $(PRINT) "-> $(DOXYFILE) not found. Generating fresh one."; \
        if $(DOC_GENERATOR) -g $(DOXYFILE) $(DOC_GEN_STREAM_REDIRECT); then \
            $(PRINT) "-> Successfully generated $(DOXYFILE)."; \
        else \
            $(PRINT) "-> Couldn't generate $(DOXYFILE)."; \
            $(PRINT) "Check $(DOC_GEN_LOG) for details."; \
        fi; \
    else \
        $(PRINT) "-> $(DOXYFILE) found. Using it as configuration file."; \
    fi     

docs: $(DOXYFILE) doc_path log_path
    @ $(PRINT) "-> Generating documentation out of configuration file: $(DOXYFILE)"
    @ if $(CD) $(DOC_PATH) && $(DOC_GENERATOR) $(DOXYFILE) \
        $(DOC_GEN_STREAM_REDIRECT); then \
        $(PRINT) "-> Documentation successfully generated to $(DOC_PATH)."; \
    else \
        $(PRINT) "-> Couldn't generate documentation."; \
        $(PRINT) "-> Check $(DOC_GEN_LOG) for details"; \
    fi

obj_path: 
    @ if [ ! -d $(OBJ_PATH) ]; then \
        $(PRINT) "-> Creating $(OBJ_DIR) directory..."; \
        $(MKDIR) $(OBJ_PATH); \
    fi

doc_path:
    @ if [ ! -d $(DOC_PATH) ]; then \
        $(PRINT) "-> Creating $(DOC_DIR) directory..."; \
        $(MKDIR) $(DOC_PATH); \
    fi

audit_path:
    @ if [ ! -d $(AUDIT_PATH) ]; then \
        $(PRINT) "-> Creating $(AUDIT_DIR) directory..."; \
        $(MKDIR) $(AUDIT_PATH); \
    fi

bin_path:
    @ if [ ! -d $(BIN_PATH) ]; then \
        $(PRINT) "-> Creating $(BIN_DIR) directory..."; \
        $(MKDIR) $(BIN_PATH); \
    fi

log_path:
    @ if [ ! -d $(LOG_PATH) ]; then \
        $(PRINT) "-> Creating $(LOG_DIR) directory..."; \
        $(MKDIR) $(LOG_PATH); \
    fi

check_tools:
    $(check-tools)

clean_log:
    @ $(PRINT) "-> Cleaning log files..."
    @ $(RM) -r -d $(LOG_PATH)
    @ $(PRINT) "-> Done."  

clean:
    @ $(PRINT) "-> Cleaning object files..."
    @ $(RM) -r -d $(OBJ_PATH)
    @ $(PRINT) "-> Done."  
    @ $(MAKE) -s clean_log
    @ $(PRINT) "-> Cleaning binary files..."
    @ $(RM) -r -d $(BIN_PATH)
    @ $(PRINT) "-> Done."
    @ $(PRINT) "-> Cleaning auto-generated documentation..."
    @ $(RM) -r -d $(DOC_PATH)
    @ $(PRINT) "-> Done."
    @ $(PRINT) "-> Cleaning code printed for audit..."
    @ $(RM) -r -d $(DOC_PATH) $(AUDIT_PATH)
    @ $(PRINT) "-> Done."
    @ $(PRINT) "-> Cleaning temporary files..."
    @ $(RM) `$(FIND_IN_PROJECT_ROOT) *~` 
    @ $(PRINT) "-> Done."

Ostatnie szlify

"Kiedy powiem sobie dość… a ja wiem, że to już niedługo" - śpiewała Agnieszka Chylińska do muzyki łysego. Mógłbym mój plik make ulepszać jeszcze długo, dodając automatyzacje nowych rzeczy (np. bardzo jedną ważną - kontrolę wersji) i dopieszczając to co istnieje, ale doszedłem do wniosku, że powiem sobie "dość" w tym momencie. Wydaje mi się, że idea, którą chciałem się podzielić jest dość jasna po przejrzeniu tego artykułu.

Na sam koniec warto się jeszcze upewnić, że dostaniemy świeże pakiety z plikami binarnymi oraz źródłami (np. do wystawienia na serwerze, by ktoś mógł sobie je ściągnąć). W tym celu wprowadzę regułę 'final':

final: all release_path
    @ $(PRINT) "------------------------------------------------"
    @ $(PRINT) "---------- POST-BUILD FINAL TOUCH --------------"
    @ $(PRINT) "------------------------------------------------"
    @ $(PRINT) "Backing-up debug versions..."
    @ $(COPY) $(TEST_BIN) $(DEBUG_TEST_BIN)
    @ $(COPY) $(MAIN_BIN) $(DEBUG_MAIN_BIN)
    @ $(PRINT) "Preparing release binaries versions..."
    @ $(STRIP) $(TEST_BIN)
    @ $(STRIP) $(MAIN_BIN)
    @ $(PRINT) "Preparing release source package..."
    @ $(TAR) $(TAR_PACK_FLAGS) $(RELEASE_PATH)/src_`$(DATE)`.tar.gz \
    $(SRC_PATH) $(MAKEFILE) $(DOXYFILE) 1>/dev/null 2>&1
    @ $(PRINT) "Preparing release binary package..."
    @ cd $(BIN_PATH) && $(TAR) $(TAR_PACK_FLAGS) \
    $(RELEASE_PATH)/bin_`$(DATE)`.tar.gz $(MAIN_BIN_NAME) 1>/dev/null 2>&1   
    @ $(PRINT) "------------------------------------------------"
    @ $(PRINT) "------------- END OF FINAL TOUCH ---------------"
    @ $(PRINT) "------------------------------------------------"

Jak widać, reguła ta ma na liście zależności regułę release_path, która, jak inne reguły zakończone słówkiem "path", tworzy odpowiedni katalog.

Działanie reguły 'final' polega na utworzeniu kopii zapasowych utworzonych plików binarnych, odarcie tych plików z symboli debugowania, oraz spakowanie źródeł i plików binarnych i umieszczenie ich w odpowiednim katalogu (można by do tego dodać automatyczne wysłanie na ftp i inne tego typu bajery).

Ostateczny kod tego pliku (nie pozbawiony paru wad, ale pokazujący to co trzeba) przedstawia się następująco:

# paths
PROJECT_ROOT = /home/astral/workspace/PlaygroundForCcLinux

SRC_DIR = src
OBJ_DIR = obj
BIN_DIR = bin
DOC_DIR = doc
AUDIT_DIR = audit
LOG_DIR = log
RELEASE_DIR = release

SRC_PATH = $(PROJECT_ROOT)/$(SRC_DIR)
OBJ_PATH = $(PROJECT_ROOT)/$(OBJ_DIR)
BIN_PATH = $(PROJECT_ROOT)/$(BIN_DIR)
DOC_PATH = $(PROJECT_ROOT)/$(DOC_DIR)
AUDIT_PATH = $(PROJECT_ROOT)/$(AUDIT_DIR)
LOG_PATH = $(PROJECT_ROOT)/$(LOG_DIR)
RELEASE_PATH = $(PROJECT_ROOT)/$(RELEASE_DIR)

MAKEFILE = $(PROJECT_ROOT)/Makefile

COMPILATION_LOG = $(LOG_PATH)/Compilation.log
TEST_LOG = $(LOG_PATH)/Test.log
AUDIT_LOG = $(LOG_PATH)/Audit.log
DOC_GEN_LOG = $(LOG_PATH)/Documentation.log
SUMMARY_LOG = $(LOG_PATH)/Summary.log

# system commands
SHELL = sh
CXX = g++
RM = rm -f
A2PS = a2ps
PS2PDF = ps2pdf
DOC_GENERATOR = doxygen
MAKE = make
PRINT = echo
CD = cd
COPY = cp
MKDIR = mkdir
WHICH = which
FIND_IN_PROJECT_ROOT = find $(PROJECT_ROOT) -name 
CAT = cat
STRIP = strip
TAR = tar
DATE = date +%F_%H%M

#stream redirects
COMPILATION_STREAM_REDIRECT = 1>>$(COMPILATION_LOG) 2>&1
TEST_STREAM_REDIRECT = 1>>$(TEST_LOG) 2>&1
AUDIT_STREAM_REDIRECT = 1>>$(AUDIT_LOG) 2>&1
DOC_GEN_STREAM_REDIRECT = 1>>$(DOC_GEN_LOG) 2>&1

#compiler flags
CXXFLAGS = -Wall -pedantic

#a2ps specific flags
A2PS_COLUMNS = 1
A2PS_PRINT_MODE = -R
A2PS_MEDIUM = A4
A2PS_LINE_NUMBERS_STEP = 1
A2PS_LEFT_FOOTER_TEXT = 'Version from: $$D $$C printed on: %C %D'
A2PS_LEFT_TITLE_TEXT = '' 
A2PS_RIGHT_FOOTER_TEXT = '$$p./%p\#'
A2PS_INPUT_FILES = $(SRC_PATH)/*.hh $(SRC_PATH)/*.cc Makefile
A2PS_OUTPUT_FILE = $(AUDIT_PATH)/CodeHandout.ps
A2PS_PRINT_FLAGS = -E -g

A2PS_ARGUMENTS = -M $(A2PS_MEDIUM) \
        $(A2PS_PRINT_MODE) \
        --columns=$(A2PS_COLUMNS) \
        --line-numbers=$(A2PS_LINE_NUMBERS_STEP) \
        --left-footer=$(A2PS_LEFT_FOOTER_TEXT) \
        --left-title=$(A2PS_LEFT_TITLE_TEXT) \
        --right-footer=$(A2PS_RIGHT_FOOTER_TEXT) \
        $(A2PS_PRINT_FLAGS) \
        $(A2PS_INPUT_FILES) \
        -o $(A2PS_OUTPUT_FILE)

# ps2pdf specific flags
PS2PDF_OUTPUT_FILE = $(A2PS_OUTPUT_FILE:.ps=.pdf)

# doxygen specific options
DOXYFILE = $(PROJECT_ROOT)/Doxyfile

#tar specific flags
TAR_PACK_FLAGS = -chvvf

# source files names
COMMON_SRC = $(SRC_PATH)/ClassOne.cc $(SRC_PATH)/ClassTwo.cc
TEST_SRC = $(SRC_PATH)/Test.cc
MAIN_SRC = $(SRC_PATH)/Main.cc

MAIN_BIN_NAME = Main
TEST_BIN_NAME = Test 

# object files names
COMMON_OBJ = $(COMMON_SRC:$(SRC_PATH)/%.cc=$(OBJ_PATH)/%.o)
TEST_OBJ = $(TEST_SRC:$(SRC_PATH)/%.cc=$(OBJ_PATH)/%.o)
MAIN_OBJ = $(MAIN_SRC:$(SRC_PATH)/%.cc=$(OBJ_PATH)/%.o)

# binary files names
TEST_BIN = $(TEST_SRC:$(SRC_PATH)/%.cc=$(BIN_PATH)/$(MAIN_BIN_NAME))
MAIN_BIN = $(MAIN_SRC:$(SRC_PATH)/%.cc=$(BIN_PATH)/$(TEST_BIN_NAME))
DEBUG_TEST_BIN = $(TEST_BIN:%=%Debug)
DEBUG_MAIN_BIN = $(MAIN_BIN:%=%Debug)

# 'all' rule is the default one 
.DEFAULT: all

# phony targets 
.PHONY : clean docs obj_path doc_path audit_path bin_path log_path \
release_path clean_log final 

#defined routines:

define check-tools
    @ if $(WHICH) $(CXX) > /dev/null || [ -f $(CXX) ]; then \
      $(PRINT) "-> Standard compiler $(CXX) found."; \
    else \
      $(PRINT) "-> Standard compiler $(CXX) not found."; \
    fi

    @ if $(WHICH) $(A2PS) > /dev/null || [ -f $(A2PS) ]; then \
      $(PRINT) "-> Code to postscript converter $(A2PS) found."; \
    else \
      $(PRINT) "-> Code to postscript converter $(A2PS) not found."; \
    fi

    @ if $(WHICH) $(PS2PDF) > /dev/null || [ -f $(PS2PDF) ]; then \
      $(PRINT) "-> Postscript to pdf converter $(PS2PDF) found."; \
    else \
      $(PRINT) "-> Postscript to pdf converter $(PS2PDF) not found."; \
    fi

    @ if $(WHICH) $(DOC_GENERATOR) > /dev/null || [ -f $(DOC_GENERATOR) ]; then \
      $(PRINT) "-> Documentation generator $(DOC_GENERATOR) found."; \
    else \
      $(PRINT) "-> Documentation generator $(DOC_GENERATOR) not found."; \
    fi
endef

define compile-bin-if-posssible
    @ if $(WHICH) $(CXX) > /dev/null || [ -f $(CXX) ]; then \
        $(PRINT) "-> Building test binary..."; \
        $(MAKE) -s $(TEST_BIN); \
        $(PRINT) "-> Executing tests..."; \
        if ! $(TEST_BIN) $(TEST_STREAM_REDIRECT); then \
            $(PRINT) "Tests Failed, further building stopped. $$?"; \
            exit 1; \
        fi; \
        $(PRINT) "-> Tests passed."; \
        $(MAKE) -s $(MAIN_BIN); \
    else \
        $(PRINT) "Since $(CXX) wasn't found, skipping build."; \
    fi
endef

define generate-audit-materials-if-possible
    @ if $(WHICH) $(A2PS) > /dev/null || [ -f $(A2PS) ]; then \
        if $(MAKE) -s $(A2PS_OUTPUT_FILE); then \
            if $(WHICH) $(PS2PDF) > /dev/null || [ -f $(PS2PDF) ]; then \
              $(MAKE) -s $(PS2PDF_OUTPUT_FILE); \
            else \
              $(PRINT) "$(PS2PDF) not found. Skipping PDF generation."; \
            fi; \
        fi; \
    else \
        $(PRINT) "$(A2PS) wasn't found, so neither $(A2PS), nor $(PS2PDF) \
        can be used"; \
    fi
endef

define generate-documentation-if-possible
    @ if $(WHICH) $(DOC_GENERATOR) > /dev/null || [ -f $(DOC_GENERATOR) ]; then \
      $(MAKE) -s docs; \
    else \
      $(PRINT) "$(DOC_GENERATOR) not found. Skipping documentation\
       generation"; \
    fi
endef

# rules:
all:
    @ $(PRINT) "------------------------------------------------"
    @ $(PRINT) "------------- AVAILABLE TOOLS INFO -------------"
    @ $(PRINT) "------------------------------------------------"
    @ $(MAKE) -s check_tools
    @ $(PRINT) "------------------------------------------------"
    @ $(PRINT) "-------------- PREPARATIONS INFO ---------------"
    @ $(PRINT) "------------------------------------------------"
    @ $(MAKE) -s clean_log
    @ $(MAKE) -s log_path
    @ $(PRINT) "------------------------------------------------"
    @ $(PRINT) "-------------- BINARY BUILD INFO ---------------"
    @ $(PRINT) "------------------------------------------------"
    $(compile-bin-if-posssible)
    @ $(PRINT) "------------------------------------------------"
    @ $(PRINT) "------------- AUDIT MATERIALS INFO -------------"
    @ $(PRINT) "------------------------------------------------"
    $(generate-audit-materials-if-possible)    
    @ $(PRINT) "------------------------------------------------"
    @ $(PRINT) "------------- DOCS GENERATION INFO -------------"
    @ $(PRINT) "------------------------------------------------"
    $(generate-documentation-if-possible)
    @ $(PRINT) "Summary Build log" > $(SUMMARY_LOG)
    @ $(PRINT) "Started: `date`" >> $(SUMMARY_LOG)
    @ $(PRINT) "COMPILATION LOG" >> $(SUMMARY_LOG)
    @ $(CAT) $(COMPILATION_LOG) >> $(SUMMARY_LOG)
    @ $(PRINT) "AUDIT MATERIALS GENERATION LOG" >> $(SUMMARY_LOG)
    @ $(CAT) $(AUDIT_LOG) >> $(SUMMARY_LOG)
    @ $(PRINT) "DOCUMENTATION GENERATION LOG" >> $(SUMMARY_LOG)
    @ $(CAT) $(DOC_GEN_LOG) >> $(SUMMARY_LOG)
    @ $(PRINT) "------------------------------------------------"
    @ $(PRINT) "------------------- RESULT ---------------------"
    @ $(PRINT) "------------------------------------------------"
    @ $(PRINT) "-> Build finished successfully."
    @ $(PRINT) "-> Check $(SUMMARY_LOG) for details."
    @ $(PRINT) "------------------------------------------------"
    @ $(PRINT) "-------------------- END -----------------------"
    @ $(PRINT) "------------------------------------------------"

final: all
    @ $(PRINT) "------------------------------------------------"
    @ $(PRINT) "---------- POST-BUILD FINAL TOUCH --------------"
    @ $(PRINT) "------------------------------------------------"
    @ $(MAKE) -s release_path
    @ $(PRINT) "-> Backing-up debug versions..."
    @ $(COPY) $(TEST_BIN) $(DEBUG_TEST_BIN)
    @ $(COPY) $(MAIN_BIN) $(DEBUG_MAIN_BIN)
    @ $(PRINT) "-> Preparing binaries release versions..."
    @ $(STRIP) $(TEST_BIN)
    @ $(STRIP) $(MAIN_BIN)
    @ $(PRINT) "-> Preparing release source package..."
    @ $(TAR) $(TAR_PACK_FLAGS) $(RELEASE_PATH)/src_`$(DATE)`.tar.gz \
    $(SRC_PATH) $(MAKEFILE) $(DOXYFILE) 1>/dev/null 2>&1
    @ $(PRINT) "-> Preparing release binary package..."
    @ cd $(BIN_PATH) && $(TAR) $(TAR_PACK_FLAGS) \
    $(RELEASE_PATH)/bin_`$(DATE)`.tar.gz $(MAIN_BIN_NAME) 1>/dev/null 2>&1
    @ $(PRINT) "-> Packages created in $(RELEASE_PATH)."
    @ $(PRINT) "------------------------------------------------"
    @ $(PRINT) "------------- END OF FINAL TOUCH ---------------"
    @ $(PRINT) "------------------------------------------------"

$(OBJ_PATH)/%.o: $(SRC_PATH)/%.cc obj_path log_path
    @ $(PRINT) "-> Creating: \n$@ \nout of: \n$<" 
    @ if $(CXX) -c $< -o $@ $(COMPILATION_STREAM_REDIRECT); then \
        $(PRINT) "-> Compilation successful."; \
    else \
        $(PRINT) "-> Compilation failed. See $(COMPILATION_LOG) for details"; \
        exit 1; \
    fi

$(MAIN_BIN): $(MAIN_OBJ) $(COMMON_OBJ) bin_path log_path
    @ $(PRINT) "-> Linking: \n$(TEST_OBJ) $(COMMON_OBJ) \ninto: \n$@"
    @ if $(CXX) $(MAIN_OBJ) $(COMMON_OBJ) $(CXXFLAGS) -o $@ \
    $(COMPILATION_STREAM_REDIRECT); then \
        $(PRINT) "-> Linking successful."; \
    else \
        $(PRINT) "-> Linking failed. See $(COMPILATION_LOG) for details"; \
        exit 1; \
    fi

$(TEST_BIN): $(TEST_OBJ) $(COMMON_OBJ) bin_path log_path
    @ $(PRINT) "-> Linking: \n$(TEST_OBJ) $(COMMON_OBJ) \ninto: \n$@"
    @ if $(CXX) $(TEST_OBJ) $(COMMON_OBJ) $(CXXFLAGS) -o $@ \
    $(COMPILATION_STREAM_REDIRECT); then \
        $(PRINT) "-> Linking successful."; \
    else \
        $(PRINT) "-> Linking failed. See $(COMPILATION_LOG) for details"; \
        exit 1; \
    fi

$(A2PS_OUTPUT_FILE): audit_path log_path
    @ $(PRINT) "-> Generating postscript file: $(A2PS_OUTPUT_FILE)..."
    @ if $(A2PS) $(A2PS_ARGUMENTS) $(AUDIT_STREAM_REDIRECT); then \
        $(PRINT) "-> Postscript generation successful."; \
    else \
        $(PRINT) "-> Couldn't generate postscript file."; \
        $(PRINT) "-> Check $(AUDIT_LOG) for details"; \
        exit 1; \
    fi

$(PS2PDF_OUTPUT_FILE): log_path
    @ $(PRINT) "-> Generating pdf file: $(PS2PDF_OUTPUT_FILE)"
    @ if $(PS2PDF) $(A2PS_OUTPUT_FILE) $(PS2PDF_OUTPUT_FILE) \
    $(AUDIT_STREAM_REDIRECT); then \
        $(PRINT) "-> PDF generation successful."; \
    else \
        $(PRINT) "-> Couldn't generate PDF file."; \
        $(PRINT) "-> Check $(AUDIT_LOG) for details"; \
        exit 1; \
    fi

$(DOXYFILE): log_path
    @ if [ ! -f $(DOXYFILE) ]; then \
        $(PRINT) "-> $(DOXYFILE) not found. Generating fresh one."; \
        if $(DOC_GENERATOR) -g $(DOXYFILE) $(DOC_GEN_STREAM_REDIRECT); then \
            $(PRINT) "-> Successfully generated $(DOXYFILE)."; \
        else \
            $(PRINT) "-> Couldn't generate $(DOXYFILE)."; \
            $(PRINT) "Check $(DOC_GEN_LOG) for details."; \
        fi; \
    else \
        $(PRINT) "-> $(DOXYFILE) found. Using it as configuration file."; \
    fi     

docs: $(DOXYFILE) doc_path log_path
    @ $(PRINT) "-> Generating documentation out of configuration file: $(DOXYFILE)"
    @ if $(CD) $(DOC_PATH) && $(DOC_GENERATOR) $(DOXYFILE) \
        $(DOC_GEN_STREAM_REDIRECT); then \
        $(PRINT) "-> Documentation successfully generated to $(DOC_PATH)."; \
    else \
        $(PRINT) "-> Couldn't generate documentation."; \
        $(PRINT) "-> Check $(DOC_GEN_LOG) for details"; \
    fi

obj_path: 
    @ if [ ! -d $(OBJ_PATH) ]; then \
        $(PRINT) "-> Creating $(OBJ_DIR) directory..."; \
        $(MKDIR) $(OBJ_PATH); \
    fi

doc_path:
    @ if [ ! -d $(DOC_PATH) ]; then \
        $(PRINT) "-> Creating $(DOC_DIR) directory..."; \
        $(MKDIR) $(DOC_PATH); \
    fi

audit_path:
    @ if [ ! -d $(AUDIT_PATH) ]; then \
        $(PRINT) "-> Creating $(AUDIT_DIR) directory..."; \
        $(MKDIR) $(AUDIT_PATH); \
    fi

bin_path:
    @ if [ ! -d $(BIN_PATH) ]; then \
        $(PRINT) "-> Creating $(BIN_DIR) directory..."; \
        $(MKDIR) $(BIN_PATH); \
    fi

log_path:
    @ if [ ! -d $(LOG_PATH) ]; then \
        $(PRINT) "-> Creating $(LOG_DIR) directory..."; \
        $(MKDIR) $(LOG_PATH); \
    fi

release_path:
    @ if [ ! -d $(RELEASE_PATH) ]; then \
        $(PRINT) "-> Creating $(RELEASE_DIR) directory..."; \
        $(MKDIR) $(RELEASE_PATH); \
    fi

check_tools:
    $(check-tools)

clean_log:
    @ $(PRINT) "-> Cleaning log files..."
    @ $(RM) -r -d $(LOG_PATH)
    @ $(PRINT) "-> Done."  

clean:
    @ $(PRINT) "-> Cleaning object files..."
    @ $(RM) -r -d $(OBJ_PATH)
    @ $(PRINT) "-> Done."  
    @ $(MAKE) -s clean_log
    @ $(PRINT) "-> Cleaning binary files..."
    @ $(RM) -r -d $(BIN_PATH)
    @ $(PRINT) "-> Done."
    @ $(PRINT) "-> Cleaning release archives..."
    @ $(RM) -r -d $(RELEASE_PATH)
    @ $(PRINT) "-> Done."
    @ $(PRINT) "-> Cleaning auto-generated documentation..."
    @ $(RM) -r -d $(DOC_PATH)
    @ $(PRINT) "-> Done."
    @ $(PRINT) "-> Cleaning code printed for audit..."
    @ $(RM) -r -d $(DOC_PATH) $(AUDIT_PATH)
    @ $(PRINT) "-> Done."
    @ $(PRINT) "-> Cleaning temporary files..."
    @ $(RM) `$(FIND_IN_PROJECT_ROOT) *~` 
    @ $(PRINT) "-> Done."

który wypluwa na standardowe wyjście następujący, przyjemny tekst:

------------------------------------------------
------------- AVAILABLE TOOLS INFO -------------
------------------------------------------------
-> Standard compiler g++ found.
-> Code to postscript converter a2ps found.
-> Postscript to pdf converter ps2pdf found.
-> Documentation generator doxygen found.
------------------------------------------------
-------------- PREPARATIONS INFO ---------------
------------------------------------------------
-> Cleaning log files...
-> Done.
-> Creating log directory...
------------------------------------------------
-------------- BINARY BUILD INFO ---------------
------------------------------------------------
-> Building test binary...
-> Creating obj directory...
-> Creating: 
/home/astral/workspace/PlaygroundForCcLinux/obj/Test.o 
out of: 
/home/astral/workspace/PlaygroundForCcLinux/src/Test.cc
-> Compilation successful.
-> Creating: 
/home/astral/workspace/PlaygroundForCcLinux/obj/ClassOne.o 
out of: 
/home/astral/workspace/PlaygroundForCcLinux/src/ClassOne.cc
-> Compilation successful.
-> Creating: 
/home/astral/workspace/PlaygroundForCcLinux/obj/ClassTwo.o 
out of: 
/home/astral/workspace/PlaygroundForCcLinux/src/ClassTwo.cc
-> Compilation successful.
-> Creating bin directory...
-> Linking: 
/home/astral/workspace/PlaygroundForCcLinux/obj/Test.o /home/astral/workspace/PlaygroundForCcLinux/obj/ClassOne.o /home/astral/workspace/PlaygroundForCcLinux/obj/ClassTwo.o 
into: 
/home/astral/workspace/PlaygroundForCcLinux/bin/Main
-> Linking successful.
-> Executing tests...
-> Tests passed.
-> Creating: 
/home/astral/workspace/PlaygroundForCcLinux/obj/Main.o 
out of: 
/home/astral/workspace/PlaygroundForCcLinux/src/Main.cc
-> Compilation successful.
-> Creating: 
/home/astral/workspace/PlaygroundForCcLinux/obj/ClassOne.o 
out of: 
/home/astral/workspace/PlaygroundForCcLinux/src/ClassOne.cc
-> Compilation successful.
-> Creating: 
/home/astral/workspace/PlaygroundForCcLinux/obj/ClassTwo.o 
out of: 
/home/astral/workspace/PlaygroundForCcLinux/src/ClassTwo.cc
-> Compilation successful.
-> Linking: 
/home/astral/workspace/PlaygroundForCcLinux/obj/Test.o /home/astral/workspace/PlaygroundForCcLinux/obj/ClassOne.o /home/astral/workspace/PlaygroundForCcLinux/obj/ClassTwo.o 
into: 
/home/astral/workspace/PlaygroundForCcLinux/bin/Test
-> Linking successful.
------------------------------------------------
------------- AUDIT MATERIALS INFO -------------
------------------------------------------------
-> Creating audit directory...
-> Generating postscript file: /home/astral/workspace/PlaygroundForCcLinux/audit/CodeHandout.ps...
-> Postscript generation successful.
-> Generating pdf file: /home/astral/workspace/PlaygroundForCcLinux/audit/CodeHandout.pdf
-> PDF generation successful.
------------------------------------------------
------------- DOCS GENERATION INFO -------------
------------------------------------------------
-> /home/astral/workspace/PlaygroundForCcLinux/Doxyfile found. Using it as configuration file.
-> Creating doc directory...
-> Generating documentation out of configuration file: /home/astral/workspace/PlaygroundForCcLinux/Doxyfile
-> Documentation successfully generated to /home/astral/workspace/PlaygroundForCcLinux/doc.
------------------------------------------------
------------------- RESULT ---------------------
------------------------------------------------
-> Build finished successfully.
-> Check /home/astral/workspace/PlaygroundForCcLinux/log/Summary.log for details.
------------------------------------------------
-------------------- END -----------------------
------------------------------------------------
------------------------------------------------
---------- POST-BUILD FINAL TOUCH --------------
------------------------------------------------
-> Creating release directory...
-> Backing-up debug versions...
-> Preparing binaries release versions...
-> Preparing release source package...
-> Preparing release binary package...
-> Packages created in /home/astral/workspace/PlaygroundForCcLinux/release.
------------------------------------------------
------------- END OF FINAL TOUCH ---------------
------------------------------------------------

Literatura:

- Porady pragmatycznych programistów
- Manual GNU Make
- Advanced bash scripting guide
- Manuale do poszczególnych narzędzi użytych w pliku make.

O ile nie zaznaczono inaczej, treść tej strony objęta jest licencją Creative Commons Attribution-Share Alike 2.5 License.