CMake i C++ — bracia na zawsze

CMake i C++ — bracia na zawsze

W trakcie rozwoju lubię zmieniać kompilatory, tryby kompilacji, wersje zależności, przeprowadzać analizę statyczną, mierzyć wydajność, zbierać pokrycie, generować dokumentację itd. I bardzo lubię CMake, ponieważ pozwala mi robić wszystko, co chcę.

Wielu krytykuje CMake, i często zasłużenie, ale jeśli się zagłębić, nie jest tak źle, a w ostatnim czasie jest nawet całkiem dobrze, a kierunek rozwoju jest całkiem pozytywny.

W tej notatce chcę opowiedzieć, jak stosunkowo łatwo zorganizować bibliotekę nagłówkową w języku C++ w systemie CMake, aby uzyskać następującą funkcjonalność:

  1. Kompilacja;
  2. Automatyczne uruchamianie testów;
  3. Mierzenie pokrycia kodu;
  4. Instalacja;
  5. Autodokumentowanie;
  6. Generowanie online sandbox;
  7. Analiza statyczna.

Kto już zna C++ i CMake może po prostu pobrać szablon projektu i zacząć go używać.


Spis treści

  1. Projekt od środka
    1. Struktura projektu
    2. Główny plik CMake (.\/CMakeLists.txt)
      1. Informacje o projekcie
      2. Opcje projektu
      3. Opcje kompilacji
      4. Główna cel
      5. Instalacja
      6. Testy
      7. Dokumentacja
      8. Online sandbox
    3. Skrypt dla testów (test\/CMakeLists.txt)
      1. Testowanie
      2. Zasięg
    4. Skrypt dla dokumentacji (doc\/CMakeLists.txt)
    5. Skrypt dla online sandbox (online\/CMakeLists.txt)
  2. Projekt z zewnątrz
    1. Kompilacja
      1. Generowanie
      2. Kompilacja
    2. Opcje
      1. MYLIB_COVERAGE
      2. MYLIB_TESTING
      3. MYLIB_DOXYGEN_LANGUAGE
    3. Cele budowy
      1. Domyślnie
      2. mylib-unit-tests
      3. sprawdzenie
      4. coverage
      5. doc
      6. wandbox
    4. Przykłady
  3. Narzędzia
  4. Analiza statyczna
  5. Epilog

Projekt od środka

Struktura projektu

.
├── CMakeLists.txt
├── README.en.md
├── README.md
├── doc
│   ├── CMakeLists.txt
│   └── Doxyfile.in
├── include
│   └── mylib
│       └── myfeature.hpp
├── online
│   ├── CMakeLists.txt
│   ├── mylib-example.cpp
│   └── wandbox.py
└── test
    ├── CMakeLists.txt
    ├── mylib
    │   └── myfeature.cpp
    └── test_main.cpp

Głównie mowa będzie o tym, jak zorganizować skrypty CMake, dlatego będą one szczegółowo opisane. Pozostałe pliki każdy chętny może zobaczyć bezpośrednio na stronie projektu-szablonu.

Główny plik CMake (.\/CMakeLists.txt)

Informacje o projekcie

W pierwszej kolejności należy zażądać odpowiedniej wersji systemu CMake. CMake się rozwija, zmieniają się sygnatury poleceń, zachowanie w różnych warunkach. Aby CMake od razu zrozumiał, czego od niego oczekujemy, musimy od razu ustalić nasze wymagania.

cmake_minimum_required(VERSION 3.13)

Następnie określimy nasz projekt, jego nazwę, wersję, używane języki itd. (patrz. polecenie project).

W tym przypadku wskazujemy język CXX (czyli C++), aby CMake nie musiał szukać kompilatora dla języka C (domyślnie w CMake włączone są dwa języki: C i C++).

project(Mylib VERSION 1.0 LANGUAGES CXX)

Można tu od razu sprawdzić, czy nasz projekt jest częścią innego projektu jako podprojekt. To znacząco pomoże w dalszej pracy.

get_directory_property(IS_SUBPROJECT PARENT_DIRECTORY)

