CMake i C++ są braćmi na zawsze

CMake i C++ są braćmi na zawsze

Podczas programowania lubię zmieniać kompilatory, tryby budowania, wersje zależności, przeprowadzać analizę statyczną, mierzyć wydajność, zbierać pokrycie, generować dokumentację itp. I naprawdę kocham CMake, ponieważ pozwala mi robić wszystko, co chcę.

Wiele osób krytykuje CMake i często zasłużenie, ale jeśli się przyjrzeć, nie wszystko jest takie złe, a ostatnio tak ogólnie to nieźle, a kierunek rozwoju jest całkiem pozytywny.

W tej notatce chcę Ci powiedzieć, jak w prosty sposób zorganizować bibliotekę nagłówków w C++ w systemie CMake, aby uzyskać następującą funkcjonalność:

  1. Montaż;
  2. Autorun testy;
  3. Pomiar pokrycia kodu;
  4. Instalacja;
  5. Autodokumentacja;
  6. Generowanie piaskownicy online;
  7. Analiza statyczna.

Każdy, kto już rozumie zalety i markę C, może po prostu pobierz szablon projektu i zacznij z niego korzystać.


Zawartość

  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łówny cel
      5. Instalacja
      6. Testy
      7. Dokumentacja
      8. Piaskownica internetowa
    3. Skrypt testowy (test/CMakeLists.txt)
      1. Testowanie
      2. Okładka
    4. Skrypt do dokumentacji (doc/CMakeLists.txt)
    5. Skrypt dla piaskownicy online (online/CMakeLists.txt)
  2. Projekt na zewnątrz
    1. montaż
      1. Generacja
      2. montaż
    2. Opcje
      1. MYLIB_COVERAGE
      2. MYLIB_TESTOWANIE
      3. MYLIB_DOXYGEN_LANGUAGE
    3. Cele montażowe
      1. Domyślnie
      2. testy jednostkowe mylib
      3. ZOBACZ
      4. pokrycie
      5. doc
      6. pudełko na różdżki
    4. Примеры
  3. Narzędzia
  4. Analiza statyczna
  5. Posłowie

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

Będziemy głównie rozmawiać o tym, jak zorganizować skrypty CMake, dlatego zostaną one omówione szczegółowo. Każdy może bezpośrednio przeglądać resztę plików na stronie szablonu projektu.

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

Informacje o projekcie

Przede wszystkim musisz zamówić wymaganą wersję systemu CMake. CMake ewoluuje, zmieniają się sygnatury poleceń i zachowanie w różnych warunkach. Aby CMake od razu zrozumiał, czego od niego oczekujemy, musimy od razu zapisać nasze wymagania wobec niego.

cmake_minimum_required(VERSION 3.13)

Następnie określimy nasz projekt, jego nazwę, wersję, używane języki itp. (patrz. команду project).

W tym przypadku wskazujemy język CXX (a to oznacza C++), aby CMake nie męczył się i nie szukał kompilatora języka C (domyślnie CMake zawiera dwa języki: C i C++).

project(Mylib VERSION 1.0 LANGUAGES CXX)

Tutaj możesz od razu sprawdzić, czy nasz projekt jest uwzględniony w innym projekcie jako podprojekt. To bardzo pomoże w przyszłości.

get_directory_property(IS_SUBPROJECT PARENT_DIRECTORY)

Opcje projektu

Zapewnimy dwie opcje.

Pierwsza opcja to MYLIB_TESTING — aby wyłączyć testy jednostkowe. Może to być konieczne, jeśli mamy pewność, że z testami wszystko jest w porządku, a my chcemy jedynie np. zainstalować lub spakować nasz projekt. Lub też nasz projekt zostaje włączony jako podprojekt - w tym wypadku użytkownik naszego projektu nie jest zainteresowany przeprowadzaniem naszych testów. Nie testujesz zależności, których używasz, prawda?

option(MYLIB_TESTING "Включить модульное тестирование" ON)

