[Из песочницы] Использование GitHub Actions с C++ и CMake

?v=1

Привет, Хабр! Предлагаю вашему вниманию перевод статьи «Using GitHub Actions with C++ and CMake» о сборке проекта на C++ с использованием GitHub Actions и CMake автора Кристиана Адама.


Использование GitHub Actions с C++ и CMake

В этом посте я хочу показать файл конфигурации GitHub Actions для проекта C++, использующего CMake.

GitHub Actions это предоставляемая GitHub инфраструктура CI/CD. Сейчас GitHub Actions предлагает следующие виртуальные машины (runners):

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


  • 2х ядерное CPU
  • 7 Гб оперативной памяти
  • 14 Гб на диске SSD

Каждое задание рабочего процесса может выполняться до 6 часов.

К сожалению, когда я включил GitHub Actions в проекте C++, мне предложили такой рабочий процесс:

./configure
make
make check
make distcheck

Это немного не то, что можно использовать с CMake.


Hello World

Я хочу собрать традиционное тестовое приложение C++:

#include 

int main()
{
  std::cout << "Hello world\n";
}

Со следующим проектом CMake:

cmake_minimum_required(VERSION 3.16)

project(main)

add_executable(main main.cpp)

install(TARGETS main)

enable_testing()
add_test(NAME main COMMAND main)

TL; DR смотрите проект на GitHub.


Матрица сборки

Я начал со следующей матрицы сборки:

name: CMake Build Matrix

on: [push]

jobs:
  build:
    name: ${ { matrix.config.name } }
    runs-on: ${ { matrix.config.os } }
    strategy:
      fail-fast: false
      matrix:
        config:
        - {
            name: "Windows Latest MSVC", artifact: "Windows-MSVC.tar.xz",
            os: windows-latest,
            build_type: "Release", cc: "cl", cxx: "cl",
            environment_script: "C:/Program Files (x86)/Microsoft Visual Studio/2019/Enterprise/VC/Auxiliary/Build/vcvars64.bat"
          }
        - {
            name: "Windows Latest MinGW", artifact: "Windows-MinGW.tar.xz",
            os: windows-latest,
            build_type: "Release", cc: "gcc", cxx: "g++"
          }
        - {
            name: "Ubuntu Latest GCC", artifact: "Linux.tar.xz",
            os: ubuntu-latest,
            build_type: "Release", cc: "gcc", cxx: "g++"
          }
        - {
            name: "macOS Latest Clang", artifact: "macOS.tar.xz",
            os: macos-latest,
            build_type: "Release", cc: "clang", cxx: "clang++"
          }


Свежие CMake и Ninja

На странице установленного ПО виртуальных машин мы видим, что CMake есть везде, но в разных версиях:

Это значит, что нужно будет ограничить минимальную версию CMake до 3.12 или обновить CMake.

CMake 3.16 поддерживает прекомпиляцию заголовков и Unity Builds, которые помогают сократить время сборки.

Поскольку у CMake и Ninja есть репозитории на GitHub, я решил скачать нужные релизы с GitHub.

Для написания скрипта я использовал CMake, потому что виртуальные машины по умолчанию используют свойственный им язык скриптов (bash для Linux и powershell для Windows). CMake умеет выполнять процессы, загружать файлы, извлекать архивы и делать еще много полезных вещей.

