Привет, Хабр! Предлагаю вашему вниманию перевод статьи "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.
gecube
Спасибо! Выглядит непривычно, но возможно, что это подстегнет гитлаб быстрее внедрять нужные и полезные возможности в свои пайплайны. Из этой статьи я узнал, что гитхаб экшенс — это не так уж и страшно, и то, что в github есть матрицы — это очень важно, т.к. в гитлабе до сих пор если нужно делать несколько сборок под разные платформы, то без копипасты не обойтись.
это, конечно, провал, т.к. логично, что токен должен действовать на всю длительность билд процесса.
dead_moros Автор
Да, возможности потрясающие, особенно если учесть, что это бесплатно. Более того, даже в приватных репозиториях можно использовать GitHub Action, правда с ограничением в 2000 минут в месяц.
klirichek
А что тут про гитлаб то?
gecube
Не понимаю, что Вам не нравится? Или Вы не считаете, то гитхаб и гитлаб конкуренты? Вот наличие 2000 мин в гитхабе — это именно ответка на бесплатные 2000 мин в гитлабе. Потому что последний в какой-то момент перетянул много клиентов с гитхаба. Т.е. у них в каком-то смысле соревнование, конкуренция за клиента.
Или Вы намекаете на анекдот про блох?
klirichek
Не намекаю. Просто подумал, что Вы опечатались.
Статья вся про гитхаб.
Вы единственным словом написали "гитлаб", и дальше тоже только гитхаб.
Потому и возник вопрос.
Конкурировать гитлабу с майкрософтом — наверное, сложновато )
gecube
Нет, не не опечатался. И учитывая, что Майкрософт — корпорация зла, я бы не доверил им свои наработки. Я не говорю, что гитлаб белые и пушистые. Отправлено и в свое время "прославились" скандалом с удалением базы данных. Но это майкрософту не сладко, а не гитлабу ) т.к. рынок инструментальных средств высококонкурентен. И тому же гитлабу не надо распыляться на какие-то сторонние проекты (у мс там много чего в портфеле — и винда, и офис, и вижуал студия, и облако, и многое другое — и не все прибыльно, не все прям огненно для пользователя)