CMake und C++ sind für immer Brüder

CMake und C++ sind für immer Brüder

Während der Entwicklung ändere ich gerne Compiler, Build-Modi und Abhängigkeitsversionen, führe statische Analysen durch, messe die Leistung, sammle Abdeckung, erstelle Dokumentation und so weiter. Und ich liebe CMake wirklich, weil ich damit tun kann, was ich will.

Viele schimpfen über CMake, und das oft zu Recht, aber wenn man hinschaut, ist es nicht so schlimm, aber in letzter Zeit gar nicht so schlecht, und die Richtung der Entwicklung ist durchaus positiv.

In diesem Hinweis möchte ich Ihnen erklären, wie einfach es ist, eine C++-Header-Bibliothek in einem CMake-System zu organisieren, um die folgende Funktionalität zu erhalten:

  1. Montage;
  2. Autorun-Tests;
  3. Messung der Codeabdeckung;
  4. Installation;
  5. Autodokumentation;
  6. Online-Sandbox-Generierung;
  7. Statische Analyse.

Wer die Vor- und Nachteile bereits versteht, kann es einfach tun Projektvorlage herunterladen und fangen Sie an, es zu verwenden.


Inhalt

  1. Projekt von innen
    1. Projektstruktur
    2. Haupt-CMake-Datei (./CMakeLists.txt)
      1. Projekt Information
      2. Projektoptionen
      3. Kompilierungsoptionen
      4. Das Hauptziel
      5. Einstellung
      6. Tests
      7. Dokumentation
      8. Online-Sandbox
    3. Skript für Tests (test/CMakeLists.txt)
      1. Testing
      2. Abdeckung
    4. Dokumentationsskript (doc/CMakeLists.txt)
    5. Skript für Online-Sandbox (online/CMakeLists.txt)
  2. Projekt draußen
    1. Montage
      1. Generation
      2. Montage
    2. Optionen
      1. MYLIB_COVERAGE
      2. MYLIB_TESTING
      3. MYLIB_DOXYGEN_LANGUAGE
    3. Montageziele
      1. Standardmäßig
      2. mylib-Unit-Tests
      3. aus der Ferne überprüfen
      4. Berichterstattung
      5. Dock
      6. Zauberstabbox
    4. Примеры
  3. Werkzeuge
  4. Statische Analyse
  5. Nachwort

Projekt von innen

Projektstruktur

.
├── 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

Dabei geht es vor allem um die Organisation von CMake-Skripten, daher werden diese im Detail analysiert. Die restlichen Dateien können von jedem direkt eingesehen werden. auf der Seite des Vorlagenprojekts.

Haupt-CMake-Datei (./CMakeLists.txt)

Projekt Information

Zunächst müssen Sie die gewünschte Version des CMake-Systems anfordern. CMake entwickelt sich, Befehlssignaturen ändern sich, Verhalten unter verschiedenen Bedingungen. Damit CMake sofort versteht, was wir davon wollen, müssen wir unsere Anforderungen sofort festlegen.

cmake_minimum_required(VERSION 3.13)

Dann bezeichnen wir unser Projekt, seinen Namen, seine Version, die verwendeten Sprachen usw. команду project).

Geben Sie in diesem Fall die Sprache an CXX (was C++ bedeutet), damit sich CMake nicht die Mühe macht, nach einem C-Compiler zu suchen (standardmäßig sind in CMake zwei Sprachen enthalten: C und C++).

project(Mylib VERSION 1.0 LANGUAGES CXX)

Hier können Sie sofort prüfen, ob unser Projekt als Teilprojekt in ein anderes Projekt eingebunden ist. Das wird in Zukunft sehr helfen.

get_directory_property(IS_SUBPROJECT PARENT_DIRECTORY)

Projektoptionen

Betrachten wir zwei Optionen.

Die erste Option ist MYLIB_TESTING - um Unit-Tests zu deaktivieren. Dies kann erforderlich sein, wenn wir sicher sind, dass bei den Tests alles in Ordnung ist, und wir beispielsweise unser Projekt nur installieren oder verpacken möchten. Oder unser Projekt wird als Unterprojekt eingebunden – in diesem Fall ist der Benutzer unseres Projekts nicht daran interessiert, unsere Tests durchzuführen. Sie testen die Abhängigkeiten, die Sie verwenden, nicht, oder?

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

