Непрерывная интеграция (CI) для GitHub проектов на С/C++ с CMake-сборкой

  • Tutorial
Continuous Integration

Про непрерывную интеграцию и её целебные свойства слышали, наверное, все — не буду повторять написанное в многочисленных вводных статьях и обзорах и рассказывать, что же такое непрерывная интеграция, и как именно она упрощает жизнь разработчикам, релиз-инженерам и менеджерам. Предполагается, что читатель и сам прекрасно понимает, что CI — вне зависимости от применяемых в проекте языков программирования — это стопроцентно правильный подход, но… Но вот до практического его внедрения руки в силу разных причин пока не дошли.


Данная статья — инструкция по прикручиванию базовой непрерывной интеграции (build-test-deploy) к гитхабовским C/C++ проектам с CMake сборкой — по непонятным причинам, на Хабре такого до сих пор не проскальзывало. Впрочем, если моё гугл-фу подвело меня, и таки проскальзывало — не беда. Лишний туториал, описывающий всё под несколько другим углом и предостерегающий от неповторимого набора набитых автором шишек, совершенно точно не повредит.


Выбираем сервисы


Критериев выбора у меня было немного (как я наивно полагал).


  • Поддержка основных десктопных платформ:
    • Windows
    • Linux
    • Mac OS X
  • Облачный хостинг — чтобы не нужно было поднимать и настраивать свой сервер
  • Из-коробочная интеграция с GitHub
  • Бесплатность
  • Размер и активность пользовательской базы

Вопреки моим ожиданиям, сервиса, в одиночку покрывающего все эти требования, не нашлось. Странно. Ну да ничего страшного — следующей пары сервисов оказывается вполне достаточно:


1) Travis CI — для Linux и Mac OS X
2) AppVeyor — для Windows


Общим планом


И Travis CI, и AppVeyor позволяют запускать виртуальные машины с предустановленным инструментарием, на которых будет производиться процесс сборки, тестирования и развёртывания.


Docker-контейнеры в Travis

Строго говоря, Travis умеет работать как из полновесной виртуальной машины, так и из Docker-контейнера. В теории, это позволяет сократить время между git push и началом сборки — приблизительно на одну минуту. К сожалению, на практике за это ускорение придётся заплатить потерей возможности делать sudo, а это, в свою очередь, ведёт к ограничениям при установке нужных зависимостей. После непродолжительных плясок с бубном в попытке поставить в контейнерный Тревис 32-битные библиотеки для кросс-компиляции, я сказал себе: ускорение билда всего на одну минуту — и всего на одной из трёх платформ, т.к. Docker-контейнеры в Travis доступны только для Linux! — просто не стоит связанных с этим трудностей.


Как именно собирать, тестировать и развёртывать проект, описывается нами в специальном конфигурационном файле на языке YAML. Этот файл должен лежать в корне репозитория и иметь имя .travis.yml или appveyor.yml (допускается .appveyor.yml) — для Travis CI и AppVeyor соответственно. Мы хотим использовать оба сервиса одновременно, а значит и файлов будет два.


Предостережение для тех, кто, никогда не использовал YAML ранее. YAML это язык с синтаксическим структурированием с помощью отступов (как и, например, Питон), но при этом не разрешается использование табуляции. Поэтому придётся либо настроить ваш текстовый редактор на expand-tabs-to-spaces для .yml файлов, либо не использовать табы вообще и вбивать отступы пробелами.


После того, как YAML файлы добавлены в репозиторий, нужно будет включить непрерывную интеграцию для заданного проекта на сайтах Travis и AppVeyor. Делается это очень просто. Заходим на https://travis-ci.org под своим GitHub аккаунтом, соглашаемся с доступом, который запрашивает Travis CI (ему нужно будет получать уведомления о новых коммитах), синхронизуем список своих проектов, выбираем нужный и щёлкаем на включатель. Готово. Далее повторяем аналогичный процесс на сайте https://ci.appveyor.com


Начиная с этого момента каждый git push в ваш репозиторий будет запускать процесс непрерывной интеграции: сервисы Travis и AppVeyor поднимут виртуальную машину, настроят среду, установят зависимости, скачают ваш проект, соберут и протестируют его, а также, при желании, выложат инсталляторы, архивы с исходниками и документацию — всё согласно спецификации в YAML-файлах.


Собственно, в создании YAML-файлов и будет заключаться наша основная работа.


