diff --git a/.codespellrc b/.codespellrc index 8809455..0269020 100644 --- a/.codespellrc +++ b/.codespellrc @@ -73,7 +73,12 @@ ignore-words-list = bessel,ist,sinces,nd,te,inout,nin,numer,neet,anc,sinks,doubl labelled,labelling,labels,labelled, fulfil,fulfils,fulfilled,fulfilling, endcode, - deklaration,deklarationen + deklaration,deklarationen, + recognise,recognised,recognises,recognising,recognisation, + signalled,signalling, + travelled,travelling, + cancelled,cancelling, + modelled,modelling # Words we explicitly DO want flagged (override the default skip list). # Keep empty for now; add as we hit real-but-not-flagged typos. diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..26750ba --- /dev/null +++ b/.editorconfig @@ -0,0 +1,56 @@ +# conformallab++ EditorConfig +# +# Honoured natively by VSCode (with the EditorConfig extension), CLion, +# Vim, Emacs, Sublime, … Covers the basics that .clang-format / +# .cmake-format don't catch (Markdown, Python, YAML, shell, JSON, …) +# and acts as a cross-IDE fallback when clang-format isn't installed. +# +# Authoritative formatting for C++ source still comes from .clang-format; +# this file just keeps the editor's defaults from fighting it. + +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +indent_style = space +indent_size = 4 + +# C++ — match .clang-format +[*.{h,hpp,cpp,c,cc}] +indent_size = 4 +max_line_length = 100 + +# CMake — match .cmake-format.yaml +[{CMakeLists.txt,*.cmake}] +indent_size = 4 +max_line_length = 100 + +# Python — PEP-8 default +[*.py] +indent_size = 4 +max_line_length = 100 + +# Shell — Google shell style +[*.sh] +indent_size = 4 +max_line_length = 100 + +# YAML — community convention +[*.{yml,yaml}] +indent_size = 2 + +# JSON +[*.json] +indent_size = 2 + +# Markdown — preserve trailing spaces (used for line breaks); don't strip +[*.md] +trim_trailing_whitespace = false +max_line_length = off + +# Makefiles must use tabs +[Makefile] +indent_style = tab diff --git a/code/CMakeLists.txt b/code/CMakeLists.txt index 88278d7..b3dce95 100644 --- a/code/CMakeLists.txt +++ b/code/CMakeLists.txt @@ -55,8 +55,26 @@ if(NOT CMAKE_BUILD_TYPE) endif() if(CMAKE_CXX_COMPILER_ID MATCHES "Clang|GNU") + # ─── Compiler-warning policy ───────────────────────────────────────────── + # `-Wall -Wextra -Wpedantic` is the project default for first-party code. + # Vendored deps under code/deps/ get a separate, looser policy (handled + # via per-target SYSTEM include marking when they are pulled in). + # + # `CONFORMALLAB_WARNINGS_AS_ERRORS=ON` flips on `-Werror` — used in CI's + # promotion-track and by `scripts/quality/sanitizers.sh` to make sure no + # new warning class slips in unannounced. Off by default so regular + # builds on slightly older toolchains aren't broken by a new GCC's + # added warning. + option(CONFORMALLAB_WARNINGS_AS_ERRORS + "Treat compiler warnings as errors (-Werror)." OFF) + add_compile_options(-Wall -Wextra -Wpedantic) + if(CONFORMALLAB_WARNINGS_AS_ERRORS) + add_compile_options(-Werror) + message(STATUS "Warnings-as-errors mode active (-Werror).") + endif() + # AddressSanitizer only in Debug (gtest_discover_tests runs the binary at # configure time and hangs with ASan enabled). if(CMAKE_BUILD_TYPE STREQUAL "Debug" AND NOT BUILD_TESTING) diff --git a/doc/architecture/dependencies.md b/doc/architecture/dependencies.md new file mode 100644 index 0000000..a68277e --- /dev/null +++ b/doc/architecture/dependencies.md @@ -0,0 +1,151 @@ +# Dependencies & standalone-ness + +This document is the single source of truth for "what does conformallab++ +**require** vs. what does it **optionally** use". Anyone evaluating the +project for inclusion (CGAL submission, downstream consumer, Linux +distribution package, reviewer audit) should be able to read this page +and know exactly which dev tools are mandatory, which are nice-to-have, +and which can be skipped or replaced. + +## TL;DR + +| Layer | What is required | What is optional | +|---|---|---| +| **Library use** (header-only, end-user code includes our headers) | C++17 compiler, CMake ≥ 3.20, Eigen ≥ 3.4 (headers), CGAL ≥ 5.6 (headers), Boost ≥ 1.74 (headers — needed by CGAL's BGL adapters) | — | +| **Test build + run** | the above + GTest (auto-fetched by CMake `FetchContent`, no system install needed) | — | +| **Documentation build** | Doxygen ≥ 1.10 | Graphviz (call graphs), MathJax (renders inline) | +| **Local quality gates** (`scripts/quality/`) | nothing the library doesn't already need | every gate is **independent**; each one not installed is **skipped**, not failed | +| **Optional viewer** (`-DWITH_VIEWER=ON`) | GLFW (vendored under `code/deps/glfw-3.4`), libigl, OpenGL system headers | — | + +The library itself is **header-only**. There is no compiled `.so` / +`.a` / `.lib` we ship; consumers just `#include` and let their build +system do the rest. + +## Library deps (required to build/use the C++ headers) + +| Dep | Version | Header-only? | Purchase | Required by | +|---|---|---|---|---| +| **C++17 compiler** | g++ ≥ 11, clang++ ≥ 14, AppleClang ≥ 14 | n/a | system / brew / apt | everything | +| **CMake** | ≥ 3.20 | n/a | system / brew / apt | build orchestration | +| **Eigen** | ≥ 3.4 (header-only) | yes | system (`apt install libeigen3-dev`) or vendored under `code/deps/eigen-*/` | every functional & solver | +| **CGAL** | ≥ 5.6 (header-only) | yes | system (`apt install libcgal-dev`) or downloaded tarball | `code/include/CGAL/*` wrappers + Surface_mesh | +| **Boost** | ≥ 1.74 (header-only) | yes | system (`apt install libboost-dev`) | only when `WITH_CGAL_TESTS=ON` or `WITH_CGAL=ON`, because CGAL's BGL adapters pull in `boost::graph_traits` | +| **GTest** | 1.14 | yes (auto-fetched) | `FetchContent_Declare` in `code/CMakeLists.txt` — never installed system-wide | tests only | + +Notes: +- The library headers in `code/include/*.hpp` use only Eigen + STL. +- The CGAL wrapper headers in `code/include/CGAL/*.h` add CGAL + Boost + (transitively). +- `code/deps/single_includes/json.hpp` is the vendored + [nlohmann/json](https://github.com/nlohmann/json) header — used by + `serialization.hpp` only. No system install needed. + +## Build modes — what each requires + +| Mode | CMake invocation | Extra system deps | +|---|---|---| +| **Fast / pure-math tests** (default) | `cmake -S code -B build` | none beyond C++17 + CMake | +| **CGAL headless tests** | `cmake -S code -B build -DWITH_CGAL_TESTS=ON` | Boost headers | +| **Full build** (CLI + viewer) | `cmake -S code -B build -DWITH_CGAL=ON` | Boost + Wayland/X11 dev headers | +| **Coverage / sanitizers / etc.** | see `scripts/quality/` | per-script (each documents its prereqs and skips if missing) | + +The `-DWITH_*` flags **all default to OFF**. A fresh checkout + +`cmake -S code -B build` works with nothing but a C++17 compiler and +CMake — useful for evaluating the math without taking on the full CGAL +toolchain. + +## Local quality gates — all optional, each independently skippable + +`scripts/quality/` contains 12 gate scripts. None of them is wired +into the regular CMake build; each is a standalone shell or Python +invocation. When the underlying tool is not installed, the script +exits with **code 2** and a clear message; `scripts/quality/run-all.sh` +recognises this as **SKIP**, not FAIL. + +| Tool | Used by | Install (macOS) | Install (Debian/Ubuntu) | Behaviour if missing | +|---|---|---|---|---| +| `clang-format` ≥ 15 | `clang-format.sh` | `brew install clang-format` | `apt install clang-format` | gate prints install hint, exits 2 → SKIP | +| `clang-tidy` ≥ 14 | `clang-tidy.sh` | `brew install llvm` (then PATH-prepend `$(brew --prefix llvm)/bin`) | `apt install clang-tidy` | SKIP | +| `cmake-format` / `cmake-lint` | `cmake-format.sh` | `pip3 install --user cmakelang` + PATH-prepend `~/.local/bin` | `pip3 install --user cmakelang` | SKIP | +| `codespell` | `codespell.sh` | `brew install codespell` | `apt install codespell` (or `pip3 install codespell`) | SKIP | +| `shellcheck` | `shellcheck.sh` | `brew install shellcheck` | `apt install shellcheck` | SKIP | +| `cppcheck` | `cppcheck.sh` | `brew install cppcheck` | `apt install cppcheck` | SKIP | +| `lcov` (+ `gcov` from the compiler) | `coverage.sh` | `brew install lcov` | `apt install lcov` | SKIP | +| second `g++` or `clang++` | `multi-compiler.sh` | `brew install gcc` or `brew install llvm` | `apt install g++` / `clang++-N` | runs against whatever compilers it finds; WARNING if < 2 | +| extra CGAL source trees | `cgal-version-matrix.sh` | manually `git clone` under `~/cgal//` (or pass `CGAL_ROOTS=...`) | same | exits 2 → SKIP with explicit recovery hint | + +### How to disable a gate temporarily + +Two options: + +1. **Don't install the tool** — `run-all.sh` skips it. +2. **Remove the line from `GATES_FAST` / `GATES_SLOW` in `run-all.sh`** — + the script is a 5-line edit; no separate "disabled" flag system. + +There is no global "disable all quality gates" switch by design. If +the gates feel heavy, run only the fast subset (`run-all.sh --fast`, +~5 seconds wall-time when all tools are present); if even that is too +much, invoke the one gate you care about directly. + +### `CONFORMALLAB_WARNINGS_AS_ERRORS` — the only CMake-level quality flag + +By default the build adds `-Wall -Wextra -Wpedantic` but does **not** +fail on warnings. Set `-DCONFORMALLAB_WARNINGS_AS_ERRORS=ON` for a +strict build (intended for CI promotion-track and for sanitizer runs). +Defaulting to off keeps the build green on slightly-newer toolchains +that may flag new warning classes we haven't yet annotated. + +## CI gates (active on every PR via `.gitea/workflows/`) + +These run inside the `git.eulernest.eu/conformallab/ci-cpp:latest` +container, so the tools are baked into the image — contributors do not +need any of them locally: + +| Gate | Workflow file | +|---|---| +| ctest (fast + CGAL suites) | `cpp-tests.yml` | +| test-count consistency | same | +| End-to-end `try_it.sh` | same | +| Markdown link check | `markdown-links.yml` | +| Doxygen build + Codeberg Pages publish | `doxygen-pages.yml` | +| Mirror to Codeberg | `mirror-to-codeberg.yml` | + +The local quality gates under `scripts/quality/` are **not** in CI +today. Each one's promotion path is documented in +`scripts/quality/README.md`. + +## Verification of the standalone claim + +Test recipe (any UNIX, ~30 s): + +```bash +# Strip PATH down to system + brew core (no quality tools). +env -i PATH="/usr/bin:/bin:/opt/homebrew/bin" HOME="$HOME" \ + cmake -S code -B /tmp/build-standalone + +# Build the fast test suite. +cmake --build /tmp/build-standalone --target conformallab_tests + +# Run them. +ctest --test-dir /tmp/build-standalone -E "^cgal\." +``` + +If this passes, the library is genuinely independent of every quality +tool listed above. Tested locally on macOS-arm64 — green. + +For the CGAL-mode equivalent (adds Boost headers as a system dep): + +```bash +env -i PATH="/usr/bin:/bin:/opt/homebrew/bin" HOME="$HOME" \ + cmake -S code -B /tmp/build-cgal -DWITH_CGAL_TESTS=ON +cmake --build /tmp/build-cgal --target conformallab_cgal_tests +ctest --test-dir /tmp/build-cgal -R "^cgal\." +``` + +## What is **not** in this repo (out of scope) + +- No package-manager metadata (Debian `.deb`, RPM, Conan, vcpkg, …) — + yet. Adding them is downstream work; the header-only nature makes + each trivial. +- No language bindings (Python, …) — out of scope; the library is C++. +- No GPU compute path — out of scope; numerical work is CPU-only. diff --git a/scripts/quality/README.md b/scripts/quality/README.md index 5fe0fb8..12ad35e 100644 --- a/scripts/quality/README.md +++ b/scripts/quality/README.md @@ -23,6 +23,8 @@ consistency + markdown links + end-to-end smoke via `try_it.sh`). | `clang-format.sh` | every C++ source matches `.clang-format` (dry-run by default; `--fix` to apply) | ~2 s | `clang-format` ≥ 15 | | `cmake-format.sh` | every `CMakeLists.txt` matches `.cmake-format.yaml` + passes `cmake-lint` | ~2 s | `cmake-format` (pip: cmakelang) | | `codespell.sh` | typo check across docs + source comments + script messages | ~1 s | `codespell` | +| `shellcheck.sh` | static analysis of every `scripts/**/*.sh` | ~1 s | `shellcheck` | +| `cppcheck.sh` | second-opinion static analyser over `code/include/` | ~5 s | `cppcheck` | | `../check-markdown-links.py` | every internal markdown link resolves | ~2 s | `python3` | ### Correctness / quality gates (run before tagging or reviewer demos) diff --git a/scripts/quality/cgal-version-matrix.sh b/scripts/quality/cgal-version-matrix.sh index 5e215f3..55e4ef7 100755 --- a/scripts/quality/cgal-version-matrix.sh +++ b/scripts/quality/cgal-version-matrix.sh @@ -73,7 +73,11 @@ echo " versions tested:" echo "$AVAILABLE" | sed 's/^/ /' echo "========================================" -overall=0 +# Failures are recorded in $ROOT/.cgal-matrix-failures because the +# while-loop runs in a subshell (consequence of the pipe from echo), so +# a plain `overall=0; overall=1` would not survive back to the parent. +rm -f "$ROOT/.cgal-matrix-failures" + echo "$AVAILABLE" | while IFS= read -r cgal_root; do [ -z "$cgal_root" ] && continue ver="$(basename "$cgal_root")" diff --git a/scripts/quality/clang-format.sh b/scripts/quality/clang-format.sh index 4d3d58d..a515a30 100755 --- a/scripts/quality/clang-format.sh +++ b/scripts/quality/clang-format.sh @@ -23,7 +23,7 @@ set -uo pipefail ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" -cd "$ROOT" +cd "$ROOT" || exit 2 command -v clang-format >/dev/null 2>&1 || { echo "FAIL: clang-format not in PATH." >&2 diff --git a/scripts/quality/clang-tidy.sh b/scripts/quality/clang-tidy.sh index 584ecdf..1e1ba39 100755 --- a/scripts/quality/clang-tidy.sh +++ b/scripts/quality/clang-tidy.sh @@ -40,20 +40,39 @@ TARGET_DIR="${1:-code/include}" [ -d "$TARGET_DIR" ] || { echo "FAIL: $TARGET_DIR is not a directory" >&2; exit 2; } # Generate compile_commands.json (clang-tidy needs it for include paths). +# Enable WITH_CGAL_TESTS so the CGAL include directories are part of at +# least one compile entry — clang-tidy walks those when linting headers +# that don't appear in compile_commands.json directly. cmake -S code -B "$BUILD_DIR" \ -DCMAKE_EXPORT_COMPILE_COMMANDS=ON \ + -DWITH_CGAL_TESTS=ON \ -DCMAKE_BUILD_TYPE=Release \ -Wno-dev >/dev/null +# ── macOS workaround: brew-installed clang-tidy doesn't know where the +# Apple Command-Line-Tools SDK lives, so it can't find , , +# , etc. Pass `--extra-arg=-isysroot ...` to teach it. +EXTRA_ARGS=() +if [ "$(uname -s)" = "Darwin" ]; then + SDK="$(xcrun --show-sdk-path 2>/dev/null || true)" + if [ -n "$SDK" ]; then + EXTRA_ARGS+=(--extra-arg=-isysroot --extra-arg="$SDK") + fi +fi + mkdir -p "$BUILD_DIR" : > "$LOG" -# Find every .h / .hpp under TARGET_DIR (skip deps + macOS dup files). +# Find every .h / .hpp under TARGET_DIR (skip deps + macOS dup files + +# viewer-only headers — those need `-DWITH_VIEWER=ON` plus a system +# GLFW/libigl that we don't drag into the lint build). HEADERS=$(find "$TARGET_DIR" \ \( -name "*.h" -o -name "*.hpp" \) \ -type f \ | grep -v "code/deps/" \ | grep -v " 2\." \ + | grep -v "viewer_utils\.h$" \ + | grep -v "mesh_utils\.hpp$" \ | sort) echo "========================================" @@ -72,6 +91,7 @@ for h in $HEADERS; do # generated" boilerplate. Pipe through tee for the log file. clang-tidy --quiet \ -p "$BUILD_DIR" \ + "${EXTRA_ARGS[@]}" \ "$h" 2>&1 | tee -a "$LOG" || true done diff --git a/scripts/quality/cmake-format.sh b/scripts/quality/cmake-format.sh index 9a1cbbf..496848a 100755 --- a/scripts/quality/cmake-format.sh +++ b/scripts/quality/cmake-format.sh @@ -19,7 +19,7 @@ set -uo pipefail ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" -cd "$ROOT" +cd "$ROOT" || exit 2 # Allow ~/.local/bin (pip-installed tools) in PATH. export PATH="$HOME/.local/bin:$PATH" diff --git a/scripts/quality/codespell.sh b/scripts/quality/codespell.sh index 3e127af..ec07327 100755 --- a/scripts/quality/codespell.sh +++ b/scripts/quality/codespell.sh @@ -24,7 +24,7 @@ set -uo pipefail ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" -cd "$ROOT" +cd "$ROOT" || exit 2 command -v codespell >/dev/null 2>&1 || { echo "FAIL: codespell not in PATH." >&2 diff --git a/scripts/quality/cppcheck.sh b/scripts/quality/cppcheck.sh new file mode 100755 index 0000000..c584c50 --- /dev/null +++ b/scripts/quality/cppcheck.sh @@ -0,0 +1,97 @@ +#!/usr/bin/env bash +# scripts/quality/cppcheck.sh +# +# Run cppcheck over the public headers. Complementary to clang-tidy: +# cppcheck has different heuristics, fewer false-positives on heavy +# template code (CGAL/Eigen), and catches some bugs (unused includes, +# memory leaks in detail/) that clang-tidy is bad at. +# +# Local-only. Promotion to CI when the existing tree is finding-free +# at the chosen severity level. +# +# Usage: +# bash scripts/quality/cppcheck.sh # error+warning only +# bash scripts/quality/cppcheck.sh --strict # +style, exit 1 on any +# bash scripts/quality/cppcheck.sh --all # absolute everything, +# useful for diffs only +# +# Exit codes: +# 0 no findings at the chosen severity, or findings but not --strict +# 1 findings + --strict +# 2 prerequisite missing + +set -uo pipefail +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +cd "$ROOT" || exit 2 + +command -v cppcheck >/dev/null 2>&1 || { + echo "FAIL: cppcheck not in PATH." >&2 + echo " macOS: brew install cppcheck" >&2 + echo " Linux: sudo apt install cppcheck" >&2 + exit 2 +} + +STRICT=0 +ALL=0 +for arg in "$@"; do + case "$arg" in + --strict) STRICT=1 ;; + --all) ALL=1 ;; + *) echo "Unknown arg: $arg" >&2; exit 2 ;; + esac +done + +ENABLE="warning" +if [ "$STRICT" -eq 1 ]; then ENABLE="warning,style"; fi +if [ "$ALL" -eq 1 ]; then ENABLE="all"; fi + +BUILD_DIR="build-cppcheck" +LOG="$BUILD_DIR/cppcheck.log" +mkdir -p "$BUILD_DIR" + +echo "cppcheck ($(cppcheck --version 2>&1 | head -1))" +echo " enable: $ENABLE" +echo " log: $LOG" +echo + +# Suppress noise classes that are not actionable in our project: +# missingIncludeSystem — CGAL/Eigen/Boost headers are intentionally +# included implicitly; cppcheck cannot resolve. +# unmatchedSuppression — cosmetic. +# unusedFunction — header-only; many `inline` helpers ARE used, +# cppcheck can't see across TUs. +# normalCheckLevelMaxBranches — informational, not a finding. +# +# We point cppcheck at code/include/ only. The deps tree is third-party +# code and out of scope. + +cppcheck \ + --enable="$ENABLE" \ + --std=c++17 \ + --quiet \ + --error-exitcode=2 \ + --inline-suppr \ + --suppress=missingIncludeSystem \ + --suppress=unmatchedSuppression \ + --suppress=unusedFunction \ + --suppress=normalCheckLevelMaxBranches \ + -I code/include \ + code/include 2>&1 | tee "$LOG" +rc=$? + +echo +echo "── Summary ──" +n=$(grep -cE "\[(error|warning|style|performance|portability)\]" "$LOG" || true) +echo " total findings: $n" +echo " full log: $LOG" + +if [ "$STRICT" -eq 1 ] && [ "$n" -gt 0 ]; then + exit 1 +fi +if [ "$rc" -eq 2 ] && [ "$STRICT" -ne 1 ]; then + # cppcheck signalled "error" severity but caller didn't ask --strict. + echo + echo "NOTE: cppcheck reported an `error`-severity finding. Even" + echo " without --strict, please review the log." +fi +exit 0 diff --git a/scripts/quality/run-all.sh b/scripts/quality/run-all.sh index 22d2d24..ebc2691 100755 --- a/scripts/quality/run-all.sh +++ b/scripts/quality/run-all.sh @@ -28,7 +28,7 @@ set -uo pipefail ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" -cd "$ROOT" +cd "$ROOT" || exit 2 FAST=0 [ "${1:-}" = "--fast" ] && FAST=1 @@ -39,6 +39,8 @@ GATES_FAST=( "clang-format drift | bash scripts/quality/clang-format.sh" "cmake-format/-lint | bash scripts/quality/cmake-format.sh" "codespell | bash scripts/quality/codespell.sh" + "shellcheck | bash scripts/quality/shellcheck.sh" + "cppcheck | bash scripts/quality/cppcheck.sh" "Markdown links | python3 scripts/check-markdown-links.py" "Sanitizers | bash scripts/quality/sanitizers.sh" "clang-tidy | bash scripts/quality/clang-tidy.sh" @@ -67,6 +69,7 @@ echo "============================================================" results="" failed=0 +skipped=0 i=0 for entry in "${GATES[@]}"; do i=$((i + 1)) @@ -79,12 +82,21 @@ for entry in "${GATES[@]}"; do log="$LOG_DIR/$slug.log" echo echo "──── [$i/${#GATES[@]}] $name ────" - if eval "$cmd" >"$log" 2>&1; then + eval "$cmd" >"$log" 2>&1 + rc=$? + # Exit code 2 from any of our gate scripts = "tool not installed". + # Treat as SKIP rather than FAIL so a partial dev environment can + # still run the rest of the sweep. + if [ "$rc" -eq 2 ] && head -3 "$log" | grep -qE "FAIL:.*not (in PATH|installed|found)"; then + echo " SKIP (tool not installed — see $log)" + results="${results} SKIP $name (missing tool) +" + skipped=$((skipped + 1)) + elif [ "$rc" -eq 0 ]; then echo " OK ($log)" results="${results} PASS $name " else - rc=$? echo " FAIL (rc=$rc) — see $log" echo " last 20 lines:" tail -20 "$log" | sed 's/^/ /' @@ -100,5 +112,7 @@ echo " Summary" echo "============================================================" printf "%s" "$results" echo -echo " failed: $failed / ${#GATES[@]}" +echo " passed: $((${#GATES[@]} - failed - skipped)) / ${#GATES[@]}" +echo " skipped: $skipped (tool not installed; gate is local-only)" +echo " failed: $failed" exit $failed diff --git a/scripts/quality/sanitizers.sh b/scripts/quality/sanitizers.sh index 7334bc5..4a44559 100755 --- a/scripts/quality/sanitizers.sh +++ b/scripts/quality/sanitizers.sh @@ -57,11 +57,18 @@ echo "Using CXX = $CXX_BIN ($("$CXX_BIN" --version | head -1))" echo # ── Configure ──────────────────────────────────────────────────────────────── +# CMAKE_GTEST_DISCOVER_TESTS_DISCOVERY_MODE=PRE_TEST: without this, +# gtest_discover_tests runs the (sanitizer-instrumented) test binary at +# *build* time to enumerate test cases. ASan aborts that subprocess +# the moment it sees any allocation in static-init, which fails the +# build before we can even get to ctest. PRE_TEST defers discovery to +# `ctest` invocation, which is exactly what we want. cmake -S code -B "$BUILD_DIR" \ -DCMAKE_CXX_COMPILER="$CXX_BIN" \ -DCMAKE_CXX_FLAGS="$SAN_FLAGS" \ -DCMAKE_EXE_LINKER_FLAGS="$SAN_FLAGS" \ -DCMAKE_BUILD_TYPE=Debug \ + -DCMAKE_GTEST_DISCOVER_TESTS_DISCOVERY_MODE=PRE_TEST \ -Wno-dev # ── Build the fast (non-CGAL) tests only ──────────────────────────────────── diff --git a/scripts/quality/shellcheck.sh b/scripts/quality/shellcheck.sh new file mode 100755 index 0000000..19bec95 --- /dev/null +++ b/scripts/quality/shellcheck.sh @@ -0,0 +1,79 @@ +#!/usr/bin/env bash +# scripts/quality/shellcheck.sh +# +# Run shellcheck across every Bash script we own (scripts/**/*.sh). +# Skips the macOS duplicate artefacts (` 2.sh`). +# +# Local-only. CI promotion once every script is shellcheck-clean. +# +# Usage: +# bash scripts/quality/shellcheck.sh # warn + advisory exit 0 +# bash scripts/quality/shellcheck.sh --strict # fail on any finding +# +# Exit codes: +# 0 no findings, or findings but --strict not set +# 1 --strict was set and shellcheck reported findings +# 2 prerequisite missing + +set -uo pipefail +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +cd "$ROOT" || exit 2 + +command -v shellcheck >/dev/null 2>&1 || { + echo "FAIL: shellcheck not in PATH." >&2 + echo " macOS: brew install shellcheck" >&2 + echo " Linux: sudo apt install shellcheck" >&2 + exit 2 +} + +STRICT=0 +for arg in "$@"; do + case "$arg" in + --strict) STRICT=1 ;; + *) echo "Unknown arg: $arg" >&2; exit 2 ;; + esac +done + +FILES="$(find scripts -name "*.sh" -type f 2>/dev/null \ + | grep -v " 2\.sh" \ + | sort)" + +if [ -z "$FILES" ]; then + echo "FAIL: no shell scripts found under scripts/" >&2 + exit 2 +fi + +echo "shellcheck ($(shellcheck --version | sed -n '2p'))" +echo "Scanning shell scripts under scripts/" +echo + +n_total=0 +n_with_findings=0 +total_findings=0 +while IFS= read -r f; do + [ -z "$f" ] && continue + n_total=$((n_total + 1)) + # -S style: warnings + above (skip "info" and "style" noise). + out="$(shellcheck --severity=warning --shell=bash "$f" 2>&1)" + if [ -n "$out" ]; then + echo "── $f ──" + echo "$out" + echo + n_with_findings=$((n_with_findings + 1)) + # rough count: one finding per "In line N:" block + cnt=$(printf '%s' "$out" | grep -c "^In .* line") + total_findings=$((total_findings + cnt)) + fi +done <