Opcje projektu

Zakładamy dwie opcje.

Pierwsza opcja — MYLIB_TESTING — do wyłączenia testów modułowych. Może być to potrzebne, jeśli jesteśmy pewni, że testy są w porządku, a chcemy na przykład tylko zainstalować lub spakować nasz projekt. Lub jeśli nasz projekt jest włączony jako podprojekt — w takim przypadku użytkownik naszego projektu nie jest zainteresowany uruchamianiem naszych testów. Czy wy testujecie zależności, których używacie?

option(MYLIB_TESTING "Włączyć testy modułowe" ON)

Ponadto stworzymy oddzielną opcję MYLIB_COVERAGE do pomiaru pokrycia kodu testami, ale wymaga ona dodatkowych narzędzi, więc będzie musiała być włączana jawnie.

option(MYLIB_COVERAGE "Włączyć pomiar pokrycia kodu testami" OFF)

Opcje kompilacji

Oczywiście, jesteśmy świetnymi programistami C++, dlatego chcemy, aby kompilator dostarczał maksymalny poziom diagnostyki czasu kompilacji. Żaden błąd nie umknie.

add_compile_options(
    -Werror

    -Wall
    -Wextra
    -Wpedantic

    -Wcast-align
    -Wcast-qual
    -Wconversion
    -Wctor-dtor-privacy
    -Wenum-compare
    -Wfloat-equal
    -Wnon-virtual-dtor
    -Wold-style-cast
    -Woverloaded-virtual
    -Wredundant-decls
    -Wsign-conversion
    -Wsign-promo
)

Wyłączenie rozszerzeń również zapewni pełne przestrzeganie standardu C++. Domyślnie w CMake są one włączone.

if(NOT CMAKE_CXX_EXTENSIONS)
    set(CMAKE_CXX_EXTENSIONS OFF)
endif()

Główna cel

Nasza biblioteka składa się tylko z plików nagłówkowych, co oznacza, że nie mamy żadnego wyjścia w postaci statycznych lub dynamicznych bibliotek. Z drugiej strony, aby korzystać z naszej biblioteki na zewnątrz, trzeba ją zainstalować, aby była widoczna w systemie i mogła być podłączona do swojego projektu, a przy tym muszą być do niej dołączone te nagłówki oraz być może jakieś dodatkowe właściwości.

W tym celu tworzymy bibliotekę interfejsową.

add_library(mylib INTERFACE)

Przypisujemy nagłówki do naszej biblioteki interfejsowej.

Nowoczesne, modne, młodzieżowe użycie CMake zakłada, że nagłówki, właściwości itd. są przekazywane przez jeden, jedyny cel. W ten sposób wystarczy powiedzieć target_link_libraries(target PRIVATE dependency), a wszystkie nagłówki, które są związane z celem dependency, będą dostępne dla źródeł należących do celu target. I nie są wymagane żadne [target_]include_directoriesBędzie to demonstrowane poniżej w analizie skryptu CMake dla testów modułowych.

Warto również zwrócić uwagę na tzw. wyrażenia-generatory: $.

Ta komenda łączy potrzebne nagłówki z naszą biblioteką interfejsową, przy czym, jeśli nasza biblioteka będzie podłączona do jakiegoś celu w ramach jednej hierarchii CMake, to zostaną do niej przypisane nagłówki z katalogu ${CMAKE_CURRENT_SOURCE_DIR}/include, a jeśli nasza biblioteka jest zainstalowana w systemie i podłączona w innym projekcie za pomocą komendy find_package, to zostaną do niej przypisane nagłówki z katalogu include względem katalogu instalacji.

target_include_directories(mylib INTERFACE
    $
    $
)

Ustawimy standard języka. Oczywiście, najnowszy. Przy tym nie tylko włączamy standard, ale również rozszerzamy go na tych, którzy będą korzystać z naszej biblioteki. Osiąga się to dzięki temu, że ustawiona właściwość ma kategorię INTERFACE (patrz. komendę target_compile_features).

