refactor(build): enhance build_cleaner.py for auto-discovery of CMake libraries

- Updated the discover_cmake_libraries function to support new marker comments for auto-maintenance.
- Improved variable extraction logic to handle decomposed libraries and subdirectory detection.
- Removed hardcoded CMake source blocks in favor of auto-discovery, streamlining the management of graphics library sources.

Benefits:
- Simplifies the maintenance of CMake files by automating source list updates.
- Enhances build efficiency and clarity by reducing manual configuration requirements.
This commit is contained in:
scawful
2025-10-13 20:09:48 -04:00
parent 85e6fb5a83
commit 99424fa2b2
2 changed files with 244 additions and 153 deletions

244
scripts/build_cleaner.py Normal file → Executable file
View File

@@ -93,13 +93,19 @@ def discover_cmake_libraries() -> List[CMakeSourceBlock]:
""" """
Auto-discover CMake library files that explicitly opt-in to auto-maintenance. 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" Looks for marker comments like:
or "# AUTO-MAINTAINED" to identify variables that should be auto-updated. - "# build_cleaner:auto-maintain"
""" - "# auto-maintained by build_cleaner.py"
blocks = [] - "# AUTO-MAINTAINED"
seen_vars: Set[str] = set()
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"): for cmake_file in SOURCE_ROOT.rglob("*.cmake"):
if 'lib/' in str(cmake_file) or 'third_party/' in str(cmake_file): if 'lib/' in str(cmake_file) or 'third_party/' in str(cmake_file):
continue continue
@@ -108,41 +114,78 @@ def discover_cmake_libraries() -> List[CMakeSourceBlock]:
content = cmake_file.read_text(encoding='utf-8') content = cmake_file.read_text(encoding='utf-8')
lines = content.splitlines() lines = content.splitlines()
# Look for source variable definitions with auto-maintain markers
for i, line in enumerate(lines): for i, line in enumerate(lines):
# Check if previous lines indicate auto-maintenance # Check if previous lines indicate auto-maintenance
auto_maintained = False auto_maintained = False
for j in range(max(0, i-3), i): for j in range(max(0, i-5), i):
if 'auto-maintain' in lines[j].lower() or 'auto_maintain' in lines[j].lower(): 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 auto_maintained = True
break break
if not auto_maintained: if not auto_maintained:
continue continue
# Extract variable name from set() statement # Extract variable name (allow for line breaks or closing paren)
match = re.search(r'set\s*\(\s*(\w+(?:_SRC|_SOURCES|_SOURCE))\s', line) match = re.search(r'set\s*\(\s*(\w+(?:_SRC|_SOURCES|_SOURCE))(?:\s|$|\))', line)
if match: if match:
var_name = match.group(1) var_name = match.group(1)
if cmake_file not in file_variables:
# Skip if we've already seen this variable file_variables[cmake_file] = []
if var_name in seen_vars: if var_name not in file_variables[cmake_file]:
continue file_variables[cmake_file].append(var_name)
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),),
))
except Exception as e: except Exception as e:
print(f"Warning: Could not process {cmake_file}: {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 return blocks
@@ -180,42 +223,42 @@ STATIC_CONFIG: Sequence[CMakeSourceBlock] = (
cmake_path=SOURCE_ROOT / "util/util.cmake", cmake_path=SOURCE_ROOT / "util/util.cmake",
directories=(DirectorySpec(SOURCE_ROOT / "util"),), directories=(DirectorySpec(SOURCE_ROOT / "util"),),
), ),
CMakeSourceBlock( # Note: These are commented out in favor of auto-discovery via markers
variable="GFX_TYPES_SRC", # CMakeSourceBlock(
cmake_path=SOURCE_ROOT / "app/gfx/gfx_library.cmake", # variable="GFX_TYPES_SRC",
directories=(DirectorySpec(SOURCE_ROOT / "app/gfx/types"),), # cmake_path=SOURCE_ROOT / "app/gfx/gfx_library.cmake",
), # directories=(DirectorySpec(SOURCE_ROOT / "app/gfx/types"),),
CMakeSourceBlock( # ),
variable="GFX_BACKEND_SRC", # CMakeSourceBlock(
cmake_path=SOURCE_ROOT / "app/gfx/gfx_library.cmake", # variable="GFX_BACKEND_SRC",
directories=(DirectorySpec(SOURCE_ROOT / "app/gfx/backend"),), # cmake_path=SOURCE_ROOT / "app/gfx/gfx_library.cmake",
), # directories=(DirectorySpec(SOURCE_ROOT / "app/gfx/backend"),),
CMakeSourceBlock( # ),
variable="GFX_RESOURCE_SRC", # CMakeSourceBlock(
cmake_path=SOURCE_ROOT / "app/gfx/gfx_library.cmake", # variable="GFX_RESOURCE_SRC",
directories=(DirectorySpec(SOURCE_ROOT / "app/gfx/resource"),), # cmake_path=SOURCE_ROOT / "app/gfx/gfx_library.cmake",
exclude={Path("app/gfx/render/background_buffer.cc")}, # This is in resource but used by render # directories=(DirectorySpec(SOURCE_ROOT / "app/gfx/resource"),),
), # ),
CMakeSourceBlock( # CMakeSourceBlock(
variable="GFX_CORE_SRC", # variable="GFX_CORE_SRC",
cmake_path=SOURCE_ROOT / "app/gfx/gfx_library.cmake", # cmake_path=SOURCE_ROOT / "app/gfx/gfx_library.cmake",
directories=(DirectorySpec(SOURCE_ROOT / "app/gfx/core"),), # directories=(DirectorySpec(SOURCE_ROOT / "app/gfx/core"),),
), # ),
CMakeSourceBlock( # CMakeSourceBlock(
variable="GFX_UTIL_SRC", # variable="GFX_UTIL_SRC",
cmake_path=SOURCE_ROOT / "app/gfx/gfx_library.cmake", # cmake_path=SOURCE_ROOT / "app/gfx/gfx_library.cmake",
directories=(DirectorySpec(SOURCE_ROOT / "app/gfx/util"),), # directories=(DirectorySpec(SOURCE_ROOT / "app/gfx/util"),),
), # ),
CMakeSourceBlock( # CMakeSourceBlock(
variable="GFX_RENDER_SRC", # variable="GFX_RENDER_SRC",
cmake_path=SOURCE_ROOT / "app/gfx/gfx_library.cmake", # cmake_path=SOURCE_ROOT / "app/gfx/gfx_library.cmake",
directories=(DirectorySpec(SOURCE_ROOT / "app/gfx/render"),), # directories=(DirectorySpec(SOURCE_ROOT / "app/gfx/render"),),
), # ),
CMakeSourceBlock( # CMakeSourceBlock(
variable="GFX_DEBUG_SRC", # variable="GFX_DEBUG_SRC",
cmake_path=SOURCE_ROOT / "app/gfx/gfx_library.cmake", # cmake_path=SOURCE_ROOT / "app/gfx/gfx_library.cmake",
directories=(DirectorySpec(SOURCE_ROOT / "app/gfx/debug"),), # directories=(DirectorySpec(SOURCE_ROOT / "app/gfx/debug"),),
), # ),
CMakeSourceBlock( CMakeSourceBlock(
variable="GUI_CORE_SRC", variable="GUI_CORE_SRC",
cmake_path=SOURCE_ROOT / "app/gui/gui_library.cmake", cmake_path=SOURCE_ROOT / "app/gui/gui_library.cmake",
@@ -351,12 +394,18 @@ def gather_expected_sources(block: CMakeSourceBlock, gitignore_spec: Any = None)
continue continue
if is_ignored(source_file, gitignore_spec): if is_ignored(source_file, gitignore_spec):
continue 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("\\", "/") 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: if rel_path_str not in conditional_files:
entries.add(rel_path_str) 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: def has_include(lines: Sequence[str], header_variants: Iterable[str]) -> bool:
include_patterns = {f'#include "{variant}"' for variant in header_variants} """Check if any line includes one of the header variants (with any path or quote style)."""
return any(line.strip() in include_patterns for line in lines) # 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: 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: 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): if should_ignore_path(source):
return False 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) header = find_self_header(source)
if header is None: if header is None:
# No corresponding header found - this is OK, not all sources have headers
return False return False
try: try:
@@ -632,15 +714,33 @@ def ensure_self_header_include(source: Path, dry_run: bool) -> bool:
except UnicodeDecodeError: except UnicodeDecodeError:
return False 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_variants = {
header.name, header.name, # Just filename
str(header.relative_to(SOURCE_ROOT)).replace("\\", "/"), 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): if has_include(lines, header_variants):
# Header is already included (possibly with different path)
return False 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) insert_idx = find_insert_index(lines)
lines.insert(insert_idx, include_line) lines.insert(insert_idx, include_line)

View File

@@ -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 # This file implements a layered graphics library to avoid circular dependencies
# docs/gfx-refactor.md. The monolithic yaze_gfx library is decomposed # and improve build times.
# into smaller, layered static libraries to improve build times and clarify #
# dependencies. # 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() endmacro()
# ============================================================================== # ==============================================================================
# 3.1. gfx_types (Foundation) # SOURCE LISTS (auto-maintained by build_cleaner.py)
# Responsibility: Pure data structures for SNES graphics primitives. # Paths are relative to src/ directory
# Dependencies: None
# ============================================================================== # ==============================================================================
# build_cleaner:auto-maintain
set(GFX_TYPES_SRC set(GFX_TYPES_SRC
app/gfx/types/snes_color.cc app/gfx/types/snes_color.cc
app/gfx/types/snes_palette.cc app/gfx/types/snes_palette.cc
app/gfx/types/snes_tile.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")
# ============================================================================== # build_cleaner:auto-maintain
# 3.2. gfx_backend (Rendering Abstraction)
# Responsibility: Low-level rendering interface and SDL2 implementation.
# Dependencies: SDL2
# ==============================================================================
set(GFX_BACKEND_SRC set(GFX_BACKEND_SRC
app/gfx/backend/sdl2_renderer.cc 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")
# ============================================================================== # build_cleaner:auto-maintain
# 3.3. gfx_resource (Resource Management)
# Responsibility: Manages memory and GPU resources.
# Dependencies: gfx_backend
# ==============================================================================
set(GFX_RESOURCE_SRC set(GFX_RESOURCE_SRC
app/gfx/resource/arena.cc app/gfx/resource/arena.cc
app/gfx/resource/memory_pool.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")
# ============================================================================== # build_cleaner:auto-maintain
# 3.4. gfx_core (Core Graphics Object)
# Responsibility: The central Bitmap class.
# Dependencies: gfx_types, gfx_resource
# ==============================================================================
set(GFX_CORE_SRC set(GFX_CORE_SRC
app/gfx/core/bitmap.cc 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")
# ============================================================================== # build_cleaner:auto-maintain
# 3.5. gfx_util (Utilities)
# Responsibility: Standalone graphics data conversion and compression.
# Dependencies: gfx_core
# ==============================================================================
set(GFX_UTIL_SRC set(GFX_UTIL_SRC
app/gfx/util/bpp_format_manager.cc app/gfx/util/bpp_format_manager.cc
app/gfx/util/compression.cc app/gfx/util/compression.cc
app/gfx/util/scad_format.cc
app/gfx/util/palette_manager.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")
# ============================================================================== # build_cleaner:auto-maintain
# 3.6. gfx_render (High-Level Rendering)
# Responsibility: Advanced rendering strategies.
# Dependencies: gfx_core, gfx_backend
# ==============================================================================
set(GFX_RENDER_SRC set(GFX_RENDER_SRC
app/gfx/render/atlas_renderer.cc app/gfx/render/atlas_renderer.cc
app/gfx/render/background_buffer.cc app/gfx/render/background_buffer.cc
app/gfx/render/texture_atlas.cc app/gfx/render/texture_atlas.cc
app/gfx/render/tilemap.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")
# ============================================================================== # build_cleaner:auto-maintain
# 3.7. gfx_debug (Performance & Analysis)
# Responsibility: Profiling, debugging, and optimization tools.
# Dependencies: gfx_util, gfx_render
# ==============================================================================
set(GFX_DEBUG_SRC set(GFX_DEBUG_SRC
app/gfx/debug/graphics_optimizer.cc
app/gfx/debug/performance/performance_dashboard.cc app/gfx/debug/performance/performance_dashboard.cc
app/gfx/debug/performance/performance_profiler.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) # LIBRARY DEFINITIONS AND LINK STRUCTURE (manually configured)
# Provides a single link target for external consumers (e.g., yaze_gui). # 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) add_library(yaze_gfx INTERFACE)
target_link_libraries(yaze_gfx INTERFACE target_link_libraries(yaze_gfx INTERFACE
yaze_gfx_types yaze_gfx_types
@@ -163,9 +157,6 @@ target_link_libraries(yaze_gfx INTERFACE
yaze_gfx_util yaze_gfx_util
yaze_gfx_render yaze_gfx_render
yaze_gfx_debug yaze_gfx_debug
yaze_util
yaze_common
${ABSL_TARGETS}
) )
# Conditionally add PNG support # Conditionally add PNG support