Files
klubhaus-doorbell/libraries/FastLED/ci/compiler/test_example_compilation.py
2026-02-12 00:45:31 -08:00

2596 lines
98 KiB
Python

#!/usr/bin/env python3
"""
FastLED Example Compilation Testing Script
Ultra-fast compilation testing of Arduino .ino examples
ENHANCED with Simple Build System Integration:
- Uses proven Compiler class infrastructure for actual compilation
- Preserves all existing functionality (system info, timing, error handling)
- Adds informative stubs for complex features
- Maintains compatibility with existing command-line interface
"""
import argparse
import concurrent.futures
import hashlib
import os
import platform
import shutil
import stat
import subprocess
import sys
import tempfile
import time
import tomllib
from concurrent.futures import Future, as_completed
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Callable, Dict, List, Optional, Union
import psutil
import toml # type: ignore
_IS_GITHUB_ACTIONS = os.getenv("GITHUB_ACTIONS") == "true"
_TIMEOUT = 600 if _IS_GITHUB_ACTIONS else 120
# Add the parent directory to Python path for imports
# Import the proven Compiler infrastructure
from ci.compiler.clang_compiler import (
BuildFlags,
Compiler,
CompilerOptions,
LinkOptions,
Result,
)
from ci.util.running_process import EndOfStream, RunningProcess
# Abort threshold to prevent flooding logs with repeated failures
MAX_FAILURES_BEFORE_ABORT = 3
# Color output functions using ANSI escape codes
def red_text(text: str) -> str:
"""Return text in red color."""
return f"\033[31m{text}\033[0m"
def green_text(text: str) -> str:
"""Return text in green color."""
return f"\033[32m{text}\033[0m"
def orange_text(text: str) -> str:
"""Return text in orange color."""
return f"\033[33m{text}\033[0m"
@dataclass
class CompilationResult:
"""Results from compiling a set of examples."""
successful_count: int
failed_count: int
compile_time: float
failed_examples: List[Dict[str, Any]]
object_file_map: Optional[Dict[Path, List[Path]]]
@dataclass
class LinkingResult:
"""Results from linking examples into executable programs."""
linked_count: int
failed_count: int
cache_hits: int = 0
cache_misses: int = 0
@property
def total_count(self) -> int:
"""Total number of examples processed (linked + failed)"""
return self.linked_count + self.failed_count
@property
def cache_hit_rate(self) -> float:
"""Cache hit rate as percentage"""
total_processed = self.cache_hits + self.cache_misses
return (self.cache_hits / total_processed * 100) if total_processed > 0 else 0.0
@dataclass
class ExecutionFailure:
"""Represents a failed example execution with identifying info and reason."""
name: str
reason: str
stdout: str
def load_build_flags_toml(toml_path: str) -> Dict[str, Any]:
"""Load and parse build_example.toml file."""
try:
with open(toml_path, "rb") as f:
config = tomllib.load(f)
if not config:
raise RuntimeError(
f"build_example.toml at {toml_path} is empty or invalid"
)
return config
except FileNotFoundError:
raise RuntimeError(
f"CRITICAL: build_example.toml not found at {toml_path}. This file is required for proper compilation flags."
)
except Exception as e:
raise RuntimeError(
f"CRITICAL: Failed to parse build_example.toml at {toml_path}: {e}"
)
def extract_compiler_flags_from_toml(config: Dict[str, Any]) -> List[str]:
"""Extract compiler flags from TOML config - includes universal [all] flags and [build_modes.quick] flags."""
flags: List[str] = []
# First, extract universal compiler flags from [all] section
if "all" in config and isinstance(config["all"], dict):
all_section: Dict[str, Any] = config["all"] # type: ignore
if "compiler_flags" in all_section and isinstance(
all_section["compiler_flags"], list
):
compiler_flags: List[str] = all_section["compiler_flags"] # type: ignore
flags.extend(compiler_flags)
print(
f"[CONFIG] Loaded {len(compiler_flags)} universal compiler flags from [all] section"
)
# Then, extract flags from [build_modes.quick] section for sketch compilation
if "build_modes" in config and isinstance(config["build_modes"], dict):
build_modes: Dict[str, Any] = config["build_modes"] # type: ignore
if "quick" in build_modes and isinstance(build_modes["quick"], dict):
quick_section: Dict[str, Any] = build_modes["quick"] # type: ignore
if "flags" in quick_section and isinstance(quick_section["flags"], list):
quick_flags: List[str] = quick_section["flags"] # type: ignore
flags.extend(quick_flags)
print(
f"[CONFIG] Loaded {len(quick_flags)} quick mode flags from [build_modes.quick] section"
)
return flags
def extract_stub_platform_defines_from_toml(config: Dict[str, Any]) -> List[str]:
"""Extract stub platform defines from TOML config - uses [stub_platform] section."""
defines: List[str] = []
# Extract defines from [stub_platform] section
if "stub_platform" in config and isinstance(config["stub_platform"], dict):
stub_section: Dict[str, Any] = config["stub_platform"] # type: ignore
if "defines" in stub_section and isinstance(stub_section["defines"], list):
stub_defines: List[str] = stub_section["defines"] # type: ignore
defines.extend(stub_defines)
print(
f"[CONFIG] Loaded {len(stub_defines)} stub platform defines from [stub_platform] section"
)
else:
raise RuntimeError(
"CRITICAL: No 'defines' list found in [stub_platform] section. "
"This is required for stub platform compilation."
)
else:
raise RuntimeError(
"CRITICAL: No [stub_platform] section found in build_example.toml. "
"This section is MANDATORY for stub platform compilation."
)
# Validate that all required defines are present
required_defines = [
"STUB_PLATFORM",
"ARDUINO=", # Partial match for version number
"FASTLED_USE_STUB_ARDUINO",
"FASTLED_STUB_IMPL",
]
for required in required_defines:
if not any(define.startswith(required) for define in defines):
raise RuntimeError(
f"CRITICAL: Required stub platform define '{required}' not found in configuration. "
f"Please ensure [stub_platform] section contains all required defines."
)
return defines
def extract_stub_platform_include_paths_from_toml(config: Dict[str, Any]) -> List[str]:
"""Extract stub platform include paths from TOML config - uses [stub_platform] section."""
include_paths: List[str] = []
# Extract include paths from [stub_platform] section
if "stub_platform" in config and isinstance(config["stub_platform"], dict):
stub_section: Dict[str, Any] = config["stub_platform"] # type: ignore
if "include_paths" in stub_section and isinstance(
stub_section["include_paths"], list
):
stub_include_paths: List[str] = stub_section["include_paths"] # type: ignore
include_paths.extend(stub_include_paths)
print(
f"[CONFIG] Loaded {len(stub_include_paths)} stub platform include paths from [stub_platform] section"
)
else:
raise RuntimeError(
"CRITICAL: No 'include_paths' list found in [stub_platform] section. "
"This is required for stub platform compilation."
)
else:
raise RuntimeError(
"CRITICAL: No [stub_platform] section found in build_example.toml. "
"This section is MANDATORY for stub platform compilation."
)
return include_paths
def get_system_info() -> Dict[str, Union[str, int, float]]:
"""Get basic system configuration information.
OPTIMIZED: Removed expensive subprocess calls for compiler detection
since we know the build system uses Zig/Clang after migration.
"""
try:
# Get CPU information
cpu_count = os.cpu_count() or 1
# Get memory information
memory = psutil.virtual_memory()
memory_gb = memory.total / (1024**3)
# Get OS information
os_name = platform.system()
os_version = platform.release()
# Use known compiler info since we're post-migration to Zig/Clang
# No expensive subprocess calls needed
compiler_info = "Zig/Clang (known)"
return {
"os": f"{os_name} {os_version}",
"compiler": compiler_info,
"cpu_cores": cpu_count,
"memory_gb": memory_gb,
}
except Exception as e:
return {
"os": f"{platform.system()} {platform.release()}",
"compiler": "Zig/Clang (fallback)",
"cpu_cores": os.cpu_count() or 1,
"memory_gb": 8.0, # Fallback estimate
}
def get_build_configuration() -> Dict[str, Union[bool, str]]:
"""Get build configuration information."""
config: Dict[str, Union[bool, str]] = {}
# Check unified compilation (note: simple build system uses direct compilation)
config["unified_compilation"] = False # Simple build system uses direct compilation
# Compiler cache disabled
config["cache_type"] = "none"
# Build mode for simple build system
config["build_mode"] = "simple_direct"
return config
def format_file_size(size_bytes: int) -> str:
"""Format file size in human readable format."""
if size_bytes == 0:
return "0B"
units = ["B", "KB", "MB", "GB"]
size = float(size_bytes)
unit_index = 0
while size >= 1024 and unit_index < len(units) - 1:
size /= 1024
unit_index += 1
if unit_index == 0:
return f"{int(size)}{units[unit_index]}"
else:
return f"{size:.1f}{units[unit_index]}"
# Stub functions for complex features (with informative messages)
def generate_pch_header(source_filename: str) -> None:
"""Stub: PCH generation not needed in simple build system."""
print(
f"[INFO] PCH header generation for {source_filename} not implemented - using direct compilation"
)
def detect_build_cache() -> bool:
"""Stub: Build cache detection not implemented."""
print("[INFO] Build cache detection not implemented - performing fresh compilation")
return False
def check_incremental_build() -> bool:
"""Stub: Incremental builds not implemented."""
print(
"[INFO] Incremental build detection not implemented - performing full compilation"
)
return False
def optimize_parallel_jobs() -> int:
"""Stub: Manual parallel job optimization not needed."""
print(
"[INFO] Parallel job optimization handled automatically by ThreadPoolExecutor"
)
cpu_count = os.cpu_count() or 1
return cpu_count * 2 # Return sensible default
def check_pch_status(build_dir: Path) -> Dict[str, Union[bool, Path, int, str]]:
"""Check PCH file status - stub for simple build system."""
# Simple build system doesn't use PCH, but we maintain the interface
print("[INFO] PCH status check - simple build system uses direct compilation")
return {"exists": False, "path": None, "size": 0, "size_formatted": "0B"} # type: ignore
def create_fastled_compiler(use_pch: bool, parallel: bool) -> Compiler:
"""Create compiler with standard FastLED settings for simple build system."""
import os
import tempfile
# Get absolute paths to ensure they work from any working directory
current_dir = os.getcwd()
src_path = os.path.join(current_dir, "src")
arduino_stub_path = os.path.join(current_dir, "src", "platforms", "stub")
# Load build_example.toml configuration directly from ci/ directory
toml_path = os.path.join(
os.path.dirname(os.path.dirname(__file__)), "build_example.toml"
)
build_config = load_build_flags_toml(
toml_path
) # Will raise RuntimeError if not found
# Extract additional compiler flags from TOML (using ci/build_example.toml directly)
toml_flags = extract_compiler_flags_from_toml(
build_config
) # Will raise RuntimeError if critical flags missing
print(f"Loaded {len(toml_flags)} total compiler flags from build_example.toml")
# Extract stub platform defines from TOML configuration
stub_defines = extract_stub_platform_defines_from_toml(
build_config
) # Will raise RuntimeError if critical defines missing
print(f"Loaded {len(stub_defines)} stub platform defines from build_example.toml")
# Extract stub platform include paths from TOML configuration
stub_include_paths = extract_stub_platform_include_paths_from_toml(
build_config
) # Will raise RuntimeError if critical paths missing
print(
f"Loaded {len(stub_include_paths)} stub platform include paths from build_example.toml"
)
# Base compiler settings - convert relative paths to absolute
base_args: list[str] = []
for include_path in stub_include_paths:
if os.path.isabs(include_path):
base_args.append(f"-I{include_path}")
else:
# Convert relative path to absolute from project root
abs_path = os.path.join(current_dir, include_path)
base_args.append(f"-I{abs_path}")
print(f"[CONFIG] Added {len(base_args)} include paths from configuration")
# Combine base args with TOML flags
all_args: List[str] = base_args + toml_flags
# PCH output path in build cache directory (persistent)
pch_output_path = None
if use_pch:
cache_dir = Path(".build") / "cache"
cache_dir.mkdir(parents=True, exist_ok=True)
pch_output_path = str(cache_dir / "fastled_pch.hpp.pch")
# Determine compiler command (with or without cache)
compiler_cmd = "python -m ziglang c++"
cache_args: List[str] = []
print("Using direct compilation with ziglang c++")
# Combine cache args with other args (cache args go first)
final_args: List[str] = cache_args + all_args
settings = CompilerOptions(
include_path=src_path,
defines=stub_defines, # Use configuration-based defines instead of hardcoded ones
std_version="c++14",
compiler=compiler_cmd,
compiler_args=final_args,
use_pch=use_pch,
pch_output_path=pch_output_path,
parallel=parallel,
)
# Load build flags from TOML
current_dir_path = Path(current_dir)
build_flags_path = current_dir_path / "ci" / "build_example.toml"
build_flags = BuildFlags.parse(
build_flags_path, quick_build=False, strict_mode=False
)
return Compiler(settings, build_flags)
def compile_examples_simple(
compiler: Compiler,
ino_files: List[Path],
pch_compatible_files: set[Path],
log_timing: Callable[[str], None],
full_compilation: bool,
verbose: bool = False,
) -> CompilationResult:
"""
Compile examples using the simple build system (Compiler class).
Args:
compiler: The Compiler instance to use
ino_files: List of .ino files to compile
pch_compatible_files: Set of files that are PCH compatible
log_timing: Logging function
full_compilation: If True, preserve object files for linking; if False, use temp files
Returns:
CompilationResult: Results from compilation including counts and failed examples
"""
compile_start = time.time()
results: List[Dict[str, Any]] = []
# Create build directory structure if full compilation is enabled
build_dir: Optional[Path] = None
if full_compilation:
build_dir = Path(".build/examples")
build_dir.mkdir(parents=True, exist_ok=True)
log_timing(f"[BUILD] Created build directory: {build_dir}")
# Submit all compilation jobs with file tracking
future_to_file: Dict[Future[Result], Path] = {}
# Track object files for linking (if full_compilation enabled)
object_file_map: Dict[Path, List[Path]] = {} # ino_file -> [obj_files]
pch_files_count = 0
direct_files_count = 0
total_cpp_files = 0
for ino_file in ino_files:
# Find additional .cpp files in the same directory
cpp_files = compiler.find_cpp_files_for_example(ino_file)
if cpp_files:
total_cpp_files += len(cpp_files)
log_timing(
f"[SIMPLE] Found {len(cpp_files)} .cpp file(s) for {ino_file.name}: {[f.name for f in cpp_files]}"
)
# Find include directories for this example
include_dirs = compiler.find_include_dirs_for_example(ino_file)
if len(include_dirs) > 1: # More than just the example root directory
log_timing(
f"[SIMPLE] Found {len(include_dirs)} include dir(s) for {ino_file.name}: {[Path(d).name for d in include_dirs]}"
)
# Build additional flags with include directories
additional_flags: list[str] = []
for include_dir in include_dirs:
additional_flags.append(f"-I{include_dir}")
# Determine if this file should use PCH
use_pch_for_file = ino_file in pch_compatible_files
# Set up object file paths for full compilation
ino_output_path: Optional[Path] = None
example_obj_files: List[Path] = []
if full_compilation and build_dir:
# Create example-specific build directory
example_name = ino_file.parent.name
example_build_dir = build_dir / example_name
example_build_dir.mkdir(exist_ok=True)
# Set specific output path for .ino file
ino_output_path = example_build_dir / f"{ino_file.stem}.o"
example_obj_files.append(ino_output_path)
# Compile the .ino file
if verbose:
pch_status = "with PCH" if use_pch_for_file else "direct compilation"
print(
f"[VERBOSE] Compiling {ino_file.relative_to(Path('examples'))} ({pch_status})"
)
log_timing(
f"[VERBOSE] Compiling {ino_file.relative_to(Path('examples'))} ({pch_status})"
)
future = compiler.compile_ino_file(
ino_file,
output_path=ino_output_path,
use_pch_for_this_file=use_pch_for_file,
additional_flags=additional_flags,
)
future_to_file[future] = ino_file
if use_pch_for_file:
pch_files_count += 1
else:
direct_files_count += 1
# Compile additional .cpp files in the same directory
for cpp_file in cpp_files:
cpp_output_path: Optional[Path] = None
if full_compilation and build_dir:
example_name = ino_file.parent.name
example_build_dir = build_dir / example_name
cpp_output_path = example_build_dir / f"{cpp_file.stem}.o"
example_obj_files.append(cpp_output_path)
if verbose:
pch_status = "with PCH" if use_pch_for_file else "direct compilation"
print(
f"[VERBOSE] Compiling {cpp_file.relative_to(Path('examples'))} ({pch_status})"
)
log_timing(
f"[VERBOSE] Compiling {cpp_file.relative_to(Path('examples'))} ({pch_status})"
)
cpp_future = compiler.compile_cpp_file(
cpp_file,
output_path=cpp_output_path,
use_pch_for_this_file=use_pch_for_file,
additional_flags=additional_flags,
)
future_to_file[cpp_future] = cpp_file
if use_pch_for_file:
pch_files_count += 1
else:
direct_files_count += 1
# Track object files for this example (for linking)
if full_compilation and example_obj_files:
object_file_map[ino_file] = example_obj_files
total_files = len(ino_files) + total_cpp_files
log_timing(
f"[SIMPLE] Submitted {total_files} compilation jobs to ThreadPoolExecutor ({len(ino_files)} .ino + {total_cpp_files} .cpp)"
)
log_timing(
f"[SIMPLE] Using PCH for {pch_files_count} files, direct compilation for {direct_files_count} files"
)
# Collect results as they complete with timeout to prevent hanging
completed_count = 0
failure_count = 0
for future in as_completed(
future_to_file.keys(), timeout=_TIMEOUT
): # 2 minute total timeout
try:
result: Result = future.result(timeout=30) # 30 second timeout per file
source_file: Path = future_to_file[future]
completed_count += 1
except Exception as e:
source_file: Path = future_to_file[future]
completed_count += 1
print(f"ERROR: Compilation timed out or failed for {source_file}: {e}")
result = Result(
ok=False, stdout="", stderr=f"Compilation timeout: {e}", return_code=-1
)
# Show verbose completion status or only final completion
if verbose:
status = "SUCCESS" if result.ok else "FAILED"
log_timing(
f"[VERBOSE] {status}: {source_file.relative_to(Path('examples'))} ({completed_count}/{total_files})"
)
elif completed_count == total_files:
log_timing(
f"[SIMPLE] Completed {completed_count}/{total_files} compilations"
)
# Show compilation errors immediately for better debugging
if not result.ok and result.stderr.strip():
failure_count += 1
log_timing(
f"[ERROR] Compilation failed for {source_file.relative_to(Path('examples'))}:"
)
error_lines = result.stderr.strip().split("\n")
for line in error_lines[:20]: # Limit to first 20 lines per file
log_timing(f"[ERROR] {line}")
if len(error_lines) > 20:
log_timing(f"[ERROR] ... ({len(error_lines) - 20} more lines)")
if failure_count >= MAX_FAILURES_BEFORE_ABORT:
log_timing(
f"[ERROR] Reached failure threshold ({MAX_FAILURES_BEFORE_ABORT}). Aborting remaining compilation jobs to avoid log spam."
)
break
file_result: Dict[str, Any] = {
"file": str(source_file.name),
"path": str(source_file.relative_to(Path("examples"))),
"success": bool(result.ok),
"stderr": str(result.stderr),
}
results.append(file_result)
compile_time = time.time() - compile_start
# Analyze results
successful = [r for r in results if r["success"]]
failed = [r for r in results if not r["success"]]
log_timing(
f"[SIMPLE] Compilation completed: {len(successful)} succeeded, {len(failed)} failed"
)
# Report failures summary if any (detailed errors already shown above)
if failed:
if verbose:
log_timing(
f"[SIMPLE] Failed examples summary: {[f['path'] for f in failed]}"
)
else:
# Show full details only in non-verbose mode since we didn't show them above
if len(failed) <= 10:
log_timing("[SIMPLE] Failed examples with full error details:")
for failure in failed[:10]:
log_timing(f"[SIMPLE] === FAILED: {failure['path']} ===")
if failure["stderr"]:
# Show full error message, not just preview
error_lines = failure["stderr"].strip().split("\n")
for line in error_lines:
log_timing(f"[SIMPLE] {line}")
else:
log_timing(f"[SIMPLE] No error details available")
log_timing(f"[SIMPLE] === END: {failure['path']} ===")
elif failed:
log_timing(
f"[SIMPLE] {len(failed)} examples failed (too many to list full details)"
)
log_timing("[SIMPLE] First 10 failure summaries:")
for failure in failed[:10]:
err = "\n" + failure["stderr"]
error_preview = err if err.strip() else "No error details"
log_timing(f"[SIMPLE] {failure['path']}: {error_preview}...")
return CompilationResult(
successful_count=len(successful),
failed_count=len(failed),
compile_time=compile_time,
failed_examples=failed,
object_file_map=object_file_map if full_compilation else None,
)
def compile_examples_unity(
compiler: Compiler,
ino_files: list[Path],
log_timing: Callable[[str], None],
unity_custom_output: Optional[str] = None,
unity_additional_flags: Optional[List[str]] = None,
) -> CompilationResult:
"""
Compile FastLED examples using UNITY build approach.
Creates a single unity.cpp file containing all .ino examples and their
associated .cpp files, then compiles everything as one unit.
"""
log_timing("[UNITY] Starting UNITY build compilation...")
compile_start = time.time()
# Collect all .cpp files from examples
all_cpp_files: list[Path] = []
for ino_file in ino_files:
# Convert .ino to .cpp (we'll create a temporary .cpp file)
ino_cpp = ino_file.with_suffix(".cpp")
# Create temporary .cpp file from .ino
try:
ino_content = ino_file.read_text(encoding="utf-8", errors="ignore")
cpp_content = f"""#include "FastLED.h"
{ino_content}
"""
# Create temporary .cpp file
temp_cpp = Path(tempfile.gettempdir()) / f"unity_{ino_file.stem}.cpp"
temp_cpp.write_text(cpp_content, encoding="utf-8")
all_cpp_files.append(temp_cpp)
# Also find any additional .cpp files in the example directory
example_dir = ino_file.parent
additional_cpp_files = compiler.find_cpp_files_for_example(ino_file)
all_cpp_files.extend(additional_cpp_files)
except Exception as e:
log_timing(f"[UNITY] ERROR: Failed to process {ino_file.name}: {e}")
return CompilationResult(
successful_count=0,
failed_count=len(ino_files),
compile_time=0.0,
failed_examples=[
{
"file": ino_file.name,
"path": str(ino_file.relative_to(Path("examples"))),
"success": False,
"stderr": f"Failed to prepare for UNITY build: {e}",
}
],
object_file_map=None,
)
if not all_cpp_files:
log_timing("[UNITY] ERROR: No .cpp files to compile")
return CompilationResult(
successful_count=0,
failed_count=len(ino_files),
compile_time=0.0,
failed_examples=[
{
"file": "unity_build",
"path": "unity_build",
"success": False,
"stderr": "No .cpp files found for UNITY build",
}
],
object_file_map=None,
)
log_timing(f"[UNITY] Collected {len(all_cpp_files)} .cpp files for UNITY build")
# Log advanced unity options if used
if unity_custom_output:
log_timing(f"[UNITY] Using custom output path: {unity_custom_output}")
if unity_additional_flags:
log_timing(
f"[UNITY] Using additional flags: {' '.join(unity_additional_flags)}"
)
# Parse additional flags properly - handle both "flag1 flag2" and ["flag1", "flag2"] formats
additional_flags = ["-c"] # Compile only, don't link
if unity_additional_flags:
for flag_group in unity_additional_flags:
# Split space-separated flags into individual flags
additional_flags.extend(flag_group.split())
# Create CompilerOptions for unity compilation (reuse the same settings as the main compiler)
unity_options = CompilerOptions(
include_path=compiler.settings.include_path,
compiler=compiler.settings.compiler,
defines=compiler.settings.defines,
std_version=compiler.settings.std_version,
compiler_args=compiler.settings.compiler_args,
use_pch=False, # Unity builds don't typically need PCH
additional_flags=additional_flags,
)
try:
# Perform UNITY compilation - pass unity_output_path as parameter
unity_future = compiler.compile_unity(
unity_options, all_cpp_files, unity_custom_output
)
unity_result = unity_future.result()
compile_time = time.time() - compile_start
# Clean up temporary .cpp files
for cpp_file in all_cpp_files:
if cpp_file.name.startswith("unity_") and cpp_file.parent == Path(
tempfile.gettempdir()
):
try:
cpp_file.unlink()
except:
pass # Ignore cleanup errors
if unity_result.ok:
log_timing(
f"[UNITY] UNITY build completed successfully in {compile_time:.2f}s"
)
log_timing(f"[UNITY] All {len(ino_files)} examples compiled as single unit")
return CompilationResult(
successful_count=len(ino_files),
failed_count=0,
compile_time=compile_time,
failed_examples=[],
object_file_map=None,
)
else:
log_timing(f"[UNITY] UNITY build failed: {unity_result.stderr}")
return CompilationResult(
successful_count=0,
failed_count=len(ino_files),
compile_time=compile_time,
failed_examples=[
{
"file": "unity_build",
"path": "unity_build",
"success": False,
"stderr": unity_result.stderr,
}
],
object_file_map=None,
)
except Exception as e:
compile_time = time.time() - compile_start
# Clean up temporary .cpp files on error
for cpp_file in all_cpp_files:
if cpp_file.name.startswith("unity_") and cpp_file.parent == Path(
tempfile.gettempdir()
):
try:
cpp_file.unlink()
except:
pass # Ignore cleanup errors
log_timing(f"[UNITY] UNITY build failed with exception: {e}")
return CompilationResult(
successful_count=0,
failed_count=len(ino_files),
compile_time=compile_time,
failed_examples=[
{
"file": "unity_build",
"path": "unity_build",
"success": False,
"stderr": f"UNITY build exception: {e}",
}
],
object_file_map=None,
)
def get_fastled_core_sources() -> List[Path]:
"""Get essential FastLED .cpp files for library creation."""
src_dir = Path("src")
# Core FastLED files that must be included
core_files: List[Path] = [
src_dir / "FastLED.cpp",
src_dir / "colorutils.cpp",
src_dir / "hsv2rgb.cpp",
]
# Find all .cpp files in key directories
additional_sources: List[Path] = []
for pattern in ["*.cpp", "lib8tion/*.cpp", "platforms/stub/*.cpp"]:
additional_sources.extend(list(src_dir.glob(pattern)))
# Include essential .cpp files from nested directories
additional_sources.extend(list(src_dir.rglob("*.cpp")))
# Filter out duplicates and ensure files exist
all_sources: List[Path] = []
seen_files: set[Path] = set()
for cpp_file in core_files + additional_sources:
# Skip stub_main.cpp since we create individual main.cpp files for each example
if cpp_file.name == "stub_main.cpp":
continue
if cpp_file.exists() and cpp_file not in seen_files:
all_sources.append(cpp_file)
seen_files.add(cpp_file)
return all_sources
def create_fastled_library(
compiler: Compiler,
fastled_build_dir: Path,
log_timing: Callable[[str], None],
verbose: bool,
) -> Path:
"""Create libfastled.a static library."""
log_timing("[LIBRARY] Creating FastLED static library...")
# Compile all FastLED sources to object files
fastled_sources = get_fastled_core_sources()
fastled_objects: List[Path] = []
obj_dir = fastled_build_dir / "obj"
obj_dir.mkdir(exist_ok=True)
log_timing(f"[LIBRARY] Compiling {len(fastled_sources)} FastLED source files...")
# Compile each source file
futures: List[tuple[Future[Result], Path, Path]] = []
for cpp_file in fastled_sources:
# Create unique object file name by including relative path to prevent collisions
# Convert path separators to underscores to create valid filename
src_dir = Path("src")
if cpp_file.is_relative_to(src_dir):
rel_path = cpp_file.relative_to(src_dir)
else:
rel_path = cpp_file
# Replace path separators with underscores for unique object file names
obj_name = str(rel_path.with_suffix(".o")).replace("/", "_").replace("\\", "_")
obj_file = obj_dir / obj_name
if verbose:
log_timing(f"[LIBRARY] Compiling src/{rel_path} -> {obj_file.name}")
future = compiler.compile_cpp_file(cpp_file, obj_file)
futures.append((future, obj_file, cpp_file))
# Wait for compilation to complete
compiled_count = 0
failed_count = 0
for future, obj_file, cpp_file in futures:
try:
result: Result = future.result()
if result.ok:
fastled_objects.append(obj_file)
compiled_count += 1
if verbose:
log_timing(
f"[LIBRARY] SUCCESS: {cpp_file.relative_to(Path('src'))}"
)
else:
failed_count += 1
log_timing(
f"[LIBRARY] ERROR: Failed to compile {cpp_file.relative_to(Path('src'))}: {result.stderr[:300]}..."
)
except Exception as e:
failed_count += 1
log_timing(
f"[LIBRARY] ERROR: Exception compiling {cpp_file.relative_to(Path('src'))}: {e}"
)
log_timing(
f"[LIBRARY] Successfully compiled {compiled_count}/{len(fastled_sources)} FastLED sources"
)
# FAIL FAST: If any source files failed to compile, abort immediately
if failed_count > 0:
raise Exception(
f"CRITICAL: {failed_count} FastLED source files failed to compile. "
f"This indicates a serious build environment issue (likely PCH invalidation). "
f"All source files must compile successfully."
)
if not fastled_objects:
raise Exception("No FastLED source files compiled successfully")
# Create static library using ar
lib_file = fastled_build_dir / "libfastled.a"
log_timing(f"[LIBRARY] Creating static library: {lib_file}")
archive_future = compiler.create_archive(fastled_objects, lib_file)
archive_result = archive_future.result()
if not archive_result.ok:
raise Exception(f"Library creation failed: {archive_result.stderr}")
log_timing(f"[LIBRARY] SUCCESS: FastLED library created: {lib_file}")
return lib_file
def get_platform_linker_args() -> List[str]:
"""Get platform-specific linker arguments for FastLED executables."""
import platform
system = platform.system()
if system == "Windows":
# In this project, we use Zig toolchain which has GNU target
# The compiler is hardcoded to "python -m ziglang c++" in create_fastled_compiler()
# Zig toolchain uses GNU-style linking even on Windows
return [
"-static-libgcc", # Static link GCC runtime
"-static-libstdc++", # Static link C++ runtime
"-lpthread", # Threading support
"-lm", # Math library
# Windows system libraries linked automatically
]
elif system == "Linux":
return [
"-pthread", # Threading support
"-lm", # Math library
"-ldl", # Dynamic loading
"-lrt", # Real-time extensions
]
elif system == "Darwin": # macOS
return [
"-pthread", # Threading support
"-lm", # Math library
"-framework",
"CoreFoundation",
"-framework",
"IOKit",
]
else:
return [
"-pthread", # Threading support
"-lm", # Math library
]
def get_executable_name(example_name: str) -> str:
"""Get platform-appropriate executable name."""
import platform
if platform.system() == "Windows":
return f"{example_name}.exe"
else:
return example_name
def create_main_cpp_for_example(example_build_dir: Path) -> Path:
"""Create a main.cpp file with sketch runner support and stub main function."""
main_cpp_content = """// Auto-generated main.cpp for Arduino sketch with sketch runner support
// This file provides both the main() function and sketch runner exports
#include <stdio.h>
#ifdef FASTLED_STUB_IMPL
#include "platforms/stub/time_stub.h"
#include "fl/function.h"
#endif
// Arduino sketch functions (provided by compiled .ino file)
extern void setup();
extern void loop();
// Sketch runner exports for external calling
extern "C" {
void sketch_setup() {
setup();
}
void sketch_loop() {
loop();
}
}
// Main function for direct execution
int main() {
printf("RUNNER: Starting sketch execution\\n");
#ifdef FASTLED_STUB_IMPL
// Override delay function to return immediately for fast testing
setDelayFunction([](uint32_t ms) {
// Fast delay override - do nothing for speed
(void)ms; // Suppress unused parameter warning
});
#endif
// Initialize sketch
printf("RUNNER: Calling setup()\\n");
setup();
printf("RUNNER: Setup complete\\n");
// Run sketch loop limited times for testing (not infinite)
printf("RUNNER: Running loop() 5 times for testing\\n");
for (int i = 1; i <= 5; i++) {
printf("RUNNER: Loop iteration %d\\n", i);
loop();
}
printf("RUNNER: Sketch execution complete\\n");
return 0;
}
"""
main_cpp_path = example_build_dir / "main.cpp"
main_cpp_path.write_text(main_cpp_content)
return main_cpp_path
def link_examples(
object_file_map: Dict[Path, List[Path]],
fastled_lib: Path,
build_dir: Path,
compiler: Compiler,
log_timing: Callable[[str], None],
) -> LinkingResult:
"""Link all examples into executable programs."""
linked_count = 0
failed_count = 0
for ino_file, obj_files in object_file_map.items():
example_name = ino_file.parent.name
example_build_dir = build_dir / example_name
# Create executable name
executable_name = get_executable_name(example_name)
executable_path = example_build_dir / executable_name
# Validate that all object files exist
missing_objects = [obj for obj in obj_files if not obj.exists()]
if missing_objects:
log_timing(
f"[LINKING] FAILED: {executable_name}: Missing object files: {[str(obj) for obj in missing_objects]}"
)
failed_count += 1
continue
try:
# Create and compile main.cpp for this example
main_cpp_path = create_main_cpp_for_example(example_build_dir)
main_obj_path = example_build_dir / "main.o"
main_future = compiler.compile_cpp_file(main_cpp_path, main_obj_path)
main_result: Result = main_future.result()
if not main_result.ok:
log_timing(
f"[LINKING] FAILED: {executable_name}: Failed to compile main.cpp: {main_result.stderr[:300]}..."
)
failed_count += 1
continue
# Add main.o to object files for linking
all_obj_files = obj_files + [main_obj_path]
# Set up linking options
link_options = LinkOptions(
output_executable=str(executable_path),
object_files=[str(obj) for obj in all_obj_files],
static_libraries=[str(fastled_lib)],
linker_args=get_platform_linker_args(),
)
# Perform linking
link_future = compiler.link_program(link_options)
link_result: Result = link_future.result()
if link_result.ok:
log_timing(f"[LINKING] SUCCESS: {executable_name}")
# Verify the executable was actually created
if executable_path.exists():
try:
file_stat = executable_path.stat()
is_executable = bool(file_stat.st_mode & stat.S_IXUSR)
log_timing(
f"[LINKING] VERIFIED: {executable_name} created, size: {file_stat.st_size} bytes, executable: {is_executable}"
)
# Fix permissions if needed on Unix systems
if not is_executable and not platform.system() == "Windows":
try:
executable_path.chmod(
executable_path.stat().st_mode
| stat.S_IXUSR
| stat.S_IXGRP
| stat.S_IXOTH
)
log_timing(
f"[LINKING] FIXED: Added execute permissions to {executable_name}"
)
except Exception as e:
log_timing(
f"[LINKING] WARNING: Could not fix permissions for {executable_name}: {e}"
)
# Only count as success if executable actually exists and is valid
linked_count += 1
except Exception as e:
log_timing(
f"[LINKING] WARNING: Error checking created executable: {e}"
)
# Count as success anyway since file exists, just can't check it
linked_count += 1
else:
# CRITICAL BUG FIX: If executable doesn't exist, count as failure!
failed_count += 1
log_timing(
f"[LINKING] FAILED: {executable_name} reported success but file not found at {executable_path}"
)
else:
failed_count += 1
log_timing(
f"[LINKING] FAILED: {executable_name}: {link_result.stderr[:200]}..."
)
except Exception as e:
failed_count += 1
log_timing(f"[LINKING] FAILED: {executable_name}: Exception: {e}")
if failed_count >= MAX_FAILURES_BEFORE_ABORT:
log_timing(
f"[LINKING] Reached failure threshold ({MAX_FAILURES_BEFORE_ABORT}). Aborting further linking to avoid repeated errors."
)
break
return LinkingResult(linked_count=linked_count, failed_count=failed_count)
def link_examples_with_cache(
object_file_map: Dict[Path, List[Path]],
fastled_lib: Path,
build_dir: Path,
compiler: Compiler,
log_timing: Callable[[str], None],
) -> LinkingResult:
"""
Link all examples into executable programs with intelligent caching.
Leverages the existing FastLEDTestCompiler cache infrastructure to skip
linking when all input artifacts (object files, library, linker args) are unchanged.
Uses parallel cache detection for much faster cache checking (~10x speedup).
"""
from ci.compiler.clang_compiler import link_program_sync
linked_count = 0
failed_count = 0
cache_hits = 0
cache_misses = 0
# Create cache directory (same as FastLEDTestCompiler)
cache_dir = Path(".build/link_cache")
cache_dir.mkdir(parents=True, exist_ok=True)
@dataclass
class ExampleCacheInfo:
"""Information about an example's cache status and linking requirements"""
ino_file: Path
obj_files: List[Path]
example_name: str
executable_name: str
executable_path: Path
main_obj_path: Path
all_obj_files: List[Path]
cache_key: str
cached_exe: Optional[Path]
is_cache_hit: bool
main_compile_future: Optional[Any] = None
# Check if parallel processing is disabled
no_parallel = False
try:
# Access the config through the module's current instance if available
import inspect
from ci.compiler.test_example_compilation import CompilationTestRunner
frame = inspect.currentframe()
while frame:
if "self" in frame.f_locals and hasattr(frame.f_locals["self"], "config"):
config = frame.f_locals["self"].config
no_parallel = getattr(config, "no_parallel", False)
break
frame = frame.f_back
except Exception:
# Fallback: check environment variable
import os
no_parallel = os.getenv("NO_PARALLEL") == "1"
def parallel_cache_detection(
object_file_map: Dict[Path, List[Path]],
fastled_lib: Path,
build_dir: Path,
compiler: Compiler,
log_timing: Callable[[str], None],
) -> tuple[int, int, int, int]:
"""
Parallel cache detection and linking for much faster processing.
Returns: (linked_count, failed_count, cache_hits, cache_misses)
"""
linked_count = 0
failed_count = 0
cache_hits = 0
cache_misses = 0
# Phase 1: Prepare all examples and submit main.cpp compilations in parallel
example_infos: List[ExampleCacheInfo] = []
log_timing(
f"[LINKING] Starting parallel preparation for {len(object_file_map)} examples..."
)
for ino_file, obj_files in object_file_map.items():
example_name = ino_file.parent.name
example_build_dir = build_dir / example_name
executable_name = get_executable_name(example_name)
executable_path = example_build_dir / executable_name
# Validate that all object files exist
missing_objects = [obj for obj in obj_files if not obj.exists()]
if missing_objects:
log_timing(
f"[LINKING] FAILED: {executable_name}: Missing object files: {[str(obj) for obj in missing_objects]}"
)
failed_count += 1
continue
# Create main.cpp and submit compilation (non-blocking)
main_cpp_path = create_main_cpp_for_example(example_build_dir)
main_obj_path = example_build_dir / "main.o"
main_future = compiler.compile_cpp_file(main_cpp_path, main_obj_path)
# Create example info (cache key will be calculated after main.cpp compiles)
example_info = ExampleCacheInfo(
ino_file=ino_file,
obj_files=obj_files,
example_name=example_name,
executable_name=executable_name,
executable_path=executable_path,
main_obj_path=main_obj_path,
all_obj_files=[], # Will be filled after main.cpp compiles
cache_key="", # Will be calculated
cached_exe=None, # Will be checked
is_cache_hit=False, # Will be determined
main_compile_future=main_future,
)
example_infos.append(example_info)
# Phase 2: Wait for main.cpp compilations and calculate cache keys in parallel
log_timing(
f"[LINKING] Parallel cache key calculation for {len(example_infos)} examples..."
)
for example_info in example_infos[
:
]: # Use slice to allow removal during iteration
try:
main_result: Result = example_info.main_compile_future.result()
if not main_result.ok:
log_timing(
f"[LINKING] FAILED: {example_info.executable_name}: Failed to compile main.cpp: {main_result.stderr[:100]}..."
)
failed_count += 1
example_infos.remove(example_info)
continue
# Update example info with complete information
example_info.all_obj_files = example_info.obj_files + [
example_info.main_obj_path
]
linker_args = get_platform_linker_args()
example_info.cache_key = calculate_multiple_objects_cache_key(
example_info.all_obj_files, fastled_lib, linker_args
)
example_info.cached_exe = get_cached_executable(
example_info.example_name, example_info.cache_key
)
example_info.is_cache_hit = example_info.cached_exe is not None
except Exception as e:
log_timing(
f"[LINKING] FAILED: {example_info.executable_name}: Exception during preparation: {e}"
)
failed_count += 1
example_infos.remove(example_info)
if failed_count >= MAX_FAILURES_BEFORE_ABORT:
log_timing(
f"[LINKING] Reached failure threshold ({MAX_FAILURES_BEFORE_ABORT}) during preparation. Aborting."
)
return linked_count, failed_count, cache_hits, cache_misses
# Phase 3: Separate cache hits from cache misses
cache_hit_examples = [info for info in example_infos if info.is_cache_hit]
cache_miss_examples = [info for info in example_infos if not info.is_cache_hit]
log_timing(
f"[LINKING] Cache analysis: {len(cache_hit_examples)} hits, {len(cache_miss_examples)} misses"
)
# Phase 4: Process cache hits in parallel (copy cached executables)
if cache_hit_examples:
def copy_cached_executable(example_info: ExampleCacheInfo) -> bool:
try:
if example_info.cached_exe is None:
return False
shutil.copy2(example_info.cached_exe, example_info.executable_path)
return True
except Exception as e:
log_timing(
f"[LINKING] Warning: Failed to copy cached {example_info.executable_name}, will relink: {e}"
)
return False
with concurrent.futures.ThreadPoolExecutor(
max_workers=min(len(cache_hit_examples), 8)
) as executor:
copy_futures = {
executor.submit(copy_cached_executable, info): info
for info in cache_hit_examples
}
for future in concurrent.futures.as_completed(copy_futures):
example_info = copy_futures[future]
try:
success = future.result()
if success:
linked_count += 1
cache_hits += 1
log_timing(
f"[LINKING] {green_text('SUCCESS (CACHED)')}: {example_info.executable_name}"
)
else:
# Failed to copy, add to cache misses for actual linking
cache_miss_examples.append(example_info)
except Exception as e:
log_timing(
f"[LINKING] Exception copying cached {example_info.executable_name}: {e}"
)
cache_miss_examples.append(example_info)
# Phase 5: Process cache misses in parallel (actual linking)
if cache_miss_examples:
def link_example(example_info: ExampleCacheInfo) -> bool:
try:
linker_args = get_platform_linker_args()
link_options = LinkOptions(
output_executable=str(example_info.executable_path),
object_files=[str(obj) for obj in example_info.all_obj_files],
static_libraries=[str(fastled_lib)],
linker_args=linker_args,
)
link_result: Result = link_program_sync(
link_options, compiler.build_flags
)
if link_result.ok:
# Cache the successful executable
cache_executable(
example_info.example_name,
example_info.cache_key,
example_info.executable_path,
)
return True
else:
log_timing(
f"[LINKING] FAILED: {example_info.executable_name}: {link_result.stderr[:200]}..."
)
return False
except Exception as e:
log_timing(
f"[LINKING] FAILED: {example_info.executable_name}: Exception: {e}"
)
return False
with concurrent.futures.ThreadPoolExecutor(
max_workers=min(len(cache_miss_examples), 4)
) as executor:
link_futures = {
executor.submit(link_example, info): info
for info in cache_miss_examples
}
for future in concurrent.futures.as_completed(link_futures):
example_info = link_futures[future]
try:
success = future.result()
if success:
linked_count += 1
cache_misses += 1
log_timing(
f"[LINKING] {green_text('SUCCESS (REBUILT)')}: {example_info.executable_name}"
)
else:
failed_count += 1
if failed_count >= MAX_FAILURES_BEFORE_ABORT:
log_timing(
f"[LINKING] Reached failure threshold ({MAX_FAILURES_BEFORE_ABORT}). Aborting remaining linking jobs."
)
break
except Exception as e:
log_timing(
f"[LINKING] Exception linking {example_info.executable_name}: {e}"
)
failed_count += 1
if failed_count >= MAX_FAILURES_BEFORE_ABORT:
log_timing(
f"[LINKING] Reached failure threshold ({MAX_FAILURES_BEFORE_ABORT}). Aborting remaining linking jobs."
)
break
return linked_count, failed_count, cache_hits, cache_misses
# Helper functions using the same algorithms as FastLEDTestCompiler
def calculate_file_hash(file_path: Path) -> str:
"""Calculate SHA256 hash of a file (same as FastLEDTestCompiler._calculate_file_hash)"""
if not file_path.exists():
return "no_file"
hash_sha256 = hashlib.sha256()
with open(file_path, "rb") as f:
for chunk in iter(lambda: f.read(4096), b""):
hash_sha256.update(chunk)
return hash_sha256.hexdigest()
def calculate_linker_args_hash(linker_args: List[str]) -> str:
"""Calculate SHA256 hash of linker arguments (same as FastLEDTestCompiler._calculate_linker_args_hash)"""
args_str = "|".join(sorted(linker_args))
hash_sha256 = hashlib.sha256()
hash_sha256.update(args_str.encode("utf-8"))
return hash_sha256.hexdigest()
def calculate_multiple_objects_cache_key(
obj_files: List[Path], fastled_lib: Path, linker_args: List[str]
) -> str:
"""
Calculate cache key for multiple object files linking.
Extends FastLEDTestCompiler logic to handle multiple object files.
"""
# Calculate hash for each object file
obj_hashes: List[str] = []
for obj_file in obj_files:
obj_hashes.append(calculate_file_hash(obj_file))
# Combine object file hashes in a stable way (sorted by path for consistency)
sorted_obj_paths = sorted(str(obj) for obj in obj_files)
sorted_obj_hashes: List[str] = []
for obj_path in sorted_obj_paths:
obj_file = Path(obj_path)
sorted_obj_hashes.append(calculate_file_hash(obj_file))
combined_obj_hash = hashlib.sha256(
"|".join(sorted_obj_hashes).encode("utf-8")
).hexdigest()
# Calculate other hashes
fastled_hash = calculate_file_hash(fastled_lib)
linker_hash = calculate_linker_args_hash(linker_args)
# Include header dependency fingerprint to invalidate link cache when headers change
try:
header_hash = ""
if hasattr(compiler, "_get_pch_dependencies"):
dep_paths = compiler._get_pch_dependencies() # type: ignore[attr-defined]
dep_hashes: List[str] = []
for p in dep_paths:
dep_hashes.append(calculate_file_hash(p))
header_hash = hashlib.sha256(
"|".join(dep_hashes).encode("utf-8")
).hexdigest()
else:
header_hash = "no_header_info"
except KeyboardInterrupt:
import _thread
_thread.interrupt_main()
raise
except Exception:
header_hash = "header_scan_error"
# Combine all components (same format as FastLEDTestCompiler)
combined = f"fastled:{fastled_hash}|objects:{combined_obj_hash}|flags:{linker_hash}|hdr:{header_hash}"
final_hash = hashlib.sha256(combined.encode("utf-8")).hexdigest()
return final_hash[
:16
] # Use first 16 chars for readability (same as FastLEDTestCompiler)
def get_cached_executable(example_name: str, cache_key: str) -> Optional[Path]:
"""Check if cached executable exists (same as FastLEDTestCompiler._get_cached_executable)"""
cached_exe = cache_dir / f"{example_name}_{cache_key}.exe"
return cached_exe if cached_exe.exists() else None
def cache_executable(example_name: str, cache_key: str, exe_path: Path) -> None:
"""Cache an executable (same as FastLEDTestCompiler._cache_executable)"""
if not exe_path.exists():
return
cached_exe = cache_dir / f"{example_name}_{cache_key}.exe"
try:
shutil.copy2(exe_path, cached_exe)
except Exception as e:
log_timing(f"[LINKING] Warning: Failed to cache {example_name}: {e}")
# Choose between parallel and serial processing based on --no-parallel flag
if no_parallel:
log_timing("[LINKING] Serial cache detection (--no-parallel specified)")
# Original serial processing
for ino_file, obj_files in object_file_map.items():
example_name = ino_file.parent.name
example_build_dir = build_dir / example_name
# Create executable name and path
executable_name = get_executable_name(example_name)
executable_path = example_build_dir / executable_name
# Validate that all object files exist
missing_objects = [obj for obj in obj_files if not obj.exists()]
if missing_objects:
log_timing(
f"[LINKING] FAILED: {executable_name}: Missing object files: {[str(obj) for obj in missing_objects]}"
)
failed_count += 1
continue
try:
# Create and compile main.cpp for this example
main_cpp_path = create_main_cpp_for_example(example_build_dir)
main_obj_path = example_build_dir / "main.o"
main_future = compiler.compile_cpp_file(main_cpp_path, main_obj_path)
main_result: Result = main_future.result()
if not main_result.ok:
log_timing(
f"[LINKING] FAILED: {executable_name}: Failed to compile main.cpp: {main_result.stderr[:100]}..."
)
failed_count += 1
continue
# Combine all object files for linking
all_obj_files = obj_files + [main_obj_path]
# Get platform-specific linker arguments
linker_args = get_platform_linker_args()
# Calculate cache key using the same algorithm as FastLEDTestCompiler
cache_key = calculate_multiple_objects_cache_key(
all_obj_files, fastled_lib, linker_args
)
# Check for cached executable
cached_exe = get_cached_executable(example_name, cache_key)
if cached_exe:
try:
# Copy cached executable to target location
shutil.copy2(cached_exe, executable_path)
linked_count += 1
cache_hits += 1
log_timing(
f"[LINKING] {green_text('SUCCESS (CACHED)')}: {executable_name}"
)
continue # Skip actual linking
except Exception as e:
log_timing(
f"[LINKING] Warning: Failed to copy cached {executable_name}, will relink: {e}"
)
# Fall through to actual linking
# No cache hit - perform actual linking using existing link_program_sync
link_options = LinkOptions(
output_executable=str(executable_path),
object_files=[str(obj) for obj in all_obj_files],
static_libraries=[str(fastled_lib)],
linker_args=linker_args,
)
# Use link_program_sync for synchronous linking
link_result: Result = link_program_sync(
link_options, compiler.build_flags
)
if link_result.ok:
linked_count += 1
cache_misses += 1
log_timing(
f"[LINKING] {green_text('SUCCESS (REBUILT)')}: {executable_name}"
)
# Cache the newly linked executable for future use
cache_executable(example_name, cache_key, executable_path)
else:
failed_count += 1
log_timing(
f"[LINKING] FAILED: {executable_name}: {link_result.stderr[:200]}..."
)
except Exception as e:
failed_count += 1
log_timing(f"[LINKING] FAILED: {executable_name}: Exception: {e}")
else:
# Use parallel cache detection for much faster processing
log_timing("[LINKING] Parallel cache detection enabled")
linked_count, failed_count, cache_hits, cache_misses = parallel_cache_detection(
object_file_map, fastled_lib, build_dir, compiler, log_timing
)
return LinkingResult(
linked_count=linked_count,
failed_count=failed_count,
cache_hits=cache_hits,
cache_misses=cache_misses,
)
@dataclass
class CompilationTestConfig:
"""Configuration for the compilation test."""
specific_examples: Optional[List[str]]
clean_build: bool
disable_pch: bool
unity_build: bool
unity_custom_output: Optional[str]
unity_additional_flags: Optional[List[str]]
full_compilation: bool
no_parallel: bool
verbose: bool
@dataclass
class CompilationTestResults:
"""Results from compilation test."""
successful_count: int
failed_count: int
failed_examples: List[Dict[str, str]]
compile_time: float
linking_time: float
linked_count: int
linking_failed_count: int
object_file_map: Optional[Dict[Path, List[Path]]]
execution_failures: List[ExecutionFailure]
# Sketch runner execution results
executed_count: int = 0
execution_failed_count: int = 0
execution_time: float = 0.0
class CompilationTestRunner:
"""Handles the orchestration of FastLED example compilation tests."""
def __init__(self, config: CompilationTestConfig):
self.config = config
self.global_start_time = time.time()
def log_timing(self, message: str) -> None:
"""Log a message with timestamp relative to start."""
elapsed = time.time() - self.global_start_time
print(f"[{elapsed:6.2f}s] {message}")
def initialize_system(
self,
) -> tuple[
Compiler, Dict[str, Union[str, int, float]], Dict[str, Union[bool, str]]
]:
"""Initialize the compiler and get system information."""
self.log_timing("==> FastLED Example Compilation Test (SIMPLE BUILD SYSTEM)")
self.log_timing("=" * 70)
# Get system information
self.log_timing("Getting system information...")
system_info = get_system_info()
build_config = get_build_configuration()
self.log_timing(
f"[SYSTEM] OS: {system_info['os']}, Compiler: {system_info['compiler']}, CPU: {system_info['cpu_cores']} cores"
)
self.log_timing(f"[SYSTEM] Memory: {system_info['memory_gb']:.1f}GB available")
# Initialize compiler
self.log_timing("Initializing simple build system...")
try:
compiler = create_fastled_compiler(
use_pch=not self.config.disable_pch,
parallel=not self.config.no_parallel,
)
# Verify compiler accessibility
version_result = compiler.check_clang_version()
if not version_result.success:
raise RuntimeError(
f"Compiler accessibility check failed: {version_result.error}"
)
return compiler, system_info, build_config
except Exception as e:
raise RuntimeError(f"Failed to initialize simple build system: {e}")
def discover_examples(self, compiler: Compiler) -> List[Path]:
"""Discover and validate .ino examples to compile."""
self.log_timing("Discovering .ino examples...")
if self.config.verbose:
print(
f"[VERBOSE] Discovering examples with filter: {self.config.specific_examples}"
)
try:
filter_names = (
self.config.specific_examples if self.config.specific_examples else None
)
ino_files = compiler.find_ino_files("examples", filter_names=filter_names)
if not ino_files:
if self.config.specific_examples:
# Show detailed error with suggestions
all_ino_files = list(Path("examples").rglob("*.ino"))
if all_ino_files:
available_names = sorted([f.stem for f in all_ino_files])
suggestion = f"\nAvailable examples include: {', '.join(available_names[:10])}..."
if len(available_names) > 10:
suggestion += (
f"\n ... and {len(available_names) - 10} more examples"
)
else:
suggestion = ""
raise ValueError(
f"No .ino files found matching: {self.config.specific_examples}{suggestion}"
)
else:
raise ValueError("No .ino files found in examples directory")
# Report discovery results
if self.config.specific_examples:
self.log_timing(
f"[DISCOVER] Found {len(ino_files)} specific examples: {', '.join([f.stem for f in ino_files])}"
)
if self.config.verbose:
print(f"[VERBOSE] Specific examples found:")
for ino_file in ino_files:
print(f"[VERBOSE] - {ino_file}")
else:
self.log_timing(
f"[DISCOVER] Found {len(ino_files)} total .ino examples in examples/"
)
if self.config.verbose:
print(f"[VERBOSE] All examples found:")
for ino_file in ino_files:
print(f"[VERBOSE] - {ino_file.stem}")
return ino_files
except Exception as e:
raise RuntimeError(f"Failed to discover examples: {e}")
def analyze_pch_compatibility(
self, compiler: Compiler, ino_files: List[Path]
) -> tuple[set[Path], List[str], List[str]]:
"""Analyze files for PCH compatibility and return configuration."""
config_parts = ["Simple Build System: enabled"]
if self.config.unity_build:
config_parts.append("UNITY build: enabled")
else:
config_parts.append("Direct .ino compilation: enabled")
# Caching is disabled
config_parts.append("cache: disabled")
pch_compatible_files: set[Path] = set()
pch_incompatible_files: List[str] = []
if self.config.unity_build:
self.log_timing("[UNITY] PCH disabled for UNITY builds (not needed)")
config_parts.append("PCH: disabled (UNITY mode)")
elif compiler.settings.use_pch:
self.log_timing("[PCH] Analyzing examples for PCH compatibility...")
for ino_file in ino_files:
if compiler.analyze_ino_for_pch_compatibility(ino_file):
pch_compatible_files.add(ino_file)
else:
pch_incompatible_files.append(ino_file.name)
if pch_incompatible_files:
self.log_timing(
f"[PCH] Found {len(pch_incompatible_files)} incompatible files (will use direct compilation):"
)
for filename in pch_incompatible_files[:5]: # Show first 5
self.log_timing(f"[PCH] - {filename} (has code before FastLED.h)")
if len(pch_incompatible_files) > 5:
self.log_timing(
f"[PCH] - ... and {len(pch_incompatible_files) - 5} more"
)
config_parts.append(
f"PCH: selective ({len(pch_compatible_files)} files)"
)
else:
self.log_timing(f"[PCH] All {len(ino_files)} files are PCH compatible")
config_parts.append("PCH: enabled (all files)")
else:
self.log_timing("[PCH] PCH disabled globally")
config_parts.append("PCH: disabled")
return pch_compatible_files, pch_incompatible_files, config_parts
def setup_pch(self, compiler: Compiler, pch_compatible_files: set[Path]) -> bool:
"""Setup precompiled headers if applicable."""
if (
not self.config.unity_build
and compiler.settings.use_pch
and len(pch_compatible_files) > 0
):
pch_start = time.time()
pch_success = compiler.create_pch_file()
pch_time = time.time() - pch_start
if pch_success:
self.log_timing(f"[PCH] Precompiled header created in {pch_time:.2f}s")
return True
else:
self.log_timing(
"[PCH] Precompiled header creation failed, using direct compilation for all files"
)
pch_compatible_files.clear()
return False
elif compiler.settings.use_pch and len(pch_compatible_files) == 0:
self.log_timing(
"[PCH] No PCH-compatible files found, skipping PCH creation"
)
return True
def compile_examples(
self,
compiler: Compiler,
ino_files: List[Path],
pch_compatible_files: set[Path],
enable_fingerprint_cache: bool = True,
cache_file: str = ".build/fingerprint_cache.json",
cache_verbose: bool = False,
) -> CompilationTestResults:
"""Execute the compilation process."""
parallel_status = "disabled" if self.config.no_parallel else "enabled"
self.log_timing(
f"[PERF] Parallel compilation: {parallel_status} (managed by compiler)"
)
self.log_timing("[PERF] Direct compilation enabled (no CMake overhead)")
if self.config.verbose:
print(f"[VERBOSE] Starting compilation with configuration:")
print(f"[VERBOSE] - Examples: {len(ino_files)}")
print(f"[VERBOSE] - PCH compatible: {len(pch_compatible_files)}")
print(f"[VERBOSE] - PCH disabled: {self.config.disable_pch}")
print(f"[VERBOSE] - Parallel: {not self.config.no_parallel}")
print(f"[VERBOSE] - Full compilation: {self.config.full_compilation}")
print(f"[VERBOSE] - Unity build: {self.config.unity_build}")
self.log_timing(f"\n[BUILD] Starting example compilation...")
self.log_timing(f"[BUILD] Target examples: {len(ino_files)}")
start_time = time.time()
try:
if self.config.unity_build:
result = compile_examples_unity(
compiler,
ino_files,
self.log_timing,
unity_custom_output=self.config.unity_custom_output,
unity_additional_flags=self.config.unity_additional_flags,
)
else:
if enable_fingerprint_cache:
# Use cache-aware compilation
from ci.compiler.cache_enhanced_compilation import (
create_cache_compiler,
)
cache_compiler = create_cache_compiler(
compiler, Path(cache_file), verbose=cache_verbose
)
cache_result = cache_compiler.compile_with_cache(
ino_files,
pch_compatible_files,
self.log_timing,
self.config.full_compilation,
self.config.verbose,
)
result = cache_result["compilation_result"]
else:
# Standard compilation without cache
result = compile_examples_simple(
compiler,
ino_files,
pch_compatible_files,
self.log_timing,
self.config.full_compilation,
self.config.verbose,
)
compile_time = time.time() - start_time
return CompilationTestResults(
successful_count=result.successful_count,
failed_count=result.failed_count,
failed_examples=result.failed_examples,
compile_time=compile_time,
linking_time=0.0,
linked_count=0,
linking_failed_count=0,
object_file_map=getattr(result, "object_file_map", None),
execution_failures=[],
)
except Exception as e:
raise RuntimeError(f"Compilation failed: {e}")
def handle_linking(
self, compiler: Compiler, results: CompilationTestResults
) -> CompilationTestResults:
"""Handle linking phase if full compilation is requested."""
if (
not self.config.full_compilation
or results.failed_count > 0
or not results.object_file_map
):
if self.config.full_compilation and results.failed_count > 0:
self.log_timing(
f"[LINKING] Skipping linking due to {results.failed_count} compilation failures"
)
return results
self.log_timing("\n[LINKING] Starting real program linking...")
linking_start = time.time()
try:
# Create FastLED static library
self.log_timing("[LINKING] Creating FastLED static library...")
fastled_build_dir = Path(".build/fastled")
fastled_build_dir.mkdir(parents=True, exist_ok=True)
fastled_lib = create_fastled_library(
compiler,
fastled_build_dir,
self.log_timing,
verbose=self.config.verbose,
)
# Link examples
self.log_timing("[LINKING] Linking example programs...")
build_dir = Path(".build/examples")
linking_result = link_examples_with_cache(
results.object_file_map,
fastled_lib,
build_dir,
compiler,
self.log_timing,
)
results.linked_count = linking_result.linked_count
results.linking_failed_count = linking_result.failed_count
results.linking_time = time.time() - linking_start
if results.linking_failed_count == 0:
self.log_timing(
f"[LINKING] SUCCESS: Successfully linked {results.linked_count} executable programs"
)
else:
self.log_timing(
f"[LINKING] WARNING: Linked {results.linked_count} programs, {results.linking_failed_count} failed"
)
# Report cache statistics
if hasattr(linking_result, "cache_hits") and hasattr(
linking_result, "cache_misses"
):
total_processed = (
linking_result.cache_hits + linking_result.cache_misses
)
if total_processed > 0:
hit_rate = linking_result.cache_hit_rate
self.log_timing(
f"[LINKING] Cache: {linking_result.cache_hits} hits, {linking_result.cache_misses} misses ({hit_rate:.1f}% hit rate)"
)
self.log_timing(
f"[LINKING] Real linking completed in {results.linking_time:.2f}s"
)
except Exception as e:
results.linking_time = time.time() - linking_start
self.log_timing(f"[LINKING] ERROR: Linking failed: {e}")
results.linking_failed_count = (
results.successful_count
) # Mark all as failed
results.linked_count = 0
return results
def handle_execution(
self, results: CompilationTestResults
) -> CompilationTestResults:
"""Execute linked programs when full compilation is requested."""
if (
not self.config.full_compilation
or results.linking_failed_count > 0
or results.linked_count == 0
):
if self.config.full_compilation and results.linking_failed_count > 0:
self.log_timing(
f"[EXECUTION] Skipping execution due to {results.linking_failed_count} linking failures"
)
return results
self.log_timing("\n[EXECUTION] Starting sketch runner execution...")
execution_start = time.time()
executed_count = 0
execution_failed_count = 0
execution_failures: List[ExecutionFailure] = []
build_dir = Path(".build/examples")
# Only execute examples that were compiled and linked in this run
if not results.object_file_map:
self.log_timing(
"[EXECUTION] No object file map available - no examples to execute"
)
return results
# Execute only the examples from this compilation run
failure_threshold_reached = False
for ino_file in results.object_file_map.keys():
example_name = ino_file.parent.name
example_dir = build_dir / example_name
if not example_dir.is_dir():
continue
executable_name = get_executable_name(example_name)
executable_path = example_dir / executable_name
if not executable_path.exists():
# Enhanced logging to debug why executable is missing
self.log_timing(
f"[EXECUTION] SKIPPED: {executable_name}: Executable not found at {executable_path}"
)
# Show what files actually exist in the directory
try:
existing_files = list(example_dir.iterdir())
self.log_timing(
f"[EXECUTION] DEBUG: Directory {example_dir} contains: {[f.name for f in existing_files]}"
)
except Exception as e:
self.log_timing(f"[EXECUTION] DEBUG: Error listing directory: {e}")
continue
# Check executable permissions
import stat
try:
file_stat = executable_path.stat()
is_executable = bool(file_stat.st_mode & stat.S_IXUSR)
self.log_timing(
f"[EXECUTION] DEBUG: {executable_name} exists, size: {file_stat.st_size} bytes, executable: {is_executable}"
)
except Exception as e:
self.log_timing(
f"[EXECUTION] DEBUG: Error checking file permissions: {e}"
)
rp = RunningProcess(
command=[str(executable_path.absolute())],
cwd=example_dir.absolute(),
check=False,
auto_run=True,
enable_stack_trace=True,
on_complete=None,
output_formatter=None,
)
try:
self.log_timing(f"[EXECUTION] Running: {executable_name}")
# Use RunningProcess to execute and stream output
with rp.line_iter(timeout=60) as it:
for line in it:
if self.config.verbose:
self.log_timing(f"[EXECUTION] {line}")
rc = rp.wait()
if rc == 0:
executed_count += 1
self.log_timing(f"[EXECUTION] SUCCESS: {executable_name}")
else:
execution_failed_count += 1
self.log_timing(
f"[EXECUTION] FAILED: {executable_name}: Exit code {rc}"
)
execution_failures.append(
ExecutionFailure(
name=example_name,
reason=f"exit code {rc}",
stdout=rp.stdout,
)
)
if self.config.verbose:
self.log_timing(f"[EXECUTION] Failed output:")
# If nothing meaningful captured, note it
stdout = rp.stdout
if not stdout.strip():
self.log_timing(
"[EXECUTION] No output captured from failed execution"
)
except TimeoutError:
execution_failed_count += 1
self.log_timing(
f"[EXECUTION] FAILED: {executable_name}: Execution timeout (30s)"
)
timeout_stdout: str = "\n".join(rp.stdout)
execution_failures.append(
ExecutionFailure(
name=example_name,
reason="timeout (30s)",
stdout=timeout_stdout,
)
)
except Exception as e:
execution_failed_count += 1
self.log_timing(
f"[EXECUTION] FAILED: {executable_name}: Exception: {e}"
)
exc_stdout: str = "\n".join(rp.stdout)
execution_failures.append(
ExecutionFailure(
name=example_name,
reason=f"exception: {e}",
stdout=exc_stdout,
)
)
if execution_failed_count >= MAX_FAILURES_BEFORE_ABORT:
self.log_timing(
f"[EXECUTION] Reached failure threshold ({MAX_FAILURES_BEFORE_ABORT}). Aborting further execution to avoid repeated errors."
)
failure_threshold_reached = True
break
execution_time = time.time() - execution_start
if execution_failed_count == 0:
self.log_timing(
f"[EXECUTION] SUCCESS: Successfully executed {executed_count} sketch programs"
)
else:
self.log_timing(
f"[EXECUTION] WARNING: Executed {executed_count} programs, {execution_failed_count} failed"
)
self.log_timing(
f"[EXECUTION] Sketch execution completed in {execution_time:.2f}s"
)
# Store execution results in the results object
results.executed_count = executed_count
results.execution_failed_count = execution_failed_count
results.execution_time = execution_time
results.execution_failures = execution_failures
return results
def report_results(
self,
ino_files: List[Path],
results: CompilationTestResults,
config_parts: List[str],
) -> int:
"""Generate the final report and return exit code."""
total_time = (
results.compile_time + results.linking_time + results.execution_time
)
parallel_status = "disabled" if self.config.no_parallel else "enabled"
self.log_timing(f"[CONFIG] Mode: {', '.join(config_parts)}")
self.log_timing(
f"\n[BUILD] Parallel compilation: {parallel_status} (managed by compiler)"
)
# Enhanced timing breakdown
self.log_timing(f"\n[TIMING] Compilation: {results.compile_time:.2f}s")
linking_mode = (
"(with program generation)"
if self.config.full_compilation and results.failed_count == 0
else "(compile-only mode)"
)
self.log_timing(f"[TIMING] Linking: {results.linking_time:.2f}s {linking_mode}")
if self.config.full_compilation and results.execution_time > 0:
self.log_timing(
f"[TIMING] Execution: {results.execution_time:.2f}s (sketch runner)"
)
self.log_timing(f"[TIMING] Total: {total_time:.2f}s")
# Performance summary
self.log_timing(f"\n[SUMMARY] FastLED Example Compilation Performance:")
self.log_timing(f"[SUMMARY] Examples processed: {len(ino_files)}")
if self.config.verbose:
print(f"\n[VERBOSE] Detailed compilation results:")
print(f"[VERBOSE] - Total examples: {len(ino_files)}")
print(f"[VERBOSE] - Successful: {results.successful_count}")
print(f"[VERBOSE] - Failed: {results.failed_count}")
print(f"[VERBOSE] - Compilation time: {results.compile_time:.2f}s")
print(f"[VERBOSE] - Linking time: {results.linking_time:.2f}s")
print(f"[VERBOSE] - Execution time: {results.execution_time:.2f}s")
print(f"[VERBOSE] - Total time: {total_time:.2f}s")
self.log_timing(f"[SUMMARY] Successful: {results.successful_count}")
self.log_timing(f"[SUMMARY] Failed: {results.failed_count}")
if self.config.full_compilation:
self.log_timing(f"[SUMMARY] Linked: {results.linked_count}")
self.log_timing(
f"[SUMMARY] Linking failed: {results.linking_failed_count}"
)
self.log_timing(f"[SUMMARY] Executed: {results.executed_count}")
self.log_timing(
f"[SUMMARY] Execution failed: {results.execution_failed_count}"
)
self.log_timing(f"[SUMMARY] Parallel compilation: {parallel_status}")
self.log_timing(f"[SUMMARY] Build time: {results.compile_time:.2f}s")
# Show both compilation speed and total throughput for complete picture
if results.compile_time > 0:
self.log_timing(
f"[SUMMARY] Compilation speed: {len(ino_files) / results.compile_time:.1f} examples/second"
)
if total_time > 0:
self.log_timing(
f"[SUMMARY] Total throughput: {len(ino_files) / total_time:.1f} examples/second"
)
# Detailed execution failure reporting (rich output without timestamps)
if self.config.full_compilation and results.execution_failed_count > 0:
print()
print("########################################################")
print("# ERROR: TEST EXECUTION FAILED (see detailed output below) #")
print("########################################################")
print()
for failure in results.execution_failures:
print(f"{failure.name} failed with:\n")
preview: str = failure.stdout or failure.reason
preview = preview.strip()
if len(preview) > 300:
preview = preview[:300]
if preview:
print(preview)
else:
print("(no output captured)")
print("\n------------\n")
print("Failing tests (see detailed output above)")
for failure in results.execution_failures:
print(f" * {failure.name}")
# Determine success
overall_success = results.failed_count == 0
if self.config.full_compilation:
overall_success = (
overall_success
and results.linking_failed_count == 0
and results.execution_failed_count == 0
)
if overall_success:
if self.config.full_compilation:
self.log_timing(
"\n[SUCCESS] EXAMPLE COMPILATION + LINKING + EXECUTION TEST: SUCCESS"
)
self.log_timing(
f"[SUCCESS] {len(ino_files)} examples compiled ({results.successful_count} compilation jobs), "
f"{results.linked_count} linked, and {results.executed_count} executed successfully"
)
else:
self.log_timing("\n[SUCCESS] EXAMPLE COMPILATION TEST: SUCCESS")
self.log_timing(
f"[SUCCESS] {len(ino_files)} examples compiled successfully ({results.successful_count} compilation jobs)"
)
print(green_text("### SUCCESS ###"))
return 0
else:
# Show failed examples
print(f"\n{red_text('### ERROR ###')}")
if results.failed_count > 0:
self.log_timing(f"[ERROR] {results.failed_count} compilation failures:")
for failure in results.failed_examples:
path = failure["path"].replace("\\", "/")
print(f" {orange_text(f'examples/{path}')}")
if self.config.full_compilation and results.linking_failed_count > 0:
self.log_timing(
f"[ERROR] {results.linking_failed_count} linking failures detected"
)
self.log_timing(
"[ERROR] Test failed due to linker errors - see linking output above"
)
return 1
def run_example_compilation_test(
specific_examples: Optional[List[str]],
clean_build: bool,
disable_pch: bool,
unity_build: bool,
unity_custom_output: Optional[str],
unity_additional_flags: Optional[List[str]],
full_compilation: bool,
no_parallel: bool,
verbose: bool = False,
enable_fingerprint_cache: bool = True,
cache_file: str = ".build/fingerprint_cache.json",
cache_verbose: bool = False,
) -> int:
"""Run the example compilation test using enhanced simple build system."""
try:
# Create configuration
config = CompilationTestConfig(
specific_examples=specific_examples,
clean_build=clean_build,
disable_pch=disable_pch,
unity_build=unity_build,
unity_custom_output=unity_custom_output,
unity_additional_flags=unity_additional_flags,
full_compilation=full_compilation,
no_parallel=no_parallel,
verbose=verbose,
)
# Create test runner
runner = CompilationTestRunner(config)
# Initialize system and compiler
compiler, system_info, build_config = runner.initialize_system()
# Discover examples to compile
ino_files = runner.discover_examples(compiler)
# Analyze PCH compatibility and configuration
pch_compatible_files, pch_incompatible_files, config_parts = (
runner.analyze_pch_compatibility(compiler, ino_files)
)
# Setup PCH if needed
runner.setup_pch(compiler, pch_compatible_files)
# Dump compiler information
compiler_args = compiler.get_compiler_args()
if config.verbose:
print(f"\n[VERBOSE] Compiler configuration:")
print(f"[VERBOSE] Base compiler command: {' '.join(compiler_args[:3])}")
print(f"[VERBOSE] Compiler flags ({len(compiler_args) - 3} total):")
for i, arg in enumerate(compiler_args[3:], 1):
print(f"[VERBOSE] {i:2d}. {arg}")
print()
# Compile examples
results = runner.compile_examples(
compiler,
ino_files,
pch_compatible_files,
enable_fingerprint_cache=enable_fingerprint_cache,
cache_file=cache_file,
cache_verbose=cache_verbose,
)
# Handle linking if requested
results = runner.handle_linking(compiler, results)
# Handle execution if requested (after linking)
results = runner.handle_execution(results)
# Generate report and return exit code
return runner.report_results(ino_files, results, config_parts)
except Exception as e:
print(f"\n{red_text('### ERROR ###')}")
print(f"Unexpected error: {e}")
return 1
if __name__ == "__main__":
parser = argparse.ArgumentParser(
description="FastLED Example Compilation Testing Script (Enhanced with Simple Build System)"
)
parser.add_argument(
"examples",
nargs="*",
help="Specific examples to compile (if none specified, compile all examples)",
)
parser.add_argument(
"--clean",
action="store_true",
help="Force a clean build (note: simple build system always performs clean builds).",
)
parser.add_argument(
"--no-pch",
action="store_true",
help="Disable precompiled headers (PCH) for compilation - useful for debugging or compatibility issues.",
)
parser.add_argument(
"--no-cache",
action="store_true",
help="Disable cache for compilation (cache disabled by default).",
)
parser.add_argument(
"--unity",
action="store_true",
help="Enable UNITY build mode - compile all source files as a single unit for improved performance and optimization.",
)
parser.add_argument(
"--custom-output",
type=str,
help="Custom output path for unity.cpp file (only used with --unity)",
)
parser.add_argument(
"--additional-flags",
nargs="+",
help="Additional compiler flags to pass to unity build (only used with --unity)",
)
parser.add_argument(
"--full",
action="store_true",
help="Enable full compilation mode: compile AND link examples into executable programs",
)
parser.add_argument(
"--no-parallel",
action="store_true",
help="Disable parallel compilation - useful for debugging or single-threaded environments",
)
parser.add_argument(
"--verbose",
"-v",
action="store_true",
help="Enable verbose output showing detailed compilation commands and results",
)
parser.add_argument(
"--no-fingerprint-cache",
action="store_true",
help="Disable fingerprint cache (cache is enabled by default for faster incremental builds)",
)
parser.add_argument(
"--cache-file",
type=str,
default=".build/fingerprint_cache.json",
help="Path to fingerprint cache file (default: .build/fingerprint_cache.json)",
)
parser.add_argument(
"--cache-verbose",
action="store_true",
help="Enable verbose cache output showing which files are skipped or compiled",
)
args = parser.parse_args()
# Pass specific examples to the test function
specific_examples = args.examples if args.examples else None
sys.exit(
run_example_compilation_test(
specific_examples,
args.clean,
disable_pch=args.no_pch,
unity_build=args.unity,
unity_custom_output=args.custom_output,
unity_additional_flags=args.additional_flags,
full_compilation=args.full,
no_parallel=args.no_parallel,
verbose=args.verbose,
enable_fingerprint_cache=not args.no_fingerprint_cache,
cache_file=args.cache_file,
cache_verbose=args.cache_verbose,
)
)