Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
143 changes: 72 additions & 71 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,55 +12,69 @@ concurrency:
cancel-in-progress: true

env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
BUILD_TYPE: Release
BUILD_WRAPPER_OUT_DIR: build_wrapper_output_directory # Directory where build-wrapper output will be placed

jobs:
build:
name: Build & Test
runs-on: macos-26
# Build the engine + Catch2 unit tests with CMake/Ninja + Clang/libc++ and run
# them through ctest on both macOS (Homebrew LLVM) and Ubuntu (apt.llvm.org
# Clang + libc++) — the multi-platform proof. The macOS leg also produces the
# SonarCloud coverage report from Clang source-based coverage.
build-and-test:
name: Build & Test (${{ matrix.os }})
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [macos-26, ubuntu-latest]

steps:
- name: Checkout repository
uses: actions/checkout@v5
with:
submodules: recursive

- name: Set up Homebrew
id: set-up-homebrew
if: runner.os == 'macOS'
uses: Homebrew/actions/setup-homebrew@main

- name: Install dependencies
- name: Install dependencies (macOS)
if: runner.os == 'macOS'
run: bash ./.github/workflows/scripts/brew.sh

- name: Install dependencies (Ubuntu)
if: runner.os == 'Linux'
run: bash ./.github/workflows/scripts/ubuntu_deps.sh

# Pin CMake (and a matching Ninja) to 4.3.3 so CI matches local dev and
# lands in the [4.3, 4.4) CMAKE_EXPERIMENTAL_CXX_IMPORT_STD UUID branch in
# CMakeLists.txt. The runner's default CMake (3.31.x) has no pinned UUID and
# fails configuration. Runs before build_dep.sh since that also calls cmake.
- name: Setup CMake & Ninja
uses: lukka/get-cmake@v4.3.3

- name: Build C++ Libraries
run: bash ./scripts/build_dep.sh

- name: Select Xcode version
run: sudo xcode-select -switch /Applications/Xcode.app
# scripts/build.sh selects the toolchain per platform (Homebrew LLVM on
# macOS, clang/clang++ on Linux), configures with Ninja, and builds the
# library, executable, and unit_tests. ENABLE_COVERAGE instruments the
# macOS build for the coverage report below.
- name: Configure & build
run: ENABLE_COVERAGE=${{ runner.os == 'macOS' && 'ON' || 'OFF' }} bash ./scripts/build.sh

- name: Run tests
run: ctest --test-dir build --output-on-failure

- name: Generate coverage report
if: runner.os == 'macOS'
run: >
OTHER_CFLAGS="-fprofile-instr-generate -fcoverage-mapping"
OTHER_CPLUSPLUSFLAGS="-fprofile-instr-generate -fcoverage-mapping"
OTHER_SWIFT_FLAGS="-profile-generate -profile-coverage-mapping"
LLVM_PROFILE_FILE="/tmp/coverage.profraw"
CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO
xcodebuild
-scheme tests
-destination 'platform=macOS,arch=arm64'
-resultBundlePath TestResult/
-enableCodeCoverage YES
-derivedDataPath "${RUNNER_TEMP}/Build/DerivedData"
-parallelizeTargets
-jobs "$(sysctl -n hw.logicalcpu)"
clean build test
| xcpretty -r junit && exit ${PIPESTATUS[0]}

- name: Convert coverage report to sonarqube format
run: >
bash ./.github/workflows/scripts/xccov-to-sonarqube-generic.sh *.xcresult/ > sonarqube-generic-coverage.xml
bash ./.github/workflows/scripts/llvmcov-to-sonarqube-generic.sh build
> sonarqube-generic-coverage.xml

- name: Upload coverage report
if: runner.os == 'macOS'
uses: actions/upload-artifact@v7
with:
name: sonarqube-coverage
Expand All @@ -69,7 +83,7 @@ jobs:

sonar-scan:
name: SonarCloud Scan
needs: build
needs: build-and-test
runs-on: ubuntu-latest

steps:
Expand All @@ -78,45 +92,27 @@ jobs:
uses: actions/checkout@v5
with:
fetch-depth: 0
submodules: recursive

