diff --git a/.github/workflows/clang-tidy.yml b/.github/workflows/clang-tidy.yml new file mode 100644 index 00000000..6607b372 --- /dev/null +++ b/.github/workflows/clang-tidy.yml @@ -0,0 +1,99 @@ +# Copyright 2026 atframework +name: "clang-tidy" + +on: + push: + branches: + - main + pull_request: + branches: [main] + +permissions: + contents: read + +jobs: + clang-tidy: + name: Clang-Tidy + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - cc: clang + cxx_standard: "14" + env: + CC: ${{ matrix.cc }} + CXX: "clang++" + CXX_STANDARD: ${{ matrix.cxx_standard }} + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + submodules: recursive + + - name: Generate cache key + shell: bash + run: git submodule > '.github/.cache-key' + + - name: Cache packages + uses: actions/cache@v5 + with: + path: | + third_party/install + key: ${{ runner.os }}-clang-tidy-${{ hashFiles('.github/.cache-key') }} + + - name: Setup dependencies + shell: bash + run: | + sudo apt-get update + sudo apt-get install -y --no-install-recommends \ + build-essential cmake ninja-build \ + libssl-dev libcurl4-openssl-dev \ + clang-tidy + + - name: Build with clang-tidy + id: build + shell: bash + run: | + bash ci/do_ci.sh clang-tidy + echo "build_log=${{ github.workspace }}/build_jobs_clang_tidy/clang-tidy.log" >> "$GITHUB_OUTPUT" + + - name: Analyze clang-tidy output + id: analyze + shell: bash + run: | + python3 ci/parse_clang_tidy_warnings.py \ + --build_log "${{ steps.build.outputs.build_log }}" \ + --output "./clang_tidy_report.md" + + TOTAL_WARNINGS=$(python3 ci/parse_clang_tidy_warnings.py \ + --build_log "${{ steps.build.outputs.build_log }}" \ + --count-only) + + echo "warning_count=$TOTAL_WARNINGS" >> "$GITHUB_OUTPUT" + echo "report_path=./clang_tidy_report.md" >> "$GITHUB_OUTPUT" + + # Append report to step summary + if [[ -f "./clang_tidy_report.md" ]]; then + cat "./clang_tidy_report.md" >> "$GITHUB_STEP_SUMMARY" + fi + + - name: Upload build log + uses: actions/upload-artifact@v4 + if: always() + with: + name: Logs-clang-tidy + path: ${{ steps.build.outputs.build_log }} + + - name: Upload warning report + uses: actions/upload-artifact@v4 + if: always() + with: + name: Report-clang-tidy + path: ${{ steps.analyze.outputs.report_path }} + + - name: Report warnings + if: always() && steps.analyze.outputs.warning_count > 0 + shell: bash + run: | + echo "::warning::clang-tidy found ${{ steps.analyze.outputs.warning_count }} warnings. See the uploaded report for details." diff --git a/.github/workflows/iwyu.yml b/.github/workflows/iwyu.yml new file mode 100644 index 00000000..585882df --- /dev/null +++ b/.github/workflows/iwyu.yml @@ -0,0 +1,90 @@ +# Copyright 2026 atframework +name: "include-what-you-use" + +on: + push: + branches: + - main + pull_request: + branches: [main] + +permissions: + contents: read + +jobs: + iwyu: + name: Include-What-You-Use + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - cc: clang + cxx_standard: "14" + env: + CC: ${{ matrix.cc }} + CXX: "clang++" + CXX_STANDARD: ${{ matrix.cxx_standard }} + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + submodules: recursive + + - name: Generate cache key + shell: bash + run: git submodule > '.github/.cache-key' + + - name: Cache packages + uses: actions/cache@v5 + with: + path: | + third_party/install + key: ${{ runner.os }}-iwyu-${{ hashFiles('.github/.cache-key') }} + + - name: Setup dependencies + shell: bash + run: | + sudo apt-get update + sudo apt-get install -y --no-install-recommends \ + build-essential cmake ninja-build \ + libssl-dev libcurl4-openssl-dev \ + clang llvm-dev libclang-dev + + - name: Install include-what-you-use + shell: bash + run: | + sudo bash ci/install_iwyu.sh + + - name: Build with include-what-you-use + id: build + shell: bash + run: | + bash ci/do_ci.sh iwyu + echo "build_log=${{ github.workspace }}/build_jobs_iwyu/iwyu.log" >> "$GITHUB_OUTPUT" + + - name: Count IWYU warnings + id: analyze + shell: bash + run: | + BUILD_LOG="${{ steps.build.outputs.build_log }}" + if [[ -f "$BUILD_LOG" ]]; then + WARNING_COUNT=$(grep -c "include-what-you-use reported diagnostics:" "$BUILD_LOG" || true) + else + WARNING_COUNT=0 + fi + echo "warning_count=$WARNING_COUNT" >> "$GITHUB_OUTPUT" + echo "IWYU warning count: $WARNING_COUNT" + + - name: Upload IWYU log + uses: actions/upload-artifact@v4 + if: always() + with: + name: Logs-iwyu + path: ${{ steps.build.outputs.build_log }} + + - name: Report warnings + if: always() && steps.analyze.outputs.warning_count > 0 + shell: bash + run: | + echo "::warning::include-what-you-use found ${{ steps.analyze.outputs.warning_count }} diagnostics. See the uploaded log for details." diff --git a/ci/do_ci.sh b/ci/do_ci.sh index 2340ffc2..f9ccbd06 100644 --- a/ci/do_ci.sh +++ b/ci/do_ci.sh @@ -183,5 +183,18 @@ elif [[ "$1" == "msys2.mingw.test" ]]; then done echo "PATH=$PATH" ctest . -VV -C $CONFIGURATION -L atframe_utils +elif [[ "$1" == "clang-tidy" ]]; then + CRYPTO_OPTIONS="-DATFRAMEWORK_CMAKE_TOOLSET_THIRD_PARTY_CRYPTO_USE_OPENSSL=ON" + bash cmake_dev.sh -t -u -b RelWithDebInfo -r build_jobs_clang_tidy -c "${USE_CC:-clang}" -- $CRYPTO_OPTIONS \ + "-DATFRAMEWORK_CMAKE_TOOLSET_THIRD_PARTY_LOW_MEMORY_MODE=ON" + cd build_jobs_clang_tidy + cmake --build . -j --config $CONFIGURATION 2>&1 | tee clang-tidy.log || cmake --build . --config $CONFIGURATION 2>&1 | tee clang-tidy.log +elif [[ "$1" == "iwyu" ]]; then + CRYPTO_OPTIONS="-DATFRAMEWORK_CMAKE_TOOLSET_THIRD_PARTY_CRYPTO_USE_OPENSSL=ON" + IWYU_OPTIONS="-DCMAKE_CXX_INCLUDE_WHAT_YOU_USE=include-what-you-use" + bash cmake_dev.sh -u -b RelWithDebInfo -r build_jobs_iwyu -c "${USE_CC:-clang}" -- $CRYPTO_OPTIONS $IWYU_OPTIONS \ + "-DATFRAMEWORK_CMAKE_TOOLSET_THIRD_PARTY_LOW_MEMORY_MODE=ON" + cd build_jobs_iwyu + cmake --build . -j --config $CONFIGURATION 2>&1 | tee iwyu.log || cmake --build . --config $CONFIGURATION 2>&1 | tee iwyu.log fi diff --git a/ci/install_iwyu.sh b/ci/install_iwyu.sh new file mode 100755 index 00000000..6e6bba5e --- /dev/null +++ b/ci/install_iwyu.sh @@ -0,0 +1,43 @@ +#!/bin/bash +# Copyright 2026 atframework +# Build and install include-what-you-use from source, matching the installed clang version. + +set -euo pipefail + +# Detect installed clang version +CLANG_VERSION=$(clang --version | head -1 | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1) +CLANG_MAJOR=$(echo "$CLANG_VERSION" | cut -d. -f1) + +echo "Detected clang version: $CLANG_VERSION (major: $CLANG_MAJOR)" + +# Map clang major version to IWYU branch +# IWYU tracks LLVM release branches (clang_18, clang_19, etc.) +IWYU_BRANCH="clang_${CLANG_MAJOR}" + +echo "Will build IWYU from branch: $IWYU_BRANCH" + +IWYU_BUILD_DIR=$(mktemp -d) +trap "rm -rf $IWYU_BUILD_DIR" EXIT + +echo "Cloning include-what-you-use..." +git clone --depth 1 --branch "$IWYU_BRANCH" https://github.com/include-what-you-use/include-what-you-use.git "$IWYU_BUILD_DIR/iwyu" + +mkdir -p "$IWYU_BUILD_DIR/iwyu/build" +cd "$IWYU_BUILD_DIR/iwyu/build" + +echo "Configuring include-what-you-use..." +cmake .. \ + -G Ninja \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_PREFIX_PATH="$(llvm-config --prefix)" + +echo "Building include-what-you-use..." +cmake --build . -j"$(nproc 2>/dev/null || echo 4)" + +echo "Installing include-what-you-use..." +cmake --install . + +echo "Verifying installation..." +include-what-you-use --version + +echo "include-what-you-use installed successfully." diff --git a/ci/parse_clang_tidy_warnings.py b/ci/parse_clang_tidy_warnings.py new file mode 100644 index 00000000..92302afb --- /dev/null +++ b/ci/parse_clang_tidy_warnings.py @@ -0,0 +1,131 @@ +#!/usr/bin/env python3 +# Copyright 2026 atframework +# Parse clang-tidy build log and generate a markdown report with deduplicated warnings. + +import argparse +import re +import sys +from pathlib import Path + + +def parse_warnings(log_content: str) -> list[dict]: + """Extract clang-tidy warnings from build log content.""" + # Match lines like: /path/to/file.cpp:123:45: warning: message [check-name] + pattern = re.compile( + r"^(.+?):(\d+):(\d+):\s+warning:\s+(.+?)\s+\[([a-zA-Z0-9,._-]+)\]\s*$" + ) + warnings = [] + for line in log_content.splitlines(): + m = pattern.match(line.strip()) + if m: + warnings.append( + { + "file": m.group(1), + "line": int(m.group(2)), + "col": int(m.group(3)), + "message": m.group(4), + "check": m.group(5), + } + ) + return warnings + + +def deduplicate(warnings: list[dict]) -> list[dict]: + """Deduplicate warnings by (file, line, check).""" + seen = set() + unique = [] + for w in warnings: + key = (w["file"], w["line"], w["check"]) + if key not in seen: + seen.add(key) + unique.append(w) + return unique + + +def normalize_path(filepath: str, workspace: str = "") -> str: + """Normalize file path relative to workspace root.""" + if workspace and filepath.startswith(workspace): + rel = filepath[len(workspace) :].lstrip("/") + return rel + return filepath + + +def generate_report(warnings: list[dict], workspace: str = "") -> str: + """Generate a markdown report from deduplicated warnings.""" + lines = ["# Clang-Tidy Warning Report", ""] + + if not warnings: + lines.append("No warnings found. :tada:") + return "\n".join(lines) + + lines.append(f"**Total unique warnings: {len(warnings)}**") + lines.append("") + + # Group by check name + by_check: dict[str, list[dict]] = {} + for w in warnings: + check = w["check"] + by_check.setdefault(check, []).append(w) + + lines.append("## Summary by Check") + lines.append("") + lines.append("| Check | Count |") + lines.append("|-------|-------|") + for check in sorted(by_check.keys()): + lines.append(f"| `{check}` | {len(by_check[check])} |") + lines.append("") + + # Detailed list + lines.append("## Details") + lines.append("") + for w in warnings: + fpath = normalize_path(w["file"], workspace) + lines.append( + f"- `{fpath}:{w['line']}:{w['col']}` — {w['message']} [`{w['check']}`]" + ) + lines.append("") + + return "\n".join(lines) + + +def main(): + parser = argparse.ArgumentParser(description="Parse clang-tidy warnings from build log") + parser.add_argument("--build_log", required=True, help="Path to the build log file") + parser.add_argument("--output", default="", help="Path to write the markdown report") + parser.add_argument("--workspace", default="", help="Workspace root for path normalization") + parser.add_argument( + "--count-only", + action="store_true", + help="Only print the deduplicated warning count", + ) + args = parser.parse_args() + + log_path = Path(args.build_log) + if not log_path.exists(): + print(f"Build log not found: {log_path}", file=sys.stderr) + if args.count_only: + print(0) + sys.exit(0) + + log_content = log_path.read_text(encoding="utf-8", errors="replace") + warnings = parse_warnings(log_content) + unique_warnings = deduplicate(warnings) + + if args.count_only: + print(len(unique_warnings)) + return + + report = generate_report(unique_warnings, workspace=args.workspace) + + if args.output: + out_path = Path(args.output) + out_path.write_text(report, encoding="utf-8") + print(f"Report written to: {out_path}") + print(f"TOTAL_WARNINGS={len(unique_warnings)}") + print(f"REPORT_PATH={out_path}") + else: + print(report) + + +if __name__ == "__main__": + main() diff --git a/cmake_dev.sh b/cmake_dev.sh index f83e2042..e0c3b5a0 100755 --- a/cmake_dev.sh +++ b/cmake_dev.sh @@ -125,7 +125,7 @@ while getopts "ab:c:d:e:hlr:tus-" OPTION; do CUSTOM_BUILD_DIR="$OPTARG" ;; t) - CMAKE_CLANG_TIDY="-D -checks=* --" + CMAKE_CLANG_TIDY="-DCMAKE_CXX_CLANG_TIDY=clang-tidy" ;; u) CMAKE_OPTIONS="$CMAKE_OPTIONS -DPROJECT_ENABLE_UNITTEST=YES -DBUILD_TESTING=ON" @@ -169,11 +169,11 @@ if [[ $CMAKE_CLANG_ANALYZER -ne 0 ]]; then fi if [[ "x$NINJA_BIN" != "x" ]]; then - ${CMAKE_BIN[@]} .. -G Ninja -DCMAKE_BUILD_TYPE=$CMAKE_BUILD_TYPE $CMAKE_OPTIONS "$@" + ${CMAKE_BIN[@]} .. -G Ninja -DCMAKE_BUILD_TYPE=$CMAKE_BUILD_TYPE $CMAKE_OPTIONS $CMAKE_CLANG_TIDY "$@" elif [[ "$CHECK_MSYS" == "mingw" ]]; then - ${CMAKE_BIN[@]} .. -G "MinGW Makefiles" -DCMAKE_BUILD_TYPE=$CMAKE_BUILD_TYPE $CMAKE_OPTIONS "$@" + ${CMAKE_BIN[@]} .. -G "MinGW Makefiles" -DCMAKE_BUILD_TYPE=$CMAKE_BUILD_TYPE $CMAKE_OPTIONS $CMAKE_CLANG_TIDY "$@" else - ${CMAKE_BIN[@]} .. -DCMAKE_BUILD_TYPE=$CMAKE_BUILD_TYPE $CMAKE_OPTIONS "$@" + ${CMAKE_BIN[@]} .. -DCMAKE_BUILD_TYPE=$CMAKE_BUILD_TYPE $CMAKE_OPTIONS $CMAKE_CLANG_TIDY "$@" fi if [[ 1 -eq $CMAKE_CLANG_ANALYZER ]]; then