Cmake

Цели (targets): add_executable, add_library, типы библиотек (STATIC/SHARED/INTERFACE)

Введение

Статья 573 кратко создала цель через add_executable — в этой статье разберём концепцию целей (targets) систематически, включая add_library с тремя различающимися типами библиотек, лежащую в основе всего современного, «target-based» подхода CMake (в противовес устаревшему стилю с глобальными переменными, кратко упоминаемому в подводных камнях).

Концепция

Цель в CMake — это не просто «конечный продукт сборки» (исполняемый файл или библиотека), а полноценный объект с собственным набором свойств (директории включения, флаги компиляции, зависимости от других целей, статьи 575-576), который можно настраивать через команды вида target_* — этот «target-based» подход (стандарт для современного CMake начиная с версии 3.x) принципиально отличается от устаревшего стиля, основанного на глобальных, применяемых ко всем целям сразу переменных (include_directories() без указания конкретной цели), который существенно усложнял управление зависимостями в проектах с множеством целей. STATIC-библиотека компилируется в архив объектных файлов, физически включаемый в итоговый исполняемый файл при линковке (увеличивая его размер, но не требуя наличия отдельного файла библиотеки во время выполнения) — SHARED-библиотека компилируется в отдельный файл (.dll/.so), загружаемый динамически во время выполнения программы (статья 524 цикла синтаксиса C/C++ про динамическую память отдельная тема, здесь речь о динамической линковке) — INTERFACE-библиотека не содержит собственного кода для компиляции вовсе, существуя лишь как способ группировки и распространения настроек (директорий включения, флагов, статья 576) для целей, которые на неё ссылаются, что особенно полезно для header-only библиотек.

Пример кода: Цели (targets)

# Различные типы целей в одном проекте
add_executable(MyApp main.cpp)

add_library(MathUtils STATIC math_utils.cpp math_utils.h) # статическая — "вшивается" в каждого потребителя

add_library(NetworkUtils SHARED network_utils.cpp network_utils.h) # динамическая — отдельный файл .so/.dll

add_library(HeaderOnlyConfig INTERFACE) # НЕТ исходных файлов — только настройки для потребителей
target_include_directories(HeaderOnlyConfig INTERFACE include/) # путь к header-only заголовкам

target_link_libraries(MyApp PRIVATE MathUtils NetworkUtils HeaderOnlyConfig) # статья 575 — PRIVATE
# Выбор типа библиотеки через переменную, а не жёсткое указание STATIC/SHARED в коде
option(BUILD_SHARED_LIBS "Собирать библиотеки как динамические" OFF)

add_library(MathUtils math_utils.cpp) # БЕЗ явного STATIC/SHARED — тип определяется значением BUILD_SHARED_LIBS
# Псевдонимы целей (ALIAS) — особенно полезны при использовании библиотеки через find_package/FetchContent
add_library(MyMath::MathUtils ALIAS MathUtils) # удобное, "пространство-имён"-подобное имя для использования

Пояснения к коду

Сравнение STATIC/SHARED/INTERFACE показывает три принципиально разных подхода к организации повторно используемого кода — выбор между статической и динамической линковкой влияет на размер итогового исполняемого файла, возможность обновления библиотеки без пересборки потребителей, и сложность распространения (динамическая библиотека требует, чтобы соответствующий .so/.dll файл был доступен в системе при запуске программы), и INTERFACE-библиотека HeaderOnlyConfig демонстрирует элегантный способ «упаковать» набор настроек (в данном случае — путь к заголовочным файлам) как переиспользуемую цель, на которую другие цели могут ссылаться через target_link_libraries, даже не имея отношения к «линковке» в обычном смысле этого слова. add_library(MathUtils math_utils.cpp) без явного указания типа показывает практику, позволяющую конечному пользователю/собирающему проект выбрать желаемый тип библиотеки через глобальную переменную BUILD_SHARED_LIBS (статья 577 про переменные/кеш), что даёт гибкость без необходимости поддерживать отдельные, явно различающиеся описания цели для каждого возможного варианта. ALIAS-цель показывает практичный паттерн для библиотек, предназначенных для использования через find_package (статья 578) — псевдоним вида Namespace::Target (имитирующий пространства имён C++, статья 539 цикла синтаксиса) — общепринятое, ожидаемое соглашение для современных CMake-пакетов.

Подводные камни

  • Использование устаревшего, «директорий-ориентированного» стиля CMake (include_directories(), link_libraries() без указания конкретной цели) вместо современного target-based подхода — старый стиль применяет настройки ко ВСЕМ целям, объявленным в текущей директории и поддиректориях, что в нетривиальных проектах с множеством целей быстро становится трудно контролируемым источником случайных, неявных зависимостей между несвязанными целями, тогда как target_include_directories/target_link_libraries (статьи 575-576) дают точный, явный контроль для каждой конкретной цели по отдельности.
  • Неосознанный выбор между STATIC и SHARED без понимания практических последствий этого выбора — статическая линковка упрощает распространение (не нужно отдельно поставлять файл библиотеки), но увеличивает размер каждого исполняемого файла, использующего эту библиотеку, и требует полной пересборки всех таких исполняемых файлов при изменении библиотеки, тогда как динамическая линковка позволяет обновлять библиотеку независимо от собранных с ней исполняемых файлов (при сохранении совместимости интерфейса), но требует явного управления распространением и поиском соответствующих библиотечных файлов во время выполнения.
  • Попытка добавить исходные файлы для компиляции к INTERFACE-библиотеке — поскольку INTERFACE-цель по определению не компилирует собственный код, попытка передать ей реальные .cpp-файлы для компиляции (а не только заголовки или настройки) является логической ошибкой использования этого типа цели, и для случаев, требующих реальной компиляции кода, нужен STATIC или SHARED тип, тогда как INTERFACE предназначен исключительно для header-only кода или групп настроек, разделяемых между несколькими целями.
  • Игнорирование платформенных особенностей динамических библиотек (необходимость явного экспорта символов на Windows через __declspec(dllexport)/dllimport, в отличие от значительно более permissive поведения по умолчанию на Linux/macOS) — код, написанный и протестированный исключительно на одной платформе для SHARED-библиотеки, может не компилироваться или не линковаться корректно на другой платформе без явного учёта этих различий, что часто требует использования специального макроса видимости символов, автоматически генерируемого CMake через модуль GenerateExportHeader.