среда, 15 июня 2011 г.

Система CMake - часть 2

Вы помните, кто такой Кевин Митник? Когда-то призывы досрочно освободить этого электронного взломщика из тюрьмы украшали сетевые странички многих начинающих хакеров. Противники Митника злорадно разъясняли, что «мастер социальной инженерии» не так уж хорошо разбирается в программировании. Приводились даже распечатки с форумов, где Митник, предположительно, просил других людей собрать ему его программы. Сколько в этих обвинениях было правды, я не знаю. Между прочим, сегодня, Митник – респектабельный ИТ-специалист, основатель компании, занимающейся компьютерной безопасностью (www.kevinmitnick.com). Как бы там ни было, я должен вас обрадовать. Чем бы вы ни занялись в будущем, никто не сможет обвинить вас в том, что вы не умеете собирать программы, если вы научитесь писать файлы управления сборкой для системы CMake.
Того, что мы узнали о CMake в прошлый раз, достаточно для написания простейших проектов сборки, однако для сборки более сложных проектов требуются более глубокие знания.

Исследуем систему

В предыдущей статье говорилось о том, что одним из преимуществ CMake является мощная система определения параметров платформы, для которой выполняется сборка. Источником информации о платформе служат специальные переменные, значения которых устанавливаются средой CMake, и команды. Мы уже знаем, что в результате загрузки расширения CMake нам становятся доступны специальные переменные, позволяющие выяснить параметры подключаемых к проекту библиотек и вспомогательных программ, используемых для сборки. Рассмотрим переменные и команды, которые позволяют определить параметры системы, не зависящие от загруженных расширений. В архиве sysinfo.tar.gz вы найдете несколько необычный файл 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(), как вы, конечно, догадались, реализует условные переходы в файле мета-проекта. Синтаксис команды if(), к сожалению, не столь очевиден. Аргументом команды является логическое выражение, управляющее переходом к одному из блоков команд. Практически любое значение переменной или операции может рассматриваться в языке CMake как логическое выражение. Если переменной не присвоено значение, или присвоено одно из значений N, NO, OFF, FALSE, NOTFOUND, -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. Для ускорения работы в ходе первой генерации файлов проекта CMake создает файл кэша CMakeCache.txt и в дальнейшем использует значения переменных из этого фала. Дальнейшие изменения файла CMakeLists.txt, равно как и изменения в системе, не приводят к обновлению кэша, и в результате CMake продолжает использовать старые значения переменных. Если интроспекция работает не так, как вы ожидали, первое, что нужно сделать – очистить кэш.

Генерация заголовочных файлов с помощью CMake

CMake умеет генерировать заголовочные файлы, основываясь на файлах шаблонов. Помимо обычных элементов заголовочного файла файл шаблона содержит специальные ключевые символы. В результирующем заголовочном файле эти символы заменяются выражениями C/C++. Саму генерацию заголовочных файлов выполняет команда configure_file(). Вместо долгих объяснений лучше один раз показать все это на практике. Добавьте в файл CMalekists.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, содержащий инструкции сборки целей данной директории, а в корневой директории создать файл CMake.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 уже использовался нами неявно при загрузке расширений 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 и client.

Ключи программы cmake

Рассмотрим кратко наиболее интересные ключи программы cmake:
-D – позволяет записать в кэш CMake переменную и ее значение. Формат определения переменной соответсвует синтаксису файла кэш, ИМЯ[:ТИП]=ЗНАЧЕНИЕ например:

cmake -D TESTVAR:BOOL=1 ./
В результате в файл CMakeCache.txt будет добавлена запись
TESTVAR:BOOL=1
-U – удаление из кэша переменных, имена которых соответствуют регулярному выражению, переданному в качестве элемента команды.
-G – эта команда позволяет указать имя генератора файлов проекта (в том формате, в котором имена генераторов возвращаются переменной CMAKE_GENERATOR). Список генераторов, доступных в текущей системе, можно узнать с помощью команды cmake --help
-E – запускает CMake в командном режиме. Аргументом ключа должна быть команда, которую нужно выполнить. Для получения списка доступных команд введите: cmake -E help
-i – запуск CMake в режиме мастера
--system-information – распечатывает информацию о CMake и проекте, если текущая директория содержит файл CMakeLists.txt. В качестве параметра команды может быть указан файл для сохранения информации о системе.
--help-command – выводит информацию о команде, имя которой передано ключу в качестве аргумента. В качестве второго параметра команды может быть указан файл для сохранения информации о системе. Например: cmake --help-command project --help-command-list – распечатывает список команд, для которых можно получить справку. В качестве параметра команды может быть указан файл для сохранения информации.
--help-commands – распечатывает справку по всем командам cmake. В качестве параметра команды может быть указан файл для сохранения информации.
--help-module – распечатывает справочные данные о заданном модуле. В качестве второго параметра команды может быть указан файл для сохранения информации.
--help-property – делает то же, что и --help-command, но только для свойств.
--help-variable – делает то же, что и --help-command, но только для переменных.

В следующий раз мы рассмотрим более сложные аспекты использования CMake – установку приложений, сложные проверки и написание собственных расширений.
Исходники для этой статьи

Комментариев нет:

Отправить комментарий