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

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

Мы завершаем знакомство с CMake обзором тех возможностей системы, которые обычно используются на завершающих стадиях работы над проектом - установки собранного приложения, создания модуля расширения CMake, генерации дистрибутивов.

Создание и установка собственной библиотеки, директива install

В этом разделе мы создадим собственную разделяемую библиотеку и установим ее в системе средствами CMake. Поскольку в CMake отсуствует модуль расширения для работы с нашей библиотекой (и наша библиотека - далеко не единственная, для которой у CMake нет модуля расширения), мы рассмотрим подключение библиотек к проектам CMake напрямую, без модуля расширения. Наконец, мы напишем для нашей библиотеки модуль расширения CMake и рассмотрим подключение библиотеки к проекту с помощью этого модуля.
Структурно проект библиотеки мало чем отличается от проекта программы. Фактически, отличие сводится к одной единственной команде. На прилагаемом диске вы найдете проект библиотеки demolib. Эта библиотека экспортирует одну единственную функцию testfunc(). Функция testfunc() распечатывает на экране сообщение о том, что она была вызвана и завершается (вряд ли в природе существует более простая библиотека). Исходный текст библиотеки нас не интересует, переходим сразу к файлу CMakeLists.txt:
cmake_minimum_required(VERSION 2.6)
project(demolib C)
add_library(demolib SHARED demolib.c demolib.h)
if(${CMAKE_SYSTEM_NAME} STREQUAL Windows)
set(LIB_INSTALL_PATH ${CMAKE_INSTALL_PREFIX}/lib/)
elseif(${CMAKE_SYSTEM_NAME} STREQUAL Linux)
set(LIB_INSTALL_PATH /usr/lib/)
endif()
install(TARGETS demolib DESTINATION ${LIB_INSTALL_PATH})
find_path(LIB_INCLUDE_PATH string.h)
install(FILES demolib.h DESTINATION ${LIB_INCLUDE_PATH})
Значительная часть команд в этом файле предназначена для обеспечения кросс-платформенности. За обычным заголовком мета-проекта CMake следует команда add_library(). Как нетрудно догадаться, эта команда представляет собой аналог знакомой нам команды add_executable(), только в качестве цели сборки выступает не исполнимый файл программы, а файл библиотеки. Первым аргументом команды add_library() должно быть имя библиотеки (которое по совместительству является именем соответствующей цели сборки). Имя указывается в кросс-платформенном виде (без префикса lib и расширения so). Далее следует определение типа создаваемой библиотеки (SHARED – разделяемая, STATIC – статическая, MODULE – динамически загружаемый разделяемый модуль). Затем, как и в команде add_executable(), мы указываем список файлов исходных текстов, необходимых для сборки цели.
Конструкция if(${CMAKE_SYSTEM_NAME} STREQUAL XXX) позволяет нам определить, выполняется ли сценарий CMake на платформе XXX или на какой либо другой платформе (об этом шла речь в предыдущей статье). В CMake можно использовать и более краткую запись: if(WIN32), if(UNIX). Переменная UNIX обозначает все Unix-платформы. Поскольку между разными Юниксами все-таки существуют различия, я предпочитаю вариант с использованием переменной
CMAKE_SYSTEM_NAME
В приведенном выше примере мета-проекта задействована еще одна возможность CMake, с которой мы ранее не встречались – установка собранной цели проекта. CMake предоставляет нам несколько команд, с помощью которых мы можем добавить в создаваемый проект средства установки ПО. Самой полезной из этих команд является команда install(). У команды install() три группы аргументов: спецификатор, определяющий, что именно мы устанавливаем, список имен устанавливаемых объектов и целевая директория. Команда install(), вызванная со спецификатором TARGETS, устанавливает файлы, являющиеся целями (то есть, результатом) сборки. В качестве аргументов мы передаем команде имя цели сборки и директорию, в которую должны быть установлены целевые файлы. Для того чтобы сгенерировать инструкции установки файла, не являющегося целью сборки (например, файла demolib.h), мы вызываем команду install() со спецификатором FILES. У команды install() есть и другие опции, можно, например, указать, какую из конфигураций сборки (RELEASE, DEBUG и т.д.) следует использовать для установки (если вы думаете, что никому не понадобится устанавливать проект, собранный в отладочной конфигурации, то ошибаетесь, - многие библиотеки, модули расширения, да и программы можно отлаживать только после полной установки). В команде install() можно также указывать разрешения для устанавливаемого файла. Спецификатор SCRIPTS команды install() позволяет выполнять сценарии CMake до и после установки файлов. Это может оказаться полезным в тех случаях, когда для корректной установки необходимо не только скопировать файл, но и выполнить некоторые дополнительные действия – запустить утилиты, настроить файлы конфигурации, добавить записи в реестр (ой, о чем это я?)...
Корректная установка файлов возможна только при правильном выборе целевых директорий. В Linux и других Юниксах проблем с выбором директорий обычно не возникает (таким образом, мы еще раз убеждаемся в технической рациональности идеи файловой системы с единым корнем). На платформе Windows все гораздо сложнее. Мало того, что разные важные директории (точнее – каталоги) могут быть расположены на разных дисках, в Windows вообще не существует единых правил относительно установки библиотек и разделяемых файлов. Например, широко используемые разделяемые библиотеки устанавливаются в директории %WINDIR%/system/, %WINDIR%/system32/ и т.п., однако, в эти директории устанавливаются только файлы DLL, а файлы lib, которые требуются для связывания программ и разделяемых библиотек, устанавливаются в директории сред разработки. В то же время команда CMake install() по умолчанию устанавливает файл lib в ту же директорию, что и библиотеку DLL.
Если в мета-проекте CMake вам нужно получить значение переменной окружения, для которой не существует переменной-двойника в CMake, вы можете воспользоваться конструкцией $ENV{ИМЯ_ПЕРЕМЕННОЙ}, например: $ENV{SHELL}
$ENV{WINDIR}

