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

478 lines
17 KiB
Python

#!/usr/bin/env python3
"""
Implementation Files Checker
Scans src/fl/ and src/fx/ directories for *.hpp and *.cpp.hpp files.
Provides statistics and can verify inclusion in the all-source build.
"""
import argparse
import json
import os
from pathlib import Path
from typing import Dict, List, Set
# Get project root directory
PROJECT_ROOT = Path(__file__).parent.parent.parent
SRC_ROOT = PROJECT_ROOT / "src"
FL_DIR = SRC_ROOT / "fl"
FX_DIR = SRC_ROOT / "fx"
ALL_SOURCE_BUILD_FILE = SRC_ROOT / "fastled_compile.hpp.cpp"
# Hierarchical compile files
HIERARCHICAL_FILES = [
SRC_ROOT / "fl" / "fl_compile.hpp",
SRC_ROOT / "fx" / "fx_compile.hpp",
SRC_ROOT / "sensors" / "sensors_compile.hpp",
SRC_ROOT / "platforms" / "platforms_compile.hpp",
SRC_ROOT / "third_party" / "third_party_compile.hpp",
SRC_ROOT / "src_compile.hpp",
]
# Detect if running in CI/test environment for ASCII-only output
USE_ASCII_ONLY = (
os.environ.get("FASTLED_CI_NO_INTERACTIVE") == "true"
or os.environ.get("GITHUB_ACTIONS") == "true"
or os.environ.get("CI") == "true"
)
def collect_files_by_type(directory: Path) -> Dict[str, List[Path]]:
"""Collect files by type (.hpp vs .cpp.hpp) from a directory.
Args:
directory: Directory to scan
Returns:
Dictionary with 'hpp' and 'cpp_hpp' keys containing lists of files
"""
files = {"hpp": [], "cpp_hpp": []}
if not directory.exists():
print(f"Warning: Directory {directory} does not exist")
return files
# Recursively find all .hpp and .cpp.hpp files
for file_path in directory.rglob("*.hpp"):
if file_path.name.endswith(".cpp.hpp"):
files["cpp_hpp"].append(file_path)
else:
files["hpp"].append(file_path)
return files
def get_all_source_build_includes() -> Set[str]:
"""Extract the list of #include statements from the all-source build files.
This function handles the hierarchical structure by checking:
1. The main all-source build file (fastled_compile.hpp.cpp)
2. All hierarchical module compile files (fl_compile.hpp, fx_compile.hpp, etc.)
Returns:
Set of included file paths (relative to src/)
"""
includes = set()
# Check main all-source build file
if not ALL_SOURCE_BUILD_FILE.exists():
print(f"Warning: All-source build file {ALL_SOURCE_BUILD_FILE} does not exist")
return includes
# Function to extract includes from a file
def extract_includes_from_file(file_path: Path) -> Set[str]:
file_includes = set()
if not file_path.exists():
return file_includes
try:
with open(file_path, "r", encoding="utf-8") as f:
for line in f:
line = line.strip()
# Look for #include statements
if line.startswith('#include "') and line.endswith('"'):
# Extract the include path
include_path = line[10:-1] # Remove '#include "' and '"'
file_includes.add(include_path)
except Exception as e:
print(f"Error reading file {file_path}: {e}")
return file_includes
# Extract includes from main file
includes.update(extract_includes_from_file(ALL_SOURCE_BUILD_FILE))
# Extract includes from all hierarchical files
for hierarchical_file in HIERARCHICAL_FILES:
if hierarchical_file.exists():
hierarchical_includes = extract_includes_from_file(hierarchical_file)
includes.update(hierarchical_includes)
return includes
def check_inclusion_in_all_source_build(
files: Dict[str, List[Path]], base_dir: Path
) -> Dict[str, Dict[str, bool]]:
"""Check which implementation files are included in the all-source build.
Args:
files: Dictionary of files by type
base_dir: Base directory (fl or fx) for relative path calculation
Returns:
Dictionary mapping file types to dictionaries of file -> included status
"""
all_source_includes = get_all_source_build_includes()
inclusion_status = {"hpp": {}, "cpp_hpp": {}}
for file_type, file_list in files.items():
if file_type == "cpp_hpp": # Only check .cpp.hpp files for inclusion
for file_path in file_list:
# Calculate relative path from src/
relative_path = file_path.relative_to(SRC_ROOT)
relative_path_str = str(relative_path).replace(
"\\", "/"
) # Normalize path separators
# Check if this file is included
is_included = relative_path_str in all_source_includes
inclusion_status[file_type][str(file_path)] = is_included
return inclusion_status
def print_file_list(
files: List[Path], title: str, base_dir: Path, show_relative: bool = True
):
"""Print a formatted list of files.
Args:
files: List of file paths
title: Title for the section
base_dir: Base directory for relative path calculation
show_relative: Whether to show relative paths
"""
print(f"\n{title} ({len(files)} files):")
print("-" * (len(title) + 20))
if not files:
print(" (none)")
return
# Sort files for consistent output
sorted_files = sorted(files, key=lambda p: str(p))
for i, file_path in enumerate(sorted_files, 1):
if show_relative:
rel_path = file_path.relative_to(base_dir)
print(f" {i:2d}. {rel_path}")
else:
print(f" {i:2d}. {file_path.name}")
def print_inclusion_report(
inclusion_status: Dict[str, Dict[str, bool]], base_dir: Path
):
"""Print a report of which implementation files are included in all-source build.
Args:
inclusion_status: Dictionary of inclusion status by file type
base_dir: Base directory for relative path calculation
"""
cpp_hpp_status = inclusion_status.get("cpp_hpp", {})
if not cpp_hpp_status:
print("\nNo .cpp.hpp files found to check for inclusion")
return
included_files = [path for path, included in cpp_hpp_status.items() if included]
missing_files = [path for path, included in cpp_hpp_status.items() if not included]
print("\nALL-SOURCE BUILD INCLUSION STATUS:")
print("=" * 50)
# Use ASCII or Unicode symbols based on environment
check_symbol = "[+]" if USE_ASCII_ONLY else ""
cross_symbol = "[-]" if USE_ASCII_ONLY else ""
print(f"{check_symbol} Included in all-source build: {len(included_files)}")
if included_files:
for file_path in sorted(included_files):
rel_path = Path(file_path).relative_to(SRC_ROOT)
print(f" {check_symbol} {rel_path}")
print(f"\n{cross_symbol} Missing from all-source build: {len(missing_files)}")
if missing_files:
for file_path in sorted(missing_files):
rel_path = Path(file_path).relative_to(SRC_ROOT)
print(f" {cross_symbol} {rel_path}")
def generate_summary_report(
fl_files: Dict[str, List[Path]], fx_files: Dict[str, List[Path]]
) -> Dict:
"""Generate a summary report with statistics.
Args:
fl_files: Files found in fl/ directory
fx_files: Files found in fx/ directory
Returns:
Dictionary containing summary statistics
"""
summary = {
"fl_directory": {
"hpp_files": len(fl_files["hpp"]),
"cpp_hpp_files": len(fl_files["cpp_hpp"]),
"total_files": len(fl_files["hpp"]) + len(fl_files["cpp_hpp"]),
},
"fx_directory": {
"hpp_files": len(fx_files["hpp"]),
"cpp_hpp_files": len(fx_files["cpp_hpp"]),
"total_files": len(fx_files["hpp"]) + len(fx_files["cpp_hpp"]),
},
"totals": {
"hpp_files": len(fl_files["hpp"]) + len(fx_files["hpp"]),
"cpp_hpp_files": len(fl_files["cpp_hpp"]) + len(fx_files["cpp_hpp"]),
"total_files": len(fl_files["hpp"])
+ len(fl_files["cpp_hpp"])
+ len(fx_files["hpp"])
+ len(fx_files["cpp_hpp"]),
},
}
return summary
def print_summary_report(summary: Dict):
"""Print a formatted summary report.
Args:
summary: Summary statistics dictionary
"""
print("\n" + "=" * 80)
print("IMPLEMENTATION FILES SUMMARY REPORT")
print("=" * 80)
# Use ASCII or Unicode symbols based on environment
folder_symbol = "[DIR]" if USE_ASCII_ONLY else "📁"
chart_symbol = "[STATS]" if USE_ASCII_ONLY else "📊"
ratio_symbol = "[RATIO]" if USE_ASCII_ONLY else "📈"
print(f"\n{folder_symbol} FL DIRECTORY ({FL_DIR.relative_to(PROJECT_ROOT)}):")
print(
f" Header files (.hpp): {summary['fl_directory']['hpp_files']:3d}"
)
print(
f" Implementation files (.cpp.hpp): {summary['fl_directory']['cpp_hpp_files']:3d}"
)
print(
f" Total files: {summary['fl_directory']['total_files']:3d}"
)
print(f"\n{folder_symbol} FX DIRECTORY ({FX_DIR.relative_to(PROJECT_ROOT)}):")
print(
f" Header files (.hpp): {summary['fx_directory']['hpp_files']:3d}"
)
print(
f" Implementation files (.cpp.hpp): {summary['fx_directory']['cpp_hpp_files']:3d}"
)
print(
f" Total files: {summary['fx_directory']['total_files']:3d}"
)
print(f"\n{chart_symbol} TOTALS:")
print(f" Header files (.hpp): {summary['totals']['hpp_files']:3d}")
print(
f" Implementation files (.cpp.hpp): {summary['totals']['cpp_hpp_files']:3d}"
)
print(f" Total files: {summary['totals']['total_files']:3d}")
# Calculate ratios
total_hpp = summary["totals"]["hpp_files"]
total_cpp_hpp = summary["totals"]["cpp_hpp_files"]
if total_hpp > 0:
impl_ratio = (total_cpp_hpp / total_hpp) * 100
print(f"\n{ratio_symbol} IMPLEMENTATION RATIO:")
print(f" Implementation files per header: {impl_ratio:.1f}%")
print(f" ({total_cpp_hpp} .cpp.hpp files for {total_hpp} .hpp files)")
def main():
"""Main function to run the implementation files checker."""
parser = argparse.ArgumentParser(
description="Check *.hpp and *.cpp.hpp files in fl/ and fx/ directories"
)
parser.add_argument("--list", action="store_true", help="List all files found")
parser.add_argument(
"--check-inclusion",
action="store_true",
help="Check which .cpp.hpp files are included in all-source build",
)
parser.add_argument("--json", action="store_true", help="Output summary as JSON")
parser.add_argument("--verbose", "-v", action="store_true", help="Verbose output")
parser.add_argument(
"--ascii-only",
action="store_true",
help="Use ASCII-only output (no Unicode emoji)",
)
parser.add_argument(
"--suppress-summary-on-100-percent",
action="store_true",
help="Suppress summary report when inclusion percentage is 100%",
)
args = parser.parse_args()
# Override USE_ASCII_ONLY if command line flag is set
global USE_ASCII_ONLY
if args.ascii_only:
USE_ASCII_ONLY = True
# Collect files from both directories
print("Scanning implementation files...")
fl_files = collect_files_by_type(FL_DIR)
fx_files = collect_files_by_type(FX_DIR)
# Generate summary
summary = generate_summary_report(fl_files, fx_files)
# Define symbols once for all output modes
search_symbol = "[SEARCH]" if USE_ASCII_ONLY else "🔍"
stats_symbol = "[STATS]" if USE_ASCII_ONLY else "📊"
config_symbol = "[CONFIG]" if USE_ASCII_ONLY else "🔧"
# Calculate inclusion percentage to determine if we should suppress summary
should_suppress_summary = False
if args.suppress_summary_on_100_percent:
all_cpp_hpp_files = fl_files["cpp_hpp"] + fx_files["cpp_hpp"]
all_source_includes = get_all_source_build_includes()
included_count = 0
for file_path in all_cpp_hpp_files:
relative_path = file_path.relative_to(SRC_ROOT)
relative_path_str = str(relative_path).replace("\\", "/")
if relative_path_str in all_source_includes:
included_count += 1
total_impl_files = len(all_cpp_hpp_files)
if total_impl_files > 0:
inclusion_percentage = (included_count / total_impl_files) * 100
should_suppress_summary = inclusion_percentage >= 100.0
# Output based on requested format
if args.json:
# Add file lists to summary for JSON output
summary["fl_files"] = {
"hpp": [str(p.relative_to(SRC_ROOT)) for p in fl_files["hpp"]],
"cpp_hpp": [str(p.relative_to(SRC_ROOT)) for p in fl_files["cpp_hpp"]],
}
summary["fx_files"] = {
"hpp": [str(p.relative_to(SRC_ROOT)) for p in fx_files["hpp"]],
"cpp_hpp": [str(p.relative_to(SRC_ROOT)) for p in fx_files["cpp_hpp"]],
}
print(json.dumps(summary, indent=2))
return
# Print summary report only if not suppressed
if not should_suppress_summary:
print_summary_report(summary)
else:
print("Summary report suppressed: 100% inclusion coverage achieved")
# List files if requested
if args.list:
print("\n" + "=" * 80)
print("DETAILED FILE LISTINGS")
print("=" * 80)
# FL directory files
print_file_list(fl_files["hpp"], "FL Header Files (.hpp)", FL_DIR)
print_file_list(
fl_files["cpp_hpp"], "FL Implementation Files (.cpp.hpp)", FL_DIR
)
# FX directory files
print_file_list(fx_files["hpp"], "FX Header Files (.hpp)", FX_DIR)
print_file_list(
fx_files["cpp_hpp"], "FX Implementation Files (.cpp.hpp)", FX_DIR
)
# Check inclusion in all-source build if requested
if args.check_inclusion:
# Only show inclusion check if not suppressing or if we don't have 100% coverage
if not should_suppress_summary:
print("\n" + "=" * 80)
print("ALL-SOURCE BUILD INCLUSION CHECK")
print("=" * 80)
fl_inclusion = check_inclusion_in_all_source_build(fl_files, FL_DIR)
fx_inclusion = check_inclusion_in_all_source_build(fx_files, FX_DIR)
print(f"\n{search_symbol} FL DIRECTORY INCLUSION:")
print_inclusion_report(fl_inclusion, FL_DIR)
print(f"\n{search_symbol} FX DIRECTORY INCLUSION:")
print_inclusion_report(fx_inclusion, FX_DIR)
# Overall inclusion statistics
all_cpp_hpp_files = fl_files["cpp_hpp"] + fx_files["cpp_hpp"]
all_source_includes = get_all_source_build_includes()
included_count = 0
for file_path in all_cpp_hpp_files:
relative_path = file_path.relative_to(SRC_ROOT)
relative_path_str = str(relative_path).replace("\\", "/")
if relative_path_str in all_source_includes:
included_count += 1
total_impl_files = len(all_cpp_hpp_files)
if total_impl_files > 0:
inclusion_percentage = (included_count / total_impl_files) * 100
print(f"\n{stats_symbol} OVERALL INCLUSION STATISTICS:")
print(f" Total .cpp.hpp files found: {total_impl_files}")
print(f" Included in all-source build: {included_count}")
print(f" Inclusion percentage: {inclusion_percentage:.1f}%")
# Always check for missing files and exit with error if any are missing
all_cpp_hpp_files = fl_files["cpp_hpp"] + fx_files["cpp_hpp"]
all_source_includes = get_all_source_build_includes()
included_count = 0
for file_path in all_cpp_hpp_files:
relative_path = file_path.relative_to(SRC_ROOT)
relative_path_str = str(relative_path).replace("\\", "/")
if relative_path_str in all_source_includes:
included_count += 1
total_impl_files = len(all_cpp_hpp_files)
total_missing = total_impl_files - included_count
if total_missing > 0:
# Print an explicit error message before exiting
error_symbol = "[ERROR]" if USE_ASCII_ONLY else "🚨"
print(
f"\n{error_symbol} {total_missing} implementation file(s) are missing from the all-source build!"
)
print(" Failing script due to incomplete all-source build inclusion.")
import sys
sys.exit(1)
if args.verbose:
print(f"\n{config_symbol} CONFIGURATION:")
print(f" Project root: {PROJECT_ROOT}")
print(f" FL directory: {FL_DIR}")
print(f" FX directory: {FX_DIR}")
print(f" All-source build file: {ALL_SOURCE_BUILD_FILE}")
print(" Hierarchical compile files:")
for hfile in HIERARCHICAL_FILES:
status = "" if hfile.exists() else ""
print(f" {status} {hfile.relative_to(PROJECT_ROOT)}")
if __name__ == "__main__":
main()