CMake w praktyce
Poprzednim razem zaprezentowałem CMake od strony teoretycznej, nie opierając wiedzy na żadnym konkretnym przykładzie. W dzisiejszym wpisie przedstawię CMake od strony praktycznej. Celem dzisiejszego wpisu jest stworzenie projektu korzystającego z bibliotek SFML oraz GoogleTest.
Zaczynamy! :)
Założenia
- Działamy na systemie MacOS
- Korzystamy z najnowszych wersji bibliotek załączanych do projektu
- Jeżeli jakikolwiek test w projekcie nie przechodzi, nie zostanie zbudowany plik z programem
- W niniejszym wpisie wykorzystujemy wiedzę zawartą we wcześniejszym wpisie
Kompilacja SFML
SFML jest jedną z najbardziej popularnych bibliotek do tworzenia programów wykorzystujących grafikę 2D. Stronę projektu możecie odwiedzić wchodząc w link http://sfml-dev.org. Po wejściu na stronę downloads wybieramy opcję ściągnięcia plików źródłowych. Po ściągnięciu archiwum .zip wypakowywujemy je do katalogu roboczego (u mnie to jest Pulpit - względnie ~/Desktop
, i raczej tą nazwą będziemy posługiwać się dalej). Nastepnie otwieramy terminal, i wykonuję serię poleceń:
#!/bin/bash
cd ~/Desktop/SFML
mkdir build
cd build
Czyli wchodzimy do katalogu biblioteki, tworzymy katalog build
, w którym znajdować się będą pliki projektowe CMake, i wchodzimy do niego. Przechodzimy do generowania projektu i kompilacji:
#!/bin/bash
cmake ..
make
Kod został skompilowany do bibliotek dynamicznych (u mnie w systemie są to pliki *.dylib), które następnie dołączymy do projektu. Skompilowane pliki bibliotek znajdują się wewnątrz katalog ~/Desktop/SFML/build/lib
. Ponieważ SFML jako zależności używa zewnętrznych bibliotek, potrzebujemy wykonać polecenie instalacyjne, które zainstaluje te biblioteki w systemie:
#!/bin/bash
make install
W moim przypadku, do wykonania tego polecenia potrzebowałem uprawnień użytkownika root.
Kompilacja GoogleTest
Biblioteka GoogleTest jest jedną z tych, które są dla mnie wybawieniem. Służy do tworzenia testów automatycznych, bez których mój świat programistyczny NIE ISTNIEJE ;) Sam projekt nie ma swojej strony domowej. Jego źródła znajdują się na GitHubie, o tutaj. Ściągamy cały projekt na pulpit, w podobny sposób jak SFML. W podobny sposób jak wyżej, wykonujemy instrukcje:
#!/bin/bash
cd ~/Desktop/googletest
mkdir build
cd build
cmake ..
make
W tym momencie następuje kompilacja do bibliotek statycznych (w moim systemie pliki .a). GoogleTest kompiluje się do kilku plików, jednakże na potrzeby tego wpisu interesuje nas jedynie plik ~/Desktop/googletest/build/googlemock/gtest/libgtest.a
.
Koncepcja struktury w projekcie
Cały projekt zamieściłem na GitHubie, zachęcam do zerknięcia. Całość wymaga nieco omówienia z mojej strony:
bin
orazbin/lib
- katalogi, wewnątrz których wygenerowane zostaną pliki targetów (kod produkcyjny, testy oraz logika biznesowa)classes
- katalog, wewnątrz którego znajduje się logika biznesowa współdzielona pomiędzy kodem produkcyjnym a testamiinclude
- pliki nagłówkowe załączanych bibliotek zewnętrznych (SFML i GoogleTest)
lib - katalog zawierający skompilowane biblioteki zewnętrzne (SFML i GoogleTest)tests
- katalog z kodem testówbuild.sh
- skrypt automatyzujący proces budowania projektu
Wyjaśnić należy, dlaczego zdecydowałem się zawrzeć pliki bibliotek wewnątrz projektu. Powód jest bardzo prosty: aby móc pracować zawsze na tej samej wersji biblioteki (niezależnie od komputera, na którym pracuję) oraz aby móc w miarę sprawnie aktualizować wersje bibliotek przez podmianę plików. Oczywiście, z uwagi na zależności biblioteki SFML nie jest to idealne rozwiązanie, ale w mojej opinii wystarczająco bliskie ideału, aby móc z niego skorzystać.
Zawartość pliku CMakeLists.txt
Wewnątrz projektu utworzyłem tylko jeden plik CMakeLists.txt
, w którym zamieściłem reguły do budowania wszystkich plików targetu znajdujących się w projekcie. Oto jego zawartość:
cmake_minimum_required(VERSION 3.10)
project(Project)
set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/bin/lib)
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/bin/lib)
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/bin)
# BUSINESS LOGIC
set(LOGIC_SOURCES classes/math.cpp)
add_library(businessLogic ${LOGIC_SOURCES})
# PROGRAM
set(SOURCES main.cpp)
add_executable(program ${SOURCES})
target_include_directories(program PUBLIC include)
set(SFML_DIR ${CMAKE_SOURCE_DIR}/lib/sfml/)
file(GLOB SFML_LIBS ${SFML_DIR}*)
target_link_libraries(program ${SFML_LIBS})
target_link_libraries(program businessLogic)
# TESTS
set(TEST_SOURCES tests/main.cpp tests/math.cpp)
add_executable(tests ${TEST_SOURCES})
target_include_directories(tests PUBLIC include)
target_include_directories(tests PUBLIC ${CMAKE_SOURCE_DIR})
set(GTEST_LIBS ${CMAKE_SOURCE_DIR}/lib/gtest/libgtest.a)
target_link_libraries(tests ${SFML_LIBS} ${GTEST_LIBS})
target_link_libraries(tests businessLogic)
add_custom_target(NAME run_tests COMMAND tests)
Trójpodział targetowy
Powyższy przykład mógłbym podzielić na mniejsze pliki, ale ostatecznie z tego pomysłu zrezygnowałem na rzecz lepszej czytelności. Gdyby ten kod był bardziej obszerny, na pewno podzieliłbym go na pliki. Przejdźmy zatem do omówienia tego listingu.
Projekt podzieliłem na trzy targety: logikę biznesową (jako biblioteka statyczna businessLogic
), kod testów (plik wykonywalny tests
) oraz kod produkcyjny (plik wykonywalny program
). Logika biznesowa to kod, który zostaje współdzielony pomiędzy testami oraz kodem produkcyjnym. Zaletą kompilowania części kodu do bibliotek jest to, że kompilujemy go tylko raz, a używać go możemy w wielu targetach. Gdybyśmy zamiast budować biblioteki skopiowali listę plików źródłowych do obydwu targetów, kompilacja tych plików odbyłaby się dwukrotnie.
Linkowanie zewnętrznych bibliotek
Do obydwu targetów dołączyliśmy zewnętrzne biblioteki (target_link_libraries(...)
). Jak można zauważyć, linkowanie biblioteki statycznej (GoogleTest) oraz biblioteki dynamicznej (SFML) nie różni się od siebie w ogóle. Na uwagę zasługują również wykorzystania komendy target_include_directories(...)
. Jeżeli linkowana biblioteka nie znajduje się w naszym systemie operacyjnym, należy ręcznie wskazać kompilatorowi miejsce jej nagłówków.
Skrypt automatyzujący budowanie projektu
Dłuższą chwilę zastanawiałem się, w jaki sposób mogę uzależnić kompilację pliku programu od rezultatu testów. Najprostszym, a zarazem bardzo prostym rozwiązaniem okazało się stworzenie pliku powłoki:
#!/bin/bash
mkdir -p ./build
cd ./build
cmake ..
make tests
../bin/tests
if [[ $? -eq 0 ]]; then
make all
fi
Powyższy plik znajduje się głównym katalogu projektu. Najpierw tworzymy katalog build
, i do niego wchodzimy. Następnie przebudowujemy pliki projektowe (cmake ..
) i kompilujemy testy (make tests
). Kolejnym krokiem jest uruchomienie pliku wykonywalnego testów. Reguła jest prosta: jeżeli wszystkie testy zostaną zakończone sukcesem, program uruchamiający testy zwróci wartość 0. W przeciwnym wypadku zwrócona zostanie wartość inna niż 0. Ostatnie linijki w pliku to sprawdzenie tego warunku ($?
to zmienna automatyczna, która pamięta wartość zwróconą przez ostatnio wykonany program/polecenie). Jeżeli testy zostaną zakończone pomyślnie, skompilowana zostanie reszta projektu.
Tak powstały projekt możemy potraktować jako bazę do nowo zaczerpniętych pomysłów.
Podsumowanie
Dzisiaj wykorzystaliśmy w sposób praktyczny wiedzę teoretyczną zdobytą do tej pory. Zbudowaliśmy projekt składający się z najnowszych wersji dwóch bardzo popularnych bibliotek. Przy okazji zaczerpnęliśmy odrobinę z programowania skryptów powłoki, automatyzując proces budowania projektu.
Jeżeli znacie inne rozwiązania poruszonego przeze mnie problemu, bądź znacie jakieś drobiazgi, które mogły by wnieść coś wartościowego, wspomnijcie o tym na grupie Cpp Polska :)