LXF110:Cmake
|
|
|
Содержание |
Интроспекция и логика
- ЧАСТЬ 2 Основная задача инструментов типа CMake – обеспечить единый файл проекта для его сборки на любой системе. Андрей Боровский покажет, за счет чего это достигается.
Вы помните, кто такой Кевин Митник? Когда-то призывы досрочно освободить этого электронного взломщика из тюрьмы украшали сетевые странички многих начинающих хакеров. Противники Митника злорадно разъясняли, что «мастер социальной инженерии» не так уж хорошо разбирается в программировании. Приводились даже распечатки с форумов, где Митник, предположительно, просил других людей собрать ему его программы. Сколько в этих обвинениях было правды, я не знаю. Между прочим, сегодня Митник – респектабельный ИТ-специалист, основатель компании, занимающейся компьютерной безопасностью (www.kevinmitnick.com). Как бы там ни было, я должен вас обрадовать. Чем бы вы ни занялись в будущем, никто не сможет обвинить вас в том, что вы не умеете собирать программы, если сегодня вы освоите CMake.
Того, что мы узнали о CMake в прошлый раз, достаточно для написания простейших случаев, однако для сборки более сложных проектов требуются более глубокие знания.
Исследуем систему
В предыдущей статье говорилось, что одним из преимуществ CMake является мощная система определения параметров платформы, для которой выполняется сборка. Источником информации о платформе служат специальные переменные, значения которых устанавливаются средой CMake, и команды. Мы уже знаем, что в результате загрузки расширения CMake нам становятся доступны специальные переменные, позволяющие выяснить параметры подключаемых к проекту библиотек и вспомогательных программ, используемых для сборки. Рассмотрим переменные и команды, которые позволяют определить параметры системы, не зависящие от загруженных расширений. В архиве sysinfo.tar.gz на LXFDVD вы найдете несколько необычный файл CMakeLists.txt. Он не связан со сборкой какого-либо проекта, а просто демонстрирует возможности CMake по определению параметров системы:
project(SystemInfo CXX) message(STATUS "System: " ${CMAKE_SYSTEM_NAME} " " ${CMAKE_SYSTEM_VERSION}) message(STATUS "Processor: " ${CMAKE_HOST_SYSTEM_PROCESSOR}) if(${CMAKE_SYSTEM_NAME} STREQUAL Windows) if(MSVC) message(STATUS "Compiler: MSVC, version: " ${MSVC_VERSION}) endif(MSVC) if(BORLAND) message(STATUS "Compiler: BCC") endif(BORLAND) else(${CMAKE_SYSTEM_NAME} STREQUAL Linux) message(STATUS "Only GCC is supported on Linux") endif(${CMAKE_SYSTEM_NAME} STREQUAL Windows) message(STATUS "CMake generates " ${CMAKE_GENERATOR})
Назначение команды message() – выводить различные сообщения во время генерации файлов проекта утилитой CMake. Ее первым аргументом является тип сообщения. Допустимы три варианта: SEND_ERROR, FATAL_ERROR и STATUS. Первые два типа предназначены для вывода сообщений об ошибках разной степени тяжести. Если в процессе обработки файла CMakeLists.txt генерируется сообщение типа SEND_ERROR, обработка текущего файла CMakeLists.txt завершается. Генерация сообщения с типом FATAL_ERROR приводит к завершению работы CMake в целом. Сообщения типа STATUS не влияют на генерацию файлов проекта – они просто распечатывают данные. Мы используем команду message() для отображения значений некоторых переменных CMake. Переменная CMake_SYSTEM_NAME содержит короткое имя системы (например, Linux или Windows). В переменной CMake_SYSTEM_VERSION содержится номер версии системы (для Linux это версия ядра).
Команда if(), как вы, конечно, догадались, реализует условные переходы в файле мета-проекта. Однако ее синтаксис, к сожалению, не столь очевиден. Аргументом команды является логическое выражение, управляющее переходом к одному из последующих блоков. Практически любое значение переменной или операции может рассматриваться в языке CMake как логическое выражение. Если переменной не присвоено значение, или присвоено одно из значений N, NO, OFF, FALSE, NOTFOUND, <variable>-NOTFOUND, это интерпретируется как «ложь» (False), в противном случае значение считается истинным (True). Логическое значение переменной может быть инвертировано с помощью оператора NOT. Операторы сравнения в языке CMake выглядят довольно необычно. Для сопоставления числовых значений используются операторы EQUAL (равно), GREATER (больше), LESS (меньше). Для сравнения строковых значений используются, соответственно, операторы STREQUAL, STRGREATER и STRLESS. Чтобы определить, присвоено ли переменной какое-либо значение в принципе, можно воспользоваться оператором DEFNED:
if(DEFINED variable)
Оператор MATCHES выполняет сравнение по регулярному выражению.
Помимо команды if() в нашем распоряжении есть команды else() и elseif(), с помощью которых мы можем определять альтернативные ветви условных переходов. В качестве аргументов этих команд также используются логические выражения. Аргументом команды elseif() должно быть альтернативное выражение, а аргументом else() – то же выражение, что и для команды if(). Каждой команде if() должна соответствовать команда endif(), аргумент которой должен совпадать с переданным if() и else() (этого требует документация по CMake). Необходимость указывать логические выражения в качестве аргументов для команд else() и endif() наверняка покажется вам излишней, и вы будете правы. Аргументы этих команд указывают, скорее, для того, чтобы не запутаться в сложных конструкциях, содержащих вложенные условные переходы. Хотя в примерах CMake команд else() и endif() всегда вызываются с аргументами, практика показывает, что это не обязательно.
Если «скормить» CMake написанный нами файл, то в Linux будет распечатано следующее:
-- System: Linux 2.6.25.5-1.1-default -- Processor: i686 -- Only GCC is supported on Linux -- CMake generates Unix Makefiles
На платформе Windows XP + Visual Studio 2005 информация будет отличаться:
System: Windows 5.1 Processor: x86 Compiler: MSVC, version: 1400 CMake generates Visual Studio 8 2005
Кэш CMake
Значения некоторых переменных CMake, описывающих настройку инструментов сборки и компоновки, сохраняются в файле кэша. Он создается для каждого мета-проекта и имеет имя CMakeCache.txt. С помощью CMakeCache.txt вы не только можете узнать, какие переменные CMake определяют параметры инструментов сборки, но и отредактировать их значения прямо в этом файле (однако, поскольку файл CMakeCache.txt генерируется автоматически, постоянные модификации значений переменных все же следует вносить в файл CMakeLists.txt). Структура записей в файле CMakeCache.txt очень проста [однако не является официально заявленной и может меняться от версии к версии – будьте внимательны, – прим. ред.]. Каждая запись имеет вид
ИМЯ[:ТИП]=ЗНАЧЕНИЕ
где ИМЯ – имя переменной, ТИП – необязательный элемент, указывающий ее тип. Переменные, определенные в основном модуле CMake и загружаемых расширениях, снабжены поясняющими комментариями. Изучение переменных, внесенных в кэш CMake, позволит вам лучше понять работу системы; кроме того, редактирование содержимого кэша может пригодиться при правке самого файла проекта CMake.txt (подробнее об этом сказано во врезке).
Простые проверки
Переменные, определенные в CMake и загружаемых расширениях, не всегда могут предоставить вам всю необходимую информацию о системе. В этой ситуации вы можете воспользоваться теми же инструментами интроспекции, которые применяет CMake для присвоения значений стандартным переменным. Дополнительные модули CMake предоставляют нам несколько команд, позволяющих выяснить, «что где лежит». Модуль CheckIncludeFile экспортирует команду check_include_file(), проверяющую, доступен ли системе сборки тот или иной заголовочный файл. Вот как, например, можно узнать, установлен ли файл GL/glx.h:
include(CheckIncludeFile) check_include_file("GL/glx.h" HAVE_GLX_H)
Команда include() позволяет включить в наш мета-проект файл расширения, заданный именем (ее можно рассматривать как более общий аналог команды find_package()). После этого нам становятся доступны переменные и команды, определенные в файле расширения. Первым аргументом команды check_include_file() должно быть имя заголовочного файла, вторым – имя переменной, в которой будет сохранен результат. Если указанный заголовочный файл найден, переменной присваивается значение 1, если не найден – 0. Если команда не изменила значение переменной, например, оставила его неопределенным, значит, выполнить поиск файла по каким-то причинам не удалось.
У команды check_include_file() есть несколько родственников. Модуль CheckIncludeFiles экспортирует команду check_include_files(), умеющую проверять доступность одновременно нескольких заголовочных файлов. Модуль CheckIncludeFilesCXX экспортирует команду check_include_files_cxx(), которая проверяет доступность заголовочных файлов программ на C++.
Если вы добавили команду check_include_file() или ей подобную в файл CMakeLists.txt, не забудьте очистить кэш (самый простой способ сделать это – удалить файл CMakeCache.txt из директории проекта).
Модули расширения предоставляют еще несколько команд, работающих аналогично check_include_files(). Команда check_symbol_exists() из модуля CheckSymbolExists позволяет проверить, содержат ли указанные команде заголовочные файлы заданный символ. Команда check_library_exists() (модуль CheckLibraryExists) уточняет наличие заданной библиотеки. С помощью команды check_function_exists() (модуль CheckFunctionExists) можно выяснить, доступна ли проекту некоторая функция. Подробные описания этих команд вы можете прочитать в файлах модулей расширения, которые по умолчанию хранятся в директории /usr/share/CMake/Modules/ и имеют расширения .CMake. Обычно в начале каждого модуля располагается комментарий, поясняющий, как работать с объявленными в нем командами. Получить справку о функциях модуля можно также с помощью специальной команды CMake (об этом сказано ниже).
Все это хорошо, скажете вы, но каким образом значения переменных CMake могут повлиять на генерацию файлов проекта? Во-первых, условные переходы позволяют пустить ее различными путями в зависимости от заданных значений переменных. Кроме того, значения переменных CMake можно переносить в заголовочные файлы.
Генерация заголовочных файлов с помощью CMake
CMake умеет генерировать заголовочные файлы, основываясь на шаблонах. Помимо обычных элементов, такой шаблон содержит специальные ключевые символы, заменяемые в результирующем заголовочном файле выражениями C/C++. Саму генерацию заголовочных файлов выполняет команда configure_file(). Впрочем, вместо долгих объяснений лучше один раз показать все это на практике. Добавьте в файл CMakeLists.txt строчки:
check_include_file_cxx("GL/glx.h" HAVE_GLX_H) set (NUM_VAR 16) configure_file(config.h.in config.h)
Обратите внимание на аргументы configure_file(). config.h.in – это шаблон, на основе которого CMake и генерирует файл config.h (сам config.h.in останется неизменным). Вам следует создать файл config.h.in, который должен быть расположен в той же директории, что и проект CMake, и добавить в него строки
#cmakedefine HAVE_GLX_H #define NUM_VAR ${NUM_VAR}
В этой же директории скомандуйте
cmake ./
В результате в каталоге появится файл config.h следующего содержания:
#define HAVE_GLX_H #define NUM_VAR 16
Если переменной CMake HAVE_GLX_H присвоено значение, эквивалентное логическому True, строка шаблона
#cmakedefine HAVE_GLX_H
заменяется в результирующем файле на
#define HAVE_GLX_H
Если же переменной HAVE_GLX_H присвоено значение, эквивалентное логическому False, или переменная не инициализирована, указанная строка шаблона будет заменена в заголовочном файле на
/* #undef HAVE_GLX_H */
Если в шаблоне присутствуют ключевые слова вида ${VARIABLE} или @VARIABLE@, то в результирующем заголовочном файле они будут заменены значением переменной VARIABLE (подстановка выполняется во всем файле, а не только в директивах #define).
Сборка нескольких целей в разных директориях
Обычно все исходные тексты, необходимые для сборки конкретной цели, расположены в одной директории. Однако проект может содержать несколько целей, у каждой из которых есть свой каталог. Нам, естественно, хотелось бы предоставить пользователю возможность собирать цели из всех поддиректорий одной командой – так сказать, одним махом семерых собирахом (простите мне мой древнеславянский). Решить эту проблему в CMake не составляет труда. Если проект состоит из нескольких директорий, причем каждая из них содержит исходные тексты одной (или нескольких) целей сборки, все, что нам нужно сделать – это сохранить в каждой директории файл CMakeLists.txt, содержащий инструкции сборки целей данной директории, а в корневом каталоге создать файл CMakeLists.txt, управляющий всем проектом в целом.
В качестве примера рассмотрим набор из двух программ, демонстрирующих возможности межпроцессного взаимодействия с помощью библиотеки wxWidgets. Исходные тексты этих программ (клиента и сервера) расположены в директории /samples/ipc дистрибутива wxWidgets. Я изменил структуру примера, расположив программу-клиент и программу-сервер в разных поддиректориях ipc: server – для сервера и client – для клиента (мой вариант примера вы найдете на диске, в файле ipc.tar.gz). Файлы CMakeLists.txt, предназначенные для управления сборкой каждой отдельной цели, заметно отличаются от тех, с которыми мы работали ранее. Вот, например, как выглядит текст файла CMakeLists.txt для цели client (он, как и положено, находится в поддиректории client):
project(client) cmake_minimum_required(VERSION 2.6) set(client_SRCS client.cpp) if(WIN32) set(client_SRCS ${client_SRCS} client.rc) endif(WIN32) add_executable(client WIN32 ${client_SRCS}) target_link_libraries(client ${wxWidgets_LIBRARIES})
Обратите внимание, что в файле сборки цели отсутствует команда find_package() и другие команды, необходимые для настройки среды окружения проекта.
Чтобы собирать все цели одной командой CMake, мы добавляем в корневую директорию ipc еще один файл CMakeLists.txt:
project(ipc) cmake_minimum_required(VERSION 2.6) set(wxWidgets_USE_LIBS base; core; net) find_package(wxWidgets REQUIRED) include(${wxWidgets_USE_FILE}) include_directories(${CMAKE_CURRENT_SOURCE_DIR}) add_subdirectory(client bin) add_subdirectory(server bin)
Как видите, команды find_package() и include(), как и объявление переменной wxWidgets_USE_LIBS, теперь перенесены в корневой файл мета-проекта. В список необходимых модулей wxWidgets, хранящийся в переменной wxWidgets_USE_LIBS, мы добавили модуль net, отвечающий за сетевые взаимодействия. Команда add_subdirectory() добавляет к мета-проекту сборки новую поддиректорию.
За всеми этими манипуляциями стоит довольно простая идеология: в проекте, состоящем из нескольких директорий и, соответственно, нескольких файлов CMakeLists.txt, эти файлы образуют иерархическую структуру, а команда add_subdirectory() просто добавляет в нее новые элементы. Важная особенность иерархии сценариев CMakeLists.txt заключается в том, что операции, выполненные в файле более высокого уровня, имеют силу и для файлов более низких уровней. Благодаря этому мы можем выделить в файлах, управляющих сборкой каждой цели, общие элементы и перенести их в корневой скрипт. Этот принцип иерархической структуры файлов мета-проектов уже использовался нами неявно при загрузке расширений CMake.
Обратим еще раз внимание на команду add_subdirectory(). Первым аргументом команды должно быть имя поддиректории, в которой содержится файл сборки цели. Во втором, необязательном, параметре команды мы передаем имя директории, в которой должен быть сохранен результат сборки. Таким образом, мы можем указать одну и ту же директорию для сохранения результатов сборки всех целей (в нашем примере это bin).
В корневом файле CMakeLists.txt присутствует еще одна команда, с которой мы раньше не встречались – include_directories(). Она добавляет в список директорий заголовочных файлов дополнительные каталоги, помимо тех, что генерируются в результате загрузки расширений CMake. В нашем примере обе цели сборки используют заголовочный файл ipcsetup.h, который хранится в корневой директории проекта. С помощью команды include_directories() мы добавляем ее в вышеупомянутый список. Аргументом include_directories() должно быть имя добавляемой директории (полное или относительное). Мы могли бы использовать и ./, но это выглядит не очень кросс-платформенно, поэтому мы задействуем переменную CMake_CURRENT_SOURCE_DIR, возвращающую имя директории исходных текстов для того файла CMakeLists.txt, в котором мы к ней обращаемся. Для корневого сценария CMakeLists.txt это будет, естественно, корневая директория. Теперь для сборки всего проекта необходимо просто скомандовать в корневом каталоге:
cmake ./ make
В результате в поддиректории bin появятся программы server и . Понравилось? Тогда оставайтесь на связи: в следующий раз мы рассмотрим более сложные аспекты использования CMake – установку приложений, комплексные проверки и написание собственных расширений.