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