Используем GYP для сборки С/C++ проекта

в 12:51, , рубрики: c++, cmake, make, Песочница, метки: , ,

Введение

На хабре уже несколько раз упоминалась система генерации скриптов сборки и проектных файлов CMake. Система достаточно популярна и неплохо документирована. Не все знают, что есть неплохая альтернатива этой (безусловно, замечательной) системе. Да, речь в статье пойдёт о проекте Generate Your Project (GYP).

Предыстория

GYP разрабатывался для сборки конкретного проекта — браузера Chrome/Chromium. Решение разрабатывать свой инструмент вызвало множество споров, многие не понимали (возможно, до сих пор не понимают), зачем изобретать свой «велосипед». Тем не менее, GYP стал довольно популярным инструментом, и сейчас он используется за пределами инфраструктуры Chromium, например, для сборки V8 и нативных модулей NodeJS.

Сходтсва с и отличия от CMake

Создатели GYP преследовали практически ту же цель, что и авторы CMake: реализовать кросс-платформенную систему, позволяющую описывать процесс сборки высокоуровневым языком, подходящим для генерации различных проектных файлов и скриптов для систем сборки. GYP на данный момент поддерживает GNU make, SCons, Ninja, Eclipse (CDT), VisualStudio и XCode.

Основные отличия GYP от CMake (на мой взгляд):

  • Гораздо более приятный и понятный язык описания структуры проекта. Обратите внимание, GYP-файл именно задаёт структуру, а не описывает последовательность действий, как CMakeLists.txt. Всё очень интуитивно и декларативно. К тому же, такой формат очень удобен для автоматизированной обработки и анализа.
  • Удобные средства для композиции проектов из отдельных модулей. Цель, объявленная в GYP-файле, может декларировать в качестве зависимостей цели других GYP-файлов.
  • Более плотная интеграция с возможностями IDE. GYP позволяет объявлять свойства целей, которые будут учитываться, к примеру, только для проектов VS, и игнорироваться для прочих проектных файлов.
  • GYP реализован на Python и имеет достаточно компактный, структурированный и читаемый исходный код (что мне сложно сказать о CMake). Если мне, к примеру, нужно будет реализовать генератор нового вида проектных файлов (заглушки для EDE, например), я без раздумий предпочту GYP в качестве базы.

Желающие узнать об отличиях GYP от CMake из первых уст могут посетить соответствующую страницу на официальной Wiki. Там же можно найти ссылку на mailing list с горячей перепалкой разработчиков.

Установка

Установка потребует наличия Python, желательно версии 2.6 или выше. Пользователям Linux, скорее всего, не потребуется его устанавливать, пользователи Windows могут обратиться к ActivePython или скачать дистрибутив с официального сайт Python.

Я предпочитаю использовать последнюю версию GYP из официального репозитория. Для инсталляции из исходного кода нужно зайти в каталог с исходным кодом и выполнить команду sudo python setup.py install. Если у вас нет желания устанавливать GYP глобально с правами администратора, можете просто добавить каталог с исходниками GYP в переменную окружения PATH.

Популярные Linux-дистрибутивы часто имеют пакет gyp в стандартном репозитории (возможно, не самой свежей версии).

Мне также нравится подход авторов V8, которые производят checkout определённой протестированной версии GYP в подкаталог build и используют именно эту версию для генерации Make-файлов для различных платформ.

Для пользователей Emacs может оказаться полезным плагин для редактирования GYP-файлов.

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

Собираем самодостаточный модуль

Итак, настало время показать инструмент в действии. Думаю, мало кого интересует демонстрация на примере вроде «Hello World», поэтому для начала опишем конфигурацию для сборки какого-нибудь полезного модуля, например, для библиотеки GTest (для которой в комплекте идёт CMake-скрипт, так что GTest вдвойне полезней в качестве образца: любопытный читатель сможет оценить читабельность обеих спецификаций и выбрать тот инструмент, который ему больше нравится).

Ниже представлена несколько упрощённая и урезанная версия спецификации, находящейся в репозитории Chromium.