# Same Clang + libc++ + Boost toolchain as the Ubuntu test leg, so the
# build-wrapper captures a build identical to the one that is tested.
- name: Install dependencies
run: sudo apt install -y libssl-dev libpq-dev libcurl4-openssl-dev
# ubuntu-latest (24.04) defaults to GCC 13, whose libstdc++ lacks the
# C++23 <print> header (std::print/std::println). That header was added
# in GCC 14, so install it and make it the default compiler for every
# subsequent step (dependency build, CMake configure, build-wrapper).
- name: Install GCC 14 (for C++23 <print>)
run: |
sudo apt install -y gcc-14 g++-14
sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-14 100
sudo update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-14 100
# Ubuntu's apt Boost (1.83 on 24.04) predates Boost.Redis, which was added
# in Boost 1.84, so <boost/redis.hpp> is absent. Install a newer Boost.
# Boost.Redis/Asio are header-only, so we only need the headers plus the
# CMake package config — building just Boost.System generates both quickly.
- name: Install Boost >= 1.84 (for Boost.Redis)
run: |
BOOST_VERSION=1.86.0
BOOST_DIR="boost_${BOOST_VERSION//./_}"
wget -q "https://archives.boost.io/release/${BOOST_VERSION}/source/${BOOST_DIR}.tar.gz"
tar xzf "${BOOST_DIR}.tar.gz"
cd "${BOOST_DIR}"
./bootstrap.sh > /dev/null
# -d0 silences per-action output (otherwise b2 prints one line per
# copied header — ~15k lines just for the header install).
sudo ./b2 install -d0 --prefix=/usr/local --with-system -j"$(nproc)"
- name: Check compiler version, for debugging
run: |
g++ --version
cmake --version
run: bash ./.github/workflows/scripts/ubuntu_deps.sh

# Match local dev / the [4.3, 4.4) import-std UUID branch in CMakeLists.txt;
# the runner's default CMake (3.31.x) has no pinned UUID. See the build job.
- name: Setup CMake & Ninja
uses: lukka/get-cmake@v4.3.3

- name: Build C++ Libraries
run: bash ./scripts/build_dep.sh
# SonarQube Server and Cloud (formerly SonarQube and SonarCloud) is a widely used static
# analysis solution for continuous code quality and security inspection.
# This action now supports and is the official entrypoint for scanning C++ projects via GitHub actions.

# SonarQube Server and Cloud is a widely used static analysis solution for
# continuous code quality and security inspection, and the official
# entrypoint for scanning C++ projects via GitHub actions.
# https://github.com/SonarSource/sonarqube-scan-action
- name: Install Build Wrapper
uses: SonarSource/sonarqube-scan-action/install-build-wrapper@v4.2.1
# This step installs the SonarQube build wrapper, which is necessary for analyzing C/C++ projects.

# Lands at ./artifact/sonarqube-generic-coverage.xml so the existing
# sonar.coverageReportPaths argument keeps working unchanged.
Expand All @@ -126,16 +122,21 @@ jobs:
name: sonarqube-coverage
path: artifact

# Configures the CMake build system, specifying the source directory and build directory, and setting the build type
- name: Configure CMake
run: cmake -S ${{github.workspace}} -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} -DCMAKE_EXPORT_COMPILE_COMMANDS=ON

# Runs the build wrapper to capture build commands and outputs them to the specified directory. Then builds the project using CMake
- name: Run build-wrapper
run: |
build-wrapper-linux-x86-64 --out-dir ${{ env.BUILD_WRAPPER_OUT_DIR }} cmake --build ${{github.workspace}}/build --config ${{env.BUILD_TYPE}} --clean-first

# Performs the SonarQube scan using the scan action. Uses captured build commands for analysis and requires GitHub and SonarQube tokens for authentication
# Build through scripts/build.sh under the build-wrapper so the analyzed
# build uses the exact same toolchain as the tested build (apt.llvm.org
# Clang + libc++ + OpenMP). A bare `cmake -DCMAKE_CXX_COMPILER=clang++`
# picked up the runner's system Clang 18 — which has no libc++ `std` module
# and no matching libomp, so find_package(OpenMP REQUIRED) failed. The
# build-wrapper intercepts the Ninja compile commands for the scan; deps
# were already built above so build.sh skips them and only our TUs are
# captured.
- name: Build under build-wrapper
run: >
build-wrapper-linux-x86-64 --out-dir ${{ env.BUILD_WRAPPER_OUT_DIR }}
bash ./scripts/build.sh

