Работа с современными программными проектами затруднительна без использования сторонних библиотек. Такие библиотеки часто называют внешними зависимостями. Таким образом эффективное управление зависимостями является одной из важных задач для команды разработчиков. При этом ручное управление часто требует много времени на настройку и постоянную поддержку зависимостей. К счастью для автоматизации решения данной задачи отлично подходит CMake. Так как CMake фактически является стандартом в области автоматизации сборки, то многие библиотеки (их также называют пакетами) совместимы с ней.
Зависимости можно разделить на два типа. К первому относятся те, которые уже установлены в системе и требуется только указать CMake как их найти. Ко второму относятся библиотеки, не установленные в системе, например находящиеся в сетевом репозитории. Основными методами добавления зависимостей в сборку являются: команда find_package для зависимостей первого типа и модуль FetchContent для зависимостей второго типа.
Команда find_package(<Package> [version]) ищет пакет, заданный именем Package. Для него может быть задан необязательный параметр version. Команда работает в двух режимах: режим модуля и режим конфигурации. В режиме модуля команда ищет файл с именем Find<Package>.cmake. Сначала она ищет по пути, заданному в CMAKE_MODULE_PATH, а затем в директории с установленным CMake. Если модуль поиска найден, он загружается для поиска отдельных компонентов пакета. Модули поиска содержат специфичные для пакета знания о библиотеках и других файлах, которые они ожидают найти, и внутренне используют команды, такие как find_library, для их поиска. CMake предоставляет модули поиска для многих распространенных пакетов, список которых можно посмотреть здесь.
В режим конфигурации find_package переходит после неудачной попытки найти модуль поиска. В режиме конфигурации команда ищет файл конфигурации пакета с именем <Package>Config.cmake или <package>-config.cmake. Учитывая имя пакета, команда find_package выполняет поиск, используя множестве путей вида <prefix>/lib/<package>/<package>-config.cmake. Здесь prefix - путь поиска, генерируемый CMake. Подробности можно найти здесь. Допустим мы хотим найти пакет GTest чтобы воспользоваться функционалом библиотеки GoogleTest. Тогда команда find_package(GTest) сначала будет искать файл FindGTest.cmake, а в случае неудачи - файл GTestConfig.cmake. Подробно на поиске предварительно установленных пакетов останавливаться не будем и перейдем к рассмотрению вопросов управления зависимостями, отсутствующими в системе, при помощи FetchContent.
Модуль FetchContent выполняет следующие функции:
• Управление структурой каталогов для внешнего проекта.
• Загрузка исходников из URL, а также их извлечение из архивов при необходимости.
• Работа с популярными форматами репозиториев: Git, Subversion, Mercurial и CVS.
• Скачивание обновлений при необходимости.
• Настройка и сборка загруженной внешней зависимости с помощью CMake, Make.
Использование модуля FetchContent включает три основных шага:
1. Добавление модуля в свой проект с помощью команды include(FetchContent).
2. Настройка зависимости с помощью команды FetchContent_Declare. Она сообщит FetchContent, где находятся зависимости и какую версию следует использовать.
3. Завершение настройки зависимостей с помощью команды FetchContent_MakeAvailable.
Команда FetchContent_Declare имеет следующую сигнатуру:
FetchContent_Declare(<depName> <contentOptions>...)
depName — это уникальный идентификатор зависимости, который позже будет использоваться командой FetchContent_MakeAvailable. contentOptions предоставляет информацию о конфигурации зависимости.
Если в проекте больше одной зависимости, то необходимо написать вызов команды FetchContent_Declare для каждой из них. При этом идентификатор каждой зависимости должен быть уникальным. Нет необходимости вызывать команду FetchContent_MakeAvailable больше одного раза, поскольку в качестве ее аргумента поддерживается список идентификаторов зависимостей. При этом вызов команды будет иметь следующий вид: FetchContent_MakeAvailable(lib-depA lib-depB lib-depC). Идентификаторы зависимостей нечувствительны к регистру.
Рассмотрим наиболее распространенный сценарий получения зависимости - ее скачивание из сети Интернет. Как было сказано выше, CMake поддерживает множество источников загрузки: HTTP-сервер, Git, Subversion и т.д. Общий вид команды: FetchContent_Declare( <name> <contentOptions>...). Здесь name - имя зависимости. В большинстве случаев contentOptions представляет собой две пары ключ - значение, определяющих метод загрузки и специфичные для метода детали, такие как тег коммита или хэш архива. Например URL и URL_HASH или GIT_REPOSITORY и GIT_TAG. Для получения данных c HTTP-сервера команда может иметь следующий вид:
FetchContent_Declare(
myCompanyIcons
URL https://intranet.mycompany.com/assets/iconset_1.12.tar.gz
URL_HASH MD5=5588a7b18261c20068beabfb4f530b87
)
Перед скачиванием зависимостей из Git репозитория необходимо предварительно убедиться что данная программа установлена на Вашей операционной системе. Для этого можно использовать команду git --version. Подойдет версия 1.6.5 или более поздняя. Общий вид команды для загрузки проекта из Git:
FetchContent_Declare(dependency-id
GIT_REPOSITORY <url>
GIT_TAG <tag>)
<url> и <tag> должны быть совместимы с командой git, то есть содержать адрес реально существующего репозитория, в котором имеется коммит с указанным индентификатором или тегом, например
FetchContent_Declare(
googletest
GIT_REPOSITORY https://github.com/google/googletest.git
GIT_TAG 703bd9caab50b139428cea1aaff9974ebee5742e
)
При этом в поле GIT_TAG можно указывать как тэг, например v1.15.0 для GoogleTest, так и идентификатор коммита.
Зависимости не обязательно должны быть предварительно собраны для использования их с CMake. Их можно собрать из исходных кодов как часть основного проекта. Модуль FetchContent предоставляет функциональность для загрузки контента (в первую очередь исходного кода) и добавления его в основной проект, если зависимость для своей сборки из исходников также использует CMake. Исходные коды зависимости будут собраны вместе с остальной частью проекта. Когда зависимость добавляется с помощью FetchContent, проект ссылается на цель, представляющую собой эту зависимость, как и на любую другую цель.
Перейдем, наконец, от теории к практике. Подключим к проекту библиотеку GoogleTest и напишем несколько тестов. Репозиторий GoogleTest содержит два проекта: GTest - основной фреймворк для тестирования и GMock - библиотеку, которая добавляет функционал имитационных объектов (mock objects), о которой будет рассказано в другой статье. Таким образом можно загрузить оба проекта одним вызовом FetchContent. Перед добавлением библиотеки дерево каталогов проекта имеет вид, приведенный в конце предыдущей статьи. Добавим в корневую директорию проекта папку test. В ней создадим CMakeLists.txt с кодом настройки библиотеки, а также два файла arithmetic_test.cpp и logic_test.cpp c тестами арифметических и логических функций нашего калькулятора. Следуя принципам чистой архитектуры, выполняем разделение тестов по файлам согласно функционалу. Дерево проекта примет следующий вид:
test/CMakeLists.txt
include(FetchContent)
FetchContent_Declare(googletest
GIT_REPOSITORY https://github.com/google/googletest.git
GIT_TAG v1.15.0)
# For Windows: Prevent overriding the parent project's compiler/linker settings
set(gtest_force_shared_crt ON CACHE BOOL "" FORCE)
FetchContent_MakeAvailable(googletest)
add_executable(unit_tests arithmetic_test.cpp logic_test.cpp)
target_link_libraries(unit_tests PRIVATE arithmetic logic gtest_main)
include(GoogleTest)
gtest_discover_tests(unit_tests)
В файле CMakeLists.txt указываем что хотим получить библиотеку, отсутствующую в системе, при помощи FetchContent. Далее добавляем цель unit_tests командой add_executable и линкуем цели - библиотеки с функционалом калькулятора (arithmetic и logic), а также библиотеку тестирования gtest_main. В конце вызываем gtest_discover_tests для поиска тестов в нашей цели - исполняемом файле unit_tests.
Тесты группируются в тестовые наборы (test suite). Объединенные наборы тестов составляют тестовую программу (test program). Для написания тестов используются макросы, в том числе TEST. Его общий вид:
TEST(TestSuiteName, TestName) {
... statements ...
}
Первый аргумент (TestSuiteName) задает название тестового набора, которое должно быть уникальным. Второй (TestName) - название теста. Оно может быть одинаковым в двух разных тестовых наборах.
Внутри можно объявлять переменные, вызывать тестируемые функции и сравнивать возвращаемый ими результат с ожидаемым, например при помощи макроса EXPECT_EQ(arg1, arg2), который определяет равны ли переданные ему аргументы. Для работы с GoogleTest нужно добавить gtest.h в файл с тестами. Далее приведены примеры тестовых наборов для арифметических и логических операций.
test/arithmetic_test.cpp
#include <gtest/gtest.h>
#include "arithmetic.h"
TEST(ArithmeticTest, Sum) {
int a = 10;
int b = 20;
EXPECT_EQ(sum(a, b), 30);
}
TEST(ArithmeticTest, Sub) {
int a = 10;
int b = 20;
EXPECT_EQ(sub(a, b), -10);
}
TEST(ArithmeticTest, Mult) {
int a = 10;
int b = 20;
EXPECT_EQ(mult(a, b), 200);
}
Необходимо понимать, что тесты приведены только для демонстрации работоспособности сборки. В реальном проекте тестов должно быть гораздо больше.
test/logic_test.cpp
#include <gtest/gtest.h>
#include "logic.h"
TEST(LogicTest, BitAnd) {
int a = 1;
int b = 2;
EXPECT_EQ(bitwise_and(a, b), 0);
}
TEST(LogicTest, BitOr) {
int a = 1;
int b = 2;
EXPECT_EQ(bitwise_or(a, b), 3);
}
TEST(LogicTest, BitXor1) {
int a = 1;
int b = 2;
EXPECT_EQ(bitwise_xor(a, b), 3);
}
В файл CMakeLists.txt, находящимся в корне проекта, добавим директорию с тестами при помощи вызова команды add_subdirectory(test bin). Параметр bin задает каталог, куда будет записана собранная тестовая программа.
test/CMakeLists.txt
cmake_minimum_required(VERSION 3.15)
set(ProjectName Main)
project(${ProjectName} CXX)
add_subdirectory(src)
add_subdirectory(test bin)
Перейдем в корневой каталог проекта и запустим сборку (команда cmake -B build) . В результате будет создана директория build, а в ней поддиректория _deps, куда будет скачан GoogleTest. Далее перейдем в каталог build и введем команду cmake --build .
Как видно из лога CMake вначале построил тестируемые библиотеки и цель Calculator, далее собрал библиотеки gtest и gtest_main из скачанных исходников. После этого была собрана тестовая программа unit_tests.
Запустив этот файл убедимся в том, что все тесты прошли успешно и тестируемый функционал работает как и ожидалось.
Если в каком-то тестовом наборе допущена ошибка, например в TEST(ArithmeticTest, Mult) вместо EXPECT_EQ(mult(a, b), 200) напишем EXPECT_EQ(mult(a, b), 250), то GoogleTest выдаст отчет, в котором покажет какой тест завершился с ошибкой и выведет ожидаемое и фактическое значение.
Если нас интересуют только тесты, завершившиеся с ошибкой, то при запуске тестовой программы нужно указать параметр --gtest_brief=1 . Получим следующий отчет:
В следующей статье рассмотрим подключение к программе внешних зависимостей при помощи пакетного менеджера Conan.