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.
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)

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
# 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