diff --git a/.clang-tidy b/.clang-tidy new file mode 100644 index 00000000..e4269c07 --- /dev/null +++ b/.clang-tidy @@ -0,0 +1,39 @@ +--- +# 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 + +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..ce35de63 --- /dev/null +++ b/.githooks/pre-commit @@ -0,0 +1,197 @@ +#!/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) +# +# Install (automatic via cmake/setup-git-hooks.cmake): +# .githooks/pre-commit +# +# Skip temporarily: +# git commit --no-verify +# +# 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, 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) + +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" + exit 1 +fi + +exit 0 diff --git a/.gitlab/custom-jobs-and-variables.yml b/.gitlab/custom-jobs-and-variables.yml index 4ca399fe..3ac96172 100644 --- a/.gitlab/custom-jobs-and-variables.yml +++ b/.gitlab/custom-jobs-and-variables.yml @@ -14,25 +14,25 @@ variables: # Dane # Arguments for top level allocation - DANE_SHARED_ALLOC: "--exclusive --reservation=ci --time=45 --nodes=1" + DANE_SHARED_ALLOC: "--account=lpbf --exclusive --reservation=ci --time=45 --nodes=1" # Arguments for job level allocation - DANE_JOB_ALLOC: "--mpi=none --reservation=ci --nodes=1" + DANE_JOB_ALLOC: "--account=lpbf --mpi=none --reservation=ci --nodes=1" # Add variables that should apply to all the jobs on a machine: # DANE_MY_VAR: "..." # Tioga # Arguments for top level allocation # OPTIONAL: "-o per-resource.count=2" allows to get 2 jobs running on each node. - TIOGA_SHARED_ALLOC: "--queue=pci --exclusive --time-limit=1h --nodes=1 -o per-resource.count=4" + TIOGA_SHARED_ALLOC: "--bank=asccasc --queue=pci --exclusive --time-limit=1h --nodes=1 -o per-resource.count=4" # Arguments for job level allocation, we need to match the time limit. Otherwise it immediately exits - TIOGA_JOB_ALLOC: "--nodes=1 --begin-time=+5s --time-limit=1h" + TIOGA_JOB_ALLOC: "--bank=asccasc --nodes=1 --begin-time=+5s --time-limit=1h" # Add variables that should apply to all the jobs on a machine: # TIOGA_MY_VAR: "..." # Tuo - TUOLUMNE_SHARED_ALLOC: "--queue=pci --exclusive --time-limit=1h --nodes=1 -o per-resource.count=4" + TUOLUMNE_SHARED_ALLOC: "--bank=lpbf --queue=pci --exclusive --time-limit=1h --nodes=1 -o per-resource.count=4" # Arguments for job level allocation - TUOLUMNE_JOB_ALLOC: "--nodes=1 --begin-time=+5s --time-limit=1h" + TUOLUMNE_JOB_ALLOC: "--bank=lpbf --nodes=1 --begin-time=+5s --time-limit=1h" # Configuration shared by build and test jobs specific to this project. # Not all configuration can be shared. Here projects can fine tune the 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() 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" +