1# -*- coding: utf-8 -*- 2""" 3Toolchain management for RT-Thread build system. 4 5This module provides abstraction for different toolchains (GCC, Keil, IAR, etc.). 6""" 7 8import os 9import shutil 10import subprocess 11from abc import ABC, abstractmethod 12from typing import Dict, List, Optional, Tuple 13from dataclasses import dataclass 14 15 16@dataclass 17class ToolchainInfo: 18 """Toolchain information.""" 19 name: str 20 version: str 21 path: str 22 prefix: str = "" 23 suffix: str = "" 24 25 26class Toolchain(ABC): 27 """Abstract base class for toolchains.""" 28 29 def __init__(self): 30 self.info = None 31 32 @abstractmethod 33 def get_name(self) -> str: 34 """Get toolchain name.""" 35 pass 36 37 @abstractmethod 38 def detect(self) -> bool: 39 """Detect if toolchain is available.""" 40 pass 41 42 @abstractmethod 43 def configure_environment(self, env) -> None: 44 """Configure SCons environment for this toolchain.""" 45 pass 46 47 @abstractmethod 48 def get_compile_flags(self, cpu: str, fpu: str = None, float_abi: str = None) -> Dict[str, str]: 49 """Get compilation flags for target CPU.""" 50 pass 51 52 def get_version(self) -> Optional[str]: 53 """Get toolchain version.""" 54 return self.info.version if self.info else None 55 56 def _run_command(self, cmd: List[str]) -> Tuple[int, str, str]: 57 """Run command and return (returncode, stdout, stderr).""" 58 try: 59 result = subprocess.run(cmd, capture_output=True, text=True) 60 return result.returncode, result.stdout, result.stderr 61 except Exception as e: 62 return -1, "", str(e) 63 64 65class GccToolchain(Toolchain): 66 """GCC toolchain implementation.""" 67 68 def __init__(self, prefix: str = ""): 69 super().__init__() 70 self.prefix = prefix or "arm-none-eabi-" 71 72 def get_name(self) -> str: 73 return "gcc" 74 75 def detect(self) -> bool: 76 """Detect GCC toolchain.""" 77 gcc_path = shutil.which(self.prefix + "gcc") 78 if not gcc_path: 79 return False 80 81 # Get version 82 ret, stdout, _ = self._run_command([gcc_path, "--version"]) 83 if ret == 0: 84 lines = stdout.split('\n') 85 if lines: 86 version = lines[0].split()[-1] 87 self.info = ToolchainInfo( 88 name="gcc", 89 version=version, 90 path=os.path.dirname(gcc_path), 91 prefix=self.prefix 92 ) 93 return True 94 95 return False 96 97 def configure_environment(self, env) -> None: 98 """Configure environment for GCC.""" 99 env['CC'] = self.prefix + 'gcc' 100 env['CXX'] = self.prefix + 'g++' 101 env['AS'] = self.prefix + 'gcc' 102 env['AR'] = self.prefix + 'ar' 103 env['LINK'] = self.prefix + 'gcc' 104 env['SIZE'] = self.prefix + 'size' 105 env['OBJDUMP'] = self.prefix + 'objdump' 106 env['OBJCPY'] = self.prefix + 'objcopy' 107 108 # Set default flags 109 env['ARFLAGS'] = '-rc' 110 env['ASFLAGS'] = '-x assembler-with-cpp' 111 112 # Path 113 if self.info and self.info.path: 114 env.PrependENVPath('PATH', self.info.path) 115 116 def get_compile_flags(self, cpu: str, fpu: str = None, float_abi: str = None) -> Dict[str, str]: 117 """Get GCC compilation flags.""" 118 flags = { 119 'CFLAGS': [], 120 'CXXFLAGS': [], 121 'ASFLAGS': [], 122 'LDFLAGS': [] 123 } 124 125 # CPU flags 126 cpu_flags = { 127 'cortex-m0': '-mcpu=cortex-m0 -mthumb', 128 'cortex-m0+': '-mcpu=cortex-m0plus -mthumb', 129 'cortex-m3': '-mcpu=cortex-m3 -mthumb', 130 'cortex-m4': '-mcpu=cortex-m4 -mthumb', 131 'cortex-m7': '-mcpu=cortex-m7 -mthumb', 132 'cortex-m23': '-mcpu=cortex-m23 -mthumb', 133 'cortex-m33': '-mcpu=cortex-m33 -mthumb', 134 'cortex-a7': '-mcpu=cortex-a7', 135 'cortex-a9': '-mcpu=cortex-a9' 136 } 137 138 if cpu in cpu_flags: 139 base_flags = cpu_flags[cpu] 140 for key in ['CFLAGS', 'CXXFLAGS', 'ASFLAGS']: 141 flags[key].append(base_flags) 142 143 # FPU flags 144 if fpu: 145 fpu_flag = f'-mfpu={fpu}' 146 for key in ['CFLAGS', 'CXXFLAGS']: 147 flags[key].append(fpu_flag) 148 149 # Float ABI 150 if float_abi: 151 abi_flag = f'-mfloat-abi={float_abi}' 152 for key in ['CFLAGS', 'CXXFLAGS']: 153 flags[key].append(abi_flag) 154 155 # Common flags 156 common_flags = ['-ffunction-sections', '-fdata-sections'] 157 flags['CFLAGS'].extend(common_flags) 158 flags['CXXFLAGS'].extend(common_flags) 159 160 # Linker flags 161 flags['LDFLAGS'].extend(['-Wl,--gc-sections']) 162 163 # Convert lists to strings 164 return {k: ' '.join(v) for k, v in flags.items()} 165 166 167class ArmccToolchain(Toolchain): 168 """ARM Compiler (Keil) toolchain implementation.""" 169 170 def get_name(self) -> str: 171 return "armcc" 172 173 def detect(self) -> bool: 174 """Detect ARM Compiler toolchain.""" 175 armcc_path = shutil.which("armcc") 176 if not armcc_path: 177 # Try common Keil installation paths 178 keil_paths = [ 179 r"C:\Keil_v5\ARM\ARMCC\bin", 180 r"C:\Keil\ARM\ARMCC\bin", 181 "/opt/arm/bin" 182 ] 183 for path in keil_paths: 184 test_path = os.path.join(path, "armcc") 185 if os.path.exists(test_path): 186 armcc_path = test_path 187 break 188 189 if not armcc_path: 190 return False 191 192 # Get version 193 ret, stdout, _ = self._run_command([armcc_path, "--version"]) 194 if ret == 0: 195 lines = stdout.split('\n') 196 for line in lines: 197 if "ARM Compiler" in line: 198 version = line.split()[-1] 199 self.info = ToolchainInfo( 200 name="armcc", 201 version=version, 202 path=os.path.dirname(armcc_path) 203 ) 204 return True 205 206 return False 207 208 def configure_environment(self, env) -> None: 209 """Configure environment for ARM Compiler.""" 210 env['CC'] = 'armcc' 211 env['CXX'] = 'armcc' 212 env['AS'] = 'armasm' 213 env['AR'] = 'armar' 214 env['LINK'] = 'armlink' 215 216 # ARM Compiler specific settings 217 env['ARCOM'] = '$AR --create $TARGET $SOURCES' 218 env['LIBPREFIX'] = '' 219 env['LIBSUFFIX'] = '.lib' 220 env['LIBLINKPREFIX'] = '' 221 env['LIBLINKSUFFIX'] = '.lib' 222 env['LIBDIRPREFIX'] = '--userlibpath ' 223 224 # Path 225 if self.info and self.info.path: 226 env.PrependENVPath('PATH', self.info.path) 227 228 def get_compile_flags(self, cpu: str, fpu: str = None, float_abi: str = None) -> Dict[str, str]: 229 """Get ARM Compiler flags.""" 230 flags = { 231 'CFLAGS': [], 232 'CXXFLAGS': [], 233 'ASFLAGS': [], 234 'LDFLAGS': [] 235 } 236 237 # CPU selection 238 cpu_map = { 239 'cortex-m0': '--cpu Cortex-M0', 240 'cortex-m0+': '--cpu Cortex-M0+', 241 'cortex-m3': '--cpu Cortex-M3', 242 'cortex-m4': '--cpu Cortex-M4', 243 'cortex-m7': '--cpu Cortex-M7' 244 } 245 246 if cpu in cpu_map: 247 cpu_flag = cpu_map[cpu] 248 for key in flags: 249 flags[key].append(cpu_flag) 250 251 # Common flags 252 flags['CFLAGS'].extend(['--c99', '--gnu']) 253 flags['CXXFLAGS'].extend(['--cpp', '--gnu']) 254 255 return {k: ' '.join(v) for k, v in flags.items()} 256 257 258class IarToolchain(Toolchain): 259 """IAR toolchain implementation.""" 260 261 def get_name(self) -> str: 262 return "iar" 263 264 def detect(self) -> bool: 265 """Detect IAR toolchain.""" 266 iccarm_path = shutil.which("iccarm") 267 if not iccarm_path: 268 # Try common IAR installation paths 269 iar_paths = [ 270 r"C:\Program Files (x86)\IAR Systems\Embedded Workbench 8.0\arm\bin", 271 r"C:\Program Files\IAR Systems\Embedded Workbench 8.0\arm\bin", 272 "/opt/iar/bin" 273 ] 274 for path in iar_paths: 275 test_path = os.path.join(path, "iccarm.exe" if os.name == 'nt' else "iccarm") 276 if os.path.exists(test_path): 277 iccarm_path = test_path 278 break 279 280 if not iccarm_path: 281 return False 282 283 self.info = ToolchainInfo( 284 name="iar", 285 version="8.x", # IAR version detection is complex 286 path=os.path.dirname(iccarm_path) 287 ) 288 return True 289 290 def configure_environment(self, env) -> None: 291 """Configure environment for IAR.""" 292 env['CC'] = 'iccarm' 293 env['CXX'] = 'iccarm' 294 env['AS'] = 'iasmarm' 295 env['AR'] = 'iarchive' 296 env['LINK'] = 'ilinkarm' 297 298 # IAR specific settings 299 env['LIBPREFIX'] = '' 300 env['LIBSUFFIX'] = '.a' 301 env['LIBLINKPREFIX'] = '' 302 env['LIBLINKSUFFIX'] = '.a' 303 304 # Path 305 if self.info and self.info.path: 306 env.PrependENVPath('PATH', self.info.path) 307 308 def get_compile_flags(self, cpu: str, fpu: str = None, float_abi: str = None) -> Dict[str, str]: 309 """Get IAR flags.""" 310 flags = { 311 'CFLAGS': [], 312 'CXXFLAGS': [], 313 'ASFLAGS': [], 314 'LDFLAGS': [] 315 } 316 317 # CPU selection 318 cpu_map = { 319 'cortex-m0': '--cpu=Cortex-M0', 320 'cortex-m0+': '--cpu=Cortex-M0+', 321 'cortex-m3': '--cpu=Cortex-M3', 322 'cortex-m4': '--cpu=Cortex-M4', 323 'cortex-m7': '--cpu=Cortex-M7' 324 } 325 326 if cpu in cpu_map: 327 cpu_flag = cpu_map[cpu] 328 flags['CFLAGS'].append(cpu_flag) 329 flags['CXXFLAGS'].append(cpu_flag) 330 331 # Common flags 332 flags['CFLAGS'].extend(['-e', '--dlib_config', 'DLib_Config_Normal.h']) 333 334 return {k: ' '.join(v) for k, v in flags.items()} 335 336 337class ToolchainManager: 338 """Manager for toolchain selection and configuration.""" 339 340 def __init__(self): 341 self.toolchains: Dict[str, Toolchain] = {} 342 self.current_toolchain: Optional[Toolchain] = None 343 self._register_default_toolchains() 344 345 def _register_default_toolchains(self) -> None: 346 """Register default toolchains.""" 347 # Try to detect available toolchains 348 toolchain_classes = [ 349 (GccToolchain, ['arm-none-eabi-', 'riscv32-unknown-elf-', 'riscv64-unknown-elf-']), 350 (ArmccToolchain, ['']), 351 (IarToolchain, ['']) 352 ] 353 354 for toolchain_class, prefixes in toolchain_classes: 355 for prefix in prefixes: 356 if toolchain_class == GccToolchain: 357 tc = toolchain_class(prefix) 358 else: 359 tc = toolchain_class() 360 361 if tc.detect(): 362 name = f"{tc.get_name()}-{prefix}" if prefix else tc.get_name() 363 self.register_toolchain(name, tc) 364 365 def register_toolchain(self, name: str, toolchain: Toolchain) -> None: 366 """Register a toolchain.""" 367 self.toolchains[name] = toolchain 368 369 def select_toolchain(self, name: str) -> Toolchain: 370 """Select a toolchain by name.""" 371 if name not in self.toolchains: 372 # Try to create it 373 if name == 'gcc': 374 tc = GccToolchain() 375 elif name == 'armcc' or name == 'keil': 376 tc = ArmccToolchain() 377 elif name == 'iar': 378 tc = IarToolchain() 379 else: 380 raise ValueError(f"Unknown toolchain: {name}") 381 382 if tc.detect(): 383 self.register_toolchain(name, tc) 384 else: 385 raise RuntimeError(f"Toolchain '{name}' not found") 386 387 self.current_toolchain = self.toolchains[name] 388 return self.current_toolchain 389 390 def get_current(self) -> Optional[Toolchain]: 391 """Get current toolchain.""" 392 return self.current_toolchain 393 394 def list_toolchains(self) -> List[str]: 395 """List available toolchains.""" 396 return list(self.toolchains.keys())