Dodatkowo zrobimy osobną opcję MYLIB_COVERAGE do pomiaru pokrycia kodu testami, ale będzie to wymagało dodatkowych narzędzi, więc trzeba będzie je jawnie włączyć.

option(MYLIB_COVERAGE "Включить измерение покрытия кода тестами" OFF)

Opcje kompilacji

Oczywiście jesteśmy fajnymi programistami, więc chcemy od kompilatora maksymalnego poziomu diagnostyki w czasie kompilacji. Żadna mysz się nie prześlizgnie.

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łączymy także rozszerzenia, aby w pełni zachować zgodność ze standardem języka C++. Są one domyślnie włączone w CMake.

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

Główny cel

Nasza biblioteka składa się wyłącznie z plików nagłówkowych, co oznacza, że ​​nie mamy żadnego wyczerpania w postaci bibliotek statycznych lub dynamicznych. Natomiast aby móc korzystać z naszej biblioteki zewnętrznie należy ją zainstalować, musi być ona wykrywalna w systemie i połączona z Twoim projektem, a przy tym mieć te same nagłówki i ewentualnie jakieś dodatkowe, są z nim powiązane właściwości.

W tym celu tworzymy bibliotekę interfejsu.

add_library(mylib INTERFACE)

Łączymy nagłówki z naszą biblioteką interfejsu.

Nowoczesne, modne i młodzieżowe wykorzystanie CMake oznacza, że ​​nagłówki, właściwości itp. transmitowane przez jeden cel. Wystarczy więc powiedzieć target_link_libraries(target PRIVATE dependency)i wszystkie nagłówki powiązane z elementem docelowym dependency, będzie dostępny dla źródeł należących do celu target. I nie potrzebujesz żadnego [target_]include_directories. Zostanie to wykazane poniżej w analizie Skrypt CMake do testów jednostkowych.

Warto zwrócić także uwagę na tzw. выражения-генераторы: $<...>.

To polecenie wiąże potrzebne nam nagłówki z naszą biblioteką interfejsu, a jeśli nasza biblioteka jest podłączona do dowolnego celu w tej samej hierarchii CMake, to nagłówki z katalogu zostaną z nią powiązane ${CMAKE_CURRENT_SOURCE_DIR}/include, oraz jeśli nasza biblioteka jest zainstalowana w systemie i podłączona do innego projektu za pomocą polecenia find_package, wówczas nagłówki z katalogu zostaną z nim powiązane include względem katalogu instalacyjnego.

target_include_directories(mylib INTERFACE
    $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
    $<INSTALL_INTERFACE:include>
)

Ustalmy standard językowy. Oczywiście, że ostatni. Jednocześnie nie tylko uwzględniamy standard, ale także 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 (Zob. polecenie target_compile_features).

target_compile_features(mylib INTERFACE cxx_std_17)

Stwórzmy alias dla naszej biblioteki. Co więcej, dla urody, będzie on znajdował się w specjalnej „przestrzeni nazw”. Przyda się to gdy w naszej bibliotece pojawią się różne moduły i przystąpimy do łączenia ich niezależnie od siebie. Jak na przykład w Buscie.

add_library(Mylib::mylib ALIAS mylib)

Instalacja

Instalowanie naszych nagłówków w systemie. Tutaj wszystko jest proste. Mówimy, że folder ze wszystkimi nagłówkami powinien trafić do katalogu include w stosunku do miejsca instalacji.

install(DIRECTORY include/mylib DESTINATION include)

Następnie informujemy system kompilacji, że chcemy móc wywoływać polecenie w projektach innych firm find_package(Mylib) i zdobyć cel Mylib::mylib.

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

