Host docker images for cross-compiler CI test

Bases of suggestion

There is a common desire of testing library across multiple compiler versions.
e.g. Enable latest versions of compilers on main · Issue #73 · bemanproject/exemplar · GitHub / attempt in option26

This is needed but sometimes prohibitive due to setup complexity (there’s numerous matrixs like this GitHub - egor-tensin/setup-gcc: GitHub action to set up GCC that shows what compiler version is easily available on each platform). Sometimes apt-install takes care of the job, sometimes you need a specific ppa, sometimes you have to install via the release page, sometimes you have to build it.

Install scripts often needs to source the build from multiple places (GitHub, PPA) that creates uncertainty to job execution time and increases unnecessary fault.

GitHub Actions community does not have a well maintained setup helper for compilers that minimizes setup time.

Attempts as listed:

Suggestion

It might be beneficial for us to have a pre-built docker image hosted on GitHub to ensure common compiler-test environment. We should have its script hosted in the new infrastructure repo.

Ideally we should be able to test across compiler versions by just setting the CI job image to e.g. beman/cpp-image:gnu-14 provides gcc-14 compiler. Cross-compiler test should be as easy as “run on this image (e.g. llvm-17), configure and test”.

Non-goal

I don’t think we should move all CI jobs to our specific images. Running linters and basic testing should still be in the default GitHub provided environment. The default images are fast to setup, well-maintained, feature-rich, and provides good cross-platform support.

I don’t think this image should be used for personal development use, e.g. putting in developer tools such as language servers, vscode, fancy terminals. It does not attempt to resolve the “it works on my machine” from the developer side. Though it definitely provides the infrastructure for a better devcontainer image.

Example of this simplifying CI jobs

Before:

  compiler-test:
    runs-on: ubuntu-24.04
    strategy:
      matrix:
        compilers:
          - class: GNU
            version: 14
          - class: GNU
            version: 13
          - class: LLVM
            version: 17
    name: "Compiler: ${{ matrix.compilers.class }} ${{ matrix.compilers.version }}"
    steps:
      - uses: actions/checkout@v4
      - name: Setup build environment
        uses: lukka/get-cmake@latest
        with:
          cmakeVersion: "~3.25.0"
          ninjaVersion: "^1.11.1"
      - name: Install Compiler
        id: install-compiler
        run: |
          if [ "${{ matrix.compilers.class }}" = "GNU" ]; then
            CC=gcc-${{ matrix.compilers.version }}
            CXX=g++-${{ matrix.compilers.version }}
            sudo add-apt-repository universe
            sudo apt-get update
            sudo apt-get install -y $CC
            sudo apt-get install -y $CXX
            $CC --version
            $CXX --version
          else
            wget https://apt.llvm.org/llvm.sh
            chmod +x llvm.sh
            sudo bash llvm.sh ${{ matrix.compilers.version }}
            CC=clang-${{ matrix.compilers.version }}
            CXX=clang++-${{ matrix.compilers.version }}
            $CC --version
            $CXX --version
          fi
          echo "CC=$CC" >> "$GITHUB_OUTPUT"
          echo "CXX=$CXX" >> "$GITHUB_OUTPUT"
      - name: Configure CMake
        run: |
          cmake -B build -S . -DCMAKE_CXX_STANDARD=20
        env:
          CC: ${{ steps.install-compiler.outputs.CC }}
          CXX: ${{ steps.install-compiler.outputs.CXX }}
          CMAKE_GENERATOR: "Ninja Multi-Config"
      - name: Build Debug
        run: |
          cmake --build build --config Debug --verbose
          cmake --build build --config Debug --target all_verify_interface_header_sets
          cmake --install build --config Debug --prefix /opt/beman.exemplar
          find /opt/beman.exemplar -type f
      - name: Test Debug
        run: ctest --test-dir build --build-config Debug

After:

  compiler-test:
    runs-on: ubuntu-24.04
    strategy:
      matrix:
        image: ["gnu-14", "gnu-13", "llvm-17", "llvm-head"]
    container:
      image: ghcr.io/beman/cpp-docker:${{ matrix.image }}
    name: "Compiler: ${{ matrix.image }}"
    steps:
      - uses: actions/checkout@v4
      - name: Configure CMake
        run: |
          cmake -B build -S . -DCMAKE_CXX_STANDARD=20
        env:
          # consistent symlink at image build time from compiler (e.g. gcc-14) to 
          # common location e.g. /beman/compilers
          CC: "/beman/compilers/c" 
          CXX: "/beman/compilers/cc"
          CMAKE_GENERATOR: "Ninja Multi-Config"
      - name: Build Debug
        run: |
          cmake --build build --config Debug --verbose
          cmake --build build --config Debug --target all_verify_interface_header_sets
          cmake --install build --config Debug --prefix /opt/beman.exemplar
          find /opt/beman.exemplar -type f
      - name: Test Debug
        run: ctest --test-dir build --build-config Debug

Furtuer benefit

This would enable testing on HEAD of compilers/ branch (thus a PR) of a compiler that would be prohibitive right now due to the need to build the compiler at setup.

This would also enable us to switch to older versions of compilers that are hard to setup on newer images.

When we have the infrastructure up, we will be able to setup a more feature-rich devcontainer environment that contains all the compilers as requested by @neatudarius . Current devcontainer builds the container at setup, making this prohibitive.

Downside

We need to maintain setup script (1) and docker images (2), and have central repo (3) to host these.

