Files
klubhaus-doorbell/libraries/FastLED/ci/ci-iwyu.py
2026-02-12 00:45:31 -08:00

272 lines
8.2 KiB
Python
Executable File

#!/usr/bin/env python3
# pyright: reportUnknownMemberType=false
import argparse
import os
import subprocess
import sys
from pathlib import Path
from typing import List
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Run include-what-you-use on the project"
)
parser.add_argument("board", nargs="?", help="Board to check, optional")
parser.add_argument(
"--fix",
action="store_true",
help="Automatically apply suggested fixes using fix_includes",
)
parser.add_argument(
"--mapping-file",
action="append",
help="Additional mapping file to use (can be specified multiple times)",
)
parser.add_argument(
"--max-line-length",
type=int,
default=100,
help="Maximum line length for suggestions (default: 100)",
)
parser.add_argument("--verbose", action="store_true", help="Enable verbose output")
return parser.parse_args()
def find_platformio_project_dir(board_dir: Path) -> Path | None:
"""Find a directory containing platformio.ini file in the board's build directory.
With the new pio ci build system, the structure is:
.build/uno/Blink/platformio.ini
.build/uno/SomeExample/platformio.ini
We need to find one of these example directories that has a platformio.ini file.
"""
if not board_dir.exists():
return None
# Look for subdirectories containing platformio.ini
for subdir in board_dir.iterdir():
if subdir.is_dir():
platformio_ini = subdir / "platformio.ini"
if platformio_ini.exists():
print(f"Found platformio.ini in {subdir}")
return subdir
# Fallback: check if there's a platformio.ini directly in the board directory (old system)
if (board_dir / "platformio.ini").exists():
print(f"Found platformio.ini directly in {board_dir}")
return board_dir
return None
def check_iwyu_available() -> bool:
"""Check if include-what-you-use is available in the system"""
try:
result = subprocess.run(
["include-what-you-use", "--version"],
capture_output=True,
text=True,
timeout=10,
)
return result.returncode == 0
except (
subprocess.CalledProcessError,
FileNotFoundError,
subprocess.TimeoutExpired,
):
return False
def run_iwyu_on_cpp_tests(args: argparse.Namespace) -> int:
"""Run IWYU on the C++ test suite using CMake integration"""
here = Path(__file__).parent
project_root = here.parent
print("Running include-what-you-use on C++ test suite...")
# Use the existing test infrastructure with --check flag
cmd = [
"uv",
"run",
"test.py",
"--cpp",
"--check", # This enables IWYU via CMake
"--clang",
"--no-interactive",
]
if args.verbose:
cmd.append("--verbose")
try:
result = subprocess.run(cmd, cwd=project_root)
return result.returncode
except subprocess.CalledProcessError as e:
print(f"IWYU analysis failed with return code {e.returncode}")
return e.returncode
def run_iwyu_on_platformio_project(project_dir: Path, args: argparse.Namespace) -> int:
"""Run IWYU on a PlatformIO project"""
print(f"Running include-what-you-use in {project_dir}")
os.chdir(str(project_dir))
# Build mapping file arguments
mapping_args: List[str] = []
# Add FastLED mapping files if they exist
project_root = project_dir
while project_root.parent != project_root: # Find project root
if (project_root / "ci" / "iwyu").exists():
break
project_root = project_root.parent
fastled_mapping = project_root / "ci" / "iwyu" / "fastled.imp"
stdlib_mapping = project_root / "ci" / "iwyu" / "stdlib.imp"
if fastled_mapping.exists():
mapping_args.extend(["--mapping_file", str(fastled_mapping)])
if stdlib_mapping.exists():
mapping_args.extend(["--mapping_file", str(stdlib_mapping)])
# Add user-specified mapping files
if args.mapping_file:
for mapping in args.mapping_file:
mapping_args.extend(["--mapping_file", mapping])
# Build IWYU command
iwyu_cmd = [
"include-what-you-use",
f"--max_line_length={args.max_line_length}",
"--quoted_includes_first",
"--no_comments",
]
if args.verbose:
iwyu_cmd.append("--verbose=3")
else:
iwyu_cmd.append("--verbose=1")
iwyu_cmd.extend(mapping_args)
# Run through PlatformIO's check system with IWYU
pio_cmd = [
"pio",
"check",
"--skip-packages",
"--src-filters=+<src/>",
"--tool=include-what-you-use",
"--flags",
] + iwyu_cmd[1:] # Skip the include-what-you-use binary name
try:
result = subprocess.run(pio_cmd)
return result.returncode
except subprocess.CalledProcessError as e:
print(f"PlatformIO IWYU check failed with return code {e.returncode}")
return e.returncode
def apply_iwyu_fixes(source_dir: Path) -> int:
"""Apply IWYU fixes using fix_includes tool"""
try:
# Check if fix_includes is available
subprocess.run(["fix_includes", "--help"], capture_output=True, check=True)
except (subprocess.CalledProcessError, FileNotFoundError):
print(
"Warning: fix_includes tool not found. Install IWYU tools to use --fix option."
)
return 1
# Find all .h and .cpp files
cpp_files: List[Path] = []
for pattern in ["**/*.cpp", "**/*.h", "**/*.hpp"]:
cpp_files.extend(source_dir.glob(pattern))
if not cpp_files:
print("No C++ files found to fix")
return 0
print(f"Applying IWYU fixes to {len(cpp_files)} files...")
# Run fix_includes on the source directory
cmd = ["fix_includes", "--update_comments", str(source_dir)]
try:
result = subprocess.run(cmd)
if result.returncode == 0:
print("IWYU fixes applied successfully")
else:
print(f"fix_includes failed with return code {result.returncode}")
return result.returncode
except subprocess.CalledProcessError as e:
print(f"Failed to apply IWYU fixes: {e}")
return e.returncode
def main() -> int:
args = parse_args()
here = Path(__file__).parent
project_root = here.parent
# Check if IWYU is available
if not check_iwyu_available():
print("Error: include-what-you-use not found in PATH")
print("Install it with:")
print(" Ubuntu/Debian: sudo apt install iwyu")
print(" macOS: brew install include-what-you-use")
print(" Or build from source: https://include-what-you-use.org/")
return 1
print("Found include-what-you-use")
# If no board specified, run on C++ test suite
if not args.board:
print("No board specified, running IWYU on C++ test suite")
return run_iwyu_on_cpp_tests(args)
# Run on specific board
build = project_root / ".build"
if not build.exists():
print(f"Build directory {build} not found")
print("Run a compilation first: ./compile [board] --examples [example]")
return 1
board_dir = build / args.board
if not board_dir.exists():
print(f"Board {args.board} not found in {build}")
print("Available boards:")
for d in build.iterdir():
if d.is_dir():
print(f" {d.name}")
return 1
project_dir = find_platformio_project_dir(board_dir)
if not project_dir:
print(f"No platformio.ini found in {board_dir} or its subdirectories")
print("This usually means the board hasn't been compiled yet.")
print(f"Try running: ./compile {args.board} --examples Blink")
return 1
# Run IWYU on the PlatformIO project
result = run_iwyu_on_platformio_project(project_dir, args)
# Apply fixes if requested and analysis succeeded
if args.fix and result == 0:
src_dir = project_dir / "src"
if src_dir.exists():
fix_result = apply_iwyu_fixes(src_dir)
if fix_result != 0:
result = fix_result
return result
if __name__ == "__main__":
sys.exit(main())