chore: remove macOS Finder duplicate files accidentally committed in 7b097fb
Some checks failed
C++ Tests / test-fast (pull_request) Successful in 2m15s
API Docs / doc-build (pull_request) Successful in 49s
Markdown link check / check (pull_request) Successful in 49s
C++ Tests / test-cgal (pull_request) Failing after 10m50s
C++ Tests / quality-gates (pull_request) Successful in 1m51s

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 <noreply@anthropic.com>
This commit is contained in:
Tarik Moussa
2026-05-24 20:07:39 +02:00
parent 7b097fbdd1
commit d84810efc8
15 changed files with 0 additions and 1728 deletions

View File

@@ -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/<ver>/` (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 — ~2540 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/<gate>.log`
when invoked via `run-all.sh`, and to its own per-gate build directory
(`build-sanitizers/`, `build-coverage/`, `build-multi-<cc>/`, …) 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/<ver>/` (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.

View File

@@ -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_<DIRS>_<FILENAME>_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 "<none>"
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())

View File

@@ -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/<version>/` (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 <<EOF
FAIL: no CGAL installs found.
Searched: $ROOTS
Expected layout for each version (any one of these is enough):
<root>/include/CGAL/version.h
<root>/cmake/modules/UseCGAL.cmake
<root>/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

View File

@@ -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 <<EOF
$FILES_LIST
EOF
echo "clang-format ($(clang-format --version | head -1))"
echo "Scanned $n_total source files."
if [ "$FIX" -eq 1 ]; then
echo "FIX mode: applied formatting in place."
exit 0
fi
if [ "$n_drift" -gt 0 ]; then
echo
echo "DRIFT: $n_drift file(s) do not match .clang-format policy:"
printf "%s" "$drift_list"
echo
echo "Recovery:"
echo " bash scripts/quality/clang-format.sh --fix # apply"
echo " git diff # review"
if [ "$STRICT" -eq 1 ]; then
exit 1
fi
echo " (--strict not set → exiting 0 anyway)"
exit 0
fi
echo "OK: every file matches .clang-format policy."
exit 0

View File

@@ -1,110 +0,0 @@
#!/usr/bin/env bash
# scripts/quality/clang-tidy.sh
#
# Run clang-tidy over every public header under code/include/. We use
# `--use-color` and tee output to build-tidy/clang-tidy.log for later
# diffing.
#
# Local-only. The check is exploratory until we agree on which warning
# classes are reasonable to enforce — CGAL header-only code triggers a
# lot of `modernize-*` / `readability-*` warnings that are upstream's
# choice, not ours. See `.clang-tidy` for the curated subset.
#
# Usage:
# bash scripts/quality/clang-tidy.sh # all headers
# bash scripts/quality/clang-tidy.sh code/include/CGAL # subset
#
# Prerequisite: a compile_commands.json with the right include paths
# (cmake generates this automatically with CMAKE_EXPORT_COMPILE_COMMANDS=ON).
#
# Exit codes:
# 0 clang-tidy ran (output captured)
# 1 clang-tidy reported at least one error (post-policy filter)
# 2 prerequisite missing
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
cd "$ROOT"
BUILD_DIR="build-tidy"
LOG="$BUILD_DIR/clang-tidy.log"
command -v clang-tidy >/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 <cmath>, <complex>,
# <CGAL/...>, 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

View File

@@ -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 <<EOF
$FILES
EOF
if [ "$FIX" -eq 1 ]; then
echo "FIX mode: applied formatting in place."
exit 1 # signal to caller that the tree changed
fi
if [ "$n_drift" -gt 0 ]; then
echo
echo "DRIFT: $n_drift / $n_total CMake file(s) do not match .cmake-format.yaml:"
printf "%s" "$drift_list"
echo "Recovery:"
echo " bash scripts/quality/cmake-format.sh --fix # apply"
if [ "$STRICT" -eq 1 ]; then
exit 1
fi
fi
# ── Semantic lint (always runs, never fails unless --strict) ───────────────
echo
echo "── cmake-lint ──"
n_lint_err=0
while IFS= read -r f; do
[ -z "$f" ] && continue
out="$(cmake-lint "$f" 2>&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 <<EOF
$FILES
EOF
echo
echo "── Summary ──"
echo " CMake files checked: $n_total"
echo " cmake-format drift: $n_drift (recover: --fix)"
echo " cmake-lint findings: $n_lint_err"
if [ "$STRICT" -eq 1 ] && [ $((n_drift + n_lint_err)) -gt 0 ]; then
exit 1
fi
exit 0

View File

@@ -1,87 +0,0 @@
#!/usr/bin/env bash
# scripts/quality/codespell.sh
#
# Run codespell across the repo to catch typos in:
# * doc/**/*.md (reviewer-facing material)
# * code/include/**/*.{h,hpp} (Doxygen comments are user-facing)
# * code/src/, code/tests/ (test names, error messages)
# * scripts/**/*.{sh,py} (CI messages reach contributors)
# * README.md, CHANGELOG.md, CLAUDE.md
#
# Skips vendored deps, build dirs, generated Doxygen output (see
# `.codespellrc`).
#
# Local-only. CI promotion once the existing tree is 0-typo.
#
# Usage:
# bash scripts/quality/codespell.sh # dry-run, exit 1 on any hit
# bash scripts/quality/codespell.sh --fix # interactively apply suggestions
#
# Exit codes:
# 0 no typos
# 1 at least one typo found (no --fix), or --fix completed
# 2 prerequisite missing
set -uo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
cd "$ROOT" || exit 2
command -v codespell >/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

View File

@@ -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

View File

@@ -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

View File

@@ -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 <<EOF
$FILES_LIST
EOF
if [ "$n_total" -eq 0 ]; then
echo "FAIL: no source files found under code/{include,src,tests}/" >&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

View File

@@ -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-<cc>/ 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

View File

@@ -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

View File

@@ -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 ~2540 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

View File

@@ -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 45× 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

View File

@@ -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 <file> line N:" block
cnt=$(printf '%s' "$out" | grep -c "^In .* line")
total_findings=$((total_findings + cnt))
fi
done <<EOF
$FILES
EOF
echo "── Summary ──"
echo " scripts scanned: $n_total"
echo " scripts with issues: $n_with_findings"
echo " total findings: $total_findings"
if [ "$STRICT" -eq 1 ] && [ "$total_findings" -gt 0 ]; then
exit 1
fi
exit 0