Синтаксис обращения к переменным окружения из CMake не зависит от платформы.
В нашем мета-проекте мы сохраняем полное имя директории для установки библиотеки в переменной LIB_INSTALL_PATH. На платформе Windows мы записываем в эту переменную значение ${CMAKE_INSTALL_PREFIX}/lib/ (которое разрешается, например, в C:\Program Files\demolib\lib), а под Linux используем жестко заданное /usr/lib/. Обратите внимание на важную особенность работы install() – если указанная в этой команде директория не существует, она будет создана. С одной стороны это удобно, с другой стороны в случае ошибки или опечатки в системе могут появиться странные директории. В ходе моих экспериментов с Windows и Linux были случайно созданы директории C:\usr\local\lib и /usr/lib/;/.
Для определения имени директории, в которую следует установить заголовочный файл, мы пользуемся довольно распространенным при работе в CMake методом интроспекции: с помощью команды find_path() определяем директорию, в которой располагается какой-либо общераспространенный файл того же типа (в нашем примере – файл string.h) и устанавливаем файл demolib.h в эту же директорию. На платформе Linux искомой директорией окажется, почти непременно, /usr/include/ (можно было бы и не напрягаться), а вот при работе под Windows все будет сложнее. Путь к директории заголовочных файлов зависит от того, какое средство разработки мы используем и где оно установлено. Замечено, что под Windows данный метод интроспекции не всегда может корректно определить требуемую директорию с первого раза - нужно выйти из программы CMake GUI и запустить ее снова. Альтернативный вариант – указать расположение заголовочного файла вручную в окне CMake GUI (само наличие такой опции является признанием того, что интроспекция под Windows работает хуже, чем хотелось бы). Как изменится сгенерированный проект от того, что в мета-проект была добавлена команда install()? На разных это будет выглядеть по-разному. Если целевой платформой является Linux, в make-файл добавляется цель install, так что установить нашу библиотеку можно командой
sudo make install
При работе под Windows (среда Ms Visual C++) в проект (Solution) Visual Studio добавляется специальный под-проект INSTALL. Выглядит это несколько неуклюже, но другого универсального способа установки проектов под Windows по-видимому пока что не существует.

Подключение библиотеки

