1
0

toolchain.py 13 KB

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