Cmake

CMake: организация многомодульного Qt-проекта (target_link_libraries, INTERFACE)

Введение

По мере роста Qt-проекта единый монолитный CMakeLists.txt со всеми исходниками становится неудобен: компиляция замедляется, границы между подсистемами размываются, а повторное использование кода в других проектах требует копирования файлов. Решение — разбить проект на модули (библиотеки), каждый со своим CMakeLists.txt, и связать их через target_link_libraries. В статье разберём, как организовать многомодульный Qt-проект и в чём разница между PUBLIC, PRIVATE и INTERFACE зависимостями.

Концепция

Современный CMake (с версии 3.x, target-based подход) описывает зависимости не как глобальные флаги компилятора, а как свойства конкретных целей (targets). Когда модуль A линкуется с модулем B через target_link_libraries(A PRIVATE B), заголовки и флаги B становятся видны при компиляции A, но не «протекают» дальше — в код, который линкуется с A. PUBLIC означает, что зависимость нужна и для компиляции самого A, и распространяется на всех, кто линкуется с A. INTERFACE — зависимость не нужна для компиляции A самого, но обязательна для тех, кто его использует (типичный случай для header-only библиотек или модулей, которые просто агрегируют флаги).

Правильный выбор видимости — PRIVATE по умолчанию, и только при реальной необходимости PUBLIC — снижает связанность модулей и ускоряет повторную сборку при изменении внутренних деталей.

Пример кода: CMake

# core/CMakeLists.txt — базовая библиотека без UI-зависимостей
add_library(AppCore STATIC
    src/datamodel.cpp
    src/datamodel.h
)
target_include_directories(AppCore PUBLIC include)
target_link_libraries(AppCore PUBLIC Qt6::Core)

# network/CMakeLists.txt — модуль сети, зависит от AppCore
add_library(AppNetwork STATIC
    src/apiclient.cpp
    src/apiclient.h
)
target_link_libraries(AppNetwork
    PUBLIC AppCore
    PRIVATE Qt6::Network
)

# ui/CMakeLists.txt — модуль интерфейса, видит AppCore и AppNetwork,
# но детали реализации сети (Qt6::Network) ему PRIVATE и не нужны
add_library(AppUi STATIC
    src/mainwindow.cpp
    src/mainwindow.h
)
target_link_libraries(AppUi
    PUBLIC AppCore
    PRIVATE AppNetwork Qt6::Widgets
)

# Главный CMakeLists.txt — собирает исполняемый файл
add_executable(MyApp src/main.cpp)
target_link_libraries(MyApp PRIVATE AppUi)

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

AppCore объявляет Qt6::Core как PUBLIC — это означает, что любой модуль, линкующийся с AppCore, автоматически получает доступ к Qt Core, что разумно, поскольку публичные заголовки AppCore сами используют типы из Qt Core. AppNetwork зависит от Qt6::Network только PRIVATE — потому что использование сети — деталь реализации, не «протекающая» в публичный интерфейс AppNetwork (если, конечно, публичные заголовки модуля не возвращают типы из QtNetwork напрямую).

AppUi демонстрирует ключевую идею: AppNetwork указан как PRIVATE, то есть MyApp, линкуясь с AppUi, не получает прямого доступа к AppNetwork и не «видит» его заголовки — это инкапсулирует сетевой слой внутри UI-модуля, если внешний код не должен напрямую обращаться к API сети.

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

  • Объявление всех зависимостей как PUBLIC «на всякий случай» превращает граф зависимостей в плоскую кашу, где любое изменение в глубине дерева требует пересборки всего проекта — это убивает главное преимущество модульной архитектуры.
  • Циклические зависимости между модулями (AppCore зависит от AppUi, а AppUi от AppCore) не поддерживаются статической линковкой CMake и потребуют рефакторинга — выделения общего кода в отдельный, более низкоуровневый модуль.
  • Несогласованность target_include_directories. Если заголовки указаны как PRIVATE include, а должны быть видны зависимым модулям, при сборке зависимого таргета компилятор не найдёт нужные заголовки — частая причина непонятных ошибок file not found при рефакторинге структуры проекта.
  • Смешивание старого (directory-based, include_directories(), link_directories()) и нового (target-based) стиля CMake в одном проекте усложняет понимание реальных зависимостей — современный CMake рекомендует полностью отказаться от директивных команд в пользу target_*.