Структура .travis.yml


Рассмотрим пример файла .travis.yml с комментариями:


dist:     trusty    # используем Ubuntu 14.04 Trusty Tahr (а не 12.02 Precise Pangolin)
sudo:     required  # используем Virtual Machine (а не Docker container)

language: cpp       # на практике разницы между специфичным для C++ окружением
                    # и, скажем, python -- никакой. Но пусть будет cpp.

os:
    # будем прогонять CI и на Linux, и на Mac OS X...
    - linux
    - osx

compiler:
    # ... и с помощью GCC, и Clang
    - gcc
    - clang

env:
    # список переменных окружения, влияющих на матрицу сборки:
    - TARGET_CPU=amd64 BUILD_CONFIGURATION=Debug
    - TARGET_CPU=amd64 BUILD_CONFIGURATION=Release
    - TARGET_CPU=x86 BUILD_CONFIGURATION=Debug
    - TARGET_CPU=x86 BUILD_CONFIGURATION=Release

matrix:
    exclude:
        # вручную исключим ненужные элементы из матрицы сборки:
        - os:       osx
          compiler: gcc

        - os:  osx
          env: TARGET_CPU=x86 BUILD_CONFIGURATION=Debug

        - os:  osx
          env: TARGET_CPU=x86 BUILD_CONFIGURATION=Release

install:
    # скрипт настройки среды и установки зависимостей:
    - source ci/travis/install-$TRAVIS_OS_NAME.sh

script:
    # скрипт сборки и тестирования проекта:
    - mkdir build
    - cd build
    - cmake .. -DCMAKE_BUILD_TYPE=$BUILD_CONFIGURATION -DTARGET_CPU=$TARGET_CPU
    - cmake --build .
    - ctest --output-on-failure

deploy:
    # выкладываем tagged-коммиты на GitHub Releases:
    provider:     releases
    file:         <package-file>
    skip_cleanup: true
    overwrite:    true

    api_key:
        secure:   <encrypted-github-token>

    on:
        tags:     true

Скрипты этапов установки и развёртывания требуют пояснений — они будут даны далее. Сначала же остановимся на одном из ключевых понятий непрерывной интеграции — матрице сборки (build matrix).


Все мы знакомы с ситуацией, когда мы внесли минимальные изменения в проект, собрали, запустили, проверили — всё работает. Сделали релиз, и — БАМ! Он тут же упал на целевой машине. Проверяем у себя ещё раз — работает, собака! В английском языке есть специальный акроним на этот счёт:


IWOMM — It works on my machine!


Разница, приводящая к проблеме, может таиться где угодно — разные версии компилятора у разработчика и на сборочной машине, отличающаяся версия библиотеки на целевой, тестировали на одной операционке, а запускали на другой — да мало ли что ещё!


Способ борьбы с этим пакостным феноменом может быть только один — собирать и тестировать с использованием обширной матрицы сборки, то есть сразу на множестве возможных конфигураций. Это, вкупе с достаточным покрытием исходного кода автоматическими тестами, позволяет значительно снизить вероятность IWOMM.


C/C++ проект на Travis CI имеет смысл гонять, как минимум, под следующей матрицей:


  • OS
    • Linux
    • Mac OS X
  • Compiler
    • GCC
    • Clang
  • Target CPU
    • amd64
    • x86
  • Configuration
    • Debug
    • Release

К сожалению, Тревис предлагает на выбор всего два дистрибутива Linux — Ubuntu 12.04 Precise Pangolin (по умолчанию) и Ubuntu 14.04 Trusty Tahr. Даже если отвлечься от отсутствия каких-либо альтернатив Ubuntu, напомню, что текущая LTS (long-term-support) версия Ubuntu — это Ubuntu 16.04 Yakkety Yak, а текущая не-LTS версия — Ubuntu 17.04 Zesty Zapus. Иными словами, в мае 2017 года нам по умолчанию предлагается версия Ubuntu, выпущенная в 2012 году и отстающая на 5 мажорных релизов. Мда. Ну что ж, будем играть картами, что есть на руках.


Как бы то ни было, данная матрица содержит 16 конфигураций. Выбрасываем отсюда x86 под Mac OS X — целевая машина там однозначно будет 64-битная. Также выбрасываем GCC под Mac OS X — GCC под Маком это не более чем адаптер для GCC-специфичных опций командной строки, реальный же компилятор всё равно будет Clang. Исключение элементов из матрицы сборки осуществляется в секции


