In this post I am going to provide a GitHub Actions configuration yaml file for C++ projects using CMake.
GitHub Actions is a CI/CD infrastructure provided by GitHub. GitHub Actions
currently offers the following virtual machines (runners) :
Virtual environment
YAML workflow label
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
Each virtual machine has the same hardware resources available:
2-core CPU
7 GB of RAM memory
14 GB of SSD disk space
Each job in a workflow can run for up to 6 hours of execution time.
Unfortunately when I enabled GitHub Actions on a C++ project I was presented with this workflow:
./configure
make
make check
make distcheck
This is not something you can use with CMake though
Hello World
I am going to build the following C++ hello world program:
#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 the project on GitHub .
Build Matrix
I have 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++"
}
Latest CMake and Ninja
In the software installed on the runners page we
can see that CMake is installed on all runners, but with 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 would mean that one would have to limit the minimum CMake version to 3.12, or upgrade CMake.
CMake 3.16 comes with support for Precompile Headers
and Unity Builds , which help reducing build times.
Since CMake and Ninja have GitHub Releases, I decided to download those GitHub releases.
I used CMake as a scripting language, since the default scripting language for runners is different (bash, and powershell).
CMake can execute processes, download files, extract archives.
- 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()
Now that I have CMake and Ninja, all I have to do is configure 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 the CC
and CXX
environment variables, and for MSVC, I had to run the vcvars64.bat
script,
get all the environment variables, and set them for the CMake running script.
Build step
The build step involves running the CMake with --build
parameter:
- 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()
I set the NINJA_STATUS
variable, to see how fast the compilation is in the respective runners.
For MSVC I reused the environment_script_output.txt
script from the Configure step.
Run tests step
This step calls ctest
with number of cores passed as -j
argument:
- 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()
Install, pack, upload steps
This steps involve running CMake with --install
, then creating a tar.xz
archive with CMake, and
uploading it 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 scripting language, since this just involves calling CMake with parameters, and the
default shells can handle this
Handling Releases
When you tag a release in git, you would also want the build artifacts promoted as releases:
git tag -a v1.0.0 -m "Release v1.0.0"
git push origin v1.0.0
The code to do this is below, gets triggered 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
This looks complicated, but it’s needed since actions/create-release
needs to be called only once, otherwise it will
fail. See issue #14 , issue #27 for
more information.
Even though you can use a workflow for 6 hours, the secrets.GITHUB_TOKEN
expires in one hour . You can either create a personal token, or
upload the artifacts manually to the release. See this GitHub community
thread for more information.
Closing
Enabling GitHub Actions on your CMake project is as easy at creating a .github/workflows/build_cmake.yml
file with the content from
build_cmake.yml .
You can see the GitHub Actions at my Hello World GitHub project.