{
  'targets': [                     # <- Список целей
    # Первая цель
    {
      'target_name': 'gtest',      # <- Имя цели
      'cflags': ['-pthread'],      # <- Флаги компиляции
      'link_settings': {           # <- Настройки линковщика
        'libraries': ['-lpthread'] # <- Список необходимых библиотек
      },
      'type': 'static_library',    # <- Тип цели, возможны варианты static_library,
                                   # shared_library, executable, none

      'standalone_static_library': 1, # <- Не собирать библиотеку как thin archive
                                      # Опция может быть недоступна в старых версиях GYP

      'sources': [                 # <- Список исходных файлов
        'include/gtest/gtest-death-test.h',
        'include/gtest/gtest-message.h',
        'include/gtest/gtest-param-test.h',
        'include/gtest/gtest-printers.h',
        'include/gtest/gtest-spi.h',
        'include/gtest/gtest-test-part.h',
        'include/gtest/gtest-typed-test.h',
        'include/gtest/gtest.h',
        'include/gtest/gtest_pred_impl.h',
        'include/gtest/internal/gtest-death-test-internal.h',
        'include/gtest/internal/gtest-filepath.h',
        'include/gtest/internal/gtest-internal.h',
        'include/gtest/internal/gtest-linked_ptr.h',
        'include/gtest/internal/gtest-param-util-generated.h',
        'include/gtest/internal/gtest-param-util.h',
        'include/gtest/internal/gtest-port.h',
        'include/gtest/internal/gtest-string.h',
        'include/gtest/internal/gtest-tuple.h',
        'include/gtest/internal/gtest-type-util.h',
        'src/gtest-all.cc',
        'src/gtest-death-test.cc',
        'src/gtest-filepath.cc',
        'src/gtest-internal-inl.h',
        'src/gtest-port.cc',
        'src/gtest-printers.cc',
        'src/gtest-test-part.cc',
        'src/gtest-typed-test.cc',
        'src/gtest.cc',
      ],
      'sources!': [                      # <- Перечисленные исходные файлы нужно исключить,
        'src/gtest-all.cc',              # эту директиву удобно использовать в
      ],                                 # секциях conditions

      'include_dirs': [                  # <- Список каталогов с заголовочными файлами
        '.',
        './include',
      ],
      'conditions': [                    # <- Раздел с конфигурацией, зависящей от
        ['OS == "linux"', {              # платформы и целевого формата
          'defines': [
            'GTEST_HAS_RTTI=0',
          ],
          'direct_dependent_settings': {
            'defines': [
              'GTEST_HAS_RTTI=0',
            ],
          },
        }],
        ['OS=="win" and (MSVS_VERSION=="2012" or MSVS_VERSION=="2012e")', {
          'defines': [
            '_VARIADIC_MAX=10',
          ],
          'direct_dependent_settings': {
            'defines': [
              '_VARIADIC_MAX=10',
            ],
          },
        }],
      ],
      'direct_dependent_settings': { # <- Настройки, которые будут добавлены к целям,
                                     # использующим цель gtest прямую, т. е. не транзитивно

        'defines': [                 # <- Определения препроцессора
          'UNIT_TEST',
        ],
        'include_dirs': [            # <- Каталог с заголовочными файлами include будет
          'include',                 # автоматически добавлен всем зависимым целям,
        ],                           # причём будет использован абсолютный путь, рассчитанный
                                     # как </path/to/this/gypfile>/include

        'msvs_disabled_warnings': [4800],
      },
    },
    # Вторая цель
    {
      'target_name': 'gtest_main',
      'type': 'static_library',
      'standalone_static_library': 1,
      'dependencies': ['gtest'],     # <- Список зависимостей цели, в данном случае
                                     # вторая цель зависит от первой
      'sources': [
        'src/gtest_main.cc',
      ],
    },
  ],
}

Думаю, многим эта спецификация покажется довольно прозрачной (за исключением, возможно, секций с условиями).

В глаза сразу бросается необходимость перечисления всех файлов с исходным кодом. Это может показаться утомительным и чрезмерно многословным. GYP не поддерживает аналог GLOB из CMake, более того, эта возможность не была реализована сознательно. По мнению разработчиков, отсутствие GLOB уменьшает вероятность ошибок и увеличивает «герметичность» и воспроизводимость сборок.