- name: Download Ninja and CMake
  id: cmake_and_ninja
  shell: cmake -P {0}
  run: |
    set(ninja_version "1.9.0")
    set(cmake_version "3.16.2")

    message(STATUS "Using host CMake version: ${CMAKE_VERSION}")

    if ("${ { runner.os } }" STREQUAL "Windows")
      set(ninja_suffix "win.zip")
      set(cmake_suffix "win64-x64.zip")
      set(cmake_dir "cmake-${cmake_version}-win64-x64/bin")
    elseif ("${ { runner.os } }" STREQUAL "Linux")
      set(ninja_suffix "linux.zip")
      set(cmake_suffix "Linux-x86_64.tar.gz")
      set(cmake_dir "cmake-${cmake_version}-Linux-x86_64/bin")
    elseif ("${ { runner.os } }" STREQUAL "macOS")
      set(ninja_suffix "mac.zip")
      set(cmake_suffix "Darwin-x86_64.tar.gz")
      set(cmake_dir "cmake-${cmake_version}-Darwin-x86_64/CMake.app/Contents/bin")
    endif()

    set(ninja_url "https://github.com/ninja-build/ninja/releases/download/v${ninja_version}/ninja-${ninja_suffix}")
    file(DOWNLOAD "${ninja_url}" ./ninja.zip SHOW_PROGRESS)
    execute_process(COMMAND ${CMAKE_COMMAND} -E tar xvf ./ninja.zip)

    set(cmake_url "https://github.com/Kitware/CMake/releases/download/v${cmake_version}/cmake-${cmake_version}-${cmake_suffix}")
    file(DOWNLOAD "${cmake_url}" ./cmake.zip SHOW_PROGRESS)
    execute_process(COMMAND ${CMAKE_COMMAND} -E tar xvf ./cmake.zip)

    # Save the path for other steps
    file(TO_CMAKE_PATH "$ENV{GITHUB_WORKSPACE}/${cmake_dir}" cmake_dir)
    message("::set-output name=cmake_dir::${cmake_dir}")

    if (NOT "${ { runner.os } }" STREQUAL "Windows")
      execute_process(
        COMMAND chmod +x ninja
        COMMAND chmod +x ${cmake_dir}/cmake
      )
    endif()


Шаг настройки

Теперь, когда у меня есть CMake и Ninja, все, что мне нужно сделать, это настроить проект таким образом:

- name: Configure
  shell: cmake -P {0}
  run: |
    set(ENV{CC} ${ { matrix.config.cc } })
    set(ENV{CXX} ${ { matrix.config.cxx } })

    if ("${ { runner.os } }" STREQUAL "Windows" AND NOT "x${ { matrix.config.environment_script } }" STREQUAL "x")
      execute_process(
        COMMAND "${ { matrix.config.environment_script } }" && set
        OUTPUT_FILE environment_script_output.txt
      )
      file(STRINGS environment_script_output.txt output_lines)
      foreach(line IN LISTS output_lines)
        if (line MATCHES "^([a-zA-Z0-9_-]+)=(.*)$")
          set(ENV{${CMAKE_MATCH_1} } "${CMAKE_MATCH_2}")
        endif()
      endforeach()
    endif()

    file(TO_CMAKE_PATH "$ENV{GITHUB_WORKSPACE}/ninja" ninja_program)

    execute_process(
      COMMAND ${ { steps.cmake_and_ninja.outputs.cmake_dir } }/cmake
        -S .
        -B build
        -D CMAKE_BUILD_TYPE=${ { matrix.config.build_type } }
        -G Ninja
        -D CMAKE_MAKE_PROGRAM=${ninja_program}
      RESULT_VARIABLE result
    )
    if (NOT result EQUAL 0)
      message(FATAL_ERROR "Bad exit status")
    endif()

Я установил переменные окружения CC и CXX, а для MSVC мне пришлось выполнить скрипт vcvars64.bat, получить все переменные окружения и установить их для выполняющегося скрипта CMake.


Шаг сборки

Шаг сборки включает в себя запуск CMake с параметром --build:

- name: Build
  shell: cmake -P {0}
  run: |
    set(ENV{NINJA_STATUS} "[%f/%t %o/sec] ")

    if ("${ { runner.os } }" STREQUAL "Windows" AND NOT "x${ { matrix.config.environment_script } }" STREQUAL "x")
      file(STRINGS environment_script_output.txt output_lines)
      foreach(line IN LISTS output_lines)
        if (line MATCHES "^([a-zA-Z0-9_-]+)=(.*)$")
          set(ENV{${CMAKE_MATCH_1} } "${CMAKE_MATCH_2}")
        endif()
      endforeach()
    endif()

    execute_process(
      COMMAND ${ { steps.cmake_and_ninja.outputs.cmake_dir } }/cmake --build build
      RESULT_VARIABLE result
    )
    if (NOT result EQUAL 0)
      message(FATAL_ERROR "Bad exit status")
    endif()

Что бы увидеть скорость компиляции на разном виртуальном окружении, я установил переменную NINJA_STATUS.

Для переменных MSVC я использовал скрипт environment_script_output.txt, полученный на шаге настройки.


Шаг запуска тестов

На этом шаге вызывается ctest с передачей числа ядер процессора через аргумент -j:

- name: Run tests
  shell: cmake -P {0}
  run: |
    include(ProcessorCount)
    ProcessorCount(N)

    execute_process(
      COMMAND ${ { steps.cmake_and_ninja.outputs.cmake_dir } }/ctest -j ${N}
      WORKING_DIRECTORY build
      RESULT_VARIABLE result
    )
    if (NOT result EQUAL 0)
      message(FATAL_ERROR "Running tests failed!")
    endif()


