CMake und C++ — BrĂŒder fĂŒr immer

CMake und C++ — BrĂŒder fĂŒr immer

Bei der Entwicklung wechsle ich gern Compiler, Build-Modi, AbhĂ€ngigkeitsversionen, fĂŒhre statische Analysen durch, messe die Leistung, erstelle Coverage-Reports, generiere Dokumentationen usw. Und ich liebe CMake, weil es mir erlaubt, alles zu tun, was ich möchte.

Viele kritisieren CMake, oft zu Recht, aber wenn man genauer hinsieht, ist nicht alles so schlecht, und in letzter Zeit lÀuft es sogar ziemlich gut, und die Entwicklung ist durchaus positiv.

In diesem Beitrag möchte ich erklÀren, wie man eine Header-Bibliothek in C++ mit CMake relativ einfach organisieren kann, um die folgende FunktionalitÀt zu erhalten:

  1. Build;
  2. Automatischer Teststart;
  3. Code Coverage-Messung;
  4. Installation;
  5. Automatisierte Dokumentation;
  6. Generierung einer Online-Sandbox;
  7. Statische Analyse.

Wer sich bereits mit C++ und CMake auskennt, kann einfach eine Projektschablone herunterladen und damit beginnen.


Inhalt

  1. Das Projekt von innen
    1. Projektstruktur
    2. Haupt-CMake-Datei (./CMakeLists.txt)
      1. Projektinformationen
      2. Projektoptionen
      3. Kompilierungsoptionen
      4. Hauptziel
      5. Installation
      6. Tests
      7. Dokumentation
      8. Online-Sandbox
    3. Tests-Skript (test/CMakeLists.txt)
      1. Tests
      2. Abdeckung
    4. Dokumentations-Skript (doc/CMakeLists.txt)
    5. Online-Sandbox-Skript (online/CMakeLists.txt)
  2. Das Projekt von außen
    1. Bau
      1. Generierung
      2. Bau
    2. Optionen
      1. MYLIB_COVERAGE
      2. MYLIB_TESTING
      3. MYLIB_DOXYGEN_LANGUAGE
    3. Build-Ziele
      1. StandardmĂ€ĂŸig
      2. mylib-unit-tests
      3. check
      4. coverage
      5. doc
      6. wandbox
    4. Beispiele
  3. Werkzeuge
  4. Statische Analyse
  5. Nachwort

Das 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

Im Folgenden wird erlĂ€utert, wie CMake-Skripte organisiert werden, weshalb sie im Detail behandelt werden. Die ĂŒbrigen Dateien kann jeder Interessierte direkt auf der Projektvorlagenseite.

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

Projektinformationen

ZunĂ€chst muss die benötigte Version von CMake angefordert werden. CMake entwickelt sich weiter, die Kommandos Ă€ndern sich, das Verhalten unter verschiedenen Bedingungen. Damit CMake sofort versteht, was wir von ihm wollen, mĂŒssen wir unsere Anforderungen gleich festlegen.

cmake_minimum_required(VERSION 3.13)

Dann geben wir unser Projekt, seinen Namen, die Version, die verwendeten Sprachen usw. an (siehe Befehl project).

In diesem Fall geben wir die Sprache an, CXX (das bedeutet C++), damit CMake nicht verwirrt wird und nicht nach einem C-Compiler sucht (standardmĂ€ĂŸig sind in CMake zwei Sprachen aktiviert: C und C++).

project(Mylib VERSION 1.0 LANGUAGES CXX)

Hier können wir auch sofort ĂŒberprĂŒfen, ob unser Projekt in ein anderes Projekt als Unterprojekt integriert ist. Das wird in Zukunft sehr hilfreich sein.

get_directory_property(IS_SUBPROJECT PARENT_DIRECTORY)

Projektoptionen

Lassen Sie uns zwei Optionen vorsehen.

Die erste Option — MYLIB_TESTING — zur Deaktivierung von Modultests. Dies kann nĂŒtzlich sein, wenn wir sicher sind, dass mit den Tests alles in Ordnung ist und wir beispielsweise nur unser Projekt installieren oder paketieren möchten. Oder wenn unser Projekt als Unterprojekt integriert ist — in diesem Fall ist es fĂŒr die Benutzer unseres Projekts nicht interessant, unsere Tests auszufĂŒhren. Testen Sie nicht die AbhĂ€ngigkeiten, die Sie verwenden?

option(MYLIB_TESTING "Modultests aktivieren" ON)