Заголовочные файлы тоже нужно включать в список исходников, иначе они просто не будут видны при генерации проектов Visual Studio.

Чтобы выполнить сборку, достаточно выполнить следующие команды:

gyp --depth=. gtest.gyp  # Создаём Makefile 
make                     # Запускаем make

После изменении файла gtest.gyp Makefile будет автоматически перегенерирован при следующем запуске make.

Артефакты, полученные в результате сборки, можно найти в подкаталогах каталога out/BUILDTYPE, по умолчанию это out/Default.

Замечание: Генератор проектных файлов выбирается в зависимости от операционной системы (msvs для Windows, make для Linux, xcode для Mac) и переменной окружения GYPGENERATORS, причём последняя имеет приоритет. Чтобы запустить конкретный генератор (или несколько сразу), нужно выполнить команду, подобную следующей:

GYP_GENERATORS=make,scons,eclipse gyp --depth=. gtest.gyp

К сожалению, у меня возникли проблемы с генераторами scons и msvs под Linux. Думаю, желательно всё же использовать генератор по умолчанию для вашей платформы (тем не менее, генератор make под Mac должен работать без проблем).

Подстановка переменных

Раскрытие переменных в GYP происходит в две фазы: на «ранней» фазе происходит вычисление условий внутри секций conditions и переменных, объявленных с квантификатором <; на «поздней» — вычисление условий раздела target_conditions и переменных с квантификатором >, а также подстановка вывода внешних команд.

Для большинства задач подходят переменные «ранней» фазы.

Переменные ранней и поздней фазы отличаются направлением первого символа в месте использования: <(var) — ранняя фаза (стрелочка указывает влево, т.е. вычисление происходит раньше по шкале времени), >(var) — поздняя фаза (стрелочка указывает вправо).

Значения переменных можно вычислить в двух различных контекстах:

  • Строковый контекст (<(var), >(var)) — значение переменной подставляется как есть.
  • Списковый контекст (<@(var), >@(var)) — значение переменной встраивается в список, в котором она вычисляется (такое вычисление должно обязательно происходить внутри списка).

Пример использования различных контекстов:

{
  'variables': {
    'component_type': 'shared_library',
    'public_api_headers': [
      'include/mylib.h',
      'include/mylib_extra.h',
    ],
    'private_headers': [
      'internals.h',
    ],
  },
  'targets': [
    {
      'target_name': 'mylib',
      'type': '<(component_type)', # <- Строковый контекст
      'include_dirs': ['include'],
      'sources': [
        '<@(public_api_headers)',  # <- Списки public_api_headers
        '<@(private_headers)',     # и private_headers будут встроены
        'src/impl.cc',             # в список sources
      ],
    },
  ],
}

Переменные, значением которых является список, можно вычислять в строковом контексте. Результатом вычисления будет строка, состоящая из элементов списка, разделённых пробелами. Аналогично, переменную, значением которой является строка, можно вычислить в списковом контексте. При этом из неё будет сконструирован список, в качестве разделителя элементов будет использован пробел.

Иногда требуется, чтобы значение переменной было вычислено при помощи внешней команды, для этого используются конструкции <!(cmd) и <!@(cmd):

'variables' : [
  'foo': '<!(echo Build Date <!(date))',
],

Для переменных можно задавать значение по умолчанию, оно будет использовано, если иначе переменная окажется не определённой в месте использования. Синтаксис задания значения по умолчанию не особо интуитивен:

{
  'variables': {
    'component_type%': 'shared_library', # <- Символ % в конце имени переменной
                                         # означает значение по умолчанию
  }
  #...
}

Возможно также ссылаться на переменные, определённые во внешней системе сборки. В случае make можно использовать знак $ (например, $(INCLUDES)). К сожалению, использование таких переменных делает сборку менее переносимой.

Условия

Секция conditions позволяет объявлять части конфигурации, зависящие от факторов, внешних по отношению к собираемому модулю. Например, в зависимости от целевой операционной системы или от желаемого вида собираемого компонента (статически или динамически линкуемая библиотека) требуется изменять флаги компиляции или добавлять/исключать файлы с исходным кодом.

В случае, если условие секции выполняется, её декларации будут объединены с декларациями цели, в которой определено условие (или с декларациями всех целей, если условие объявлено в разделе target_conditions).

