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