Шаги установки, упаковки и загрузки

Эти шаги включают запуск CMake с --install, последующий вызов CMake для создания архива tar.xz и загрузку архива как артефакта сборки.

- name: Install Strip
  run: ${ { steps.cmake_and_ninja.outputs.cmake_dir } }/cmake --install build --prefix instdir --strip

- name: Pack
  working-directory: instdir
  run: ${ { steps.cmake_and_ninja.outputs.cmake_dir } }/cmake -E tar cJfv ../${ { matrix.config.artifact } } .

- name: Upload
  uses: actions/upload-artifact@v1
  with:
    path: ./${ { matrix.config.artifact } }
    name: ${ { matrix.config.artifact } }

Я не стал использовать CMake в качестве языка сценариев для простых вызовов CMake с параметрами, оболочки по умолчанию прекрасно с этим справляются.


Обработка релизов

Когда вы помечаете релиз в git, вы также хотите, чтобы артефакты сборки прикрепились к релизу:

git tag -a v1.0.0 -m "Release v1.0.0"
git push origin v1.0.0

Ниже приведён код для этого, который сработает, если git refpath содержит tags/v:

release:
  if: contains(github.ref, 'tags/v')
  runs-on: ubuntu-latest
  needs: build

  steps:
  - name: Create Release
    id: create_release
    uses: actions/create-release@v1.0.0
    env:
      GITHUB_TOKEN: ${ { secrets.GITHUB_TOKEN } }
    with:
      tag_name: ${ { github.ref } }
      release_name: Release ${ { github.ref } }
      draft: false
      prerelease: false

  - name: Store Release url
    run: |
      echo "${ { steps.create_release.outputs.upload_url } }" > ./upload_url

  - uses: actions/upload-artifact@v1
    with:
      path: ./upload_url
      name: upload_url

publish:
  if: contains(github.ref, 'tags/v')
  name: ${ { matrix.config.name } }
  runs-on: ${ { matrix.config.os } }
  strategy:
    fail-fast: false
    matrix:
      config:
      - {
          name: "Windows Latest MSVC", artifact: "Windows-MSVC.tar.xz",
          os: ubuntu-latest
        }
      - {
          name: "Windows Latest MinGW", artifact: "Windows-MinGW.tar.xz",
          os: ubuntu-latest
        }
      - {
          name: "Ubuntu Latest GCC", artifact: "Linux.tar.xz",
          os: ubuntu-latest
        }
      - {
          name: "macOS Latest Clang", artifact: "macOS.tar.xz",
          os: ubuntu-latest
        }
  needs: release

  steps:
  - name: Download artifact
    uses: actions/download-artifact@v1
    with:
      name: ${ { matrix.config.artifact } }
      path: ./

  - name: Download URL
    uses: actions/download-artifact@v1
    with:
      name: upload_url
      path: ./
  - id: set_upload_url
    run: |
      upload_url=`cat ./upload_url`
      echo ::set-output name=upload_url::$upload_url

  - name: Upload to Release
    id: upload_to_release
    uses: actions/upload-release-asset@v1.0.1
    env:
      GITHUB_TOKEN: ${ { secrets.GITHUB_TOKEN } }
    with:
      upload_url: ${ { steps.set_upload_url.outputs.upload_url } }
      asset_path: ./${ { matrix.config.artifact } }
      asset_name: ${ { matrix.config.artifact } }
      asset_content_type: application/x-gtar

Это выглядит сложным, но это необходимо, так как actions/create-release можно вызвать однократно, иначе это действие закончится ошибкой. Это обсуждается в issue #14 и issue #27.

Несмотря на то, что вы можете использовать рабочий процесс до 6 часов, токен secrets.GITHUB_TOKEN действителен один час. Вы можете создать личный токен или загрузить артефакты в релиз вручную. Подробности в обсуждении сообщества GitHub.


Заключение

Включить GitHub Actions в вашем проекте на CMake становится проще, если создать файл .github/workflows/build_cmake.yml с содержимым из build_cmake.yml.

Вы можете посмотреть GitHub Actions в моем проекте Hello World GitHub.

Оригинальный текст опубликован под лицензией CC BY 4.0.

© Habrahabr.ru