В предыдущей статье мы создали простой проект при помощи CMake и начали рассматривать конструкции языка описания сборки. Одним из недостатков полученного файла CMakeLists.txt является то, что в него включен список всех исходников, что нормально для небольшого проекта, но становится проблемой по мере его увеличения. Дерево проекта имеет следующий вид.
Вынесем список исходников в отдельный файл sources.cmake, поместив этот файл в директорию src.
sources.cmake
set(sources src/main.cpp
src/arithmetic.cpp
src/logic.cpp)
Переменная sources содержит список имен файлов проекта. Обратите внимание на то, что указание пути к файлу является обязательным, несмотря на то что все файлы находятся в одной директории src. Теперь файл CMakeLists.txt несколько уменьшится и примет следующий вид (изменения выделены жирным шрифтом):
cmake_minimum_required(VERSION 3.15)
set(ProjectName Calculator)
project(${ProjectName} CXX)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin)
include(src/sources.cmake)
if (CMAKE_BUILD_TYPE STREQUAL Debug)
message("build debug version of project")
else()
message("build release version of project")
endif()
message(${CMAKE_BINARY_DIR})
add_executable(${ProjectName} ${sources})
Для того чтобы переменная sources была видна для CMakeLists.txt необходимо подключить sources.cmake при помощи команды include(src/sources.cmake) аналогично тому как подключаются заголовочные файлы в C++. Далее указываем переменную sources в директиве add_executable. Во время построения проекта CMake подставит список исходников вместо sources. Согласно документации add_executable(<name> <sources>...) добавляет цель (исполняемый файл) с именем name в проект с использованием исходников, задаваемых в sources.
Что такое цель? Цель представляет собой исполняемый файл, библиотеку или утилиту, собранную CMake. Она создается при помощи команд add_library, add_executable, add_custom_target. Одна цель может зависеть от других, создавая таким образом граф зависимостей при построении проекта. На первом шаге строятся цели, которые не зависят от других. Далее собираются цели, зависящие от построенных на первом шаге, и процесс повторяется пока не будет построена конечная цель.
Приведенное разбиение файлов сборки имеет недостаток, связанный с тем, что переменная sources вводится в глобальное пространство имен CMake. Команда include не создает новую область видимости для переменных, задаваемых в файле, который передается ей в качестве аргумента. Перечисление исходников во включаемых файлах становится проблемой при разрастании дерева исходников проекта, так как возрастает риск того, что в двух и более файлах будут содержаться переменные с одинаковым именем. Кроме того любые изменения в конфигурации влекут за собой необходимость пере компиляции всех исходников, даже если изменения не касаются некоторых из них. Также все пути во включаемых файлах задаются относительно корневой директории и их необходимо указывать для каждого файла, что противоречит одному из ключевых принципов программирования DRY - don't repeat yourself (не повторяться).
Альтернативным решением является использование команды add_subdirectory(source_dir [binary_dir] [EXCLUDE_FROM_ALL] [SYSTEM]). Она добавляет каталог source_dir к сборке. При желании мы можем указать путь, по которому будут записаны собранные файлы ( параметр binary_dir). Необязательный параметр EXCLUDE_FROM_ALL позволяет отключить автоматическую сборку целей, определенных в подкаталоге. Это может быть полезно для разделения частей проекта, которые не нужны для основного функционала. Необязательный параметр SYSTEM здесь рассматривать не будем. Команда add_subdirectory определяет путь source_dir относительно текущего каталога и ищет файл CMakeLists.txt, находящийся в нем. Этот файл анализируется в отдельной области видимости, устраняя проблемы, описанные выше:
• Переменные изолированы в отдельной области.
• Вложенные артефакты можно настраивать независимо.
• Изменение вложенного файла CMakeLists.txt не требует перестройки несвязанных целей.
• Пути локализуются в каталоге и могут быть добавлены в родительский путь включения при желании.
Для реализации описанного решения добавим CMakeLists.txt в директорию src. Файловая структура проекта примет следующий вид:
src/CMakeLists.txt выглядит следующим образом:
add_library(src OBJECT main.cpp arithmetic.cpp logic.cpp)
target_include_directories(src PUBLIC .)
Как было сказано выше, команда add_library создает цель - библиотеку с именем src и добавляет ее в сборку. Необходимо отметить что наиболее часто создаются статические или динамические библиотеки. Общая форма команды имеет следующий вид:
add_library(<name> [<type>] [EXCLUDE_FROM_ALL] <sources>...)
Здесь name- название библиотеки (цели). Необязательный параметр type определяет тип создаваемой библиотеки и может принимать следующие значения:
- STATIC - статическая библиотека.
- SHARED - динамическая библиотека, которая может связываться (линковаться) с другими целями и загружаться во время выполнения программы.
- MODULE - плагин, который может не связываться с другими целями, но динамически загружаться во время выполнения.
sources - список названий файлов. В нашем случае используется другой тип библиотеки - объектная. Общий вид команды для ее создания add_library(<name> OBJECT <sources>...). Тип библиотеки OBJECT определяет коллекцию объектных файлов, полученную в результате компиляции заданных исходных файлов. Эта коллекция может использоваться в качестве входных данных для других целей. Шаг связывания других целей будет использовать объектные файлы из объектных библиотек.
Команда target_include_directories(<target> [SYSTEM] [AFTER|BEFORE] <INTERFACE|PUBLIC|PRIVATE> [items1...] [<INTERFACE|PUBLIC|PRIVATE> [items2...] ...]) задает подключаемые директории, которые необходимо использовать при компиляции цели(target). Цель, как и ранее, задается командой add_executable или add_library. Ключевые слова INTERFACE, PUBLIC и PRIVATE требуются для указания области действия аргументов, следующих за ними (items1, items2 и т.д.), в данном случае - директории включения. В файле CMakeLists.txt из директории src в качестве директории включения выступает текущая директория (символ "точка").
calculator/CMakeLists.txt:
cmake_minimum_required(VERSION 3.15)
set(ProjectName Calculator)
project(${ProjectName} CXX)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin)
add_subdirectory(src)
if (CMAKE_BUILD_TYPE STREQUAL Debug)
message("build debug version of project")
else()
message("build release version of project")
endif()
message(${CMAKE_BINARY_DIR})
add_executable(${ProjectName})
target_link_libraries(${ProjectName} PRIVATE src)
От предыдущей версии этот файл отличается тем, что к сборке добавляется каталог src при помощи команды add_subdirectory. Также происходит связывание объектной библиотеки src c исполняемым файлом при помощи команды target_link_libraries(<target> <PRIVATE|PUBLIC|INTERFACE> <item>... [<PRIVATE|PUBLIC|INTERFACE> <item>...]...). target имеет тот же смысл, что и в уже рассмотренных командах. Наилучшее, на мой взгляд, объяснение ключевым словам PRIVATE PUBLIC INTERFACE дано в этом ответе. Приведу его здесь в укороченном виде.
- PRIVATE служит для того, чтобы указать какие элементы (исходники, библиотеки, цели) необходимы для сборки этой цели. Если цель A зависит от цели Target, у которой в качестве приватной зависимости указана цель Dep, то A не будет зависеть от Dep, а Target будет использовать Dep при сборке себя.
- INTERFACE служит для указания элементов, которые не нужны для сборки этой цели, но они должны быть «прокинуты» для любой цели, которая данную цель указывает в качестве зависимости. Если цель A зависит от цели Target, у которой в качестве интерфейсной зависимости указана цель Dep, то A будет зависеть от Dep, но Target не будет использовать Dep при сборке себя.
- PUBLIC аналогичен связке INTERFACE и PRIVATE. Если цель A зависит от цели Target, у которой в качестве публичной зависимости указана цель Dep, то A будет зависеть от Dep, а Target будет использовать Dep при сборке себя.
По мере увеличения количества файлов в проекте хранение их в одной директории src приведет к ее захламлению. Ситуацию будет усугублять необходимость добавления внешних библиотек, тестов, генераторов документации, покрытия и т.д. В связи с этим логично будет разделить исходники по директориям в соответствии с функционалом. Допустим наш проект калькулятора расширяется, и принято решение разнести исходники по директориям arithmetic и logic. При этом из исходников, находящихся в каждой из этих директорий будут собраны статические библиотеки. Названия библиотек совпадают с названиями директорий. Тогда структура проекта примет следующий вид:
Она проста, интуитивно понятна, расширяема и будет использоваться в дальнейшем. Кроме исходников в каталогах arithmetic и logic также присутствуют файлы CMakeLists.txt.
arithmetic/CMakeLists.txt:
add_library(arithmetic STATIC arithmetic.cpp)
target_include_directories(arithmetic PUBLIC .)
logic/CMakeLists.txt:
add_library(logic STATIC logic.cpp)
target_include_directories(logic PUBLIC .)
В каждом файле определяется цель для сборки - статическая библиотека. В качестве директории для поиска заголовочных файлов указывается текущая директория. При добавлении нового функционала cpp файлы добавляются в аргументы команды add_library после ключевого слова STATIC.
src/CMakeLists.txt:
cmake_minimum_required(VERSION 3.15)
set(ProjectName Calculator)
project(${ProjectName} CXX)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin)
add_subdirectory(arithmetic)
add_subdirectory(logic)
if (CMAKE_BUILD_TYPE STREQUAL Debug)
message("build debug version of project")
else()
message("build release version of project")
endif()
message(${CMAKE_BINARY_DIR})
add_executable(${ProjectName} main.cpp)
target_link_libraries(${ProjectName} PRIVATE arithmetic logic)
Необходимо отметить что благодаря команде target_include_directories пути включения logic.h и arithmetic.h в файле main.cpp не поменялись несмотря на то, что эти файлы были перемещены в другие папки. При помощи add_subdirectory добавлены обе новые директории. Кроме того командой target_link_libraries к проекту линкуются обе библиотеки.
Файл CMakeLists.txt также присутствует в корневой директории проекта и имеет следующий вид:
cmake_minimum_required(VERSION 3.15)
set(ProjectName Main)
project(${ProjectName} CXX)
add_subdirectory(src)
По сути этот файл нужен только для того чтобы указать CMake директорию в которой нужно искать CMakeLists.txt с основной целью для сборки. В следующей статье добавим в корневой CMakeLists путь для сборки цели с тестами. Запустим сборку (у меня корневая директория называется не calculator, а сalc_17).
Как видно из лога сборки, выводимого CMake, вначале была собрана цель arithmetic (static library libarithmetic.a), затем цель logic(static library liblogic.a) в том порядке, в котором они перечислены в команде target_link_libraries файла src/CMakeLists.txt. И только после этого собрана цель Calculator, так как она зависит от предыдущих двух целей. Получившийся проект можно посмотреть на гитхабе. В следующей статье начнем тему подключения к проекту внешних библиотек.