target_compile_features(mylib INTERFACE cxx_std_17)

Tworzymy alias dla naszej biblioteki. Przy czym dla estetyki będzie on w specjalnej "przestrzeni nazw". Będzie to przydatne, gdy nasza biblioteka zyska różne moduły i będziemy mogli je dodawać niezależnie od siebie. Tak jak w Boost, na przykład.

add_library(Mylib::mylib ALIAS mylib)

Instalacja

Instalacja naszych nagłówków w systemie. To jest proste. Mówimy, że folder ze wszystkimi nagłówkami powinien trafić do katalogu include względem miejsca instalacji.

install(DIRECTORY include/mylib DESTINATION include)

Następnie informujemy system budowy, że chcemy mieć możliwość w projektach zewnętrznych wywoływać komendę find_package(Mylib) i uzyskiwać cel Mylib::mylib.

install(TARGETS mylib EXPORT MylibConfig)
install(EXPORT MylibConfig NAMESPACE Mylib:: DESTINATION share/Mylib/cmake)

Następne zaklęcie należy rozumieć tak. Gdy w zewnętrznym projekcie wywołamy komendę find_package(Mylib 1.2.3 REQUIRED), a rzeczywista wersja zainstalowanej biblioteki okaże się niezgodna z wersją 1.2.3, CMake automatycznie wygeneruje błąd. Oznacza to, że nie trzeba będzie ręcznie śledzić wersji.

include(CMakePackageConfigHelpers)
write_basic_package_version_file("${PROJECT_BINARY_DIR}/MylibConfigVersion.cmake"
    VERSION
        ${PROJECT_VERSION}
    COMPATIBILITY
        AnyNewerVersion
)
install(FILES "${PROJECT_BINARY_DIR}/MylibConfigVersion.cmake" DESTINATION share/Mylib/cmake)

Testy

Jeśli testy są wyłączone jednoznacznie za pomocą odpowiedniej opcji lub nasz projekt jest podprojektem, co oznacza, że jest podłączony do innego projektu CMake za pomocą polecenia add_subdirectory, nie przechodzimy dalej w hierarchii, a skrypt, w którym opisane są polecenia do generowania i uruchamiania testów, po prostu się nie uruchomi.

if(NOT MYLIB_TESTING)
    message(STATUS "Testowanie projektu Mylib jest wyłączone")
elseif(IS_SUBPROJECT)
    message(STATUS "Mylib nie jest testowany w trybie podmodułu")
else()
    add_subdirectory(test)
endif()

Dokumentacja

Dokumentacja również nie będzie generowana w przypadku podprojektu.

if(NOT IS_SUBPROJECT)
    add_subdirectory(doc)
endif()

Online sandbox

Podobnie, nie będzie również internetowej piaskownicy dla podprojektu.

if(NOT IS_SUBPROJECT)
    add_subdirectory(online)
endif()

Skrypt dla testów (test\/CMakeLists.txt)

Testowanie

Na początku znajdujemy pakiet z odpowiednim frameworkiem testowym (zastąp swoim ulubionym).

find_package(doctest 2.3.3 REQUIRED)

Tworzymy nasz plik wykonywalny z testami. Zazwyczaj do binarnego pliku wykonywalnego dodaję tylko plik, w którym będzie funkcja main.

add_executable(mylib-unit-tests test_main.cpp)

A pliki, w których opisane są same testy, dodaję później. Ale nie jest to konieczne.

target_sources(mylib-unit-tests PRIVATE mylib/myfeature.cpp)

Podłączamy zależności. Zauważ, że do naszego binarnego pliku powiązaliśmy tylko potrzebne cele CMake i nie wywoływaliśmy polecenia target_include_directories. Nagłówki z frameworka testowego oraz z naszej Mylib::mylib, a także parametry kompilacji (w naszym przypadku jest to standard języka C++) przeszły razem z tymi celami.

target_link_libraries(mylib-unit-tests
    PRIVATE
        Mylib::mylib
        doctest::doctest
)

