Умови, опції, цикли та функції в CMake
Умови, опції, цикли та функції в 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(ЗМІННА), визначають окремі правила. Реальний запуск підтверджує їх:
Значення вважаються істинними: 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 у таблиці кодів більший за 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 .
Реальний вивід двох конфігурацій одного проєкту:
Після другого запуску значення залишилися в кеші 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()
Діапазон 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}")
Генерація заголовка з версією: configure_file
Команда configure_file читає шаблон, підставляє в нього значення змінних CMake та записує результат у потрібну папку. Так версія проєкту з project(... VERSION ...) потрапляє у код C++ без дублювання.
Шаблон config.h.in містить місця для підстановки у форматі @ІМ'Я@. Під час конфігурації CMake замінює їх значеннями змінних і створює build/config.h. Готовий файл лежить у папці збірки, не в джерелах, тому target_include_directories вказує на CMAKE_BINARY_DIR.
Зміна версії в project() оновить заголовок автоматично. У шаблоні також підтримується запис #cmakedefine ІМ'Я: рядок перетворюється на #define ІМ'Я, якщо змінна істинна, і на закоментований, якщо хибна. Так опції CMake перетворюють на макроси C++.
Питання для самоперевірки
UNIX разом з APPLE. Якщо потрібно відокремити Linux від macOS, перевіряйте APPLE першим або пишіть if(UNIX AND NOT APPLE).option() записує значення в кеш CMakeCache.txt і наступні конфігурації його не перезаписують. Змінити значення можна новим -D..., а повністю скинути — видаленням папки build/.PARENT_SCOPE: set(${RESULT} ${VALUE} PARENT_SCOPE). Без PARENT_SCOPE змінна залишиться локальною.3.), далі 9 порівнюється з 1, а 9 має більший код. Для версій користуйтеся операторами VERSION_GREATER, VERSION_LESS та VERSION_EQUAL.Практичне завдання
Створіть проєкт Configurable з main.cpp, який виводить версію проєкту та повідомляє, чи ввімкнено логування. Потрібно:
- Версія
1.4.0задається вproject()і потрапляє у код черезconfigure_file. - Опція
ENABLE_LOGGING(за замовчуваннямON) визначає макросLOGGING_ENABLED. - Опція
BUILD_EXAMPLES(за замовчуваннямOFF) за увімкнення виводитьmessage(STATUS ...)про те, що приклади будуть зібрані. - Перевірте, що
cmake -DENABLE_LOGGING=OFFзмінює вивід програми.
cmake_minimum_required(VERSION 3.10)
project(Configurable VERSION 1.4.0)
option(ENABLE_LOGGING "Enable logging" ON)
option(BUILD_EXAMPLES "Build examples" OFF)
configure_file(
"${CMAKE_SOURCE_DIR}/config.h.in"
"${CMAKE_BINARY_DIR}/config.h"
)
add_executable(Configurable main.cpp)
target_include_directories(Configurable PRIVATE ${CMAKE_BINARY_DIR})
if(ENABLE_LOGGING)
target_compile_definitions(Configurable PRIVATE LOGGING_ENABLED)
endif()
if(BUILD_EXAMPLES)
message(STATUS "Examples will be built")
endif()
#include <iostream>
#include "config.h"
int main() {
std::cout << "Version " << VERSION_STRING << std::endl;
#ifdef LOGGING_ENABLED
std::cout << "Logging: on" << std::endl;
#else
std::cout << "Logging: off" << std::endl;
#endif
}
Файл config.h.in такий самий, як у прикладі вище.
Резюме
🔀 Умови
if перевіряє змінні, рядки, числа, файли та платформу. Для рядків використовуйте STREQUAL, для версій VERSION_* оператори.🎚️ Опції
option() створює булеву змінну в кеші. Передається через -DІМ'Я=ON, зберігається між запусками.🔁 Цикли
foreach перебирає списки й діапазони, while повторює до виконання умови. Арифметика через math(EXPR ...).🧱 Функції й макроси
PARENT_SCOPE.📄 configure_file
.in і створює заголовок, наприклад з версією проєкту.$<...> та відмінність між single-config і multi-config генераторами.Підключення зовнішніх бібліотек: find_package, add_subdirectory, FetchContent, pkg-config
П'ять способів підключити чужу бібліотеку до CMake-проєкту, режими Module і Config у find_package, автоматичне завантаження через FetchContent, ручне підключення і pkg-config, порівняльна таблиця.
Конфігурації збірки: Debug, Release і вирази-генератори
Типи збірки Debug, Release, RelWithDebInfo та MinSizeRel, реальні прапорці компілятора для кожного, налаштування через CMAKE_BUILD_TYPE, вирази-генератори $<...>, відмінність single-config та multi-config генераторів.