Tak należy rozumieć kolejne zaklęcie. Kiedy w projekcie innej firmy wywołujemy polecenie find_package(Mylib 1.2.3 REQUIRED), a rzeczywista wersja zainstalowanej biblioteki będzie z nią niekompatybilna 1.2.3CMake automatycznie wygeneruje błąd. Oznacza to, że nie będzie konieczne ręczne śledzenie 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ą wyraźnie wyłączone przy użyciu odpowiednia opcja lub nasz projekt jest podprojektem, czyli jest połączony z innym projektem CMake za pomocą polecenia add_subdirectory, nie poruszamy się dalej w hierarchii, a skrypt opisujący polecenia służące do generowania i uruchamiania testów po prostu nie działa.

if(NOT MYLIB_TESTING)
    message(STATUS "Тестирование проекта Mylib выключено")
elseif(IS_SUBPROJECT)
    message(STATUS "Mylib не тестируется в режиме подмодуля")
else()
    add_subdirectory(test)
endif()

Dokumentacja

Dokumentacja nie będzie generowana także w przypadku podprojektu.

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

Piaskownica internetowa

Podobnie podprojekt nie będzie miał piaskownicy online.

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

Skrypt testowy (test/CMakeLists.txt)

Testowanie

W pierwszej kolejności znajdziemy paczkę z wymaganym frameworkiem testowym (wymień na swój ulubiony).

find_package(doctest 2.3.3 REQUIRED)

Stwórzmy nasz plik wykonywalny z testami. Zwykle dodaję bezpośrednio do pliku wykonywalnego tylko plik, który będzie zawierał funkcję main.

add_executable(mylib-unit-tests test_main.cpp)

Oraz dodaję pliki w których później opisane są same testy. Ale nie musisz tego robić.

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

Łączymy zależności. Pamiętaj, że do naszego pliku binarnego połączyliśmy tylko potrzebne cele CMake i nie wywołaliśmy polecenia target_include_directories. Nagłówki ze środowiska testowego i z naszego Mylib::mylib, a także parametry kompilacji (w naszym przypadku jest to standard języka C++) spełniły się wraz z tymi celami.

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

Na koniec tworzymy fikcyjny cel, którego „kompilacja” jest równoznaczna z uruchomieniem testów i dodajemy ten cel do domyślnej kompilacji (odpowiada za to atrybut ALL). Oznacza to, że domyślna kompilacja uruchamia testy, co oznacza, że ​​nigdy nie zapomnimy ich uruchomić.

add_custom_target(check ALL COMMAND mylib-unit-tests)

Okładka

Następnie włączamy pomiar pokrycia kodu, jeśli zostanie zaznaczona odpowiednia opcja. Nie będę się wdawał w szczegóły, bo dotyczą one bardziej narzędzia do pomiaru pokrycia niż CMake'a. Należy tylko pamiętać, że na podstawie wyników zostanie stworzony cel coverage, od którego wygodnie jest rozpocząć pomiar pokrycia.

find_program(GCOVR_EXECUTABLE gcovr)
if(MYLIB_COVERAGE AND GCOVR_EXECUTABLE)
    message(STATUS "Измерение покрытия кода тестами включено")

    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 "Для замеров покрытия кода тестами требуется программа gcovr")
endif()

Skrypt do dokumentacji (doc/CMakeLists.txt)

Znalazłem Doxygena.

find_package(Doxygen)

Następnie sprawdzamy, czy użytkownik ustawił zmienną językową. Jeśli tak, to nie dotykamy tego, jeśli nie, to bierzemy rosyjski. Następnie konfigurujemy pliki systemowe Doxygen. Wszystkie niezbędne zmienne, łącznie z językiem, trafiają tam podczas procesu konfiguracji (patrz. команду configure_file).

Następnie tworzymy cel doc, który rozpocznie generowanie dokumentacji. Ponieważ generowanie dokumentacji nie jest największą potrzebą w procesie programowania, cel nie będzie domyślnie włączony; będzie musiał zostać uruchomiony jawnie.

if (Doxygen_FOUND)
    if (NOT MYLIB_DOXYGEN_LANGUAGE)
        set(MYLIB_DOXYGEN_LANGUAGE Russian)
    endif()
    message(STATUS "Doxygen documentation will be generated in ${MYLIB_DOXYGEN_LANGUAGE}")
    configure_file(Doxyfile.in Doxyfile)
    add_custom_target(doc COMMAND ${DOXYGEN_EXECUTABLE} ${CMAKE_CURRENT_BINARY_DIR}/Doxyfile)