Darüber hinaus werden wir eine separate Option erstellen MYLIB_COVERAGE zum Messen der Codeabdeckung mit Tests, es sind jedoch zusätzliche Tools erforderlich, sodass Sie es explizit aktivieren müssen.

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

Kompilierungsoptionen

Natürlich sind wir coole Programmierer, also wollen wir vom Compiler ein Höchstmaß an Diagnose zur Kompilierungszeit. Keine einzige Maus kommt durch.

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
)

Wir werden auch Erweiterungen deaktivieren, um dem C++-Sprachstandard vollständig zu entsprechen. Sie sind in CMake standardmäßig aktiviert.

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

Das Hauptziel

Unsere Bibliothek besteht nur aus Header-Dateien, das heißt, wir haben keine Ausgabe in Form von statischen oder dynamischen Bibliotheken. Um unsere Bibliothek von außen nutzen zu können, müssen Sie sie hingegen installieren, im System finden und mit Ihrem Projekt verbinden können, und gleichzeitig auch diese Header sowie möglicherweise einige zusätzliche Eigenschaften.

Zu diesem Zweck erstellen wir eine Schnittstellenbibliothek.

add_library(mylib INTERFACE)

Wir binden die Header an unsere Schnittstellenbibliothek.

Der moderne, trendige und jugendliche Einsatz von CMake bedeutet, dass Header, Eigenschaften usw. über ein einzelnes Ziel übertragen. Es reicht also zu sagen target_link_libraries(target PRIVATE dependency)und alle Header, die dem Ziel zugeordnet sind dependency, wird für Quellen verfügbar sein, die zum Ziel gehören target. Und du brauchst keine [target_]include_directories. Dies wird im Folgenden beim Parsen demonstriert CMake-Skript für Unit-Tests.

Es lohnt sich auch, auf das sogenannte zu achten. выражения-генераторы: $<...>.

Dieser Befehl verknüpft die von uns benötigten Header mit unserer Schnittstellenbibliothek. Wenn unsere Bibliothek mit einem beliebigen Ziel innerhalb derselben CMake-Hierarchie verbunden ist, werden ihr die Header aus dem Verzeichnis zugeordnet ${CMAKE_CURRENT_SOURCE_DIR}/include, und wenn unsere Bibliothek auf dem System installiert und über den Befehl mit einem anderen Projekt verbunden ist find_package, dann werden ihm die Header aus dem Verzeichnis zugeordnet include relativ zum Installationsverzeichnis.

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

Legen Sie den Sprachstandard fest. Natürlich das Neueste. Gleichzeitig nehmen wir den Standard nicht nur auf, sondern verteilen ihn auch an diejenigen, die unsere Bibliothek nutzen werden. Dies wird dadurch erreicht, dass die festgelegte Eigenschaft eine Kategorie hat INTERFACE (Siehe. target_compile_features-Befehl).

target_compile_features(mylib INTERFACE cxx_std_17)

Wir bekommen einen Alias ​​für unsere Bibliothek. Und der Schönheit halber wird es in einem speziellen „Namensraum“ sein. Dies ist nützlich, wenn in unserer Bibliothek verschiedene Module erscheinen und wir sie unabhängig voneinander verbinden. Wie zum Beispiel in Busta.

add_library(Mylib::mylib ALIAS mylib)

Einstellung

Installieren unserer Header im System. Hier ist alles einfach. Wir sagen, dass der Ordner mit allen Headern in das Verzeichnis fallen sollte include bezüglich des Installationsortes.

install(DIRECTORY include/mylib DESTINATION include)

Als nächstes teilen wir dem Build-System mit, dass wir den Befehl in Drittprojekten aufrufen können möchten find_package(Mylib) und ein Ziel finden Mylib::mylib.

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

So ist die folgende Beschwörung zu verstehen. In einem Nebenprojekt rufen wir den Befehl auf find_package(Mylib 1.2.3 REQUIRED), und gleichzeitig ist die echte Version der installierten Bibliothek mit der Version nicht kompatibel 1.2.3, CMake generiert automatisch einen Fehler. Das heißt, Sie müssen die Versionen nicht manuell nachverfolgen.

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)

Tests