Простой библиотеке – простая программа. На диске вы найдете программку libtests, которая вызывает функцию testfunc() из библиотеки demolib. Само подключение выполняется с помощью уже знакомой нам команды target_link_libraries(). Ниже следует файл CMakeLists.txt для сборки программы libtest.
cmake_minimum_required(VERSION 2.6)
project(libtest)
find_path(DEMOLIB_INCLUDE_DIR demolib.h)
include_directories(${DEMOLIB_INCLUDE_DIR})
add_executable(libtest libtest.c)
if(${CMAKE_SYSTEM_NAME} STREQUAL Windows)
target_link_libraries(libtest $ENV{PROGRAMFILES}/demolib/lib/demolib.lib)
elseif(${CMAKE_SYSTEM_NAME} STREQUAL Linux)
target_link_libraries(libtest demolib)
endif()

Создание модуля CMake

Если разделяемую библиотеку планируется использовать во многих проектах, целесообразно написать для нее собственный модуль, в котором будет выполняться поиск связанных с библиотекой файлов. Мы напишем такой модуль для библиотеки demolib (хотя, честно говоря, широкое распространение этой библиотеки не предвидится). О том, что именно делают модули расширений CMake, говорилось в предыдущий статье. Перейдем сразу к начинке модуля (файл FindDemoLib.cmake):
include(FindPackageHandleStandardArgs)
if(DEMOLIB_INCLUDE_DIR AND DEMOLIB_LIBRARIES)
set(DemoLib_FIND_QUIETLY TRUE)
endif(DEMOLIB_INCLUDE_DIR AND DEMOLIB_LIBRARIES)
find_path(DEMOLIB_INCLUDE_HINT string.h)
find_path(DEMOLIB_INCLUDE_DIR demolib.h HINTS ${DEMOLIB_INCLUDE_HINT})
find_library(DEMOLIB_LIBRARIES demolib HINTS $ENV{PROGRAMFILES}/demolib/lib/ /usr/lib)
find_package_handle_standard_args(DemoLib DEFAULT_MSG DEMOLIB_LIBRARIES DEMOLIB_INCLUDE_DIR)
mark_as_advanced(DEMOLIB_LIBRARIES)
Самое меньшее, что должен сделать модуль загрузки разделяемой библиотеки XXX, - записать в переменные XXX_INCLUDE_DIR и XXX_LIBRARIES пути, соответственно, к заголовочным файлам и самой библиотеке. Кроме того должны быть инициализированы служебные переменные, например XXX_FOUND. Помимо этого модуль может предоставлять специальные команды, дополнительные переменные и многое другое, но в нашем примере мы ограничимся минимумом. Модуль FindDemoLib записывает путь к библиотеке demolib в переменную DEMOLIB_LIBRARIES, а путь к файлу demolib.h – в переменную DEMOLIB_INCLUDE_DIR.
В первой строчке модуля FindDemoLib мы загружаем модуль FindPackageHandleStandardArgs, который содержит полезную вспомогательную команду. Далее мы проверяем, не установлены ли уже значения переменных DEMOLIB_INCLUDE_DIR и DEMOLIB_LIBRARIES. Если значения обеих переменных установлены, значит кэш уже содержит эти переменные (об особенностях обновления кэша CMake говорилось в предыдущей статье). Если кэш обновлять не нужно, мы присваиваем TRUE переменной DemoLib_FIND_QUIETLY. Обратите внимание на префикс DemoLib, который соответствует основе имени файла модуля. В процессе обработки модуля система CMake проверяет значение этой и еще нескольких подобных служебных переменных. Дальше мы выполняем ту самую интроспекцию, ради которой все и затевалось. В команде fund_path() используется новый элемент – спецификатор HINTS. Этот спецификатор позволяет делать среде CMake «подсказки», упрощающие поиск файлов. За спецификатором HINTS обычно следует список директорий, в которых может находиться (а может и не находиться) искомый файл. Если файл не будет найден в «подсказанных» директориях, система выполнит стандартный поиск. Спецификатор HINTS не следует путать со спецификатором PATHS, с помощью которого мы можем жестко указать список директорий для поиска. Отметим, что даже с подсказкой система не всегда может найти директорию заголовочных файлов на платформе Windows. В этом случае придется вводить имя директории вручную в окне графической утилиты CMake. Команда find_package_handle_standard_args(), предоставляемая загруженным модулем, выполняет рутинные действия по инициализации служебных переменных. Первый аргумент команды – основа имени модуля, которая используется, например, для генерации мен служебных переменных. Второй аргумент определяет, что именно программа должна сказать в том случае, если библиотека demolib не будет найдена. Вместо значения DEFAULT_MSG можно указать свой собственный текст. Далее следуют имена переменных, в которых содержаться путь к библиотеке и заголовочным файлам соответственно.
Завершающая команда mark_as_advanced() помечает переменные, переданные ей в качестве аргумента, меткой advanced. Такие переменные не отображаются в окне графического инструмента CMake. Файл FindDemoLib.cmake следует скопировать в директорию Modules (директория расширений CMake). В результате в мета-проекте для сборки libtest мы сможем обойтись без интроспекции:
cmake_minimum_required(VERSION 2.6)
project(libtest)
find_package(DemoLib REQUIRED)
include_directories(${DEMOLIB_INCLUDE_DIR})
add_executable(libtest libtest.c)
target_link_libraries(libtest ${DEMOLIB_LIBRARIES})

