Предлагаю вашему вниманию перевод статьи "Using GitHub Actions with C++ and CMake" от Cristian Adam, написанной около трех лет назад. За это время в GitHub Actions появилось много улучшений и некоторые приемы в статье могут показаться велосипедостроением. Тем не менее, это остается хорошим вводным обзором.


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

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

Виртуальное окружение

Имя рабочего процесса YAML

Windows Server 2022

windows-latest

Ubuntu 20.04

ubuntu-latest или ubuntu-20.04

Ubuntu 18.04

ubuntu-18.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.

Build Matrix

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

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.

Комментарии (6)


  1. IvaYan
    15.08.2022 10:46

    Из всей статьи я так и не понял, зачем тут Ninja. При том что мы уже используем CMake. Ну и зачем переводить пост трёхлетней давности, если даже переводчик признает что с тех пор что-то могло поменяться. Кстати, что именно?


    1. RussianWarShip Автор
      15.08.2022 11:15

      Поменялись версии операционных систем, доступных в GitHub Actions. Версия Cmake теперь минимум 23, а не 16. В маркете появилось много удобных дополнений, в частности для установки Qt.

      Cmake сам не занимается сборкой, по умолчанию это делает make или nmake. С Ninja Кристиан получил одну и ту же сборочную систему на трех разных операционных системах.


      1. IvaYan
        15.08.2022 11:27

        Cmake сам не занимается сборкой, по умолчанию это делает make или nmake. С Ninja Кристиан получил одну и ту же сборочную систему на трех разных операционных системах.

        А как же cmake --build? Команда вполне абстрагирует сборочную систему на разных ОС.


    1. Playa
      15.08.2022 22:26

      Ninja - это быстро ¯\_(ツ)_/¯


  1. rat1
    15.08.2022 11:23
    +1

    Статья очень старая. На github уже давно есть шаблоны для cmake и сам cmake.


    1. RussianWarShip Автор
      16.08.2022 15:10

      Статья не столько о наличии Cmake, сколько о возможных способах получить больше, чем дает GitHub Actions по умолчанию. И я надеялся, что будет дискуссия – оставлять ли портянки из Cmake в .yml или лучше вынести их в отдельные Actions.