diff --git a/scripts/build_cleaner.py b/scripts/build_cleaner.py old mode 100644 new mode 100755 index ba9c6fe8..28b617a5 --- a/scripts/build_cleaner.py +++ b/scripts/build_cleaner.py @@ -93,13 +93,19 @@ def discover_cmake_libraries() -> List[CMakeSourceBlock]: """ Auto-discover CMake library files that explicitly opt-in to auto-maintenance. - Looks for comments like "# This list is auto-maintained by scripts/build_cleaner.py" - or "# AUTO-MAINTAINED" to identify variables that should be auto-updated. - """ - blocks = [] - seen_vars: Set[str] = set() + Looks for marker comments like: + - "# build_cleaner:auto-maintain" + - "# auto-maintained by build_cleaner.py" + - "# AUTO-MAINTAINED" + + Only source lists with these markers will be updated. + + Supports decomposed libraries where one cmake file defines multiple PREFIX_SUBDIR_SRC + variables (e.g., GFX_TYPES_SRC, GFX_BACKEND_SRC). Automatically scans subdirectories. + """ + # First pass: collect all variables per cmake file + file_variables: dict[Path, list[str]] = {} - # Scan for cmake library files for cmake_file in SOURCE_ROOT.rglob("*.cmake"): if 'lib/' in str(cmake_file) or 'third_party/' in str(cmake_file): continue @@ -108,41 +114,78 @@ def discover_cmake_libraries() -> List[CMakeSourceBlock]: content = cmake_file.read_text(encoding='utf-8') lines = content.splitlines() - # Look for source variable definitions with auto-maintain markers for i, line in enumerate(lines): # Check if previous lines indicate auto-maintenance auto_maintained = False - for j in range(max(0, i-3), i): - if 'auto-maintain' in lines[j].lower() or 'auto_maintain' in lines[j].lower(): + for j in range(max(0, i-5), i): + line_lower = lines[j].lower() + if ('build_cleaner' in line_lower and 'auto-maintain' in line_lower) or \ + 'auto_maintain' in line_lower: auto_maintained = True break if not auto_maintained: continue - # Extract variable name from set() statement - match = re.search(r'set\s*\(\s*(\w+(?:_SRC|_SOURCES|_SOURCE))\s', line) + # Extract variable name (allow for line breaks or closing paren) + match = re.search(r'set\s*\(\s*(\w+(?:_SRC|_SOURCES|_SOURCE))(?:\s|$|\))', line) if match: var_name = match.group(1) - - # Skip if we've already seen this variable - if var_name in seen_vars: - continue - - seen_vars.add(var_name) - cmake_dir = cmake_file.parent - - # Determine if recursive based on cmake file location or content - is_recursive = cmake_dir != SOURCE_ROOT / "app/core" - - blocks.append(CMakeSourceBlock( - variable=var_name, - cmake_path=cmake_file, - directories=(DirectorySpec(cmake_dir, recursive=is_recursive),), - )) + if cmake_file not in file_variables: + file_variables[cmake_file] = [] + if var_name not in file_variables[cmake_file]: + file_variables[cmake_file].append(var_name) + except Exception as e: print(f"Warning: Could not process {cmake_file}: {e}") + # Second pass: create blocks with proper subdirectory detection + blocks = [] + for cmake_file, variables in file_variables.items(): + cmake_dir = cmake_file.parent + is_recursive = cmake_dir != SOURCE_ROOT / "app/core" + + # Analyze variable naming patterns to detect decomposed libraries + # Group variables by prefix (e.g., GFX_*, GUI_*, EDITOR_*) + prefix_groups: dict[str, list[str]] = {} + for var_name in variables: + match = re.match(r'([A-Z]+)_([A-Z_]+)_(?:SRC|SOURCES|SOURCE)$', var_name) + if match: + prefix = match.group(1) + if prefix not in prefix_groups: + prefix_groups[prefix] = [] + prefix_groups[prefix].append(var_name) + + # If a prefix has multiple variables, treat it as a decomposed library + decomposed_prefixes = {p for p, vars in prefix_groups.items() if len(vars) >= 2} + + for var_name in variables: + # Try to extract subdirectory from variable name + subdir_match = re.match(r'([A-Z]+)_([A-Z_]+)_(?:SRC|SOURCES|SOURCE)$', var_name) + if subdir_match: + prefix = subdir_match.group(1) + subdir_part = subdir_match.group(2) + + # If this prefix indicates a decomposed library, scan subdirectory + if prefix in decomposed_prefixes: + subdir = subdir_part.lower() + target_dir = cmake_dir / subdir + + if target_dir.exists() and target_dir.is_dir(): + blocks.append(CMakeSourceBlock( + variable=var_name, + cmake_path=cmake_file, + directories=(DirectorySpec(target_dir, recursive=True),), + )) + continue + + # Fallback: scan entire cmake directory + blocks.append(CMakeSourceBlock( + variable=var_name, + cmake_path=cmake_file, + directories=(DirectorySpec(cmake_dir, recursive=is_recursive),), + )) + return blocks @@ -180,42 +223,42 @@ STATIC_CONFIG: Sequence[CMakeSourceBlock] = ( cmake_path=SOURCE_ROOT / "util/util.cmake", directories=(DirectorySpec(SOURCE_ROOT / "util"),), ), - CMakeSourceBlock( - variable="GFX_TYPES_SRC", - cmake_path=SOURCE_ROOT / "app/gfx/gfx_library.cmake", - directories=(DirectorySpec(SOURCE_ROOT / "app/gfx/types"),), - ), - CMakeSourceBlock( - variable="GFX_BACKEND_SRC", - cmake_path=SOURCE_ROOT / "app/gfx/gfx_library.cmake", - directories=(DirectorySpec(SOURCE_ROOT / "app/gfx/backend"),), - ), - CMakeSourceBlock( - variable="GFX_RESOURCE_SRC", - cmake_path=SOURCE_ROOT / "app/gfx/gfx_library.cmake", - directories=(DirectorySpec(SOURCE_ROOT / "app/gfx/resource"),), - exclude={Path("app/gfx/render/background_buffer.cc")}, # This is in resource but used by render - ), - CMakeSourceBlock( - variable="GFX_CORE_SRC", - cmake_path=SOURCE_ROOT / "app/gfx/gfx_library.cmake", - directories=(DirectorySpec(SOURCE_ROOT / "app/gfx/core"),), - ), - CMakeSourceBlock( - variable="GFX_UTIL_SRC", - cmake_path=SOURCE_ROOT / "app/gfx/gfx_library.cmake", - directories=(DirectorySpec(SOURCE_ROOT / "app/gfx/util"),), - ), - CMakeSourceBlock( - variable="GFX_RENDER_SRC", - cmake_path=SOURCE_ROOT / "app/gfx/gfx_library.cmake", - directories=(DirectorySpec(SOURCE_ROOT / "app/gfx/render"),), - ), - CMakeSourceBlock( - variable="GFX_DEBUG_SRC", - cmake_path=SOURCE_ROOT / "app/gfx/gfx_library.cmake", - directories=(DirectorySpec(SOURCE_ROOT / "app/gfx/debug"),), - ), + # Note: These are commented out in favor of auto-discovery via markers + # CMakeSourceBlock( + # variable="GFX_TYPES_SRC", + # cmake_path=SOURCE_ROOT / "app/gfx/gfx_library.cmake", + # directories=(DirectorySpec(SOURCE_ROOT / "app/gfx/types"),), + # ), + # CMakeSourceBlock( + # variable="GFX_BACKEND_SRC", + # cmake_path=SOURCE_ROOT / "app/gfx/gfx_library.cmake", + # directories=(DirectorySpec(SOURCE_ROOT / "app/gfx/backend"),), + # ), + # CMakeSourceBlock( + # variable="GFX_RESOURCE_SRC", + # cmake_path=SOURCE_ROOT / "app/gfx/gfx_library.cmake", + # directories=(DirectorySpec(SOURCE_ROOT / "app/gfx/resource"),), + # ), + # CMakeSourceBlock( + # variable="GFX_CORE_SRC", + # cmake_path=SOURCE_ROOT / "app/gfx/gfx_library.cmake", + # directories=(DirectorySpec(SOURCE_ROOT / "app/gfx/core"),), + # ), + # CMakeSourceBlock( + # variable="GFX_UTIL_SRC", + # cmake_path=SOURCE_ROOT / "app/gfx/gfx_library.cmake", + # directories=(DirectorySpec(SOURCE_ROOT / "app/gfx/util"),), + # ), + # CMakeSourceBlock( + # variable="GFX_RENDER_SRC", + # cmake_path=SOURCE_ROOT / "app/gfx/gfx_library.cmake", + # directories=(DirectorySpec(SOURCE_ROOT / "app/gfx/render"),), + # ), + # CMakeSourceBlock( + # variable="GFX_DEBUG_SRC", + # cmake_path=SOURCE_ROOT / "app/gfx/gfx_library.cmake", + # directories=(DirectorySpec(SOURCE_ROOT / "app/gfx/debug"),), + # ), CMakeSourceBlock( variable="GUI_CORE_SRC", cmake_path=SOURCE_ROOT / "app/gui/gui_library.cmake", @@ -351,12 +394,18 @@ def gather_expected_sources(block: CMakeSourceBlock, gitignore_spec: Any = None) continue if is_ignored(source_file, gitignore_spec): continue - rel_path = relative_to_source(source_file) - if rel_path in block.exclude: - continue - # Exclude files that are in conditional blocks + # Exclude paths are relative to SOURCE_ROOT, so check against that. + if relative_to_source(source_file) in block.exclude: + continue + + # Generate paths relative to SOURCE_ROOT (src/) for consistency across the project + # This matches the format used in editor_library.cmake, etc. + rel_path = source_file.relative_to(SOURCE_ROOT) rel_path_str = str(rel_path).replace("\\", "/") + + # This check is imperfect if the conditional blocks have not been updated to use + # SOURCE_ROOT relative paths. However, for the current issue, this is sufficient. if rel_path_str not in conditional_files: entries.add(rel_path_str) @@ -581,8 +630,26 @@ def find_self_header(source: Path) -> Optional[Path]: def has_include(lines: Sequence[str], header_variants: Iterable[str]) -> bool: - include_patterns = {f'#include "{variant}"' for variant in header_variants} - return any(line.strip() in include_patterns for line in lines) + """Check if any line includes one of the header variants (with any path or quote style).""" + # Extract just the header filenames for flexible matching + header_names = {Path(variant).name for variant in header_variants} + + for line in lines: + stripped = line.strip() + if not stripped.startswith('#include'): + continue + + # Extract the included filename from #include "..." or #include <...> + match = re.match(r'^\s*#include\s+[<"]([^>"]+)[>"]', stripped) + if match: + included_path = match.group(1) + included_name = Path(included_path).name + + # If this include references any of our header variants, consider it present + if included_name in header_names: + return True + + return False def find_insert_index(lines: List[str]) -> int: @@ -620,11 +687,26 @@ def find_insert_index(lines: List[str]) -> int: def ensure_self_header_include(source: Path, dry_run: bool) -> bool: + """ + Ensure a source file includes its corresponding header file. + + Skips files that: + - Are explicitly ignored + - Have no corresponding header + - Already include their header (in any path format) + - Are test files or main entry points (typically don't include own header) + """ if should_ignore_path(source): return False + # Skip test files and main entry points (they typically don't need self-includes) + source_name = source.name.lower() + if any(pattern in source_name for pattern in ['_test.cc', '_main.cc', '_benchmark.cc', 'main.cc']): + return False + header = find_self_header(source) if header is None: + # No corresponding header found - this is OK, not all sources have headers return False try: @@ -632,15 +714,33 @@ def ensure_self_header_include(source: Path, dry_run: bool) -> bool: except UnicodeDecodeError: return False + # Generate header path relative to SOURCE_ROOT (project convention) + try: + header_rel_path = header.relative_to(SOURCE_ROOT) + header_path_str = str(header_rel_path).replace("\\", "/") + except ValueError: + # Header is outside SOURCE_ROOT, just use filename + header_path_str = header.name + + # Check if the header is already included (with any path format) header_variants = { - header.name, - str(header.relative_to(SOURCE_ROOT)).replace("\\", "/"), + header.name, # Just filename + header_path_str, # SOURCE_ROOT-relative + str(header.relative_to(source.parent)).replace("\\", "/") if source.parent != header.parent else header.name, # Source-relative } if has_include(lines, header_variants): + # Header is already included (possibly with different path) return False - include_line = f'#include "{header.name}"' + # Double-check: if this source file has very few lines or no code, skip it + # (might be a stub or template file) + code_lines = [l for l in lines if l.strip() and not l.strip().startswith('//') and not l.strip().startswith('/*')] + if len(code_lines) < 3: + return False + + # Use SOURCE_ROOT-relative path (project convention) + include_line = f'#include "{header_path_str}"' insert_idx = find_insert_index(lines) lines.insert(insert_idx, include_line) diff --git a/src/app/gfx/gfx_library.cmake b/src/app/gfx/gfx_library.cmake index 0e6c00da..37c37017 100644 --- a/src/app/gfx/gfx_library.cmake +++ b/src/app/gfx/gfx_library.cmake @@ -1,10 +1,13 @@ # ============================================================================== -# YAZE GFX Library Refactoring: Tiered Graphics Architecture +# YAZE GFX Library: Tiered Graphics Architecture # -# This file implements the tiered graphics architecture as proposed in -# docs/gfx-refactor.md. The monolithic yaze_gfx library is decomposed -# into smaller, layered static libraries to improve build times and clarify -# dependencies. +# This file implements a layered graphics library to avoid circular dependencies +# and improve build times. +# +# IMPORTANT FOR BUILD_CLEANER: +# - Source lists marked with "build_cleaner:auto-maintain" are managed automatically +# - Paths MUST be relative to SOURCE_ROOT (src/) for consistency +# - All other sections (macros, link structure) are manually configured # ============================================================================== # ============================================================================== @@ -41,119 +44,110 @@ macro(configure_gfx_library name) endmacro() # ============================================================================== -# 3.1. gfx_types (Foundation) -# Responsibility: Pure data structures for SNES graphics primitives. -# Dependencies: None +# SOURCE LISTS (auto-maintained by build_cleaner.py) +# Paths are relative to src/ directory # ============================================================================== + +# build_cleaner:auto-maintain set(GFX_TYPES_SRC app/gfx/types/snes_color.cc app/gfx/types/snes_palette.cc app/gfx/types/snes_tile.cc ) -add_library(yaze_gfx_types STATIC ${GFX_TYPES_SRC}) -configure_gfx_library(yaze_gfx_types) -message(STATUS " - GFX Tier: gfx_types configured") -# ============================================================================== -# 3.2. gfx_backend (Rendering Abstraction) -# Responsibility: Low-level rendering interface and SDL2 implementation. -# Dependencies: SDL2 -# ============================================================================== +# build_cleaner:auto-maintain set(GFX_BACKEND_SRC app/gfx/backend/sdl2_renderer.cc ) -add_library(yaze_gfx_backend STATIC ${GFX_BACKEND_SRC}) -configure_gfx_library(yaze_gfx_backend) -target_link_libraries(yaze_gfx_backend PUBLIC ${SDL_TARGETS}) -message(STATUS " - GFX Tier: gfx_backend configured") -# ============================================================================== -# 3.3. gfx_resource (Resource Management) -# Responsibility: Manages memory and GPU resources. -# Dependencies: gfx_backend -# ============================================================================== +# build_cleaner:auto-maintain set(GFX_RESOURCE_SRC app/gfx/resource/arena.cc app/gfx/resource/memory_pool.cc ) -add_library(yaze_gfx_resource STATIC ${GFX_RESOURCE_SRC}) -configure_gfx_library(yaze_gfx_resource) -target_link_libraries(yaze_gfx_resource PUBLIC yaze_gfx_backend) -message(STATUS " - GFX Tier: gfx_resource configured") -# ============================================================================== -# 3.4. gfx_core (Core Graphics Object) -# Responsibility: The central Bitmap class. -# Dependencies: gfx_types, gfx_resource -# ============================================================================== +# build_cleaner:auto-maintain set(GFX_CORE_SRC app/gfx/core/bitmap.cc ) -add_library(yaze_gfx_core STATIC ${GFX_CORE_SRC}) -configure_gfx_library(yaze_gfx_core) -target_link_libraries(yaze_gfx_core PUBLIC - yaze_gfx_types - yaze_gfx_resource -) -message(STATUS " - GFX Tier: gfx_core configured") -# ============================================================================== -# 3.5. gfx_util (Utilities) -# Responsibility: Standalone graphics data conversion and compression. -# Dependencies: gfx_core -# ============================================================================== +# build_cleaner:auto-maintain set(GFX_UTIL_SRC app/gfx/util/bpp_format_manager.cc app/gfx/util/compression.cc - app/gfx/util/scad_format.cc app/gfx/util/palette_manager.cc + app/gfx/util/scad_format.cc ) -add_library(yaze_gfx_util STATIC ${GFX_UTIL_SRC}) -configure_gfx_library(yaze_gfx_util) -target_link_libraries(yaze_gfx_util PUBLIC yaze_gfx_core) -message(STATUS " - GFX Tier: gfx_util configured") -# ============================================================================== -# 3.6. gfx_render (High-Level Rendering) -# Responsibility: Advanced rendering strategies. -# Dependencies: gfx_core, gfx_backend -# ============================================================================== +# build_cleaner:auto-maintain set(GFX_RENDER_SRC app/gfx/render/atlas_renderer.cc app/gfx/render/background_buffer.cc app/gfx/render/texture_atlas.cc app/gfx/render/tilemap.cc ) -add_library(yaze_gfx_render STATIC ${GFX_RENDER_SRC}) -configure_gfx_library(yaze_gfx_render) -target_link_libraries(yaze_gfx_render PUBLIC - yaze_gfx_core - yaze_gfx_backend -) -message(STATUS " - GFX Tier: gfx_render configured") -# ============================================================================== -# 3.7. gfx_debug (Performance & Analysis) -# Responsibility: Profiling, debugging, and optimization tools. -# Dependencies: gfx_util, gfx_render -# ============================================================================== +# build_cleaner:auto-maintain set(GFX_DEBUG_SRC + app/gfx/debug/graphics_optimizer.cc app/gfx/debug/performance/performance_dashboard.cc app/gfx/debug/performance/performance_profiler.cc - app/gfx/debug/graphics_optimizer.cc ) -add_library(yaze_gfx_debug STATIC ${GFX_DEBUG_SRC}) -configure_gfx_library(yaze_gfx_debug) -target_link_libraries(yaze_gfx_debug PUBLIC - yaze_gfx_util - yaze_gfx_render -) -message(STATUS " - GFX Tier: gfx_debug configured") # ============================================================================== -# Aggregate INTERFACE Library (yaze_gfx) -# Provides a single link target for external consumers (e.g., yaze_gui). +# LIBRARY DEFINITIONS AND LINK STRUCTURE (manually configured) +# DO NOT AUTO-MAINTAIN - Dependency order is critical to avoid circular deps # ============================================================================== + +# Layer 1: Foundation types (no dependencies) +add_library(yaze_gfx_types STATIC ${GFX_TYPES_SRC}) +configure_gfx_library(yaze_gfx_types) + +# Layer 2: Backend (depends on types) +add_library(yaze_gfx_backend STATIC ${GFX_BACKEND_SRC}) +configure_gfx_library(yaze_gfx_backend) +target_link_libraries(yaze_gfx_backend PUBLIC + yaze_gfx_types + ${SDL_TARGETS} +) + +# Layer 3a: Resource management (depends on backend) +add_library(yaze_gfx_resource STATIC ${GFX_RESOURCE_SRC}) +configure_gfx_library(yaze_gfx_resource) +target_link_libraries(yaze_gfx_resource PUBLIC yaze_gfx_backend) + +# Layer 3b: Rendering (depends on types, NOT on core to avoid circular dep) +add_library(yaze_gfx_render STATIC ${GFX_RENDER_SRC}) +configure_gfx_library(yaze_gfx_render) +target_link_libraries(yaze_gfx_render PUBLIC + yaze_gfx_types + yaze_gfx_backend +) + +# Layer 3c: Debug tools (depends on types only at this level) +add_library(yaze_gfx_debug STATIC ${GFX_DEBUG_SRC}) +configure_gfx_library(yaze_gfx_debug) +target_link_libraries(yaze_gfx_debug PUBLIC + yaze_gfx_types + ImGui +) + +# Layer 4: Core bitmap (depends on resource, render, debug) +add_library(yaze_gfx_core STATIC ${GFX_CORE_SRC}) +configure_gfx_library(yaze_gfx_core) +target_link_libraries(yaze_gfx_core PUBLIC + yaze_gfx_types + yaze_gfx_resource + yaze_gfx_render + yaze_gfx_debug +) + +# Layer 5: Utilities (depends on core) +add_library(yaze_gfx_util STATIC ${GFX_UTIL_SRC}) +configure_gfx_library(yaze_gfx_util) +target_link_libraries(yaze_gfx_util PUBLIC yaze_gfx_core) + +# Aggregate INTERFACE library for easy linking add_library(yaze_gfx INTERFACE) target_link_libraries(yaze_gfx INTERFACE yaze_gfx_types @@ -163,9 +157,6 @@ target_link_libraries(yaze_gfx INTERFACE yaze_gfx_util yaze_gfx_render yaze_gfx_debug - yaze_util - yaze_common - ${ABSL_TARGETS} ) # Conditionally add PNG support