DarĂŒber hinaus werden wir eine separate Option schaffen MYLIB_COVERAGE zur Messung der Testabdeckungsquote, die jedoch zusĂ€tzliche Werkzeuge erfordert, daher muss sie ausdrĂŒcklich aktiviert werden.

option(MYLIB_COVERAGE "Messe die Testabdeckungsquote aktivieren" OFF)

Kompilierungsoptionen

NatĂŒrlich sind wir grandiose C++-Programmierer und möchten vom Compiler das Maximum an diagnostischen Informationen zur Compile-Zeit. Keine Maus wird unentdeckt bleiben.

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
)

Auch Erweiterungen deaktivieren wir, um vollstĂ€ndig dem C++-Standard zu entsprechen. StandardmĂ€ĂŸig sind sie in CMake aktiviert.

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

Hauptziel

Unsere Bibliothek besteht nur aus Header-Dateien, weswegen wir keine Ausgaben in Form von statischen oder dynamischen Bibliotheken haben. Andererseits, um unsere Bibliothek extern zu verwenden, muss sie installiert werden, damit sie im System erkannt und in Ihr Projekt eingebunden werden kann, wobei zusammen mit ihr auch diese Header-Anweisungen gebunden werden mĂŒssen, sowie möglicherweise einige zusĂ€tzliche Eigenschaften.

Zu diesem Zweck erstellen wir eine Schnittstellenbibliothek.

add_library(mylib INTERFACE)

Binden Sie die Header an unsere Schnittstellenbibliothek.

Die moderne, trendige Nutzung von CMake impliziert, dass die Header, Eigenschaften usw. ĂŒber ein einziges Ziel ĂŒbergeben werden. Es genĂŒgt daher zu sagen target_link_libraries(target PRIVATE dependency), und alle Header, die mit dem Ziel assoziiert sind dependency, stehen den Quellcodes zur VerfĂŒgung, die zum Ziel gehören target. Und es sind keine [target_]include_directories. Dies wird unten bei der Analyse des CMake-Skripts fĂŒr Modultests demonstriert.

Es ist auch erwĂ€hnenswert, die sogenannten Generator-AusdrĂŒcke: $.

Dieser Befehl verknĂŒpft die benötigten Header mit unserer Schnittstellenbibliothek. Falls unsere Bibliothek zu einem Ziel innerhalb einer CMake-Hierarchie hinzugefĂŒgt wird, werden die Header aus dem Verzeichnis ${CMAKE_CURRENT_SOURCE_DIR}/include, und wenn unsere Bibliothek im System installiert und in ein anderes Projekt mittels des Befehls find_package, dann werden die Header aus dem Verzeichnis verknĂŒpft include relativ zum Installationsverzeichnis.

target_include_directories(mylib INTERFACE
    $
    $
)

Wir setzen den Sprachstandard fest. NatĂŒrlich den neuesten. Dabei aktivieren wir nicht nur den Standard, sondern machen ihn auch fĂŒr diejenigen verfĂŒgbar, die unsere Bibliothek nutzen werden. Dies wird dadurch erreicht, dass die eingestellte Eigenschaft die Kategorie INTERFACE (siehe den Befehl target_compile_features).

target_compile_features(mylib INTERFACE cxx_std_17)

Wir erstellen einen Alias fĂŒr unsere Bibliothek. Schönheitshalber wird er in einem speziellen „Namensraum“ angelegt. Das ist nĂŒtzlich, wenn in unserer Bibliothek verschiedene Module erscheinen und wir diese unabhĂ€ngig einfĂŒgen wollen. Wie beim Boost, zum Beispiel.

add_library(Mylib::mylib ALIAS mylib)

Installation

Die Installation unserer Header im System. Hier ist alles einfach. Wir sagen, dass der Ordner mit all den Headern in das Verzeichnis include relativ zum Installationsort soll.

install(DIRECTORY include/mylib DESTINATION include)

Anschließend informieren wir das Build-System darĂŒber, dass wir wollen, dass in externen Projekten der Befehl find_package(Mylib) aufgerufen wird und das Ziel Mylib::mylib.

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

Der nĂ€chste Zauberspruch sollte so verstanden werden. Wenn wir in einem externen Projekt den Befehl find_package(Mylib 1.2.3 REQUIRED), und die tatsĂ€chliche Version der installierten Bibliothek inkompatibel mit der Version ist, 1.2.3, wird CMake automatisch einen Fehler generieren. Das heißt, man muss die Versionen nicht manuell ĂŒberwachen.

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 der entsprechenden Option deaktiviert sind oder unser Projekt ist ein Teilprojekt, das heißt, es wird in ein anderes CMake-Projekt mit dem Befehl verbunden add_subdirectory, wir gehen nicht weiter in der Hierarchie und das Skript, das die Befehle zur Generierung und AusfĂŒhrung der Tests beschreibt, wird einfach nicht gestartet.

