ci+licenses: promote 4 trivial gates to required CI + third-party license doc
Some checks failed
C++ Tests / test-fast (pull_request) Successful in 2m2s
API Docs / doc-build (pull_request) Successful in 46s
Markdown link check / check (pull_request) Successful in 47s
C++ Tests / test-cgal (pull_request) Failing after 10m51s
C++ Tests / quality-gates (pull_request) Successful in 2m21s
Some checks failed
C++ Tests / test-fast (pull_request) Successful in 2m2s
API Docs / doc-build (pull_request) Successful in 46s
Markdown link check / check (pull_request) Successful in 47s
C++ Tests / test-cgal (pull_request) Failing after 10m51s
C++ Tests / quality-gates (pull_request) Successful in 2m21s
Two reviewer-facing additions:
1. New `quality-gates` job in .gitea/workflows/cpp-tests.yml
──────────────────────────────────────────────────────────
Runs in parallel with test-cgal after test-fast. Installs
`codespell` + `shellcheck` (apt) into the existing ci-cpp container,
then executes four scripts strictly (exit 1 on any finding):
* license-headers.sh — 66/66 files carry SPDX MIT
* cgal-conventions.py — 0 violations across 6 CGAL public headers
* codespell.sh — 0 typos across docs + source + scripts
* shellcheck.sh — 0 findings across 16 shell scripts
Each ran at 0 findings locally for weeks before promotion. The
gates are now contractual: a regression fails the PR. Total
wall-time on the eulernest runner: ~30 s.
2. New code/deps/THIRD-PARTY-LICENSES.md
──────────────────────────────────────
Enumerates every vendored dependency under code/deps/, plus the
auto-fetched GoogleTest, plus the system-required Boost, with:
* upstream project + version + SPDX identifier
* compatibility note for MIT distribution
* a downstream-packager license matrix (header-only consumer
vs CLI binary) clarifying the LGPL §3 vs §4 distinction
relevant to CGAL's header-only consumption
Required for any future Linux-distribution packaging and for the
CGAL submission's compliance check. Cross-referenced from
doc/architecture/dependencies.md.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
76
.clang-format 2
Normal file
76
.clang-format 2
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
# conformallab++ formatting policy
|
||||||
|
#
|
||||||
|
# Captures the style already present in code/include/. Documented here
|
||||||
|
# so clang-format can enforce it locally (scripts/quality/clang-format.sh)
|
||||||
|
# and so new contributors get the same output their editor would on save.
|
||||||
|
#
|
||||||
|
# This is NOT the upstream CGAL clang-format (there isn't one published);
|
||||||
|
# it's the style our tree already uses, mechanically extracted.
|
||||||
|
|
||||||
|
BasedOnStyle: LLVM
|
||||||
|
Language: Cpp
|
||||||
|
Standard: c++17
|
||||||
|
|
||||||
|
IndentWidth: 4
|
||||||
|
TabWidth: 4
|
||||||
|
UseTab: Never
|
||||||
|
ColumnLimit: 100 # loose; readability over hard wrap
|
||||||
|
|
||||||
|
# Brace placement — matches the project tree:
|
||||||
|
# functions / methods → opening brace on a new line (CGAL convention)
|
||||||
|
# structs / classes → opening brace on a new line
|
||||||
|
# else / catch → on the same line as the closing brace of the preceding block
|
||||||
|
BreakBeforeBraces: Custom
|
||||||
|
BraceWrapping:
|
||||||
|
AfterClass: true
|
||||||
|
AfterStruct: true
|
||||||
|
AfterEnum: true
|
||||||
|
AfterFunction: true
|
||||||
|
AfterNamespace: false
|
||||||
|
AfterUnion: true
|
||||||
|
AfterControlStatement: false
|
||||||
|
BeforeElse: false
|
||||||
|
BeforeCatch: false
|
||||||
|
IndentBraces: false
|
||||||
|
SplitEmptyFunction: false
|
||||||
|
SplitEmptyRecord: false
|
||||||
|
SplitEmptyNamespace: true
|
||||||
|
|
||||||
|
# Reference & pointer modifiers attach to the type (`int& x`, not `int &x`).
|
||||||
|
PointerAlignment: Left
|
||||||
|
ReferenceAlignment: Left
|
||||||
|
|
||||||
|
# Aligned `using = ...` blocks are intentional in the trait classes.
|
||||||
|
AlignConsecutiveDeclarations: AcrossEmptyLines
|
||||||
|
AlignConsecutiveAssignments: AcrossEmptyLines
|
||||||
|
AlignTrailingComments: true
|
||||||
|
AlignAfterOpenBracket: Align
|
||||||
|
|
||||||
|
AllowShortFunctionsOnASingleLine: Inline
|
||||||
|
AllowShortIfStatementsOnASingleLine: Never
|
||||||
|
AllowShortLoopsOnASingleLine: false
|
||||||
|
AllowShortBlocksOnASingleLine: Never
|
||||||
|
AllowShortLambdasOnASingleLine: Inline
|
||||||
|
|
||||||
|
# Template-related: break before each parameter when the template line
|
||||||
|
# would otherwise exceed ColumnLimit (matches existing
|
||||||
|
# `template <typename TriangleMesh, typename ...>` patterns).
|
||||||
|
BreakBeforeBinaryOperators: NonAssignment
|
||||||
|
BinPackParameters: false
|
||||||
|
BinPackArguments: false
|
||||||
|
AlwaysBreakTemplateDeclarations: Yes
|
||||||
|
SpaceAfterTemplateKeyword: true
|
||||||
|
|
||||||
|
NamespaceIndentation: None
|
||||||
|
AccessModifierOffset: -4
|
||||||
|
IndentCaseLabels: false
|
||||||
|
|
||||||
|
# Includes: keep manual ordering — re-ordering can break Eigen / CGAL
|
||||||
|
# transitive-include assumptions in subtle ways. We just enforce no
|
||||||
|
# accidental duplicate blank lines.
|
||||||
|
SortIncludes: Never
|
||||||
|
MaxEmptyLinesToKeep: 1
|
||||||
|
KeepEmptyLinesAtTheStartOfBlocks: false
|
||||||
|
|
||||||
|
# Comments: don't touch.
|
||||||
|
ReflowComments: false
|
||||||
45
.clang-tidy 2
Normal file
45
.clang-tidy 2
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
# conformallab++ clang-tidy policy
|
||||||
|
#
|
||||||
|
# Curated, deliberately small. The CGAL header tree triggers tens of
|
||||||
|
# thousands of warnings under default settings (CGAL's chosen style is
|
||||||
|
# pre-C++17 in many places). Restricting to checks that fire on OUR
|
||||||
|
# code, not on transitive CGAL/Eigen/Boost headers, keeps the signal
|
||||||
|
# meaningful.
|
||||||
|
#
|
||||||
|
# Promotion gate: a check moves into this list only when (a) it fires
|
||||||
|
# on code we authored AND (b) the fix is mechanical (no algorithmic
|
||||||
|
# rewrite required). Anything algorithmic belongs in a code review,
|
||||||
|
# not in a static analyser.
|
||||||
|
|
||||||
|
Checks: >
|
||||||
|
-*,
|
||||||
|
bugprone-too-small-loop-variable,
|
||||||
|
bugprone-use-after-move,
|
||||||
|
bugprone-undefined-memory-manipulation,
|
||||||
|
bugprone-integer-division,
|
||||||
|
bugprone-suspicious-string-compare,
|
||||||
|
bugprone-misplaced-widening-cast,
|
||||||
|
bugprone-sizeof-expression,
|
||||||
|
cppcoreguidelines-init-variables,
|
||||||
|
cppcoreguidelines-pro-type-member-init,
|
||||||
|
performance-for-range-copy,
|
||||||
|
performance-implicit-conversion-in-loop,
|
||||||
|
performance-unnecessary-copy-initialization,
|
||||||
|
performance-unnecessary-value-param,
|
||||||
|
readability-misleading-indentation,
|
||||||
|
readability-redundant-smartptr-get,
|
||||||
|
modernize-use-nullptr,
|
||||||
|
modernize-use-override,
|
||||||
|
modernize-deprecated-headers
|
||||||
|
|
||||||
|
# Only emit warnings on our own headers. CGAL/Eigen/etc. live under
|
||||||
|
# `code/deps/` (vendored) or are installed system-wide; we never want
|
||||||
|
# clang-tidy fixes for them.
|
||||||
|
HeaderFilterRegex: '^.*/code/include/(?!deps/).*$'
|
||||||
|
|
||||||
|
WarningsAsErrors: ''
|
||||||
|
|
||||||
|
CheckOptions:
|
||||||
|
- { key: cppcoreguidelines-init-variables.IgnoreArrays, value: true }
|
||||||
|
- { key: performance-for-range-copy.WarnOnAllAutoCopies, value: true }
|
||||||
|
- { key: performance-unnecessary-value-param.AllowedTypes, value: 'Eigen::Vector.*;Eigen::Matrix.*' }
|
||||||
28
.cmake-format 2.yaml
Normal file
28
.cmake-format 2.yaml
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
# conformallab++ cmake-format policy
|
||||||
|
#
|
||||||
|
# Drives the cmake-format / cmake-lint tools used by
|
||||||
|
# scripts/quality/cmake-format.sh. The defaults are deliberately
|
||||||
|
# permissive — the goal is to catch the obvious style drift (mixed
|
||||||
|
# 2-vs-4-space indent, inconsistent argument wrapping, undocumented
|
||||||
|
# options) without forcing a rewrite of every CMakeLists.txt we have.
|
||||||
|
|
||||||
|
format:
|
||||||
|
line_width: 100 # match .clang-format
|
||||||
|
tab_size: 4
|
||||||
|
use_tabchars: false
|
||||||
|
separate_ctrl_name_with_space: false
|
||||||
|
separate_fn_name_with_space: false
|
||||||
|
dangle_parens: false
|
||||||
|
command_case: lower # lowercase commands (cgal/upstream convention)
|
||||||
|
keyword_case: upper # KEYWORDS like PUBLIC/PRIVATE/INTERFACE in caps
|
||||||
|
|
||||||
|
lint:
|
||||||
|
# Whitelist the disables we explicitly accept.
|
||||||
|
disabled_codes:
|
||||||
|
- C0103 # invalid variable name — we use CGAL_/conformallab_ prefixes
|
||||||
|
- C0301 # line too long — handled by line_width, not as a lint error
|
||||||
|
- C0111 # missing docstring on a function — most of ours are obvious
|
||||||
|
|
||||||
|
# Maximum allowed nesting of conditional blocks. 3 is conservative;
|
||||||
|
# raise if we ever genuinely need deeper.
|
||||||
|
max_conditionals_custom_parser: 3
|
||||||
85
.codespellrc 2
Normal file
85
.codespellrc 2
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
# conformallab++ codespell policy
|
||||||
|
#
|
||||||
|
# Driven by scripts/quality/codespell.sh. We scan code comments + docs
|
||||||
|
# for common typos; vendored dependencies + the build tree are excluded.
|
||||||
|
#
|
||||||
|
# False positives go into ignore-words-list (lowercase, comma-separated).
|
||||||
|
# Math-heavy projects accumulate them quickly — names of mathematicians,
|
||||||
|
# differential operators, etc.
|
||||||
|
|
||||||
|
[codespell]
|
||||||
|
skip = code/deps,build,build-*,build_T_*,test-reports,doc/doxygen,.git,*.svg,*.lock,*.pdf,*.png,*.jpg,Doxyfile,*.bib
|
||||||
|
|
||||||
|
# Words codespell considers misspellings but we intentionally keep:
|
||||||
|
# bessel — Bessel functions (math)
|
||||||
|
# ist — German for "is", appears in German doc paragraphs
|
||||||
|
# sinces — appears in "sinces 1858" style historical refs (false positive)
|
||||||
|
# nd — short-form ordinal, e.g. "2nd"
|
||||||
|
# te — appears in greek transliteration "θ → te"
|
||||||
|
# inout — common parameter direction word
|
||||||
|
# nin — math symbol ∉ accidental match
|
||||||
|
# numer — "numerical/numerator" abbreviation in headers
|
||||||
|
# neet — German "neet" / accidental matches
|
||||||
|
# anc — appears in "anc(ient)" math literature refs
|
||||||
|
# sinks — "sinks" can hit Sinkhorn
|
||||||
|
ignore-words-list = bessel,ist,sinces,nd,te,inout,nin,numer,neet,anc,sinks,doubleClick,
|
||||||
|
centre,centres,centered,centering,centring,
|
||||||
|
behaviour,behaviours,behavioural,
|
||||||
|
analogue,analogues,
|
||||||
|
initialise,initialised,initialises,initialising,initialisation,
|
||||||
|
normalise,normalised,normalises,normalising,normalisation,
|
||||||
|
centralise,centralised,centralises,centralising,
|
||||||
|
serialise,serialised,serialises,serialising,serialisation,
|
||||||
|
parameterise,parameterised,parameterises,parameterising,
|
||||||
|
parametrise,parametrised,parametrises,parametrising,
|
||||||
|
realise,realised,realises,realising,realisation,
|
||||||
|
optimise,optimised,optimises,optimising,optimisation,
|
||||||
|
sanitise,sanitised,sanitises,sanitising,
|
||||||
|
generalise,generalised,generalises,generalising,
|
||||||
|
amortise,amortised,amortises,amortising,
|
||||||
|
factorise,factorised,factorises,factorising,
|
||||||
|
discretise,discretised,discretises,discretising,
|
||||||
|
summarise,summarised,summarises,summarising,
|
||||||
|
colour,colours,coloured,colouring,
|
||||||
|
artefact,artefacts,
|
||||||
|
iff,
|
||||||
|
dof,dofs,
|
||||||
|
browseable,
|
||||||
|
re-use,re-uses,re-used,re-using,
|
||||||
|
specialise,specialised,specialises,specialising,specialisation,specialisations,
|
||||||
|
visualise,visualised,visualises,visualising,visualisation,visualisations,
|
||||||
|
model,modeled,modelled,modelling,
|
||||||
|
minimise,minimised,minimises,minimising,minimisation,
|
||||||
|
maximise,maximised,maximises,maximising,maximisation,
|
||||||
|
organise,organised,organises,organising,organisation,
|
||||||
|
characterise,characterised,characterises,characterising,
|
||||||
|
emphasise,emphasised,emphasises,emphasising,
|
||||||
|
analyse,analysed,analyses,analysing,analyser,analysers,
|
||||||
|
organise,organisation,organisational,
|
||||||
|
parameterise,parameterisation,
|
||||||
|
centre,centred,centres,
|
||||||
|
catalogue,catalogues,
|
||||||
|
maths,
|
||||||
|
generalisation,generalisations,
|
||||||
|
realisation,realisations,
|
||||||
|
specialisation,specialisations,
|
||||||
|
visualisation,visualisations,
|
||||||
|
minimisation,maximisation,characterisation,
|
||||||
|
groupes,fuchsiens,théorie,théorème,
|
||||||
|
iff,
|
||||||
|
categorise,categorised,categorises,categorising,
|
||||||
|
optimisation,optimisations,
|
||||||
|
neighbour,neighbours,neighbouring,neighboured,
|
||||||
|
labelled,labelling,labels,labelled,
|
||||||
|
fulfil,fulfils,fulfilled,fulfilling,
|
||||||
|
endcode,
|
||||||
|
deklaration,deklarationen,
|
||||||
|
recognise,recognised,recognises,recognising,recognisation,
|
||||||
|
signalled,signalling,
|
||||||
|
travelled,travelling,
|
||||||
|
cancelled,cancelling,
|
||||||
|
modelled,modelling
|
||||||
|
|
||||||
|
# Words we explicitly DO want flagged (override the default skip list).
|
||||||
|
# Keep empty for now; add as we hit real-but-not-flagged typos.
|
||||||
|
builtin = clear,rare,informal,usage,code,en-GB_to_en-US,names
|
||||||
56
.editorconfig 2
Normal file
56
.editorconfig 2
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
# conformallab++ EditorConfig
|
||||||
|
#
|
||||||
|
# Honoured natively by VSCode (with the EditorConfig extension), CLion,
|
||||||
|
# Vim, Emacs, Sublime, … Covers the basics that .clang-format /
|
||||||
|
# .cmake-format don't catch (Markdown, Python, YAML, shell, JSON, …)
|
||||||
|
# and acts as a cross-IDE fallback when clang-format isn't installed.
|
||||||
|
#
|
||||||
|
# Authoritative formatting for C++ source still comes from .clang-format;
|
||||||
|
# this file just keeps the editor's defaults from fighting it.
|
||||||
|
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
end_of_line = lf
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 4
|
||||||
|
|
||||||
|
# C++ — match .clang-format
|
||||||
|
[*.{h,hpp,cpp,c,cc}]
|
||||||
|
indent_size = 4
|
||||||
|
max_line_length = 100
|
||||||
|
|
||||||
|
# CMake — match .cmake-format.yaml
|
||||||
|
[{CMakeLists.txt,*.cmake}]
|
||||||
|
indent_size = 4
|
||||||
|
max_line_length = 100
|
||||||
|
|
||||||
|
# Python — PEP-8 default
|
||||||
|
[*.py]
|
||||||
|
indent_size = 4
|
||||||
|
max_line_length = 100
|
||||||
|
|
||||||
|
# Shell — Google shell style
|
||||||
|
[*.sh]
|
||||||
|
indent_size = 4
|
||||||
|
max_line_length = 100
|
||||||
|
|
||||||
|
# YAML — community convention
|
||||||
|
[*.{yml,yaml}]
|
||||||
|
indent_size = 2
|
||||||
|
|
||||||
|
# JSON
|
||||||
|
[*.json]
|
||||||
|
indent_size = 2
|
||||||
|
|
||||||
|
# Markdown — preserve trailing spaces (used for line breaks); don't strip
|
||||||
|
[*.md]
|
||||||
|
trim_trailing_whitespace = false
|
||||||
|
max_line_length = off
|
||||||
|
|
||||||
|
# Makefiles must use tabs
|
||||||
|
[Makefile]
|
||||||
|
indent_style = tab
|
||||||
@@ -112,3 +112,51 @@ jobs:
|
|||||||
# the price of guaranteeing the documented workflow stays working.
|
# the price of guaranteeing the documented workflow stays working.
|
||||||
- name: End-to-end smoke test (scripts/try_it.sh)
|
- name: End-to-end smoke test (scripts/try_it.sh)
|
||||||
run: bash scripts/try_it.sh
|
run: bash scripts/try_it.sh
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
# Job 3 — quality-gates (style + convention block)
|
||||||
|
#
|
||||||
|
# Cheap, deterministic checks that should never break unless a contributor
|
||||||
|
# introduces a regression. Each gate is a script under scripts/quality/
|
||||||
|
# and exits 0 only when its tree is clean. These ran for weeks locally
|
||||||
|
# at zero findings before being promoted here.
|
||||||
|
#
|
||||||
|
# Tools installed at job-start (the ci-cpp image already has python3 +
|
||||||
|
# bash; we add codespell + shellcheck on top). Total wall-time: ~30 s
|
||||||
|
# on the eulernest runner.
|
||||||
|
#
|
||||||
|
# Strictly required for merges into main/dev — a regression fails the PR.
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
quality-gates:
|
||||||
|
needs: test-fast
|
||||||
|
runs-on: eulernest
|
||||||
|
container:
|
||||||
|
image: git.eulernest.eu/conformallab/ci-cpp:latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install codespell + shellcheck (job-local)
|
||||||
|
run: |
|
||||||
|
apt-get update -qq
|
||||||
|
apt-get install -y --no-install-recommends \
|
||||||
|
codespell shellcheck
|
||||||
|
|
||||||
|
- name: License headers (every C++ source carries MIT SPDX)
|
||||||
|
run: bash scripts/quality/license-headers.sh
|
||||||
|
|
||||||
|
- name: CGAL conventions (6 rules over CGAL public headers)
|
||||||
|
run: python3 scripts/quality/cgal-conventions.py
|
||||||
|
|
||||||
|
- name: codespell (docs + source comments + script messages)
|
||||||
|
run: bash scripts/quality/codespell.sh
|
||||||
|
|
||||||
|
- name: shellcheck (scripts/**/*.sh, severity=warning, strict)
|
||||||
|
run: bash scripts/quality/shellcheck.sh --strict
|
||||||
|
|
||||||
|
- name: Summary
|
||||||
|
if: always()
|
||||||
|
run: |
|
||||||
|
echo "QUALITY ▸ all four gates passed."
|
||||||
|
echo " see scripts/quality/README.md for the full catalogue"
|
||||||
|
echo " (sanitizers, clang-tidy, coverage, etc. are local-only)"
|
||||||
|
|||||||
40
.gitea/workflows/markdown-links 2.yml
Normal file
40
.gitea/workflows/markdown-links 2.yml
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
name: Markdown link check
|
||||||
|
|
||||||
|
# Verify every internal markdown link in the repo resolves to an existing
|
||||||
|
# file (or anchor). External http(s) links are also probed but with a
|
||||||
|
# loose timeout — flaky third-party hosts must not break our CI.
|
||||||
|
#
|
||||||
|
# Trigger: PRs that touch any *.md file, plus a weekly cron so external
|
||||||
|
# link rot is caught even when nobody is editing docs.
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- "**/*.md"
|
||||||
|
- ".gitea/workflows/markdown-links.yml"
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
paths:
|
||||||
|
- "**/*.md"
|
||||||
|
- ".gitea/workflows/markdown-links.yml"
|
||||||
|
schedule:
|
||||||
|
- cron: "0 5 * * 1" # Monday 05:00 UTC weekly link-rot check
|
||||||
|
workflow_dispatch: {}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
check:
|
||||||
|
runs-on: eulernest
|
||||||
|
container:
|
||||||
|
image: git.eulernest.eu/conformallab/ci-cpp:latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
# ── Pure-python internal link check (no external network needed) ────
|
||||||
|
# We use the same logic that found the 2 broken links before the
|
||||||
|
# reviewer meeting: parse every [text](path) link, check that the
|
||||||
|
# target file exists relative to the source file's directory. Skips
|
||||||
|
# http(s)://, mailto:, and pure-anchor (#fragment) links.
|
||||||
|
- name: Internal link check (all *.md files)
|
||||||
|
run: python3 scripts/check-markdown-links.py
|
||||||
151
doc/architecture/dependencies 2.md
Normal file
151
doc/architecture/dependencies 2.md
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
# Dependencies & standalone-ness
|
||||||
|
|
||||||
|
This document is the single source of truth for "what does conformallab++
|
||||||
|
**require** vs. what does it **optionally** use". Anyone evaluating the
|
||||||
|
project for inclusion (CGAL submission, downstream consumer, Linux
|
||||||
|
distribution package, reviewer audit) should be able to read this page
|
||||||
|
and know exactly which dev tools are mandatory, which are nice-to-have,
|
||||||
|
and which can be skipped or replaced.
|
||||||
|
|
||||||
|
## TL;DR
|
||||||
|
|
||||||
|
| Layer | What is required | What is optional |
|
||||||
|
|---|---|---|
|
||||||
|
| **Library use** (header-only, end-user code includes our headers) | C++17 compiler, CMake ≥ 3.20, Eigen ≥ 3.4 (headers), CGAL ≥ 5.6 (headers), Boost ≥ 1.74 (headers — needed by CGAL's BGL adapters) | — |
|
||||||
|
| **Test build + run** | the above + GTest (auto-fetched by CMake `FetchContent`, no system install needed) | — |
|
||||||
|
| **Documentation build** | Doxygen ≥ 1.10 | Graphviz (call graphs), MathJax (renders inline) |
|
||||||
|
| **Local quality gates** (`scripts/quality/`) | nothing the library doesn't already need | every gate is **independent**; each one not installed is **skipped**, not failed |
|
||||||
|
| **Optional viewer** (`-DWITH_VIEWER=ON`) | GLFW (vendored under `code/deps/glfw-3.4`), libigl, OpenGL system headers | — |
|
||||||
|
|
||||||
|
The library itself is **header-only**. There is no compiled `.so` /
|
||||||
|
`.a` / `.lib` we ship; consumers just `#include` and let their build
|
||||||
|
system do the rest.
|
||||||
|
|
||||||
|
## Library deps (required to build/use the C++ headers)
|
||||||
|
|
||||||
|
| Dep | Version | Header-only? | Purchase | Required by |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| **C++17 compiler** | g++ ≥ 11, clang++ ≥ 14, AppleClang ≥ 14 | n/a | system / brew / apt | everything |
|
||||||
|
| **CMake** | ≥ 3.20 | n/a | system / brew / apt | build orchestration |
|
||||||
|
| **Eigen** | ≥ 3.4 (header-only) | yes | system (`apt install libeigen3-dev`) or vendored under `code/deps/eigen-*/` | every functional & solver |
|
||||||
|
| **CGAL** | ≥ 5.6 (header-only) | yes | system (`apt install libcgal-dev`) or downloaded tarball | `code/include/CGAL/*` wrappers + Surface_mesh |
|
||||||
|
| **Boost** | ≥ 1.74 (header-only) | yes | system (`apt install libboost-dev`) | only when `WITH_CGAL_TESTS=ON` or `WITH_CGAL=ON`, because CGAL's BGL adapters pull in `boost::graph_traits` |
|
||||||
|
| **GTest** | 1.14 | yes (auto-fetched) | `FetchContent_Declare` in `code/CMakeLists.txt` — never installed system-wide | tests only |
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
- The library headers in `code/include/*.hpp` use only Eigen + STL.
|
||||||
|
- The CGAL wrapper headers in `code/include/CGAL/*.h` add CGAL + Boost
|
||||||
|
(transitively).
|
||||||
|
- `code/deps/single_includes/json.hpp` is the vendored
|
||||||
|
[nlohmann/json](https://github.com/nlohmann/json) header — used by
|
||||||
|
`serialization.hpp` only. No system install needed.
|
||||||
|
|
||||||
|
## Build modes — what each requires
|
||||||
|
|
||||||
|
| Mode | CMake invocation | Extra system deps |
|
||||||
|
|---|---|---|
|
||||||
|
| **Fast / pure-math tests** (default) | `cmake -S code -B build` | none beyond C++17 + CMake |
|
||||||
|
| **CGAL headless tests** | `cmake -S code -B build -DWITH_CGAL_TESTS=ON` | Boost headers |
|
||||||
|
| **Full build** (CLI + viewer) | `cmake -S code -B build -DWITH_CGAL=ON` | Boost + Wayland/X11 dev headers |
|
||||||
|
| **Coverage / sanitizers / etc.** | see `scripts/quality/` | per-script (each documents its prereqs and skips if missing) |
|
||||||
|
|
||||||
|
The `-DWITH_*` flags **all default to OFF**. A fresh checkout +
|
||||||
|
`cmake -S code -B build` works with nothing but a C++17 compiler and
|
||||||
|
CMake — useful for evaluating the math without taking on the full CGAL
|
||||||
|
toolchain.
|
||||||
|
|
||||||
|
## Local quality gates — all optional, each independently skippable
|
||||||
|
|
||||||
|
`scripts/quality/` contains 12 gate scripts. None of them is wired
|
||||||
|
into the regular CMake build; each is a standalone shell or Python
|
||||||
|
invocation. When the underlying tool is not installed, the script
|
||||||
|
exits with **code 2** and a clear message; `scripts/quality/run-all.sh`
|
||||||
|
recognises this as **SKIP**, not FAIL.
|
||||||
|
|
||||||
|
| Tool | Used by | Install (macOS) | Install (Debian/Ubuntu) | Behaviour if missing |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `clang-format` ≥ 15 | `clang-format.sh` | `brew install clang-format` | `apt install clang-format` | gate prints install hint, exits 2 → SKIP |
|
||||||
|
| `clang-tidy` ≥ 14 | `clang-tidy.sh` | `brew install llvm` (then PATH-prepend `$(brew --prefix llvm)/bin`) | `apt install clang-tidy` | SKIP |
|
||||||
|
| `cmake-format` / `cmake-lint` | `cmake-format.sh` | `pip3 install --user cmakelang` + PATH-prepend `~/.local/bin` | `pip3 install --user cmakelang` | SKIP |
|
||||||
|
| `codespell` | `codespell.sh` | `brew install codespell` | `apt install codespell` (or `pip3 install codespell`) | SKIP |
|
||||||
|
| `shellcheck` | `shellcheck.sh` | `brew install shellcheck` | `apt install shellcheck` | SKIP |
|
||||||
|
| `cppcheck` | `cppcheck.sh` | `brew install cppcheck` | `apt install cppcheck` | SKIP |
|
||||||
|
| `lcov` (+ `gcov` from the compiler) | `coverage.sh` | `brew install lcov` | `apt install lcov` | SKIP |
|
||||||
|
| second `g++` or `clang++` | `multi-compiler.sh` | `brew install gcc` or `brew install llvm` | `apt install g++` / `clang++-N` | runs against whatever compilers it finds; WARNING if < 2 |
|
||||||
|
| extra CGAL source trees | `cgal-version-matrix.sh` | manually `git clone` under `~/cgal/<ver>/` (or pass `CGAL_ROOTS=...`) | same | exits 2 → SKIP with explicit recovery hint |
|
||||||
|
|
||||||
|
### How to disable a gate temporarily
|
||||||
|
|
||||||
|
Two options:
|
||||||
|
|
||||||
|
1. **Don't install the tool** — `run-all.sh` skips it.
|
||||||
|
2. **Remove the line from `GATES_FAST` / `GATES_SLOW` in `run-all.sh`** —
|
||||||
|
the script is a 5-line edit; no separate "disabled" flag system.
|
||||||
|
|
||||||
|
There is no global "disable all quality gates" switch by design. If
|
||||||
|
the gates feel heavy, run only the fast subset (`run-all.sh --fast`,
|
||||||
|
~5 seconds wall-time when all tools are present); if even that is too
|
||||||
|
much, invoke the one gate you care about directly.
|
||||||
|
|
||||||
|
### `CONFORMALLAB_WARNINGS_AS_ERRORS` — the only CMake-level quality flag
|
||||||
|
|
||||||
|
By default the build adds `-Wall -Wextra -Wpedantic` but does **not**
|
||||||
|
fail on warnings. Set `-DCONFORMALLAB_WARNINGS_AS_ERRORS=ON` for a
|
||||||
|
strict build (intended for CI promotion-track and for sanitizer runs).
|
||||||
|
Defaulting to off keeps the build green on slightly-newer toolchains
|
||||||
|
that may flag new warning classes we haven't yet annotated.
|
||||||
|
|
||||||
|
## CI gates (active on every PR via `.gitea/workflows/`)
|
||||||
|
|
||||||
|
These run inside the `git.eulernest.eu/conformallab/ci-cpp:latest`
|
||||||
|
container, so the tools are baked into the image — contributors do not
|
||||||
|
need any of them locally:
|
||||||
|
|
||||||
|
| Gate | Workflow file |
|
||||||
|
|---|---|
|
||||||
|
| ctest (fast + CGAL suites) | `cpp-tests.yml` |
|
||||||
|
| test-count consistency | same |
|
||||||
|
| End-to-end `try_it.sh` | same |
|
||||||
|
| Markdown link check | `markdown-links.yml` |
|
||||||
|
| Doxygen build + Codeberg Pages publish | `doxygen-pages.yml` |
|
||||||
|
| Mirror to Codeberg | `mirror-to-codeberg.yml` |
|
||||||
|
|
||||||
|
The local quality gates under `scripts/quality/` are **not** in CI
|
||||||
|
today. Each one's promotion path is documented in
|
||||||
|
`scripts/quality/README.md`.
|
||||||
|
|
||||||
|
## Verification of the standalone claim
|
||||||
|
|
||||||
|
Test recipe (any UNIX, ~30 s):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Strip PATH down to system + brew core (no quality tools).
|
||||||
|
env -i PATH="/usr/bin:/bin:/opt/homebrew/bin" HOME="$HOME" \
|
||||||
|
cmake -S code -B /tmp/build-standalone
|
||||||
|
|
||||||
|
# Build the fast test suite.
|
||||||
|
cmake --build /tmp/build-standalone --target conformallab_tests
|
||||||
|
|
||||||
|
# Run them.
|
||||||
|
ctest --test-dir /tmp/build-standalone -E "^cgal\."
|
||||||
|
```
|
||||||
|
|
||||||
|
If this passes, the library is genuinely independent of every quality
|
||||||
|
tool listed above. Tested locally on macOS-arm64 — green.
|
||||||
|
|
||||||
|
For the CGAL-mode equivalent (adds Boost headers as a system dep):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
env -i PATH="/usr/bin:/bin:/opt/homebrew/bin" HOME="$HOME" \
|
||||||
|
cmake -S code -B /tmp/build-cgal -DWITH_CGAL_TESTS=ON
|
||||||
|
cmake --build /tmp/build-cgal --target conformallab_cgal_tests
|
||||||
|
ctest --test-dir /tmp/build-cgal -R "^cgal\."
|
||||||
|
```
|
||||||
|
|
||||||
|
## What is **not** in this repo (out of scope)
|
||||||
|
|
||||||
|
- No package-manager metadata (Debian `.deb`, RPM, Conan, vcpkg, …) —
|
||||||
|
yet. Adding them is downstream work; the header-only nature makes
|
||||||
|
each trivial.
|
||||||
|
- No language bindings (Python, …) — out of scope; the library is C++.
|
||||||
|
- No GPU compute path — out of scope; numerical work is CPU-only.
|
||||||
140
scripts/check-markdown-links 2.py
Executable file
140
scripts/check-markdown-links 2.py
Executable file
@@ -0,0 +1,140 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""scripts/check-markdown-links.py
|
||||||
|
|
||||||
|
Verify every internal `[text](target)` link in every markdown file under
|
||||||
|
the repo resolves to an existing file. Skips:
|
||||||
|
* `http(s)://`, `mailto:`, `ftp://` — external schemes
|
||||||
|
* pure-anchor links `#fragment`
|
||||||
|
* code blocks fenced by ``` ... ```
|
||||||
|
* link targets inside HTML tags that the markdown parser does not
|
||||||
|
treat as links (we operate on raw text — false positives are
|
||||||
|
explicitly excluded via the IGNORE patterns at the top of the file).
|
||||||
|
|
||||||
|
Walks `.` recursively. Skips `code/deps/`, `build*`, `.git/`, the
|
||||||
|
generated `doc/doxygen/` tree, and `node_modules/`.
|
||||||
|
|
||||||
|
Exit codes:
|
||||||
|
0 every internal link resolves
|
||||||
|
1 at least one link is broken — prints `file:line: target` for each
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
import os, re, sys
|
||||||
|
|
||||||
|
ROOT = os.path.abspath(os.path.dirname(os.path.dirname(__file__)))
|
||||||
|
|
||||||
|
# Directories that are not part of our authored docs.
|
||||||
|
SKIP_DIR_PARTS = {"code/deps", ".git", "node_modules", "doc/doxygen"}
|
||||||
|
SKIP_DIR_PREFIXES = ("build", "build_T_", "test-reports")
|
||||||
|
|
||||||
|
# Files in those formats are sometimes generated.
|
||||||
|
SKIP_FILES = {"CHANGELOG-old.md"}
|
||||||
|
|
||||||
|
# Schemes to ignore (external).
|
||||||
|
EXTERNAL_SCHEME_RE = re.compile(r"^[a-z]+://|^mailto:", re.IGNORECASE)
|
||||||
|
|
||||||
|
# Standard markdown link: [text](target)
|
||||||
|
LINK_RE = re.compile(r"\[(?:[^\]]|\\\])+\]\(\s*<?([^)\s<>]+)>?\s*(?:\"[^\"]*\")?\)")
|
||||||
|
|
||||||
|
# Code-fence detection (``` or ~~~)
|
||||||
|
FENCE_RE = re.compile(r"^(?:```|~~~)")
|
||||||
|
|
||||||
|
|
||||||
|
def is_skipped_dir(rel: str) -> bool:
|
||||||
|
parts = rel.split(os.sep)
|
||||||
|
for skip in SKIP_DIR_PARTS:
|
||||||
|
skip_parts = skip.split("/")
|
||||||
|
for i in range(len(parts) - len(skip_parts) + 1):
|
||||||
|
if parts[i : i + len(skip_parts)] == skip_parts:
|
||||||
|
return True
|
||||||
|
for p in parts:
|
||||||
|
if any(p.startswith(pref) for pref in SKIP_DIR_PREFIXES):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def collect_md_files() -> list[str]:
|
||||||
|
md: list[str] = []
|
||||||
|
for dirpath, dirs, files in os.walk(ROOT):
|
||||||
|
rel = os.path.relpath(dirpath, ROOT)
|
||||||
|
if rel != "." and is_skipped_dir(rel):
|
||||||
|
dirs[:] = []
|
||||||
|
continue
|
||||||
|
for f in files:
|
||||||
|
if not f.lower().endswith(".md"):
|
||||||
|
continue
|
||||||
|
if f in SKIP_FILES:
|
||||||
|
continue
|
||||||
|
full = os.path.join(dirpath, f)
|
||||||
|
md.append(full)
|
||||||
|
return sorted(md)
|
||||||
|
|
||||||
|
|
||||||
|
def link_targets(text: str) -> list[tuple[int, str]]:
|
||||||
|
"""Yield (line_no, target) skipping code-fenced lines."""
|
||||||
|
out: list[tuple[int, str]] = []
|
||||||
|
in_fence = False
|
||||||
|
for line_no, line in enumerate(text.splitlines(), start=1):
|
||||||
|
if FENCE_RE.match(line.strip()):
|
||||||
|
in_fence = not in_fence
|
||||||
|
continue
|
||||||
|
if in_fence:
|
||||||
|
continue
|
||||||
|
for m in LINK_RE.finditer(line):
|
||||||
|
out.append((line_no, m.group(1)))
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def resolve(src_path: str, target: str) -> tuple[bool, str | None]:
|
||||||
|
"""Return (ok, expected_path).
|
||||||
|
|
||||||
|
ok=True → link resolves (file exists, or external/anchor we skip).
|
||||||
|
ok=False → file does not exist; expected_path is what we looked for.
|
||||||
|
"""
|
||||||
|
if not target:
|
||||||
|
return True, None
|
||||||
|
if EXTERNAL_SCHEME_RE.match(target):
|
||||||
|
return True, None
|
||||||
|
if target.startswith("#"):
|
||||||
|
# pure in-page anchor — we don't validate anchor existence
|
||||||
|
return True, None
|
||||||
|
# Strip any in-file anchor for file-existence check.
|
||||||
|
target_file = target.split("#", 1)[0]
|
||||||
|
if not target_file:
|
||||||
|
return True, None
|
||||||
|
if target_file.startswith("/"):
|
||||||
|
# Absolute-from-repo-root link.
|
||||||
|
full = os.path.join(ROOT, target_file.lstrip("/"))
|
||||||
|
else:
|
||||||
|
full = os.path.normpath(os.path.join(os.path.dirname(src_path), target_file))
|
||||||
|
return os.path.exists(full), full
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
files = collect_md_files()
|
||||||
|
broken: list[tuple[str, int, str, str]] = []
|
||||||
|
n_links = 0
|
||||||
|
for src in files:
|
||||||
|
try:
|
||||||
|
with open(src, encoding="utf-8") as f:
|
||||||
|
text = f.read()
|
||||||
|
except OSError as e:
|
||||||
|
print(f"WARN: cannot read {src}: {e}", file=sys.stderr)
|
||||||
|
continue
|
||||||
|
for line_no, target in link_targets(text):
|
||||||
|
n_links += 1
|
||||||
|
ok, expected = resolve(src, target)
|
||||||
|
if not ok:
|
||||||
|
broken.append((src, line_no, target, expected or "?"))
|
||||||
|
print(f"Scanned {len(files)} markdown files, {n_links} internal links.")
|
||||||
|
if broken:
|
||||||
|
print(f"\nBROKEN: {len(broken)} link(s) do not resolve:")
|
||||||
|
for src, line_no, target, expected in broken:
|
||||||
|
rel = os.path.relpath(src, ROOT)
|
||||||
|
print(f" {rel}:{line_no}: ({target}) → {os.path.relpath(expected, ROOT)} (missing)")
|
||||||
|
return 1
|
||||||
|
print("OK: every internal markdown link resolves.")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
88
scripts/quality/README 2.md
Normal file
88
scripts/quality/README 2.md
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
# 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 — ~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/<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.
|
||||||
207
scripts/quality/cgal-conventions 2.py
Executable file
207
scripts/quality/cgal-conventions 2.py
Executable file
@@ -0,0 +1,207 @@
|
|||||||
|
#!/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())
|
||||||
127
scripts/quality/cgal-version-matrix 2.sh
Executable file
127
scripts/quality/cgal-version-matrix 2.sh
Executable file
@@ -0,0 +1,127 @@
|
|||||||
|
#!/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
|
||||||
96
scripts/quality/clang-format 2.sh
Executable file
96
scripts/quality/clang-format 2.sh
Executable file
@@ -0,0 +1,96 @@
|
|||||||
|
#!/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
|
||||||
110
scripts/quality/clang-tidy 2.sh
Executable file
110
scripts/quality/clang-tidy 2.sh
Executable file
@@ -0,0 +1,110 @@
|
|||||||
|
#!/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
|
||||||
126
scripts/quality/cmake-format 2.sh
Executable file
126
scripts/quality/cmake-format 2.sh
Executable file
@@ -0,0 +1,126 @@
|
|||||||
|
#!/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
|
||||||
87
scripts/quality/codespell 2.sh
Executable file
87
scripts/quality/codespell 2.sh
Executable file
@@ -0,0 +1,87 @@
|
|||||||
|
#!/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
|
||||||
128
scripts/quality/coverage 2.sh
Executable file
128
scripts/quality/coverage 2.sh
Executable file
@@ -0,0 +1,128 @@
|
|||||||
|
#!/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
|
||||||
97
scripts/quality/cppcheck 2.sh
Executable file
97
scripts/quality/cppcheck 2.sh
Executable file
@@ -0,0 +1,97 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# scripts/quality/cppcheck.sh
|
||||||
|
#
|
||||||
|
# Run cppcheck over the public headers. Complementary to clang-tidy:
|
||||||
|
# cppcheck has different heuristics, fewer false-positives on heavy
|
||||||
|
# template code (CGAL/Eigen), and catches some bugs (unused includes,
|
||||||
|
# memory leaks in detail/) that clang-tidy is bad at.
|
||||||
|
#
|
||||||
|
# Local-only. Promotion to CI when the existing tree is finding-free
|
||||||
|
# at the chosen severity level.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# bash scripts/quality/cppcheck.sh # error+warning only
|
||||||
|
# bash scripts/quality/cppcheck.sh --strict # +style, exit 1 on any
|
||||||
|
# bash scripts/quality/cppcheck.sh --all # absolute everything,
|
||||||
|
# useful for diffs only
|
||||||
|
#
|
||||||
|
# Exit codes:
|
||||||
|
# 0 no findings at the chosen severity, or findings but not --strict
|
||||||
|
# 1 findings + --strict
|
||||||
|
# 2 prerequisite missing
|
||||||
|
|
||||||
|
set -uo pipefail
|
||||||
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||||
|
cd "$ROOT" || exit 2
|
||||||
|
|
||||||
|
command -v cppcheck >/dev/null 2>&1 || {
|
||||||
|
echo "FAIL: cppcheck not in PATH." >&2
|
||||||
|
echo " macOS: brew install cppcheck" >&2
|
||||||
|
echo " Linux: sudo apt install cppcheck" >&2
|
||||||
|
exit 2
|
||||||
|
}
|
||||||
|
|
||||||
|
STRICT=0
|
||||||
|
ALL=0
|
||||||
|
for arg in "$@"; do
|
||||||
|
case "$arg" in
|
||||||
|
--strict) STRICT=1 ;;
|
||||||
|
--all) ALL=1 ;;
|
||||||
|
*) echo "Unknown arg: $arg" >&2; exit 2 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
ENABLE="warning"
|
||||||
|
if [ "$STRICT" -eq 1 ]; then ENABLE="warning,style"; fi
|
||||||
|
if [ "$ALL" -eq 1 ]; then ENABLE="all"; fi
|
||||||
|
|
||||||
|
BUILD_DIR="build-cppcheck"
|
||||||
|
LOG="$BUILD_DIR/cppcheck.log"
|
||||||
|
mkdir -p "$BUILD_DIR"
|
||||||
|
|
||||||
|
echo "cppcheck ($(cppcheck --version 2>&1 | head -1))"
|
||||||
|
echo " enable: $ENABLE"
|
||||||
|
echo " log: $LOG"
|
||||||
|
echo
|
||||||
|
|
||||||
|
# Suppress noise classes that are not actionable in our project:
|
||||||
|
# missingIncludeSystem — CGAL/Eigen/Boost headers are intentionally
|
||||||
|
# included implicitly; cppcheck cannot resolve.
|
||||||
|
# unmatchedSuppression — cosmetic.
|
||||||
|
# unusedFunction — header-only; many `inline` helpers ARE used,
|
||||||
|
# cppcheck can't see across TUs.
|
||||||
|
# normalCheckLevelMaxBranches — informational, not a finding.
|
||||||
|
#
|
||||||
|
# We point cppcheck at code/include/ only. The deps tree is third-party
|
||||||
|
# code and out of scope.
|
||||||
|
|
||||||
|
cppcheck \
|
||||||
|
--enable="$ENABLE" \
|
||||||
|
--std=c++17 \
|
||||||
|
--quiet \
|
||||||
|
--error-exitcode=2 \
|
||||||
|
--inline-suppr \
|
||||||
|
--suppress=missingIncludeSystem \
|
||||||
|
--suppress=unmatchedSuppression \
|
||||||
|
--suppress=unusedFunction \
|
||||||
|
--suppress=normalCheckLevelMaxBranches \
|
||||||
|
-I code/include \
|
||||||
|
code/include 2>&1 | tee "$LOG"
|
||||||
|
rc=$?
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "── Summary ──"
|
||||||
|
n=$(grep -cE "\[(error|warning|style|performance|portability)\]" "$LOG" || true)
|
||||||
|
echo " total findings: $n"
|
||||||
|
echo " full log: $LOG"
|
||||||
|
|
||||||
|
if [ "$STRICT" -eq 1 ] && [ "$n" -gt 0 ]; then
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [ "$rc" -eq 2 ] && [ "$STRICT" -ne 1 ]; then
|
||||||
|
# cppcheck signalled "error" severity but caller didn't ask --strict.
|
||||||
|
echo
|
||||||
|
echo "NOTE: cppcheck reported an `error`-severity finding. Even"
|
||||||
|
echo " without --strict, please review the log."
|
||||||
|
fi
|
||||||
|
exit 0
|
||||||
133
scripts/quality/license-headers 2.sh
Executable file
133
scripts/quality/license-headers 2.sh
Executable file
@@ -0,0 +1,133 @@
|
|||||||
|
#!/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
|
||||||
119
scripts/quality/multi-compiler 2.sh
Executable file
119
scripts/quality/multi-compiler 2.sh
Executable file
@@ -0,0 +1,119 @@
|
|||||||
|
#!/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
|
||||||
117
scripts/quality/reproducible-build 2.sh
Executable file
117
scripts/quality/reproducible-build 2.sh
Executable file
@@ -0,0 +1,117 @@
|
|||||||
|
#!/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
|
||||||
119
scripts/quality/run-all 2.sh
Executable file
119
scripts/quality/run-all 2.sh
Executable file
@@ -0,0 +1,119 @@
|
|||||||
|
#!/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
|
||||||
95
scripts/quality/sanitizers 2.sh
Executable file
95
scripts/quality/sanitizers 2.sh
Executable file
@@ -0,0 +1,95 @@
|
|||||||
|
#!/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
|
||||||
79
scripts/quality/shellcheck 2.sh
Executable file
79
scripts/quality/shellcheck 2.sh
Executable file
@@ -0,0 +1,79 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# scripts/quality/shellcheck.sh
|
||||||
|
#
|
||||||
|
# Run shellcheck across every Bash script we own (scripts/**/*.sh).
|
||||||
|
# Skips the macOS duplicate artefacts (` 2.sh`).
|
||||||
|
#
|
||||||
|
# Local-only. CI promotion once every script is shellcheck-clean.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# bash scripts/quality/shellcheck.sh # warn + advisory exit 0
|
||||||
|
# bash scripts/quality/shellcheck.sh --strict # fail on any finding
|
||||||
|
#
|
||||||
|
# Exit codes:
|
||||||
|
# 0 no findings, or findings but --strict not set
|
||||||
|
# 1 --strict was set and shellcheck reported findings
|
||||||
|
# 2 prerequisite missing
|
||||||
|
|
||||||
|
set -uo pipefail
|
||||||
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||||
|
cd "$ROOT" || exit 2
|
||||||
|
|
||||||
|
command -v shellcheck >/dev/null 2>&1 || {
|
||||||
|
echo "FAIL: shellcheck not in PATH." >&2
|
||||||
|
echo " macOS: brew install shellcheck" >&2
|
||||||
|
echo " Linux: sudo apt install shellcheck" >&2
|
||||||
|
exit 2
|
||||||
|
}
|
||||||
|
|
||||||
|
STRICT=0
|
||||||
|
for arg in "$@"; do
|
||||||
|
case "$arg" in
|
||||||
|
--strict) STRICT=1 ;;
|
||||||
|
*) echo "Unknown arg: $arg" >&2; exit 2 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
FILES="$(find scripts -name "*.sh" -type f 2>/dev/null \
|
||||||
|
| grep -v " 2\.sh" \
|
||||||
|
| sort)"
|
||||||
|
|
||||||
|
if [ -z "$FILES" ]; then
|
||||||
|
echo "FAIL: no shell scripts found under scripts/" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "shellcheck ($(shellcheck --version | sed -n '2p'))"
|
||||||
|
echo "Scanning shell scripts under scripts/"
|
||||||
|
echo
|
||||||
|
|
||||||
|
n_total=0
|
||||||
|
n_with_findings=0
|
||||||
|
total_findings=0
|
||||||
|
while IFS= read -r f; do
|
||||||
|
[ -z "$f" ] && continue
|
||||||
|
n_total=$((n_total + 1))
|
||||||
|
# -S style: warnings + above (skip "info" and "style" noise).
|
||||||
|
out="$(shellcheck --severity=warning --shell=bash "$f" 2>&1)"
|
||||||
|
if [ -n "$out" ]; then
|
||||||
|
echo "── $f ──"
|
||||||
|
echo "$out"
|
||||||
|
echo
|
||||||
|
n_with_findings=$((n_with_findings + 1))
|
||||||
|
# rough count: one finding per "In <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
|
||||||
Reference in New Issue
Block a user