From 7f79060de07b22eaf9c2c24f1ee1fc5b91745016 Mon Sep 17 00:00:00 2001 From: Loic Pottier Date: Thu, 4 Jun 2026 12:26:56 -0700 Subject: [PATCH 1/7] Added commit pre hooks for clang-format + clang-tidy Signed-off-by: Loic Pottier --- .clang-tidy | 42 ++++++++++ .githooks/pre-commit | 150 ++++++++++++++++++++++++++++++++++++ CMakeLists.txt | 2 + cmake/setup-git-hooks.cmake | 23 ++++++ 4 files changed, 217 insertions(+) create mode 100644 .clang-tidy create mode 100755 .githooks/pre-commit create mode 100644 cmake/setup-git-hooks.cmake diff --git a/.clang-tidy b/.clang-tidy new file mode 100644 index 00000000..bec9f7d7 --- /dev/null +++ b/.clang-tidy @@ -0,0 +1,42 @@ +--- +# AMS clang-tidy configuration +# +# Run manually: clang-tidy -p build/ src/AMSlib/wf/workflow.hpp +# Integrated via the pre-commit git hook. + +Checks: > + -*, + bugprone-*, + -bugprone-easily-swappable-parameters, + -bugprone-narrowing-conversions, + misc-*, + -misc-non-private-member-variables-in-classes, + -misc-no-recursion, + modernize-*, + -modernize-use-trailing-return-type, + -modernize-avoid-c-arrays, + -modernize-use-nodiscard, + performance-*, + readability-identifier-naming, + readability-redundant-*, + readability-simplify-*, + readability-braces-around-statements, + +WarningsAsErrors: '' + +HeaderFilterRegex: 'src/AMSlib/.*' + +CheckOptions: + - key: readability-identifier-naming.ClassCase + value: CamelCase + - key: readability-identifier-naming.PrivateMemberPrefix + value: '_' + - key: readability-identifier-naming.FunctionCase + value: camelBack + - key: readability-identifier-naming.NamespaceCase + value: lower_case + - key: modernize-use-override.AllowOverrideAndFinal + value: false + - key: performance-unnecessary-value-param.AllowedTypes + value: 'std::shared_ptr;std::unique_ptr' + diff --git a/.githooks/pre-commit b/.githooks/pre-commit new file mode 100755 index 00000000..9c5c29a4 --- /dev/null +++ b/.githooks/pre-commit @@ -0,0 +1,150 @@ +#!/usr/bin/env bash +# +# Git pre-commit hook for AMS: +# 1. Runs clang-format on staged C/C++ files (auto-fixes and re-stages) +# 2. Runs clang-tidy on staged .cpp files (reports errors, blocks commit) +# +# Install (automatic via cmake/setup-git-hooks.cmake): +# .githooks/pre-commit +# +# Skip temporarily: +# git commit --no-verify +# +# Skip only clang-tidy (format still runs): +# AMS_SKIP_TIDY=1 git commit +# +# Requires: clang-format, clang-tidy on PATH +# clang-tidy requires compile_commands.json (generated by cmake) + +set -euo pipefail + +# --------------------------------------------------------------------------- +# Find tools +# --------------------------------------------------------------------------- + +find_tool() { + local base="$1" + for candidate in "${base}-18" "${base}-17" "${base}-16" "${base}-15" "${base}-14" "${base}"; do + if command -v "$candidate" &>/dev/null; then + echo "$candidate" + return + fi + done +} + +CLANG_FORMAT=$(find_tool clang-format) +CLANG_TIDY=$(find_tool clang-tidy) + +# File extensions to process +EXTENSIONS='\.(cpp|hpp|cc|hh|h|c|cxx|hxx)$' +# clang-tidy only on translation units (not headers) +TIDY_EXTENSIONS='\.(cpp|cc|c|cxx)$' + +# --------------------------------------------------------------------------- +# Collect staged files +# --------------------------------------------------------------------------- + +STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep -E "$EXTENSIONS" || true) + +if [[ -z "$STAGED_FILES" ]]; then + exit 0 +fi + +# --------------------------------------------------------------------------- +# Phase 1: clang-format (auto-fix + re-stage) +# --------------------------------------------------------------------------- + +if [[ -n "$CLANG_FORMAT" ]]; then + REFORMATTED=0 + + for file in $STAGED_FILES; do + [[ -f "$file" ]] || continue + + "$CLANG_FORMAT" -i -style=file "$file" + + if ! git diff --quiet -- "$file"; then + echo " clang-format: reformatted $file" + git add "$file" + REFORMATTED=1 + fi + done + + if [[ "$REFORMATTED" -eq 1 ]]; then + echo "" + echo "clang-format: reformatted files have been re-staged." + echo "" + fi +else + echo "WARNING: clang-format not found, skipping format check." +fi + +# --------------------------------------------------------------------------- +# Phase 2: clang-tidy (lint, block on errors) +# --------------------------------------------------------------------------- + +# Allow skipping clang-tidy via environment variable +if [[ "${AMS_SKIP_TIDY:-0}" == "1" ]]; then + exit 0 +fi + +if [[ -z "$CLANG_TIDY" ]]; then + echo "WARNING: clang-tidy not found, skipping lint check." + exit 0 +fi + +# Find compile_commands.json — check common build directory names +COMPILE_DB="" +for dir in build build-* cmake-build-* out; do + candidate=$(find . -maxdepth 2 -path "./${dir}/compile_commands.json" -print -quit 2>/dev/null) + if [[ -n "$candidate" ]]; then + COMPILE_DB="$candidate" + break + fi +done + +if [[ -z "$COMPILE_DB" ]]; then + echo "WARNING: compile_commands.json not found, skipping clang-tidy." + echo " Run cmake first to generate it." + exit 0 +fi + +COMPILE_DB_DIR=$(dirname "$COMPILE_DB") + +# Filter to translation units only +TIDY_FILES=$(echo "$STAGED_FILES" | grep -E "$TIDY_EXTENSIONS" || true) + +if [[ -z "$TIDY_FILES" ]]; then + exit 0 +fi + +TIDY_ERRORS=0 + +for file in $TIDY_FILES; do + [[ -f "$file" ]] || continue + + # Only run on files that appear in compile_commands.json + # (headers and test files outside the build may not be there) + if ! grep -q "\"file\".*$(basename "$file")\"" "$COMPILE_DB" 2>/dev/null; then + continue + fi + + echo " clang-tidy: checking $file" + + if ! "$CLANG_TIDY" -p "$COMPILE_DB_DIR" --quiet "$file" 2>&1 | \ + grep -v "^$" | grep -v "warnings generated" ; then + : # clean + else + TIDY_ERRORS=1 + fi +done + +if [[ "$TIDY_ERRORS" -eq 1 ]]; then + echo "" + echo "clang-tidy: issues found (see above)." + echo "Fix them, or skip with: AMS_SKIP_TIDY=1 git commit" + echo "Or skip all hooks with: git commit --no-verify" + exit 1 +fi + +exit 0 + diff --git a/CMakeLists.txt b/CMakeLists.txt index a04fd71d..fb71fd02 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -14,6 +14,8 @@ cmake_policy(SET CMP0074 NEW) project(AMS VERSION 0.1.1 LANGUAGES CXX C) +# We insall AMS githook to run clang-format etc when commiting +include(cmake/setup-git-hooks.cmake) # NOTE: This may break some of our integrations with the applications. But flux requires > C++20, RMQ requires C++17 and although AMS does not have # any restrictions on the CXX standard the application may impose those. The solution would be to compile RMQ with an older GCC version (8) and diff --git a/cmake/setup-git-hooks.cmake b/cmake/setup-git-hooks.cmake new file mode 100644 index 00000000..a342cc65 --- /dev/null +++ b/cmake/setup-git-hooks.cmake @@ -0,0 +1,23 @@ +# Automatically configure git hooks when the project is configured. +# Place in cmake/setup-git-hooks.cmake and include from the top-level CMakeLists.txt. + +find_package(Git QUIET) + +if(GIT_FOUND AND EXISTS "${PROJECT_SOURCE_DIR}/.git") + # Only set if not already configured (respects developer overrides) + execute_process( + COMMAND ${GIT_EXECUTABLE} config --get core.hooksPath + WORKING_DIRECTORY ${PROJECT_SOURCE_DIR} + OUTPUT_VARIABLE _current_hooks_path + OUTPUT_STRIP_TRAILING_WHITESPACE + RESULT_VARIABLE _rc + ) + + if(NOT _rc EQUAL 0 OR NOT _current_hooks_path STREQUAL ".githooks") + message(STATUS "Setting git hooks path to .githooks/") + execute_process( + COMMAND ${GIT_EXECUTABLE} config core.hooksPath .githooks + WORKING_DIRECTORY ${PROJECT_SOURCE_DIR} + ) + endif() +endif() From 754e13a85490f79e3f68b8c75b266466f9ec29db Mon Sep 17 00:00:00 2001 From: Loic Pottier Date: Thu, 4 Jun 2026 15:38:52 -0700 Subject: [PATCH 2/7] Fixed clang tidy support Signed-off-by: Loic Pottier --- .clang-tidy | 5 +---- .githooks/pre-commit | 34 +++++++++++++++++++++------------- 2 files changed, 22 insertions(+), 17 deletions(-) diff --git a/.clang-tidy b/.clang-tidy index bec9f7d7..e4269c07 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -17,10 +17,7 @@ Checks: > -modernize-avoid-c-arrays, -modernize-use-nodiscard, performance-*, - readability-identifier-naming, - readability-redundant-*, - readability-simplify-*, - readability-braces-around-statements, + readability-identifier-naming WarningsAsErrors: '' diff --git a/.githooks/pre-commit b/.githooks/pre-commit index 9c5c29a4..1be05145 100755 --- a/.githooks/pre-commit +++ b/.githooks/pre-commit @@ -13,6 +13,9 @@ # Skip only clang-tidy (format still runs): # AMS_SKIP_TIDY=1 git commit # +# Specify build directory (for compile_commands.json): +# export AMS_BUILD_DIR=/path/to/build +# # Requires: clang-format, clang-tidy on PATH # clang-tidy requires compile_commands.json (generated by cmake) @@ -38,13 +41,14 @@ CLANG_TIDY=$(find_tool clang-tidy) # File extensions to process EXTENSIONS='\.(cpp|hpp|cc|hh|h|c|cxx|hxx)$' # clang-tidy only on translation units (not headers) -TIDY_EXTENSIONS='\.(cpp|cc|c|cxx)$' +TIDY_EXTENSIONS='\.(cpp|cc|c|cxx|hpp|h|hh|hxx)$' # --------------------------------------------------------------------------- # Collect staged files # --------------------------------------------------------------------------- -STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep -E "$EXTENSIONS" || true) +REPO_ROOT=$(git rev-parse --show-toplevel) +STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep -E "$EXTENSIONS" | sed "s|^|${REPO_ROOT}/|" || true) if [[ -z "$STAGED_FILES" ]]; then exit 0 @@ -92,19 +96,25 @@ if [[ -z "$CLANG_TIDY" ]]; then exit 0 fi -# Find compile_commands.json — check common build directory names +# Find compile_commands.json +# Priority: AMS_BUILD_DIR env var > common build directory names COMPILE_DB="" -for dir in build build-* cmake-build-* out; do - candidate=$(find . -maxdepth 2 -path "./${dir}/compile_commands.json" -print -quit 2>/dev/null) - if [[ -n "$candidate" ]]; then - COMPILE_DB="$candidate" - break - fi -done + +if [[ -n "${AMS_BUILD_DIR:-}" && -f "${AMS_BUILD_DIR}/compile_commands.json" ]]; then + COMPILE_DB="${AMS_BUILD_DIR}/compile_commands.json" +else + for dir in build build-* cmake-build-* out; do + candidate=$(find . -maxdepth 2 -path "./${dir}/compile_commands.json" -print -quit 2>/dev/null) + if [[ -n "$candidate" ]]; then + COMPILE_DB="$candidate" + break + fi + done +fi if [[ -z "$COMPILE_DB" ]]; then echo "WARNING: compile_commands.json not found, skipping clang-tidy." - echo " Run cmake first to generate it." + echo " Run cmake first, or set AMS_BUILD_DIR to your build directory." exit 0 fi @@ -120,8 +130,6 @@ fi TIDY_ERRORS=0 for file in $TIDY_FILES; do - [[ -f "$file" ]] || continue - # Only run on files that appear in compile_commands.json # (headers and test files outside the build may not be there) if ! grep -q "\"file\".*$(basename "$file")\"" "$COMPILE_DB" 2>/dev/null; then From d399cd736bd828e6e03350f8a401427bccbd421e Mon Sep 17 00:00:00 2001 From: Loic Pottier Date: Thu, 4 Jun 2026 17:36:09 -0700 Subject: [PATCH 3/7] Added python linting to the pre-hook (ruff-based) Signed-off-by: Loic Pottier --- .githooks/pre-commit | 143 +++++++++++++++++++++++++++---------------- pyproject.toml | 60 ++++-------------- 2 files changed, 104 insertions(+), 99 deletions(-) diff --git a/.githooks/pre-commit b/.githooks/pre-commit index 1be05145..ce35de63 100755 --- a/.githooks/pre-commit +++ b/.githooks/pre-commit @@ -1,8 +1,9 @@ #!/usr/bin/env bash # # Git pre-commit hook for AMS: -# 1. Runs clang-format on staged C/C++ files (auto-fixes and re-stages) -# 2. Runs clang-tidy on staged .cpp files (reports errors, blocks commit) +# Phase 1: clang-format on staged C/C++ files (auto-fixes, re-stages) +# Phase 2: ruff on staged Python files (auto-fixes, re-stages) +# Phase 3: clang-tidy on staged .cpp files (reports errors, blocks commit) # # Install (automatic via cmake/setup-git-hooks.cmake): # .githooks/pre-commit @@ -10,21 +11,17 @@ # Skip temporarily: # git commit --no-verify # -# Skip only clang-tidy (format still runs): +# Skip only clang-tidy (format + ruff still run): # AMS_SKIP_TIDY=1 git commit # # Specify build directory (for compile_commands.json): # export AMS_BUILD_DIR=/path/to/build # -# Requires: clang-format, clang-tidy on PATH +# Requires: clang-format, clang-tidy, ruff on PATH # clang-tidy requires compile_commands.json (generated by cmake) set -euo pipefail -# --------------------------------------------------------------------------- -# Find tools -# --------------------------------------------------------------------------- - find_tool() { local base="$1" for candidate in "${base}-18" "${base}-17" "${base}-16" "${base}-15" "${base}-14" "${base}"; do @@ -37,54 +34,99 @@ find_tool() { CLANG_FORMAT=$(find_tool clang-format) CLANG_TIDY=$(find_tool clang-tidy) +RUFF=$(command -v ruff 2>/dev/null || true) -# File extensions to process -EXTENSIONS='\.(cpp|hpp|cc|hh|h|c|cxx|hxx)$' -# clang-tidy only on translation units (not headers) -TIDY_EXTENSIONS='\.(cpp|cc|c|cxx|hpp|h|hh|hxx)$' - -# --------------------------------------------------------------------------- -# Collect staged files -# --------------------------------------------------------------------------- +CPP_EXTENSIONS='\.(cpp|hpp|cc|hh|h|c|cxx|hxx)$' +TIDY_EXTENSIONS='\.(cpp|cc|c|cxx)$' +PY_EXTENSIONS='\.py$' REPO_ROOT=$(git rev-parse --show-toplevel) -STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep -E "$EXTENSIONS" | sed "s|^|${REPO_ROOT}/|" || true) -if [[ -z "$STAGED_FILES" ]]; then +ALL_STAGED=$(git diff --cached --name-only --diff-filter=ACM || true) + +CPP_FILES=$(echo "$ALL_STAGED" | grep -E "$CPP_EXTENSIONS" | sed "s|^|${REPO_ROOT}/|" || true) +PY_FILES=$(echo "$ALL_STAGED" | grep -E "$PY_EXTENSIONS" | sed "s|^|${REPO_ROOT}/|" || true) + +if [[ -z "$CPP_FILES" && -z "$PY_FILES" ]]; then exit 0 fi -# --------------------------------------------------------------------------- -# Phase 1: clang-format (auto-fix + re-stage) -# --------------------------------------------------------------------------- +if [[ -n "$CPP_FILES" ]]; then + if [[ -n "$CLANG_FORMAT" ]]; then + REFORMATTED=0 -if [[ -n "$CLANG_FORMAT" ]]; then - REFORMATTED=0 + for file in $CPP_FILES; do + "$CLANG_FORMAT" -i -style=file "$file" - for file in $STAGED_FILES; do - [[ -f "$file" ]] || continue + if ! git diff --quiet -- "$file"; then + echo " clang-format: reformatted $file" + git add "$file" + REFORMATTED=1 + fi + done - "$CLANG_FORMAT" -i -style=file "$file" + if [[ "$REFORMATTED" -eq 1 ]]; then + echo "" + echo "clang-format: reformatted files have been re-staged." + echo "" + fi + else + echo "WARNING: clang-format not found, skipping C/C++ format check." + fi +fi - if ! git diff --quiet -- "$file"; then - echo " clang-format: reformatted $file" - git add "$file" - REFORMATTED=1 +if [[ -n "$PY_FILES" ]]; then + if [[ -n "$RUFF" ]]; then + PY_REFORMATTED=0 + + # Format (equivalent to black) + for file in $PY_FILES; do + "$RUFF" format --quiet "$file" + done + + # Lint with auto-fix (import sorting, safe fixes) + for file in $PY_FILES; do + "$RUFF" check --fix --quiet "$file" 2>/dev/null || true + done + + # Re-stage any modified files + for file in $PY_FILES; do + if ! git diff --quiet -- "$file"; then + echo " ruff: reformatted $file" + git add "$file" + PY_REFORMATTED=1 + fi + done + + if [[ "$PY_REFORMATTED" -eq 1 ]]; then + echo "" + echo "ruff: reformatted files have been re-staged." + echo "" fi - done - if [[ "$REFORMATTED" -eq 1 ]]; then - echo "" - echo "clang-format: reformatted files have been re-staged." - echo "" + # Lint check (report remaining issues — unfixable ones) + RUFF_ERRORS=0 + for file in $PY_FILES; do + if ! "$RUFF" check --quiet "$file" 2>/dev/null; then + RUFF_ERRORS=1 + fi + done + + if [[ "$RUFF_ERRORS" -eq 1 ]]; then + echo "" + echo "ruff: lint errors found (see above). Fix them before committing." + echo "Or skip all hooks with: git commit --no-verify" + exit 1 + fi + else + echo "WARNING: ruff not found, skipping Python format/lint check." + echo " Install with: pip install ruff" fi -else - echo "WARNING: clang-format not found, skipping format check." fi -# --------------------------------------------------------------------------- -# Phase 2: clang-tidy (lint, block on errors) -# --------------------------------------------------------------------------- +if [[ -z "$CPP_FILES" ]]; then + exit 0 +fi # Allow skipping clang-tidy via environment variable if [[ "${AMS_SKIP_TIDY:-0}" == "1" ]]; then @@ -92,19 +134,18 @@ if [[ "${AMS_SKIP_TIDY:-0}" == "1" ]]; then fi if [[ -z "$CLANG_TIDY" ]]; then - echo "WARNING: clang-tidy not found, skipping lint check." + echo "WARNING: clang-tidy not found, skipping C++ lint check." exit 0 fi # Find compile_commands.json -# Priority: AMS_BUILD_DIR env var > common build directory names COMPILE_DB="" if [[ -n "${AMS_BUILD_DIR:-}" && -f "${AMS_BUILD_DIR}/compile_commands.json" ]]; then COMPILE_DB="${AMS_BUILD_DIR}/compile_commands.json" else for dir in build build-* cmake-build-* out; do - candidate=$(find . -maxdepth 2 -path "./${dir}/compile_commands.json" -print -quit 2>/dev/null) + candidate=$(find "${REPO_ROOT}" -maxdepth 2 -path "${REPO_ROOT}/${dir}/compile_commands.json" -print -quit 2>/dev/null) if [[ -n "$candidate" ]]; then COMPILE_DB="$candidate" break @@ -121,7 +162,7 @@ fi COMPILE_DB_DIR=$(dirname "$COMPILE_DB") # Filter to translation units only -TIDY_FILES=$(echo "$STAGED_FILES" | grep -E "$TIDY_EXTENSIONS" || true) +TIDY_FILES=$(echo "$CPP_FILES" | grep -E "$TIDY_EXTENSIONS" || true) if [[ -z "$TIDY_FILES" ]]; then exit 0 @@ -131,28 +172,26 @@ TIDY_ERRORS=0 for file in $TIDY_FILES; do # Only run on files that appear in compile_commands.json - # (headers and test files outside the build may not be there) if ! grep -q "\"file\".*$(basename "$file")\"" "$COMPILE_DB" 2>/dev/null; then continue fi echo " clang-tidy: checking $file" - if ! "$CLANG_TIDY" -p "$COMPILE_DB_DIR" --quiet "$file" 2>&1 | \ - grep -v "^$" | grep -v "warnings generated" ; then - : # clean - else + # clang-tidy exits non-zero when WarningsAsErrors checks fire. + # Warnings (not promoted to errors) print but don't affect the exit code. + if ! "$CLANG_TIDY" -p "$COMPILE_DB_DIR" --quiet "$file"; then TIDY_ERRORS=1 fi done if [[ "$TIDY_ERRORS" -eq 1 ]]; then echo "" - echo "clang-tidy: issues found (see above)." - echo "Fix them, or skip with: AMS_SKIP_TIDY=1 git commit" + echo "clang-tidy: errors found (see above)." + echo "Warnings are informational and do not block the commit." + echo "Fix errors, or skip with: AMS_SKIP_TIDY=1 git commit" echo "Or skip all hooks with: git commit --no-verify" exit 1 fi exit 0 - diff --git a/pyproject.toml b/pyproject.toml index 4fa48839..4a2a949b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,6 +32,7 @@ classifiers = [ "Programming Language :: Python :: 3 :: Only", ] dependencies = [ + "ruff", "h5py", "argparse", "SQLAlchemy", @@ -56,51 +57,11 @@ AMSDeploy = "ams_wf.AMSDeploy:main" package-dir = {"" = "src/AMSWorkflow"} packages = ["ams_wf", "ams"] -# Black formatting -[tool.black] -line-length = 120 -include = '\.pyi?$' -exclude = ''' -/( - .eggs # exclude a few common directories in the - | .git # root of the project - | .hg - | .mypy_cache - | .tox - | venv - | _build - | buck-out - | build - | dist - )/ -''' - -# iSort -[tool.isort] -profile = "black" -line_length = 120 -multi_line_output = 3 -include_trailing_comma = true -virtual_env = "venv" - -# flake8 -[tool.flake8] -ignore = ["E501", "W503", "E226", "BLK100", "E203"] -max-line-length = 120 -exclude = [ - # No need to traverse our git directory - ".git", - # There's no value in checking cache directories - "__pycache__", - "*.egg-info", - "build" -] # E501: Line too long # W503: Line break occurred before binary operator # E226: Missing white space around arithmetic operator - [tool.ruff] -lint.ignore = ["E501", "E226", "E203"] +line-length = 120 show-fixes = true exclude = [ ".git", @@ -108,10 +69,15 @@ exclude = [ "*.egg-info", "build" ] -# change the default line length number or characters. -line-length = 120 -lint.select = ['E', 'F', 'W', 'A', 'PLC', 'PLE', 'PLW', 'I', 'N', 'Q'] -[tool.yapf] -ignore = ["E501", "W503", "E226", "BLK100", "E203"] -column_limit = 120 +[tool.ruff.lint] +select = ['E', 'F', 'W', 'A', 'PLC', 'PLE', 'PLW', 'I', 'N', 'Q', 'B', 'UP', 'SIM', 'RUF'] +ignore = ["E501", "E226", "E203"] + +[tool.ruff.lint.isort] +known-first-party = ["ams", "ams_wf"] + +[tool.ruff.format] +quote-style = "double" +indent-style = "space" + From 430bcad00ec27e65cd98a5087a80c63565836e9e Mon Sep 17 00:00:00 2001 From: Loic Pottier Date: Wed, 10 Jun 2026 10:33:11 -0700 Subject: [PATCH 4/7] Removed installing hook in CMake and made ruff a dev dependencies Signed-off-by: Loic Pottier --- CMakeLists.txt | 3 --- cmake/setup-git-hooks.cmake | 23 ----------------------- pyproject.toml | 6 +++++- 3 files changed, 5 insertions(+), 27 deletions(-) delete mode 100644 cmake/setup-git-hooks.cmake diff --git a/CMakeLists.txt b/CMakeLists.txt index fb71fd02..e43f0c7b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -14,9 +14,6 @@ cmake_policy(SET CMP0074 NEW) project(AMS VERSION 0.1.1 LANGUAGES CXX C) -# We insall AMS githook to run clang-format etc when commiting -include(cmake/setup-git-hooks.cmake) - # NOTE: This may break some of our integrations with the applications. But flux requires > C++20, RMQ requires C++17 and although AMS does not have # any restrictions on the CXX standard the application may impose those. The solution would be to compile RMQ with an older GCC version (8) and # have flux to be an external of the system environment diff --git a/cmake/setup-git-hooks.cmake b/cmake/setup-git-hooks.cmake deleted file mode 100644 index a342cc65..00000000 --- a/cmake/setup-git-hooks.cmake +++ /dev/null @@ -1,23 +0,0 @@ -# Automatically configure git hooks when the project is configured. -# Place in cmake/setup-git-hooks.cmake and include from the top-level CMakeLists.txt. - -find_package(Git QUIET) - -if(GIT_FOUND AND EXISTS "${PROJECT_SOURCE_DIR}/.git") - # Only set if not already configured (respects developer overrides) - execute_process( - COMMAND ${GIT_EXECUTABLE} config --get core.hooksPath - WORKING_DIRECTORY ${PROJECT_SOURCE_DIR} - OUTPUT_VARIABLE _current_hooks_path - OUTPUT_STRIP_TRAILING_WHITESPACE - RESULT_VARIABLE _rc - ) - - if(NOT _rc EQUAL 0 OR NOT _current_hooks_path STREQUAL ".githooks") - message(STATUS "Setting git hooks path to .githooks/") - execute_process( - COMMAND ${GIT_EXECUTABLE} config core.hooksPath .githooks - WORKING_DIRECTORY ${PROJECT_SOURCE_DIR} - ) - endif() -endif() diff --git a/pyproject.toml b/pyproject.toml index 4a2a949b..d940a7b8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,7 +32,6 @@ classifiers = [ "Programming Language :: Python :: 3 :: Only", ] dependencies = [ - "ruff", "h5py", "argparse", "SQLAlchemy", @@ -42,6 +41,11 @@ dependencies = [ "flux-python>=0.75.0" ] +[project.optional-dependencies] +dev = [ + "ruff", +] + [project.scripts] AMSBroker = "ams_wf.AMSBroker:main" AMSDBStage = "ams_wf.AMSDBStage:main" From cbaf3e0620dba8405417dd7e185240789e436ebf Mon Sep 17 00:00:00 2001 From: Loic Pottier Date: Tue, 23 Jun 2026 13:26:27 -0700 Subject: [PATCH 5/7] Adding an explicit scripts to reformat the codebase Signed-off-by: Loic Pottier --- .githooks/pre-commit | 181 +---------------- scripts/run-code-quality.sh | 394 ++++++++++++++++++++++++++++++++++++ 2 files changed, 403 insertions(+), 172 deletions(-) create mode 100755 scripts/run-code-quality.sh diff --git a/.githooks/pre-commit b/.githooks/pre-commit index ce35de63..f2320852 100755 --- a/.githooks/pre-commit +++ b/.githooks/pre-commit @@ -1,9 +1,7 @@ #!/usr/bin/env bash # # Git pre-commit hook for AMS: -# Phase 1: clang-format on staged C/C++ files (auto-fixes, re-stages) -# Phase 2: ruff on staged Python files (auto-fixes, re-stages) -# Phase 3: clang-tidy on staged .cpp files (reports errors, blocks commit) +# Runs non-mutating checks on staged C/C++ and Python files. # # Install (automatic via cmake/setup-git-hooks.cmake): # .githooks/pre-commit @@ -11,187 +9,26 @@ # Skip temporarily: # git commit --no-verify # -# Skip only clang-tidy (format + ruff still run): +# Skip only clang-tidy: # AMS_SKIP_TIDY=1 git commit # # Specify build directory (for compile_commands.json): # export AMS_BUILD_DIR=/path/to/build # +# Apply fixes explicitly: +# scripts/run-code-quality.sh --fix +# # Requires: clang-format, clang-tidy, ruff on PATH # clang-tidy requires compile_commands.json (generated by cmake) set -euo pipefail -find_tool() { - local base="$1" - for candidate in "${base}-18" "${base}-17" "${base}-16" "${base}-15" "${base}-14" "${base}"; do - if command -v "$candidate" &>/dev/null; then - echo "$candidate" - return - fi - done -} - -CLANG_FORMAT=$(find_tool clang-format) -CLANG_TIDY=$(find_tool clang-tidy) -RUFF=$(command -v ruff 2>/dev/null || true) - -CPP_EXTENSIONS='\.(cpp|hpp|cc|hh|h|c|cxx|hxx)$' -TIDY_EXTENSIONS='\.(cpp|cc|c|cxx)$' -PY_EXTENSIONS='\.py$' - REPO_ROOT=$(git rev-parse --show-toplevel) +SCRIPT="${REPO_ROOT}/scripts/run-code-quality.sh" -ALL_STAGED=$(git diff --cached --name-only --diff-filter=ACM || true) - -CPP_FILES=$(echo "$ALL_STAGED" | grep -E "$CPP_EXTENSIONS" | sed "s|^|${REPO_ROOT}/|" || true) -PY_FILES=$(echo "$ALL_STAGED" | grep -E "$PY_EXTENSIONS" | sed "s|^|${REPO_ROOT}/|" || true) - -if [[ -z "$CPP_FILES" && -z "$PY_FILES" ]]; then - exit 0 -fi - -if [[ -n "$CPP_FILES" ]]; then - if [[ -n "$CLANG_FORMAT" ]]; then - REFORMATTED=0 - - for file in $CPP_FILES; do - "$CLANG_FORMAT" -i -style=file "$file" - - if ! git diff --quiet -- "$file"; then - echo " clang-format: reformatted $file" - git add "$file" - REFORMATTED=1 - fi - done - - if [[ "$REFORMATTED" -eq 1 ]]; then - echo "" - echo "clang-format: reformatted files have been re-staged." - echo "" - fi - else - echo "WARNING: clang-format not found, skipping C/C++ format check." - fi -fi - -if [[ -n "$PY_FILES" ]]; then - if [[ -n "$RUFF" ]]; then - PY_REFORMATTED=0 - - # Format (equivalent to black) - for file in $PY_FILES; do - "$RUFF" format --quiet "$file" - done - - # Lint with auto-fix (import sorting, safe fixes) - for file in $PY_FILES; do - "$RUFF" check --fix --quiet "$file" 2>/dev/null || true - done - - # Re-stage any modified files - for file in $PY_FILES; do - if ! git diff --quiet -- "$file"; then - echo " ruff: reformatted $file" - git add "$file" - PY_REFORMATTED=1 - fi - done - - if [[ "$PY_REFORMATTED" -eq 1 ]]; then - echo "" - echo "ruff: reformatted files have been re-staged." - echo "" - fi - - # Lint check (report remaining issues — unfixable ones) - RUFF_ERRORS=0 - for file in $PY_FILES; do - if ! "$RUFF" check --quiet "$file" 2>/dev/null; then - RUFF_ERRORS=1 - fi - done - - if [[ "$RUFF_ERRORS" -eq 1 ]]; then - echo "" - echo "ruff: lint errors found (see above). Fix them before committing." - echo "Or skip all hooks with: git commit --no-verify" - exit 1 - fi - else - echo "WARNING: ruff not found, skipping Python format/lint check." - echo " Install with: pip install ruff" - fi -fi - -if [[ -z "$CPP_FILES" ]]; then - exit 0 -fi - -# Allow skipping clang-tidy via environment variable -if [[ "${AMS_SKIP_TIDY:-0}" == "1" ]]; then - exit 0 -fi - -if [[ -z "$CLANG_TIDY" ]]; then - echo "WARNING: clang-tidy not found, skipping C++ lint check." - exit 0 -fi - -# Find compile_commands.json -COMPILE_DB="" - -if [[ -n "${AMS_BUILD_DIR:-}" && -f "${AMS_BUILD_DIR}/compile_commands.json" ]]; then - COMPILE_DB="${AMS_BUILD_DIR}/compile_commands.json" -else - for dir in build build-* cmake-build-* out; do - candidate=$(find "${REPO_ROOT}" -maxdepth 2 -path "${REPO_ROOT}/${dir}/compile_commands.json" -print -quit 2>/dev/null) - if [[ -n "$candidate" ]]; then - COMPILE_DB="$candidate" - break - fi - done -fi - -if [[ -z "$COMPILE_DB" ]]; then - echo "WARNING: compile_commands.json not found, skipping clang-tidy." - echo " Run cmake first, or set AMS_BUILD_DIR to your build directory." - exit 0 -fi - -COMPILE_DB_DIR=$(dirname "$COMPILE_DB") - -# Filter to translation units only -TIDY_FILES=$(echo "$CPP_FILES" | grep -E "$TIDY_EXTENSIONS" || true) - -if [[ -z "$TIDY_FILES" ]]; then - exit 0 -fi - -TIDY_ERRORS=0 - -for file in $TIDY_FILES; do - # Only run on files that appear in compile_commands.json - if ! grep -q "\"file\".*$(basename "$file")\"" "$COMPILE_DB" 2>/dev/null; then - continue - fi - - echo " clang-tidy: checking $file" - - # clang-tidy exits non-zero when WarningsAsErrors checks fire. - # Warnings (not promoted to errors) print but don't affect the exit code. - if ! "$CLANG_TIDY" -p "$COMPILE_DB_DIR" --quiet "$file"; then - TIDY_ERRORS=1 - fi -done - -if [[ "$TIDY_ERRORS" -eq 1 ]]; then - echo "" - echo "clang-tidy: errors found (see above)." - echo "Warnings are informational and do not block the commit." - echo "Fix errors, or skip with: AMS_SKIP_TIDY=1 git commit" - echo "Or skip all hooks with: git commit --no-verify" +if [[ ! -f "$SCRIPT" ]]; then + echo "ERROR: missing helper script: $SCRIPT" exit 1 fi -exit 0 +exec bash "$SCRIPT" --check --staged --fail-on-partial diff --git a/scripts/run-code-quality.sh b/scripts/run-code-quality.sh new file mode 100755 index 00000000..a7cd40eb --- /dev/null +++ b/scripts/run-code-quality.sh @@ -0,0 +1,394 @@ +#!/usr/bin/env bash +# +# Copyright 2021-2026 Lawrence Livermore National Security, LLC and other +# AMSLib Project Developers +# +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +# +# Developer utility for AMS code-quality checks and explicit fixes. +# +# Examples: +# scripts/run-code-quality.sh +# scripts/run-code-quality.sh --fix +# scripts/run-code-quality.sh --ruff --fix +# scripts/run-code-quality.sh --clang-format --clang-tidy src tests +# scripts/run-code-quality.sh --check --staged + +set -euo pipefail + +usage() { + cat <<'EOF' +Usage: scripts/run-code-quality.sh [options] [paths...] + +Run code-quality tools across the repository or a selected subset of files. +By default, this runs check-only mode on tracked files under: + src tests examples tools scripts .githooks + +Options: + --check Run non-mutating checks only (default) + --fix Apply clang-format and ruff fixes explicitly + --staged Operate on staged files instead of the full codebase + --fail-on-partial Refuse staged runs when relevant files also have unstaged changes + --ruff Run ruff format/check on Python files + --clang-format Run clang-format on C/C++ files + --clang-tidy Run clang-tidy on C/C++ translation units + --build-dir DIR Build directory containing compile_commands.json + --help Show this help text + +Notes: + - If no tool is selected, the script runs ruff, clang-format, and clang-tidy. + - --fix does not apply clang-tidy fixes; clang-tidy remains check-only. + - AMS_SKIP_TIDY=1 skips clang-tidy. + - AMS_BUILD_DIR can be used instead of --build-dir. +EOF +} + +find_tool() { + local base="$1" + local candidate="" + + for candidate in "${base}-18" "${base}-17" "${base}-16" "${base}-15" "${base}-14" "${base}"; do + if command -v "$candidate" >/dev/null 2>&1; then + printf '%s\n' "$candidate" + return 0 + fi + done + + return 1 +} + +warn() { + printf 'WARNING: %s\n' "$*" +} + +die() { + printf 'ERROR: %s\n' "$*" >&2 + exit 1 +} + +append_if_exists() { + local path="$1" + + if [[ -e "$path" ]]; then + RELEVANT_FILES+=("$path") + fi +} + +is_cpp_file() { + local path="$1" + [[ "$path" =~ \.(cpp|hpp|cc|hh|h|c|cxx|hxx)$ ]] +} + +is_tidy_file() { + local path="$1" + [[ "$path" =~ \.(cpp|cc|c|cxx)$ ]] +} + +is_python_file() { + local path="$1" + [[ "$path" =~ \.py$ ]] +} + +collect_files() { + local target="" + + if [[ "$USE_STAGED" -eq 1 ]]; then + while IFS= read -r -d '' target; do + append_if_exists "$target" + done < <(git diff --cached --name-only -z --diff-filter=ACM -- "${TARGETS[@]}") + return + fi + + while IFS= read -r -d '' target; do + append_if_exists "$target" + done < <(git ls-files -z -- "${TARGETS[@]}") +} + +detect_partial_staging() { + local file="" + local partial_files=() + + [[ "$USE_STAGED" -eq 1 && "$FAIL_ON_PARTIAL" -eq 1 ]] || return 0 + + for file in "${RELEVANT_FILES[@]}"; do + if ! git diff --quiet -- "$file"; then + partial_files+=("$file") + fi + done + + if [[ "${#partial_files[@]}" -eq 0 ]]; then + return 0 + fi + + printf 'ERROR: refusing to run on partially staged files.\n' >&2 + printf 'Stage or stash the remaining edits first:\n' >&2 + for file in "${partial_files[@]}"; do + printf ' %s\n' "$file" >&2 + done + return 1 +} + +find_compile_db() { + local candidate="" + local dir="" + + if [[ -n "$BUILD_DIR" && -f "$BUILD_DIR/compile_commands.json" ]]; then + printf '%s\n' "$BUILD_DIR/compile_commands.json" + return 0 + fi + + for dir in build build-* cmake-build-* out; do + while IFS= read -r candidate; do + if [[ -n "$candidate" ]]; then + printf '%s\n' "$candidate" + return 0 + fi + done < <(find "$REPO_ROOT" -maxdepth 2 -path "$REPO_ROOT/$dir/compile_commands.json" -print 2>/dev/null) + done + + return 1 +} + +run_clang_format() { + local file="" + local failed=0 + + if [[ "${#CPP_FILES[@]}" -eq 0 ]]; then + return 0 + fi + + if [[ -z "$CLANG_FORMAT" ]]; then + warn "clang-format not found, skipping C/C++ formatting." + return 0 + fi + + if [[ "$APPLY_FIXES" -eq 1 ]]; then + for file in "${CPP_FILES[@]}"; do + "$CLANG_FORMAT" -i -style=file "$file" + printf 'clang-format: updated %s\n' "$file" + done + return 0 + fi + + for file in "${CPP_FILES[@]}"; do + if ! "$CLANG_FORMAT" --dry-run -Werror -style=file "$file"; then + failed=1 + fi + done + + if [[ "$failed" -eq 1 ]]; then + printf '\nclang-format: formatting issues found.\n' + printf 'Apply them explicitly with: scripts/run-code-quality.sh --clang-format --fix\n' + return 1 + fi + + return 0 +} + +run_ruff() { + local failed=0 + + if [[ "${#PY_FILES[@]}" -eq 0 ]]; then + return 0 + fi + + if [[ -z "$RUFF" ]]; then + warn "ruff not found, skipping Python checks." + warn "Install with: pip install ruff" + return 0 + fi + + if [[ "$APPLY_FIXES" -eq 1 ]]; then + "$RUFF" format "${PY_FILES[@]}" + "$RUFF" check --fix "${PY_FILES[@]}" || true + else + if ! "$RUFF" format --check "${PY_FILES[@]}"; then + failed=1 + fi + fi + + if ! "$RUFF" check "${PY_FILES[@]}"; then + failed=1 + fi + + if [[ "$failed" -eq 1 ]]; then + printf '\nruff: Python formatting or lint issues found.\n' + printf 'Apply fixes explicitly with: scripts/run-code-quality.sh --ruff --fix\n' + return 1 + fi + + return 0 +} + +run_clang_tidy() { + local compile_db="" + local compile_db_dir="" + local file="" + local failed=0 + local rel_file="" + local abs_file="" + + if [[ "$SKIP_TIDY" -eq 1 || "${#TIDY_FILES[@]}" -eq 0 ]]; then + return 0 + fi + + if [[ -z "$CLANG_TIDY" ]]; then + warn "clang-tidy not found, skipping C/C++ lint checks." + return 0 + fi + + if ! compile_db=$(find_compile_db); then + warn "compile_commands.json not found, skipping clang-tidy." + warn "Run cmake first, or set AMS_BUILD_DIR / --build-dir." + return 0 + fi + + compile_db_dir=$(dirname "$compile_db") + + for file in "${TIDY_FILES[@]}"; do + rel_file="$file" + abs_file="$REPO_ROOT/$file" + + if ! grep -Fq "\"$rel_file\"" "$compile_db" && ! grep -Fq "\"$abs_file\"" "$compile_db"; then + continue + fi + + printf 'clang-tidy: checking %s\n' "$file" + if ! "$CLANG_TIDY" -p "$compile_db_dir" --quiet "$file"; then + failed=1 + fi + done + + if [[ "$failed" -eq 1 ]]; then + printf '\nclang-tidy: errors found.\n' + printf 'Fix the reported issues, or skip tidy with: AMS_SKIP_TIDY=1 ...\n' + return 1 + fi + + return 0 +} + +REPO_ROOT=$(git rev-parse --show-toplevel) +cd "$REPO_ROOT" + +DEFAULT_TARGETS=(src tests examples tools scripts .githooks) +TARGETS=() +RELEVANT_FILES=() +CPP_FILES=() +PY_FILES=() +TIDY_FILES=() + +USE_STAGED=0 +FAIL_ON_PARTIAL=0 +APPLY_FIXES=0 +RUN_RUFF=0 +RUN_CLANG_FORMAT=0 +RUN_CLANG_TIDY=0 +BUILD_DIR="${AMS_BUILD_DIR:-}" +SKIP_TIDY=0 + +if [[ "${AMS_SKIP_TIDY:-0}" == "1" ]]; then + SKIP_TIDY=1 +fi + +while [[ "$#" -gt 0 ]]; do + case "$1" in + --check) + APPLY_FIXES=0 + ;; + --fix) + APPLY_FIXES=1 + ;; + --staged) + USE_STAGED=1 + ;; + --fail-on-partial) + FAIL_ON_PARTIAL=1 + ;; + --ruff) + RUN_RUFF=1 + ;; + --clang-format) + RUN_CLANG_FORMAT=1 + ;; + --clang-tidy) + RUN_CLANG_TIDY=1 + ;; + --build-dir) + shift + [[ "$#" -gt 0 ]] || die "--build-dir requires a value" + BUILD_DIR="$1" + ;; + --help|-h) + usage + exit 0 + ;; + --) + shift + while [[ "$#" -gt 0 ]]; do + TARGETS+=("$1") + shift + done + break + ;; + -*) + die "unknown option: $1" + ;; + *) + TARGETS+=("$1") + ;; + esac + shift +done + +if [[ "$RUN_RUFF" -eq 0 && "$RUN_CLANG_FORMAT" -eq 0 && "$RUN_CLANG_TIDY" -eq 0 ]]; then + RUN_RUFF=1 + RUN_CLANG_FORMAT=1 + RUN_CLANG_TIDY=1 +fi + +if [[ "${#TARGETS[@]}" -eq 0 ]]; then + TARGETS=("${DEFAULT_TARGETS[@]}") +fi + +CLANG_FORMAT=$(find_tool clang-format || true) +CLANG_TIDY=$(find_tool clang-tidy || true) +RUFF=$(command -v ruff 2>/dev/null || true) + +collect_files + +for file in "${RELEVANT_FILES[@]}"; do + if [[ "$RUN_CLANG_FORMAT" -eq 1 ]] && is_cpp_file "$file"; then + CPP_FILES+=("$file") + fi + + if [[ "$RUN_RUFF" -eq 1 ]] && is_python_file "$file"; then + PY_FILES+=("$file") + fi + + if [[ "$RUN_CLANG_TIDY" -eq 1 ]] && is_tidy_file "$file"; then + TIDY_FILES+=("$file") + fi +done + +if [[ "${#CPP_FILES[@]}" -eq 0 && "${#PY_FILES[@]}" -eq 0 && "${#TIDY_FILES[@]}" -eq 0 ]]; then + exit 0 +fi + +detect_partial_staging + +status=0 + +if [[ "$RUN_CLANG_FORMAT" -eq 1 ]] && ! run_clang_format; then + status=1 +fi + +if [[ "$RUN_RUFF" -eq 1 ]] && ! run_ruff; then + status=1 +fi + +if [[ "$RUN_CLANG_TIDY" -eq 1 ]] && ! run_clang_tidy; then + status=1 +fi + +exit "$status" From 98f40132b8a8c9c2d3f23cdcd70a01eae12afc24 Mon Sep 17 00:00:00 2001 From: Loic Pottier Date: Tue, 23 Jun 2026 13:30:25 -0700 Subject: [PATCH 6/7] Fixing typo in comments Signed-off-by: Loic Pottier --- .githooks/pre-commit | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.githooks/pre-commit b/.githooks/pre-commit index f2320852..8c220f45 100755 --- a/.githooks/pre-commit +++ b/.githooks/pre-commit @@ -3,8 +3,8 @@ # Git pre-commit hook for AMS: # Runs non-mutating checks on staged C/C++ and Python files. # -# Install (automatic via cmake/setup-git-hooks.cmake): -# .githooks/pre-commit +# Install: +# git config core.hooksPath .githooks # # Skip temporarily: # git commit --no-verify From a2ab9d1760937e991775ffa5e1a030432bc98d06 Mon Sep 17 00:00:00 2001 From: Loic Pottier Date: Wed, 8 Jul 2026 17:31:46 -0700 Subject: [PATCH 7/7] Fixed scripts/run-code-quality.sh for clang-tidy Signed-off-by: Loic Pottier --- scripts/run-code-quality.sh | 222 ++++++++++++++++++++++++++++++++---- 1 file changed, 198 insertions(+), 24 deletions(-) diff --git a/scripts/run-code-quality.sh b/scripts/run-code-quality.sh index a7cd40eb..86ff30c5 100755 --- a/scripts/run-code-quality.sh +++ b/scripts/run-code-quality.sh @@ -11,8 +11,8 @@ # scripts/run-code-quality.sh # scripts/run-code-quality.sh --fix # scripts/run-code-quality.sh --ruff --fix -# scripts/run-code-quality.sh --clang-format --clang-tidy src tests -# scripts/run-code-quality.sh --check --staged +# scripts/run-code-quality.sh --all --clang-format --clang-tidy src tests +# scripts/run-code-quality.sh --check set -euo pipefail @@ -20,14 +20,15 @@ usage() { cat <<'EOF' Usage: scripts/run-code-quality.sh [options] [paths...] -Run code-quality tools across the repository or a selected subset of files. -By default, this runs check-only mode on tracked files under: +Run code-quality tools on staged files by default, or across the repository +with --all. Full-codebase runs use tracked files under: src tests examples tools scripts .githooks Options: --check Run non-mutating checks only (default) --fix Apply clang-format and ruff fixes explicitly - --staged Operate on staged files instead of the full codebase + --staged Operate on staged files (default) + --all Operate on the full codebase instead of staged files --fail-on-partial Refuse staged runs when relevant files also have unstaged changes --ruff Run ruff format/check on Python files --clang-format Run clang-format on C/C++ files @@ -76,17 +77,25 @@ append_if_exists() { is_cpp_file() { local path="$1" - [[ "$path" =~ \.(cpp|hpp|cc|hh|h|c|cxx|hxx)$ ]] + local lower="${path,,}" + [[ "$lower" =~ \.(cpp|hpp|cc|hh|h|c|cxx|hxx)$ ]] } is_tidy_file() { local path="$1" - [[ "$path" =~ \.(cpp|cc|c|cxx)$ ]] + local lower="${path,,}" + [[ "$lower" =~ \.(cpp|cc|c|cxx)$ ]] +} + +is_dependency_file() { + local path="$1" + [[ "$path" == _deps/* || "$path" == */_deps/* ]] } is_python_file() { local path="$1" - [[ "$path" =~ \.py$ ]] + local lower="${path,,}" + [[ "$lower" =~ \.py$ ]] } collect_files() { @@ -149,6 +158,162 @@ find_compile_db() { return 1 } +append_unique_compile_db_tidy_file() { + local candidate="$1" + local file="" + + for file in "${COMPILE_DB_TIDY_FILES[@]}"; do + if [[ "$file" == "$candidate" ]]; then + return 0 + fi + done + + COMPILE_DB_TIDY_FILES+=("$candidate") +} + +tidy_file_in_compile_db() { + local candidate="$1" + local file="" + + for file in "${COMPILE_DB_TIDY_FILES[@]}"; do + if [[ "$file" == "$candidate" ]]; then + return 0 + fi + done + + return 1 +} + +collect_compile_db_tidy_files() { + local file="" + local parsed_files="" + + [[ "$RUN_CLANG_TIDY" -eq 1 && "$SKIP_TIDY" -eq 0 ]] || return 0 + + if ! COMPILE_DB=$(find_compile_db); then + return 0 + fi + + COMPILE_DB_DIR=$(dirname "$COMPILE_DB") + + if ! command -v python3 >/dev/null 2>&1; then + warn "python3 not found, cannot read compile_commands.json for clang-tidy discovery." + return 0 + fi + + if ! parsed_files=$(python3 - "$REPO_ROOT" "$COMPILE_DB" "${TARGETS[@]}" <<'PY' +import json +import os +import sys + +repo_root = os.path.realpath(sys.argv[1]) +compile_db = sys.argv[2] +targets = sys.argv[3:] +tidy_extensions = (".cpp", ".cc", ".c", ".cxx") + + +def repo_relative(path): + abs_path = os.path.realpath(path) + try: + rel_path = os.path.relpath(abs_path, repo_root) + except ValueError: + return None + if rel_path == os.pardir or rel_path.startswith(os.pardir + os.sep): + return None + return rel_path.replace(os.sep, "/") + + +def normalize_target(target): + if target in ("", "."): + return "" + if os.path.isabs(target): + rel_target = repo_relative(target) + else: + rel_target = repo_relative(os.path.join(repo_root, target)) + if rel_target in (None, "."): + return "" + return rel_target.rstrip("/") + + +target_filters = [normalize_target(target) for target in targets] +target_filters = [target for target in target_filters if target is not None] + + +def target_matches(rel_path): + for target in target_filters: + if target == "" or rel_path == target or rel_path.startswith(target + "/"): + return True + return False + + +def is_dependency_path(rel_path): + return "_deps" in rel_path.split("/") + + +with open(compile_db, encoding="utf-8") as handle: + entries = json.load(handle) + +seen = set() +for entry in entries: + raw_file = entry.get("file") + if not isinstance(raw_file, str): + continue + + if os.path.isabs(raw_file): + source_path = raw_file + else: + directory = entry.get("directory") + if not isinstance(directory, str): + directory = os.path.dirname(compile_db) + source_path = os.path.join(directory, raw_file) + + rel_path = repo_relative(source_path) + if rel_path is None or rel_path in seen: + continue + if is_dependency_path(rel_path): + continue + if not rel_path.lower().endswith(tidy_extensions): + continue + if not target_matches(rel_path): + continue + + seen.add(rel_path) + print(rel_path) +PY +); then + warn "failed to read $COMPILE_DB for clang-tidy discovery." + return 0 + fi + + COMPILE_DB_PARSED=1 + + while IFS= read -r file; do + if [[ -n "$file" ]]; then + append_unique_compile_db_tidy_file "$file" + fi + done <<< "$parsed_files" +} + +sync_tidy_files_with_compile_db() { + local file="" + local staged_tidy_files=() + + [[ "$RUN_CLANG_TIDY" -eq 1 && "$COMPILE_DB_PARSED" -eq 1 ]] || return 0 + + if [[ "$USE_STAGED" -eq 0 ]]; then + TIDY_FILES=("${COMPILE_DB_TIDY_FILES[@]}") + return 0 + fi + + for file in "${TIDY_FILES[@]}"; do + if tidy_file_in_compile_db "$file"; then + staged_tidy_files+=("$file") + fi + done + + TIDY_FILES=("${staged_tidy_files[@]}") +} + run_clang_format() { local file="" local failed=0 @@ -221,12 +386,10 @@ run_ruff() { } run_clang_tidy() { - local compile_db="" - local compile_db_dir="" + local compile_db="$COMPILE_DB" + local compile_db_dir="$COMPILE_DB_DIR" local file="" local failed=0 - local rel_file="" - local abs_file="" if [[ "$SKIP_TIDY" -eq 1 || "${#TIDY_FILES[@]}" -eq 0 ]]; then return 0 @@ -237,19 +400,20 @@ run_clang_tidy() { return 0 fi - if ! compile_db=$(find_compile_db); then - warn "compile_commands.json not found, skipping clang-tidy." - warn "Run cmake first, or set AMS_BUILD_DIR / --build-dir." - return 0 + if [[ -z "$compile_db" ]]; then + if ! compile_db=$(find_compile_db); then + warn "compile_commands.json not found, skipping clang-tidy." + warn "Run cmake first, or set AMS_BUILD_DIR / --build-dir." + return 0 + fi + compile_db_dir=$(dirname "$compile_db") fi - - compile_db_dir=$(dirname "$compile_db") - for file in "${TIDY_FILES[@]}"; do - rel_file="$file" - abs_file="$REPO_ROOT/$file" + if is_dependency_file "$file"; then + continue + fi - if ! grep -Fq "\"$rel_file\"" "$compile_db" && ! grep -Fq "\"$abs_file\"" "$compile_db"; then + if [[ "$COMPILE_DB_PARSED" -eq 1 ]] && ! tidy_file_in_compile_db "$file"; then continue fi @@ -277,8 +441,12 @@ RELEVANT_FILES=() CPP_FILES=() PY_FILES=() TIDY_FILES=() +COMPILE_DB_TIDY_FILES=() +COMPILE_DB="" +COMPILE_DB_DIR="" +COMPILE_DB_PARSED=0 -USE_STAGED=0 +USE_STAGED=1 FAIL_ON_PARTIAL=0 APPLY_FIXES=0 RUN_RUFF=0 @@ -302,6 +470,9 @@ while [[ "$#" -gt 0 ]]; do --staged) USE_STAGED=1 ;; + --all) + USE_STAGED=0 + ;; --fail-on-partial) FAIL_ON_PARTIAL=1 ;; @@ -366,11 +537,14 @@ for file in "${RELEVANT_FILES[@]}"; do PY_FILES+=("$file") fi - if [[ "$RUN_CLANG_TIDY" -eq 1 ]] && is_tidy_file "$file"; then + if [[ "$RUN_CLANG_TIDY" -eq 1 ]] && is_tidy_file "$file" && ! is_dependency_file "$file"; then TIDY_FILES+=("$file") fi done +collect_compile_db_tidy_files +sync_tidy_files_with_compile_db + if [[ "${#CPP_FILES[@]}" -eq 0 && "${#PY_FILES[@]}" -eq 0 && "${#TIDY_FILES[@]}" -eq 0 ]]; then exit 0 fi