Подготовка к распространению

Помимо пакета CMake компания Kitware выпускает еще несколько полезных пакетов, в том числе CPack – средство создания дистрибутивов. Пакет CPack входит в состав пакета CMake и им можно управлять из сценариев CMake, так что уместно будет рассмотреть CPack здесь. Для того чтобы задействовать CPack в сценарии CMake достаточно включить в сценарий модуль расширения CMake:
include(CPack)
Если теперь мы запустим утилиту cmake, то в результирующем Make-файле появятся цели package и package_source. Первая цель предназначена для создания двоичного пакета, вторая – для создания пакета исходников. Если теперь мы скомандуем
sudo make package
(команду make package необходимо выполнять от имени суперпользователя), то в результате получим файл сценария оболочки с расширением .sh, а также файлы с расширениями .tar.gz, tar.Z и tar.bz2. Имена файлов сконструированы из имени проекта, номера версии и названия платформы. Например, для проекта demolib, на примере которого мы изучаем CPack, все эти файлы будут иметь имя demolib-0.1.1-Linux. Перечисленные файлы представляют собой двоичные пакеты в разных форматах (по умолчанию CPack создает сразу несколько пакетов). Файл с расширением .sh – это сценарий оболочки с встроенным архивом tar.gz. Если мы запустим сценарий на выполнение, он задаст нам несколько вопросов по поводу согласия с лицензией и путей установки, после чего (если наши ответы его устроят) распакует содержимое встроенного архива в заданную директорию. Файлы с расширениями .tar.* в комментариях не нуждаются.
Если изложенное выше навело вас на мысль, что создавать двоичные пакеты с помощью CPack очень просто, то вы почти правы. На практике, однако, можно столкнуться с некоторыми сложностями. Механизм генерации пакетов CMake-CPack использует инструкции, заданные нами для генерации цели install (установки проекта). Попросту говоря, для создания цели package умалчиваемое значение переменной CMAKE_INSTALL_PREFIX (напомню, что эта переменная содержит путь к корневой директории для установки) заменяется на путь к некоей временной директории. Далее вызывается цель install, в результате чего выполняется «холостая» установка проекта во временную директорию. Затем содержимое временной директории упаковывается в архивы. Этот факт имеет несколько последствий. Во-первых, вы можете создать двоичный пакет только в том случае, если ваш мета-проект содержит инструкции для генерации цели install. Во-вторых, поскольку процесс создания пакета использует подмену значения CMAKE_INSTALL_PREFIX, генерация может пройти успешно только в том случае, если команды install() используют эту переменную. Если вы выполняете нестандартные действия с директориями, будьте готовы к неожиданным проблемам с генерацией пакетов. Для того чтобы CMake мог задействовать переменную CMAKE_INSTALL_PREFIX, в команде install() следует указывать относительные, а не абсолютные пути (например, lib а не /usr/lib). Наконец, в-третьих, выполнение процесса установки в ходе генерации пакета может вызвать побочные эффекты в том случае, если установка включает в себя какие-то действия помимо копирования файлов.
Конфигурирование CPack из мета-проектов CMake выполняется с помощью переменных, которые использует модуль CPack. Перечислим наиболее интересные переменные.
  • CPACK_BINARY_DEB – указывает, нужно ли создавать пакет в формате Debian.
  • CPACK_BINARY_RPM – указывает, нужно ли создавать RPM-пакет.
  • CPACK_BINARY_STGZ – указывает, нужно ли создавать файл скрипта оболочки со встроенным архивом tar.gz.
  • CPACK_BINARY_TGZ – нужно ли создавать архив tar.gz.
  • CPACK_BINARY_TZ – нужно ли создавать архив tar.Z.
  • CPACK_BINARY_TBZ2 – нужен ли архив tar.bz2.
