Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 99 additions & 0 deletions .github/workflows/clang-tidy.yml
Original file line number Diff line number Diff line change
@@ -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."
90 changes: 90 additions & 0 deletions .github/workflows/iwyu.yml
Original file line number Diff line number Diff line change
@@ -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."
13 changes: 13 additions & 0 deletions ci/do_ci.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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

43 changes: 43 additions & 0 deletions ci/install_iwyu.sh
Original file line number Diff line number Diff line change
@@ -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."
131 changes: 131 additions & 0 deletions ci/parse_clang_tidy_warnings.py
Original file line number Diff line number Diff line change
@@ -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()
Loading
Loading