diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4e9951c..9801b1f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,6 +6,9 @@ on: pull_request: branches: [ "main", "master" ] +env: + BUILD_DEPS: cmake ninja-build libsdl2-dev protobuf-compiler + jobs: build-and-test: runs-on: ubuntu-latest @@ -17,17 +20,53 @@ jobs: - name: Install dependencies run: | sudo apt-get update - sudo apt-get install -y cmake clang-format clang-tidy-20 libsdl2-dev protobuf-compiler gcovr + sudo apt-get install -y $BUILD_DEPS clang-format gcovr - name: Check Code Formatting run: | find src include tests -type f \( -name "*.cpp" -o -name "*.hpp" -o -name "*.c" -o -name "*.h" \) | xargs clang-format --dry-run -Werror - name: Configure CMake - run: cmake -B build -DCMAKE_BUILD_TYPE=Release -DNEURON_IDE_ENABLE_COVERAGE=ON + run: cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DNEURON_IDE_ENABLE_COVERAGE=ON -DNEURON_IDE_ENABLE_CLANG_TIDY=OFF - name: Build - run: cmake --build build --config Release + run: cmake --build build --parallel - name: Run Tests & Check Coverage run: cmake --build build --target coverage + + clang-tidy: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + # On pull requests HEAD is the merge commit and HEAD^1 the base branch, which the + # changed-file list below diffs against. + fetch-depth: 2 + + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install -y $BUILD_DEPS clang-tidy-20 + + - name: Configure CMake + run: cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_EXPORT_COMPILE_COMMANDS=ON -DNEURON_IDE_ENABLE_CLANG_TIDY=OFF + + - name: Generate protobuf headers + run: cmake --build build --target neuronide_proto + + - name: Run clang-tidy + run: | + if [ "$GITHUB_EVENT_NAME" = "pull_request" ] && git diff --quiet HEAD^1 HEAD -- .clang-tidy; then + files=$(git diff --name-only --diff-filter=d HEAD^1 HEAD -- \ + 'src/*.cpp' 'src/*.hpp' 'include/*.hpp' 'tests/*.cpp' 'tests/*.hpp') + else + files=$(git ls-files 'src/*.cpp' 'tests/*.cpp') + fi + if [ -z "$files" ]; then + echo "No C++ files changed." + exit 0 + fi + echo "$files" | xargs -P "$(nproc)" -n 1 clang-tidy-20 -p build --quiet diff --git a/README.md b/README.md index efa93db..959f8e6 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ EEG samples and the stimulus markers must share a **common clock domain** so tha | Concern | Choice | | ------------------------ | ---------------------------------------------------------------------- | | Language | C++20 | -| Build system | CMake (≥ 3.25), Ninja-friendly | +| Build system | CMake (≥ 3.25), Ninja recommended | | Experiment file format | Protocol Buffers (proto3) — see `protoFiles/neuronide.proto` | | Device config format | JSON (`config.json`) parsed with [nlohmann/json](https://github.com/nlohmann/json) | | EEG acquisition | [LSL — Lab Streaming Layer](https://github.com/sccn/liblsl) (`liblsl`) | @@ -407,21 +407,50 @@ All commands are run from the `Neuron-IDE-runtime/` directory. ```bash sudo apt update -sudo apt install cmake clang-format clang-tidy libsdl2-dev protobuf-compiler gcovr -# GTest, LSL and concurrentqueue are fetched automatically by CMake. +sudo apt install cmake ninja-build clang-format clang-tidy-20 libsdl2-dev protobuf-compiler gcovr +# GTest, LSL, concurrentqueue and nlohmann/json are fetched automatically by CMake. ``` ### Configure & build +Use the [Ninja](https://ninja-build.org/) generator: it builds on all CPU cores by default and +rebuilds only what changed. + ```bash -cmake -B build +cmake -B build -G Ninja cmake --build build ./build/src/NeuronIDE config.json experiment.neuroz # waits for the EEG stream, then records and runs the experiment ``` +`-G Ninja` is needed only the first time you configure a build directory; CMake remembers it. +A `build/` already configured with the default generator (Make) can't be switched, so delete it +first (`rm -rf build`). If you stay on Make, build with `cmake --build build --parallel`; +without `--parallel`, Make compiles one file at a time. + `NeuronIDE` takes the path to a device `config.json` (defaults to `config.json` in the working directory). +#### Faster builds without clang-tidy + +By default clang-tidy runs on every file during the build and takes several times longer than +compiling it. While iterating, turn it off. The setting is remembered in `build/`; configure +with `=ON` to turn it back on. + +```bash +cmake -B build -G Ninja -DNEURON_IDE_ENABLE_CLANG_TIDY=OFF -DCMAKE_EXPORT_COMPILE_COMMANDS=ON +cmake --build build +``` + +CI still runs clang-tidy and fails on any warning, so run the same check before pushing. It +reads the `compile_commands.json` that `CMAKE_EXPORT_COMPILE_COMMANDS` writes: + +```bash +git ls-files 'src/*.cpp' 'tests/*.cpp' | xargs -P "$(nproc)" -n 1 clang-tidy-20 -p build --quiet +``` + +Lines like `43669 warnings generated.` count warnings in third-party headers, which +`.clang-tidy` filters out; only `error:` lines are problems. + ### Tests ```bash @@ -444,11 +473,13 @@ ctest -L component --output-on-failure ```bash cmake --build build --target format ``` +- clang-tidy runs as part of the build; see + [Faster builds without clang-tidy](#faster-builds-without-clang-tidy) to turn it off. ### Coverage ```bash -cmake -B build -DNEURON_IDE_ENABLE_COVERAGE=ON +cmake -B build -G Ninja -DNEURON_IDE_ENABLE_COVERAGE=ON cmake --build build cmake --build build --target coverage # runs tests + gcovr xdg-open build/coverage/index.html diff --git a/cmake/StaticAnalysis.cmake b/cmake/StaticAnalysis.cmake index 9b9d697..1accfe9 100644 --- a/cmake/StaticAnalysis.cmake +++ b/cmake/StaticAnalysis.cmake @@ -1,12 +1,18 @@ # cmake/StaticAnalysis.cmake # --- Clang-Tidy Configuration --- -find_program(CLANG_TIDY_EXE NAMES "clang-tidy-20") -if(CLANG_TIDY_EXE) - message(STATUS "clang-tidy found: ${CLANG_TIDY_EXE}") - set(CMAKE_CXX_CLANG_TIDY "${CLANG_TIDY_EXE}") -else() - message(WARNING "clang-tidy not found.") +# Runs clang-tidy next to the compiler on every file, which costs several times more than the +# compile itself. CI turns it off and runs clang-tidy in a separate job instead. +option(NEURON_IDE_ENABLE_CLANG_TIDY "Run clang-tidy on every file as part of the build" ON) + +if(NEURON_IDE_ENABLE_CLANG_TIDY) + find_program(CLANG_TIDY_EXE NAMES "clang-tidy-20") + if(CLANG_TIDY_EXE) + message(STATUS "clang-tidy found: ${CLANG_TIDY_EXE}") + set(CMAKE_CXX_CLANG_TIDY "${CLANG_TIDY_EXE}") + else() + message(WARNING "clang-tidy not found.") + endif() endif() # --- Clang-Format Target ---