CMake і vcpkg

Умови, опції, цикли та функції в CMake

Умовні конструкції if/elseif/else, перевірка змінних і файлів, логічні оператори, платформо-залежні налаштування, опції option, цикли foreach і while, функції та макроси, генерація заголовка з версією через configure_file.

Умови, опції, цикли та функції в CMake

🎯 Мета лекції

  • Писати умови if/elseif/else та правильно перевіряти змінні, рядки, числа й файли.
  • Підлаштовувати збірку під платформу та компілятор.
  • Додавати налаштовувані опції option() і передавати їх через -D.
  • Користуватися циклами, функціями й макросами та знати різницю між останніми двома.
  • Передавати інформацію з CMake в код C++ через configure_file.

🔑 Ключові терміни

  • Опція (option): булева змінна в кеші, яку користувач може перемкнути командою -D.
  • Область видимості (scope): місце, у якому діє змінна.
  • Функція та макрос: користувацькі команди, що відрізняються областю видимості змінних.
  • configure_file: команда, що підставляє значення змінних CMake у шаблон файлу.

CMakeLists.txt — програма, тому в ній є розгалуження, цикли і підпрограми. Вони потрібні, щоб один файл збирав проєкт і на Windows, і на Linux, а також щоб користувач міг увімкнути або вимкнути частини проєкту.

Умовні конструкції

if, elseif, else

# Проста умова
if(WIN32)
    message("Це Windows")
endif()

# if-else
if(UNIX)
    message("Це Unix-подібна система (Linux/macOS)")
else()
    message("Це Windows")
endif()

# if-elseif-else
if(WIN32)
    message("Windows")
elseif(APPLE)
    message("macOS")
elseif(UNIX)
    message("Linux")
else()
    message("Невідома система")
endif()

Кожний if закривається endif(). Змінні WIN32, APPLE, UNIX визначає сам CMake. Зверніть увагу, що UNIX істинна також на macOS, тому перевірку APPLE слід розташовувати перед UNIX, якщо різниця важлива.

Перевірка змінних

# Чи змінна визначена
if(DEFINED MY_VARIABLE)
    message("Змінна MY_VARIABLE визначена")
endif()

# Булеве значення
set(ENABLE_FEATURE ON)
if(ENABLE_FEATURE)
    message("Feature увімкнена")
endif()

# Порівняння рядків
set(BUILD_TYPE "Debug")
if(BUILD_TYPE STREQUAL "Debug")
    message("Debug режим")
endif()

# Заперечення
if(NOT BUILD_TYPE STREQUAL "Release")
    message("Це не Release збірка")
endif()

Як CMake трактує значення в if(ЗМІННА), визначають окремі правила. Реальний запуск підтверджує їх:

cmake -P conditions.cmake
Debug режим
NOPE не визначена
FLAG ON -> істина # set(FLAG ON)
FLAG NO -> хиба # set(FLAG "NO"), перевірка if(NOT FLAG)

Значення вважаються істинними: 1, ON, YES, TRUE, Y, а також будь-яке ненульове число. Хибними: 0, OFF, NO, FALSE, N, IGNORE, NOTFOUND, порожній рядок і рядки, що закінчуються на -NOTFOUND.

Команда if(ЗМІННА)автоматично розіменовує змінну: якщо в ЗМІННА лежить рядок, що збігається з іменем іншої змінної, CMake піде за нею. Це джерело неочевидних помилок. Для порівняння з конкретним рядком завжди використовуйте STREQUAL, а не голе if(...). Для перевірки, чи змінна взагалі існує, є DEFINED.

Логічні оператори

# AND
if(WIN32 AND CMAKE_CXX_COMPILER_ID STREQUAL "MSVC")
    message("Windows з компілятором MSVC")
endif()

# OR
if(APPLE OR UNIX)
    message("Unix-подібна система")
endif()

# NOT
if(NOT WIN32)
    message("Не Windows")
endif()

# Комбінація з дужками
if((WIN32 OR APPLE) AND CMAKE_BUILD_TYPE STREQUAL "Debug")
    message("Debug збірка на Windows або macOS")
