first commit for chrg
This commit is contained in:
@@ -0,0 +1,345 @@
|
||||
# RT-Thread Next Generation Build System
|
||||
|
||||
## 概述
|
||||
|
||||
RT-Thread NG(Next 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许可证。
|
||||
@@ -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'
|
||||
]
|
||||
@@ -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)
|
||||
@@ -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('_')]
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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', '')
|
||||
@@ -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())
|
||||
@@ -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
|
||||
"""
|
||||
@@ -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()
|
||||
@@ -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())
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user