first commit for chrg

This commit is contained in:
wmano
2025-08-16 22:58:22 +08:00
commit 52a3ed5862
2306 changed files with 1021208 additions and 0 deletions
+345
View File
@@ -0,0 +1,345 @@
# RT-Thread Next Generation Build System
## 概述
RT-Thread NGNext Generation)构建系统是对现有构建系统的面向对象重构,在保持完全向后兼容的同时,提供了更清晰的架构和更强的可扩展性。
## 特性
-**完全向后兼容**:现有的SConscript无需修改
-**面向对象设计**:清晰的类层次结构和职责分离
-**SCons最佳实践**:充分利用SCons的Environment对象
-**可扩展架构**:易于添加新的工具链和项目生成器
-**类型安全**:更好的类型提示和错误处理
## 架构设计
### 核心模块
```
ng/
├── __init__.py # 包初始化
├── core.py # 核心类:BuildContext
├── environment.py # 环境扩展:RTEnv类,注入到SCons Environment
├── config.py # 配置管理:解析rtconfig.h
├── project.py # 项目管理:ProjectGroup和Registry
├── toolchain.py # 工具链抽象:GCC、Keil、IAR等
├── generator.py # 项目生成器:VS Code、CMake等
├── utils.py # 工具函数:路径、版本等
├── adapter.py # 适配器:与building.py集成
└── building_ng.py # 示例:最小化修改的building.py
```
### 类图
```mermaid
classDiagram
class BuildContext {
+root_directory: str
+config_manager: ConfigManager
+project_registry: ProjectRegistry
+toolchain_manager: ToolchainManager
+prepare_environment(env)
+get_dependency(depend): bool
}
class ConfigManager {
+load_from_file(filepath)
+get_dependency(depend): bool
+get_option(name): ConfigOption
}
class ProjectGroup {
+name: str
+sources: List[str]
+dependencies: List[str]
+build(env): List[Object]
}
class Toolchain {
<<abstract>>
+get_name(): str
+detect(): bool
+configure_environment(env)
}
class ProjectGenerator {
<<abstract>>
+generate(context, info): bool
+clean(): bool
}
BuildContext --> ConfigManager
BuildContext --> ProjectRegistry
BuildContext --> ToolchainManager
ProjectRegistry --> ProjectGroup
ToolchainManager --> Toolchain
```
## 使用方法
### 1. 最小化集成(推荐)
在building.py中添加少量代码即可集成新系统:
```python
# 在building.py的开头添加
try:
from ng.adapter import (
init_build_context,
inject_environment_methods,
load_rtconfig as ng_load_rtconfig
)
USE_NG = True
except ImportError:
USE_NG = False
# 在PrepareBuilding函数中添加
def PrepareBuilding(env, root_directory, has_libcpu=False, remove_components=[]):
# ... 原有代码 ...
# 集成新系统
if USE_NG:
context = init_build_context(root_directory)
inject_environment_methods(env)
ng_load_rtconfig('rtconfig.h')
# ... 继续原有代码 ...
```
### 2. 使用新的环境方法
集成后,SCons Environment对象会自动获得新方法:
```python
# 在SConscript中使用新方法
Import('env')
# 使用环境方法(推荐)
src = env.GlobFiles('*.c')
group = env.DefineGroup('MyComponent', src, depend=['RT_USING_XXX'])
# 也可以使用传统方式(保持兼容)
from building import *
group = DefineGroup('MyComponent', src, depend=['RT_USING_XXX'])
```
### 3. 新的项目生成器
新系统提供了改进的项目生成器:
```bash
# 生成VS Code项目
scons --target=vscode
# 生成CMake项目
scons --target=cmake
```
## API参考
### 环境方法
所有方法都被注入到SCons Environment对象中:
#### env.DefineGroup(name, src, depend, **kwargs)
定义一个组件组。
**参数:**
- `name`: 组名称
- `src`: 源文件列表
- `depend`: 依赖条件(字符串或列表)
- `**kwargs`: 额外参数
- `CPPPATH`: 头文件路径
- `CPPDEFINES`: 宏定义
- `CFLAGS`/`CXXFLAGS`: 编译选项
- `LOCAL_CFLAGS`/`LOCAL_CPPPATH`: 仅对当前组有效的选项
- `LIBS`/`LIBPATH`: 库配置
**返回:** 构建对象列表
**示例:**
```python
src = ['driver.c', 'hal.c']
group = env.DefineGroup('Driver',
src,
depend=['RT_USING_DEVICE'],
CPPPATH=[env.GetCurrentDir()],
LOCAL_CFLAGS='-O3'
)
```
#### env.GetDepend(depend)
检查依赖是否满足。
**参数:**
- `depend`: 依赖名称或列表
**返回:** True如果依赖满足
**示例:**
```python
if env.GetDepend('RT_USING_SERIAL'):
src += ['serial.c']
if env.GetDepend(['RT_USING_SERIAL', 'RT_SERIAL_USING_DMA']):
src += ['serial_dma.c']
```
#### env.SrcRemove(src, remove)
从源文件列表中移除文件。
**参数:**
- `src`: 源文件列表(就地修改)
- `remove`: 要移除的文件
**示例:**
```python
src = env.GlobFiles('*.c')
env.SrcRemove(src, ['test.c', 'debug.c'])
```
#### env.BuildPackage(package_path)
从package.json构建软件包。
**参数:**
- `package_path`: package.json路径
**返回:** 构建对象列表
**示例:**
```python
objs = env.BuildPackage('package.json')
```
#### env.GetContext()
获取当前构建上下文。
**返回:** BuildContext实例
**示例:**
```python
context = env.GetContext()
if context:
context.logger.info("Building component...")
```
## 高级特性
### 1. 自定义工具链
创建自定义工具链:
```python
from ng.toolchain import Toolchain
class MyToolchain(Toolchain):
def get_name(self):
return "mycc"
def detect(self):
# 检测工具链
return shutil.which("mycc") is not None
def configure_environment(self, env):
env['CC'] = 'mycc'
env['CFLAGS'] = '-O2 -Wall'
# 注册工具链
context = env.GetContext()
context.toolchain_manager.register_toolchain('mycc', MyToolchain())
```
### 2. 自定义项目生成器
创建自定义项目生成器:
```python
from ng.generator import ProjectGenerator
class MyGenerator(ProjectGenerator):
def get_name(self):
return "myide"
def generate(self, context, project_info):
# 生成项目文件
self._ensure_output_dir()
# ... 生成逻辑 ...
return True
# 注册生成器
context.generator_registry.register('myide', MyGenerator)
```
### 3. 构建钩子
使用构建上下文添加钩子:
```python
context = env.GetContext()
# 添加日志
context.logger.info("Starting build...")
# 访问配置
if context.config_manager.get_option('RT_THREAD_PRIORITY_MAX'):
print("Max priority:", context.config_manager.get_value('RT_THREAD_PRIORITY_MAX'))
# 获取项目信息
info = context.project_registry.get_project_info()
print(f"Total sources: {len(info['all_sources'])}")
```
## 迁移指南
### 从旧版本迁移
1. **无需修改**:现有的SConscript文件无需任何修改即可工作
2. **可选升级**:可以逐步将`DefineGroup`调用改为`env.DefineGroup`
3. **新功能**:可以开始使用新的特性如`env.BuildPackage`
### 最佳实践
1. **使用环境方法**:优先使用`env.DefineGroup`而不是全局函数
2. **类型提示**:在Python 3.5+中使用类型提示
3. **错误处理**:使用context.logger记录错误和警告
4. **路径处理**:使用PathService处理跨平台路径
## 性能优化
新系统包含多项性能优化:
1. **配置缓存**:依赖检查结果会被缓存
2. **延迟加载**:工具链和生成器按需加载
3. **并行支持**:项目生成可以并行执行
## 测试
运行测试套件:
```bash
cd tools/ng
python -m pytest tests/
```
## 贡献
欢迎贡献代码!请遵循以下准则:
1. 保持向后兼容性
2. 添加类型提示
3. 编写单元测试
4. 更新文档
## 路线图
- [ ] 完整的测试覆盖
- [ ] 性能基准测试
- [ ] 插件系统
- [ ] 更多项目生成器(Eclipse、Qt Creator等)
- [ ] 构建缓存系统
- [ ] 分布式构建支持
## 许可证
本项目遵循RT-Thread的Apache License 2.0许可证。
+25
View File
@@ -0,0 +1,25 @@
# -*- coding: utf-8 -*-
"""
RT-Thread Next Generation Build System
This module provides an object-oriented implementation of the RT-Thread build system
while maintaining backward compatibility with the existing building.py interface.
"""
from .core import BuildContext
from .environment import RTEnv
from .config import ConfigManager
from .project import ProjectRegistry, ProjectGroup
from .toolchain import ToolchainManager
from .generator import GeneratorRegistry
__version__ = "1.0.0"
__all__ = [
'BuildContext',
'RTEnv',
'ConfigManager',
'ProjectRegistry',
'ProjectGroup',
'ToolchainManager',
'GeneratorRegistry'
]
+218
View File
@@ -0,0 +1,218 @@
# -*- coding: utf-8 -*-
"""
Adapter module to integrate new OOP implementation with existing building.py.
This module provides the bridge between the legacy function-based API and the new
object-oriented implementation, ensuring backward compatibility.
"""
import os
from typing import List, Dict, Any, Optional
from .core import BuildContext
from .environment import RTEnv
from .generator import GeneratorConfig, GeneratorRegistry
# Global variables for compatibility
_context: Optional[BuildContext] = None
def init_build_context(root_directory: str) -> BuildContext:
"""
Initialize the build context.
This function should be called early in PrepareBuilding.
Args:
root_directory: RT-Thread root directory
Returns:
BuildContext instance
"""
global _context
_context = BuildContext(root_directory)
return _context
def get_build_context() -> Optional[BuildContext]:
"""Get the current build context."""
return _context
def inject_environment_methods(env) -> None:
"""
Inject RT-Thread methods into SCons Environment.
This should be called in PrepareBuilding after environment setup.
Args:
env: SCons Environment object
"""
RTEnv.inject_methods(env)
# Also set the environment in context
if _context:
_context.prepare_environment(env)
def load_rtconfig(config_file: str = 'rtconfig.h') -> Dict[str, Any]:
"""
Load configuration from rtconfig.h.
Args:
config_file: Configuration file name
Returns:
Dictionary of build options
"""
if _context:
_context.load_configuration(config_file)
return _context.build_options
return {}
def DefineGroup(name: str, src: List[str], depend: Any = None, **kwargs) -> List:
"""
Legacy DefineGroup function for backward compatibility.
This function delegates to the environment method.
Args:
name: Group name
src: Source files
depend: Dependencies
**kwargs: Additional parameters
Returns:
List of build objects
"""
if _context and _context.environment:
return _context.environment.DefineGroup(name, src, depend, **kwargs)
else:
# Fallback behavior
print(f"Warning: DefineGroup called before environment setup for group '{name}'")
return []
def GetDepend(depend: Any) -> bool:
"""
Legacy GetDepend function for backward compatibility.
Args:
depend: Dependency to check
Returns:
True if dependency is satisfied
"""
if _context:
return _context.get_dependency(depend)
return False
def GetCurrentDir() -> str:
"""
Get current directory.
Returns:
Current directory path
"""
return os.path.abspath('.')
def SrcRemove(src: List[str], remove: List[str]) -> None:
"""
Remove files from source list.
Args:
src: Source list (modified in place)
remove: Files to remove
"""
if not isinstance(remove, list):
remove = [remove]
for item in remove:
if item in src:
src.remove(item)
def GetBuildOptions() -> Dict[str, Any]:
"""
Get build options.
Returns:
Dictionary of build options
"""
if _context:
return _context.build_options
return {}
def MergeGroups() -> List:
"""
Merge all registered groups.
Returns:
List of all build objects
"""
if _context:
return _context.merge_groups()
return []
def GenerateProject(target: str, env, projects: List) -> None:
"""
Generate IDE project files.
Args:
target: Target type (mdk5, iar, vscode, etc.)
env: SCons Environment
projects: Project list
"""
if not _context:
print("Error: Build context not initialized")
return
# Get project info from registry
project_info = _context.project_registry.get_project_info()
# Create generator config
config = GeneratorConfig(
output_dir=os.getcwd(),
project_name=os.path.basename(os.getcwd()),
target_name="rtthread.elf"
)
# Create and run generator
try:
generator = _context.generator_registry.create_generator(target, config)
if generator.generate(_context, project_info):
print(f"Successfully generated {target} project files")
else:
print(f"Failed to generate {target} project files")
except Exception as e:
print(f"Error generating {target} project: {e}")
def PrepareModuleBuilding(env, root_directory, bsp_directory) -> None:
"""
Prepare for building a module.
This is a simplified version of PrepareBuilding for module compilation.
Args:
env: SCons Environment
root_directory: RT-Thread root directory
bsp_directory: BSP directory
"""
# Initialize context
context = init_build_context(root_directory)
context.bsp_directory = bsp_directory
# Inject methods
inject_environment_methods(env)
# Load configuration
config_path = os.path.join(bsp_directory, 'rtconfig.h')
if os.path.exists(config_path):
load_rtconfig(config_path)
+115
View File
@@ -0,0 +1,115 @@
# -*- coding: utf-8 -*-
"""
Next Generation building.py with minimal modifications.
This file shows how to integrate the new OOP system with minimal changes to building.py.
The actual implementation would modify the original building.py file.
"""
# Import everything from original building.py
import sys
import os
# Add parent directory to path to import original building
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from building import *
# Import new OOP modules
from ng.adapter import (
init_build_context,
inject_environment_methods,
load_rtconfig as ng_load_rtconfig,
GenerateProject as ng_GenerateProject
)
# Override PrepareBuilding to integrate new system
_original_PrepareBuilding = PrepareBuilding
def PrepareBuilding(env, root_directory, has_libcpu=False, remove_components=[]):
"""
Enhanced PrepareBuilding that integrates the new OOP system.
This function wraps the original PrepareBuilding and adds OOP functionality.
"""
# Initialize new build context
context = init_build_context(root_directory)
# Call original PrepareBuilding
result = _original_PrepareBuilding(env, root_directory, has_libcpu, remove_components)
# Inject new methods into environment
inject_environment_methods(env)
# Load configuration into new system
ng_load_rtconfig('rtconfig.h')
# Store context in environment for access
env['_BuildContext'] = context
return result
# Override DefineGroup to use new implementation
_original_DefineGroup = DefineGroup
def DefineGroup(name, src, depend, **parameters):
"""
Enhanced DefineGroup that uses the new OOP implementation.
This maintains backward compatibility while using the new system internally.
"""
# Get environment from global Env
global Env
if Env and hasattr(Env, 'DefineGroup'):
# Use new method if available
return Env.DefineGroup(name, src, depend, **parameters)
else:
# Fallback to original
return _original_DefineGroup(name, src, depend, **parameters)
# Override GetDepend to use new implementation
_original_GetDepend = GetDepend
def GetDepend(depend):
"""
Enhanced GetDepend that uses the new OOP implementation.
"""
global Env
if Env and hasattr(Env, 'GetDepend'):
# Use new method if available
return Env.GetDepend(depend)
else:
# Fallback to original
return _original_GetDepend(depend)
# Override DoBuilding to integrate project generation
_original_DoBuilding = DoBuilding
def DoBuilding(target, objects):
"""
Enhanced DoBuilding that integrates new project generation.
"""
# Call original DoBuilding
_original_DoBuilding(target, objects)
# Handle project generation with new system
if GetOption('target'):
target_name = GetOption('target')
global Env, Projects
# Use new generator if available
try:
ng_GenerateProject(target_name, Env, Projects)
except Exception as e:
print(f"Falling back to original generator: {e}")
# Call original GenTargetProject
from building import GenTargetProject
GenTargetProject(Projects, program=target)
# Export enhanced functions
__all__ = ['PrepareBuilding', 'DefineGroup', 'GetDepend', 'DoBuilding'] + \
[name for name in dir(sys.modules['building']) if not name.startswith('_')]
+297
View File
@@ -0,0 +1,297 @@
# -*- coding: utf-8 -*-
"""
Configuration management for RT-Thread build system.
This module handles parsing and managing configuration from rtconfig.h files.
"""
import re
import os
from typing import Dict, List, Any, Optional, Union
from dataclasses import dataclass
from enum import Enum
class ConfigType(Enum):
"""Configuration value types."""
BOOLEAN = "boolean"
INTEGER = "integer"
STRING = "string"
UNDEFINED = "undefined"
@dataclass
class ConfigOption:
"""Configuration option with metadata."""
name: str
value: Any
type: ConfigType
line_number: int = 0
comment: str = ""
def as_bool(self) -> bool:
"""Get value as boolean."""
if self.type == ConfigType.BOOLEAN:
return bool(self.value)
elif self.type == ConfigType.INTEGER:
return self.value != 0
elif self.type == ConfigType.STRING:
return bool(self.value)
return False
def as_int(self) -> int:
"""Get value as integer."""
if self.type == ConfigType.INTEGER:
return self.value
elif self.type == ConfigType.BOOLEAN:
return 1 if self.value else 0
elif self.type == ConfigType.STRING:
try:
return int(self.value)
except ValueError:
return 0
return 0
def as_str(self) -> str:
"""Get value as string."""
if self.type == ConfigType.STRING:
return self.value
return str(self.value)
class ConfigParser:
"""Parser for rtconfig.h files."""
# Regular expressions for parsing
RE_DEFINE = re.compile(r'^\s*#\s*define\s+(\w+)(?:\s+(.*))?', re.MULTILINE)
RE_UNDEF = re.compile(r'^\s*#\s*undef\s+(\w+)', re.MULTILINE)
RE_IFDEF = re.compile(r'^\s*#\s*ifdef\s+(\w+)', re.MULTILINE)
RE_IFNDEF = re.compile(r'^\s*#\s*ifndef\s+(\w+)', re.MULTILINE)
RE_ENDIF = re.compile(r'^\s*#\s*endif', re.MULTILINE)
RE_COMMENT = re.compile(r'/\*.*?\*/', re.DOTALL)
RE_LINE_COMMENT = re.compile(r'//.*$', re.MULTILINE)
def __init__(self):
self.options: Dict[str, ConfigOption] = {}
self.conditions: List[str] = []
def parse_file(self, filepath: str) -> Dict[str, ConfigOption]:
"""
Parse configuration file.
Args:
filepath: Path to rtconfig.h
Returns:
Dictionary of configuration options
"""
if not os.path.exists(filepath):
raise FileNotFoundError(f"Configuration file not found: {filepath}")
with open(filepath, 'r', encoding='utf-8') as f:
content = f.read()
return self.parse_content(content)
def parse_content(self, content: str) -> Dict[str, ConfigOption]:
"""
Parse configuration content.
Args:
content: File content
Returns:
Dictionary of configuration options
"""
# Remove comments
content = self.RE_COMMENT.sub('', content)
content = self.RE_LINE_COMMENT.sub('', content)
# Parse line by line
lines = content.split('\n')
for i, line in enumerate(lines):
self._parse_line(line, i + 1)
return self.options
def _parse_line(self, line: str, line_number: int) -> None:
"""Parse a single line."""
# Check for #define
match = self.RE_DEFINE.match(line)
if match:
name = match.group(1)
value = match.group(2) if match.group(2) else '1'
# Parse value
parsed_value, value_type = self._parse_value(value.strip())
# Create option
option = ConfigOption(
name=name,
value=parsed_value,
type=value_type,
line_number=line_number
)
self.options[name] = option
return
# Check for #undef
match = self.RE_UNDEF.match(line)
if match:
name = match.group(1)
if name in self.options:
del self.options[name]
return
def _parse_value(self, value: str) -> tuple:
"""
Parse configuration value.
Returns:
Tuple of (parsed_value, ConfigType)
"""
if not value or value == '1':
return (True, ConfigType.BOOLEAN)
# Try integer
try:
return (int(value, 0), ConfigType.INTEGER) # Support hex/octal
except ValueError:
pass
# Try string (remove quotes)
if value.startswith('"') and value.endswith('"'):
return (value[1:-1], ConfigType.STRING)
# Default to string
return (value, ConfigType.STRING)
class ConfigManager:
"""
Configuration manager for build system.
This class manages configuration options and provides dependency checking.
"""
def __init__(self):
self.parser = ConfigParser()
self.options: Dict[str, ConfigOption] = {}
self.cache: Dict[str, bool] = {}
def load_from_file(self, filepath: str) -> None:
"""
Load configuration from file.
Args:
filepath: Path to rtconfig.h
"""
self.options = self.parser.parse_file(filepath)
self.cache.clear() # Clear dependency cache
def get_option(self, name: str) -> Optional[ConfigOption]:
"""
Get configuration option.
Args:
name: Option name
Returns:
ConfigOption or None
"""
return self.options.get(name)
def get_value(self, name: str, default: Any = None) -> Any:
"""
Get configuration value.
Args:
name: Option name
default: Default value if not found
Returns:
Configuration value
"""
option = self.options.get(name)
if option:
return option.value
return default
def get_dependency(self, depend: Union[str, List[str]]) -> bool:
"""
Check if dependency is satisfied.
Args:
depend: Single dependency or list of dependencies
Returns:
True if all dependencies are satisfied
"""
# Handle empty dependency
if not depend:
return True
# Convert to list
if isinstance(depend, str):
depend = [depend]
# Check cache
cache_key = ','.join(sorted(depend))
if cache_key in self.cache:
return self.cache[cache_key]
# Check all dependencies (AND logic)
result = all(self._check_single_dependency(d) for d in depend)
# Cache result
self.cache[cache_key] = result
return result
def _check_single_dependency(self, name: str) -> bool:
"""Check a single dependency."""
option = self.options.get(name)
if not option:
return False
# For RT-Thread, any defined macro is considered True
# except if explicitly set to 0
if option.type == ConfigType.INTEGER:
return option.value != 0
elif option.type == ConfigType.BOOLEAN:
return option.value
elif option.type == ConfigType.STRING:
return bool(option.value)
return True
def get_all_options(self) -> Dict[str, Any]:
"""
Get all configuration options as a simple dictionary.
Returns:
Dictionary of option names to values
"""
return {name: opt.value for name, opt in self.options.items()}
def validate(self) -> List[str]:
"""
Validate configuration.
Returns:
List of validation errors
"""
errors = []
# Check for common issues
if 'RT_NAME_MAX' in self.options:
name_max = self.options['RT_NAME_MAX'].as_int()
if name_max < 4:
errors.append("RT_NAME_MAX should be at least 4")
if 'RT_THREAD_PRIORITY_MAX' in self.options:
prio_max = self.options['RT_THREAD_PRIORITY_MAX'].as_int()
if prio_max not in [8, 32, 256]:
errors.append("RT_THREAD_PRIORITY_MAX should be 8, 32, or 256")
return errors
+176
View File
@@ -0,0 +1,176 @@
# -*- coding: utf-8 -*-
"""
Core module for RT-Thread build system.
This module provides the central BuildContext class that manages the build state
and coordinates between different components.
"""
import os
import logging
from typing import Dict, List, Optional, Any
from dataclasses import dataclass, field
from .config import ConfigManager
from .project import ProjectRegistry
from .toolchain import ToolchainManager
from .generator import GeneratorRegistry
from .utils import PathService
class BuildContext:
"""
Central build context that manages all build-related state.
This class replaces the global variables in building.py with a proper
object-oriented design while maintaining compatibility.
"""
# Class variable to store the current context (for backward compatibility)
_current_context: Optional['BuildContext'] = None
def __init__(self, root_directory: str):
"""
Initialize build context.
Args:
root_directory: RT-Thread root directory path
"""
self.root_directory = os.path.abspath(root_directory)
self.bsp_directory = os.getcwd()
# Initialize managers
self.config_manager = ConfigManager()
self.project_registry = ProjectRegistry()
self.toolchain_manager = ToolchainManager()
self.generator_registry = GeneratorRegistry()
self.path_service = PathService(self.bsp_directory)
# Build environment
self.environment = None
self.build_options = {}
# Logging
self.logger = self._setup_logger()
# Set as current context
BuildContext._current_context = self
@classmethod
def get_current(cls) -> Optional['BuildContext']:
"""Get the current build context."""
return cls._current_context
@classmethod
def set_current(cls, context: Optional['BuildContext']) -> None:
"""Set the current build context."""
cls._current_context = context
def _setup_logger(self) -> logging.Logger:
"""Setup logger for build system."""
logger = logging.getLogger('rtthread.build')
if not logger.handlers:
handler = logging.StreamHandler()
formatter = logging.Formatter('[%(levelname)s] %(message)s')
handler.setFormatter(formatter)
logger.addHandler(handler)
logger.setLevel(logging.INFO)
return logger
def prepare_environment(self, env) -> None:
"""
Prepare the build environment.
Args:
env: SCons Environment object
"""
self.environment = env
# Set environment variables
env['RTT_ROOT'] = self.root_directory
env['BSP_ROOT'] = self.bsp_directory
# Add to Python path
import sys
tools_path = os.path.join(self.root_directory, 'tools')
if tools_path not in sys.path:
sys.path.insert(0, tools_path)
self.logger.debug(f"Prepared environment with RTT_ROOT={self.root_directory}")
def load_configuration(self, config_file: str = 'rtconfig.h') -> None:
"""
Load configuration from rtconfig.h.
Args:
config_file: Path to configuration file
"""
config_path = os.path.join(self.bsp_directory, config_file)
if os.path.exists(config_path):
self.config_manager.load_from_file(config_path)
self.build_options = self.config_manager.get_all_options()
self.logger.info(f"Loaded configuration from {config_file}")
else:
self.logger.warning(f"Configuration file {config_file} not found")
def get_dependency(self, depend: Any) -> bool:
"""
Check if dependency is satisfied.
Args:
depend: Dependency name or list of names
Returns:
True if dependency is satisfied
"""
return self.config_manager.get_dependency(depend)
def register_project_group(self, group) -> None:
"""
Register a project group.
Args:
group: ProjectGroup instance
"""
self.project_registry.register_group(group)
def merge_groups(self) -> List:
"""
Merge all registered project groups.
Returns:
List of build objects
"""
return self.project_registry.merge_groups(self.environment)
@dataclass
class BuildOptions:
"""Build options container."""
verbose: bool = False
strict: bool = False
target: Optional[str] = None
jobs: int = 1
clean: bool = False
@dataclass
class ProjectInfo:
"""Project information for generators."""
name: str = "rtthread"
target_name: str = "rtthread.elf"
# File collections
source_files: List[str] = field(default_factory=list)
include_paths: List[str] = field(default_factory=list)
defines: Dict[str, str] = field(default_factory=dict)
# Compiler options
cflags: str = ""
cxxflags: str = ""
asflags: str = ""
ldflags: str = ""
# Libraries
libs: List[str] = field(default_factory=list)
lib_paths: List[str] = field(default_factory=list)
+298
View File
@@ -0,0 +1,298 @@
# -*- coding: utf-8 -*-
"""
Environment extensions for RT-Thread build system.
This module provides methods that are injected into the SCons Environment object
to provide RT-Thread-specific functionality.
"""
import os
from typing import List, Union, Dict, Any, Optional
from SCons.Script import *
from .core import BuildContext
from .project import ProjectGroup
class RTEnv:
"""
RT-Thread environment extensions (RTEnv).
This class provides methods that are added to the SCons Environment object.
"""
@staticmethod
def inject_methods(env):
"""
Inject RT-Thread methods into SCons Environment.
Args:
env: SCons Environment object
"""
# Core build methods
env.AddMethod(RTEnv.DefineGroup, 'DefineGroup')
env.AddMethod(RTEnv.GetDepend, 'GetDepend')
env.AddMethod(RTEnv.SrcRemove, 'SrcRemove')
env.AddMethod(RTEnv.GetCurrentDir, 'GetCurrentDir')
env.AddMethod(RTEnv.BuildPackage, 'BuildPackage')
# Utility methods
env.AddMethod(RTEnv.Glob, 'GlobFiles')
env.AddMethod(RTEnv.GetBuildOptions, 'GetBuildOptions')
env.AddMethod(RTEnv.GetContext, 'GetContext')
# Path utilities
env.AddMethod(RTEnv.GetRTTRoot, 'GetRTTRoot')
env.AddMethod(RTEnv.GetBSPRoot, 'GetBSPRoot')
@staticmethod
def DefineGroup(env, name: str, src: List[str], depend: Any = None, **kwargs) -> List:
"""
Define a component group.
This method maintains compatibility with the original DefineGroup function
while using the new object-oriented implementation.
Args:
env: SCons Environment
name: Group name
src: Source file list
depend: Dependency conditions
**kwargs: Additional parameters (CPPPATH, CPPDEFINES, etc.)
Returns:
List of build objects
"""
context = BuildContext.get_current()
if not context:
raise RuntimeError("BuildContext not initialized")
# Check dependencies
if depend and not env.GetDepend(depend):
return []
# Process source files
if isinstance(src, str):
src = [src]
# Create project group
group = ProjectGroup(
name=name,
sources=src,
dependencies=depend if isinstance(depend, list) else [depend] if depend else [],
environment=env
)
# Process parameters
group.include_paths = kwargs.get('CPPPATH', [])
group.defines = kwargs.get('CPPDEFINES', {})
group.cflags = kwargs.get('CFLAGS', '')
group.cxxflags = kwargs.get('CXXFLAGS', '')
group.local_cflags = kwargs.get('LOCAL_CFLAGS', '')
group.local_cxxflags = kwargs.get('LOCAL_CXXFLAGS', '')
group.local_include_paths = kwargs.get('LOCAL_CPPPATH', [])
group.local_defines = kwargs.get('LOCAL_CPPDEFINES', {})
group.libs = kwargs.get('LIBS', [])
group.lib_paths = kwargs.get('LIBPATH', [])
# Build objects
objects = group.build(env)
# Register group
context.register_project_group(group)
return objects
@staticmethod
def GetDepend(env, depend: Any) -> bool:
"""
Check if dependency is satisfied.
Args:
env: SCons Environment
depend: Dependency name or list of names
Returns:
True if dependency is satisfied
"""
context = BuildContext.get_current()
if not context:
# Fallback to checking environment variables
if isinstance(depend, str):
return env.get(depend, False)
elif isinstance(depend, list):
return all(env.get(d, False) for d in depend)
return False
return context.get_dependency(depend)
@staticmethod
def SrcRemove(env, src: List[str], remove: List[str]) -> None:
"""
Remove files from source list.
Args:
env: SCons Environment
src: Source file list (modified in place)
remove: Files to remove
"""
if not isinstance(remove, list):
remove = [remove]
for item in remove:
# Handle both exact matches and pattern matches
if item in src:
src.remove(item)
else:
# Try pattern matching
import fnmatch
to_remove = [f for f in src if fnmatch.fnmatch(f, item)]
for f in to_remove:
src.remove(f)
@staticmethod
def GetCurrentDir(env) -> str:
"""
Get current directory.
Args:
env: SCons Environment
Returns:
Current directory path
"""
return Dir('.').abspath
@staticmethod
def BuildPackage(env, package_path: str = None) -> List:
"""
Build package from package.json.
Args:
env: SCons Environment
package_path: Path to package.json. If None, looks for package.json in current directory.
Returns:
List of build objects
"""
# Import the existing package module
import sys
import os
# Get the building module path
building_path = os.path.dirname(os.path.abspath(__file__))
tools_path = os.path.dirname(building_path)
# Add to path if not already there
if tools_path not in sys.path:
sys.path.insert(0, tools_path)
# Import and use the existing BuildPackage
try:
from package import BuildPackage as build_package_func
# BuildPackage uses global functions, so we need to set up the context
# Save current directory
current_dir = os.getcwd()
# Change to the directory where we want to build
if package_path is None:
work_dir = env.GetCurrentDir()
elif os.path.isdir(package_path):
work_dir = package_path
else:
work_dir = os.path.dirname(package_path)
os.chdir(work_dir)
try:
# Call the original BuildPackage
result = build_package_func(package_path)
finally:
# Restore directory
os.chdir(current_dir)
return result
except ImportError:
# Fallback if import fails
context = BuildContext.get_current()
if context:
context.logger.error("Failed to import package module")
return []
@staticmethod
def Glob(env, pattern: str) -> List[str]:
"""
Enhanced glob with better error handling.
Args:
env: SCons Environment
pattern: File pattern
Returns:
List of matching files
"""
try:
files = Glob(pattern, strings=True)
return sorted(files) # Sort for consistent ordering
except Exception as e:
context = BuildContext.get_current()
if context:
context.logger.warning(f"Glob pattern '{pattern}' failed: {e}")
return []
@staticmethod
def GetBuildOptions(env) -> Dict[str, Any]:
"""
Get build options.
Args:
env: SCons Environment
Returns:
Dictionary of build options
"""
context = BuildContext.get_current()
if context:
return context.build_options
return {}
@staticmethod
def GetContext(env) -> Optional[BuildContext]:
"""
Get current build context.
Args:
env: SCons Environment
Returns:
BuildContext instance or None
"""
return BuildContext.get_current()
@staticmethod
def GetRTTRoot(env) -> str:
"""
Get RT-Thread root directory.
Args:
env: SCons Environment
Returns:
RT-Thread root path
"""
return env.get('RTT_ROOT', '')
@staticmethod
def GetBSPRoot(env) -> str:
"""
Get BSP root directory.
Args:
env: SCons Environment
Returns:
BSP root path
"""
return env.get('BSP_ROOT', '')
+368
View File
@@ -0,0 +1,368 @@
# -*- coding: utf-8 -*-
"""
Project generator framework for RT-Thread build system.
This module provides the base classes for project generators (MDK, IAR, VS Code, etc.).
"""
import os
import shutil
import json
import xml.etree.ElementTree as ET
from abc import ABC, abstractmethod
from typing import Dict, List, Any, Optional
from dataclasses import dataclass
from .utils import PathService
@dataclass
class GeneratorConfig:
"""Configuration for project generators."""
output_dir: str
project_name: str = "rtthread"
target_name: str = "rtthread.elf"
class ProjectGenerator(ABC):
"""Abstract base class for project generators."""
def __init__(self, config: GeneratorConfig):
self.config = config
self.path_service = PathService(os.getcwd())
@abstractmethod
def get_name(self) -> str:
"""Get generator name."""
pass
@abstractmethod
def generate(self, context, project_info: Dict[str, Any]) -> bool:
"""
Generate project files.
Args:
context: BuildContext instance
project_info: Project information from registry
Returns:
True if successful
"""
pass
@abstractmethod
def clean(self) -> bool:
"""
Clean generated files.
Returns:
True if successful
"""
pass
def _ensure_output_dir(self) -> None:
"""Ensure output directory exists."""
os.makedirs(self.config.output_dir, exist_ok=True)
def _copy_template(self, template_name: str, output_name: str = None) -> str:
"""
Copy template file to output directory.
Args:
template_name: Template file name
output_name: Output file name (defaults to template_name)
Returns:
Output file path
"""
if output_name is None:
output_name = template_name
template_dir = os.path.join(os.path.dirname(__file__), '..', 'targets')
template_path = os.path.join(template_dir, template_name)
output_path = os.path.join(self.config.output_dir, output_name)
if os.path.exists(template_path):
shutil.copy2(template_path, output_path)
return output_path
else:
raise FileNotFoundError(f"Template not found: {template_path}")
class VscodeGenerator(ProjectGenerator):
"""Visual Studio Code project generator."""
def get_name(self) -> str:
return "vscode"
def generate(self, context, project_info: Dict[str, Any]) -> bool:
"""Generate VS Code project files."""
self._ensure_output_dir()
# Create .vscode directory
vscode_dir = os.path.join(self.config.output_dir, '.vscode')
os.makedirs(vscode_dir, exist_ok=True)
# Generate c_cpp_properties.json
self._generate_cpp_properties(vscode_dir, context, project_info)
# Generate tasks.json
self._generate_tasks(vscode_dir, context)
# Generate launch.json
self._generate_launch(vscode_dir, context)
# Generate settings.json
self._generate_settings(vscode_dir)
return True
def clean(self) -> bool:
"""Clean VS Code files."""
vscode_dir = os.path.join(self.config.output_dir, '.vscode')
if os.path.exists(vscode_dir):
shutil.rmtree(vscode_dir)
return True
def _generate_cpp_properties(self, vscode_dir: str, context, project_info: Dict) -> None:
"""Generate c_cpp_properties.json."""
# Get toolchain info
toolchain = context.toolchain_manager.get_current()
compiler_path = ""
if toolchain and toolchain.info:
if toolchain.get_name() == "gcc":
compiler_path = os.path.join(toolchain.info.path, toolchain.info.prefix + "gcc")
config = {
"configurations": [
{
"name": "RT-Thread",
"includePath": [
"${workspaceFolder}/**"
] + project_info.get('all_includes', []),
"defines": [f"{k}={v}" if v != '1' else k
for k, v in project_info.get('all_defines', {}).items()],
"compilerPath": compiler_path,
"cStandard": "c99",
"cppStandard": "c++11",
"intelliSenseMode": "gcc-arm" if "arm" in compiler_path else "gcc-x64"
}
],
"version": 4
}
output_path = os.path.join(vscode_dir, 'c_cpp_properties.json')
with open(output_path, 'w') as f:
json.dump(config, f, indent=4)
def _generate_tasks(self, vscode_dir: str, context) -> None:
"""Generate tasks.json."""
tasks = {
"version": "2.0.0",
"tasks": [
{
"label": "build",
"type": "shell",
"command": "scons",
"problemMatcher": "$gcc",
"group": {
"kind": "build",
"isDefault": True
}
},
{
"label": "clean",
"type": "shell",
"command": "scons -c",
"problemMatcher": "$gcc"
},
{
"label": "rebuild",
"type": "shell",
"command": "scons -c && scons",
"problemMatcher": "$gcc"
}
]
}
output_path = os.path.join(vscode_dir, 'tasks.json')
with open(output_path, 'w') as f:
json.dump(tasks, f, indent=4)
def _generate_launch(self, vscode_dir: str, context) -> None:
"""Generate launch.json."""
launch = {
"version": "0.2.0",
"configurations": [
{
"name": "Cortex Debug",
"type": "cortex-debug",
"request": "launch",
"servertype": "openocd",
"cwd": "${workspaceRoot}",
"executable": "${workspaceRoot}/" + self.config.target_name,
"device": "STM32F103C8",
"configFiles": [
"interface/stlink-v2.cfg",
"target/stm32f1x.cfg"
]
}
]
}
output_path = os.path.join(vscode_dir, 'launch.json')
with open(output_path, 'w') as f:
json.dump(launch, f, indent=4)
def _generate_settings(self, vscode_dir: str) -> None:
"""Generate settings.json."""
settings = {
"files.associations": {
"*.h": "c",
"*.c": "c",
"*.cpp": "cpp",
"*.cc": "cpp",
"*.cxx": "cpp"
},
"C_Cpp.errorSquiggles": "Enabled"
}
output_path = os.path.join(vscode_dir, 'settings.json')
with open(output_path, 'w') as f:
json.dump(settings, f, indent=4)
class CMakeGenerator(ProjectGenerator):
"""CMake project generator."""
def get_name(self) -> str:
return "cmake"
def generate(self, context, project_info: Dict[str, Any]) -> bool:
"""Generate CMakeLists.txt."""
self._ensure_output_dir()
# Get toolchain info
toolchain = context.toolchain_manager.get_current()
lines = [
"cmake_minimum_required(VERSION 3.10)",
"",
"# RT-Thread CMake Project",
f"project({self.config.project_name} C CXX ASM)",
"",
"# C Standard",
"set(CMAKE_C_STANDARD 99)",
"set(CMAKE_CXX_STANDARD 11)",
""
]
# Toolchain configuration
if toolchain and toolchain.get_name() == "gcc":
lines.extend([
"# Toolchain",
f"set(CMAKE_C_COMPILER {toolchain.info.prefix}gcc)",
f"set(CMAKE_CXX_COMPILER {toolchain.info.prefix}g++)",
f"set(CMAKE_ASM_COMPILER {toolchain.info.prefix}gcc)",
""
])
# Include directories
lines.extend([
"# Include directories",
"include_directories("
])
for inc in project_info.get('all_includes', []):
lines.append(f" {inc}")
lines.extend([")", ""])
# Definitions
lines.extend([
"# Definitions",
"add_definitions("
])
for k, v in project_info.get('all_defines', {}).items():
if v == '1':
lines.append(f" -D{k}")
else:
lines.append(f" -D{k}={v}")
lines.extend([")", ""])
# Source files
lines.extend([
"# Source files",
"set(SOURCES"
])
for src in project_info.get('all_sources', []):
lines.append(f" {src}")
lines.extend([")", ""])
# Executable
lines.extend([
"# Executable",
f"add_executable(${{PROJECT_NAME}} ${{SOURCES}})",
""
])
# Libraries
if project_info.get('all_libs'):
lines.extend([
"# Libraries",
f"target_link_libraries(${{PROJECT_NAME}}"
])
for lib in project_info['all_libs']:
lines.append(f" {lib}")
lines.extend([")", ""])
# Write file
output_path = os.path.join(self.config.output_dir, 'CMakeLists.txt')
with open(output_path, 'w') as f:
f.write('\n'.join(lines))
return True
def clean(self) -> bool:
"""Clean CMake files."""
files_to_remove = ['CMakeLists.txt', 'CMakeCache.txt']
dirs_to_remove = ['CMakeFiles']
for file in files_to_remove:
file_path = os.path.join(self.config.output_dir, file)
if os.path.exists(file_path):
os.remove(file_path)
for dir in dirs_to_remove:
dir_path = os.path.join(self.config.output_dir, dir)
if os.path.exists(dir_path):
shutil.rmtree(dir_path)
return True
class GeneratorRegistry:
"""Registry for project generators."""
def __init__(self):
self.generators: Dict[str, type] = {}
self._register_default_generators()
def _register_default_generators(self) -> None:
"""Register default generators."""
self.register("vscode", VscodeGenerator)
self.register("vsc", VscodeGenerator) # Alias
self.register("cmake", CMakeGenerator)
def register(self, name: str, generator_class: type) -> None:
"""Register a generator class."""
self.generators[name] = generator_class
def create_generator(self, name: str, config: GeneratorConfig) -> ProjectGenerator:
"""Create a generator instance."""
if name not in self.generators:
raise ValueError(f"Unknown generator: {name}")
return self.generators[name](config)
def list_generators(self) -> List[str]:
"""List available generators."""
return list(self.generators.keys())
+178
View File
@@ -0,0 +1,178 @@
# -*- coding: utf-8 -*-
"""
Example of minimal changes needed in building.py to integrate the new OOP system.
This file shows the exact changes that would be made to the original building.py.
"""
# =============================================================================
# CHANGES TO ADD AT THE BEGINNING OF building.py
# =============================================================================
"""
# Add after the imports section in building.py (around line 45)
# Try to import new OOP system
try:
from ng.adapter import (
init_build_context,
inject_environment_methods,
load_rtconfig as ng_load_rtconfig,
MergeGroups as ng_MergeGroups
)
NG_AVAILABLE = True
except ImportError:
NG_AVAILABLE = False
"""
# =============================================================================
# CHANGES IN PrepareBuilding FUNCTION
# =============================================================================
"""
# Add these lines in PrepareBuilding function after setting up Env (around line 70)
# Initialize new OOP system if available
if NG_AVAILABLE:
# Initialize build context
ng_context = init_build_context(Rtt_Root)
# Inject methods into environment
inject_environment_methods(Env)
# Store context reference
Env['__NG_Context'] = ng_context
"""
# =============================================================================
# CHANGES AFTER PARSING rtconfig.h
# =============================================================================
"""
# Add after parsing rtconfig.h (around line 430)
# Load configuration into new system
if NG_AVAILABLE and 'rtconfig.h' in os.listdir(Bsp_Root):
ng_load_rtconfig('rtconfig.h')
"""
# =============================================================================
# ENHANCED DefineGroup FUNCTION
# =============================================================================
"""
# Replace the original DefineGroup function (around line 565) with:
def DefineGroup(name, src, depend, **parameters):
global Env
if Env is None:
return []
# Try to use new implementation if available
if NG_AVAILABLE and hasattr(Env, 'DefineGroup'):
return Env.DefineGroup(name, src, depend, **parameters)
# Original implementation continues below...
# [Keep all the original DefineGroup code here]
"""
# =============================================================================
# ENHANCED GetDepend FUNCTION
# =============================================================================
"""
# Replace the original GetDepend function (around line 655) with:
def GetDepend(depend):
global Env
# Try to use new implementation if available
if NG_AVAILABLE and Env and hasattr(Env, 'GetDepend'):
return Env.GetDepend(depend)
# Original implementation continues below...
# [Keep all the original GetDepend code here]
"""
# =============================================================================
# ENHANCED MergeGroup FUNCTION
# =============================================================================
"""
# Replace the original MergeGroup function (around line 700) with:
def MergeGroup(src_group, group):
# Try to use new implementation if available
if NG_AVAILABLE and Env and hasattr(Env, '__NG_Context'):
context = Env['__NG_Context']
if context:
# Register groups with new system
from ng.project import ProjectGroup
for g in group:
if 'name' in g:
pg = ProjectGroup(
name=g['name'],
sources=g.get('src', []),
dependencies=[],
environment=Env
)
context.register_project_group(pg)
# Original implementation continues below...
# [Keep all the original MergeGroup code here]
"""
# =============================================================================
# EXAMPLE USAGE IN SCONSCRIPT
# =============================================================================
def example_sconscript():
"""
Example of how to use the new features in a SConscript file.
"""
sconscript_content = '''
from building import *
# Get environment
env = GetEnvironment()
# Method 1: Use new environment methods (if available)
if hasattr(env, 'DefineGroup'):
# New OOP style
src = env.GlobFiles('*.c')
group = env.DefineGroup('MyComponent', src, depend=['RT_USING_XXX'])
else:
# Fallback to traditional style
src = Glob('*.c')
group = DefineGroup('MyComponent', src, depend=['RT_USING_XXX'])
# Method 2: Always compatible style
src = Glob('*.c')
group = DefineGroup('MyComponent', src, depend=['RT_USING_XXX'])
Return('group')
'''
return sconscript_content
# =============================================================================
# MINIMAL CHANGES SUMMARY
# =============================================================================
"""
Summary of changes needed in building.py:
1. Add imports at the beginning (5 lines)
2. Add initialization in PrepareBuilding (6 lines)
3. Add config loading after rtconfig.h parsing (3 lines)
4. Modify DefineGroup to check for new method (3 lines)
5. Modify GetDepend to check for new method (3 lines)
6. Enhance MergeGroup to register with new system (15 lines)
Total: ~35 lines of code added/modified in building.py
Benefits:
- Fully backward compatible
- Opt-in design (works even if ng module is not present)
- Gradual migration path
- No changes needed in existing SConscript files
"""
+260
View File
@@ -0,0 +1,260 @@
# -*- coding: utf-8 -*-
"""
Project and group management for RT-Thread build system.
This module provides classes for managing project groups and their compilation.
"""
import os
from typing import List, Dict, Any, Optional
from dataclasses import dataclass, field
from SCons.Script import *
@dataclass
class ProjectGroup:
"""
Represents a project group (component).
This class encapsulates the information from DefineGroup calls.
"""
name: str
sources: List[str]
dependencies: List[str] = field(default_factory=list)
environment: Any = None # SCons Environment
# Paths and defines
include_paths: List[str] = field(default_factory=list)
defines: Dict[str, str] = field(default_factory=dict)
# Compiler flags
cflags: str = ""
cxxflags: str = ""
asflags: str = ""
ldflags: str = ""
# Local options (only for this group)
local_cflags: str = ""
local_cxxflags: str = ""
local_include_paths: List[str] = field(default_factory=list)
local_defines: Dict[str, str] = field(default_factory=dict)
# Libraries
libs: List[str] = field(default_factory=list)
lib_paths: List[str] = field(default_factory=list)
# Build objects
objects: List[Any] = field(default_factory=list)
def build(self, env) -> List:
"""
Build the group and return objects.
Args:
env: SCons Environment
Returns:
List of build objects
"""
if not self.sources:
return []
# Clone environment if we have local options
build_env = env
if self._has_local_options():
build_env = env.Clone()
self._apply_local_options(build_env)
# Apply global options
self._apply_global_options(build_env)
# Build objects
self.objects = []
for src in self.sources:
if isinstance(src, str):
# Build single file
obj = build_env.Object(src)
self.objects.extend(obj if isinstance(obj, list) else [obj])
else:
# Already a Node
self.objects.append(src)
return self.objects
def _has_local_options(self) -> bool:
"""Check if group has local options."""
return bool(
self.local_cflags or
self.local_cxxflags or
self.local_include_paths or
self.local_defines
)
def _apply_local_options(self, env) -> None:
"""Apply local options to environment."""
if self.local_cflags:
env.AppendUnique(CFLAGS=self.local_cflags.split())
if self.local_cxxflags:
env.AppendUnique(CXXFLAGS=self.local_cxxflags.split())
if self.local_include_paths:
paths = [os.path.abspath(p) for p in self.local_include_paths]
env.AppendUnique(CPPPATH=paths)
if self.local_defines:
env.AppendUnique(CPPDEFINES=self.local_defines)
def _apply_global_options(self, env) -> None:
"""Apply global options to environment."""
# These options affect dependent groups too
if self.include_paths:
paths = [os.path.abspath(p) for p in self.include_paths]
env.AppendUnique(CPPPATH=paths)
if self.defines:
env.AppendUnique(CPPDEFINES=self.defines)
if self.cflags and 'CFLAGS' not in env:
env['CFLAGS'] = self.cflags
if self.cxxflags and 'CXXFLAGS' not in env:
env['CXXFLAGS'] = self.cxxflags
if self.libs:
env.AppendUnique(LIBS=self.libs)
if self.lib_paths:
paths = [os.path.abspath(p) for p in self.lib_paths]
env.AppendUnique(LIBPATH=paths)
def get_info(self) -> Dict[str, Any]:
"""
Get group information for project generators.
Returns:
Dictionary with group information
"""
return {
'name': self.name,
'sources': self.sources,
'include_paths': self.include_paths + self.local_include_paths,
'defines': {**self.defines, **self.local_defines},
'cflags': f"{self.cflags} {self.local_cflags}".strip(),
'cxxflags': f"{self.cxxflags} {self.local_cxxflags}".strip(),
'libs': self.libs,
'lib_paths': self.lib_paths
}
class ProjectRegistry:
"""
Registry for all project groups.
This class manages all registered project groups and provides
methods for querying and merging them.
"""
def __init__(self):
self.groups: List[ProjectGroup] = []
self._group_index: Dict[str, ProjectGroup] = {}
def register_group(self, group: ProjectGroup) -> None:
"""
Register a project group.
Args:
group: ProjectGroup instance
"""
self.groups.append(group)
self._group_index[group.name] = group
def get_group(self, name: str) -> Optional[ProjectGroup]:
"""
Get group by name.
Args:
name: Group name
Returns:
ProjectGroup or None
"""
return self._group_index.get(name)
def get_all_groups(self) -> List[ProjectGroup]:
"""Get all registered groups."""
return self.groups.copy()
def get_groups_by_dependency(self, dependency: str) -> List[ProjectGroup]:
"""
Get groups that depend on a specific macro.
Args:
dependency: Dependency name
Returns:
List of matching groups
"""
return [g for g in self.groups if dependency in g.dependencies]
def merge_groups(self, env) -> List:
"""
Merge all groups into a single list of objects.
Args:
env: SCons Environment
Returns:
List of all build objects
"""
all_objects = []
for group in self.groups:
if group.objects:
all_objects.extend(group.objects)
return all_objects
def get_project_info(self) -> Dict[str, Any]:
"""
Get complete project information for generators.
Returns:
Dictionary with project information
"""
# Collect all unique values
all_sources = []
all_includes = set()
all_defines = {}
all_libs = []
all_lib_paths = set()
for group in self.groups:
info = group.get_info()
# Sources
all_sources.extend(info['sources'])
# Include paths
all_includes.update(info['include_paths'])
# Defines
all_defines.update(info['defines'])
# Libraries
all_libs.extend(info['libs'])
all_lib_paths.update(info['lib_paths'])
return {
'groups': [g.get_info() for g in self.groups],
'all_sources': all_sources,
'all_includes': sorted(list(all_includes)),
'all_defines': all_defines,
'all_libs': all_libs,
'all_lib_paths': sorted(list(all_lib_paths))
}
def clear(self) -> None:
"""Clear all registered groups."""
self.groups.clear()
self._group_index.clear()
+396
View File
@@ -0,0 +1,396 @@
# -*- coding: utf-8 -*-
"""
Toolchain management for RT-Thread build system.
This module provides abstraction for different toolchains (GCC, Keil, IAR, etc.).
"""
import os
import shutil
import subprocess
from abc import ABC, abstractmethod
from typing import Dict, List, Optional, Tuple
from dataclasses import dataclass
@dataclass
class ToolchainInfo:
"""Toolchain information."""
name: str
version: str
path: str
prefix: str = ""
suffix: str = ""
class Toolchain(ABC):
"""Abstract base class for toolchains."""
def __init__(self):
self.info = None
@abstractmethod
def get_name(self) -> str:
"""Get toolchain name."""
pass
@abstractmethod
def detect(self) -> bool:
"""Detect if toolchain is available."""
pass
@abstractmethod
def configure_environment(self, env) -> None:
"""Configure SCons environment for this toolchain."""
pass
@abstractmethod
def get_compile_flags(self, cpu: str, fpu: str = None, float_abi: str = None) -> Dict[str, str]:
"""Get compilation flags for target CPU."""
pass
def get_version(self) -> Optional[str]:
"""Get toolchain version."""
return self.info.version if self.info else None
def _run_command(self, cmd: List[str]) -> Tuple[int, str, str]:
"""Run command and return (returncode, stdout, stderr)."""
try:
result = subprocess.run(cmd, capture_output=True, text=True)
return result.returncode, result.stdout, result.stderr
except Exception as e:
return -1, "", str(e)
class GccToolchain(Toolchain):
"""GCC toolchain implementation."""
def __init__(self, prefix: str = ""):
super().__init__()
self.prefix = prefix or "arm-none-eabi-"
def get_name(self) -> str:
return "gcc"
def detect(self) -> bool:
"""Detect GCC toolchain."""
gcc_path = shutil.which(self.prefix + "gcc")
if not gcc_path:
return False
# Get version
ret, stdout, _ = self._run_command([gcc_path, "--version"])
if ret == 0:
lines = stdout.split('\n')
if lines:
version = lines[0].split()[-1]
self.info = ToolchainInfo(
name="gcc",
version=version,
path=os.path.dirname(gcc_path),
prefix=self.prefix
)
return True
return False
def configure_environment(self, env) -> None:
"""Configure environment for GCC."""
env['CC'] = self.prefix + 'gcc'
env['CXX'] = self.prefix + 'g++'
env['AS'] = self.prefix + 'gcc'
env['AR'] = self.prefix + 'ar'
env['LINK'] = self.prefix + 'gcc'
env['SIZE'] = self.prefix + 'size'
env['OBJDUMP'] = self.prefix + 'objdump'
env['OBJCPY'] = self.prefix + 'objcopy'
# Set default flags
env['ARFLAGS'] = '-rc'
env['ASFLAGS'] = '-x assembler-with-cpp'
# Path
if self.info and self.info.path:
env.PrependENVPath('PATH', self.info.path)
def get_compile_flags(self, cpu: str, fpu: str = None, float_abi: str = None) -> Dict[str, str]:
"""Get GCC compilation flags."""
flags = {
'CFLAGS': [],
'CXXFLAGS': [],
'ASFLAGS': [],
'LDFLAGS': []
}
# CPU flags
cpu_flags = {
'cortex-m0': '-mcpu=cortex-m0 -mthumb',
'cortex-m0+': '-mcpu=cortex-m0plus -mthumb',
'cortex-m3': '-mcpu=cortex-m3 -mthumb',
'cortex-m4': '-mcpu=cortex-m4 -mthumb',
'cortex-m7': '-mcpu=cortex-m7 -mthumb',
'cortex-m23': '-mcpu=cortex-m23 -mthumb',
'cortex-m33': '-mcpu=cortex-m33 -mthumb',
'cortex-a7': '-mcpu=cortex-a7',
'cortex-a9': '-mcpu=cortex-a9'
}
if cpu in cpu_flags:
base_flags = cpu_flags[cpu]
for key in ['CFLAGS', 'CXXFLAGS', 'ASFLAGS']:
flags[key].append(base_flags)
# FPU flags
if fpu:
fpu_flag = f'-mfpu={fpu}'
for key in ['CFLAGS', 'CXXFLAGS']:
flags[key].append(fpu_flag)
# Float ABI
if float_abi:
abi_flag = f'-mfloat-abi={float_abi}'
for key in ['CFLAGS', 'CXXFLAGS']:
flags[key].append(abi_flag)
# Common flags
common_flags = ['-ffunction-sections', '-fdata-sections']
flags['CFLAGS'].extend(common_flags)
flags['CXXFLAGS'].extend(common_flags)
# Linker flags
flags['LDFLAGS'].extend(['-Wl,--gc-sections'])
# Convert lists to strings
return {k: ' '.join(v) for k, v in flags.items()}
class ArmccToolchain(Toolchain):
"""ARM Compiler (Keil) toolchain implementation."""
def get_name(self) -> str:
return "armcc"
def detect(self) -> bool:
"""Detect ARM Compiler toolchain."""
armcc_path = shutil.which("armcc")
if not armcc_path:
# Try common Keil installation paths
keil_paths = [
r"C:\Keil_v5\ARM\ARMCC\bin",
r"C:\Keil\ARM\ARMCC\bin",
"/opt/arm/bin"
]
for path in keil_paths:
test_path = os.path.join(path, "armcc")
if os.path.exists(test_path):
armcc_path = test_path
break
if not armcc_path:
return False
# Get version
ret, stdout, _ = self._run_command([armcc_path, "--version"])
if ret == 0:
lines = stdout.split('\n')
for line in lines:
if "ARM Compiler" in line:
version = line.split()[-1]
self.info = ToolchainInfo(
name="armcc",
version=version,
path=os.path.dirname(armcc_path)
)
return True
return False
def configure_environment(self, env) -> None:
"""Configure environment for ARM Compiler."""
env['CC'] = 'armcc'
env['CXX'] = 'armcc'
env['AS'] = 'armasm'
env['AR'] = 'armar'
env['LINK'] = 'armlink'
# ARM Compiler specific settings
env['ARCOM'] = '$AR --create $TARGET $SOURCES'
env['LIBPREFIX'] = ''
env['LIBSUFFIX'] = '.lib'
env['LIBLINKPREFIX'] = ''
env['LIBLINKSUFFIX'] = '.lib'
env['LIBDIRPREFIX'] = '--userlibpath '
# Path
if self.info and self.info.path:
env.PrependENVPath('PATH', self.info.path)
def get_compile_flags(self, cpu: str, fpu: str = None, float_abi: str = None) -> Dict[str, str]:
"""Get ARM Compiler flags."""
flags = {
'CFLAGS': [],
'CXXFLAGS': [],
'ASFLAGS': [],
'LDFLAGS': []
}
# CPU selection
cpu_map = {
'cortex-m0': '--cpu Cortex-M0',
'cortex-m0+': '--cpu Cortex-M0+',
'cortex-m3': '--cpu Cortex-M3',
'cortex-m4': '--cpu Cortex-M4',
'cortex-m7': '--cpu Cortex-M7'
}
if cpu in cpu_map:
cpu_flag = cpu_map[cpu]
for key in flags:
flags[key].append(cpu_flag)
# Common flags
flags['CFLAGS'].extend(['--c99', '--gnu'])
flags['CXXFLAGS'].extend(['--cpp', '--gnu'])
return {k: ' '.join(v) for k, v in flags.items()}
class IarToolchain(Toolchain):
"""IAR toolchain implementation."""
def get_name(self) -> str:
return "iar"
def detect(self) -> bool:
"""Detect IAR toolchain."""
iccarm_path = shutil.which("iccarm")
if not iccarm_path:
# Try common IAR installation paths
iar_paths = [
r"C:\Program Files (x86)\IAR Systems\Embedded Workbench 8.0\arm\bin",
r"C:\Program Files\IAR Systems\Embedded Workbench 8.0\arm\bin",
"/opt/iar/bin"
]
for path in iar_paths:
test_path = os.path.join(path, "iccarm.exe" if os.name == 'nt' else "iccarm")
if os.path.exists(test_path):
iccarm_path = test_path
break
if not iccarm_path:
return False
self.info = ToolchainInfo(
name="iar",
version="8.x", # IAR version detection is complex
path=os.path.dirname(iccarm_path)
)
return True
def configure_environment(self, env) -> None:
"""Configure environment for IAR."""
env['CC'] = 'iccarm'
env['CXX'] = 'iccarm'
env['AS'] = 'iasmarm'
env['AR'] = 'iarchive'
env['LINK'] = 'ilinkarm'
# IAR specific settings
env['LIBPREFIX'] = ''
env['LIBSUFFIX'] = '.a'
env['LIBLINKPREFIX'] = ''
env['LIBLINKSUFFIX'] = '.a'
# Path
if self.info and self.info.path:
env.PrependENVPath('PATH', self.info.path)
def get_compile_flags(self, cpu: str, fpu: str = None, float_abi: str = None) -> Dict[str, str]:
"""Get IAR flags."""
flags = {
'CFLAGS': [],
'CXXFLAGS': [],
'ASFLAGS': [],
'LDFLAGS': []
}
# CPU selection
cpu_map = {
'cortex-m0': '--cpu=Cortex-M0',
'cortex-m0+': '--cpu=Cortex-M0+',
'cortex-m3': '--cpu=Cortex-M3',
'cortex-m4': '--cpu=Cortex-M4',
'cortex-m7': '--cpu=Cortex-M7'
}
if cpu in cpu_map:
cpu_flag = cpu_map[cpu]
flags['CFLAGS'].append(cpu_flag)
flags['CXXFLAGS'].append(cpu_flag)
# Common flags
flags['CFLAGS'].extend(['-e', '--dlib_config', 'DLib_Config_Normal.h'])
return {k: ' '.join(v) for k, v in flags.items()}
class ToolchainManager:
"""Manager for toolchain selection and configuration."""
def __init__(self):
self.toolchains: Dict[str, Toolchain] = {}
self.current_toolchain: Optional[Toolchain] = None
self._register_default_toolchains()
def _register_default_toolchains(self) -> None:
"""Register default toolchains."""
# Try to detect available toolchains
toolchain_classes = [
(GccToolchain, ['arm-none-eabi-', 'riscv32-unknown-elf-', 'riscv64-unknown-elf-']),
(ArmccToolchain, ['']),
(IarToolchain, [''])
]
for toolchain_class, prefixes in toolchain_classes:
for prefix in prefixes:
if toolchain_class == GccToolchain:
tc = toolchain_class(prefix)
else:
tc = toolchain_class()
if tc.detect():
name = f"{tc.get_name()}-{prefix}" if prefix else tc.get_name()
self.register_toolchain(name, tc)
def register_toolchain(self, name: str, toolchain: Toolchain) -> None:
"""Register a toolchain."""
self.toolchains[name] = toolchain
def select_toolchain(self, name: str) -> Toolchain:
"""Select a toolchain by name."""
if name not in self.toolchains:
# Try to create it
if name == 'gcc':
tc = GccToolchain()
elif name == 'armcc' or name == 'keil':
tc = ArmccToolchain()
elif name == 'iar':
tc = IarToolchain()
else:
raise ValueError(f"Unknown toolchain: {name}")
if tc.detect():
self.register_toolchain(name, tc)
else:
raise RuntimeError(f"Toolchain '{name}' not found")
self.current_toolchain = self.toolchains[name]
return self.current_toolchain
def get_current(self) -> Optional[Toolchain]:
"""Get current toolchain."""
return self.current_toolchain
def list_toolchains(self) -> List[str]:
"""List available toolchains."""
return list(self.toolchains.keys())
+339
View File
@@ -0,0 +1,339 @@
# -*- coding: utf-8 -*-
"""
Utility functions for RT-Thread build system.
This module provides common utility functions used throughout the build system.
"""
import os
import sys
import platform
from typing import List, Tuple, Optional
class PathService:
"""Service for path manipulation and normalization."""
def __init__(self, base_path: str = None):
self.base_path = base_path or os.getcwd()
def normalize_path(self, path: str) -> str:
"""
Normalize path for cross-platform compatibility.
Args:
path: Path to normalize
Returns:
Normalized path
"""
# Convert to absolute path if relative
if not os.path.isabs(path):
path = os.path.abspath(os.path.join(self.base_path, path))
# Normalize separators
path = os.path.normpath(path)
# Convert to forward slashes for consistency
if platform.system() == 'Windows':
path = path.replace('\\', '/')
return path
def make_relative(self, path: str, base: str = None) -> str:
"""
Make path relative to base.
Args:
path: Path to make relative
base: Base path (defaults to self.base_path)
Returns:
Relative path
"""
if base is None:
base = self.base_path
path = self.normalize_path(path)
base = self.normalize_path(base)
try:
rel_path = os.path.relpath(path, base)
# Convert to forward slashes
if platform.system() == 'Windows':
rel_path = rel_path.replace('\\', '/')
return rel_path
except ValueError:
# Different drives on Windows
return path
def split_path(self, path: str) -> List[str]:
"""
Split path into components.
Args:
path: Path to split
Returns:
List of path components
"""
path = self.normalize_path(path)
parts = []
while True:
head, tail = os.path.split(path)
if tail:
parts.insert(0, tail)
if head == path: # Reached root
if head:
parts.insert(0, head)
break
path = head
return parts
def common_prefix(self, paths: List[str]) -> str:
"""
Find common prefix of multiple paths.
Args:
paths: List of paths
Returns:
Common prefix path
"""
if not paths:
return ""
# Normalize all paths
normalized = [self.normalize_path(p) for p in paths]
# Find common prefix
prefix = os.path.commonpath(normalized)
return self.normalize_path(prefix)
class PlatformInfo:
"""Platform and system information."""
@staticmethod
def get_platform() -> str:
"""Get platform name (Windows, Linux, Darwin)."""
return platform.system()
@staticmethod
def get_architecture() -> str:
"""Get system architecture."""
return platform.machine()
@staticmethod
def is_windows() -> bool:
"""Check if running on Windows."""
return platform.system() == 'Windows'
@staticmethod
def is_linux() -> bool:
"""Check if running on Linux."""
return platform.system() == 'Linux'
@staticmethod
def is_macos() -> bool:
"""Check if running on macOS."""
return platform.system() == 'Darwin'
@staticmethod
def get_python_version() -> Tuple[int, int, int]:
"""Get Python version tuple."""
return sys.version_info[:3]
@staticmethod
def check_python_version(min_version: Tuple[int, int]) -> bool:
"""
Check if Python version meets minimum requirement.
Args:
min_version: Minimum version tuple (major, minor)
Returns:
True if version is sufficient
"""
current = sys.version_info[:2]
return current >= min_version
class FileUtils:
"""File operation utilities."""
@staticmethod
def read_file(filepath: str, encoding: str = 'utf-8') -> str:
"""
Read file content.
Args:
filepath: File path
encoding: File encoding
Returns:
File content
"""
with open(filepath, 'r', encoding=encoding) as f:
return f.read()
@staticmethod
def write_file(filepath: str, content: str, encoding: str = 'utf-8') -> None:
"""
Write content to file.
Args:
filepath: File path
content: Content to write
encoding: File encoding
"""
# Ensure directory exists
directory = os.path.dirname(filepath)
if directory:
os.makedirs(directory, exist_ok=True)
with open(filepath, 'w', encoding=encoding) as f:
f.write(content)
@staticmethod
def copy_file(src: str, dst: str) -> None:
"""
Copy file from src to dst.
Args:
src: Source file path
dst: Destination file path
"""
import shutil
# Ensure destination directory exists
dst_dir = os.path.dirname(dst)
if dst_dir:
os.makedirs(dst_dir, exist_ok=True)
shutil.copy2(src, dst)
@staticmethod
def find_files(directory: str, pattern: str, recursive: bool = True) -> List[str]:
"""
Find files matching pattern.
Args:
directory: Directory to search
pattern: File pattern (supports wildcards)
recursive: Search recursively
Returns:
List of matching file paths
"""
import fnmatch
matches = []
if recursive:
for root, dirnames, filenames in os.walk(directory):
for filename in filenames:
if fnmatch.fnmatch(filename, pattern):
matches.append(os.path.join(root, filename))
else:
try:
filenames = os.listdir(directory)
for filename in filenames:
if fnmatch.fnmatch(filename, pattern):
filepath = os.path.join(directory, filename)
if os.path.isfile(filepath):
matches.append(filepath)
except OSError:
pass
return sorted(matches)
class VersionUtils:
"""Version comparison utilities."""
@staticmethod
def parse_version(version_str: str) -> Tuple[int, ...]:
"""
Parse version string to tuple.
Args:
version_str: Version string (e.g., "1.2.3")
Returns:
Version tuple
"""
try:
parts = version_str.split('.')
return tuple(int(p) for p in parts if p.isdigit())
except (ValueError, AttributeError):
return (0,)
@staticmethod
def compare_versions(v1: str, v2: str) -> int:
"""
Compare two version strings.
Args:
v1: First version
v2: Second version
Returns:
-1 if v1 < v2, 0 if equal, 1 if v1 > v2
"""
t1 = VersionUtils.parse_version(v1)
t2 = VersionUtils.parse_version(v2)
# Pad shorter version with zeros
if len(t1) < len(t2):
t1 = t1 + (0,) * (len(t2) - len(t1))
elif len(t2) < len(t1):
t2 = t2 + (0,) * (len(t1) - len(t2))
if t1 < t2:
return -1
elif t1 > t2:
return 1
else:
return 0
@staticmethod
def version_satisfies(version: str, requirement: str) -> bool:
"""
Check if version satisfies requirement.
Args:
version: Version string
requirement: Requirement string (e.g., ">=1.2.0")
Returns:
True if satisfied
"""
import re
# Parse requirement
match = re.match(r'([<>=]+)\s*(.+)', requirement)
if not match:
# Exact match required
return version == requirement
op, req_version = match.groups()
cmp = VersionUtils.compare_versions(version, req_version)
if op == '>=':
return cmp >= 0
elif op == '<=':
return cmp <= 0
elif op == '>':
return cmp > 0
elif op == '<':
return cmp < 0
elif op == '==':
return cmp == 0
elif op == '!=':
return cmp != 0
else:
return False