endif()

Порівняння чисел

set(VERSION 15)

if(VERSION GREATER 10)
    message("Версія більша за 10")
endif()

if(VERSION LESS 20)
    message("Версія менша за 20")
endif()

if(VERSION EQUAL 15)
    message("Версія дорівнює 15")
endif()

if(VERSION GREATER_EQUAL 15)
    message("Версія >= 15")
endif()

Операторам GREATER, LESS, EQUAL потрібні числа. Для версій у форматі 3.10.2 призначені окремі оператори VERSION_GREATER, VERSION_LESS, VERSION_EQUAL: рядкове порівняння "3.9" та "3.10" дало б хибний результат.

числове і рядкове порівняння
9 LESS 10: числове # if("9" LESS "10")
9 STRLESS 10: хибно (рядкове: '9' > '1') # if("9" STRLESS "10")

Рядки порівнюються посимвольно: символ 9 у таблиці кодів більший за 1, тому "9" вважається «більшим» за "10". Це типова причина помилок у версіях, тому використовуйте числові або VERSION_* оператори.

Перевірка файлів

# Чи існує файл
if(EXISTS "${CMAKE_SOURCE_DIR}/config.txt")
    message("Конфігураційний файл знайдено")
endif()

# Чи це папка
if(IS_DIRECTORY "${CMAKE_SOURCE_DIR}/external")
    message("Папка external існує")
endif()

Перевірка EXISTS корисна для необов'язкових компонентів: наприклад, підключати папку tests, лише якщо вона є, або використати локальний конфіг, якщо його створив розробник.

Приклад: налаштування під платформу

cmake_minimum_required(VERSION 3.10)
project(CrossPlatformApp)

set(CMAKE_CXX_STANDARD 17)

# Загальні вихідні файли
set(SOURCES
    main.cpp
    app.cpp
)

# Платформо-специфічні файли
if(WIN32)
    list(APPEND SOURCES windows_specific.cpp)
    message(STATUS "Adding Windows-specific files")
elseif(APPLE)
    list(APPEND SOURCES macos_specific.cpp)
    message(STATUS "Adding macOS-specific files")
elseif(UNIX)
    list(APPEND SOURCES linux_specific.cpp)
    message(STATUS "Adding Linux-specific files")
endif()

add_executable(${PROJECT_NAME} ${SOURCES})

# Платформо-специфічні бібліотеки
if(WIN32)
    target_link_libraries(${PROJECT_NAME} ws2_32)  # Windows sockets
elseif(UNIX)
    find_package(Threads REQUIRED)
    target_link_libraries(${PROJECT_NAME} Threads::Threads)
endif()

# Платформо-специфічні прапорці компіляції
if(MSVC)
    target_compile_options(${PROJECT_NAME} PRIVATE /W4 /WX)
else()
    target_compile_options(${PROJECT_NAME} PRIVATE -Wall -Wextra -Werror)
endif()

Загальні файли зібрані в SOURCES, а специфічні додаються до списку залежно від платформи. У джерелі для POSIX-потоків написано pthread. Надійніше підключати Threads::Threads через find_package(Threads): ця ціль сама підбирає правильний прапорець або бібліотеку (на macOS окремої pthread не потрібно).

Прапорець -Werror перетворює будь-яке попередження компілятора на помилку. Його зручно вмикати у вашій CI-збірці, але небезпечно залишати за замовчуванням: нова версія компілятора з новими попередженнями «зламає» збірку у користувачів, хоча код правильний. Для бібліотек, які ставлять інші люди, його краще не вмикати.

Опції

Опція — це булева змінна в кеші, яку користувач може перемикати, не змінюючи CMakeLists.txt.

cmake_minimum_required(VERSION 3.10)
project(ConfigurableApp)

# Опції, які користувач може змінити
option(ENABLE_TESTS "Build tests" OFF)
option(ENABLE_LOGGING "Enable logging" ON)
option(USE_STATIC_LIBS "Use static libraries" OFF)

