Проєкт із кількома файлами: перелік джерел і file(GLOB)
Проєкт із кількома файлами: перелік джерел і file(GLOB)
🎯 Мета лекції
- Зібрати проєкт із кількох
.cppта.hфайлів. - Порівняти три способи вказати джерела: явний перелік, змінні,
file(GLOB). - Пояснити, чому
file(GLOB)безCONFIGURE_DEPENDSне помічає нових файлів. - Обрати спосіб, який підходить для реальних проєктів.
🔑 Ключові терміни
- Одиниця трансляції (translation unit): окремий
.cppз усіма підключеними заголовками. file(GLOB): команда, яка шукає файли за маскою під час конфігурації.CONFIGURE_DEPENDS: прапорець, який змушує систему збірки перевіряти маску знову при кожній збірці.
У лекції про перший проєкт збирався один main.cpp. Реальні програми розділені на модулі, і кожен модуль — це пара .h та .cpp (тема про багатофайлові програми). Тут розглянемо, як повідомити про всі ці файли CMake.
Приклад проєкту
Програма складається з трьох одиниць трансляції: main.cpp, math_utils.cpp і string_utils.cpp. Заголовки до одиниць трансляції не додаються окремо, бо компілятор бере їх через #include.
Усі три .cpp компілюються окремо і лише потім лінкуються. Якщо якогось з них не буде в add_executable, компіляція пройде, а лінкер повідомить про невизначені символи (undefined reference або LNK2019 у MSVC).
Спосіб 1: явний перелік усіх файлів
cmake_minimum_required(VERSION 3.10)
project(MyProject)
set(CMAKE_CXX_STANDARD 17)
add_executable(MyProject
main.cpp
math_utils.cpp
math_utils.h
string_utils.cpp
string_utils.h
)
Файли перелічені прямо в add_executable, по одному в рядку. Заголовки .h додавати не обов'язково для збірки, але корисно: IDE (Visual Studio, CLion) показує їх у дереві проєкту і не плутає з зайвими файлами.
Це найпростіший і найпрозоріший варіант. Під час код-рев'ю в git diff одразу видно, що додано новий файл.
Спосіб 2: змінні SOURCES і HEADERS
cmake_minimum_required(VERSION 3.10)
project(MyProject)
set(CMAKE_CXX_STANDARD 17)
# Список вихідних файлів
set(SOURCES
main.cpp
math_utils.cpp
string_utils.cpp
)
# Список заголовкових файлів (необов'язковий, для IDE)
set(HEADERS
math_utils.h
string_utils.h
)
add_executable(MyProject ${SOURCES} ${HEADERS})
Результат той самий, але списки винесені в змінні. Зручність у тому, що перелік можна використати в кількох місцях: наприклад, зібрати з тих самих SOURCES і виконуваний файл, і тестову програму, або додавати файли до списку умовно:
if(WIN32)
list(APPEND SOURCES windows_specific.cpp)
endif()
Спосіб 3: автоматичний пошук через file(GLOB)
cmake_minimum_required(VERSION 3.10)
project(MyProject)
set(CMAKE_CXX_STANDARD 17)
# Знайти всі .cpp файли в поточній папці
file(GLOB SOURCES "*.cpp")
# Знайти всі .h файли
file(GLOB HEADERS "*.h")
add_executable(MyProject ${SOURCES} ${HEADERS})
Команда file(GLOB ЗМІННА маска) шукає файли за маскою і записує знайдений список у змінну. Додавати файл у CMakeLists.txt вручну не потрібно. Спокуслива зручність, але з нею пов'язана проблема.
Чому GLOB ризикований
Пошук виконується один раз, під час конфігурації. Якщо після цього створити новий .cpp, система збірки про нього не дізнається: вона не знає, що список файлів міг змінитись. Перевіримо це на нашому проєкті:
Тільки після ручної повторної конфігурації logger.cpp потрапив у збірку. Така поведінка дає плутанину в команді: колега додав файл, ви зробили git pull, зібрали проєкт, і лінкер видає помилку «undefined reference», хоча код правильний. Причина прихована в кеші.
file(GLOB). Додавання або видалення файлу має бути змінною в CMakeLists.txt, бо тоді CMake сам знає, що потрібно перегенерувати файли збірки.CONFIGURE_DEPENDS: компроміс
CMake 3.12 додав прапорець CONFIGURE_DEPENDS. З ним система збірки при кожній збірці повторно перевіряє маску і запускає конфігурацію, якщо список файлів змінився:
file(GLOB SOURCES CONFIGURE_DEPENDS "*.cpp")
Результат на тому ж проєкті:
Нові файли підхоплюються без ручної повторної конфігурації. Платою за це є додаткова перевірка файлової системи при кожній збірці. На великих проєктах і повільних мережевих дисках це помітно, а в документації CMake зазначено, що не всі генератори надійно її підтримують. Тому CONFIGURE_DEPENDS слід розглядати як пом'якшення проблеми, а не як її усунення.
file(GLOB ...) не рекурсивний. Для пошуку у вкладених папках існує file(GLOB_RECURSE SOURCES "src/*.cpp"). Усі зауваження щодо ризиків діють і для нього.Порівняння способів
| Спосіб | Переваги | Недоліки |
|---|---|---|
| Явний перелік | Прозорий git diff, надійна повторна конфігурація, IDE бачить усі файли | Новий файл доводиться дописувати вручну |
Змінні SOURCES, HEADERS | Список використовується повторно, його можна збирати умовно | Те саме, що й у явного переліку |
file(GLOB) | Нічого не треба дописувати | Новий файл не помічається без повторної конфігурації |
file(GLOB ... CONFIGURE_DEPENDS) | Нові файли підхоплюються автоматично | Додаткова перевірка при кожній збірці, нестабільна на деяких генераторах |
Рекомендація: у реальних проєктах використовуйте способи 1 або 2. Спосіб 3 прийнятний для невеликих навчальних проєктів, де ви єдиний автор і готові перегенеровувати файли збірки.
Питання для самоперевірки
#include. Додавати їх варто заради IDE: Visual Studio і CLion показують у дереві проєкту лише ті файли, про які знає CMake. Крім того, зміна заголовка, доданого в ціль, надійніше викликає повторну збірку залежних файлів у деяких генераторах.file(GLOB) без CONFIGURE_DEPENDS. Файл лежить у папці, але CMake про нього не знає, бо список складений під час попередньої конфігурації. Запустіть cmake -S . -B build повторно. Щоб виправити причину, замініть GLOB на явний перелік.Практичне завдання
- Створіть проєкт із
main.cpp,math_utils.cpp/.hіstring_utils.cpp/.hта зберіть його способом 1. - Перепишіть
CMakeLists.txtна спосіб 2 (змінніSOURCESіHEADERS). - Додайте файл
logger.cpp/.hз функцієюvoid logMessage(const std::string&)та викличте її зmain. Скільки місць уCMakeLists.txtдовелося змінити? - Замініть перелік на
file(GLOB ...)безCONFIGURE_DEPENDS, додайте ще один модуль і подивіться, на якому кроці виникне помилка.
cmake_minimum_required(VERSION 3.10)
project(MyProject)
set(CMAKE_CXX_STANDARD 17)
set(SOURCES
main.cpp
math_utils.cpp
string_utils.cpp
logger.cpp
)
set(HEADERS
math_utils.h
string_utils.h
logger.h
)
add_executable(MyProject ${SOURCES} ${HEADERS})
У варіантах 1 і 2 змінюється рівно один список: додаються два рядки (logger.cpp та logger.h). У пункті 4 збірка без повторної конфігурації закінчиться помилкою лінкера undefined reference to logMessage(...).
Резюме
📃 Явний перелік
CMakeLists.txt, зміни видно в git diff, CMake завжди в курсі складу проєкту.🧺 Змінні списків
SOURCES і HEADERS дозволяють повторно використовувати списки та додавати файли за умовою.⚠️ GLOB
🔄 CONFIGURE_DEPENDS
Синтаксис CMakeLists.txt: команди, змінні, списки
Основні команди CMake (cmake_minimum_required, project, set, add_executable, add_library, target_link_libraries, message), вбудовані й власні змінні, списки, коментарі та повний приклад конфігурації.
Бібліотеки в CMake: STATIC, SHARED, INTERFACE та видимість PUBLIC/PRIVATE
Три типи бібліотек, створення власної бібліотеки, динамічні бібліотеки на Windows, вибір типу через BUILD_SHARED_LIBS, поширення налаштувань між цілями за допомогою PUBLIC, PRIVATE і INTERFACE.