Простой пример:

{
  'target_name': 'mylib',
  'type': 'static_library',
  # ...
  'conditions': [
    ['OS=="linux"', {
      'sources': ['linux_extra.cc'], # <- Включаем дополнительный файл
      'defines': ['UNIX=1'],         # <- Определяем макрос UNIX со значением 1
    }],
  ],
}

Условия вычисляются интерпретатором Python с помощью функции eval() с отключенным словарём __builtin__, следовательно, они подчиняются синтаксису, принятому в языке Python для вычисления булевых выражений. К примеру, несколько условий можно объединять операторами and и or. Список предопределённых переменных и более развёрнутые примеры можно найти на wiki.

Включаемые файлы

В крупных проектах возможна такая ситуация, что часть деклараций приходится писать заново в каждом GYP-файле. Этого можно избежать, если использовать механизм включения файлов, подобный директиве #include. В GYP такой механизм реализован в виде списка верхнего уровня includes:

{
  'includes': ['common.gypi', 'other.gypi'],
  # ...
}

Включаемые GYP-файлы обычно имеют расширение gypi и содержат декларации общих переменных, конфигураций сборки, заголовочных файлов и т.п. Все эти декларации будут объединены с декларациями GYP-файла, в который будет включен gypi-файл. Относительные пути, используемые внутри включаемого файла, рассчитываются относительно включаемого, а не включающего файла.

Debug and Release: конфигурации сборки

Практически всегда для отладки приложения и для установки его заказчику требуются различные параметры сборки. В режиме отладки хочется сохранить доступ к символам для работы с отладчиком, заказчику же желательно поставлять компактную оптимизированную версию приложения. В GYP эта потребность отражена с помощью подраздела configurations.

Добавим следующие строки в наш gtest.gyp:

{
  'target_defaults': {
    'configurations': {
      'Release': {
        'conditions': [
          ['OS=="linux"', {
              'cflags': ['-O2'],        # Включить оптимизацию
            }],
        ],
      },
      'Debug': {
        'conditions': [
          ['OS=="linux"', {
              'cflags': ['-g', '-O0'],  # Отключить оптимизацию, добавить
            }],                         # символы для отладки
        ],
      },
    },
  },
 'targets': [
    # ...
  ],
}

Обратите внимание на тот факт, что раздел configurations должен быть вложен в раздел target_defaults. Если забыть об этом, то сообщений об ошибках, скорее всего, не последует, но и конфигурации работать не будут.

Чтобы указать конфигурацию при сборке с помощью make, достаточно определить параметр BUILDTYPE. Для отладки также часто бывает полезным посмотреть реальные команды, выполняемые системой сборки. За это отвечает флаг V (verbose):

make BUILDTYPE=Release V=1

Разумеется, можно определять произвольное количество конфигураций с произвольными настройками.

Теперь библиотека отлично подходит для использования во множестве других проектов, достаточно лишь сослаться на неё из спецификации иерархического проекта. Оставшуюся скучную работу возьмёт на себя GYP. Думаю, такую композицию стоит рассмотреть поподробнее, так как она очень важна на практике.

Собираем несколько модулей

Удобство сборки проекта из независимых модулей — одно из основных качеств, которым должна обладать хорошая система управления проектом, и GYP в этом отношении отлично себя проявляет.

В качестве второго модуля я выбрал несколько функций для проверки соответствия входной строки упрощённым регулярным выражениям, описанных в первой главе книги Beautiful Code (ISBN-10: 0596510047) (также доступна онлайн-версия этой главы).

Репозиторий с исходным кодом и конфигурацией находится на GitHub.

В каталоге examples расположены два подкаталога: gtest-1.6 (компонент для написания юнит тестов, рассмотренный выше) и mini-regex — наша микро-библиотека, нуждающаяся независимой разработке и тестировании. Привожу GYP-файл для сборки библиотеки libminiregex.a, зависящей от компонента gtest:

