478 lines
17 KiB
Python
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()
|