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())