1# -*- coding: utf-8 -*-
2"""
3Project generator framework for RT-Thread build system.
4
5This module provides the base classes for project generators (MDK, IAR, VS Code, etc.).
6"""
7
8import os
9import shutil
10import json
11import xml.etree.ElementTree as ET
12from abc import ABC, abstractmethod
13from typing import Dict, List, Any, Optional
14from dataclasses import dataclass
15
16from .utils import PathService
17
18
19@dataclass
20class GeneratorConfig:
21    """Configuration for project generators."""
22    output_dir: str
23    project_name: str = "rtthread"
24    target_name: str = "rtthread.elf"
25
26
27class ProjectGenerator(ABC):
28    """Abstract base class for project generators."""
29
30    def __init__(self, config: GeneratorConfig):
31        self.config = config
32        self.path_service = PathService(os.getcwd())
33
34    @abstractmethod
35    def get_name(self) -> str:
36        """Get generator name."""
37        pass
38
39    @abstractmethod
40    def generate(self, context, project_info: Dict[str, Any]) -> bool:
41        """
42        Generate project files.
43
44        Args:
45            context: BuildContext instance
46            project_info: Project information from registry
47
48        Returns:
49            True if successful
50        """
51        pass
52
53    @abstractmethod
54    def clean(self) -> bool:
55        """
56        Clean generated files.
57
58        Returns:
59            True if successful
60        """
61        pass
62
63    def _ensure_output_dir(self) -> None:
64        """Ensure output directory exists."""
65        os.makedirs(self.config.output_dir, exist_ok=True)
66
67    def _copy_template(self, template_name: str, output_name: str = None) -> str:
68        """
69        Copy template file to output directory.
70
71        Args:
72            template_name: Template file name
73            output_name: Output file name (defaults to template_name)
74
75        Returns:
76            Output file path
77        """
78        if output_name is None:
79            output_name = template_name
80
81        template_dir = os.path.join(os.path.dirname(__file__), '..', 'targets')
82        template_path = os.path.join(template_dir, template_name)
83        output_path = os.path.join(self.config.output_dir, output_name)
84
85        if os.path.exists(template_path):
86            shutil.copy2(template_path, output_path)
87            return output_path
88        else:
89            raise FileNotFoundError(f"Template not found: {template_path}")
90
91
92class VscodeGenerator(ProjectGenerator):
93    """Visual Studio Code project generator."""
94
95    def get_name(self) -> str:
96        return "vscode"
97
98    def generate(self, context, project_info: Dict[str, Any]) -> bool:
99        """Generate VS Code project files."""
100        self._ensure_output_dir()
101
102        # Create .vscode directory
103        vscode_dir = os.path.join(self.config.output_dir, '.vscode')
104        os.makedirs(vscode_dir, exist_ok=True)
105
106        # Generate c_cpp_properties.json
107        self._generate_cpp_properties(vscode_dir, context, project_info)
108
109        # Generate tasks.json
110        self._generate_tasks(vscode_dir, context)
111
112        # Generate launch.json
113        self._generate_launch(vscode_dir, context)
114
115        # Generate settings.json
116        self._generate_settings(vscode_dir)
117
118        return True
119
120    def clean(self) -> bool:
121        """Clean VS Code files."""
122        vscode_dir = os.path.join(self.config.output_dir, '.vscode')
123        if os.path.exists(vscode_dir):
124            shutil.rmtree(vscode_dir)
125        return True
126
127    def _generate_cpp_properties(self, vscode_dir: str, context, project_info: Dict) -> None:
128        """Generate c_cpp_properties.json."""
129        # Get toolchain info
130        toolchain = context.toolchain_manager.get_current()
131        compiler_path = ""
132        if toolchain and toolchain.info:
133            if toolchain.get_name() == "gcc":
134                compiler_path = os.path.join(toolchain.info.path, toolchain.info.prefix + "gcc")
135
136        config = {
137            "configurations": [
138                {
139                    "name": "RT-Thread",
140                    "includePath": [
141                        "${workspaceFolder}/**"
142                    ] + project_info.get('all_includes', []),
143                    "defines": [f"{k}={v}" if v != '1' else k
144                              for k, v in project_info.get('all_defines', {}).items()],
145                    "compilerPath": compiler_path,
146                    "cStandard": "c99",
147                    "cppStandard": "c++11",
148                    "intelliSenseMode": "gcc-arm" if "arm" in compiler_path else "gcc-x64"
149                }
150            ],
151            "version": 4
152        }
153
154        output_path = os.path.join(vscode_dir, 'c_cpp_properties.json')
155        with open(output_path, 'w') as f:
156            json.dump(config, f, indent=4)
157
158    def _generate_tasks(self, vscode_dir: str, context) -> None:
159        """Generate tasks.json."""
160        tasks = {
161            "version": "2.0.0",
162            "tasks": [
163                {
164                    "label": "build",
165                    "type": "shell",
166                    "command": "scons",
167                    "problemMatcher": "$gcc",
168                    "group": {
169                        "kind": "build",
170                        "isDefault": True
171                    }
172                },
173                {
174                    "label": "clean",
175                    "type": "shell",
176                    "command": "scons -c",
177                    "problemMatcher": "$gcc"
178                },
179                {
180                    "label": "rebuild",
181                    "type": "shell",
182                    "command": "scons -c && scons",
183                    "problemMatcher": "$gcc"
184                }
185            ]
186        }
187
188        output_path = os.path.join(vscode_dir, 'tasks.json')
189        with open(output_path, 'w') as f:
190            json.dump(tasks, f, indent=4)
191
192    def _generate_launch(self, vscode_dir: str, context) -> None:
193        """Generate launch.json."""
194        launch = {
195            "version": "0.2.0",
196            "configurations": [
197                {
198                    "name": "Cortex Debug",
199                    "type": "cortex-debug",
200                    "request": "launch",
201                    "servertype": "openocd",
202                    "cwd": "${workspaceRoot}",
203                    "executable": "${workspaceRoot}/" + self.config.target_name,
204                    "device": "STM32F103C8",
205                    "configFiles": [
206                        "interface/stlink-v2.cfg",
207                        "target/stm32f1x.cfg"
208                    ]
209                }
210            ]
211        }
212
213        output_path = os.path.join(vscode_dir, 'launch.json')
214        with open(output_path, 'w') as f:
215            json.dump(launch, f, indent=4)
216
217    def _generate_settings(self, vscode_dir: str) -> None:
218        """Generate settings.json."""
219        settings = {
220            "files.associations": {
221                "*.h": "c",
222                "*.c": "c",
223                "*.cpp": "cpp",
224                "*.cc": "cpp",
225                "*.cxx": "cpp"
226            },
227            "C_Cpp.errorSquiggles": "Enabled"
228        }
229
230        output_path = os.path.join(vscode_dir, 'settings.json')
231        with open(output_path, 'w') as f:
232            json.dump(settings, f, indent=4)
233
234
235class CMakeGenerator(ProjectGenerator):
236    """CMake project generator."""
237
238    def get_name(self) -> str:
239        return "cmake"
240
241    def generate(self, context, project_info: Dict[str, Any]) -> bool:
242        """Generate CMakeLists.txt."""
243        self._ensure_output_dir()
244
245        # Get toolchain info
246        toolchain = context.toolchain_manager.get_current()
247
248        lines = [
249            "cmake_minimum_required(VERSION 3.10)",
250            "",
251            "# RT-Thread CMake Project",
252            f"project({self.config.project_name} C CXX ASM)",
253            "",
254            "# C Standard",
255            "set(CMAKE_C_STANDARD 99)",
256            "set(CMAKE_CXX_STANDARD 11)",
257            ""
258        ]
259
260        # Toolchain configuration
261        if toolchain and toolchain.get_name() == "gcc":
262            lines.extend([
263                "# Toolchain",
264                f"set(CMAKE_C_COMPILER {toolchain.info.prefix}gcc)",
265                f"set(CMAKE_CXX_COMPILER {toolchain.info.prefix}g++)",
266                f"set(CMAKE_ASM_COMPILER {toolchain.info.prefix}gcc)",
267                ""
268            ])
269
270        # Include directories
271        lines.extend([
272            "# Include directories",
273            "include_directories("
274        ])
275        for inc in project_info.get('all_includes', []):
276            lines.append(f"    {inc}")
277        lines.extend([")", ""])
278
279        # Definitions
280        lines.extend([
281            "# Definitions",
282            "add_definitions("
283        ])
284        for k, v in project_info.get('all_defines', {}).items():
285            if v == '1':
286                lines.append(f"    -D{k}")
287            else:
288                lines.append(f"    -D{k}={v}")
289        lines.extend([")", ""])
290
291        # Source files
292        lines.extend([
293            "# Source files",
294            "set(SOURCES"
295        ])
296        for src in project_info.get('all_sources', []):
297            lines.append(f"    {src}")
298        lines.extend([")", ""])
299
300        # Executable
301        lines.extend([
302            "# Executable",
303            f"add_executable(${{PROJECT_NAME}} ${{SOURCES}})",
304            ""
305        ])
306
307        # Libraries
308        if project_info.get('all_libs'):
309            lines.extend([
310                "# Libraries",
311                f"target_link_libraries(${{PROJECT_NAME}}"
312            ])
313            for lib in project_info['all_libs']:
314                lines.append(f"    {lib}")
315            lines.extend([")", ""])
316
317        # Write file
318        output_path = os.path.join(self.config.output_dir, 'CMakeLists.txt')
319        with open(output_path, 'w') as f:
320            f.write('\n'.join(lines))
321
322        return True
323
324    def clean(self) -> bool:
325        """Clean CMake files."""
326        files_to_remove = ['CMakeLists.txt', 'CMakeCache.txt']
327        dirs_to_remove = ['CMakeFiles']
328
329        for file in files_to_remove:
330            file_path = os.path.join(self.config.output_dir, file)
331            if os.path.exists(file_path):
332                os.remove(file_path)
333
334        for dir in dirs_to_remove:
335            dir_path = os.path.join(self.config.output_dir, dir)
336            if os.path.exists(dir_path):
337                shutil.rmtree(dir_path)
338
339        return True
340
341
342class GeneratorRegistry:
343    """Registry for project generators."""
344
345    def __init__(self):
346        self.generators: Dict[str, type] = {}
347        self._register_default_generators()
348
349    def _register_default_generators(self) -> None:
350        """Register default generators."""
351        self.register("vscode", VscodeGenerator)
352        self.register("vsc", VscodeGenerator)  # Alias
353        self.register("cmake", CMakeGenerator)
354
355    def register(self, name: str, generator_class: type) -> None:
356        """Register a generator class."""
357        self.generators[name] = generator_class
358
359    def create_generator(self, name: str, config: GeneratorConfig) -> ProjectGenerator:
360        """Create a generator instance."""
361        if name not in self.generators:
362            raise ValueError(f"Unknown generator: {name}")
363
364        return self.generators[name](config)
365
366    def list_generators(self) -> List[str]:
367        """List available generators."""
368        return list(self.generators.keys())