Журнал LinuxFormat - перейти на главную

LXF111:CMake

Материал из Linuxformat
Перейти к: навигация, поиск
CMake Кроссплатформенная система сборки для ваших приложений

Содержание

Раздвигая горизонты

CMake
ЧАСТЬ 3 CMake – не только мощный, но и расширяемый инструмент для сборки ваших приложений. Андрей Боровский покажет, как добавить ему функциональности.

Сегодня мы завершим знакомство с CMake обзором тех возможностей пакета, которым не нашлось места на предыдущих уроках. Из этой статьи вы узнаете, как писать собственные сценарии расширения CMake, научитесь устанавливать ПО и создавать дистрибутивы (нет, не Linux, но тоже полезные).

Нестандартные связи

Давайте условимся понимать под стандартными библиотеками те, для которых уже имеются готовые модули (сценарии) расширения CMake. Если вы пишете новую библиотеку, вполне логично создать для нее и модуль расширения. Этим мы займемся позже, а сейчас рассмотрим действия, необходимые для подключения библиотеки к проекту. Прежде всего, нам потребуется нестандартная (в указанном выше смысле) библиотека.

Структурно проект библиотеки мало чем отличается от проекта программы. Фактически, вся разница сводится к однойединственной команде. На прилагаемом диске вы найдете проект библиотеки demolib, которая экспортирует функцию 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 – динамически загружаемый разделяемый модуль [на большинстве систем эквивалентен SHARED, – прим. ред.]). Затем, как и в команде add_executable(), мы указываем список файлов исходных текстов, необходимых для сборки цели.

Конструкция if(${CMAKE_SYSTEM_NAME} STREQUAL XXX) позволяет определить, выполняется ли сценарий CMake на платформе XXX или на какой-либо другой (LXF110). Допустимо использовать и более краткую запись: if(WIN32), if(UNIX). Переменная UNIX обозначает все Unix-системы, но поскольку Solaris – это все же не HP-UX и не AIX, я предпочитаю более конкретный вариант с проверкой CMAKE_SYSTEM_NAME.

В приведенном выше примере мета-проекта задействована еще одна возможность CMake, с которой мы ранее не встречались – установка собранной цели. CMake предоставляет несколько команд, с помощью которых можно добавить в создаваемый проект средства инсталляции ПО. Самой полезной из них является install(), принимающая три группы аргументов: спецификатор, определяющий, что именно мы устанавливаем, список имен объектов и целевую директорию. Команда install(), вызванная со спецификатором TARGETS, устанавливает файлы, являющиеся целями (т.е. результатом сборки). В качестве аргументов ей передаются имя цели и каталог, в который должны быть помещены файлы. Чтобы сгенерировать инструкции установки файла, не являющегося целью сборки (например, demolib.h), используется команда install() со спецификатором FILES. Есть и другие опции: можно, например, указать, какую из конфигураций сборки (RELEASE, DEBUG и т.д.) следует использовать для инсталляции (если вы думаете, что никому не понадобится устанавливать DEBUG-проект, то ошибаетесь: многие библиотеки, модули расширения, да и программы можно отлаживать только после полной установки). В команде install() можно также указывать права доступа для устанавливаемого файла. Спецификатор SCRIPTS команды install() позволяет выполнять сценарии CMake до и после установки. Это может оказаться полезным в тех случаях, когда для корректной инсталляции необходимо не только скопировать файл, но и выполнить некоторые дополнительные действия – запустить утилиты, настроить файлы конфигурации, добавить записи в реестр (ой, о чем это я?..).

Корректная установка файлов возможна только при правильном выборе целевых директорий. В Linux и других *nix проблем обычно не возникает (таким образом, мы еще раз убеждаемся в технической рациональности идеи файловой системы с единым корнем). На платформе Windows все гораздо сложнее. Мало того, что разные важные директории (точнее – каталоги) могут быть расположены на разных дисках; в Windows вообще не существует единых правил относительно установки библиотек и разделяемых файлов. Например, популярные динамические библиотеки копируются в каталоги %WINDIR%/system/, %WINDIR%/system32/ и т.п., однако это касается только DLL; lib-файлы, необходимые для компоновки программ с разделяемыми библиотеками, устанавливаются в директории сред разработки. В то же время, команда install() по умолчанию копирует DLL- и LIB-файлы в один каталог.