# Performs the SonarQube scan using the captured build commands and the
# downloaded coverage report. Requires GitHub and SonarQube tokens.
- name: SonarQube Scan
uses: SonarSource/sonarqube-scan-action@v4.2.1
env:
Expand Down
5 changes: 4 additions & 1 deletion .github/workflows/scripts/brew.sh
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,7 @@ check_and_install() {
# Install packages if they don't exist
check_and_install postgresql@18 # Check and install PostgreSQL (which includes libpq)
check_and_install pkg-config # Check and install pkg-config
check_and_install boost # Check and install pkg-config
check_and_install boost # Check and install Boost
check_and_install llvm # Clang + libc++ with the `std` module (import std)
check_and_install ninja # Ninja generator (required for C++23 modules / import std)
check_and_install libomp # OpenMP runtime (linked by the engine library)
6 changes: 0 additions & 6 deletions .github/workflows/scripts/cpp_coverage.sh

This file was deleted.

79 changes: 79 additions & 0 deletions .github/workflows/scripts/llvmcov-to-sonarqube-generic.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
#!/usr/bin/env bash
# Generate a SonarQube "generic test coverage" report from the Catch2 unit tests
# using Clang source-based coverage (replaces the xcodebuild + xccov path).
#
# Pipeline: run the instrumented unit_tests binary -> llvm-profdata merge ->
# llvm-cov export (lcov) -> convert lcov to Sonar generic XML on stdout.
#
# Usage: bash llvmcov-to-sonarqube-generic.sh [BUILD_DIR] > coverage.xml
# BUILD_DIR defaults to "build". Progress goes to stderr; only the XML is
# written to stdout, so the caller can redirect it straight to a file (matching
# how xccov-to-sonarqube-generic.sh was invoked).
#
# llvm-cov / llvm-profdata are taken from $LLVM_COV / $LLVM_PROFDATA if set,
# else from Homebrew LLVM on macOS, else from PATH (apt.llvm.org on Linux puts
# versioned tools on PATH via update-alternatives).
set -euo pipefail

BUILD_DIR="${1:-build}"
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)"
TEST_BIN="${BUILD_DIR}/tests/unit_tests"

if [[ ! -x "$TEST_BIN" ]]; then
echo "Test binary not found at $TEST_BIN — build the unit_tests target with -DENABLE_COVERAGE=ON first" 1>&2
exit 1
fi

# Resolve the LLVM coverage tools.
if [[ -n "${LLVM_COV:-}" ]]; then
: # caller-provided
elif [[ "$(uname)" == "Darwin" ]] && command -v brew &>/dev/null; then
LLVM_BIN="$(brew --prefix llvm)/bin"
LLVM_COV="${LLVM_BIN}/llvm-cov"
LLVM_PROFDATA="${LLVM_BIN}/llvm-profdata"
else
LLVM_COV="llvm-cov"
LLVM_PROFDATA="llvm-profdata"
fi
LLVM_PROFDATA="${LLVM_PROFDATA:-llvm-profdata}"

PROFRAW="${BUILD_DIR}/coverage/unit_tests.profraw"
PROFDATA="${BUILD_DIR}/coverage/unit_tests.profdata"
mkdir -p "${BUILD_DIR}/coverage"

echo "Running instrumented tests..." 1>&2
LLVM_PROFILE_FILE="$PROFRAW" "$TEST_BIN" 1>&2

echo "Merging profile data..." 1>&2
"$LLVM_PROFDATA" merge -sparse "$PROFRAW" -o "$PROFDATA"