Na koniec tworzymy fikcyjny cel, którego 'kompilacja' jest równoważna uruchomieniu testów, i dodajemy ten cel do kompilacji domyślnej (za co odpowiada atrybut [START WITH …] CONNECT BY). To oznacza, że kompilacja domyślna inicjuje uruchomienie testów, co oznacza, że nigdy nie zapomnimy ich uruchomić.

add_custom_target(check ALL COMMAND mylib-unit-tests)

Zasięg

Następnie włączamy pomiar pokrycia kodu, jeśli określona jest odpowiednia opcja. Nie będę wnikał w szczegóły, ponieważ bardziej dotyczą one narzędzia do pomiaru pokrycia niż samego CMake. Ważne jest jedynie to, że w wyniku utworzony zostanie cel coverage, za pomocą którego wygodnie uruchamia się pomiar pokrycia.

find_program(GCOVR_EXECUTABLE gcovr)
if(MYLIB_COVERAGE AND GCOVR_EXECUTABLE)
    message(STATUS "Pomiar pokrycia kodu testami jest włączony")

    target_compile_options(mylib-unit-tests PRIVATE --coverage)
    target_link_libraries(mylib-unit-tests PRIVATE gcov)

    add_custom_target(coverage
        COMMAND
            ${GCOVR_EXECUTABLE}
                --root=${PROJECT_SOURCE_DIR}/include/
                --object-directory=${CMAKE_CURRENT_BINARY_DIR}
        DEPENDS
            check
    )
elseif(MYLIB_COVERAGE AND NOT GCOVR_EXECUTABLE)
    set(MYLIB_COVERAGE OFF)
    message(WARNING "Program gcovr jest wymagany do pomiaru pokrycia kodu testami")
endif()

Skrypt dla dokumentacji (doc\/CMakeLists.txt)

Znaleziono Doxygen.

find_package(Doxygen)

Zaraz sprawdzimy, czy użytkownik ustawił zmienną z językiem. Jeśli tak, to nie dotykamy, a jeśli nie, to bierzemy polski. Następnie konfigurujemy pliki systemu Doxygen. Wszystkie potrzebne zmienne, w tym język, trafiają tam w procesie konfiguracji (patrz komenda configure_file).

Po czym tworzymy cel doc, który będzie uruchamiał generowanie dokumentacji. Ponieważ generowanie dokumentacji nie jest największą potrzebą w procesie rozwoju, domyślnie cel ten nie będzie włączony, trzeba go będzie uruchomić ręcznie.

if (Doxygen_FOUND)
    if (NOT MYLIB_DOXYGEN_LANGUAGE)
        set(MYLIB_DOXYGEN_LANGUAGE Polish)
    endif()
    message(STATUS "Dokumentacja Doxygen będzie generowana w ${MYLIB_DOXYGEN_LANGUAGE}")
    configure_file(Doxyfile.in Doxyfile)
    add_custom_target(doc COMMAND ${DOXYGEN_EXECUTABLE} ${CMAKE_CURRENT_BINARY_DIR}/Doxyfile)
endif ()

Skrypt dla online sandbox (online\/CMakeLists.txt)

Tutaj znajdujemy trzeci Python i tworzymy cel wandbox, który generuje zapytanie zgodne z API serwisu Wandbox, i je wysyła. W odpowiedzi przychodzi link do gotowego sandboxa.

find_program(PYTHON3_EXECUTABLE python3)
if(PYTHON3_EXECUTABLE)
    set(WANDBOX_URL "https://wandbox.org/api/compile.json")

    add_custom_target(wandbox
        COMMAND
            ${PYTHON3_EXECUTABLE} wandbox.py mylib-example.cpp "${PROJECT_SOURCE_DIR}" include |
            curl -H "Content-type: application/json" -d @- ${WANDBOX_URL}
        WORKING_DIRECTORY
            ${CMAKE_CURRENT_SOURCE_DIR}
        DEPENDS
            mylib-unit-tests
    )
else()
    message(WARNING "Aby stworzyć online sandbox, wymagany jest interpreter Języka programowania Python w wersji 3")
endif()

Projekt z zewnątrz