if(NOT MYLIB_TESTING)
    message(STATUS "Das Testen des Mylib-Projekts ist deaktiviert")
elseif(IS_SUBPROJECT)
    message(STATUS "Mylib wird im Submodul-Modus nicht getestet")
else()
    add_subdirectory(test)
endif()

Dokumentation

Die Dokumentation wird ebenfalls nicht generiert, wenn es sich um ein Teilprojekt handelt.

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

Online-Sandbox

Ähnlich wird es auch keine Online-Sandbox fĂŒr das Teilprojekt geben.

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

Tests-Skript (test/CMakeLists.txt)

Tests

Zuerst finden wir das Paket mit dem benötigten Test-Framework (ersetzen Sie es durch Ihr Lieblingsframework).

find_package(doctest 2.3.3 REQUIRED)

Wir erstellen unsere ausfĂŒhrbare Datei mit den Tests. In der Regel fĂŒge ich nur die Datei hinzu, die die Funktion enthĂ€lt, main.

add_executable(mylib-unit-tests test_main.cpp)

Die Dateien, in denen die eigentlichen Tests beschrieben sind, fĂŒge ich spĂ€ter hinzu. Aber es ist nicht unbedingt erforderlich, das so zu machen.

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

Wir fĂŒgen AbhĂ€ngigkeiten hinzu. Beachten Sie, dass wir nur die benötigten CMake-Ziele an unsere BinĂ€rdatei gebunden haben und den Befehl nicht aufgerufen haben target_include_directories. Die Header aus dem Test-Framework und aus unserem Mylib::mylib, sowie die Build-Parameter (in unserem Fall der Sprachstandard C++) wurden zusammen mit diesen Zielen ĂŒbernommen.

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

Schließlich erstellen wir ein Dummy-Ziel, dessen "Build" dem AusfĂŒhren der Tests entspricht, und fĂŒgen dieses Ziel zum Standard-Build hinzu (das wird durch das Attribut ALL). Das bedeutet, dass der Standard-Build den Testlauf initiiert, das heißt, wir vergessen niemals, sie auszufĂŒhren.

add_custom_target(check ALL COMMAND mylib-unit-tests)

Abdeckung

Dann aktivieren wir die Codeabdeckungsmessung, wenn die entsprechende Option angegeben ist. Ich werde nicht ins Detail gehen, da diese mehr mit dem Werkzeug zur Messung der Abdeckung zu tun hat als mit CMake. Es ist nur wichtig zu beachten, dass auf Grundlage der Ergebnisse ein Ziel erstellt wird coverage, mit dem es einfach ist, die Abdeckung zu messen.

find_program(GCOVR_EXECUTABLE gcovr)
if(MYLIB_COVERAGE AND GCOVR_EXECUTABLE)
    message(STATUS "Die Messung der Codeabdeckung durch Tests ist aktiviert")

    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 "FĂŒr die Messung der Codeabdeckung durch Tests wird das Programm gcovr benötigt")
endif()

Dokumentations-Skript (doc/CMakeLists.txt)

Doxygen gefunden.

find_package(Doxygen)

Als NĂ€chstes ĂŒberprĂŒfen wir, ob der Benutzer die Sprache eingestellt hat. Wenn ja, Ă€ndern wir nichts, wenn nicht, nehmen wir Russisch. Dann konfigurieren wir die Dateien des Doxygen-Systems. Alle notwendigen Variablen, einschließlich der Sprache, gelangen wĂ€hrend des Konfigurationsprozesses dorthin (siehe den Befehl configure_file).

Danach erstellen wir ein Ziel doc, das die Generierung der Dokumentation ausfĂŒhrt. Da die Generierung der Dokumentation im Entwicklungsprozess nicht die grĂ¶ĂŸte Notwendigkeit ist, wird das Ziel standardmĂ€ĂŸig nicht aktiviert und muss explizit gestartet werden.