В нашем мета-проекте мы сохраняем полное имя директории для установки библиотеки в переменной 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 методом интроспекции (LXF110): с помощью команды find_path() определяем каталог, в котором располагается какой-либо общераспространенный файл того же типа (в нашем примере – string.h), и устанавливаем demiolib.h в него же. В Linux искомой директорией почти непременно окажется /usr/include/ (можно было бы и не напрягаться), а вот при работе под Windows все будет сложнее. Путь к директории заголовочных файлов зависит от того, какое средство разработки мы используем и где оно установлено. Замечено, что под Windows данный метод интроспекции не всегда может корректно определить требуемый каталог с первого раза – нужно выйти из программы CMake GUI и запустить ее снова. Альтернативный вариант – указать расположение заголовочного файла вручную в окне CMake GUI (само наличие такой опции является признанием того, что интроспекция под Windows работает хуже, чем хотелось бы).

Как изменится сгенерированный проект от того, что в метапроект была добавлена команда install()? На каждой ОС это будет выглядеть по-своему. Если целевой платформой является Linux, в make-файл добавляется цель install, так что установить нашу библиотеку можно командой

sudo make install

При работе под Windows (среда Microsoft Visual C++) в решение (Solution) Visual Studio добавляется специальный проект INSTALL. Выглядит это несколько неуклюже, но другого универсального способа установки проектов под Windows, по-видимому, пока что не существует.

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

Скорая помощь

Если в метапроекте CMake вам нужно получить значение переменной окружения, для которой не существует встроенного «двойника», можно воспользоваться конструкцией $ENV{ИМЯ_ПЕРЕМЕННОЙ}, например:

$ENV{SHELL}
$ENV{WINDIR}

Синтаксис обращения к переменным окружения из CMake не зависит от платформы.


Если разделяемую библиотеку планируется использовать во многих проектах, целесообразно написать для нее собственный модуль, в котором будет выполняться поиск связанных с библиотекой файлов. Мы проделаем все это для библиотеки demolib (хотя, честно говоря, ее широкое распространение не предвидится). О том, что именно делают модули расширений CMake, говорилось в LXF110, поэтому перейдем сразу к начинке (файл 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.cmake мы загружаем модуль FindPackageHandleStandardArgs, который содержит полезную вспомогательную команду. Далее мы проверяем, не установлены ли уже значения переменных DEMOLIB_INCLUDE_DIR и DEMOLIB_LIBRARIES. Если обе переменные инициализированы, значит, они уже присутствуют в кэше (LXF110). Если кэш обновлять не нужно, мы присваиваем значение TRUE переменной DemoLib_FIND_QUIETLY.

Обратите внимание на префикс DemoLib, который соответствует основе имени файла модуля. В процессе обработки модуля система CMake проверяет значение этой и еще нескольких подобных служебных переменных. Дальше мы выполняем ту самую интроспекцию, ради которой все и затевалось. В команде find_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 в сценарии CMake, достаточно подгрузить модуль:

 include(CPack)

Если теперь мы запустим утилиту cmake, то в результирующем Make-файле появятся цели package и package_source. Первая предназначена для создания двоичного пакета, вторая – для дистрибутива исходных текстов. Если теперь мы наберем

 sudo 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, CPACK_BINARY_RPM, CPACK_BINARY_STGZ – указывают, нужно ли создавать пакет в формате Debian, RPM или .sh.
  • CPACK_BINARY_TGZ, CPACK_BINARY_TZ, CPACK_BINARY_TBZ2 – управляет созданием архивов tar.gz, tar.Z или tar.bz2. Последним четырем переменным по умолчанию присвоено значение ON, первым двум – 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. LXF

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

Простой библиотеке – простая программа. На диске вы найдете приложение 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()
Персональные инструменты
купить
подписаться
Яндекс.Метрика