Структура великого проєкту: підпапки, модулі CMake, PCH та install
Структура великого проєкту: підпапки, модулі CMake, PCH та install
🎯 Мета лекції
- Розмістити код проєкту за усталеною структурою папок.
- Розділити
CMakeLists.txtна кореневий і підпапкові та зв'язати їх черезadd_subdirectory. - Винести повторювані налаштування у власний
.cmakeмодуль. - Прискорити компіляцію попередньо скомпільованими заголовками.
- Встановити готовий проєкт командою
cmake --install.
🔑 Ключові терміни
- Підпроєкт (subdirectory): папка з власним
CMakeLists.txt, підключена черезadd_subdirectory. - Модуль CMake: файл
.cmake, який підключається командоюinclude(). - PCH (precompiled header): заголовок, який компілюється один раз і використовується в усіх файлах.
- Інсталяція (install): копіювання результатів збірки у цільову папку (
/usr/local,C:\Program Files).
Одного CMakeLists.txt вистачає, доки проєкт складається з кількох файлів. Коли з'являються внутрішні бібліотеки, тести та приклади, один файл у кілька сотень рядків стає важко читати. Розв'язок: розділити конфігурацію так само, як розділено код.
Рекомендована структура папок

Призначення папок:
| Папка | Вміст |
|---|---|
include/myproject/ | Публічні заголовки, які бачать користувачі проєкту. Підпапка з назвою проєкту унеможливлює конфлікти імен файлів |
src/ | Реалізація (.cpp) та закриті заголовки |
libs/ | Внутрішні бібліотеки. Кожна має власні include/, src/ та CMakeLists.txt |
external/ | Сторонній код, скопійований або підключений як submodule |
cmake/ | Власні модулі та допоміжні скрипти CMake |
tests/ | Тести з окремим CMakeLists.txt |
examples/ | Приклади використання, які збираються за опцією |
docs/, scripts/ | Документація й допоміжні скрипти збірки |
build/ | Результат збірки, додається в .gitignore |
Структура не є вимогою CMake. Це домовленість, якої дотримується більшість open-source проєктів, і саме її очікують побачити нові учасники команди.
Кореневий CMakeLists.txt
Кореневий файл задає загальні налаштування і підключає підпапки. Конкретні цілі в ньому майже не описуються.
cmake_minimum_required(VERSION 3.15)
# Опис проєкту
project(MyProject
VERSION 1.2.3
DESCRIPTION "Awesome C++ project"
LANGUAGES CXX
)
# Налаштування C++
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
# Власні модулі CMake
list(APPEND CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake")
include(CompilerWarnings)
# Опції проєкту
option(BUILD_TESTS "Build tests" ON)
option(BUILD_EXAMPLES "Build examples" ON)
option(BUILD_SHARED_LIBS "Build shared libraries" OFF)
# Виведення інформації
message(STATUS "=================================")
message(STATUS " ${PROJECT_NAME} v${PROJECT_VERSION}")
message(STATUS "=================================")
message(STATUS "Build type: ${CMAKE_BUILD_TYPE}")
message(STATUS "C++ standard: ${CMAKE_CXX_STANDARD}")
message(STATUS "Compiler: ${CMAKE_CXX_COMPILER_ID} ${CMAKE_CXX_COMPILER_VERSION}")
message(STATUS "Build tests: ${BUILD_TESTS}")
message(STATUS "Build examples: ${BUILD_EXAMPLES}")
message(STATUS "=================================")
# Куди класти результати збірки
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin)
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib)
set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib)
# Внутрішні бібліотеки
add_subdirectory(libs/mylib1)
add_subdirectory(libs/mylib2)
# Головна програма
add_subdirectory(src)
# Тести додаються на вимогу
if(BUILD_TESTS)
enable_testing()
add_subdirectory(tests)
endif()
# Приклади додаються на вимогу
if(BUILD_EXAMPLES)
add_subdirectory(examples)
endif()
Порядок add_subdirectory має значення: бібліотеки йдуть перед програмою, яка їх використовує. Хоча CMake дозволяє посилатися на ціль, оголошену пізніше, послідовне розташування читається легше.
Змінні CMAKE_RUNTIME_OUTPUT_DIRECTORY та суміжні збирають усі виконувані файли в build/bin, а бібліотеки в build/lib. Без них кожен файл лежить у «своїй» підпапці всередині build/, і шукати їх незручно. Підтвердження на реальній збірці:
CMakeLists.txt у підпапках
src/CMakeLists.txt: головна програма
# Список вихідних файлів
set(SOURCES
main.cpp
utils.cpp
)
# Виконуваний файл
add_executable(${PROJECT_NAME} ${SOURCES})
# Підключення include-папок
target_include_directories(${PROJECT_NAME}
PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}
PUBLIC
${PROJECT_SOURCE_DIR}/include
)
# Зв'язування з внутрішніми бібліотеками
target_link_libraries(${PROJECT_NAME}
PRIVATE
MyLib1
MyLib2
)
# Властивості цілі
set_target_properties(${PROJECT_NAME}
PROPERTIES
OUTPUT_NAME "myapp" # файл називатиметься myapp, а не MyProject
DEBUG_POSTFIX "_d" # Debug-версія отримає суфікс: myapp_d
)
# Попередження компілятора (функція з модуля cmake/CompilerWarnings.cmake)
set_project_warnings(${PROJECT_NAME})
Три моменти заслуговують на увагу:
CMAKE_CURRENT_SOURCE_DIRіPROJECT_SOURCE_DIR. Перша вказує на папку цьогоCMakeLists.txt(src/), друга на корінь проєкту. Тому публічнийinclude/береться від кореня.set_target_properties. Змінює властивості конкретної цілі.OUTPUT_NAMEзадає ім'я файлу, відмінне від імені цілі, аDEBUG_POSTFIXдодає суфікс до Debug-збірки, щоб Debug і Release можна було тримати в одній папці.- Ім'я цілі
${PROJECT_NAME}. Тут воно збігається з назвою проєкту. Фактичний виконуваний файл при цьому називаєтьсяmyapp_d(див. вивід вище).
PROJECT_NAME, CMAKE_CXX_STANDARD та інші. Тому в src/CMakeLists.txt не потрібно повторно викликати project() або встановлювати стандарт C++.libs/mylib1/CMakeLists.txt: внутрішня бібліотека
# Файли бібліотеки
set(LIB_SOURCES
src/feature1.cpp
src/feature2.cpp
)
set(LIB_HEADERS
include/mylib1/feature1.h
include/mylib1/feature2.h
)
# Створення бібліотеки
add_library(MyLib1 ${LIB_SOURCES} ${LIB_HEADERS})
# Псевдонім у стилі Проєкт::Ціль
add_library(MyProject::MyLib1 ALIAS MyLib1)
# Include-папки
target_include_directories(MyLib1
PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:include>
PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/src
)
# Версія бібліотеки (діє для SHARED)
set_target_properties(MyLib1
PROPERTIES
VERSION ${PROJECT_VERSION}
SOVERSION ${PROJECT_VERSION_MAJOR}
)
Вирази $<BUILD_INTERFACE:...> і $<INSTALL_INTERFACE:...> потрібні, бо шлях до заголовків різний для збірки та після встановлення. Під час збірки це папка в дереві джерел (libs/mylib1/include), а після install заголовки лежать у <префікс>/include. Вираз вибирає правильний варіант залежно від ситуації.
Структура libs/mylib1/include/mylib1/feature1.h (ім'я бібліотеки повторюється в шляху) означає, що користувач пише #include <mylib1/feature1.h>. Завдяки цьому заголовки різних бібліотек не конфліктують між собою, навіть якщо називаються однаково.
VERSION і SOVERSION стосуються лише динамічних бібліотек: вони задають суфікси libMyLib1.so.1.2.3 та libMyLib1.so.1. Для статичних бібліотек ці властивості безпечно ігноруються.Власні модулі CMake
Налаштування, які повторюються між цілями (попередження, санітайзери), виносять у файли .cmake у папці cmake/ і підключають командою include().
cmake/CompilerWarnings.cmake
function(set_project_warnings target)
set(MSVC_WARNINGS
/W4 # Рівень попереджень 4
/permissive- # Строга відповідність стандарту
)
set(CLANG_WARNINGS
-Wall
-Wextra
-Wpedantic
-Wshadow # Змінна затіняє іншу
-Wnon-virtual-dtor # Базовий клас без віртуального деструктора
-Wold-style-cast # Приведення типів у стилі C
-Wcast-align
-Wunused
-Woverloaded-virtual
-Wconversion # Неявні перетворення зі втратою даних
-Wsign-conversion
)
set(GCC_WARNINGS
${CLANG_WARNINGS}
-Wmisleading-indentation
-Wduplicated-cond
-Wlogical-op
)
if(MSVC)
set(PROJECT_WARNINGS ${MSVC_WARNINGS})
elseif(CMAKE_CXX_COMPILER_ID MATCHES ".*Clang")
set(PROJECT_WARNINGS ${CLANG_WARNINGS})
elseif(CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
set(PROJECT_WARNINGS ${GCC_WARNINGS})
endif()
target_compile_options(${target} PRIVATE ${PROJECT_WARNINGS})
endfunction()
/WX та -Werror, які перетворюють попередження на помилки. Тут їх прибрано: у навчальному коді нове попередження не повинно блокувати збірку. Якщо потрібна жорстка перевірка, увімкніть -Werror лише у CI за допомогою окремої опції.Підключення модуля
cmake_minimum_required(VERSION 3.15)
project(MyProject)
# Додаємо шлях до власних модулів
list(APPEND CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake")
# Підключаємо модуль: CMake шукає CompilerWarnings.cmake
include(CompilerWarnings)
add_executable(MyApp main.cpp)
# Використовуємо функцію з модуля
set_project_warnings(MyApp)
Команда include(Ім'я) шукає файл Ім'я.cmake у шляхах з CMAKE_MODULE_PATH і виконує його. Після цього функція set_project_warnings доступна як звичайна команда. Тепер списки попереджень змінюються в одному місці, а всі цілі проєкту отримують однакові правила.
Типові кандидати на власні модулі: попередження компілятора, санітайзери (-fsanitize=address), підключення статичного аналізу (clang-tidy), пошук залежностей.
Попередньо скомпільовані заголовки
Кожний .cpp, який підключає <iostream>, <vector> або <string>, змушує компілятор заново розбирати тисячі рядків цих заголовків. У великому проєкті це основна частина часу компіляції. Попередньо скомпільований заголовок розбирається один раз, а результат повторно використовується:
# PCH для великих проєктів
target_precompile_headers(MyApp
PRIVATE
<iostream>
<string>
<vector>
<memory>
)
Команда з'явилась у CMake 3.16, тому проєкт, що її використовує, має вимагати cmake_minimum_required(VERSION 3.16) або новішу версію. Вона сама створює службовий заголовок і вмикає потрібні прапорці для кожного компілятора. Складати його вручну не потрібно.
Встановлення проєкту: install
Після збірки результати можна скопіювати у цільову папку, звідки програма запускається системно або бібліотека підключається іншими проєктами:
# Виконуваний файл
install(TARGETS MyApp
RUNTIME DESTINATION bin
)
# Бібліотеки
install(TARGETS MyLib1 MyLib2
LIBRARY DESTINATION lib
ARCHIVE DESTINATION lib
)
# Заголовкові файли
install(DIRECTORY include/
DESTINATION include
)
# Інші файли
install(FILES README.md LICENSE
DESTINATION share/doc/MyProject
)
Слова RUNTIME, LIBRARY, ARCHIVE — це види артефактів. До RUNTIME належать виконувані файли та .dll, до LIBRARY динамічні бібліотеки .so/.dylib, до ARCHIVE статичні бібліотеки .a/.lib. Для кожного виду задається своя папка призначення.
Запуск установки (проєкт має бути вже зібраний):
# Встановити в системну папку (потрібні права адміністратора)
cmake --install build
# Встановити у власну папку
cmake --install build --prefix /custom/path
На прикладі нашого проєкту з префіксом out:
Відносні шляхи DESTINATION додаються до префікса (CMAKE_INSTALL_PREFIX), за замовчуванням /usr/local на Unix. Для розробки зручно використовувати власний префікс і не чіпати системні папки.
bin, lib, include у зрілих проєктах використовують модуль GNUInstallDirs і змінні ${CMAKE_INSTALL_BINDIR}, ${CMAKE_INSTALL_LIBDIR}, ${CMAKE_INSTALL_INCLUDEDIR}. Вони враховують відмінності між дистрибутивами (наприклад, lib64).Питання для самоперевірки
#include <myproject/utils.h>. Якщо покласти utils.h прямо в include/, то при підключенні двох бібліотек із однаково названими заголовками один із них перекриє інший. Додатковий рівень папок дає простір імен для файлів.project() потрібен лише у випадку, коли підпапка має працювати і як самостійний проєкт.-Werror це зупиняє збірку у всіх користувачів. Жорстку перевірку краще робити в CI, не вплітаючи в проєкт за замовчуванням.Практичне завдання
Створіть проєкт Shop за такою схемою:
- Бібліотека
libs/pricingз функцієюdouble applyDiscount(double price, int percent). - Бібліотека
libs/formatз функцієюstd::string money(double value), яка повертає рядок із двома знаками після коми. - Програма
src/main.cpp, що використовує обидві бібліотеки. - Модуль
cmake/CompilerWarnings.cmake, підключений до всіх цілей. - Збірка в
build/binтаbuild/lib, а такожcmake --installу папкуout.
- У кореневому файлі:
add_subdirectory(libs/pricing),add_subdirectory(libs/format),add_subdirectory(src). - Кожна бібліотека має
target_include_directories(... PUBLIC ...)на власну папкуinclude/. - У
src/CMakeLists.txt:target_link_libraries(Shop PRIVATE Pricing Format). - Форматування:
std::ostringstreamіstd::fixed << std::setprecision(2).
Резюме
🗂️ Структура
include/, src/, libs/, external/, cmake/, tests/, examples/, docs/. build/ не потрапляє в git.🌲 Ієрархія файлів
CMakeLists.txt задає загальне і підключає підпапки через add_subdirectory. Кожна підпапка описує свої цілі.🧩 Модулі CMake
cmake/*.cmake і підключаються include().⚡ PCH та install
target_precompile_headers прискорює компіляцію, install і cmake --install розкладають результат у цільову папку.Конфігурації збірки: Debug, Release і вирази-генератори
Типи збірки Debug, Release, RelWithDebInfo та MinSizeRel, реальні прапорці компілятора для кожного, налаштування через CMAKE_BUILD_TYPE, вирази-генератори $<...>, відмінність single-config та multi-config генераторів.
Тестування з CMake: CTest, Google Test, покриття коду
Вбудована система тестування CTest, add_test, запуск і фільтрація тестів, інтеграція Google Test через FetchContent та gtest_discover_tests, пастка assert у Release, вимірювання покриття коду.