{
  'includes': ['../conf.gypi'],              # <- Общие определения
  'targets': [
    {
      'target_name': 'miniregex',
      'type': 'static_library',
      'include_dirs': ['include'],
      'sources': [
        'include/miniregex.hpp',             # <- Интерфейс
        'src/miniregex.cpp',                 # <- Реализация
      ],
      'direct_dependent_settings': {
        'include_dirs': ['include'],
      },
    },
    {
      'target_name': 'miniregex_test',
      'type': 'executable',                  # <- Исполняемый файл
      'dependencies': [
        '../gtest-1.6/gtest.gyp:gtest',      # <- Зависит от библиотек модульных тестов
        '../gtest-1.6/gtest.gyp:gtest_main', # и от libminiregex, объявленного выше
        'miniregex',                    
       ],
      'sources': [
        'src/test/test_miniregex.cpp',       # <- Исходный код тестов
      ],
    },
  ],
}

Отдельных пояснений заслуживает синтаксис указания зависимостей, объявленных в других GYP-файлах. Чтобы указать такую зависимость, достаточно указать путь к GYP-файлу и через двоеточие задать имя цели (или звёздочку, что означает зависимость от всех целей файла).

Замечание: очень важно помнить, что при сборке проекта, состоящего из нескольких модулей, все модули должны определять соответствующую конфигурацию (Debug, Release, etc.). При отсутствии нужной конфигурации на этапе компоновки проектных файлов не будет выведено никаких предупреждений, но при попытке сборки, скорее всего, будут выводиться таинственные сообщения об ошибках.

В целях демонстрации оба модуля разделяют общий файл conf.gypi, содержащий определения конфигураций. Это имеет смысл, если модули нужно хранить в одном репозитории. Тем не менее, мне кажется удачной идея выносить независимые модули, подходящие для повторного использования (наши две библиотеки, кажется, неплохо для этого подходят), в отдельные репозитории и использовать их через механизм внешних ссылок (вроде svn:externals или git submodule).

Чтобы собрать модуль mini-regex, нужно зайти в каталог examples и выполнить уже знакомые команды:

gyp --depth=. mini-regex/miniregex.gyp
make

Сначала будет произведена сборка библиотек модуля gtest, затем библиотеки libminiregex.a, затем уже будет скомпонован исполняемый файл miniregex_test, который можно найти в каталоге out/Debug. Если всё сделано правильно, при запуске этого исполняемого файла на консоли должен появиться позитивный зелёненький вывод GTest.

Пока мы связали всего два модуля, но подход отлично масштабируется, позволяя без особых проблем компоновать иерархии модулей, инкапсулируя все детали сборки каждого из модулей в собственном GYP-файле, причём собирать каждый модуль можно независимо от других. Выразительным примером такой архитектуры служит проект Chromium.

Действия и Правила

Довольно часто при сборке нужно выполнить какое-нибудь действие или нестандартное преобразование.

Для определения однократных действий используется раздел actions, для определения преобразований — раздел rules. Правила могут быть использованы для построения цепочек преобразований, аналогично тому, как это реализовано в GNU make. Правила также можно рассматривать как шаблоны действий.

В качестве реализуем действие для инсталляции библиотек и заголовочных файлов модуля GTest:

{
  'target_name': 'install',
  'type': 'none',
  'dependencies': ['gtest', 'gtest_main'],  # <- Перед инсталляцией библиотеки
                                            # нужно собрать
  'actions': [
    {
      'inputs': [],
      'outputs': ['$(LIBRARIES)/libgtest.a',
                  '$(LIBRARIES)/libgtest_main.a'],
      'action_name': 'copy_libs',
      'action': ['cp', '<(PRODUCT_DIR)/libgtest.a',
                       '<(PRODUCT_DIR)/libgtest_main.a',
                       '$(LIBRARIES)'],
      'message': 'Copying libraries',
    },
    {
      'inputs': [],
      'outputs': ['$(INCLUDES)/gtest', '$(INCLUDES)/gtest/internal'],
      'action_name': 'copy_headers',
      'action': ['cp', '-R', 'include/gtest', '$(INCLUDES)'],
      'message': 'Copying header files',
    }
  ],
}

Здесь использованы внешние переменные, подразумевается, что вызываемый make получит переменные INCLUDES и LIBRARIES через окружение или аргументы командной строки:

gyp --depth=. gtest.gyp
# Не стоит выполнять эту команду в терминале
sudo make install INCLUDES=/usr/include LIBRARIES=/lib64