# Стан опцій
message(STATUS "ENABLE_TESTS: ${ENABLE_TESTS}")
message(STATUS "ENABLE_LOGGING: ${ENABLE_LOGGING}")
message(STATUS "USE_STATIC_LIBS: ${USE_STATIC_LIBS}")

add_executable(MyApp main.cpp)

# Використання опцій
if(ENABLE_LOGGING)
    target_compile_definitions(MyApp PRIVATE LOGGING_ENABLED)
    message(STATUS "Logging is enabled")
endif()

# Тести додаємо тільки на вимогу
if(ENABLE_TESTS)
    enable_testing()
    add_subdirectory(tests)
endif()

Синтаксис: option(ІМ'Я "опис" значення_за_замовчуванням). Опис видно в GUI та у виводі cmake -LH.

Передача опцій із командного рядка

# Увімкнути тести
cmake -S . -B build -DENABLE_TESTS=ON

# Кілька опцій
cmake -S . -B build -DENABLE_LOGGING=OFF -DUSE_STATIC_LIBS=ON

# Графічний інтерфейс
cmake-gui .

Реальний вивід двох конфігурацій одного проєкту:

опції за замовчуванням і з -D
$ cmake -S . -B build
-- ENABLE_TESTS: OFF
-- ENABLE_LOGGING: ON
-- Logging is enabled
$ cmake -S . -B build -DENABLE_TESTS=ON -DENABLE_LOGGING=OFF
-- ENABLE_TESTS: ON
-- ENABLE_LOGGING: OFF
$ cmake -L -N build | grep ENABLE
ENABLE_LOGGING:BOOL=OFF
ENABLE_TESTS:BOOL=ON

Після другого запуску значення залишилися в кеші CMakeCache.txt. Наступна конфігурація без -D збереже їх, і це поширена причина здивування: «я ж не вмикав тести, а вони збираються». Скинути все можна, видаливши build/.

Команда cmake -L -N build виводить закешовані змінні без повторної конфігурації. Варіанти -LA (разом із розширеними) та -LH (з описом) зручні для діагностики.

Пастка порядку: BUILD_SHARED_LIBS

Опція BUILD_SHARED_LIBS впливає на бібліотеки, створені після її встановлення. Типова помилка зі старих прикладів:

add_library(MyLib lib.cpp)   # створена тут...

if(USE_STATIC_LIBS)
    set(BUILD_SHARED_LIBS OFF)  # ...а змінну ставимо пізніше: ефекту не буде
endif()

Правильно: спершу встановити BUILD_SHARED_LIBS, потім викликати add_library. Або, ще краще, не змінювати її всередині проєкту, а передавати через -DBUILD_SHARED_LIBS=ON з командного рядка.

Цикли

foreach

# Перебір списку
set(FRUITS apple banana cherry)
foreach(FRUIT ${FRUITS})
    message("Фрукт: ${FRUIT}")
endforeach()

# Діапазон від 0 до 3 включно
foreach(I RANGE 3)
    message("Ітерація ${I}")
endforeach()

# Діапазон із кроком
foreach(I RANGE 0 10 2)
    message("${I}")
endforeach()

# Перебір знайдених файлів
file(GLOB CPP_FILES "src/*.cpp")
foreach(FILE ${CPP_FILES})
    message("Знайдено файл: ${FILE}")
endforeach()
результат циклів foreach
Фрукт: apple
Фрукт: banana
Фрукт: cherry
Ітерація 0
Ітерація 1
Ітерація 2
Ітерація 3
0 2 4 6 8 10 # кожне число окремим рядком

Діапазон RANGE 3 дає числа 0, 1, 2, 3: кінець включається, на відміну від C++-циклів. Типове застосування foreach — створити однакові цілі за списком, наприклад по одному тестовому виконуваному файлу на кожен .cpp:

foreach(NAME test_math test_strings test_io)
    add_executable(${NAME} ${NAME}.cpp)
    target_link_libraries(${NAME} PRIVATE MyLib)
    add_test(NAME ${NAME} COMMAND ${NAME})
endforeach()

while

set(COUNT 0)
while(COUNT LESS 5)
    message("Лічильник: ${COUNT}")
    math(EXPR COUNT "${COUNT} + 1")  # COUNT = COUNT + 1
endwhile()

Для арифметики використовується команда math(EXPR ...), оскільки в CMake значення є рядками і COUNT + 1 не обчислюється саме по собі. Цикл while у CMakeLists.txt трапляється рідко, але потрібен, коли кількість повторів невідома наперед.

Функції та макроси

Функція

# Оголошення
function(print_message TEXT)
    message("=== ${TEXT} ===")
endfunction()

# Виклик
print_message("Hello CMake")

# Функція з кількома параметрами та поверненням значення
function(add_numbers RESULT A B)
    math(EXPR SUM "${A} + ${B}")
    set(${RESULT} ${SUM} PARENT_SCOPE)
endfunction()

add_numbers(MY_RESULT 5 3)
message("Результат: ${MY_RESULT}")  # 8

CMake не має операції return зі значенням. Щоб «повернути» результат, функція отримує ім'я змінної як перший параметр (RESULT) і записує в неї значення командою set(${RESULT} ... PARENT_SCOPE). Слово PARENT_SCOPE обов'язкове, бо без нього змінна існуватиме тільки всередині функції.

Макрос

# Макроси схожі на функції, але працюють в тій самій області видимості
macro(set_default_compiler_flags)
    if(MSVC)
        add_compile_options(/W4)
    else()
        add_compile_options(-Wall -Wextra)
    endif()
endmacro()

set_default_compiler_flags()

Функція чи макрос

Принципова різниця — область видимості змінних. Функція працює у власній області: змінні, створені або змінені всередині, зовні не видно. Макрос підставляє свій текст у місце виклику, тому змінює змінні викликача. Перевірка:

function(f_scope)
    set(X "змінено у функції")
endfunction()

macro(m_scope)
    set(X "змінено в макросі")
endmacro()

set(X "початкове")
f_scope()
message("після функції: ${X}")
m_scope()
message("після макросу: ${X}")
cmake -P scope.cmake
після функції: початкове
після макросу: змінено в макросі
За замовчуванням пишіть функції: вони не псують змінні викликача. Макрос варто брати лише тоді, коли дія свідомо має змінити область, з якої її викликали, наприклад задати набір загальних прапорців.

Генерація заголовка з версією: configure_file

Команда configure_file читає шаблон, підставляє в нього значення змінних CMake та записує результат у потрібну папку. Так версія проєкту з project(... VERSION ...) потрапляє у код C++ без дублювання.

Шаблон config.h.in містить місця для підстановки у форматі @ІМ'Я@. Під час конфігурації CMake замінює їх значеннями змінних і створює build/config.h. Готовий файл лежить у папці збірки, не в джерелах, тому target_include_directories вказує на CMAKE_BINARY_DIR.

результат
$ ./build/MyApp
Version: 2.5.1
Major: 2
Minor: 5
Patch: 1

Зміна версії в project() оновить заголовок автоматично. У шаблоні також підтримується запис #cmakedefine ІМ'Я: рядок перетворюється на #define ІМ'Я, якщо змінна істинна, і на закоментований, якщо хибна. Так опції CMake перетворюють на макроси C++.

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

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

Резюме

🔀 Умови

if перевіряє змінні, рядки, числа, файли та платформу. Для рядків використовуйте STREQUAL, для версій VERSION_* оператори.

🎚️ Опції

option() створює булеву змінну в кеші. Передається через -DІМ'Я=ON, зберігається між запусками.

🔁 Цикли

foreach перебирає списки й діапазони, while повторює до виконання умови. Арифметика через math(EXPR ...).

🧱 Функції й макроси

Функція має власну область видимості, макрос працює в області викликача. Результат повертається через PARENT_SCOPE.

📄 configure_file

Підставляє значення CMake у шаблон .in і створює заголовок, наприклад з версією проєкту.
Наступна тема: типи збірки Debug і Release, прапорці для кожного з них, вирази-генератори $<...> та відмінність між single-config і multi-config генераторами.
Copyright © 2026