
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ść:
- Kompilacja;
- Automatyczne uruchamianie testów;
- Mierzenie pokrycia kodu;
- Instalacja;
- Autodokumentowanie;
- Generowanie online sandbox;
- Analiza statyczna.
Kto już zna C++ i CMake może po prostu i zacząć go używać.
Spis treści
.
├── 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.cppGłó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 .
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. ).
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)
Zakładamy dwie opcje.
Pierwsza opcja — — 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ę 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)
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()
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ć , 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 .
Warto również zwrócić uwagę na tzw. .
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 , 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. ).
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. .
add_library(Mylib::mylib ALIAS mylib)
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)
Jeśli testy są wyłączone jednoznacznie za pomocą lub nasz projekt jest podprojektem, co oznacza, że jest podłączony do innego projektu CMake za pomocą polecenia , 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 również nie będzie generowana w przypadku podprojektu.
if(NOT IS_SUBPROJECT)
add_subdirectory(doc)
endif()
Podobnie, nie będzie również internetowej piaskownicy dla podprojektu.
if(NOT IS_SUBPROJECT)
add_subdirectory(online)
endif()
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)
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 , 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()
.
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 ).
Po czym tworzymy cel , 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 ()
Tutaj znajdujemy trzeci Python i tworzymy cel , który generuje zapytanie zgodne z API serwisu , 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()
Teraz zobaczmy, jak z tego wszystkiego korzystać.
Budowa tego projektu, jak i każdego innego projektu w systemie budowy CMake, składa się z dwóch etapów:
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 ...]
.
cmake --build ścieżka/do/katalogu/budowy [--target cel].
cmake -S ... -B ... -DMYLIB_COVERAGE=ON [inne opcje ...]Włącza cel , za pomocą której można uruchomić pomiar pokrycia kodu testami.
cmake -S ... -B ... -DMYLIB_TESTING=OFF [inne opcje ...]Oferuje możliwość wyłączenia kompilacji testów jednostkowych oraz celu . W konsekwencji wyłączany jest pomiar pokrycia kodu testami (zob. ).
Testowanie jest również automatycznie wyłączane, jeśli projekt jest dołączany w innym projekcie jako podprojekt za pomocą .
cmake -S ... -B ... -DMYLIB_DOXYGEN_LANGUAGE=English [inne opcje ...]Przełącza język dokumentacji generowanej przez cel na wskazany. Lista dostępnych języków znajduje się na .
Domyślnie włączony jest język rosyjski.
cmake --build path/to/build/directory
cmake --build path/to/build/directory --target allJeśli cel nie jest określony (co jest równoważne celowi wszystko), kompiluje wszystko, co możliwe, a także wywołuje cel .
cmake --build path/to/build/directory --target mylib-unit-testsKompiluje testy jednostkowe. Włączone domyślnie.
cmake --build ścieżka/do/katalogu/budowy --target checkUruchamia zebrane (kompiluje, jeśli jeszcze nie) testy jednostkowe. Włączone domyślnie.
Zob. także .
cmake --build ścieżka/do/katalogu/budowy --target coverageAnalizuje uruchomione (uruchamia, jeśli jeszcze nie) testy jednostkowe pod kątem pokrycia kodu testami przy użyciu programu .
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 .
Zob. także .
cmake --build ścieżka/do/katalogu/budowy --target docUruchamia generację dokumentacji kodu za pomocą systemu .
cmake --build ścieżka/do/katalogu/budowy --target wandboxOdpowiedź 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 . Nie wiem, jak bardzo ich serwery są elastyczne, ale myślę, że nie warto nadużywać tej możliwości.
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 16Instalacja 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 installBudowanie 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 4Generowanie 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
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.
Biblioteka testowa
Testowanie można wyłączyć (zob. ).
Aby przełączyć język, w którym zostanie wygenerowana dokumentacja, przewidziano opcję .
Interpreter Języka Programowania
Do automatycznego generowania .
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 .
W tym celu należy skorzystać z opcji :
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 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/budowyTutaj, w przeciwieństwie do przypadku z Cppcheck, należy za każdym razem uruchamiać budowę przez scan-build.
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.
→
Źródło: habr.com
