481 lines
15 KiB
Python
481 lines
15 KiB
Python
#!/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())
|