matrix:
    exclude:

Предостережение: если исключаемые элементы описываются с помощью переменных окружения (как в случае с TARGET_CPU), то придётся скопировать всю строку целиком:


env: TARGET_CPU=x86 BUILD_CONFIGURATION=Debug

Кажущегося логичным env: TARGET_CPU=x86 будет недостаточно.


Итого, остаётся 12 конфигураций на каждый git push. Согласитесь, это значительно лучше, чем одна-две, как обычно бывает при тестировании вручную, без использования непрерывной интеграции?


В терминологии Travis CI всё это звучит так. Каждый коммит (commit) приводит к запуску сборки (build), которая состоит из множества заданий (jobs) — по одному на каждую конфигурацию в матрице сборки (build matrix).


Структура .appveyor.yml


YAML-файл в AppVeyor выглядит очень похоже с поправкой на имена секций — всё тут называется немного по-другому:


image: Visual Studio 2015   # на этом VM-образе установлены все Visual Studio с 2008 по 2015

init:
    # секция инициализации, исполняется до клонирования проекта
    # скорее всего, строчка ниже необязательна (так должно быть по умолчанию),
    # вместе с тем, она присутствует в официальных примерах, так что пусть будет:
    - git config --global core.autocrlf input

clone_folder: c:\projects\my-prj # выбираем локальную директорию для клонирования
shallow_clone: true              # копируем только последний коммит, без истории (git clone --depth 1)

matrix:
    fast_finish: false           # не останавливаемся после возникновения ошибки в каком-то из заданий

platform:
    # будем гонять CI на amd64 и x86...
    - x64
    - x86

configuration:
    # ... и в дебажной, и в релизной конфигурациях ...
    - Debug
    - Release

environment:
    matrix:
        # ... на трёх студиях (2010, 2012, 2015)
        - TOOLCHAIN: msvc10
        - TOOLCHAIN: msvc12
        - TOOLCHAIN: msvc14

install:
    # скрипт настройки среды и установки зависимостей:
    - call ci\appveyor\set-env.bat %TOOLCHAIN% %PLATFORM%

    - appveyor DownloadFile <url> -FileName <local-file>
    - 7z e -y <local-file> -o<local-dir>
    - ...

build_script:
    # скрипт сборки проекта:
    - mkdir build
    - cd build
    - cmake .. %CMAKE_CONFIGURE_FLAGS%
    - cmake --build . %CMAKE_BUILD_FLAGS%

test_script:
    # скрипт тестирования проекта:
    - ctest -C %CONFIGURATION% --output-on-failure

artifacts:
    - path: <local-package-path>
      name: package_name

deploy:
    # выкладываем tagged-коммиты на GitHub Releases:
    description: '$(APPVEYOR_REPO_TAG_NAME)'
    provider: GitHub

    auth_token:
        secure: <encrypted-github-token>

    artifact: package_name
    force_update: true

    on:
        appveyor_repo_tag: true

Концепция матрицы сборки в полной мере применима и здесь. Как минимум, стоит прогонять CI для двух архитектур x86/amd64, обе под Debug/Release. Помимо этого, я бы рекомендовал также добавить в матрицу сборки и версию Visual Studio — вы ведь не знаете, какую именно студию использует наугад взятый клонер?


Для использования Visual Studio 2017 нужно подключить отдельный VM образ — на котором, увы, отсутствует большинство библиотек и инструментов, доступных на образе для Visual Studio 2015. Если вам необходимо-таки прогонять CI на VS 2017, добавьте в матрицу сборки:


- TOOLCHAIN: msvc15
  APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2017

Я же для себя решил ограничиться диапазоном VS 2010 — VS 2015. Возможно, со временем разработчики AppVeyor исправят ситуацию с VS 2017 в лучшую сторону.


  • Toolchain
    • Visual Studio 2010
    • Visual Studio 2012
    • Visual Studio 2015
  • Target CPU
    • amd64
    • x86
  • Configuration
    • Debug
    • Release

В сумме это даёт нам симпатичную матрицу на 12 конфигураций.


Обратите внимание, что в YAML файлах как для AppVeyor, так и для Travis скрипты install ссылаются на внешние shell-файлы (.sh/.bat) Возникает вопрос: нельзя ли обойтись одними YAML файлами, без вспомогательных скриптов? К сожалению, скорее всего, ответом будет "нет".