echo "Exporting coverage and converting to Sonar generic XML..." 1>&2
# Only the project's own source is relevant — keep files under source/ and drop
# system headers, Catch2, and the test files themselves.
"$LLVM_COV" export -format=lcov -instr-profile="$PROFDATA" "$TEST_BIN" \
| awk -v root="${REPO_ROOT}/" '
function xml_escape(s) {
gsub(/&/, "\\&amp;", s); gsub(/</, "\\&lt;", s); gsub(/>/, "\\&gt;", s);
gsub(/"/, "\\&quot;", s); return s;
}
BEGIN { print "<coverage version=\"1\">" }
/^SF:/ {
path = substr($0, 4);
sub("^" root, "", path); # make repo-relative
keep = (path ~ /^source\//); # project source only
if (keep) printf " <file path=\"%s\">\n", xml_escape(path);
next;
}
/^DA:/ {
if (!keep) next;
split(substr($0, 4), a, ",");
covered = (a[2] + 0 > 0) ? "true" : "false";
printf " <lineToCover lineNumber=\"%d\" covered=\"%s\"/>\n", a[1], covered;
next;
}
/^end_of_record/ {
if (keep) print " </file>";
keep = 0;
next;
}
END { print "</coverage>" }'
47 changes: 47 additions & 0 deletions .github/workflows/scripts/ubuntu_deps.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
#!/usr/bin/env bash
# Install the Ubuntu CI toolchain: Clang + libc++ from apt.llvm.org (the Linux
# equivalent of the macOS Homebrew-LLVM setup), Ninja, the engine's system
# dependencies, and a Boost new enough for Boost.Redis. Mirrors the toolchain
# scripts/build.sh expects so `import std` works the same on both platforms.
set -euo pipefail

LLVM_VERSION="${LLVM_VERSION:-20}"
BOOST_VERSION="${BOOST_VERSION:-1.90.0}"

echo "== Installing Clang ${LLVM_VERSION} + libc++ from apt.llvm.org =="
wget -q https://apt.llvm.org/llvm.sh
chmod +x llvm.sh
sudo ./llvm.sh "${LLVM_VERSION}"

sudo apt-get update
sudo apt-get install -y \
"libc++-${LLVM_VERSION}-dev" "libc++abi-${LLVM_VERSION}-dev" \
"libomp-${LLVM_VERSION}-dev" \
ninja-build \
libssl-dev libpq-dev libcurl4-openssl-dev

# Make the pinned Clang the default `clang`/`clang++` so scripts/build.sh and the
# build-wrapper pick it up via the unversioned names.
sudo update-alternatives --install /usr/bin/clang clang "/usr/bin/clang-${LLVM_VERSION}" 100
sudo update-alternatives --install /usr/bin/clang++ clang++ "/usr/bin/clang++-${LLVM_VERSION}" 100

# update-alternatives doesn't reliably repoint /usr/bin/clang on the runner (the
# image ships its own system Clang 18 at that path), so scripts/build.sh fell
# back to it — and libomp-18-dev is never installed, breaking
# find_package(OpenMP REQUIRED). Pin $CC/$CXX to the versioned binaries for every
# later workflow step; build.sh honours them over the unversioned fallback.
if [ -n "${GITHUB_ENV:-}" ]; then
echo "CC=clang-${LLVM_VERSION}" >> "$GITHUB_ENV"
echo "CXX=clang++-${LLVM_VERSION}" >> "$GITHUB_ENV"
fi

# Ubuntu's apt Boost predates Boost.Redis (added in 1.84). Boost.Redis/Asio are
# header-only, so only the headers + CMake config are needed — building just
# Boost.System generates both quickly. (-d0 silences per-action output.)
echo "== Installing Boost ${BOOST_VERSION} (for Boost.Redis) =="
BOOST_DIR="boost_${BOOST_VERSION//./_}"
wget -q "https://archives.boost.io/release/${BOOST_VERSION}/source/${BOOST_DIR}.tar.gz"
tar xzf "${BOOST_DIR}.tar.gz"
cd "${BOOST_DIR}"
./bootstrap.sh > /dev/null
sudo ./b2 install -d0 --prefix=/usr/local --with-system -j"$(nproc)"
47 changes: 0 additions & 47 deletions .github/workflows/scripts/xccov-to-sonarqube-generic.sh

This file was deleted.

Loading
Loading