#!/usr/bin/env python3 # pyright: reportUnknownMemberType=false """ Build Info Analyzer - Tool for extracting platform information from build_info.json files. This tool provides easy access to platform-specific defines, compiler flags, toolchain paths, and other build configuration information generated by PlatformIO. Usage: python build_info_analyzer.py --board uno --show-defines python build_info_analyzer.py --board esp32dev --show-all python build_info_analyzer.py --list-boards """ import argparse import json from dataclasses import asdict, dataclass, field from pathlib import Path from typing import Any, Dict, List, Optional, Tuple @dataclass class CompilerInfo: """Information about compilers and toolchain""" cc_path: str = "" cxx_path: str = "" cc_flags: List[str] = field(default_factory=lambda: list()) cxx_flags: List[str] = field(default_factory=lambda: list()) compiler_type: str = "" build_type: str = "" @dataclass class BuildInfo: """Complete build information for a platform""" board_name: str defines: Dict[str, str] = field(default_factory=lambda: dict()) compiler_info: CompilerInfo = field(default_factory=CompilerInfo) aliases: Dict[str, Optional[str]] = field(default_factory=lambda: dict()) class BuildInfoAnalyzer: """Analyzer for build_info.json files generated by PlatformIO builds.""" def __init__(self, build_dir: str): """ Initialize the analyzer. Args: build_dir: Directory containing platform build directories """ self.build_dir = Path(build_dir) def list_available_boards(self) -> List[str]: """ List all boards that have build_info.json files. Returns: List of board names that have been built """ boards: List[str] = [] if not self.build_dir.exists(): return boards for item in self.build_dir.iterdir(): if item.is_dir() and (item / "build_info.json").exists(): boards.append(item.name) return sorted(boards) def get_build_info_path(self, board_name: str) -> Optional[Path]: """ Get the path to build_info.json for a specific board. Args: board_name: Name of the board (e.g., 'uno', 'esp32dev') Returns: Path to build_info.json file or None if not found """ build_info_path = self.build_dir / board_name / "build_info.json" if build_info_path.exists(): return build_info_path return None def load_build_info(self, board_name: str) -> Optional[Dict[str, Any]]: """ Load and parse build_info.json for a board. Args: board_name: Name of the board Returns: Parsed JSON data or None if file not found """ build_info_path = self.get_build_info_path(board_name) if not build_info_path: return None try: with open(build_info_path, "r") as f: return json.load(f) except (json.JSONDecodeError, IOError) as e: print(f"Error loading build_info.json for {board_name}: {e}") return None def create_board_key_from_build_info( self, data: Dict[str, Any], board_name: str ) -> Optional[str]: """ Get the actual board key used in build_info.json. Sometimes the board name differs from the directory name. Args: data: Parsed build_info.json data board_name: Directory name of the board Returns: Actual board key used in JSON or None if not found """ # Try exact match first if board_name in data: return board_name # If only one key, use it keys = list(data.keys()) if len(keys) == 1: return keys[0] # Try to find a close match for key in keys: if board_name.lower() in key.lower() or key.lower() in board_name.lower(): return key return None def get_platform_defines(self, board_name: str) -> Tuple[bool, List[str], str]: """ Get platform-specific preprocessor defines for a board. Args: board_name: Name of the board Returns: Tuple of (success, defines_list, error_message) """ data = self.load_build_info(board_name) if not data: return False, [], f"Build info not found for {board_name}" board_key = self.create_board_key_from_build_info(data, board_name) if not board_key: available_keys = list(data.keys()) return False, [], f"Board key not found. Available keys: {available_keys}" board_data = data[board_key] defines = board_data.get("defines", []) return True, defines, "" def get_compiler_info(self, board_name: str) -> Tuple[bool, CompilerInfo, str]: """ Get compiler information for a board. Args: board_name: Name of the board Returns: Tuple of (success, compiler_info, error_message) """ data = self.load_build_info(board_name) if not data: return False, CompilerInfo(), f"Build info not found for {board_name}" board_key = self.create_board_key_from_build_info(data, board_name) if not board_key: return False, CompilerInfo(), "Board key not found in build_info.json" board_data = data[board_key] compiler_info = CompilerInfo( cc_path=board_data.get("cc_path", ""), cxx_path=board_data.get("cxx_path", ""), cc_flags=board_data.get("cc_flags", []), cxx_flags=board_data.get("cxx_flags", []), compiler_type=board_data.get("compiler_type", ""), build_type=board_data.get("build_type", ""), ) return True, compiler_info, "" def get_toolchain_aliases( self, board_name: str ) -> Tuple[bool, Dict[str, str], str]: """ Get toolchain tool aliases for a board. Args: board_name: Name of the board Returns: Tuple of (success, aliases_dict, error_message) """ data = self.load_build_info(board_name) if not data: return False, {}, f"Build info not found for {board_name}" board_key = self.create_board_key_from_build_info(data, board_name) if not board_key: return False, {}, "Board key not found in build_info.json" board_data = data[board_key] aliases = board_data.get("aliases", {}) return True, aliases, "" def get_all_info(self, board_name: str) -> Tuple[bool, Dict[str, Any], str]: """ Get all available information for a board. Args: board_name: Name of the board Returns: Tuple of (success, all_info_dict, error_message) """ data = self.load_build_info(board_name) if not data: return False, {}, f"Build info not found for {board_name}" board_key = self.create_board_key_from_build_info(data, board_name) if not board_key: return False, {}, "Board key not found in build_info.json" return True, data[board_key], "" def compare_defines( self, board1: str, board2: str ) -> Tuple[bool, Dict[str, Any], str]: """ Compare platform defines between two boards. Args: board1: First board name board2: Second board name Returns: Tuple of (success, comparison_dict, error_message) """ success1, defines1, err1 = self.get_platform_defines(board1) success2, defines2, err2 = self.get_platform_defines(board2) if not success1: return False, {}, f"Error getting defines for {board1}: {err1}" if not success2: return False, {}, f"Error getting defines for {board2}: {err2}" set1 = set(defines1) set2 = set(defines2) comparison = { "board1": board1, "board2": board2, "board1_only": sorted(list(set1 - set2)), "board2_only": sorted(list(set2 - set1)), "common": sorted(list(set1 & set2)), "board1_total": len(defines1), "board2_total": len(defines2), "common_count": len(set1 & set2), } return True, comparison, "" def print_defines(defines: List[str], board_name: str): """Print platform defines in a formatted way.""" print(f"\nšŸ“‹ Platform Defines for {board_name.upper()}:") print("=" * 50) for define in defines: print(f" {define}") print(f"\nTotal: {len(defines)} defines") def print_compiler_info(compiler_info: CompilerInfo, board_name: str): """Print compiler information in a formatted way.""" print(f"\nšŸ”§ Compiler Information for {board_name.upper()}:") print("=" * 50) print(f"Compiler Type: {compiler_info.compiler_type or 'Unknown'}") print(f"Build Type: {compiler_info.build_type or 'Unknown'}") print(f"C Compiler: {compiler_info.cc_path or 'Unknown'}") print(f"C++ Compiler: {compiler_info.cxx_path or 'Unknown'}") if compiler_info.cc_flags: print(f"\nC Flags ({len(compiler_info.cc_flags)}):") for flag in compiler_info.cc_flags: print(f" {flag}") if compiler_info.cxx_flags: print(f"\nC++ Flags ({len(compiler_info.cxx_flags)}):") for flag in compiler_info.cxx_flags: print(f" {flag}") def print_toolchain_aliases(aliases: Dict[str, str], board_name: str): """Print toolchain aliases in a formatted way.""" print(f"\nāš™ļø Toolchain Aliases for {board_name.upper()}:") print("=" * 50) for tool, path in aliases.items(): if path: # Show just the tool name from the path for readability tool_name = Path(path).name if path else "Not available" print(f" {tool:10}: {tool_name}") else: print(f" {tool:10}: Not available") def print_comparison(comparison: Dict[str, Any]): """Print a comparison between two boards.""" board1 = comparison["board1"] board2 = comparison["board2"] print("\nšŸ” Platform Defines Comparison:") print("=" * 60) print(f"šŸ“Š {board1.upper()} vs {board2.upper()}") print(f" {board1}: {comparison['board1_total']} defines") print(f" {board2}: {comparison['board2_total']} defines") print(f" Common: {comparison['common_count']} defines") if comparison["board1_only"]: print(f"\nšŸ”“ Only in {board1.upper()} ({len(comparison['board1_only'])}):") for define in comparison["board1_only"]: print(f" {define}") if comparison["board2_only"]: print(f"\nšŸ”µ Only in {board2.upper()} ({len(comparison['board2_only'])}):") for define in comparison["board2_only"]: print(f" {define}") if comparison["common"]: print(f"\n🟢 Common Defines ({len(comparison['common'])}):") for define in comparison["common"]: print(f" {define}") def main(): """Main function for command line usage.""" parser = argparse.ArgumentParser( description="Analyze build_info.json files to extract platform information", formatter_class=argparse.RawDescriptionHelpFormatter, epilog=""" Examples: %(prog)s --list-boards # List all available boards %(prog)s --board uno --show-defines # Show UNO platform defines %(prog)s --board esp32dev --show-compiler # Show ESP32 compiler info %(prog)s --board teensy31 --show-all # Show all info for Teensy 3.1 %(prog)s --compare uno esp32dev # Compare defines between boards """, ) parser.add_argument( "--build-dir", default=".build", help="Build directory containing platform subdirectories (default: .build)", ) parser.add_argument( "--list-boards", action="store_true", help="List all boards with build_info.json files", ) parser.add_argument( "--board", help="Board name to analyze (e.g., uno, esp32dev, teensy31)" ) parser.add_argument( "--show-defines", action="store_true", help="Show platform preprocessor defines" ) parser.add_argument( "--show-compiler", action="store_true", help="Show compiler information" ) parser.add_argument( "--show-toolchain", action="store_true", help="Show toolchain aliases" ) parser.add_argument( "--show-all", action="store_true", help="Show all available information" ) parser.add_argument( "--compare", nargs=2, metavar=("BOARD1", "BOARD2"), help="Compare platform defines between two boards", ) parser.add_argument( "--json", action="store_true", help="Output results in JSON format" ) args = parser.parse_args() analyzer = BuildInfoAnalyzer(args.build_dir) if args.list_boards: boards = analyzer.list_available_boards() if not boards: print("āŒ No boards with build_info.json found in build directory") print(f" Directory: {analyzer.build_dir}") print( " Try running a compilation first: uv run python -m ci.ci-compile uno --examples Blink" ) return 1 print(f"šŸ“‹ Available boards ({len(boards)}):") for board in boards: print(f" āœ… {board}") return 0 if args.compare: board1, board2 = args.compare success, comparison, error = analyzer.compare_defines(board1, board2) if not success: print(f"āŒ Error: {error}") return 1 if args.json: print(json.dumps(comparison, indent=2)) else: print_comparison(comparison) return 0 if not args.board: print("āŒ Error: --board is required (or use --list-boards or --compare)") return 1 # Handle single board analysis if args.show_defines or args.show_all: success, defines, error = analyzer.get_platform_defines(args.board) if not success: print(f"āŒ Error getting defines: {error}") return 1 if args.json: print(json.dumps({"defines": defines}, indent=2)) else: print_defines(defines, args.board) if args.show_compiler or args.show_all: success, compiler_info, error = analyzer.get_compiler_info(args.board) if not success: print(f"āŒ Error getting compiler info: {error}") return 1 if args.json: print(json.dumps({"compiler": asdict(compiler_info)}, indent=2)) else: print_compiler_info(compiler_info, args.board) if args.show_toolchain or args.show_all: success, aliases, error = analyzer.get_toolchain_aliases(args.board) if not success: print(f"āŒ Error getting toolchain aliases: {error}") return 1 if args.json: print(json.dumps({"toolchain": aliases}, indent=2)) else: print_toolchain_aliases(aliases, args.board) return 0 if __name__ == "__main__": exit(main())