endif ()

Skrypt dla piaskownicy online (online/CMakeLists.txt)

Tutaj znajdujemy trzeciego Pythona i tworzymy cel wandbox, który generuje żądanie odpowiadające API usługi różdżkai odsyła go. Odpowiedź zawiera łącze do gotowej piaskownicy.

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 "Для создания онлайн-песочницы требуется интерпретатор ЯП python 3-й версии")
endif()

Projekt na zewnątrz

Przyjrzyjmy się teraz, jak z tego wszystkiego skorzystać.

montaż

Budowanie tego projektu, podobnie jak każdego innego projektu w systemie kompilacji CMake, składa się z dwóch etapów:

Generacja

cmake -S путь/к/исходникам -B путь/к/сборочной/директории [опции ...]

Jeśli powyższe polecenie nie zadziałało ze względu na starą wersję CMake, spróbuj pominąć -S:

cmake путь/к/исходникам -B путь/к/сборочной/директории [опции ...]

Więcej o opcjach.

Budowanie projektu

cmake --build путь/к/сборочной/директории [--target target]

Więcej o celach montażowych.

Opcje

MYLIB_COVERAGE

cmake -S ... -B ... -DMYLIB_COVERAGE=ON [прочие опции ...]

Zawiera cel coverage, za pomocą którego możesz rozpocząć pomiar pokrycia kodu testami.

MYLIB_TESTOWANIE

cmake -S ... -B ... -DMYLIB_TESTING=OFF [прочие опции ...]

Zapewnia możliwość wyłączenia kompilacji i celu testów jednostkowych check. W rezultacie pomiar pokrycia kodu testami zostaje wyłączony (patrz. MYLIB_COVERAGE).

Testowanie jest również automatycznie wyłączane, jeśli projekt jest połączony z innym projektem jako podprojekt za pomocą polecenia add_subdirectory.

MYLIB_DOXYGEN_LANGUAGE

cmake -S ... -B ... -DMYLIB_DOXYGEN_LANGUAGE=English [прочие опции ...]

Zmienia język dokumentacji generowanej przez element docelowy doc do danego. Aby zapoznać się z listą dostępnych języków, zobacz Strona internetowa systemu Doxygen.

Język rosyjski jest domyślnie włączony.

Cele montażowe

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 z target all), zbiera wszystko, co może, a także wywołuje cel check.

testy jednostkowe mylib

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

Kompiluje testy jednostkowe. Domyślnie włączone.

ZOBACZ

cmake --build путь/к/сборочной/директории --target check

Uruchamia zebrane (zebrane, jeśli jeszcze nie) testy jednostkowe. Domyślnie włączone.

Zobacz także mylib-unit-tests.

pokrycie

cmake --build путь/к/сборочной/директории --target coverage

Analizuje uruchomione (uruchamiane, jeśli jeszcze nie) testy jednostkowe pod kątem pokrycia kodu testami z użyciem programu gcvr.

Wydech powłoki będzie wyglądał mniej więcej tak:

------------------------------------------------------------------------------
                           GCC Code Coverage Report
Directory: /path/to/cmakecpptemplate/include/
------------------------------------------------------------------------------
File                                       Lines    Exec  Cover   Missing
------------------------------------------------------------------------------
mylib/myfeature.hpp                            2       2   100%   
------------------------------------------------------------------------------
TOTAL                                          2       2   100%
------------------------------------------------------------------------------

Cel jest dostępny tylko wtedy, gdy opcja jest włączona MYLIB_COVERAGE.

Zobacz także check.

doc

cmake --build путь/к/сборочной/директории --target doc

Rozpoczyna generowanie dokumentacji kodu za pomocą systemu doxygen.

pudełko na różdżki

cmake --build путь/к/сборочной/директории --target wandbox