Последним 4 переменным по умолчанию присвоено значение ON, первым 2 – OFF.
  • CPACK_INSTALL_PREFIX – переменная, в которой сохраняется полное имя корневой директории для установки проекта.
  • CPACK_PACKAGE_DESCRIPTION_FILE – путь к файлу с развернутым описанием собираемого пакета.
  • CPACK_PACKAGE_DESCRIPTION_SUMMARY – краткое описание собираемого пакета.
  • CPACK_PACKAGE_FILE_NAME – основа имени файла пакета.
  • CPACK_PACKAGE_INSTALL_DIRECTORY – директория, в которую по умолчанию извлекается содержимое пакета.
  • CPACK_PACKAGE_VENDOR – имя сборщика пакета.
  • CPACK_PACKAGE_VERSION_MAJOR, CPACK_PACKAGE_VERSION_MINOR, CPACK_PACKAGE_VERSION_RELEASE – эти три переменные содержат три цифры версии распространяемого ПО – старшую, младшую и номер релиза соответственно (используется, в том числе, при конструировании имени файла пакета).
  • CPACK_RESOURCE_FILE_LICENSE – путь к файлу с текстом лицензии.
  • CPACK_RESOURCE_FILE_README – путь к файлу README.
  • CPACK_SYSTEM_NAME – имя системы (используется, в том числе, при конструировании имени файла пакета).
Если результатом сборки является скрипт или пакет RPM, информация из файлов лицензии, README и WELCOME становится частью пакета. Для того чтобы изменить настройки CPack, заданные по умолчанию, нужно изменить значения соответствующих переменных перед вызовом include(). Например, если мы хотим создать пакет RPM и привести его имя к классическому виду, можем написать:
set(CPACK_BINARY_RPM ON)
set(CPACK_SYSTEM_NAME i686)
include (CPack)
С учетом всего вышеизложенного вариант сценарий сборки demolib с дополнительной целью package выглядит так:
cmake_minimum_required(VERSION 2.6)
project(demolib C)
if(UNIX)
set(CMAKE_INSTALL_PREFIX /usr)
set(CPACK_BINARY_RPM ON)
set(CPACK_SYSTEM_NAME i686)
endif(UNIX)
set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "Demo Library Project")
set(CPACK_PACKAGE_VERSION 1.0.0)
include(CPack)
add_library(demolib SHARED demolib.c demolib.h)
install(TARGETS demolib DESTINATION lib)
install(FILES demolib.h DESTINATION include)
На платформе Windows для создания двоичных пакетов можно использовать Nullsoft NSIS (что выходит за рамки этой статьи) и zip (что не кошерно с точки зрения Windows). Как уже говорилось, с помощью CPack можно создавать не только двоичные пакеты, содержащие собранное ПО, но и пакеты исходников. Система сборки пакетов исходных текстов проработана не так хорошо, как система сборки двоичных пакетов. По умолчанию вызов
make package_source
приводит к тому, что все содержимое корневой директории проекта и всех ее поддиректорий (в том числе и с двоичными файлами) упаковывается в архив. Более того, поскольку сам файл пакета исходников по умолчанию сохраняется в той же корневой директории, может возникнуть ситуация, при которой упаковщик будет пытаться заархивировать файл сам в себя. Управление настройкой генератора пакетов исходных текстов так же выполняется с помощью переменных, имена которых начинаются с префикса CPACK_SOURCE_. Как и в случае с CMake, вы можете узнать много полезного о переменных CPack, ознакомившись с файлами CPackConfig.cmake и CPackSourceConfig.cmake. Некоторые переменные из этих файлов попадают в кэш CMake.
Надеюсь, что после всего сказанного о CMake вы придете к тем же выводам, к которым пришел и я – этот пакет не только является средством кросс-платформенной сборки, но и упрощает жизнь программиста, работающего исключительно на Linux.
Проект libtest Проект demolib

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

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