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

" ; }

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()

Configure step

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.