From 4d3d57dca515a6fb0d97fe46a272c6f89f660287 Mon Sep 17 00:00:00 2001 From: Vincent La Date: Sun, 20 Sep 2026 11:36:29 -0700 Subject: [PATCH] Speed up C++ CI and cover older GCC toolchains --- .github/workflows/cmake-multi-platform.yml | 7 ++ .github/workflows/codeql.yml | 18 ++-- .github/workflows/compat-edge-cases.yml | 18 ++-- .github/workflows/coverage.yml | 7 ++ .github/workflows/gcc-compatibility.yml | 104 +++++++++++++++++++++ .github/workflows/sanitizers.yml | 12 ++- tests/CMakeLists.txt | 1 + tests/compatibility/gcc/CMakeLists.txt | 34 +++++++ tests/compatibility/gcc/smoke.cpp | 26 ++++++ 9 files changed, 206 insertions(+), 21 deletions(-) create mode 100644 .github/workflows/gcc-compatibility.yml create mode 100644 tests/compatibility/gcc/CMakeLists.txt create mode 100644 tests/compatibility/gcc/smoke.cpp diff --git a/.github/workflows/cmake-multi-platform.yml b/.github/workflows/cmake-multi-platform.yml index ec77fad6..067ea7de 100644 --- a/.github/workflows/cmake-multi-platform.yml +++ b/.github/workflows/cmake-multi-platform.yml @@ -11,7 +11,14 @@ on: permissions: contents: read +# New PR commits supersede older checks; keep every non-PR run independent. +concurrency: + group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && format('pr-{0}', github.event.pull_request.number) || github.run_id }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + env: + # Bound compiler memory use while taking advantage of multiple runner cores. + CMAKE_BUILD_PARALLEL_LEVEL: "2" CSV_FETCHCONTENT_BASE_DIR: ${{ github.workspace }}/.cache/fetchcontent jobs: diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index ff56ec76..8234386b 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -8,6 +8,15 @@ on: schedule: - cron: '0 2 * * 0' # Weekly on Sunday at 2 AM UTC +# New PR commits supersede older checks; keep every non-PR run independent. +concurrency: + group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && format('pr-{0}', github.event.pull_request.number) || github.run_id }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +env: + # Bound compiler memory use while taking advantage of multiple runner cores. + CMAKE_BUILD_PARALLEL_LEVEL: "2" + jobs: analyze: name: CodeQL C++ @@ -31,15 +40,10 @@ jobs: - name: Initialize CodeQL uses: github/codeql-action/init@v3 with: - language: ${{ matrix.language }} + languages: ${{ matrix.language }} queries: security-and-quality config-file: ./.github/codeql/codeql-config.yml - - - name: Install dependencies - run: | - sudo apt-get update - sudo apt-get install -y build-essential cmake - + - name: Configure CMake run: | mkdir -p build diff --git a/.github/workflows/compat-edge-cases.yml b/.github/workflows/compat-edge-cases.yml index 12133ff3..1343db76 100644 --- a/.github/workflows/compat-edge-cases.yml +++ b/.github/workflows/compat-edge-cases.yml @@ -3,7 +3,14 @@ name: Compatibility Edge Cases permissions: contents: read +# New PR commits supersede older checks; keep every non-PR run independent. +concurrency: + group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && format('pr-{0}', github.event.pull_request.number) || github.run_id }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + env: + # Bound compiler memory use while taking advantage of multiple runner cores. + CMAKE_BUILD_PARALLEL_LEVEL: "2" CSV_FETCHCONTENT_BASE_DIR: ${{ github.workspace }}/.cache/fetchcontent on: @@ -29,10 +36,8 @@ jobs: runs-on: windows-latest steps: - - name: Checkout repository and submodules + - name: Checkout repository uses: actions/checkout@v5 - with: - submodules: recursive - name: Set up MSVC developer command prompt uses: ilammy/msvc-dev-cmd@v1 @@ -43,18 +48,13 @@ jobs: with: run: "false" - - name: Cache CMake FetchContent dependencies - uses: actions/cache@v5 - with: - path: ${{ env.CSV_FETCHCONTENT_BASE_DIR }} - key: fetchcontent-${{ runner.os }}-msvc-no-zc-${{ hashFiles('CMakeLists.txt', 'tests/CMakeLists.txt') }} - - name: Configure CMake (for single-header generation) run: > cmake -S ${{ github.workspace }} -B ${{ github.workspace }}/build/no-zc-cplusplus -DCSV_CXX_STANDARD=20 -DCSV_BUILD_PROGRAMS=OFF + -DCSV_BUILD_TESTS=OFF -DBUILD_PYTHON=OFF -DCSV_SINGLE_HEADER_EXECUTABLE="${{ steps.single-header.outputs.executable }}" diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 04628759..2a297b6b 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -10,7 +10,14 @@ permissions: contents: read pull-requests: write +# New PR commits supersede older checks; keep every non-PR run independent. +concurrency: + group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && format('pr-{0}', github.event.pull_request.number) || github.run_id }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + env: + # Bound compiler memory use while taking advantage of multiple runner cores. + CMAKE_BUILD_PARALLEL_LEVEL: "2" CSV_FETCHCONTENT_BASE_DIR: ${{ github.workspace }}/.cache/fetchcontent jobs: diff --git a/.github/workflows/gcc-compatibility.yml b/.github/workflows/gcc-compatibility.yml new file mode 100644 index 00000000..27307f11 --- /dev/null +++ b/.github/workflows/gcc-compatibility.yml @@ -0,0 +1,104 @@ +name: Older GCC compatibility + +on: + push: + branches: [master] + paths: + - "include/**" + - "CMakeLists.txt" + - "cmake/**" + - "single_header.json" + - "tests/compatibility/gcc/**" + - ".github/workflows/gcc-compatibility.yml" + pull_request: + branches: [master] + paths: + - "include/**" + - "CMakeLists.txt" + - "cmake/**" + - "single_header.json" + - "tests/compatibility/gcc/**" + - ".github/workflows/gcc-compatibility.yml" + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && format('pr-{0}', github.event.pull_request.number) || github.run_id }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +jobs: + gcc: + name: GCC ${{ matrix.version }} / C++17 + runs-on: ubuntu-24.04 + timeout-minutes: 15 + strategy: + fail-fast: false + matrix: + include: + - version: "8.5.0" + image: gcc:8.5.0 + - version: "9.5.0" + image: gcc:9.5.0-bullseye + - version: "10.5.0" + image: gcc:10.5.0-bullseye + # The first GCC 11 release covers the floating charconv boundary. + - version: "11.1.0" + image: gcc:11.1.0-bullseye + env: + GCC_IMAGE: ${{ matrix.image }} + GCC_VERSION: ${{ matrix.version }} + steps: + # Keep JavaScript actions on the host; old images may lack Node's glibc. + - uses: actions/checkout@v5 + + - name: Install single_header + id: single-header + uses: vincentlaucsb/single_header@0.1.0 + with: + run: "false" + + - name: Generate canonical single header + run: | + cmake -S . -B build/gcc-header -DCSV_CXX_STANDARD=17 \ + -DCSV_BUILD_TESTS=OFF -DCSV_BUILD_PROGRAMS=OFF \ + -DCSV_SINGLE_HEADER_EXECUTABLE="${{ steps.single-header.outputs.executable }}" + cmake --build build/gcc-header --target generate_single_header + + - name: Prepare portable CMake and compiler + id: toolchain + run: | + mkdir -p "$RUNNER_TEMP/csv-cmake" + curl --fail --location --retry 3 \ + https://cmake.org/files/v3.31/cmake-3.31.6-linux-x86_64.tar.gz \ + --output "$RUNNER_TEMP/csv-cmake.tar.gz" + echo "5a1133ff103c71eb5120e2cc3de922733e7d8a26a98ae716397e8676adb367bf $RUNNER_TEMP/csv-cmake.tar.gz" | sha256sum --check + tar -xzf "$RUNNER_TEMP/csv-cmake.tar.gz" -C "$RUNNER_TEMP/csv-cmake" --strip-components=1 + docker pull "$GCC_IMAGE" + docker run --rm "$GCC_IMAGE" g++ --version + test "$(docker run --rm "$GCC_IMAGE" g++ -dumpfullversion)" = "$GCC_VERSION" + + - name: Build and run CMake library consumers + run: | + docker run --rm --user "$(id -u):$(id -g)" \ + -v "$GITHUB_WORKSPACE:/src" -v "$RUNNER_TEMP/csv-cmake:/cmake:ro" \ + -w /src "$GCC_IMAGE" bash -euc ' + /cmake/bin/cmake -S tests/compatibility/gcc -B build/gcc-linked -DCMAKE_BUILD_TYPE=Release + /cmake/bin/cmake --build build/gcc-linked --parallel 2 + /cmake/bin/ctest --test-dir build/gcc-linked --output-on-failure + ' + + - name: Build and run standalone generated header consumers + # Preserve independent diagnostics even if the linked-library check fails. + if: ${{ !cancelled() && steps.toolchain.outcome == 'success' }} + run: | + docker run --rm --user "$(id -u):$(id -g)" \ + -v "$GITHUB_WORKSPACE:/src" -v "$RUNNER_TEMP/csv-cmake:/cmake:ro" \ + -w /src "$GCC_IMAGE" bash -euc ' + /cmake/bin/cmake -S tests/compatibility/gcc -B build/gcc-generated \ + -DCMAKE_BUILD_TYPE=Release \ + -DCSV_COMPAT_SINGLE_HEADER_DIR=/src/build/gcc-header/single_include_generated + /cmake/bin/cmake --build build/gcc-generated --parallel 2 + /cmake/bin/ctest --test-dir build/gcc-generated --output-on-failure + ' diff --git a/.github/workflows/sanitizers.yml b/.github/workflows/sanitizers.yml index 505e86bb..4cb8a4e5 100644 --- a/.github/workflows/sanitizers.yml +++ b/.github/workflows/sanitizers.yml @@ -9,7 +9,14 @@ on: permissions: contents: read +# New PR commits supersede older checks; keep every non-PR run independent. +concurrency: + group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && format('pr-{0}', github.event.pull_request.number) || github.run_id }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + env: + # Bound compiler memory use while taking advantage of multiple runner cores. + CMAKE_BUILD_PARALLEL_LEVEL: "2" CSV_FETCHCONTENT_BASE_DIR: ${{ github.workspace }}/.cache/fetchcontent jobs: @@ -54,11 +61,6 @@ jobs: path: ${{ env.CSV_FETCHCONTENT_BASE_DIR }} key: fetchcontent-${{ runner.os }}-sanitizer-${{ matrix.sanitizer }}-std${{ matrix.cxx_standard }}-${{ hashFiles('CMakeLists.txt', 'tests/CMakeLists.txt') }} - - name: Install dependencies - run: | - sudo apt-get update - sudo apt-get install -y build-essential cmake - - name: Configure CMake with ${{ matrix.name }} run: | mkdir -p build diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index fdc16e5c..75aeb003 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -4,6 +4,7 @@ FetchContent_Declare( Catch2 GIT_REPOSITORY https://github.com/catchorg/Catch2.git GIT_TAG v3.6.0 + GIT_SHALLOW TRUE ) FetchContent_MakeAvailable(Catch2) diff --git a/tests/compatibility/gcc/CMakeLists.txt b/tests/compatibility/gcc/CMakeLists.txt new file mode 100644 index 00000000..5dae02d9 --- /dev/null +++ b/tests/compatibility/gcc/CMakeLists.txt @@ -0,0 +1,34 @@ +cmake_minimum_required(VERSION 3.15) +project(csv_gcc_compatibility LANGUAGES C CXX) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) +enable_testing() + +function(add_float_smoke name) + add_executable(${name} smoke.cpp) + add_test(NAME ${name} COMMAND ${name}) +endfunction() + +if(CSV_COMPAT_SINGLE_HEADER_DIR) + # Standalone consumers deliberately inherit no csv target definitions. + find_package(Threads REQUIRED) + foreach(name generated_float_smoke generated_float_optout) + add_float_smoke(${name}) + target_include_directories(${name} PRIVATE "${CSV_COMPAT_SINGLE_HEADER_DIR}") + target_link_libraries(${name} PRIVATE Threads::Threads) + endforeach() + target_compile_definitions(generated_float_optout PRIVATE + CLASSIFY_SCALAR_DISABLE_STD_FLOAT_FROM_CHARS=1 + CSV_COMPAT_EXPECT_FLOAT_OPTOUT=1) +else() + set(CSV_BUILD_TESTS OFF CACHE BOOL "" FORCE) + set(CSV_BUILD_PROGRAMS OFF CACHE BOOL "" FORCE) + set(BUILD_PYTHON OFF CACHE BOOL "" FORCE) + add_subdirectory("${CMAKE_CURRENT_LIST_DIR}/../../.." csv-library) + foreach(library csv csv_no_simd) + add_float_smoke(${library}_float_smoke) + target_link_libraries(${library}_float_smoke PRIVATE ${library}) + endforeach() +endif() diff --git a/tests/compatibility/gcc/smoke.cpp b/tests/compatibility/gcc/smoke.cpp new file mode 100644 index 00000000..e75372d0 --- /dev/null +++ b/tests/compatibility/gcc/smoke.cpp @@ -0,0 +1,26 @@ +#include "csv.hpp" + +#include +#include + +#if defined(CSV_COMPAT_EXPECT_FLOAT_OPTOUT) && defined(CLASSIFY_SCALAR_HAS_STD_FLOAT_FROM_CHARS) +#error "The explicit floating-point from_chars opt-out was ignored" +#endif + +int main() { + // Exercise field conversion and linking on old libstdc++ versions without + // involving reader/chunk behavior, which is covered by the main suite. + const char* inputs[] = { "1.25", "-42.5", "6.25e2", "1.5e-3" }; + const double expected[] = { 1.25, -42.5, 625.0, 0.0015 }; + for (unsigned i = 0; i < sizeof(inputs) / sizeof(inputs[0]); ++i) { + csv::CSVField field(inputs[i]); + const double actual = field.get(); + if (!field.is_float() || !std::isfinite(actual) + || std::abs(actual - expected[i]) > 1e-12) { + std::cerr << "Incorrect floating-point conversion for " << inputs[i] + << ": " << actual << '\n'; + return 1; + } + } + return 0; +}