Odpowiedź serwisu wygląda mniej więcej tak:

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

Służy do tego usługa różdżka. Nie wiem na ile elastyczne są ich serwery, ale uważam, że tej możliwości nie należy wykorzystywać.

Примеры

Kompiluj projekt w trybie debugowania z pomiarem pokrycia

cmake -S путь/к/исходникам -B путь/к/сборочной/директории -DCMAKE_BUILD_TYPE=Debug -DMYLIB_COVERAGE=ON
cmake --build путь/к/сборочной/директории --target coverage --parallel 16

Instalacja projektu bez wstępnego montażu i testowania

cmake -S путь/к/исходникам -B путь/к/сборочной/директории -DMYLIB_TESTING=OFF -DCMAKE_INSTALL_PREFIX=путь/к/установойной/директории
cmake --build путь/к/сборочной/директории --target install

Kompiluj w trybie wydania z danym kompilatorem

cmake -S путь/к/исходникам -B путь/к/сборочной/директории -DCMAKE_BUILD_TYPE=Release -DCMAKE_CXX_COMPILER=g++-8 -DCMAKE_PREFIX_PATH=путь/к/директории/куда/установлены/зависимости
cmake --build путь/к/сборочной/директории --parallel 4

Generowanie dokumentacji w języku angielskim

cmake -S путь/к/исходникам -B путь/к/сборочной/директории -DCMAKE_BUILD_TYPE=Release -DMYLIB_DOXYGEN_LANGUAGE=English
cmake --build путь/к/сборочной/директории --target doc

Narzędzia

  1. CMake 3.13

    Tak naprawdę do uruchomienia niektórych poleceń konsoli opisanych w tej pomocy wymagana jest tylko wersja CMake 3.13. Z punktu widzenia składni skryptów CMake wersja 3.8 jest wystarczająca, jeśli generowanie jest wywoływane w inny sposób.

  2. Biblioteka testowa doktór

    Testowanie można wyłączyć (patrz опцию MYLIB_TESTING).

  3. doxygen

    Dostępna jest opcja zmiany języka, w jakim będzie generowana dokumentacja MYLIB_DOXYGEN_LANGUAGE.

  4. Tłumacz języka Python 3

    Do automatycznego generowania piaskownice internetowe.

Analiza statyczna

Dzięki CMake i kilku dobrym narzędziom możesz zapewnić analizę statyczną przy minimalnym wysiłku.

Kontrola Cpp

CMake ma wbudowaną obsługę narzędzia do analizy statycznej Kontrola Cpp.

Aby to zrobić, musisz skorzystać z opcji CMAKE_CXX_CPPCHECK:

cmake -S путь/к/исходникам -B путь/к/сборочной/директории -DCMAKE_BUILD_TYPE=Debug -DCMAKE_CXX_CPPCHECK="cppcheck;--enable=all;-Iпуть/к/исходникам/include"

Następnie analiza statyczna będzie automatycznie uruchamiana przy każdej kompilacji i rekompilacji źródła. Nie ma potrzeby wykonywania niczego dodatkowego.

Szczęk

Za pomocą wspaniałego narzędzia scan-build Możesz także błyskawicznie przeprowadzić analizę statyczną:

scan-build cmake -S путь/к/исходникам -B путь/к/сборочной/директории -DCMAKE_BUILD_TYPE=Debug
scan-build cmake --build путь/к/сборочной/директории

Tutaj, w przeciwieństwie do przypadku Cppcheck, musisz za każdym razem uruchamiać kompilację scan-build.

Posłowie

CMake to bardzo wydajny i elastyczny system, który pozwala wdrożyć funkcjonalność dla każdego gustu i koloru. I choć składnia czasami pozostawia wiele do życzenia, diabeł nadal nie jest taki straszny, jak go malują. Korzystaj z systemu kompilacji CMake z korzyścią dla społeczeństwa i zdrowia.

Pobierz szablon projektu

Źródło: www.habr.com

Dodaj komentarz