Вспомогательные скрипты


Проблема в том, что shell-команды, указанные в YAML файлах, выполняются построчно. Нужен if-then-else или for? Будьте добры уместить всё в одну строчку. Понятно, что в теории в одну строку можно утрамбовать всё что угодно. На практике же это применимо только к простейшим случаям. Всю нетривиальную многострочную логику остаётся выносить в отдельные shell-скрипты и вызывать из YAML уже их.


Посему, я для себя организовал всё следующим образом. В проекте имеется папка ci (continuous integration) с двумя подпапками travis и appveyor, в которых лежат shell-скрипты:


ci/
    travis/
        install-linux.sh
        install-osx.sh
        ...
    appveyor/
        install.bat
        set-env.bat
        ...

В YAML-файлах же используются только однострочные команды типа:


- source ci/travis/install.sh

Выделим три основных этапа непрерывной интеграции на сервисах Travis и AppVeyor и рассмотрим их более подробно.


  • Install — настройка виртуальной среды, установка инструментов для сборки, библиотек и других зависимостей
  • Build & Test — сборка и тестирование проекта (как правило, выполняются вместе)
  • Deploy — выкладывание собранных пакетов/документации в заданные локации (для примеров я буду использовать GitHub Releases)

Install


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


Travis со своей доисторической Юбунтой предлагает из коробки CMake 3.2 и GCC 4.8.4 — скорее всего, для современных проектов этого окажется недостаточно. Для получения GCC, способного переваривать C++14, в install-linux.sh пишем:


sudo add-apt-repository -y ppa:ubuntu-toolchain-r/test
sudo apt-get update -qq

sudo apt-get install -qq g++-5
sudo update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-5 90

Адекватную версию CMake придётся качать и устанавливать вручную. Добавляем в тот же install-linux.sh:


CMAKE_VERSION=3.3.2
CMAKE_VERSION_DIR=v3.3

CMAKE_OS=Linux-x86_64
CMAKE_TAR=cmake-$CMAKE_VERSION-$CMAKE_OS.tar.gz
CMAKE_URL=http://www.cmake.org/files/$CMAKE_VERSION_DIR/$CMAKE_TAR
CMAKE_DIR=$(pwd)/cmake-$CMAKE_VERSION

wget --quiet $CMAKE_URL
mkdir -p $CMAKE_DIR
tar --strip-components=1 -xzf $CMAKE_TAR -C $CMAKE_DIR
export PATH=$CMAKE_DIR/bin:$PATH

Для моих проектов CMake версии 3.3 хватает, а вот доступной по умолчанию 3.2 — нет (в 3.3 было сразу несколько критических исправлений). Если для ваших проектов нужно что-то посвежее — соответственным образом поменяйте CMAKE_VERSION и CMAKE_VERSION_DIR.


Для поддержки кросс-компиляции amd64->x86 необходимо также установить multilib:


if [ "$TARGET_CPU" == "x86" ]; then
    sudo dpkg --add-architecture i386
    sudo apt-get -qq update

    # устанавливаем 32-битные версии необходимых проекту библиотек
    sudo apt-get install -y liblua5.2-dev:i386
    sudo apt-get install -y libusb-1.0:i386
    # ...

    # g++-multilib ставим в самом конце, после i386-пакетов!
    sudo apt-get install -y g++-5-multilib
fi

И Clang, и CMake под Маком не такие древние, как под Ubuntu, поэтому install-osx.sh у меня состоит только из установки специфичных для проекта зависимостей. Осуществляется это с помощью homebrew. Например, чтобы поставить ragel, добавляем в install-osx.sh:


brew update
brew install ragel

А вот install-скрипт в AppVeyor будет выглядеть более устрашающе — как минимум, нужно правильно настроить CMake-генератор в соответствии с версией Visual Studio и целевой платформой.


Настройка CMake-генератора

Помещаем во вспомогательный скрипт ci/appveyor/set-env.bat что-то вроде:


@echo off

:loop

if "%1" == "" goto :finalize
if /i "%1" == "msvc10" goto :msvc10
if /i "%1" == "msvc12" goto :msvc12
if /i "%1" == "msvc14" goto :msvc14
if /i "%1" == "msvc15" goto :msvc15
if /i "%1" == "x86" goto :x86
if /i "%1" == "i386" goto :x86
if /i "%1" == "amd64" goto :amd64
if /i "%1" == "x86_64" goto :amd64
if /i "%1" == "x64" goto :amd64

