CMake w praktyce


2018-06-20, 00:00

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

  1. Działamy na systemie MacOS
  2. Korzystamy z najnowszych wersji bibliotek załączanych do projektu
  3. Jeżeli jakikolwiek test w projekcie nie przechodzi, nie zostanie zbudowany plik z programem
  4. 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 oraz bin/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 testami
  • include - 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ów
  • build.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 :)



Marcin Kukliński

Zawodowo backend developer, hobbystycznie pasjonat języka C++. Po godzinach poszerza swoją wiedzę na takie tematy jak teorii kompilacji oraz budowa formatów plików. Jego marzeniem jest stworzyć swój własny język programowania.

Pssst! Używamy Cookies. Poprzez używanie naszego serwisu zgadzasz się na odczytywanie i zapisywanie Cookies w swojej przeglądarce.