В своём проекте я использую подобную технику для сборки RPM-пакетов.

В качестве примера правила можно привести правило rst2html, которое я использую для компиляции документации из формата RST в формат HTML:

{
  'target_name': 'docs',
  'type': 'none',
  'sources': [
    'doc/Build.rst',
    'doc/Dictionary.rst',
    'doc/README.rst',
  ],
  'rules': [{
      'rule_name': 'rst2html',
      'extension': 'rst',
      'inputs': ['doc/css/code.css'],
      'action': ['rst2html.py',
                 '--stylesheet-path=doc/css/code.css',
                 '--embed-stylesheet',
                 '<(RULE_INPUT_PATH)',
                 '<(PRODUCT_DIR)/Doc/<(RULE_INPUT_ROOT).html'],
      'outputs': ['<(PRODUCT_DIR)/Doc/<(RULE_INPUT_ROOT).html'],
      'message': 'Compiling RST document <(RULE_INPUT_PATH)' 
        'to HTML <(PRODUCT_DIR)/Doc/<(RULE_INPUT_ROOT).html',
    }],
},

Свойство extension задаёт расширение файлов, попадающих под правило, а список inputs определяет файлы, являющиеся дополнительными зависимостями (т.е. в случае их изменения требуется повторно применить правило). Переменная RULE_INPUT_PATH привязывается к абсолютному пути входного файла действия RULE_INPUT_ROOT — к базе пути входного файла (т.е. без расширения). Остальное думаю, не должно вызывать вопросов.

Как видно из примера, синтаксис довольно прост, но компактность оставляет желать лучшего. Для сравнения, эта цель могла бы быть реализована примерно следующим кодом на make (разумеется, это не тот код, который генерирует GYP):

BUILDTYPE   ?= Debug
PRODUCT_DIR ?= out/$(BUILDTYPE)

HTML_OUT  := $(PRODUCT_DIR)/Doc
RST_DOCS  := doc/Build.rst doc/Dictionary.rst doc/README.rst
HTML_DOCS := $(patsubst doc/%.rst,$(HTML_OUT)/%.html,$(RST_DOCS))

.PHONY: docs
docs: $(HTML_DOCS)

# Собственно, само правило
$(HTML_OUT)/%.html: doc/%.rst doc/css/code.css
        mkdir -p $(HTML_OUT)
        rst2html.py --stylesheet-path=../doc/css/code.css 
                    --embed-stylesheet $< $@

Видно, что синтаксис определения правил GYP несколько более многословен, чем синтаксис make, но возможно, и более читабелен для людей, не искушённых в GNU make.

Действия и преобразования обычно стараются реализовывать с помощью кросс-платформенных средств, как правило, python-скриптов.

Описание формата декларации действий и предопределённых переменных можно найти на официальной wiki (Actions, Rules).

Out Of Source сборки

Те из читателей, кто пробовал запускать GYP под Linux, заметили, что, помимо желаемых артефактов, аккуратно сложенных в каталоге out, GYP создаёт несколько make-файлов (по одному-два на каждую цель + один основной Makefile), которые засоряют каталоги с исходным кодом. Хотелось бы, чтобы эти промежуточные файлы тоже создавались в каталоге out. Пример решения этой проблемы можно найти в исходном коде V8. Достаточно задать опцию gyp --generator-output и запускать make из указанного опцией каталога:

gyp --depth=. --generator-output=./out gtest.gyp
make -C out

Заключение

GYP является достаточно удобной альтернативой CMake со своими преимуществами и недостатками. Я пробовал обе системы в реальном проекте, и лично мне GYP показался более простым, интуитивным и модульным инструментом, хотя в нём и не хватает некоторых полезных возможностей конкурента.

Несмотря на привлекательность и зрелость проекта, документации по нему достаточно мало. Большую часть практических знаний приходится извлекать из исходного кода Chromium и V8. Уже упомянутая wiki содержит достаточно подробную спецификацию формата GYP-файлов, но примеров использования, к сожалению, не хватает. Надеюсь, эта статья хоть немного изменит ситуацию в лучшую сторону.

Ресурсы

Автор: roman_kashitsyn

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js