if (Doxygen_FOUND)
    if (NOT MYLIB_DOXYGEN_LANGUAGE)
        set(MYLIB_DOXYGEN_LANGUAGE Russisch)
    endif()
    message(STATUS "Doxygen-Dokumentation wird in ${MYLIB_DOXYGEN_LANGUAGE} generiert")
    configure_file(Doxyfile.in Doxyfile)
    add_custom_target(doc COMMAND ${DOXYGEN_EXECUTABLE} ${CMAKE_CURRENT_BINARY_DIR}/Doxyfile)
endif ()

Online-Sandbox-Skript (online/CMakeLists.txt)

Hier finden wir den dritten Python und erstellen ein Ziel wandbox, das eine Anfrage generiert, die der API des Dienstes entspricht Wandbox, und sendet sie. Als Antwort erhalten wir einen 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 "FĂŒr die Erstellung eines Online-Sandbox wird ein Interpreter fĂŒr die Programmiersprache Python der 3. Version benötigt")
endif()

Das Projekt von außen

Jetzt schauen wir uns an, wie man all dies benutzt.

Bau

Der Build dieses Projekts, wie auch jedes andere Projekt im CMake-Bausystem, besteht aus zwei Phasen:

Generierung

cmake -S pfad/zur/quelle -B pfad/zum/build/verzeichnis [optionen ...]

Wenn der obige Befehl wegen einer alten Version von CMake nicht funktioniert hat, versuchen Sie es mit -S:

cmake pfad/zur/quelle -B pfad/zum/build/verzeichnis [optionen ...]

Weitere Informationen zu den Optionen.

Projekt bauen

cmake --build pfad/zum/build/verzeichnis [--target ziel]

Weitere Informationen zu den Build-Zielen.

Optionen

MYLIB_COVERAGE

cmake -S ... -B ... -DMYLIB_COVERAGE=ON [weitere optionen ...]

Aktiviert das Ziel coverage, mit dessen Hilfe man die Codeabdeckung durch Tests ĂŒberprĂŒfen kann.

MYLIB_TESTING

cmake -S ... -B ... -DMYLIB_TESTING=OFF [weitere Optionen ...]

Bietet die Möglichkeit, den Bau von Modultests und Ziel auszuschalten check. Infolgedessen wird die Codeabdeckung durch Tests abgeschaltet (siehe MYLIB_COVERAGE).

Das Testen wird auch automatisch deaktiviert, wenn das Projekt als Unterprojekt durch den Befehl angeschlossen wird add_subdirectory.

MYLIB_DOXYGEN_LANGUAGE

cmake -S ... -B ... -DMYLIB_DOXYGEN_LANGUAGE=English [weitere Optionen ...]

Wechselt die Sprache der Dokumentation, die durch das Ziel generiert wird doc auf die angegebene. Eine Liste verfĂŒgbarer Sprachen finden Sie auf der Doxygen-Systemwebsite.

StandardmĂ€ĂŸig ist Russisch aktiviert.

Build-Ziele

StandardmĂ€ĂŸig

cmake --build pfad/zum/bauverzeichnis
cmake --build pfad/zum/bauverzeichnis --target all

Wenn das Ziel nicht angegeben wird (was dem Ziel entspricht all), wird alles gesammelt, was möglich ist, und das Ziel wird aufgerufen check.

mylib-unit-tests

cmake --build pfad/zum/bauverzeichnis --target mylib-unit-tests

Kompiliert die Modultests. StandardmĂ€ĂŸig aktiviert.

check

cmake --build pfad/zur/bauverzeichnis --target check

FĂŒhrt die gesammelten (fĂŒhrt aus, wenn noch nicht geschehen) Modultests aus. StandardmĂ€ĂŸig aktiviert.

Siehe auch mylib-unit-tests.

coverage

cmake --build pfad/zur/bauverzeichnis --target coverage

Analysiert die ausgefĂŒhrten (fĂŒhrt aus, wenn noch nicht geschehen) Modultests auf Codeabdeckung mit dem Programm gcovr.

Die Ausgabe der Abdeckung wird etwa so aussehen:

------------------------------------------------------------------------------
                           GCC Code Coverage Report
Verzeichnis: /pfad/zum/cmakecpptemplate/include/
------------------------------------------------------------------------------
Datei                                       Zeilen    AusfĂŒhrung  Abdeckung   Fehlend
------------------------------------------------------------------------------
mylib/myfeature.hpp                            2       2   100%   
------------------------------------------------------------------------------
GESAMT                                          2       2   100%
------------------------------------------------------------------------------

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

Siehe auch check.

doc

cmake --build pfad/zur/bauverzeichnis --target doc

Startet die Generierung von Dokumentationen zum Code mit dem System Doxygen.