Wenn Tests explizit mit deaktiviert werden entsprechende Option oder unser Projekt ist ein Unterprojekt, das heißt, es ist über den Befehl mit einem anderen CMake-Projekt verbunden add_subdirectory, gehen wir in der Hierarchie nicht weiter nach unten und das Skript, das die Befehle zum Generieren und Ausführen von Tests beschreibt, startet einfach nicht.

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

Dokumentation

Auch bei einem Teilprojekt wird keine Dokumentation erstellt.

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

Online-Sandbox

Ebenso wird das Teilprojekt keine Online-Sandbox haben.

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

Skript für Tests (test/CMakeLists.txt)

Testing

Zunächst finden wir ein Paket mit dem gewünschten Test-Framework (ersetzen Sie es durch Ihr Lieblings-Framework).

find_package(doctest 2.3.3 REQUIRED)

Wir erstellen unsere ausführbare Datei mit Tests. Normalerweise füge ich direkt zur ausführbaren Binärdatei nur die Datei hinzu, in der sich die Funktion befindet main.

add_executable(mylib-unit-tests test_main.cpp)

Und die Dateien, in denen die Tests selbst beschrieben sind, füge ich später hinzu. Dies ist jedoch nicht erforderlich.

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

Wir verbinden Abhängigkeiten. Bitte beachten Sie, dass wir nur die benötigten CMake-Ziele an unsere Binärdatei angehängt und den Befehl nicht aufgerufen haben target_include_directories. Header aus dem Testframework und von unserem Mylib::mylibsowie Build-Optionen (in unserem Fall der C++-Sprachstandard) wurden zusammen mit diesen Zielen gecrawlt.

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

Schließlich erstellen wir ein Dummy-Ziel, dessen „Build“ dem Ausführen von Tests entspricht, und fügen dieses Ziel dem Standard-Build (dem Attribut) hinzu ALL). Das bedeutet, dass der Standard-Build die Ausführung der Tests auslöst, sodass wir nie vergessen werden, sie auszuführen.

add_custom_target(check ALL COMMAND mylib-unit-tests)

Abdeckung

Aktivieren Sie als Nächstes die Messung der Codeabdeckung, sofern die entsprechende Option aktiviert ist. Ich werde nicht auf die Details eingehen, da sie eher mit dem Coverage-Messtool als mit CMake zusammenhängen. Wichtig ist nur zu beachten, dass aus den Ergebnissen ein Ziel entsteht coverage, mit dem Sie bequem mit der Messung der Abdeckung beginnen können.

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()

Dokumentationsskript (doc/CMakeLists.txt)

Doxygen gefunden.

find_package(Doxygen)

Als nächstes prüfen wir, ob die Variable mit der Sprache vom Benutzer festgelegt wurde. Wenn ja, dann rühren wir es nicht an, wenn nicht, dann nehmen wir Russisch. Anschließend konfigurieren wir die Doxygen-Systemdateien. Alle notwendigen Variablen, einschließlich der Sprache, gelangen während des Konfigurationsprozesses dorthin (siehe. команду configure_file).

Dann schaffen wir ein Ziel doc, wodurch mit der Erstellung der Dokumentation begonnen wird. Da die Erstellung der Dokumentation nicht den größten Bedarf im Entwicklungsprozess darstellt, wird das Ziel nicht standardmäßig einbezogen, sondern muss explizit ausgeführt werden.

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 ()

Skript für Online-Sandbox (online/CMakeLists.txt)

Hier finden wir den dritten Python und erstellen ein Ziel wandbox, wodurch eine der Dienst-API entsprechende Anfrage generiert wird Zauberstabbox, und sendet es. Als Antwort kommt ein Link zur fertigen Sandbox.

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 draußen

Schauen wir uns nun an, wie man alles nutzt.

Montage

Die Erstellung dieses Projekts besteht wie jedes andere Projekt auf dem CMake-Build-System aus zwei Phasen:

Generation

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

Wenn der obige Befehl aufgrund einer älteren Version von CMake nicht funktioniert hat, versuchen Sie es wegzulassen -S:

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

Mehr über Optionen.

Projektaufbau

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

Mehr über Montageziele.

Optionen

MYLIB_COVERAGE

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

Inklusive Ziel coverage, mit dem Sie mit der Messung der Codeabdeckung durch Tests beginnen können.