Answers:

  1. We already will need to maintain setup instructions on the CI side, we are simply centralizing this script.
  2. Everything could be implemented solely in GitHub and automated with GitHub actions.
  3. I believe we are already working on a infrastructure repo.

Difference from previous discussion

Previous attempt of using docker in CI is different from this suggestion.

Previous attempt distributes a docker image build script to each repo and builds a docker image to run in for every CI Job. It was meant to alleviate “works on my machine” problem and provide a general developer facing environment, thus in principle each repo may have variation on the docker build script.

This suggestion propose we keep the common use case inside the generic GitHub runner image. It propose we use a pre-built docker container only when we attempt to execute a cross-compiler test, of which the facilities needed to setup such environment could be easily centralized. The overhead of maintaining docker image and subsequent build jobs can be easily amortized to each repo, and provide meaningful abstraction for a common test case. This works will right now as we seem to be hitting a infrastructure prorogation bottleneck, since we are sure we will have to host a centralized infrastructure repo.

Proof of concept

I’ve made a proof of concept in contrast to Compiler test by wusatosi · Pull Request #84 · bemanproject/exemplar · GitHub .

Docker build script needed to host such automation:

Docker image hosted on GitHub:

CI implementation:

The docker image is built as a mono-image that contains all desired compiler versions, so it is different from my proposal. But there is already a speedup for setting up LLVM family compilers.

Appearently pulling a 4GB image is faster than running the LLVM install script.

Related:

Yes I would be happy to work on this if this is desired.

According to this, the GitHub stock images come with multiple compiler versions. Are these insufficient?

No they do not cover all the versions, and software that come with runner images changes.

This is actually going on right now! GitHub is switching its ubuntu-latest image from 22.04 to 24.04. You can see the range of clang support jump from 13-15 to 16-18; gnu support jump from 9-13 to 12-14.

Meanwhile exemplar is requesting gcc-12 to 15 and clang-16 to 20.

We could tie our supported compilers to that of included in the runner image. This seem to be a good representation of modern compiler’s lifetime.Though it will still make sense for us to cache latest compiler as their setup is bit of a headace. And it will present annual headace of bumping range of compilers that we test for.

Optional26 is trying to support clang 17-20 and gnu 12-14, and you can already see the complications introduced.

I think should try to reuse github images, and maybe, not using ubuntu-latest. After latest PRs that you showed, can we still use ubuntu-22 explicitly? how long?

I don’t think we should count on using earlier version of GitHub Actions Images, we should aim to either have our own build of image, or a script that set up an environment ideally throughout LTS Ubuntu environment, I would vastly prefer the later one unless there’s special cases.

Setting up a specific version of compiler is the perfect situation for “special case” here, it need a potentially lengthy setup script and could be easily broken by running on another os version.

The official documentation indicate support for the last 2 versions of the OS. Ubuntu runs on a 2 year release cycle. So 22 would still be supported for 2 years till when 26 comes out.

Yes you can direct GitHub Action to use 22 instead of latest.

I don’t think we should discourage use of latest, being on latest encourage one to write scripts that works across distributions. If we have too much assumption on the ubuntu version, we will need to evaluate os versions manually ever-so-often, and we may have a lot of headache across the entire org every 2 years when an image get deprecated. I think most of our libraries are going to be maintained for more than 2 years (or 4 years if using 24.04), consider a project is still maintained for two C++ release cycle (6 years!) after its adopted.

In the case of reusing GitHub image. The official documentation indicate native support for 3 latest major versions for GNU/ Clang, that would be GCC 11-14 (about 3 years span), Clang 16-19 (about 1.5 year span) in principle. But the actual versions varies across images and seem to mean “the latest 3 versions when the image was initially created” in practice.

The image specific documentation can be found here: for ubuntu 24.04, for ubuntu 22.04.

Version GNU Clang
22.04 10.5.0, 11.4.0, 12.3.0 13.0.1, 14.0.0, 15.0.7
24.04 12.3.0, 13.3.0, 14.2.0 16.0.6, 17.0.6, 18.1.3

Note that clang 15 is entirely omitted. This is because clang seem to operate on a 6 month release schedule. It takes 4 release cycle for clang for 1 ubuntu release, and GitHub officially packs 3 releases, so one major version of clang is dropped every 2 years. That means clang 19, the latest major release since sept 24 won’t be included in 24.04, and may never be included in 26.

This doesn’t sound like too poor of a support window. Though we do lose the control over the upper and lower range of compiler versions to support. In previous discussions we reached a consensus that we should still support C++17 (and lower) when appropriate. The earliest clang release included in GitHub image is 13, which is released in 2022, we would ideally want to support an earlier version, if someone is stuck on C++17, they probably are stuck on an earlier version of compiler as well.

On top of the loss of control, we will also need to track all these releases and know which image to use. It will suffer the same problem as the “everything breaks every 2 years” problem. I see every repo checking against every recent major compiler version, but I don’t see all repo owners understanding, checking and keeping up with all these life-cycles and support status.

I think its pretty clear we will have to maintain some kind of install script to test across compiler versions. These setup scripts sometimes breaks across ubuntu releases and have caveats, e.g. llvm-16 and lower cannot be installed on ubuntu 24.04 directly using the llvm install script, higher gnu versions are not available on 20.04 universe apt repo.

If all repos are expected to test across compiler versions, we might as well move the install script into a docker container.