echo Invalid argument: '%1'
exit -1

:msvc10
set TOOLCHAIN=msvc10
set CMAKE_GENERATOR=Visual Studio 10 2010
shift
goto :loop

:msvc12
set TOOLCHAIN=msvc12
set CMAKE_GENERATOR=Visual Studio 12 2013
shift
goto :loop

:msvc14
set TOOLCHAIN=msvc14
set CMAKE_GENERATOR=Visual Studio 14 2015
shift
goto :loop

:msvc15
set TOOLCHAIN=msvc15
set CMAKE_GENERATOR=Visual Studio 15 2017
shift
goto :loop

:x86
set TARGET_CPU=x86
set CMAKE_GENERATOR_SUFFIX=
set OPENSSL_DIR=C:\OpenSSL-Win32
shift
goto :loop

:amd64
set TARGET_CPU=amd64
set CMAKE_GENERATOR_SUFFIX= Win64
set OPENSSL_DIR=C:\OpenSSL-Win64
shift
goto :loop

:finalize

if "%TOOLCHAIN%" == "" goto :msvc14
if "%TARGET_CPU%" == "" goto :amd64
if "%CONFIGURATION%" == "" (set CONFIGURATION=Release)

set CMAKE_CONFIGURE_FLAGS=-G "%CMAKE_GENERATOR%%CMAKE_GENERATOR_SUFFIX%"

set CMAKE_BUILD_FLAGS= ^
    --config %CONFIGURATION% ^
    -- ^
    /nologo ^
    /verbosity:minimal ^
    /consoleloggerparameters:Summary

Затем вызываем этот скрипт из .appveyor.yml:


install:
    - call ci\appveyor\set-env.bat %TOOLCHAIN% %PLATFORM%

Если требуется скачать и установить специфичные для платформы зависимости, это делается с помощью утилиты appveyor, доступной на всех виртуальных машинах сервиса AppVeyor. Например, чтобы установить ragel, необходимо добавить в ci/appveyor/install.bat:


set DOWNLOAD_DIR=c:\downloads
mkdir %DOWNLOAD_DIR%

set RAGEL_DOWNLOAD_URL=http://downloads.yorickpeterse.com/files/ragel-68-visualstudio2012.7z
mkdir %DOWNLOAD_DIR%\ragel
appveyor DownloadFile %RAGEL_DOWNLOAD_URL% -FileName %DOWNLOAD_DIR%\ragel\ragel.7z
7z e -y %DOWNLOAD_DIR%\ragel\ragel.7z -o%DOWNLOAD_DIR%\ragel
set PATH=%DOWNLOAD_DIR%\ragel;%PATH%

Также я рекомендую удалить следующие файлы — они не нужны для C++ проектов, но приводят к большому количеству мусорных сообщений в логах сборки:


del "c:\Program Files (x86)\MSBuild\14.0\Microsoft.Common.targets\ImportAfter\Xamarin.Common.targets"
del "c:\Program Files (x86)\MSBuild\4.0\Microsoft.Common.targets\ImportAfter\Xamarin.Common.targets"

Build & Test


Этот этап значительно проще предыдущего — для него, скорее всего, даже не потребуется выделенного shell-скрипта. Единственная, пожалуй, хитрость состоит в настройке кросс-компиляции под Linux на Travis CI. Для поддержки кросс-компиляции amd64->x86 добавьте в корневой CMakeLists.txt следующее:


if ("${TARGET_CPU}" STREQUAL "amd64")
    set (CMAKE_SIZEOF_VOID_P 8)

    set_property (GLOBAL PROPERTY FIND_LIBRARY_USE_LIB64_PATHS TRUE)
    set_property (GLOBAL PROPERTY FIND_LIBRARY_USE_LIB32_PATHS FALSE)
