Привет, Хабр! Предлагаю вашему вниманию перевод статьи "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):


Виртуальное окружение Имя рабочего процесса YAML
Windows Server 2019 windows-latest
Ubuntu 18.04 ubuntu-latest or ubuntu-18.04
Ubuntu 16.04 ubuntu-16.04
macOS Catalina 10.15 macos-latest

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


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

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


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


./configure
make
make check
make distcheck

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


Hello World


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


#include <iostream>

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
Windows Server 2019 3.16.0
Ubuntu 18.04 3.12.4
macOS Catalina 10.15 3.15.5

Это значит, что нужно будет ограничить минимальную версию 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.