wandbox

cmake --build pfad/zur/bauverzeichnis --target wandbox

Die Antwort vom Dienst sieht etwa so aus:

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

DafĂŒr wird ein Dienst verwendet Wandbox. Ich weiß nicht, wie stabil ihre Server sind, aber ich denke, dass man diese Möglichkeit nicht ĂŒberstrapazieren sollte.

Beispiele

Projektbau im Debug-Modus mit Abdeckungserfassung

cmake -S pfad/zum/quellcode -B pfad/zum/bauverzeichnis -DCMAKE_BUILD_TYPE=Debug -DMYLIB_COVERAGE=ON
cmake --build pfad/zum/bauverzeichnis --target coverage --parallel 16

Installation des Projekts ohne vorherige Kompilierung und Tests

cmake -S pfad/zur/quellcode -B pfad/zum/bauverzeichnis -DMYLIB_TESTING=OFF -DCMAKE_INSTALL_PREFIX=pfad/zur/installationsverzeichnis
cmake --build pfad/zum/bauverzeichnis --target install

Bau im Release-Modus mit dem angegebenen Compiler

cmake -S pfad/zur/quellcode -B pfad/zum/bauverzeichnis -DCMAKE_BUILD_TYPE=Release -DCMAKE_CXX_COMPILER=g++-8 -DCMAKE_PREFIX_PATH=pfad/zur/directory/wo/abhÀngigkeiten/installiert/sind
cmake --build pfad/zum/bauverzeichnis --parallel 4

Generierung der Dokumentation in Englisch

cmake -S pfad/zur/quellcode -B pfad/zum/bauverzeichnis -DCMAKE_BUILD_TYPE=Release -DMYLIB_DOXYGEN_LANGUAGE=English
cmake --build pfad/zum/bauverzeichnis --target doc

Werkzeuge

  1. CMake 3.13

    TatsĂ€chlich wird CMake-Version 3.13 nur fĂŒr das AusfĂŒhren einiger Konsolenbefehle benötigt, die in dieser Dokumentation beschrieben sind. Was die Syntax der CMake-Skripte betrifft, reicht Version 3.8 aus, wenn die Generierung auf andere Weise aufgerufen wird.

  2. Testbibliothek doctest

    Tests können deaktiviert werden (siehe Option MYLIB_TESTING).

  3. Doxygen

    Um die Sprache zu wechseln, in der die Dokumentation generiert wird, gibt es eine Option MYLIB_DOXYGEN_LANGUAGE.

  4. Interpreter fĂŒr Programmiersprachen Python 3

    FĂŒr die automatische Generierung Online-Sandbox.

Statische Analyse

Mit CMake und ein paar guten Werkzeugen kann eine statische Analyse mit minimalem Aufwand sichergestellt werden.

Cppcheck

In CMake ist die UnterstĂŒtzung fĂŒr ein statisches Analysetool integriert Cppcheck.

DafĂŒr muss die Option verwendet werden CMAKE_CXX_CPPCHECK:

cmake -S pfad/zur/quellcode -B pfad/zum/bauverzeichnis -DCMAKE_BUILD_TYPE=Debug -DCMAKE_CXX_CPPCHECK="cppcheck;--enable=all;-Ipfad/zur/quellcode/include"

Danach wird die statische Analyse automatisch bei jeder Kompilierung und Neukompilierung des Quellcodes gestartet. Es sind keine weiteren Maßnahmen erforderlich.

Microsoft Visual C++

Mit dem wundervollen Werkzeug scan-build kann auch die statische Analyse im Handumdrehen gestartet werden:

scan-build cmake -S pfad/zur/quellcode -B pfad/zum/bauverzeichnis -DCMAKE_BUILD_TYPE=Debug
scan-build cmake --build pfad/zum/bauverzeichnis

Hier muss im Gegensatz zum Fall mit Cppcheck jedes Mal der Bau ĂŒber scan-build.

Nachwort

CMake ist ein sehr leistungsfĂ€higes und flexibles System, das es ermöglicht, FunktionalitĂ€ten ganz nach Wunsch zu realisieren. Und obwohl die Syntax manchmal zu wĂŒnschen ĂŒbrig lĂ€sst, ist der Teufel nicht so schlimm, wie er dargestellt wird. Nutzen Sie das CMake-Bausystem zum Wohle der Gesellschaft und zur Gesundheit.

→ Projektvorlage herunterladen

Quelle: habr.com

60GB SSD 8Gb DDR4