elseif ("${TARGET_CPU}" STREQUAL "x86")
    set (CMAKE_SIZEOF_VOID_P 4)

    set_property (GLOBAL PROPERTY FIND_LIBRARY_USE_LIB64_PATHS FALSE)
    set_property (GLOBAL PROPERTY FIND_LIBRARY_USE_LIB32_PATHS TRUE)

    if (GCC)
        set (CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -m32")
        set (CMAKE_C_FLAGS   "${CMAKE_C_FLAGS} -m32")
    endif ()
else ()
    message (FATAL_ERROR "Unsupported CPU: ${TARGET_CPU}")
endif ()

Далее действуем, как обычно. Сборка CMake-проектов осуществляется в выделенной папке build в два этапа: Configure и Build. Внутри travis.yml это будет выглядеть так:


script:
    - mkdir build
    - cd build
    - cmake .. -DCMAKE_BUILD_TYPE=$BUILD_CONFIGURATION -DTARGET_CPU=$TARGET_CPU
    - cmake --build .

Тестируем проект с использованием CTest сразу по окончанию сборки — конечно, данный шаг имеет смысл только в том случае, если ваш проект содержит тесты, добавленные с помощью CMake-команд enable_testing () и add_test (...):


    - ctest --output-on-failure

Я рекомендую запускать ctest с флажком --output-on-failure — в этом случае при провале теста в логе сборки будет виден выхлоп тестовой программы.


Под AppVeyor всё аналогично, со следующими незначительными отличиями. Во-первых, кросс-компиляция осуществляется не с помощью флажков C++ компилятора, а с помощью выбора нужного CMake-генератора (мы обрабатываем это в set-env.bat). Во вторых, Visual Studio генераторы, в отличие от Unix Makefiles, являются т.н. multi-configuration-генераторами, а это значит, что конфигурация Debug/Release указывается не на этапе CMake-configure, а на этапе Build. Впрочем, всё по-прежнему укладывается в те же лаконичные 4 строчки:


build_script:
    - mkdir build
    - cd build
    - cmake .. %CMAKE_CONFIGURE_FLAGS%     # содержит нужный CMake-генератор
    - cmake --build . %CMAKE_BUILD_FLAGS%  # задаёт конфигурацию Debug/Release
                                           # и дополнительные параметры для msbuild.exe

Для тестирования добавляем секцию test_script:


test_script:
    - ctest -C %CONFIGURATION% --output-on-failure

Обратите внимание, что нужно явно указать конфигурацию Debug/Release с помощью ключа -C.


Ура! Проект собирается и проходит тестирование под 24-мя конфигурациями на каждый git-push. Скорее бежим добавлять статус-бэйджики в README-файл!


Заходим на страничку проекта на сайте Travis CI по адресу:


https://travis-ci.org/<user-name>/<project-name>

Кликаем на бэйджик build|passing на страничке логов сборки — откроется окошко с линком на динамическое изображение. Копируем линк и вставляем его в README.rst:


.. image:: https://travis-ci.org/<user-name>/<project-name>.svg?branch=master
    :target: https://travis-ci.org/<user-name>/<project-name>

Мне послышалось, или кто-то сказал README.md? Ни-ког-да! reStructuredText FTW!


Аналогичным образом поступаем с бэйджиком AppVeyor. Линки на бэйджики доступны по адресу:


https://ci.appveyor.com/project/<user-name>/<project-name>/settings/badges

Копируем нужную строчку и вставляем рядышком:


.. image:: https://ci.appveyor.com/api/projects/status/<gibberish-code>?svg=true
    :target: https://ci.appveyor.com/project/<user-name>/<project-name>

Делаем коммит для обновлённого README.rst и наслаждаемся результатом:


image


Deploy


Покажем, как задействовать механизмы развёртывания на примере GitHub Releases.


Прежде всего, необходимо авторизовать сервисы Travis и AppVeyor, чтобы они могли создавать релизы в ваших GitHub проектах. Логинимся на GitHub, заходим в Profile->Settings->Developer settings->Personal access tokens. Кликаем на Generate new token, называем токен, например, CI Deploy Token и выставляем единственный флажок public_repo (разрешить носителю токена доступ к публичным репозиториям). Копируем сгенерированный токен в какой-нибудь временную локацию (вскоре его можно будет удалить).


С использованием этого токена можно как создавать релизы, так и пушить во все ваши публичные репозитории. Капитан Очевидность намекает, что выкладывать такой токен во всеобщий доступ в открытом виде нежелательно. Надо зашифровать его так, чтобы ключ для расшифровки был только у сервисов Travis CI и AppVeyor.


Чтобы зашифровать токен для Travis, необходимо установить Ruby-утилиту travis:


gem install travis

После этого запускаете в папке проекта:


travis encrypt <github-token>

Утилита зашифрует токен с использованием асимметричного шифрования, так что приватный ключ для расшифровки — созданный в момент подключения проекта на сервисе Travis CI — будет иметься только у самого Travis CI. Выход утилиты можно смело вставлять в .travis.yml.


Аналогичным образом токен асимметрично шифруется и для AppVeyor. Тут, однако, устанавливать никакие вспомогательные утилиты не надо — всё можно сделать прямо с сайта https://ci.appveyor.com через Account->Tools->Encrypt Data.


Далее, надо определиться, какие именно файлы будут выкладываться для всеобщего доступа. В терминах GitHub Releases такие файлы называются артефактами релиза (release artifacts). Это могут быть инсталляторы, архивы или же просто исполняемые файлы. Наиболее логичным подходом к генерации архивов или инсталляторов для проекта с CMake-сборкой выглядит использование CPack, но, очевидно, что и набор релиз-артефактов, и способ их генерации может разниться от проекта к проекту.


Так или иначе, допустим, в процессе сборки у нас образовались выходные файлы file1.pkg, file2.pkg, и т.д. и мы хотим прикрепить их в качестве release artifcats к релизам нашего GitHub проекта.


В .travis.yml создаём секцию deploy:


deploy:
    provider: releases # выкладываем на GitHub Releases

    api_key:
        secure: encrypted-github-token # токен, зашифрованный с помощью `travis encrypt`

    file:
        # список артефактов
        - file1.pkg
        - file2.pkg
        - ...

    skip_cleanup: true # не удалять сгенерированные во время сборки файлы перед развёртыванием

    on:
        tags: true     # выполнять deploy только для tagged-commit

В .appveyor.yml аналогичная функциональность достигается так:


artifacts:
    # файлы-артефакты нужно предварительно явно указать в данной секции
    # в терминологии AppVeyor это называется "push artifacts"
    - path: file1.pkg
    - path: file2.pkg
    - ...

deploy:
    provider: GitHub                         # выкладываем на GitHub Releases
    description: '$(APPVEYOR_REPO_TAG_NAME)' # tag как имя релиза (на Travis так по умолчанию)

    auth_token:
        secure: <secret-github-token> # токен, зашифрованный на сайте appveyor.ci

    artifact: /.*\.pkg/ # регулярное выражение, описывающее все артефакты данного релиза
    force_update: true  # перезаписывать артефакты, если файл с таким именем уже имеется

    on:
        appveyor_repo_tag: true # выполнять deploy только для tagged-commit

После этого создаём tagged-commit для нового релиза и пушим его:


git add --all
git commit --message "release 1.0.0"
git tag project-name-1.0.0
git push
git push origin project-name-1.0.0

Если сборка, тестирование и развёртывание прошло удачно, то на страничке проекта на GitHub вы должны будете увидеть новый релиз с прикреплёнными артефактами:


image


Заключение


В данной статье рассмотрен процесс подключения базовой непрерывной интеграции к C++/CMake проектам на GitHub. Разумеется, в рамках короткого туториала невозможно охватить всего множества нюансов и деталей, поэтому считаю своим долгом направить заинтересованных читателей к подробной документации сервисов Travis CI & AppVeyor:


https://docs.travis-ci.com
https://www.appveyor.com/docs


Также привожу ссылки на собственные проекты, которые демонстрируют всё вышеописанное на практике, и на которых, собственно, и был получен опыт настройки непрерывной интеграции:


https://github.com/vovkos/jancy
https://github.com/vovkos/doxyrest
https://github.com/vovkos/llvm-package-travis
https://github.com/vovkos/llvm-package-windows


Буду рад ответить на любые вопросы в комментариях.

Share post

Comments 11

    0
    Вроде как докер растет в сторону и Windows, даже офф софтина появилась (правда внутри себя работает через прослойку, но смотрится достаточно удобно и незаметно) https://www.docker.com/products/docker-toolbox
      0

      Тут дело даже не в том, запускается или нет Docker на Windows или Mac OS X (как вы верно отметили, да, запускается). Проблема в том, что Travis CI предоставляет Docker-контейнер для сборки только под Linux, а AppVeyor вообще не даёт возможности выбирать между полновесной виртуальной машиной и Docker-контейнером.


      Но это, на мой взгляд, и не важно. Выигрыш всего в минуту (!) в таком длительном, да и, чего уж там, совсем не критичном по времени исполнения процессе, как непрерывная интеграция, просто не стоит того, чтоб с ним заморачиваться.

        0
        Travis Ci это самый медленный сервис среди Gitlab Ci, Semaphore Ci, Circle Ci.
        Если у вас всего несколько билдов, то это не большая проблема. А если их много, лучше смотреть на что-то другое чем Travis Ci.
          0

          Поправьте меня, если ошибаюсь, но беглый осмотр выявил несоответствие списку требований, сформулированных в начале:


          • GitLab CI бесплатен только если проект хостится на GitLab
          • Semaphore CI не поддерживает Mac OS X
          • Circle CI не бесплатен для Mac OS X

          На самом деле, при выборе сервисов я прежде всего просканировал, что в основном используют opensource разработчики, хостящиеся на GitHub — с большим отрывом лидирует Travis и AppVeyor.


          В принципе, производительность Travis меня вполне устраивает — как мне кажется, CI далеко не является критической по времени задачей. А вот что вполне могло бы заставить меня мигрировать на другой сервис, так это наличие более широкого спектра платформ для сборки/тестирования: Ubuntu посвежее, Arch, Fedora, и т.д.


          Не подскажете, есть ли такие?

            +2
            * Отвечайте людям кликнув на «Ответ», иначе вы не создаете ветку общения с пользователем, не удобно читать и не понятно кому будет адресован ваш ответ. Спасибо
              +2

              Ваша правда, мой косяк.


              Акелла промахнулся — дважды! ;) На хабре окошко с редактором находится сразу за последним комментарием, отчего складывается впечатление, что отвечаешь на него. А перенести комментарий после создания уже нельзя.

              +1
              Gitlab CI беспланет на любом gitlab хостинге или собстенном сервере с gitlab. Деньги начинаются если вы сами хостите Gitlab большей ревизии чем CE.

              Все эти CI сервисы, включая Travis Ci поддерживают docker. Т.е. если вам надо, вы можете запускать любой контейнер, который найдете на docker hub, или дополнительно с gitlab хостинга, если вы используете gitlab ci.
              И выполнять в нем все что вам угодно.

              Я использую все эти CI за исключением appveyor. Репозиторий миррорится с gitlab.com на github.com

              CI с gitlab.com самый быстрый, но не стабильный. Часто он тормозит или вообще не работает.
              На остальных сервисах иногда возникают tmeout'ы.

              MacOS поддерживается только Travis и если использовать свой раннер Gitlab CI (т.е. надо свой хост с macos). Платные варианты не рассматриваю.
                0
                Репозиторий миррорится с gitlab.com на github.com — а это интересно, можно посмотреть на пример? Вы пулл/мерж-реквесты принимаете там и там?
                  0
                  В данном случае я делаю push во все копии скриптом, т.к. их больше 2.
                  Но для просто копирования, в gitlab есть система копирования. Она кажется раз в сутки подтягивает данные.

                  Пулл реквесты предпочитаю принимать в gitlab, но можно и там и там. Т.к. синхронизация идет с локального компьютера.

                  Пример скрипта? Добавлены remot'ы на все репозитории.
                  Далее при запуске скрипта ему передается название бранча или на пример --tags.
                  И скрипт выполняет последовательно команды для всех репозиториев-клонов: git push REMOTENAME $1

                  Т.о. образом можно одной командой пушить одну ветку или теги. ./script master или ./script --tags

                  Можно было не использовать скрипт, но прописать все клоны в origin репозитория одновренно, но тогда могут быть некоторые проблемы. Если какая-то из копий выйдет из строя, то push может остановиться на пол пути. Также проблема будет с pull из этих веток.
                    0

                    Я имел в виду не пример скрипта, а пример двух зеркалируемых репозиториев с реквестами в каждом. Впрочем, настройки не были бы видны стороннему посетителю.


                    А не бывает случаев, когда master случайно поменяли и на гитхабе, и на гитлабе, например там и там приняли по одному реквесту? Или вы принципиально мержите всё на своей машине и синхронизируете с помощью того самого скрипта?


                    (только сейчас увидел ваш ответный комментарий)

                      +1
                      Да я предпочитаю делать все в локально или брать за основу master из gitlab репозитория.
                      Пушить в master может не очень много людей, и все только в gitlab репозиторий.

                      Github pull requests не могут нормально объединяться (merge) в любом случае. они портят визуально историю.

                      Т.е. сначала я делаю git pull --rebase для gitlab репозитория, потом скрипт делает push во все репозитории. Если надо, можно вручную можно смержить pull request из github.

            Only users with full accounts can post comments. Log in, please.