Teraz zobaczmy, jak z tego wszystkiego korzystać.

Kompilacja

Budowa tego projektu, jak i każdego innego projektu w systemie budowy CMake, składa się z dwóch etapów:

Generowanie

cmake -S ścieżka/do/źródeł -B ścieżka/do/katalogu/budowy [opcje ...]

Jeśli powyższa komenda nie zadziałała z powodu starej wersji CMake, spróbuj pominąć -S:

cmake ścieżka/do/źródeł -B ścieżka/do/katalogu/budowy [opcje ...]

Więcej o opcjach.

Budowa projektu

cmake --build ścieżka/do/katalogu/budowy [--target cel]

Więcej o celach budowy.

Opcje

MYLIB_COVERAGE

cmake -S ... -B ... -DMYLIB_COVERAGE=ON [inne opcje ...]

Włącza cel coverage, za pomocą której można uruchomić pomiar pokrycia kodu testami.

MYLIB_TESTING

cmake -S ... -B ... -DMYLIB_TESTING=OFF [inne opcje ...]

Oferuje możliwość wyłączenia kompilacji testów jednostkowych oraz celu sprawdzenie. W konsekwencji wyłączany jest pomiar pokrycia kodu testami (zob. MYLIB_COVERAGE).

Testowanie jest również automatycznie wyłączane, jeśli projekt jest dołączany w innym projekcie jako podprojekt za pomocą add_subdirectory.

MYLIB_DOXYGEN_LANGUAGE

cmake -S ... -B ... -DMYLIB_DOXYGEN_LANGUAGE=English [inne opcje ...]

Przełącza język dokumentacji generowanej przez cel doc na wskazany. Lista dostępnych języków znajduje się na stronie systemu Doxygen..

Domyślnie włączony jest język rosyjski.

Cele budowy

Domyślnie

cmake --build path/to/build/directory
cmake --build path/to/build/directory --target all

Jeśli cel nie jest określony (co jest równoważne celowi wszystko), kompiluje wszystko, co możliwe, a także wywołuje cel sprawdzenie.

mylib-unit-tests

cmake --build path/to/build/directory --target mylib-unit-tests

Kompiluje testy jednostkowe. Włączone domyślnie.

sprawdzenie

cmake --build ścieżka/do/katalogu/budowy --target check

Uruchamia zebrane (kompiluje, jeśli jeszcze nie) testy jednostkowe. Włączone domyślnie.

Zob. także mylib-unit-tests.

coverage

cmake --build ścieżka/do/katalogu/budowy --target coverage

Analizuje uruchomione (uruchamia, jeśli jeszcze nie) testy jednostkowe pod kątem pokrycia kodu testami przy użyciu programu gcovr.

Wynik pokrycia będzie wyglądał mniej więcej tak:

------------------------------------------------------------------------------
                           Raport pokrycia kodu GCC
Katalog: /ścieżka/do/cmakecpptemplate/include/
------------------------------------------------------------------------------
Plik                                       Linie    Wykonano  Pokrycie   Brakujące
------------------------------------------------------------------------------
mylib/myfeature.hpp                            2       2   100%   
------------------------------------------------------------------------------
SUMA                                          2       2   100%
------------------------------------------------------------------------------

Cel dostępny tylko przy włączonej opcji MYLIB_COVERAGE.

Zob. także sprawdzenie.

doc

cmake --build ścieżka/do/katalogu/budowy --target doc

Uruchamia generację dokumentacji kodu za pomocą systemu Doxygen.

wandbox

cmake --build ścieżka/do/katalogu/budowy --target wandbox

Odpowiedź z usługi wygląda mniej więcej tak:

{
    "permlink" :    "QElvxuMzHgL9fqci",
    "status" :  "0",
    "url" : "https://wandbox.org/permlink/QElvxuMzHgL9fqci"
}

Do tego celu używana jest usługa Wandbox. Nie wiem, jak bardzo ich serwery są elastyczne, ale myślę, że nie warto nadużywać tej możliwości.

Przykłady

