From d84810efc80fd0e4a2c9bbc158b29b41cc3fd370 Mon Sep 17 00:00:00 2001 From: Tarik Moussa Date: Sun, 24 May 2026 20:07:39 +0200 Subject: [PATCH] chore: remove macOS Finder duplicate files accidentally committed in 7b097fb These files (e.g. 'scripts/quality/codespell 2.sh') are macOS Finder duplicates that appear when files are read while being moved/copied. They are caught by .cmake-format.yaml's exclusion and by license-headers.sh / clang-format.sh's grep filters, but slipped through the explicit add in the previous commit. The genuine files (without the ' 2' suffix) are unaffected. Co-Authored-By: Claude Opus 4.7 --- scripts/quality/README 2.md | 88 ---------- scripts/quality/cgal-conventions 2.py | 207 ----------------------- scripts/quality/cgal-version-matrix 2.sh | 127 -------------- scripts/quality/clang-format 2.sh | 96 ----------- scripts/quality/clang-tidy 2.sh | 110 ------------ scripts/quality/cmake-format 2.sh | 126 -------------- scripts/quality/codespell 2.sh | 87 ---------- scripts/quality/coverage 2.sh | 128 -------------- scripts/quality/cppcheck 2.sh | 97 ----------- scripts/quality/license-headers 2.sh | 133 --------------- scripts/quality/multi-compiler 2.sh | 119 ------------- scripts/quality/reproducible-build 2.sh | 117 ------------- scripts/quality/run-all 2.sh | 119 ------------- scripts/quality/sanitizers 2.sh | 95 ----------- scripts/quality/shellcheck 2.sh | 79 --------- 15 files changed, 1728 deletions(-) delete mode 100644 scripts/quality/README 2.md delete mode 100755 scripts/quality/cgal-conventions 2.py delete mode 100755 scripts/quality/cgal-version-matrix 2.sh delete mode 100755 scripts/quality/clang-format 2.sh delete mode 100755 scripts/quality/clang-tidy 2.sh delete mode 100755 scripts/quality/cmake-format 2.sh delete mode 100755 scripts/quality/codespell 2.sh delete mode 100755 scripts/quality/coverage 2.sh delete mode 100755 scripts/quality/cppcheck 2.sh delete mode 100755 scripts/quality/license-headers 2.sh delete mode 100755 scripts/quality/multi-compiler 2.sh delete mode 100755 scripts/quality/reproducible-build 2.sh delete mode 100755 scripts/quality/run-all 2.sh delete mode 100755 scripts/quality/sanitizers 2.sh delete mode 100755 scripts/quality/shellcheck 2.sh diff --git a/scripts/quality/README 2.md b/scripts/quality/README 2.md deleted file mode 100644 index 12ad35e..0000000 --- a/scripts/quality/README 2.md +++ /dev/null @@ -1,88 +0,0 @@ -# Local structural quality gates - -This directory contains the structural quality checks that run **locally** -rather than in CI. They are intentionally not wired into -`.gitea/workflows/` (yet) because each is either too slow, too brittle -against the runner environment, or both — running them before a release -or before showing the repo to an external reviewer is the intended -workflow. - -The CI gates that *are* enforced live in `.gitea/workflows/cpp-tests.yml` -and `.gitea/workflows/doxygen-pages.yml`; they cover the day-to-day -correctness loop (build + test + doxygen-coverage + test-count -consistency + markdown links + end-to-end smoke via `try_it.sh`). - -## What runs locally - -### Style / convention gates (run on every commit; cheap) - -| Script | What it checks | Wall time | Prereqs | -|---|---|---|---| -| `license-headers.sh` | every C++ source carries `SPDX-License-Identifier: MIT` | ~1 s | `bash` | -| `cgal-conventions.py` | CGAL-1…6: include-guard format, `\file` brief, namespace nesting, tag-naming, no `using namespace`, no stray `#define` | ~1 s | `python3` | -| `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) - -| Script | What it checks | Wall time | Prereqs | -|---|---|---|---| -| `sanitizers.sh` | fast test suite under ASan + UBSan | ~3 min | `clang++` ≥ 14 or `g++` ≥ 11 | -| `coverage.sh` | gcov/lcov line + branch coverage of `code/include/` | ~2 min | `lcov` | -| `clang-tidy.sh` | curated clang-tidy checks over public headers | ~2 min | `clang-tidy` ≥ 14, `.clang-tidy` | -| `multi-compiler.sh` | build + test under every detected gcc/clang | ~5 min × N compilers | any 2 of `g++`, `clang++` | -| `reproducible-build.sh` | two builds → byte-identical test executables | ~6 min | none beyond compiler | -| `cgal-version-matrix.sh` | build + CGAL test suite against multiple CGAL versions | ~5 min × N versions | CGAL trees under `~/cgal//` (or `CGAL_ROOTS=...`) | - -## How to use - -```bash -# Fast subset (license + links + sanitizers + clang-tidy) — ~5 min total -bash scripts/quality/run-all.sh --fast - -# Full sweep — ~25–40 min, intended for pre-release tagging -bash scripts/quality/run-all.sh - -# One specific gate -bash scripts/quality/sanitizers.sh -``` - -Every gate writes its full output to `build-quality-logs/.log` -when invoked via `run-all.sh`, and to its own per-gate build directory -(`build-sanitizers/`, `build-coverage/`, `build-multi-/`, …) when -invoked directly. - -## Promotion path to CI - -Each gate can be wired into `.gitea/workflows/cpp-tests.yml` once two -conditions are met: - -1. **The gate is green on the canonical dev machine.** If the script - exits 1 today, the CI gate would block every PR. -2. **There is a published policy line in `doc/release-policy.md`** that - explains what regression the gate catches and what the recovery is. - Future contributors should be able to read the error and know what - to fix. - -Promoting a gate is a one-line change to `cpp-tests.yml`; the test -recipe is the script invocation itself. - -## Known limitations - -- `cgal-version-matrix.sh` does not download CGAL. Each version must - already be on the dev machine under `~/cgal//` (override with - `CGAL_ROOTS=...:...`). The Dockerfile under - `.gitea/docker/Dockerfile.ci-cpp` could be extended to ship multiple - CGAL trees in a single image; not done yet. -- `sanitizers.sh` only instruments the fast (non-CGAL) test suite — - the CGAL templates are too expensive to compile under instrumentation - on most laptops. -- `clang-tidy.sh` requires a `.clang-tidy` config in the repo root; the - default Anthropic-quality lint set is intentionally minimal until the - reviewer signs off on the warning policy. -- `reproducible-build.sh` checks the test executables only. The - library is header-only, so there is nothing else to compare. diff --git a/scripts/quality/cgal-conventions 2.py b/scripts/quality/cgal-conventions 2.py deleted file mode 100755 index 4ae1f4d..0000000 --- a/scripts/quality/cgal-conventions 2.py +++ /dev/null @@ -1,207 +0,0 @@ -#!/usr/bin/env python3 -"""scripts/quality/cgal-conventions.py - -Lightweight checker for the CGAL-style conventions that conformallab++ -aims to follow, based on the -"CGAL Developer's Manual / Package Submission Checklist". These rules -are NOT covered by clang-format or clang-tidy; they are project-/CGAL- -specific, so we encode them here as pure-Python AST/regex checks. - -Rules enforced (each can be silenced per-file via a comment marker — -see RULES dict below): - - CGAL-1 Include guard format: every CGAL/* header has a guard of the - form `CGAL___H` matching its repo path. - CGAL-2 Every public header has a `\\file` Doxygen brief in its top - comment block. - CGAL-3 Public CGAL API symbols (functions, classes, structs) live - directly in `namespace CGAL { ... }`, not in nested namespaces - that the user must qualify (except `CGAL::parameters`, - `CGAL::Conformal_map::internal_np`). - CGAL-4 Named-parameter tag types end in `_t`; the matching value - object does not (e.g. `vertex_curvature_map_t` / - `vertex_curvature_map`). - CGAL-5 No `using namespace ...` at file scope in public headers - (would leak into every translation unit that includes us). - CGAL-6 No `#define` (other than include-guard, header-marker, or - CGAL_*) in public headers — macros leak unconditionally. - -The checker only inspects `code/include/CGAL/`. Conformallab's own -`conformallab::` namespace under `code/include/*.hpp` is non-CGAL-public -and uses its own (looser) conventions. - -Exit codes: - 0 every rule passes - 1 at least one violation was found - 2 prerequisite missing -""" -from __future__ import annotations -import os, re, sys - -ROOT = os.path.abspath(os.path.dirname(os.path.dirname(os.path.dirname(__file__)))) -CGAL_DIR = os.path.join(ROOT, "code", "include", "CGAL") - -if not os.path.isdir(CGAL_DIR): - print(f"FAIL: {CGAL_DIR} not found", file=sys.stderr) - sys.exit(2) - -# Namespaces that are *intentionally* nested under CGAL. -ALLOWED_NESTED = { - "CGAL::parameters", - "CGAL::Conformal_map", - "CGAL::Conformal_map::internal_np", - "CGAL::internal_np", # CGAL upstream - "CGAL::IO", # CGAL upstream -} - -# Macros that may appear in public headers. -ALLOWED_DEFINE_RE = re.compile(r"^\s*#\s*define\s+(CGAL_[A-Z0-9_]+|[A-Z0-9_]+_H)\b") - - -def find_headers() -> list[str]: - out: list[str] = [] - for dirpath, dirs, files in os.walk(CGAL_DIR): - for f in files: - if not (f.endswith(".h") or f.endswith(".hpp")): - continue - if " 2." in f: # macOS duplicate artefact - continue - out.append(os.path.join(dirpath, f)) - return sorted(out) - - -def expected_guard(path: str) -> str: - """The expected include-guard symbol for the given header path.""" - rel = os.path.relpath(path, ROOT) # e.g. code/include/CGAL/Discrete_conformal_map.h - # Strip the `code/include/` prefix to match CGAL upstream practice. - if rel.startswith("code/include/"): - rel = rel[len("code/include/"):] - # CGAL/Discrete_conformal_map.h → CGAL_DISCRETE_CONFORMAL_MAP_H - stem = rel.replace("/", "_").replace(".", "_") - return stem.upper() - - -def check_rule_1_include_guard(text: str, path: str) -> list[str]: - expected = expected_guard(path) - if f"#ifndef {expected}" not in text: - # Find what guard was actually used, for a helpful message. - m = re.search(r"^\s*#ifndef\s+(\w+)", text, re.MULTILINE) - actual = m.group(1) if m else "" - return [f"CGAL-1: include guard is `{actual}`, expected `{expected}`"] - return [] - - -def check_rule_2_file_brief(text: str, path: str) -> list[str]: - # \file or @file must appear somewhere in the first 40 lines. - head = "\n".join(text.splitlines()[:40]) - if re.search(r"[\\@]file\b", head): - return [] - return [f"CGAL-2: no `\\file` brief in top-of-file comment block"] - - -_NAMESPACE_RE = re.compile(r"^\s*namespace\s+(\w+)\s*\{", re.MULTILINE) - - -def check_rule_3_nested_namespace(text: str, path: str) -> list[str]: - # Walk top-level namespace declarations. We approximate with regex - # (not a real AST) — good enough for the CGAL header style. - bad = [] - stack: list[str] = [] - for line_no, line in enumerate(text.splitlines(), start=1): - m = re.match(r"^\s*namespace\s+(\w+)\s*\{?\s*$", line) - if m: - stack.append(m.group(1)) - full = "::".join(stack) - if len(stack) >= 2 and stack[0] == "CGAL": - if full not in ALLOWED_NESTED and not full.startswith("CGAL::internal"): - bad.append(f"CGAL-3: nested namespace `{full}` at line {line_no}") - elif re.match(r"^\s*\}\s*//\s*namespace\b", line) or re.match(r"^\s*\}\s*//\s*\w+", line): - if stack: - stack.pop() - return bad - - -_TAG_ENUM_RE = re.compile( - r"^\s*enum\s+(\w+)_t\s*\{\s*(\w+)\s*\}\s*;", re.MULTILINE -) - - -def check_rule_4_tag_naming(text: str, path: str) -> list[str]: - bad = [] - for m in _TAG_ENUM_RE.finditer(text): - tag_t = m.group(1) # e.g. "vertex_curvature_map" - value = m.group(2) # e.g. "vertex_curvature_map" - if value != tag_t: - bad.append( - f"CGAL-4: tag `{tag_t}_t` enclosing value `{value}` " - f"(expected `{tag_t}`)" - ) - return bad - - -def check_rule_5_using_namespace(text: str, path: str) -> list[str]: - bad = [] - for line_no, line in enumerate(text.splitlines(), start=1): - if re.match(r"^\s*using\s+namespace\s+\w", line): - bad.append(f"CGAL-5: `using namespace ...` at line {line_no} " - "(leaks into every TU that includes this header)") - return bad - - -def check_rule_6_defines(text: str, path: str) -> list[str]: - bad = [] - for line_no, line in enumerate(text.splitlines(), start=1): - if re.match(r"^\s*#\s*define\s+", line) and not ALLOWED_DEFINE_RE.match(line): - bad.append(f"CGAL-6: `{line.strip()}` at line {line_no} " - "(only CGAL_* or include-guard macros allowed)") - return bad - - -CHECKS = [ - check_rule_1_include_guard, - check_rule_2_file_brief, - check_rule_3_nested_namespace, - check_rule_4_tag_naming, - check_rule_5_using_namespace, - check_rule_6_defines, -] - - -def main() -> int: - headers = find_headers() - if not headers: - print("FAIL: no headers under code/include/CGAL/", file=sys.stderr) - return 2 - total_violations = 0 - files_with_issues = 0 - for h in headers: - try: - with open(h, encoding="utf-8") as f: - text = f.read() - except OSError as e: - print(f"WARN: cannot read {h}: {e}", file=sys.stderr) - continue - violations: list[str] = [] - for check in CHECKS: - violations.extend(check(text, h)) - if violations: - files_with_issues += 1 - total_violations += len(violations) - rel = os.path.relpath(h, ROOT) - print(f"\n{rel}:") - for v in violations: - print(f" - {v}") - - print() - print("─" * 60) - print(f"Checked {len(headers)} CGAL headers.") - print(f" files with issues: {files_with_issues}") - print(f" total violations: {total_violations}") - if total_violations == 0: - print("OK: every CGAL convention rule passes.") - return 0 - return 1 - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/scripts/quality/cgal-version-matrix 2.sh b/scripts/quality/cgal-version-matrix 2.sh deleted file mode 100755 index 55e4ef7..0000000 --- a/scripts/quality/cgal-version-matrix 2.sh +++ /dev/null @@ -1,127 +0,0 @@ -#!/usr/bin/env bash -# scripts/quality/cgal-version-matrix.sh -# -# Build and test the CGAL test suite against multiple CGAL versions in -# sequence. Catches: -# * upstream API drift between minor CGAL releases (5.x vs 6.x) -# * Surface_mesh / Polyhedron_3 trait-class changes -# * deprecated CGAL macros we still rely on -# -# Local-only. The script expects each CGAL version to live under -# `~/cgal//` (override with CGAL_ROOTS env var as a colon-list). -# If the directory tree is missing it prints the expected layout and -# exits 2 — it does not download anything (that would belong in a Docker -# image, see .gitea/docker/Dockerfile.ci-cpp). -# -# Usage: -# bash scripts/quality/cgal-version-matrix.sh -# CGAL_ROOTS=/opt/cgal-5.6:/opt/cgal-6.0 bash scripts/quality/cgal-version-matrix.sh -# -# Exit codes: -# 0 every requested CGAL version builds + tests cleanly -# 1 at least one version failed -# 2 no CGAL roots found - -set -euo pipefail -ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" -cd "$ROOT" - -# ── Discover CGAL installs ────────────────────────────────────────────────── -DEFAULT_ROOTS="$HOME/cgal/5.6:$HOME/cgal/6.0:$HOME/cgal/6.1:/opt/cgal-5.6:/opt/cgal-6.0" -ROOTS="${CGAL_ROOTS:-$DEFAULT_ROOTS}" - -# Collect existing paths. -AVAILABLE="" -OLD_IFS="$IFS" -IFS=":" -for r in $ROOTS; do - if [ -f "$r/cmake/modules/UseCGAL.cmake" ] || [ -f "$r/include/CGAL/version.h" ] || [ -f "$r/CMakeLists.txt" ]; then - AVAILABLE="${AVAILABLE}${r} -" - fi -done -IFS="$OLD_IFS" - -if [ -z "$AVAILABLE" ]; then - cat >&2 </include/CGAL/version.h - /cmake/modules/UseCGAL.cmake - /CMakeLists.txt - -Recovery (one-time, on the dev machine): - cd ~/cgal && wget https://github.com/CGAL/cgal/archive/refs/tags/v5.6.tar.gz - tar xf v5.6.tar.gz && mv cgal-5.6 5.6 - # repeat for v6.0, v6.1 - -Then re-run: - bash scripts/quality/cgal-version-matrix.sh - -Or override the lookup path: - CGAL_ROOTS=/opt/cgal-5.6:/opt/cgal-6.0 bash scripts/quality/cgal-version-matrix.sh -EOF - exit 2 -fi - -echo "========================================" -echo " CGAL version matrix" -echo " versions tested:" -echo "$AVAILABLE" | sed 's/^/ /' -echo "========================================" - -# 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")" - build="build-cgal-$ver" - echo - echo "── CGAL $ver ─────────────────────────────" - echo " root: $cgal_root" - echo " build: $build" - - if ! cmake -S code -B "$build" \ - -DCGAL_DIR="$cgal_root" \ - -DWITH_CGAL_TESTS=ON \ - -DCMAKE_BUILD_TYPE=Release \ - -Wno-dev >/dev/null 2>&1; then - echo " CONFIGURE FAILED" - echo "$ver" >> "$ROOT/.cgal-matrix-failures" - continue - fi - - if ! nice -n 19 cmake --build "$build" --target conformallab_cgal_tests \ - -j1 >"$build/build.log" 2>&1; then - echo " BUILD FAILED — see $build/build.log" - echo "$ver" >> "$ROOT/.cgal-matrix-failures" - continue - fi - - if ! ( cd "$build" && ctest -R "^cgal\." --output-on-failure >"test.log" 2>&1 ); then - echo " TESTS FAILED — see $build/test.log" - echo "$ver" >> "$ROOT/.cgal-matrix-failures" - continue - fi - - pass=$(grep -oE "tests passed.*out of [0-9]+" "$build/test.log" | head -1) - echo " OK ($pass)" -done - -if [ -f "$ROOT/.cgal-matrix-failures" ]; then - echo - echo "FAIL: the following CGAL versions did not pass:" - sed 's/^/ /' "$ROOT/.cgal-matrix-failures" - rm -f "$ROOT/.cgal-matrix-failures" - exit 1 -fi - -echo -echo "OK: every detected CGAL version built + passed the CGAL test suite." -exit 0 diff --git a/scripts/quality/clang-format 2.sh b/scripts/quality/clang-format 2.sh deleted file mode 100755 index a515a30..0000000 --- a/scripts/quality/clang-format 2.sh +++ /dev/null @@ -1,96 +0,0 @@ -#!/usr/bin/env bash -# scripts/quality/clang-format.sh -# -# Verify every source file under code/{include,src,tests} matches the -# project's `.clang-format` policy. Runs in dry-run mode by default — -# only reports diffs; never edits files. -# -# Pass `--fix` to apply the suggested formatting in place. -# -# Local-only. Promotion to CI is intended once the existing tree is -# 100 %-conformant; today we report drift but don't fail (the script -# exits non-zero only with `--strict`). -# -# Usage: -# bash scripts/quality/clang-format.sh # dry-run, exit 0 always -# bash scripts/quality/clang-format.sh --strict # dry-run, exit 1 on drift -# bash scripts/quality/clang-format.sh --fix # apply changes -# -# Exit codes: -# 0 no drift, or drift but --strict not set -# 1 drift detected AND --strict (or fixes applied AND --fix) -# 2 prerequisite missing - -set -uo pipefail -ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" -cd "$ROOT" || exit 2 - -command -v clang-format >/dev/null 2>&1 || { - echo "FAIL: clang-format not in PATH." >&2 - echo " macOS: brew install clang-format" >&2 - echo " Linux: sudo apt install clang-format" >&2 - exit 2 -} - -STRICT=0 -FIX=0 -for arg in "$@"; do - case "$arg" in - --strict) STRICT=1 ;; - --fix) FIX=1 ;; - *) echo "Unknown arg: $arg" >&2; exit 2 ;; - esac -done - -FILES_LIST="$(find code/include code/src code/tests \ - \( -name "*.h" -o -name "*.hpp" -o -name "*.cpp" \) \ - -type f 2>/dev/null \ - | grep -v "^code/deps/" \ - | grep -v " 2\." \ - | sort)" - -n_total=0 -n_drift=0 -drift_list="" -while IFS= read -r f; do - [ -z "$f" ] && continue - n_total=$((n_total + 1)) - if [ "$FIX" -eq 1 ]; then - clang-format -i "$f" - else - # --dry-run + -Werror sets non-zero exit when changes would be made. - if ! clang-format --dry-run -Werror "$f" >/dev/null 2>&1; then - n_drift=$((n_drift + 1)) - drift_list="$drift_list $f -" - fi - fi -done </dev/null 2>&1 || { - echo "FAIL: clang-tidy not in PATH." >&2 - echo " macOS: brew install llvm && export PATH=\"\$(brew --prefix llvm)/bin:\$PATH\"" >&2 - echo " Linux: sudo apt install clang-tidy" >&2 - exit 2 -} - -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 + -# 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 "========================================" -echo " clang-tidy run" -echo " target: $TARGET_DIR" -echo " config: .clang-tidy" -echo " log: $LOG" -echo "========================================" -echo - -n=0 -for h in $HEADERS; do - n=$((n + 1)) - echo "── [$n] $h ─────────────────────────────────" - # Use --quiet so we only see actual diagnostics, not "n warnings - # 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 - -echo -echo "── Summary ──────────────────────────────────" -warn=$(grep -c "warning:" "$LOG" || true) -err=$(grep -c "error:" "$LOG" || true) -echo " files inspected: $n" -echo " warnings: $warn" -echo " errors: $err" -echo " full log: $LOG" - -if [ "${err:-0}" -gt 0 ]; then - exit 1 -fi -exit 0 diff --git a/scripts/quality/cmake-format 2.sh b/scripts/quality/cmake-format 2.sh deleted file mode 100755 index 496848a..0000000 --- a/scripts/quality/cmake-format 2.sh +++ /dev/null @@ -1,126 +0,0 @@ -#!/usr/bin/env bash -# scripts/quality/cmake-format.sh -# -# Run cmake-format (drift check) + cmake-lint (semantic check) over -# every CMakeLists.txt and *.cmake we own. Skips code/deps/. -# -# Local-only. Promotion to CI once the existing CMakeLists.txt files -# pass `--strict`. -# -# Usage: -# bash scripts/quality/cmake-format.sh # dry-run, exit 0 always -# bash scripts/quality/cmake-format.sh --strict # dry-run, exit 1 on drift -# bash scripts/quality/cmake-format.sh --fix # apply formatting in place -# -# Exit codes: -# 0 no drift, or drift but --strict not set; lint produced no errors -# 1 drift and --strict, or lint reported errors, or --fix applied -# 2 prerequisite missing - -set -uo pipefail -ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" -cd "$ROOT" || exit 2 - -# Allow ~/.local/bin (pip-installed tools) in PATH. -export PATH="$HOME/.local/bin:$PATH" - -command -v cmake-format >/dev/null 2>&1 || { - echo "FAIL: cmake-format not in PATH." >&2 - echo " pip3 install --user cmakelang" >&2 - echo " export PATH=\"\$HOME/.local/bin:\$PATH\"" >&2 - exit 2 -} -command -v cmake-lint >/dev/null 2>&1 || { - echo "FAIL: cmake-lint not in PATH (ships with cmakelang)." >&2 - exit 2 -} - -STRICT=0 -FIX=0 -for arg in "$@"; do - case "$arg" in - --strict) STRICT=1 ;; - --fix) FIX=1 ;; - *) echo "Unknown arg: $arg" >&2; exit 2 ;; - esac -done - -FILES="$(find code \ - \( -name "CMakeLists.txt" -o -name "*.cmake" \) \ - -type f 2>/dev/null \ - | grep -v "/deps/" \ - | grep -v "/build" \ - | grep -v " 2\." \ - | sort)" - -if [ -z "$FILES" ]; then - echo "FAIL: no CMakeLists.txt files found" >&2 - exit 2 -fi - -echo "cmake-format ($(cmake-format --version 2>&1 | head -1))" -echo "cmake-lint ($(cmake-lint --version 2>&1 | head -1))" - -# ── Drift check / fix ────────────────────────────────────────────────────── -n_total=0 -n_drift=0 -drift_list="" -while IFS= read -r f; do - [ -z "$f" ] && continue - n_total=$((n_total + 1)) - if [ "$FIX" -eq 1 ]; then - cmake-format -i "$f" - else - if ! cmake-format --check "$f" >/dev/null 2>&1; then - n_drift=$((n_drift + 1)) - drift_list="$drift_list $f -" - fi - fi -done <&1 || true)" - if [ -n "$out" ]; then - echo "$out" - # Each warning line begins with the filename → count them. - cnt=$(printf '%s' "$out" | grep -c "^$f:" || true) - n_lint_err=$((n_lint_err + cnt)) - fi -done </dev/null 2>&1 || { - echo "FAIL: codespell not in PATH." >&2 - echo " macOS: brew install codespell" >&2 - echo " Linux: sudo apt install codespell OR pip3 install codespell" >&2 - exit 2 -} - -FIX=0 -for arg in "$@"; do - case "$arg" in - --fix) FIX=1 ;; - *) echo "Unknown arg: $arg" >&2; exit 2 ;; - esac -done - -# Targets: everything except deps + build + generated. -TARGETS=( - "doc" - "code/include" - "code/src" - "code/tests" - "scripts" - "README.md" - "CHANGELOG.md" - "CLAUDE.md" - "CITATION.cff" - "CONTRIBUTING.md" -) - -# Filter to existing entries (CONTRIBUTING.md / CHANGELOG.md may not exist -# on every branch). -EXISTING=() -for t in "${TARGETS[@]}"; do - [ -e "$t" ] && EXISTING+=("$t") -done - -echo "codespell ($(codespell --version 2>&1 | head -1))" -echo "Targets: ${EXISTING[*]}" -echo - -if [ "$FIX" -eq 1 ]; then - codespell --write-changes "${EXISTING[@]}" - rc=$? -else - codespell "${EXISTING[@]}" - rc=$? -fi - -if [ "$rc" -ne 0 ]; then - echo - echo "Recovery:" - echo " bash scripts/quality/codespell.sh --fix # apply" - echo " # or add false-positives to .codespellrc → ignore-words-list" - exit 1 -fi - -echo -echo "OK: no typos found." -exit 0 diff --git a/scripts/quality/coverage 2.sh b/scripts/quality/coverage 2.sh deleted file mode 100755 index 8b9e050..0000000 --- a/scripts/quality/coverage 2.sh +++ /dev/null @@ -1,128 +0,0 @@ -#!/usr/bin/env bash -# scripts/quality/coverage.sh -# -# Measure line/branch test coverage of code/include/ via gcov + lcov. -# -# Builds the test suite with `--coverage` (gcc) or `-fprofile-instr-generate -# -fcoverage-mapping` (clang), runs ctest, and emits: -# * build-coverage/coverage.info — lcov tracefile -# * build-coverage/lcov-html/index.html — browseable HTML report -# * stdout: per-file summary + grand total -# -# Local-only. Not gated in CI yet; once a coverage threshold is agreed -# with the reviewer (e.g. 80 %), the gate can be a single line in -# cpp-tests.yml. -# -# Usage: -# bash scripts/quality/coverage.sh # gcc default -# CXX=g++-13 bash scripts/quality/coverage.sh # specific compiler -# -# Prerequisites: gcov + lcov (apt install lcov / brew install lcov) -# -# Exit codes: -# 0 coverage report generated; prints % -# 1 tests failed (no usable trace) -# 2 prerequisite missing - -set -euo pipefail -ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" -cd "$ROOT" - -BUILD_DIR="build-coverage" - -command -v lcov >/dev/null 2>&1 || { - echo "FAIL: lcov not found in PATH. Install via:" >&2 - echo " macOS: brew install lcov" >&2 - echo " Linux: sudo apt install lcov" >&2 - exit 2 -} - -# On macOS we prefer brew-installed LLVM clang++ because Apple Clang's -# GCov-compatible `--coverage` runtime is known to deadlock during -# static-initializer profiling on arm64 with template-heavy code -# (Eigen + CGAL); the LLVM build does not. Override with `CXX=...`. -DEFAULT_CXX="g++" -if [ -z "${CXX:-}" ] && [ "$(uname -s)" = "Darwin" ] \ - && [ -x /opt/homebrew/opt/llvm/bin/clang++ ]; then - DEFAULT_CXX="/opt/homebrew/opt/llvm/bin/clang++" -fi -CXX_BIN="${CXX:-$DEFAULT_CXX}" -command -v "$CXX_BIN" >/dev/null 2>&1 || { echo "FAIL: $CXX_BIN not found" >&2; exit 2; } - -echo "========================================" -echo " Coverage build (gcov + lcov)" -echo " CXX: $CXX_BIN ($($CXX_BIN --version | head -1))" -echo " build: $BUILD_DIR" -echo "========================================" -echo - -cmake -S code -B "$BUILD_DIR" \ - -DCMAKE_CXX_COMPILER="$CXX_BIN" \ - -DCMAKE_CXX_FLAGS="--coverage -O0 -g" \ - -DCMAKE_EXE_LINKER_FLAGS="--coverage" \ - -DCMAKE_BUILD_TYPE=Debug \ - -DCMAKE_GTEST_DISCOVER_TESTS_DISCOVERY_MODE=PRE_TEST \ - -Wno-dev - -nice -n 19 cmake --build "$BUILD_DIR" --target conformallab_tests \ - -j"$(nproc 2>/dev/null || sysctl -n hw.logicalcpu 2>/dev/null || echo 2)" - -# Run the non-CGAL test suite. (CGAL tests under coverage instrumentation -# blow up to ~6 GB RAM during compilation; out-of-scope for this gate.) -cd "$BUILD_DIR" -if ! ctest -E "^cgal\." --output-on-failure; then - cd "$ROOT" - echo "FAIL: coverage build's test run did not complete cleanly." >&2 - exit 1 -fi -cd "$ROOT" - -# ── Capture trace ──────────────────────────────────────────────────────────── -# lcov ≥ 2.0 became strict about "inconsistent" / "unsupported" / "negative" -# diagnostics from gcov data; the GTest sources reliably trigger -# "inconsistent" because of their preprocessor gymnastics, and brew clang's -# gcov shim is older than the function-end-line tracking lcov wants. -# These are noise we cannot fix in our source tree — suppress them. -LCOV_TOLERANT=( - --ignore-errors inconsistent - --ignore-errors unsupported - --ignore-errors negative - --ignore-errors empty - --ignore-errors mismatch - --rc lcov_branch_coverage=1 -) - -lcov --capture --directory "$BUILD_DIR" \ - --output-file "$BUILD_DIR/coverage.raw.info" \ - --no-external \ - "${LCOV_TOLERANT[@]}" \ - >/dev/null 2>&1 || true - -# Restrict to code/include/ (our public API surface; ignore deps/tests). -lcov --extract "$BUILD_DIR/coverage.raw.info" \ - "*/code/include/*" \ - --output-file "$BUILD_DIR/coverage.info" \ - "${LCOV_TOLERANT[@]}" \ - >/dev/null 2>&1 || true - -# ── HTML report ────────────────────────────────────────────────────────────── -genhtml --branch-coverage --legend \ - --output-directory "$BUILD_DIR/lcov-html" \ - "${LCOV_TOLERANT[@]}" \ - "$BUILD_DIR/coverage.info" >/dev/null 2>&1 || true - -# ── Summary to stdout ──────────────────────────────────────────────────────── -echo -echo "── Coverage summary (code/include/) ─────────────────────────" -if [ -s "$BUILD_DIR/coverage.info" ]; then - lcov --summary "$BUILD_DIR/coverage.info" "${LCOV_TOLERANT[@]}" 2>/dev/null \ - | grep -E "lines\.\.\.\.|functions|branches" \ - | sed 's/^/ /' - echo - echo "HTML report: $BUILD_DIR/lcov-html/index.html" - echo " open $BUILD_DIR/lcov-html/index.html" -else - echo " WARN: coverage.info is empty — likely an lcov/gcov version" - echo " mismatch. Tests passed; raw .gcda files are in $BUILD_DIR." - echo " Inspect with: find $BUILD_DIR -name '*.gcda' | head" -fi diff --git a/scripts/quality/cppcheck 2.sh b/scripts/quality/cppcheck 2.sh deleted file mode 100755 index c584c50..0000000 --- a/scripts/quality/cppcheck 2.sh +++ /dev/null @@ -1,97 +0,0 @@ -#!/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/license-headers 2.sh b/scripts/quality/license-headers 2.sh deleted file mode 100755 index 035decc..0000000 --- a/scripts/quality/license-headers 2.sh +++ /dev/null @@ -1,133 +0,0 @@ -#!/usr/bin/env bash -# Requires bash ≥ 4 for `mapfile`; on macOS install via `brew install bash` -# and invoke as `bash scripts/quality/license-headers.sh`. The portable -# fallback below works on bash 3.2 too. -# scripts/quality/license-headers.sh -# -# Verify that every source file under code/ carries an SPDX-License- -# Identifier header. The project policy (doc/release-policy.md) is: -# -# * Every conformallab++ source file (.h, .hpp, .cpp under code/include -# or code/src, but NOT code/deps/) must have: -# SPDX-License-Identifier: MIT -# * The copyright line is encouraged but not enforced (the year is -# allowed to lag). -# -# Files under code/deps/ are vendored third-party code (Eigen, JSON, -# GLFW, libigl, …) and are skipped — they carry their own licenses. -# -# Exit codes: -# 0 every checked file has the SPDX header -# 1 at least one file is missing it -# 2 prerequisite missing -# -# Run from any directory; the script resolves the repo root from its own path. - -set -euo pipefail -ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" -cd "$ROOT" - -SPDX="SPDX-License-Identifier: MIT" -COPYRIGHT="Copyright (c) 2024-$(date +%Y) Tarik Moussa." - -FIX=0 -for arg in "$@"; do - case "$arg" in - --fix) FIX=1 ;; - *) echo "Unknown arg: $arg (only --fix supported)" >&2; exit 2 ;; - esac -done - -# Insert a 2-line header at the top of $1, but BELOW any existing -# `#pragma once` so the include-guard role is preserved. Idempotent — -# the caller has already verified the SPDX line is missing. -insert_header () { - local f="$1" - local tmp - tmp="$(mktemp)" - local first_line - first_line="$(head -1 "$f")" - if echo "$first_line" | grep -q "^#pragma once"; then - # Header form 1: keep `#pragma once` on line 1, then license, then blank. - { - echo "$first_line" - echo "// $COPYRIGHT" - echo "// $SPDX" - echo - tail -n +2 "$f" - } > "$tmp" - elif echo "$first_line" | grep -q "^#ifndef"; then - # Header form 2: insert license ABOVE the include guard. - { - echo "// $COPYRIGHT" - echo "// $SPDX" - echo - cat "$f" - } > "$tmp" - else - # Other (most likely a .cpp): just prepend. - { - echo "// $COPYRIGHT" - echo "// $SPDX" - echo - cat "$f" - } > "$tmp" - fi - mv "$tmp" "$f" -} - -# Files to check: code/include + code/src + code/tests. Skip code/deps. -FILES_LIST="$(find code/include code/src code/tests \ - \( -name "*.h" -o -name "*.hpp" -o -name "*.cpp" \) \ - -type f 2>/dev/null \ - | grep -v "^code/deps/" \ - | grep -v " 2\." \ - | sort)" - -n_total=0 -n_missing=0 -n_fixed=0 -missing_list="" -while IFS= read -r f; do - [ -z "$f" ] && continue - n_total=$((n_total + 1)) - # Check only the first 30 lines — SPDX must be near the top. - if ! head -30 "$f" | grep -q "$SPDX"; then - n_missing=$((n_missing + 1)) - if [ "$FIX" -eq 1 ]; then - insert_header "$f" - n_fixed=$((n_fixed + 1)) - fi - missing_list="$missing_list $f -" - fi -done <&2 - exit 2 -fi - -echo "Checked $n_total source files for SPDX header." - -if [ "$FIX" -eq 1 ]; then - if [ "$n_fixed" -gt 0 ]; then - echo "FIX mode: inserted '$SPDX' into $n_fixed file(s)." - echo " Review with: git diff" - exit 0 - fi -fi - -if [ "$n_missing" -gt 0 ]; then - echo - echo "FAIL: $n_missing file(s) missing '$SPDX' in their first 30 lines:" - printf '%s' "$missing_list" - echo - echo "Recovery: bash scripts/quality/license-headers.sh --fix" - exit 1 -fi - -echo "OK: every checked file carries '$SPDX'." -exit 0 diff --git a/scripts/quality/multi-compiler 2.sh b/scripts/quality/multi-compiler 2.sh deleted file mode 100755 index 2c3329e..0000000 --- a/scripts/quality/multi-compiler 2.sh +++ /dev/null @@ -1,119 +0,0 @@ -#!/usr/bin/env bash -# scripts/quality/multi-compiler.sh -# -# Build and test the fast (non-CGAL) suite against multiple C++ -# compilers in sequence. Catches compiler-specific issues: -# * gcc-only extensions accidentally used -# * clang's stricter template-error handling -# * libstdc++ vs libc++ ABI assumptions -# -# Local-only. Each compiler gets its own build-multi-/ directory. -# -# Usage: -# bash scripts/quality/multi-compiler.sh # auto-detect -# bash scripts/quality/multi-compiler.sh g++ clang++ # specific list -# -# Exit codes: -# 0 every detected/selected compiler builds + tests cleanly -# 1 at least one compiler failed -# 2 fewer than 2 compilers available - -set -euo pipefail -ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" -cd "$ROOT" - -# ── Pick compilers ────────────────────────────────────────────────────────── -if [ $# -gt 0 ]; then - COMPILERS=("$@") -else - COMPILERS=() - for cand in g++ g++-13 g++-12 g++-11 clang++ clang++-17 clang++-16 clang++-15; do - if command -v "$cand" >/dev/null 2>&1; then - COMPILERS+=("$cand") - fi - done -fi - -if [ "${#COMPILERS[@]}" -lt 1 ]; then - echo "FAIL: no C++ compilers detected. Install gcc and/or clang." >&2 - exit 2 -fi - -echo "========================================" -echo " Multi-compiler build matrix" -echo " compilers: ${COMPILERS[*]}" -echo "========================================" -echo - -# De-duplicate by resolving each to its absolute path. -declare -a UNIQ_BINS=() -declare -a UNIQ_NAMES=() -seen="" -for cc in "${COMPILERS[@]}"; do - if ! command -v "$cc" >/dev/null 2>&1; then - echo " skip $cc (not in PATH)" - continue - fi - full="$(command -v "$cc")" - if echo "$seen" | grep -qx "$full"; then - continue - fi - seen="$seen -$full" - UNIQ_BINS+=("$full") - # Use a sanitised name for the build dir (replace + and / etc.). - safe="$(echo "$cc" | sed 's|[/+]|_|g')" - UNIQ_NAMES+=("$safe") -done - -if [ "${#UNIQ_BINS[@]}" -lt 2 ]; then - echo "WARNING: only ${#UNIQ_BINS[@]} unique compiler(s) — matrix is degenerate." - echo " (continuing anyway; install a second toolchain for full coverage)" -fi - -# ── Run each ──────────────────────────────────────────────────────────────── -overall=0 -i=0 -for cc in "${UNIQ_BINS[@]}"; do - name="${UNIQ_NAMES[$i]}" - i=$((i + 1)) - build="build-multi-$name" - echo - echo "── [$i/${#UNIQ_BINS[@]}] $name ─────────────────────" - echo " bin: $cc" - echo " build: $build" - - if ! cmake -S code -B "$build" \ - -DCMAKE_CXX_COMPILER="$cc" \ - -DCMAKE_BUILD_TYPE=Release \ - -Wno-dev >/dev/null 2>&1; then - echo " CONFIGURE FAILED" - overall=1 - continue - fi - - if ! nice -n 19 cmake --build "$build" --target conformallab_tests \ - -j"$(nproc 2>/dev/null || sysctl -n hw.logicalcpu 2>/dev/null || echo 2)" \ - >"$build/build.log" 2>&1; then - echo " BUILD FAILED — see $build/build.log" - overall=1 - continue - fi - - if ! ( cd "$build" && ctest -E "^cgal\." --output-on-failure >"test.log" 2>&1 ); then - echo " TESTS FAILED — see $build/test.log" - overall=1 - continue - fi - - pass=$(grep -oE "tests passed.*out of [0-9]+" "$build/test.log" | head -1) - echo " OK ($pass)" -done - -echo -if [ $overall -eq 0 ]; then - echo "OK: every selected compiler built + passed the fast test suite." -else - echo "FAIL: at least one compiler did not produce a green test run." -fi -exit $overall diff --git a/scripts/quality/reproducible-build 2.sh b/scripts/quality/reproducible-build 2.sh deleted file mode 100755 index 256d9ba..0000000 --- a/scripts/quality/reproducible-build 2.sh +++ /dev/null @@ -1,117 +0,0 @@ -#!/usr/bin/env bash -# scripts/quality/reproducible-build.sh -# -# Build the project twice with the same toolchain + flags + sources, and -# verify the two outputs are byte-identical (after stripping ABI noise). -# -# Why: conformallab++ is header-only, so the binaries we ship are just -# the test executables. If the same source + same toolchain produces -# different bytes, something non-deterministic snuck in: -# * a `__DATE__` / `__TIME__` macro in the code -# * an absolute path leaked into a string literal -# * iteration over an unordered container of items -# * a parallel build with non-deterministic linking order -# -# Local-only. Two full builds at ~3 min each ≈ 6 min wall time. -# -# Usage: -# bash scripts/quality/reproducible-build.sh -# -# Exit codes: -# 0 the two builds produce byte-identical executables -# 1 there is at least one differing byte; prints which executable(s) -# 2 prerequisite missing - -set -euo pipefail -ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" -cd "$ROOT" - -DIR_A="build-repro-A" -DIR_B="build-repro-B" - -# Identical-input check: nuke any state from a previous run. -rm -rf "$DIR_A" "$DIR_B" - -# Use a fixed timezone + SOURCE_DATE_EPOCH so any time-based macros -# yield identical strings in both builds. Without this, even a perfect -# build pipeline disagrees if __TIME__ slips in. -export SOURCE_DATE_EPOCH="${SOURCE_DATE_EPOCH:-1700000000}" -export TZ=UTC -export LC_ALL=C - -echo "========================================" -echo " Reproducible-build check" -echo " build A: $DIR_A" -echo " build B: $DIR_B" -echo " SOURCE_DATE_EPOCH: $SOURCE_DATE_EPOCH" -echo "========================================" -echo - -build_once () { - local dir="$1" - cmake -S code -B "$dir" -DCMAKE_BUILD_TYPE=Release -Wno-dev >/dev/null - # Single-threaded build → deterministic link order. - cmake --build "$dir" --target conformallab_tests -j1 >"$dir/build.log" 2>&1 -} - -echo "── Build A ─────────────────────────────────" -build_once "$DIR_A" -echo " done." - -echo "── Build B ─────────────────────────────────" -build_once "$DIR_B" -echo " done." - -# ── Compare ───────────────────────────────────────────────────────────────── -# Test executables live at: -# build-repro-*/conformallab_tests (single combined fast test) -# build-repro-*/test_* (older per-suite executables) -# Compare every regular file under each build dir that ends in -# `_tests` or starts with `test_`. - -echo -echo "── Diff ────────────────────────────────────" -mismatches="" -for path_a in "$DIR_A"/conformallab_tests "$DIR_A"/test_*; do - [ -f "$path_a" ] || continue - rel="${path_a#${DIR_A}/}" - path_b="$DIR_B/$rel" - if [ ! -f "$path_b" ]; then - echo " MISS $rel (only in build A)" - mismatches="$mismatches $rel (missing in build B) -" - continue - fi - if cmp -s "$path_a" "$path_b"; then - echo " OK $rel" - else - a_sha=$(shasum -a 256 "$path_a" | cut -d' ' -f1) - b_sha=$(shasum -a 256 "$path_b" | cut -d' ' -f1) - echo " DIFF $rel" - echo " A $a_sha" - echo " B $b_sha" - mismatches="$mismatches $rel -" - fi -done - -echo -if [ -n "$mismatches" ]; then - echo "FAIL: the build is not byte-reproducible." - echo - echo "Differing files:" - printf "%s" "$mismatches" - echo - echo "Common causes:" - echo " * __DATE__ / __TIME__ macros baked into the binary" - echo " * absolute build path embedded in a debug-info string" - echo " * a parallel-link race (-j > 1) — but we already use -j1 here" - echo " * a header generated from a non-deterministic source" - echo - echo "Debug recipe:" - echo " diff <(strings $DIR_A/$rel) <(strings $DIR_B/$rel) | head -20" - exit 1 -fi - -echo "OK: every test executable is byte-identical between the two builds." -exit 0 diff --git a/scripts/quality/run-all 2.sh b/scripts/quality/run-all 2.sh deleted file mode 100755 index f01f27f..0000000 --- a/scripts/quality/run-all 2.sh +++ /dev/null @@ -1,119 +0,0 @@ -#!/usr/bin/env bash -# scripts/quality/run-all.sh -# -# Run every local quality gate in sequence. Each gate is independent; -# a failure does not stop the rest (we collect failures and report at -# the end). Use this before tagging a release or before showing the -# repo to an external reviewer. -# -# Wall-time budget on a typical dev laptop (M-series Mac): -# license-headers.sh ~1 s -# check-markdown-links.py ~2 s -# sanitizers.sh ~3 min -# coverage.sh ~2 min -# clang-tidy.sh ~2 min (depends on header count) -# multi-compiler.sh ~5 min (per compiler) -# reproducible-build.sh ~6 min -# cgal-version-matrix.sh ~5 min per CGAL version -# ───────────────────────────── -# TOTAL ~25–40 min -# -# Usage: -# bash scripts/quality/run-all.sh # everything -# bash scripts/quality/run-all.sh --fast # skip the slow gates -# (cgal-matrix, multi-compiler, -# coverage, reproducible) -# -# Exit code: number of failed gates (so 0 = green). - -set -uo pipefail -ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" -cd "$ROOT" || exit 2 - -FAST=0 -[ "${1:-}" = "--fast" ] && FAST=1 - -GATES_FAST=( - "License headers | bash scripts/quality/license-headers.sh" - "CGAL conventions | python3 scripts/quality/cgal-conventions.py" - "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" -) -GATES_SLOW=( - "Coverage | bash scripts/quality/coverage.sh" - "Multi-compiler | bash scripts/quality/multi-compiler.sh" - "Reproducible build | bash scripts/quality/reproducible-build.sh" - "CGAL version matrix | bash scripts/quality/cgal-version-matrix.sh" -) - -if [ "$FAST" -eq 1 ]; then - GATES=("${GATES_FAST[@]}") -else - GATES=("${GATES_FAST[@]}" "${GATES_SLOW[@]}") -fi - -LOG_DIR="build-quality-logs" -mkdir -p "$LOG_DIR" - -echo "============================================================" -echo " conformallab++ local quality gates" -echo " mode: $([ $FAST -eq 1 ] && echo 'FAST (4 gates)' || echo "FULL (${#GATES[@]} gates)")" -echo " logs: $LOG_DIR/" -echo "============================================================" - -results="" -failed=0 -skipped=0 -i=0 -for entry in "${GATES[@]}"; do - i=$((i + 1)) - name="${entry%%|*}" - name="${name%%[[:space:]]*([[:space:]])}" # trim trailing space - cmd="${entry##*|}" - cmd="${cmd##[[:space:]]}" - # Slug-safe filename - slug=$(echo "$name" | tr ' /[:upper:]' '_-[:lower:]' | tr -cd 'a-z0-9_-') - log="$LOG_DIR/$slug.log" - echo - echo "──── [$i/${#GATES[@]}] $name ────" - eval "$cmd" >"$log" 2>&1 - rc=$? - # Exit code 2 from any of our gate scripts = "prerequisite missing" - # (tool not in PATH, no CGAL tarball, no second compiler, etc.). - # 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)|no .* 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 - echo " FAIL (rc=$rc) — see $log" - echo " last 20 lines:" - tail -20 "$log" | sed 's/^/ /' - results="${results} FAIL $name ($log) -" - failed=$((failed + 1)) - fi -done - -echo -echo "============================================================" -echo " Summary" -echo "============================================================" -printf "%s" "$results" -echo -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 2.sh b/scripts/quality/sanitizers 2.sh deleted file mode 100755 index 4a44559..0000000 --- a/scripts/quality/sanitizers 2.sh +++ /dev/null @@ -1,95 +0,0 @@ -#!/usr/bin/env bash -# scripts/quality/sanitizers.sh -# -# Build the fast test suite with AddressSanitizer + UndefinedBehaviorSanitizer -# and run it. Catches: -# * use-after-free, double-free, heap-buffer-overflow (ASan) -# * signed integer overflow, NaN propagation, alignment violations (UBSan) -# * Eigen / CGAL template-induced UB that escapes the regular build -# -# Local-only (not in CI): the sanitizer build is ~3× slower and brittle -# against system libraries. Run it before every release tag, after -# touching any Newton/Hessian code, or when investigating intermittent -# test failures. -# -# Usage: -# bash scripts/quality/sanitizers.sh # default: ASan + UBSan -# ASAN_OPTIONS=... UBSAN_OPTIONS=... bash scripts/quality/sanitizers.sh -# -# Exit codes: -# 0 every test passes under sanitizer instrumentation -# 1 a sanitizer report was triggered (test failure or runtime error) -# 2 prerequisite missing (no clang/gcc with sanitizer support) - -set -euo pipefail -ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" -cd "$ROOT" - -BUILD_DIR="build-sanitizers" -SAN_FLAGS="-fsanitize=address,undefined -fno-omit-frame-pointer -O1 -g" - -# Default ASan/UBSan runtime options — print stack on first error, -# abort on first issue (so CI logs make the cause obvious). -export ASAN_OPTIONS="${ASAN_OPTIONS:-detect_leaks=1:abort_on_error=1:print_stacktrace=1}" -export UBSAN_OPTIONS="${UBSAN_OPTIONS:-print_stacktrace=1:halt_on_error=1}" - -echo "========================================" -echo " Sanitizer build (ASan + UBSan)" -echo " flags : $SAN_FLAGS" -echo " ASAN : $ASAN_OPTIONS" -echo " UBSAN : $UBSAN_OPTIONS" -echo "========================================" - -# ── Pick a compiler with sanitizer support ────────────────────────────────── -# Prefer clang (better diagnostics); fall back to gcc. -CXX_BIN="" -for cand in clang++-17 clang++-16 clang++-15 clang++ g++; do - if command -v "$cand" >/dev/null 2>&1; then - CXX_BIN="$cand" - break - fi -done -if [ -z "$CXX_BIN" ]; then - echo "FAIL: no clang++ / g++ found in PATH" >&2 - exit 2 -fi -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 ──────────────────────────────────── -# CGAL tests would 4–5× the build time under sanitizers and have a -# higher false-positive surface (CGAL's expression-template trickery). -# Use the fast suite as the sanitizer canary; full coverage of the CGAL -# layer is covered by coverage.sh + the regular Release build. -nice -n 19 cmake --build "$BUILD_DIR" --target conformallab_tests \ - -j"$(nproc 2>/dev/null || sysctl -n hw.logicalcpu 2>/dev/null || echo 2)" - -# ── Run ────────────────────────────────────────────────────────────────────── -cd "$BUILD_DIR" -if ctest -E "^cgal\." --output-on-failure --output-junit san-results.xml; then - cd "$ROOT" - echo - echo "OK: all sanitizer-instrumented tests passed." - exit 0 -else - cd "$ROOT" - echo - echo "FAIL: sanitizer-instrumented tests reported issues." - echo " See: $BUILD_DIR/Testing/Temporary/LastTest.log" - exit 1 -fi diff --git a/scripts/quality/shellcheck 2.sh b/scripts/quality/shellcheck 2.sh deleted file mode 100755 index 19bec95..0000000 --- a/scripts/quality/shellcheck 2.sh +++ /dev/null @@ -1,79 +0,0 @@ -#!/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 <