MYLIB_TESTING

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

Bietet die Option, Unit-Test-Build und -Ziel zu deaktivieren check. Dadurch wird die Messung der Codeabdeckung durch Tests ausgeschaltet (siehe MYLIB_COVERAGE).

Außerdem wird das Testen automatisch deaktiviert, wenn das Projekt über den Befehl als Unterprojekt mit einem anderen Projekt verbunden wird add_subdirectory.

MYLIB_DOXYGEN_LANGUAGE

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

Schaltet die Sprache der Dokumentation um, die das Ziel generiert doc zum Gegebenen. Eine Liste der verfügbaren Sprachen finden Sie unter Doxygen-Website.

Russisch ist standardmäßig aktiviert.

Montageziele

Standardmäßig

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

Wenn das Ziel nicht angegeben ist (was dem Ziel entspricht all), sammelt alles, was möglich ist, und ruft auch das Ziel auf check.

mylib-Unit-Tests

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

Kompiliert Unit-Tests. Standardmäßig aktiviert.

aus der Ferne überprüfen

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

Führt erstellte (Builds, falls noch nicht geschehen) Unit-Tests durch. Standardmäßig aktiviert.

См. также mylib-unit-tests.

Berichterstattung

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

Analysiert laufende (falls noch nicht ausgeführte) Unit-Tests auf Codeabdeckung durch Tests, die das Programm verwenden gcovr.

Die Coverage-Ausgabe sieht etwa so aus:

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

Das Ziel ist nur verfügbar, wenn die Option aktiviert ist. MYLIB_COVERAGE.

См. также check.

Dock

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

Startet die Generierung der Dokumentation für den Code mithilfe des Systems Doxygen.

Zauberstabbox

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

Die Antwort des Dienstes sieht in etwa so aus:

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

Hierzu wird der Dienst genutzt. Zauberstabbox. Ich weiß nicht, welche Rubber-Server sie haben, aber ich denke, dass man diese Funktion nicht missbrauchen sollte.

Примеры

Erstellen Sie ein Projekt im Debug-Modus mit Abdeckungsmessung

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

Installieren eines Projekts ohne Voraufbau und Tests

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

Erstellen Sie im Release-Modus durch den angegebenen Compiler

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

Erstellung der Dokumentation in englischer Sprache

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

Werkzeuge

  1. CMake 3.13

    Tatsächlich ist CMake Version 3.13 nur erforderlich, um einige der in dieser Hilfe beschriebenen Konsolenbefehle auszuführen. Aus Sicht der Syntax von CMake-Skripten ist Version 3.8 ausreichend, wenn die Generierung auf andere Weise aufgerufen wird.

  2. Testbibliothek Arzt

    Das Testen kann deaktiviert werden (siehe опцию MYLIB_TESTING).

  3. Doxygen

    Um die Sprache zu ändern, in der die Dokumentation generiert wird, besteht die Möglichkeit MYLIB_DOXYGEN_LANGUAGE.

  4. PL-Dolmetscher Python 3

    Zur automatischen Generierung Online-Sandboxen.

Statische Analyse

Mithilfe von CMake und einigen guten Tools können Sie statische Analysen mit minimalem Aufwand durchführen.

Cppcheck

Unterstützung für das in CMake integrierte statische Analysetool Cppcheck.

Nutzen Sie dazu die Option CMAKE_CXX_CPPCHECK:

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

Danach wird die statische Analyse jedes Mal automatisch gestartet, wenn die Quellen kompiliert und neu kompiliert werden. Es muss nichts extra getan werden.

Clang

Mit einem wunderbaren Werkzeug scan-build Sie können auch im Handumdrehen eine statische Analyse durchführen:

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

Anders als bei Cppcheck ist es hier erforderlich, den Build jedes Mal erneut auszuführen scan-build.

Nachwort

CMake ist ein sehr leistungsstarkes und flexibles System, mit dem Sie Funktionen für jeden Geschmack und jede Farbe implementieren können. Und obwohl die Syntax manchmal zu wünschen übrig lässt, ist der Teufel immer noch nicht so schrecklich, wie er dargestellt wird. Nutzen Sie das CMake-Build-System zum Wohle der Gesellschaft und Gesundheit.

Projektvorlage herunterladen

Source: habr.com

Kommentar hinzufügen