Budowa projektu w trybie debug z pomiarem pokrycia

cmake -S ścieżka/do/źródeł -B ścieżka/do/katalogu/budowy -DCMAKE_BUILD_TYPE=Debug -DMYLIB_COVERAGE=ON
cmake --build ścieżka/do/katalogu/budowy --target coverage --parallel 16

Instalacja projektu bez wcześniejszego budowania i testowania

cmake -S ścieżka/do/źródeł -B ścieżka/do/katalogu/budowy -DMYLIB_TESTING=OFF -DCMAKE_INSTALL_PREFIX=ścieżka/do/katalogu/instalacji
cmake --build ścieżka/do/katalogu/budowy --target install

Budowanie w trybie wydania z określonym kompilatorem

cmake -S ścieżka/do/źródeł -B ścieżka/do/katalogu/budowy -DCMAKE_BUILD_TYPE=Release -DCMAKE_CXX_COMPILER=g++-8 -DCMAKE_PREFIX_PATH=ścieżka/do/katalogu/gdzie/zainstalowano/zależności
cmake --build ścieżka/do/katalogu/budowy --parallel 4

Generowanie dokumentacji w języku angielskim

cmake -S ścieżka/do/źródeł -B ścieżka/do/katalogu/budowy -DCMAKE_BUILD_TYPE=Release -DMYLIB_DOXYGEN_LANGUAGE=English
cmake --build ścieżka/do/katalogu/budowy --target doc

Narzędzia

  1. CMake 3.13

    W rzeczywistości wersja CMake 3.13 jest wymagana tylko do uruchamiania niektórych poleceń konsolowych opisanych w tej dokumentacji. Z punktu widzenia składni skryptów CMake wystarczy wersja 3.8, jeśli generację wywołuje się innymi metodami.

  2. Biblioteka testowa doctest

    Testowanie można wyłączyć (zob. opcją MYLIB_TESTING).

  3. Doxygen

    Aby przełączyć język, w którym zostanie wygenerowana dokumentacja, przewidziano opcję MYLIB_DOXYGEN_LANGUAGE.

  4. Interpreter Języka Programowania Python 3

    Do automatycznego generowania internetowych skrzynek testowych.

Analiza statyczna

Za pomocą CMake i kilku dobrych narzędzi można zapewnić statyczną analizę przy minimalnym wysiłku.

Cppcheck

W CMake wbudowana jest obsługa narzędzia do analizy statycznej Cppcheck.

W tym celu należy skorzystać z opcji CMAKE_CXX_CPPCHECK:

cmake -S ścieżka/do/źródeł -B ścieżka/do/katalogu/budowy -DCMAKE_BUILD_TYPE=Debug -DCMAKE_CXX_CPPCHECK="cppcheck;--enable=all;-Iścieżka/do/źródeł/include"

Po tym statyczna analiza będzie automatycznie uruchamiana za każdym razem podczas kompilacji i rekompilacji źródeł. Nie ma potrzeby podejmowania dodatkowych działań.

Clang

Za pomocą cudownego narzędzia scan-build można również uruchomić analizę statyczną w mgnieniu oka:

scan-build cmake -S ścieżka/do/źródeł -B ścieżka/do/katalogu/budowy -DCMAKE_BUILD_TYPE=Debug
scan-build cmake --build ścieżka/do/katalogu/budowy

Tutaj, w przeciwieństwie do przypadku z Cppcheck, należy za każdym razem uruchamiać budowę przez scan-build.

Epilog

CMake to bardzo potężny i elastyczny system, który pozwala realizować funkcjonalność na wiele sposobów. I chociaż składnia czasem zostawia wiele do życzenia, to wcale nie jest tak strasznie, jak się to maluje. Korzystaj z systemu budowania CMake dla dobra społeczeństwa i z korzyścią dla zdrowia.

Pobierz szablon projektu

Źródło: habr.com

Kup solidny hosting stron z ochroną przed DDoS, serwery VPS VDS 🔥 Kup solidny hosting stron z ochroną przed DDoS, serwery VPS VDS | ProHoster