CMake і vcpkg

Проєкт із кількома файлами: перелік джерел і file(GLOB)

Три способи вказати файли проєкту: явний перелік, змінні SOURCES і HEADERS, автоматичний пошук через file(GLOB). Чому GLOB ризикований і як працює CONFIGURE_DEPENDS.

Проєкт із кількома файлами: перелік джерел і 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
$ cmake -S . -B build && cmake --build build
[ 25%] Building CXX object CMakeFiles/MyProject.dir/main.cpp.o
[ 50%] Building CXX object CMakeFiles/MyProject.dir/math_utils.cpp.o
[ 75%] Building CXX object CMakeFiles/MyProject.dir/string_utils.cpp.o
[100%] Built target MyProject
$ touch logger.cpp # новий файл
$ cmake --build build
[100%] Built target MyProject ← logger.cpp проігноровано
$ cmake -S . -B build && cmake --build build
[ 20%] Building CXX object CMakeFiles/MyProject.dir/logger.cpp.o
[100%] Built target MyProject

Тільки після ручної повторної конфігурації logger.cpp потрапив у збірку. Така поведінка дає плутанину в команді: колега додав файл, ви зробили git pull, зібрали проєкт, і лінкер видає помилку «undefined reference», хоча код правильний. Причина прихована в кеші.

Для вихідних файлів проєкту офіційна документація CMake радить не використовувати file(GLOB). Додавання або видалення файлу має бути змінною в CMakeLists.txt, бо тоді CMake сам знає, що потрібно перегенерувати файли збірки.

CONFIGURE_DEPENDS: компроміс

CMake 3.12 додав прапорець CONFIGURE_DEPENDS. З ним система збірки при кожній збірці повторно перевіряє маску і запускає конфігурацію, якщо список файлів змінився:

file(GLOB SOURCES CONFIGURE_DEPENDS "*.cpp")

Результат на тому ж проєкті:

GLOB з CONFIGURE_DEPENDS
$ touch extra.cpp
$ cmake --build build
[ 16%] Building CXX object CMakeFiles/MyProject.dir/extra.cpp.o
[100%] Built target MyProject

Нові файли підхоплюються без ручної повторної конфігурації. Платою за це є додаткова перевірка файлової системи при кожній збірці. На великих проєктах і повільних мережевих дисках це помітно, а в документації 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 прийнятний для невеликих навчальних проєктів, де ви єдиний автор і готові перегенеровувати файли збірки.

Питання для самоперевірки

Практичне завдання

Резюме

📃 Явний перелік

Найнадійніший спосіб: усі файли названі у CMakeLists.txt, зміни видно в git diff, CMake завжди в курсі складу проєкту.

🧺 Змінні списків

SOURCES і HEADERS дозволяють повторно використовувати списки та додавати файли за умовою.

⚠️ GLOB

Шукає файли лише під час конфігурації, тому нові файли без повторної конфігурації не потрапляють у збірку.

🔄 CONFIGURE_DEPENDS

Змушує перевіряти маску при кожній збірці. Пом'якшує проблему, але не рекомендований як основний спосіб.
Тепер проєкт із одним виконуваним файлом збирається без труднощів. Далі код розділимо ще глибше: частину файлів оформимо як бібліотеку, яку використовує кілька програм.
Copyright © 2026