Using GitHub Actions with C++ and CMake
I bring to your attention the translation of the article “Using GitHub Actions with C ++ and CMake” by Cristian Adam, written about three years ago. During this time, GitHub Actions has seen many improvements, and some of the tricks in the article may seem like bicycle building. However, it remains a good introductory overview.
In this post, I want to show the GitHub Actions configuration file for a C++ project using CMake.
GitHub Actions this is the CI/CD infrastructure provided by GitHub. GitHub Actions currently offers the following virtual machines (runners):
Virtual environment | YAML workflow name |
---|---|
Windows Server 2022 | windows-latest |
Ubuntu 20.04 | ubuntu-latest or ubuntu-20.04 |
Ubuntu 18.04 | ubuntu-18.04 |
macOS Catalina 10.15 | macos-latest |
Each virtual machine has the same available hardware resources:
Each workflow job can run up to 6 hours.
Unfortunately, when I enabled GitHub Actions in a C++ project, I was presented with this workflow:
./configure
make
make check
make distcheck
It’s a little different than what can be used with CMake.
hello world
I want to build a traditional C++ test application:
#include <iostream>
int main()
{
std::cout << "Hello world\n";
}
With the following CMake project:
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 see project at GitHub.
Build Matrix
I started with the following 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++"
}
Fresh CMake and Ninja
On the page installed software virtual machines, we see that CMake is everywhere, but in different versions:
Virtual environment | CMake version |
---|---|
Windows Server 2019 | 3.16.0 |
Ubuntu 18.04 | 3.12.4 |
macOS Catalina 10.15 | 3.15.5 |
This means that you will need to limit the minimum version of CMake to 3.12 or update CMake.
CMake 3.16 supports header precompilation and Unity Buildswhich help reduce build time.
Since CMake and Ninja have repositories on GitHub, I decided to download the required releases from GitHub.
I used CMake to write the script because VMs use by default peculiar to them scripting language (bash for Linux and powershell for Windows). CMake can execute processes, upload files, extract archives, and do many other useful things.
- 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()
Tuning step
Now that I have CMake and Ninja, all I have to do is set up the project like this:
- 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()
I have set environment variables CC
and CXX
and for MSVC I had to execute a script vcvars64.bat
get all environment variables and set them for the running CMake script.
Build step
The build step involves running CMake with the parameter --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()
To see the compilation speed on different virtual environments, I set the variable NINJA_STATUS
.
For MSVC variables I used a script environment_script_output.txt
obtained in the configuration step.
Test run step
This step calls ctest
with passing the number of processor cores through the argument -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()
Installation, Packing and Downloading Steps
These steps include running CMake with --install
subsequent call to CMake to create the archive tar.xz
and loading the archive as a build artifact.
- 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 } }
I didn’t use CMake as a scripting language for simple parameter calls to CMake, the default shells handle this just fine.
Release processing
When you tag a release in git, you also want the build artifacts to be attached to the release:
git tag -a v1.0.0 -m "Release v1.0.0"
git push origin v1.0.0
Below is the code for this, which will work if the git refpath contains 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
It looks complicated, but it’s necessary because actions/create-release
can be called once, otherwise this action will end with an error. This is discussed in issue #14 and issue #27.
Although you can use the workflow up to 6 hours, the token secrets.GITHUB_TOKEN
valid one hour. You can create a personal token or manually upload artifacts to a release. Details in discussion GitHub communities.
Conclusion
Enabling GitHub Actions in your CMake project becomes easier if you create a file .github/workflows/build_cmake.yml
with content from build_cmake.yml.
You can take a look at the GitHub Actions in Christian Adam’s project hello world github.