diff --git a/tools/docs/README.md b/tools/docs/README.md new file mode 100644 index 00000000000..a1c34073cb3 --- /dev/null +++ b/tools/docs/README.md @@ -0,0 +1,136 @@ +# RT-Thread 构建系统文档 + +欢迎使用RT-Thread构建系统文档。本文档集详细介绍了RT-Thread基于SCons的构建系统的使用方法和技术原理。 + +## 文档目录 + +### 📚 用户指南 + +1. **[构建系统使用指南](构建系统使用指南.md)** + - 快速开始 + - 命令行选项详解 + - 工具链配置 + - 项目生成 + - 软件包管理 + - 高级功能 + - 常见问题解答 + +2. **[SConscript编写指南](SConscript编写指南.md)** + - 基础语法 + - 常用模式 + - 高级技巧 + - 最佳实践 + - 示例集合 + +### 🔧 技术文档 + +3. **[构建系统技术原理](构建系统技术原理.md)** + - 系统架构设计 + - 核心模块分析 + - 构建流程详解 + - 依赖管理机制 + - 工具链适配层 + - 项目生成器架构 + - 扩展机制 + +## 快速导航 + +### 常用命令 + +```bash +# 基础编译 +scons # 默认编译 +scons -j8 # 8线程并行编译 +scons -c # 清理编译产物 + +# 配置管理 +menuconfig # 图形化配置 +scons --pyconfig # Python脚本配置 + +# 项目生成 +scons --target=mdk5 # 生成Keil MDK5项目 +scons --target=iar # 生成IAR项目 +scons --target=vsc # 生成VS Code项目 +scons --target=cmake # 生成CMake项目 + +# 软件包管理 +pkgs --update # 更新软件包 +pkgs --list # 列出已安装包 +``` + +### 核心概念 + +- **SConstruct**: BSP根目录的主构建脚本 +- **SConscript**: 各个组件/目录的构建脚本 +- **rtconfig.py**: 工具链和平台配置 +- **rtconfig.h**: RT-Thread功能配置 +- **DefineGroup**: 定义组件的核心函数 +- **GetDepend**: 检查依赖的核心函数 + +## 构建系统架构图 + +![arch](./readme_arch.drawio.png) + +## 主要特性 + +✅ **多工具链支持** +- GCC (ARM/RISC-V/x86) +- Keil MDK (ARMCC/ARMClang) +- IAR +- Visual Studio + +✅ **灵活的配置系统** +- Kconfig图形配置 +- 条件编译支持 +- 本地编译选项 + +✅ **丰富的项目生成器** +- IDE项目文件生成 +- CMake支持 +- Makefile生成 +- VS Code配置 + +✅ **模块化设计** +- 组件独立构建 +- 清晰的依赖管理 +- 可扩展架构 + +## 开发工作流 + +```mermaid +graph LR + A[配置系统] --> B[编写代码] + B --> C[构建项目] + C --> D[调试运行] + D --> E{是否完成?} + E -->|否| B + E -->|是| F[发布] + + A1[menuconfig] -.-> A + C1[scons] -.-> C + C2[IDE项目] -.-> C +``` + +## 相关链接 + +- [RT-Thread官网](https://www.rt-thread.org) +- [RT-Thread GitHub](https://github.com/RT-Thread/rt-thread) +- [SCons官方文档](https://scons.org/documentation.html) + +## 贡献指南 + +如果您发现文档中的错误或有改进建议,欢迎: + +1. 在GitHub上提交Issue +2. 提交Pull Request +3. 在RT-Thread社区论坛反馈 + +## 版本信息 + +- 文档版本:1.0.0 +- 更新日期:2024-01 +- 适用版本:RT-Thread 4.1.0+ + +--- + +**注意**:本文档基于RT-Thread最新版本编写,部分功能可能需要特定版本支持。使用前请确认您的RT-Thread版本。 \ No newline at end of file diff --git "a/tools/docs/SConscript\347\274\226\345\206\231\346\214\207\345\215\227.md" "b/tools/docs/SConscript\347\274\226\345\206\231\346\214\207\345\215\227.md" new file mode 100644 index 00000000000..f7d7490b4bb --- /dev/null +++ "b/tools/docs/SConscript\347\274\226\345\206\231\346\214\207\345\215\227.md" @@ -0,0 +1,948 @@ +# RT-Thread SConscript 编写指南 + +## 目录 + +1. [概述](#概述) +2. [基础语法](#基础语法) +3. [常用模式](#常用模式) +4. [高级技巧](#高级技巧) +5. [最佳实践](#最佳实践) +6. [示例集合](#示例集合) +7. [常见问题](#常见问题) + +## 概述 + +SConscript是RT-Thread构建系统中的模块构建脚本,每个组件或目录都可以有自己的SConscript文件。本指南将详细介绍如何编写高质量的SConscript文件。 + +### SConscript在构建系统中的位置 + +``` +项目根目录/ +├── SConstruct # 主构建脚本 +├── applications/ +│ └── SConscript # 应用层构建脚本 +├── drivers/ +│ └── SConscript # 驱动层构建脚本 +└── components/ + ├── SConscript # 组件主脚本 + └── finsh/ + └── SConscript # 子组件脚本 +``` + +## 基础语法 + +### 1. 基本结构 + +```python +# 导入构建模块 +from building import * + +# 获取当前目录 +cwd = GetCurrentDir() + +# 定义源文件 +src = ['main.c', 'app.c'] + +# 定义头文件路径 +CPPPATH = [cwd] + +# 定义组 +group = DefineGroup('Applications', src, depend = [''], CPPPATH = CPPPATH) + +# 返回组对象 +Return('group') +``` + +### 2. 导入building模块 + +```python +from building import * +``` + +这行代码导入了RT-Thread构建系统的所有函数,包括: +- `GetCurrentDir()` - 获取当前目录 +- `DefineGroup()` - 定义组件组 +- `GetDepend()` - 检查依赖 +- `Glob()` - 文件通配符匹配 +- `SrcRemove()` - 移除源文件 +- `DoBuilding()` - 执行构建 + +### 3. 源文件定义 + +#### 手动指定文件列表 +```python +src = ['file1.c', 'file2.c', 'file3.c'] +``` + +#### 使用通配符 +```python +src = Glob('*.c') # 当前目录所有.c文件 +src = Glob('src/*.c') # src子目录所有.c文件 +src = Glob('**/*.c') # 递归所有子目录的.c文件 +``` + +#### 混合使用 +```python +src = ['main.c'] + Glob('drivers/*.c') +``` + +### 4. 条件编译 + +#### 基于宏定义 +```python +src = ['common.c'] + +if GetDepend('RT_USING_SERIAL'): + src += ['serial.c'] + +if GetDepend('RT_USING_I2C'): + src += ['i2c.c'] +``` + +#### 基于平台 +```python +import rtconfig + +if rtconfig.PLATFORM == 'gcc': + src += ['gcc_specific.c'] +elif rtconfig.PLATFORM == 'armcc': + src += ['keil_specific.c'] +``` + +### 5. 移除文件 + +```python +src = Glob('*.c') +SrcRemove(src, ['test.c', 'debug.c']) # 移除不需要的文件 +``` + +## 常用模式 + +### 1. 简单组件模式 + +最基础的SConscript模式: + +```python +from building import * + +src = Glob('*.c') +CPPPATH = [GetCurrentDir()] + +group = DefineGroup('MyComponent', src, depend = ['RT_USING_MYCOMPONENT'], + CPPPATH = CPPPATH) + +Return('group') +``` + +### 2. 条件编译模式 + +根据配置选项包含不同的源文件: + +```python +from building import * + +src = ['core.c'] +CPPPATH = [GetCurrentDir()] + +# 功能模块条件编译 +if GetDepend('MYCOMPONENT_USING_FEATURE_A'): + src += ['feature_a.c'] + +if GetDepend('MYCOMPONENT_USING_FEATURE_B'): + src += ['feature_b.c'] + +# 平台相关代码 +if rtconfig.PLATFORM == 'gcc': + src += ['port_gcc.c'] +elif rtconfig.PLATFORM == 'armcc': + src += ['port_keil.c'] +elif rtconfig.PLATFORM == 'iccarm': + src += ['port_iar.c'] + +group = DefineGroup('MyComponent', src, depend = ['RT_USING_MYCOMPONENT'], + CPPPATH = CPPPATH) + +Return('group') +``` + +### 3. 多目录组织模式 + +处理复杂的目录结构: + +```python +from building import * + +cwd = GetCurrentDir() + +# 源文件来自多个目录 +src = Glob('src/*.c') +src += Glob('port/*.c') +src += Glob('hal/*.c') + +# 多个头文件路径 +CPPPATH = [ + cwd, + cwd + '/include', + cwd + '/internal', + cwd + '/port' +] + +# 根据配置添加特定目录 +if GetDepend('RT_USING_LWIP'): + src += Glob('lwip/*.c') + CPPPATH += [cwd + '/lwip'] + +group = DefineGroup('Network', src, depend = ['RT_USING_NETWORK'], + CPPPATH = CPPPATH) + +Return('group') +``` + +### 4. 递归子目录模式 + +自动处理所有子目录的SConscript: + +```python +import os +from building import * + +objs = [] +cwd = GetCurrentDir() + +# 获取所有子目录 +list = os.listdir(cwd) + +# 需要跳过的目录 +skip_dirs = ['test', 'doc', 'examples', '.git'] + +for d in list: + path = os.path.join(cwd, d) + # 检查是否是目录且包含SConscript + if os.path.isdir(path) and d not in skip_dirs: + if os.path.isfile(os.path.join(path, 'SConscript')): + objs = objs + SConscript(os.path.join(d, 'SConscript')) + +Return('objs') +``` + +### 5. 库文件链接模式 + +链接预编译的库文件: + +```python +from building import * +import os + +cwd = GetCurrentDir() + +# 只包含必要的接口文件 +src = ['lib_interface.c'] + +CPPPATH = [cwd + '/include'] + +# 库文件配置 +LIBS = [] +LIBPATH = [] + +# 根据架构选择库文件 +import rtconfig +if rtconfig.ARCH == 'arm': + if rtconfig.CPU == 'cortex-m4': + LIBS += ['mylib_cm4'] + LIBPATH += [cwd + '/lib/cortex-m4'] + elif rtconfig.CPU == 'cortex-m3': + LIBS += ['mylib_cm3'] + LIBPATH += [cwd + '/lib/cortex-m3'] + +group = DefineGroup('MyLib', src, depend = ['RT_USING_MYLIB'], + CPPPATH = CPPPATH, LIBS = LIBS, LIBPATH = LIBPATH) + +Return('group') +``` + +## 高级技巧 + +### 1. 本地编译选项 + +为特定模块设置独立的编译选项: + +```python +from building import * + +src = Glob('*.c') +CPPPATH = [GetCurrentDir()] + +# 全局编译选项(影响依赖此组件的其他组件) +CPPDEFINES = ['GLOBAL_DEFINE'] + +# 本地编译选项(仅影响当前组件) +LOCAL_CFLAGS = '' +LOCAL_CPPDEFINES = ['LOCAL_DEFINE'] +LOCAL_CPPPATH = ['./private'] + +# 根据编译器设置优化选项 +import rtconfig +if rtconfig.PLATFORM == 'gcc': + LOCAL_CFLAGS += ' -O3 -funroll-loops' +elif rtconfig.PLATFORM == 'armcc': + LOCAL_CFLAGS += ' -O3 --loop_optimization_level=2' + +group = DefineGroup('HighPerf', src, depend = ['RT_USING_HIGHPERF'], + CPPPATH = CPPPATH, + CPPDEFINES = CPPDEFINES, + LOCAL_CFLAGS = LOCAL_CFLAGS, + LOCAL_CPPDEFINES = LOCAL_CPPDEFINES, + LOCAL_CPPPATH = LOCAL_CPPPATH +) + +Return('group') +``` + +### 2. 动态源文件生成 + +在构建时生成源文件: + +```python +from building import * +import time + +def generate_version_file(): + """生成版本信息文件""" + version_c = ''' +/* Auto-generated file, do not edit! */ +#include "version.h" + +const char *build_time = "%s"; +const char *version = "%s"; +''' % (time.strftime('%Y-%m-%d %H:%M:%S'), '1.0.0') + + with open('version_gen.c', 'w') as f: + f.write(version_c) + +# 生成文件 +generate_version_file() + +src = ['main.c', 'version_gen.c'] +CPPPATH = [GetCurrentDir()] + +group = DefineGroup('App', src, depend = [''], CPPPATH = CPPPATH) + +Return('group') +``` + +### 3. 复杂依赖处理 + +处理复杂的依赖关系: + +```python +from building import * + +src = [] +CPPPATH = [GetCurrentDir()] +CPPDEFINES = [] + +# 基础功能 +if GetDepend('RT_USING_DEVICE'): + src += ['device_core.c'] + + # 串口驱动(依赖设备框架) + if GetDepend('RT_USING_SERIAL'): + src += ['serial.c'] + + # 串口DMA(依赖串口驱动) + if GetDepend('RT_SERIAL_USING_DMA'): + src += ['serial_dma.c'] + CPPDEFINES += ['SERIAL_USING_DMA'] + + # SPI驱动(依赖设备框架) + if GetDepend('RT_USING_SPI'): + src += ['spi.c'] + + # SPI DMA(依赖SPI驱动) + if GetDepend('RT_SPI_USING_DMA'): + src += ['spi_dma.c'] + +# 错误检查 +if GetDepend('RT_SERIAL_USING_DMA') and not GetDepend('RT_USING_SERIAL'): + print('Error: RT_SERIAL_USING_DMA requires RT_USING_SERIAL!') + exit(1) + +group = DefineGroup('Drivers', src, depend = ['RT_USING_DEVICE'], + CPPPATH = CPPPATH, CPPDEFINES = CPPDEFINES) + +Return('group') +``` + +### 4. 平台特定实现 + +根据不同平台包含不同实现: + +```python +from building import * +import rtconfig + +cwd = GetCurrentDir() +src = ['common.c'] +CPPPATH = [cwd, cwd + '/include'] + +# 架构相关目录映射 +arch_map = { + 'arm': { + 'cortex-m3': 'arm/cortex-m3', + 'cortex-m4': 'arm/cortex-m4', + 'cortex-m7': 'arm/cortex-m7', + 'cortex-a': 'arm/cortex-a' + }, + 'risc-v': { + 'rv32': 'riscv/rv32', + 'rv64': 'riscv/rv64' + }, + 'x86': { + 'i386': 'x86/i386', + 'x86_64': 'x86/x86_64' + } +} + +# 根据架构和CPU选择实现 +if hasattr(rtconfig, 'ARCH') and hasattr(rtconfig, 'CPU'): + arch = rtconfig.ARCH + cpu = rtconfig.CPU + + if arch in arch_map and cpu in arch_map[arch]: + port_dir = arch_map[arch][cpu] + port_src = Glob(port_dir + '/*.c') + port_src += Glob(port_dir + '/*.S') + src += port_src + CPPPATH += [cwd + '/' + port_dir] + else: + print('Warning: No port for %s - %s' % (arch, cpu)) + +group = DefineGroup('MyDriver', src, depend = ['RT_USING_MYDRIVER'], + CPPPATH = CPPPATH) + +Return('group') +``` + +### 5. 第三方库集成 + +集成第三方库的模板: + +```python +from building import * +import os + +cwd = GetCurrentDir() + +# 第三方库路径 +lib_path = cwd + '/3rdparty/libfoo' + +# 检查库是否存在 +if not os.path.exists(lib_path): + print('Error: libfoo not found at', lib_path) + print('Please run: git submodule update --init') + Return('group') + +# 库源文件 +src = Glob(lib_path + '/src/*.c') + +# 移除不需要的文件 +SrcRemove(src, [ + lib_path + '/src/test.c', + lib_path + '/src/example.c' +]) + +# 头文件路径 +CPPPATH = [ + lib_path + '/include', + cwd + '/port' # 移植层头文件 +] + +# 添加移植层 +src += Glob('port/*.c') + +# 配置宏定义 +CPPDEFINES = ['LIBFOO_RTOS_RTTHREAD'] + +# 根据配置启用功能 +if GetDepend('LIBFOO_ENABLE_FLOAT'): + CPPDEFINES += ['LIBFOO_USE_FLOAT'] + +if GetDepend('LIBFOO_ENABLE_STDIO'): + CPPDEFINES += ['LIBFOO_USE_STDIO'] + +group = DefineGroup('libfoo', src, depend = ['RT_USING_LIBFOO'], + CPPPATH = CPPPATH, CPPDEFINES = CPPDEFINES) + +Return('group') +``` + +### 6. 条件导出符号 + +根据配置导出不同的API: + +```python +from building import * + +src = ['core.c'] +CPPPATH = [GetCurrentDir()] + +# API版本控制 +if GetDepend('RT_USING_MODULE_API_V2'): + src += ['api_v2.c'] + CPPDEFINES = ['MODULE_API_VERSION=2'] +else: + src += ['api_v1.c'] + CPPDEFINES = ['MODULE_API_VERSION=1'] + +# 根据配置级别导出不同功能 +if GetDepend('MODULE_EXPERT_MODE'): + src += ['expert_api.c'] + CPPDEFINES += ['EXPORT_EXPERT_API'] + +group = DefineGroup('Module', src, depend = ['RT_USING_MODULE'], + CPPPATH = CPPPATH, CPPDEFINES = CPPDEFINES) + +Return('group') +``` + +## 最佳实践 + +### 1. 文件组织原则 + +``` +component/ +├── SConscript # 主构建脚本 +├── Kconfig # 配置选项 +├── README.md # 组件说明 +├── include/ # 公开头文件 +│ └── component.h +├── src/ # 源文件 +│ ├── core.c +│ └── utils.c +├── port/ # 平台相关代码 +│ ├── cortex-m/ +│ └── risc-v/ +└── examples/ # 示例代码 + └── example.c +``` + +### 2. 依赖管理最佳实践 + +```python +from building import * + +# 1. 明确声明依赖 +REQUIRED_DEPS = ['RT_USING_DEVICE', 'RT_USING_HEAP'] + +# 2. 检查必要依赖 +for dep in REQUIRED_DEPS: + if not GetDepend(dep): + print('Error: %s requires %s' % ('MyComponent', dep)) + Return('group') + +# 3. 可选依赖 +src = ['core.c'] + +# 可选功能 +OPTIONAL_FEATURES = { + 'RT_MYCOMPONENT_USING_DMA': 'dma.c', + 'RT_MYCOMPONENT_USING_INTERRUPT': 'interrupt.c', + 'RT_MYCOMPONENT_USING_STATS': 'statistics.c' +} + +for macro, file in OPTIONAL_FEATURES.items(): + if GetDepend(macro): + src += [file] + +group = DefineGroup('MyComponent', src, depend = ['RT_USING_MYCOMPONENT']) +Return('group') +``` + +### 3. 错误处理 + +```python +from building import * +import os + +# 检查关键文件 +critical_files = ['config.h', 'version.h'] +for f in critical_files: + if not os.path.exists(f): + print('Error: Missing required file:', f) + # 返回空组,不中断整体构建 + group = DefineGroup('MyComponent', [], depend = ['']) + Return('group') + +# 检查工具链 +import rtconfig +supported_toolchains = ['gcc', 'armcc', 'iar'] +if rtconfig.PLATFORM not in supported_toolchains: + print('Warning: Toolchain %s not tested' % rtconfig.PLATFORM) + +# 正常构建流程 +src = Glob('*.c') +group = DefineGroup('MyComponent', src, depend = ['RT_USING_MYCOMPONENT']) +Return('group') +``` + +### 4. 性能优化 + +```python +from building import * +import os + +# 1. 缓存文件列表(避免重复扫描) +_file_cache = {} + +def cached_glob(pattern): + if pattern not in _file_cache: + _file_cache[pattern] = Glob(pattern) + return _file_cache[pattern] + +# 2. 延迟加载(仅在需要时扫描) +src = [] +if GetDepend('RT_USING_MYCOMPONENT'): + src = cached_glob('*.c') + + if GetDepend('RT_MYCOMPONENT_USING_EXTRA'): + src += cached_glob('extra/*.c') + +# 3. 避免深度递归(使用显式路径) +# 不好的做法 +# src = Glob('**/*.c') # 递归所有子目录 + +# 好的做法 +src = Glob('*.c') +src += Glob('core/*.c') +src += Glob('hal/*.c') + +group = DefineGroup('MyComponent', src, depend = ['RT_USING_MYCOMPONENT']) +Return('group') +``` + +### 5. 文档化 + +```python +""" +SConscript for MyComponent + +This component provides [功能描述] + +Configuration: + RT_USING_MYCOMPONENT - Enable this component + RT_MYCOMPONENT_USING_DMA - Enable DMA support + RT_MYCOMPONENT_BUFFER_SIZE - Buffer size (default: 256) + +Dependencies: + - RT_USING_DEVICE (required) + - RT_USING_DMA (optional, for DMA support) +""" + +from building import * + +# ... 构建脚本内容 ... +``` + +## 示例集合 + +### 示例1:设备驱动SConscript + +```python +from building import * + +cwd = GetCurrentDir() + +# 驱动源文件 +src = [] +CPPPATH = [cwd + '/../inc'] + +# I2C驱动 +if GetDepend('RT_USING_I2C'): + src += ['drv_i2c.c'] + +# SPI驱动 +if GetDepend('RT_USING_SPI'): + src += ['drv_spi.c'] + + # QSPI支持 + if GetDepend('RT_USING_QSPI'): + src += ['drv_qspi.c'] + +# USB驱动 +if GetDepend('RT_USING_USB'): + src += ['drv_usb.c'] + if GetDepend('RT_USING_USB_HOST'): + src += ['drv_usbh.c'] + if GetDepend('RT_USING_USB_DEVICE'): + src += ['drv_usbd.c'] + +# SDIO驱动 +if GetDepend('RT_USING_SDIO'): + src += ['drv_sdio.c'] + +group = DefineGroup('Drivers', src, depend = [''], CPPPATH = CPPPATH) + +Return('group') +``` + +### 示例2:网络组件SConscript + +```python +from building import * + +cwd = GetCurrentDir() +src = [] +CPPPATH = [cwd] + +# 网络接口层 +if GetDepend('RT_USING_NETDEV'): + src += Glob('netdev/*.c') + CPPPATH += [cwd + '/netdev'] + +# SAL套接字抽象层 +if GetDepend('RT_USING_SAL'): + src += Glob('sal/src/*.c') + src += Glob('sal/socket/*.c') + CPPPATH += [cwd + '/sal/include'] + + # AT指令支持 + if GetDepend('SAL_USING_AT'): + src += Glob('sal/impl/af_inet_at.c') + + # LwIP支持 + if GetDepend('SAL_USING_LWIP'): + src += Glob('sal/impl/af_inet_lwip.c') + +# AT指令框架 +if GetDepend('RT_USING_AT'): + src += Glob('at/src/*.c') + CPPPATH += [cwd + '/at/include'] + + # AT Socket + if GetDepend('AT_USING_SOCKET'): + src += Glob('at/at_socket/*.c') + +group = DefineGroup('Network', src, depend = [''], CPPPATH = CPPPATH) + +Return('group') +``` + +### 示例3:文件系统SConscript + +```python +from building import * + +cwd = GetCurrentDir() +src = [] +CPPPATH = [cwd + '/include'] + +# DFS框架 +if GetDepend('RT_USING_DFS'): + src += Glob('src/*.c') + + # ELM FatFS + if GetDepend('RT_USING_DFS_ELMFAT'): + src += Glob('filesystems/elmfat/*.c') + # FatFS版本选择 + if GetDepend('RT_DFS_ELM_USE_LFN'): + src += ['filesystems/elmfat/ffunicode.c'] + + # ROMFS + if GetDepend('RT_USING_DFS_ROMFS'): + src += ['filesystems/romfs/dfs_romfs.c'] + + # DevFS + if GetDepend('RT_USING_DFS_DEVFS'): + src += ['filesystems/devfs/devfs.c'] + + # NFS + if GetDepend('RT_USING_DFS_NFS'): + src += Glob('filesystems/nfs/*.c') + CPPPATH += [cwd + '/filesystems/nfs'] + +group = DefineGroup('Filesystem', src, depend = ['RT_USING_DFS'], + CPPPATH = CPPPATH) + +Return('group') +``` + +### 示例4:使用package.json的SConscript + +```python +from building import * +import os +import json + +cwd = GetCurrentDir() + +# 尝试使用package.json +package_file = os.path.join(cwd, 'package.json') +if os.path.exists(package_file): + # 使用自动构建 + objs = BuildPackage(package_file) +else: + # 手动构建 + src = Glob('src/*.c') + CPPPATH = [cwd + '/include'] + + # 读取配置 + config_file = os.path.join(cwd, 'config.json') + if os.path.exists(config_file): + with open(config_file, 'r') as f: + config = json.load(f) + + # 根据配置添加源文件 + for feature in config.get('features', []): + if GetDepend('RT_USING_' + feature.upper()): + src += Glob('src/%s/*.c' % feature) + + objs = DefineGroup('MyPackage', src, depend = ['RT_USING_MYPACKAGE'], + CPPPATH = CPPPATH) + +Return('objs') +``` + +## 常见问题 + +### Q1: 如何调试SConscript? + +```python +from building import * + +# 1. 打印调试信息 +print('Current directory:', GetCurrentDir()) +print('GetDepend RT_USING_XXX:', GetDepend('RT_USING_XXX')) + +# 2. 打印源文件列表 +src = Glob('*.c') +print('Source files:', src) + +# 3. 条件调试输出 +if GetOption('verbose'): + print('Detailed debug info...') + +# 4. 检查环境变量 +import os +print('RTT_ROOT:', os.getenv('RTT_ROOT')) +``` + +### Q2: 如何处理可选的依赖? + +```python +from building import * + +src = ['core.c'] + +# 可选依赖处理 +optional_deps = { + 'RT_USING_SERIAL': ['serial.c', 'serial_ops.c'], + 'RT_USING_CAN': ['can.c', 'can_ops.c'], + 'RT_USING_I2C': ['i2c.c', 'i2c_ops.c'] +} + +for dep, files in optional_deps.items(): + if GetDepend(dep): + src += files + +# 检查组合依赖 +if GetDepend('RT_USING_SERIAL') and GetDepend('RT_USING_DMA'): + src += ['serial_dma.c'] + +group = DefineGroup('Drivers', src, depend = ['RT_USING_DEVICE']) +Return('group') +``` + +### Q3: 如何支持多个工具链? + +```python +from building import * +import rtconfig + +src = ['common.c'] + +# 工具链特定文件 +toolchain_files = { + 'gcc': ['gcc_startup.S', 'gcc_specific.c'], + 'armcc': ['keil_startup.s', 'keil_specific.c'], + 'iccarm': ['iar_startup.s', 'iar_specific.c'] +} + +if rtconfig.PLATFORM in toolchain_files: + src += toolchain_files[rtconfig.PLATFORM] +else: + print('Warning: Unknown toolchain', rtconfig.PLATFORM) + +# 工具链特定编译选项 +LOCAL_CFLAGS = '' +if rtconfig.PLATFORM == 'gcc': + LOCAL_CFLAGS = '-Wno-unused-function' +elif rtconfig.PLATFORM == 'armcc': + LOCAL_CFLAGS = '--diag_suppress=177' + +group = DefineGroup('MyComponent', src, depend = [''], + LOCAL_CFLAGS = LOCAL_CFLAGS) +Return('group') +``` + +### Q4: 如何处理生成的代码? + +```python +from building import * +import subprocess + +def generate_code(): + """生成代码""" + # 运行代码生成器 + cmd = ['python', 'codegen.py', '-o', 'generated.c'] + subprocess.check_call(cmd) + +# 确保生成代码 +if GetDepend('RT_USING_CODEGEN'): + generate_code() + src = ['generated.c'] +else: + src = ['default.c'] + +group = DefineGroup('Generated', src, depend = ['']) +Return('group') +``` + +### Q5: 如何组织大型项目? + +```python +# 主SConscript +from building import * + +objs = [] + +# 子模块列表 +modules = [ + 'core', + 'drivers', + 'network', + 'filesystem', + 'gui' +] + +# 根据配置包含模块 +for module in modules: + # 检查模块是否启用 + if GetDepend('RT_USING_' + module.upper()): + # 构建子模块 + objs += SConscript(module + '/SConscript') + +Return('objs') +``` + +## 总结 + +编写高质量的SConscript需要: + +1. **清晰的结构**:合理组织源文件和目录 +2. **正确的依赖**:准确声明和检查依赖关系 +3. **平台兼容**:处理不同工具链和平台的差异 +4. **性能考虑**:避免不必要的文件扫描 +5. **错误处理**:优雅处理各种异常情况 +6. **文档完善**:添加必要的注释和说明 + +通过遵循本指南的建议和最佳实践,可以编写出易维护、可扩展的构建脚本,为RT-Thread项目的构建提供坚实的基础。 \ No newline at end of file diff --git a/tools/docs/guide_arch.drawio.png b/tools/docs/guide_arch.drawio.png new file mode 100644 index 00000000000..d2e49457441 Binary files /dev/null and b/tools/docs/guide_arch.drawio.png differ diff --git a/tools/docs/package-json-support.md b/tools/docs/package-json-support.md new file mode 100644 index 00000000000..c27aed3648a --- /dev/null +++ b/tools/docs/package-json-support.md @@ -0,0 +1,128 @@ +# RT-Thread package.json 构建支持 + +## 概述 + +RT-Thread支持使用package.json来定义组件的构建配置,作为传统SConscript的简化替代方案。 + +## 现有支持 + +### package.json格式 +```json +{ + "name": "hello", + "description": "Hello World component for RT-Thread", + "type": "rt-thread-component", + "dependencies": ["RT_USING_HELLO"], + "defines": [], + "sources": [{ + "name": "src", + "dependencies": [], + "includes": ["."], + "files": ["hello.c"] + }] +} +``` + +### 字段说明 +- **name**: 组件名称(必需) +- **type**: 必须为"rt-thread-component"(必需) +- **description**: 组件描述 +- **dependencies**: 全局依赖,数组形式的宏定义 +- **defines**: 全局宏定义 +- **sources**: 源文件组数组,每组可包含: + - **name**: 源组名称 + - **dependencies**: 源组特定依赖 + - **includes**: 头文件搜索路径 + - **files**: 源文件列表(支持通配符) + +## 使用方式 + +### 1. 在SConscript中使用 + +方式一:使用PackageSConscript(推荐) +```python +from building import * + +objs = PackageSConscript('package.json') +Return('objs') +``` + +方式二:直接调用BuildPackage +```python +Import('env') +from package import BuildPackage + +objs = BuildPackage('package.json') +Return('objs') +``` + +### 2. 目录结构示例 +``` +mycomponent/ +├── SConscript +├── package.json +├── mycomponent.c +├── mycomponent.h +└── src/ + └── helper.c +``` + +### 3. 完整示例 + +package.json: +```json +{ + "name": "mycomponent", + "description": "My RT-Thread component", + "type": "rt-thread-component", + "dependencies": ["RT_USING_MYCOMPONENT"], + "defines": ["MY_VERSION=1"], + "sources": [ + { + "name": "main", + "dependencies": [], + "includes": ["."], + "files": ["mycomponent.c"] + }, + { + "name": "helper", + "dependencies": ["RT_USING_MYCOMPONENT_HELPER"], + "includes": ["src"], + "files": ["src/*.c"] + } + ] +} +``` + +## 工作原理 + +1. **依赖检查**:首先检查全局dependencies,如果不满足则跳过整个组件 +2. **源组处理**:遍历sources数组,每个源组独立检查dependencies +3. **路径处理**:includes相对路径基于package.json所在目录 +4. **文件匹配**:使用SCons的Glob函数处理文件通配符 +5. **构建调用**:最终调用DefineGroup创建构建组 + +## 与DefineGroup的对比 + +| 特性 | package.json | DefineGroup | +|------|--------------|-------------| +| 配置方式 | JSON文件 | Python代码 | +| 依赖管理 | 结构化 | 函数参数 | +| 源文件组织 | 分组管理 | 单一列表 | +| 条件编译 | 源组级别 | 整体级别 | +| 灵活性 | 中等 | 高 | +| 易用性 | 高 | 中等 | + +## 最佳实践 + +1. **简单组件优先使用package.json**:配置清晰,易于维护 +2. **复杂逻辑使用SConscript**:需要动态逻辑时使用传统方式 +3. **混合使用**:可以在同一项目中混用两种方式 + +## 注意事项 + +1. package.json必须是有效的JSON格式 +2. type字段必须为"rt-thread-component" +3. 文件路径相对于package.json所在目录 +4. 依赖不满足时会静默跳过,不会报错 +5. 与RT-Thread构建系统完全集成,不支持独立构建 \ No newline at end of file diff --git a/tools/docs/process.drawio.png b/tools/docs/process.drawio.png new file mode 100644 index 00000000000..af03d7593e7 Binary files /dev/null and b/tools/docs/process.drawio.png differ diff --git a/tools/docs/readme_arch.drawio.png b/tools/docs/readme_arch.drawio.png new file mode 100644 index 00000000000..ab50274cd2d Binary files /dev/null and b/tools/docs/readme_arch.drawio.png differ diff --git a/tools/docs/tech_arch.drawio.png b/tools/docs/tech_arch.drawio.png new file mode 100644 index 00000000000..901f665168d Binary files /dev/null and b/tools/docs/tech_arch.drawio.png differ diff --git "a/tools/docs/\346\236\204\345\273\272\347\263\273\347\273\237\344\275\277\347\224\250\346\214\207\345\215\227.md" "b/tools/docs/\346\236\204\345\273\272\347\263\273\347\273\237\344\275\277\347\224\250\346\214\207\345\215\227.md" new file mode 100644 index 00000000000..c2ed136a2ec --- /dev/null +++ "b/tools/docs/\346\236\204\345\273\272\347\263\273\347\273\237\344\275\277\347\224\250\346\214\207\345\215\227.md" @@ -0,0 +1,618 @@ +# RT-Thread 构建系统使用指南 + +## 目录 + +1. [概述](#概述) +2. [快速开始](#快速开始) +3. [命令行选项详解](#命令行选项详解) +4. [工具链配置](#工具链配置) +5. [项目生成](#项目生成) +6. [软件包管理](#软件包管理) +7. [高级功能](#高级功能) +8. [常见问题](#常见问题) + +## 概述 + +RT-Thread使用基于SCons的构建系统,提供了统一的跨平台构建体验。构建系统支持: + +- 多种编译器和IDE(GCC、Keil、IAR、VS Code等) +- 模块化的组件管理 +- 灵活的配置系统 +- 自动化的依赖处理 +- 软件包管理功能 + +### 系统架构图 + +![arch](./guide_arch.drawio.png) + +## 快速开始 + +### 基本编译流程 + +1. **进入BSP目录** + ```bash + cd bsp/stm32/stm32f103-blue-pill + ``` + +2. **配置系统**(可选) + ```bash + menuconfig # 图形化配置 + ``` + +3. **编译项目** + ```bash + scons # 默认编译 + scons -j8 # 多线程编译 + ``` + +4. **生成IDE项目** + ```bash + scons --target=mdk5 # 生成Keil MDK5项目 + scons --target=iar # 生成IAR项目 + scons --target=vsc # 生成VS Code项目 + ``` + +### 清理和重建 + +```bash +scons -c # 清理编译产物 +scons -c --target=mdk5 # 清理MDK5项目文件 +scons --dist # 生成分发包 +``` + +## 命令行选项详解 + +### 基础编译选项 + +| 选项 | 说明 | 示例 | +|------|------|------| +| `-j N` | 多线程编译,N为线程数 | `scons -j8` | +| `-c` | 清理编译产物 | `scons -c` | +| `-s` | 静默模式,不显示命令 | `scons -s` | +| `--verbose` | 详细输出模式 | `scons --verbose` | + +### 项目生成选项 + +| 选项 | 说明 | 生成的文件 | +|------|------|------------| +| `--target=mdk4` | Keil MDK4项目 | project.uvproj | +| `--target=mdk5` | Keil MDK5项目 | project.uvprojx | +| `--target=iar` | IAR工作区 | project.eww | +| `--target=vs2012` | Visual Studio项目 | project.vcxproj | +| `--target=vsc` | VS Code配置 | .vscode/目录 | +| `--target=eclipse` | Eclipse CDT项目 | .project, .cproject | +| `--target=cmake` | CMake项目 | CMakeLists.txt | +| `--target=makefile` | 通用Makefile | Makefile | + +### 配置管理选项 + +| 选项 | 说明 | 使用场景 | +|------|------|----------| +| `--menuconfig` | 启动图形配置界面 | 修改功能配置 | +| `--pyconfig` | 通过Python脚本配置 | 自动化配置 | +| `--pyconfig-silent` | 静默Python配置 | CI/CD环境 | +| `--genconfig` | 从rtconfig.h生成.config | 配置迁移 | +| `--useconfig=xxx` | 使用指定配置文件 | 切换配置 | + +### 工具链选项 + +| 选项 | 说明 | 示例 | +|------|------|------| +| `--exec-path=PATH` | 指定工具链路径 | `--exec-path=/opt/gcc-arm/bin` | +| `--exec-prefix=PREFIX` | 指定工具链前缀 | `--exec-prefix=arm-none-eabi-` | +| `--strict` | 严格编译模式 | 开启-Werror | + +### 分发和调试选项 + +| 选项 | 说明 | 用途 | +|------|------|------| +| `--dist` | 生成分发包 | 项目发布 | +| `--dist-strip` | 生成精简分发包 | 最小化项目 | +| `--dist-ide` | 生成RT-Thread Studio项目 | Studio开发 | +| `--cscope` | 生成cscope数据库 | 代码导航 | +| `--clang-analyzer` | 运行Clang静态分析 | 代码质量检查 | + +## 工具链配置 + +### rtconfig.py配置文件 + +每个BSP都有一个`rtconfig.py`文件,定义了工具链配置: + +```python +import os + +# 工具链定义 +CROSS_TOOL = 'gcc' # 工具链类型: gcc/keil/iar +PLATFORM = 'armcc' # 平台标识 + +# 编译器路径 +if os.getenv('RTT_EXEC_PATH'): + EXEC_PATH = os.getenv('RTT_EXEC_PATH') +else: + EXEC_PATH = r'C:/Keil_v5/ARM/ARMCC/bin' + +# 编译器前缀(GCC工具链) +PREFIX = 'arm-none-eabi-' + +# 编译器定义 +CC = PREFIX + 'gcc' +CXX = PREFIX + 'g++' +AS = PREFIX + 'gcc' +AR = PREFIX + 'ar' +LINK = PREFIX + 'gcc' +SIZE = PREFIX + 'size' +OBJDUMP = PREFIX + 'objdump' +OBJCPY = PREFIX + 'objcopy' + +# 设备相关参数 +DEVICE = ' -mcpu=cortex-m3 -mthumb -ffunction-sections -fdata-sections' + +# 编译标志 +CFLAGS = DEVICE + ' -Dgcc' +AFLAGS = ' -c' + DEVICE + ' -x assembler-with-cpp -Wa,-mimplicit-it=thumb ' +LFLAGS = DEVICE + ' -Wl,--gc-sections,-Map=rtthread.map,-cref,-u,Reset_Handler -T link.lds' + +# 路径定义 +CPATH = '' +LPATH = '' + +# 链接脚本 +LINK_SCRIPT = 'link.lds' + +# 后处理命令 +POST_ACTION = OBJCPY + ' -O binary $TARGET rtthread.bin\n' + SIZE + ' $TARGET \n' +``` + +### 支持的工具链 + +1. **GCC工具链** + ```python + CROSS_TOOL = 'gcc' + PREFIX = 'arm-none-eabi-' + ``` + +2. **Keil MDK** + ```python + CROSS_TOOL = 'keil' + PLATFORM = 'armcc' # ARM Compiler 5 + # 或 + PLATFORM = 'armclang' # ARM Compiler 6 + ``` + +3. **IAR** + ```python + CROSS_TOOL = 'iar' + PLATFORM = 'iccarm' + ``` + +4. **RISC-V GCC** + ```python + CROSS_TOOL = 'gcc' + PREFIX = 'riscv64-unknown-elf-' + ``` + +### 环境变量支持 + +构建系统支持通过环境变量覆盖配置: + +```bash +# 设置工具链路径 +export RTT_EXEC_PATH=/opt/gcc-arm-none-eabi-10-2020-q4-major/bin + +# 设置工具链前缀 +export RTT_CC_PREFIX=arm-none-eabi- + +# 设置工具链类型 +export RTT_CC=gcc +``` + +## 项目生成 + +### VS Code项目配置 + +使用`scons --target=vsc`生成VS Code项目,会创建以下配置文件: + +**.vscode/c_cpp_properties.json** - IntelliSense配置 +```json +{ + "configurations": [ + { + "name": "RT-Thread", + "includePath": [ + "${workspaceFolder}/**", + "${workspaceFolder}/../../components/finsh", + "${workspaceFolder}/../../include" + ], + "defines": [ + "RT_USING_FINSH", + "RT_USING_SERIAL", + "__GNUC__" + ], + "compilerPath": "/opt/gcc-arm/bin/arm-none-eabi-gcc", + "cStandard": "c99", + "cppStandard": "c++11" + } + ] +} +``` + +**.vscode/tasks.json** - 构建任务配置 +```json +{ + "version": "2.0.0", + "tasks": [ + { + "label": "build", + "type": "shell", + "command": "scons", + "problemMatcher": "$gcc", + "group": { + "kind": "build", + "isDefault": true + } + } + ] +} +``` + +### CMake项目生成 + +使用`scons --target=cmake`生成CMakeLists.txt: + +```cmake +cmake_minimum_required(VERSION 3.10) + +# 工具链设置 +set(CMAKE_SYSTEM_NAME Generic) +set(CMAKE_SYSTEM_PROCESSOR cortex-m3) +set(CMAKE_C_COMPILER arm-none-eabi-gcc) +set(CMAKE_ASM_COMPILER arm-none-eabi-gcc) + +project(rtthread C ASM) + +# 编译选项 +add_compile_options( + -mcpu=cortex-m3 + -mthumb + -ffunction-sections + -fdata-sections + -Wall + -O0 + -g +) + +# 头文件路径 +include_directories( + ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_CURRENT_SOURCE_DIR}/../../include +) + +# 源文件 +set(SOURCES + applications/main.c + ../../src/clock.c + ../../src/components.c +) + +# 生成可执行文件 +add_executable(${PROJECT_NAME}.elf ${SOURCES}) + +# 链接选项 +target_link_options(${PROJECT_NAME}.elf PRIVATE + -T${CMAKE_CURRENT_SOURCE_DIR}/link.lds + -Wl,-Map=${PROJECT_NAME}.map,--cref + -Wl,--gc-sections +) +``` + +## 软件包管理 + +### 使用package.json定义组件 + +RT-Thread支持使用`package.json`文件定义软件包: + +```json +{ + "name": "my-driver", + "version": "1.0.0", + "type": "rt-thread-component", + "license": "Apache-2.0", + "dependencies": { + "RT_USING_DEVICE": "latest" + }, + "sources": { + "common": { + "source_files": ["src/*.c"], + "header_files": ["inc/*.h"], + "header_path": ["inc"] + }, + "cortex-m": { + "condition": "defined(ARCH_ARM_CORTEX_M)", + "source_files": ["port/cortex-m/*.c"] + } + } +} +``` + +### 在SConscript中使用BuildPackage + +```python +from building import * +import os + +# 使用package.json构建 +objs = BuildPackage('package.json') + +# 或者手动指定包路径 +pkg_path = os.path.join(GetCurrentDir(), 'package.json') +objs = BuildPackage(pkg_path) + +Return('objs') +``` + +## 高级功能 + +### 1. 条件编译和依赖管理 + +**基于宏定义的条件编译** +```python +src = ['common.c'] + +if GetDepend('RT_USING_SERIAL'): + src += ['serial.c'] + +if GetDepend(['RT_USING_SPI', 'RT_USING_SFUD']): + src += ['spi_flash.c'] + +group = DefineGroup('Drivers', src, depend = ['RT_USING_DEVICE']) +``` + +**复杂依赖表达式** +```python +# 依赖可以是列表(AND关系) +depend = ['RT_USING_LWIP', 'RT_USING_NETDEV'] + +# 或者使用GetDepend进行复杂判断 +if GetDepend('RT_USING_LWIP') and not GetDepend('RT_USING_SAL'): + print('配置错误:LWIP需要SAL支持') +``` + +### 2. 本地编译选项 + +为特定模块设置独立的编译选项: + +```python +# 全局编译选项 +CPPPATH = [GetCurrentDir()] +CPPDEFINES = ['MODULE_VERSION=1'] + +# 本地编译选项(仅对当前group有效) +LOCAL_CFLAGS = '-O3 -funroll-loops' +LOCAL_CPPPATH = ['./private'] +LOCAL_CPPDEFINES = {'BUFFER_SIZE': 1024} + +group = DefineGroup('Module', src, depend = [''], + CPPPATH = CPPPATH, + CPPDEFINES = CPPDEFINES, + LOCAL_CFLAGS = LOCAL_CFLAGS, + LOCAL_CPPPATH = LOCAL_CPPPATH, + LOCAL_CPPDEFINES = LOCAL_CPPDEFINES +) +``` + +### 3. 递归构建子目录 + +自动扫描并构建子目录: + +```python +import os +from building import * + +objs = [] +cwd = GetCurrentDir() +dirs = os.listdir(cwd) + +# 黑名单目录 +skip_dirs = ['test', 'doc', 'example'] + +for d in dirs: + if d in skip_dirs: + continue + + path = os.path.join(cwd, d) + if os.path.isdir(path): + sconscript = os.path.join(path, 'SConscript') + if os.path.isfile(sconscript): + objs += SConscript(sconscript) + +Return('objs') +``` + +### 4. 自定义构建动作 + +添加构建前后的自定义动作: + +```python +from building import * + +def pre_build_action(target, source, env): + print('开始构建:', target[0]) + # 执行预处理操作 + +def post_build_action(target, source, env): + print('构建完成:', target[0]) + # 生成额外文件,如hex文件 + import subprocess + subprocess.call(['arm-none-eabi-objcopy', '-O', 'ihex', + str(target[0]), str(target[0]) + '.hex']) + +# 注册构建动作 +if GetOption('target') == None: + rtconfig.POST_ACTION = post_build_action +``` + +### 5. 分发包定制 + +创建自定义分发包: + +```python +# 在BSP的SConstruct中添加 +def dist_handle(BSP_ROOT, dist_dir): + import shutil + + # 复制必要文件 + src_files = ['applications', 'board', 'rtconfig.py', 'SConstruct'] + for src in src_files: + src_path = os.path.join(BSP_ROOT, src) + dst_path = os.path.join(dist_dir, src) + if os.path.isdir(src_path): + shutil.copytree(src_path, dst_path) + else: + shutil.copy2(src_path, dst_path) + + # 创建README + with open(os.path.join(dist_dir, 'README.md'), 'w') as f: + f.write('# RT-Thread BSP 分发包\n') + f.write('构建时间: ' + time.strftime('%Y-%m-%d %H:%M:%S\n')) + +# 注册分发处理函数 +AddOption('--dist-handle', + dest = 'dist-handle', + action = 'store_true', + default = False, + help = 'Enable dist handle') + +if GetOption('dist-handle'): + dist_handle(BSP_ROOT, dist_dir) +``` + +### 6. 代码分析集成 + +**Clang静态分析** +```bash +scons --clang-analyzer +``` + +**生成compile_commands.json** +```bash +scons --target=cmake # CMake项目会包含compile_commands.json +# 或使用 +scons --compile-commands +``` + +**生成Cscope数据库** +```bash +scons --cscope +``` + +## 常见问题 + +### Q1: 如何添加新的源文件? + +在相应目录的SConscript中添加: +```python +src = Glob('*.c') # 自动包含所有.c文件 +# 或 +src = ['file1.c', 'file2.c'] # 手动指定 +``` + +### Q2: 如何排除特定文件? + +```python +src = Glob('*.c') +SrcRemove(src, ['test.c', 'debug.c']) +``` + +### Q3: 如何处理不同配置下的源文件? + +```python +src = ['common.c'] + +if rtconfig.PLATFORM == 'gcc': + src += ['gcc_specific.c'] +elif rtconfig.PLATFORM == 'armcc': + src += ['keil_specific.c'] +``` + +### Q4: 如何调试构建问题? + +1. 使用详细输出模式: + ```bash + scons --verbose + ``` + +2. 查看预处理结果: + ```bash + scons --target=mdk5 --verbose # 查看生成的项目配置 + ``` + +3. 检查依赖关系: + ```python + # 在SConscript中添加调试输出 + print('GetDepend result:', GetDepend('RT_USING_XXX')) + ``` + +### Q5: 如何加快编译速度? + +1. 使用多线程编译: + ```bash + scons -j$(nproc) # Linux/macOS + scons -j8 # Windows + ``` + +2. 使用ccache(GCC): + ```python + # 在rtconfig.py中 + CC = 'ccache ' + PREFIX + 'gcc' + ``` + +3. 优化依赖关系,避免不必要的重编译 + +### Q6: 如何处理第三方库? + +1. **作为源码包含** + ```python + # libraries/foo/SConscript + src = Glob('src/*.c') + CPPPATH = [GetCurrentDir() + '/include'] + + group = DefineGroup('foo', src, depend = ['RT_USING_FOO'], + CPPPATH = CPPPATH) + ``` + +2. **作为预编译库** + ```python + # 添加库文件 + LIBS = ['foo'] + LIBPATH = [GetCurrentDir() + '/lib'] + + group = DefineGroup('foo', [], depend = ['RT_USING_FOO'], + LIBS = LIBS, LIBPATH = LIBPATH) + ``` + +### Q7: 如何自定义链接脚本? + +在rtconfig.py中指定: +```python +# GCC工具链 +LINK_SCRIPT = 'board/link.lds' + +# Keil MDK +LINK_SCRIPT = 'board/link.sct' + +# IAR +LINK_SCRIPT = 'board/link.icf' +``` + +## 最佳实践 + +1. **模块化设计**:每个功能模块使用独立的SConscript +2. **依赖管理**:正确设置depend参数,避免编译不需要的代码 +3. **路径处理**:使用GetCurrentDir()获取当前路径,避免硬编码 +4. **条件编译**:合理使用GetDepend进行条件判断 +5. **编译选项**:全局选项放在rtconfig.py,局部选项使用LOCAL_xxx +6. **文档维护**:在SConscript中添加必要的注释说明 + +## 总结 + +RT-Thread的构建系统提供了强大而灵活的项目管理能力。通过合理使用各种构建选项和功能,可以高效地进行嵌入式软件开发。建议开发者深入理解构建系统的工作原理,以便更好地利用其功能。 \ No newline at end of file diff --git "a/tools/docs/\346\236\204\345\273\272\347\263\273\347\273\237\346\212\200\346\234\257\345\216\237\347\220\206.md" "b/tools/docs/\346\236\204\345\273\272\347\263\273\347\273\237\346\212\200\346\234\257\345\216\237\347\220\206.md" new file mode 100644 index 00000000000..4fb1608c995 --- /dev/null +++ "b/tools/docs/\346\236\204\345\273\272\347\263\273\347\273\237\346\212\200\346\234\257\345\216\237\347\220\206.md" @@ -0,0 +1,841 @@ +# RT-Thread 构建系统技术原理 + +## 目录 + +1. [系统架构设计](#系统架构设计) +2. [核心模块分析](#核心模块分析) +3. [构建流程详解](#构建流程详解) +4. [依赖管理机制](#依赖管理机制) +5. [工具链适配层](#工具链适配层) +6. [项目生成器架构](#项目生成器架构) +7. [配置系统实现](#配置系统实现) +8. [扩展机制](#扩展机制) + +## 系统架构设计 + +### 整体架构图 + +![arch](./tech_arch.drawio.png) + +### 设计原则 + +1. **模块化设计**:每个功能模块独立,通过明确的接口交互 +2. **可扩展性**:易于添加新的工具链支持和目标生成器 +3. **跨平台兼容**:统一的抽象层处理平台差异 +4. **配置驱动**:通过配置文件控制构建行为 + +## 核心模块分析 + +### 1. building.py - 构建引擎核心 + +#### 1.1 全局变量管理 + +```python +BuildOptions = {} # 存储从rtconfig.h解析的宏定义 +Projects = [] # 存储所有的组件对象 +Rtt_Root = '' # RT-Thread根目录 +Env = None # SCons环境对象 +``` + +#### 1.2 PrepareBuilding 函数实现 + +```python +def PrepareBuilding(env, root_directory, has_libcpu=False, remove_components = []): + """ + 准备构建环境 + + 参数: + env: SCons环境对象 + root_directory: RT-Thread根目录 + has_libcpu: 是否包含libcpu + remove_components: 需要移除的组件列表 + """ + # 1. 添加命令行选项 + AddOptions() + + # 2. 设置全局环境变量 + global Env, Rtt_Root + Env = env + Rtt_Root = os.path.abspath(root_directory) + + # 3. 配置日志系统 + logging.basicConfig(level=logging.INFO) + logger = logging.getLogger('rt-scons') + Env['log'] = logger + + # 4. 工具链检测和配置 + if not utils.CmdExists(os.path.join(rtconfig.EXEC_PATH, rtconfig.CC)): + # 尝试自动检测工具链 + try: + envm = utils.ImportModule('env_utility') + exec_path = envm.GetSDKPath(rtconfig.CC) + if exec_path: + rtconfig.EXEC_PATH = exec_path + except: + pass + + # 5. 解析rtconfig.h配置 + PreProcessor = create_preprocessor_instance() + with open('rtconfig.h', 'r') as f: + PreProcessor.process_contents(f.read()) + BuildOptions = PreProcessor.cpp_namespace + + # 6. 处理目标平台 + if GetOption('target'): + # 根据目标设置工具链 + rtconfig.CROSS_TOOL, rtconfig.PLATFORM = tgt_dict[tgt_name] + + return objs +``` + +#### 1.3 DefineGroup 函数实现 + +```python +def DefineGroup(name, src, depend, **parameters): + """ + 定义一个组件组 + + 参数: + name: 组名称 + src: 源文件列表 + depend: 依赖条件 + **parameters: 编译参数(CPPPATH, CPPDEFINES, LIBS等) + + 返回: + 组对象列表 + """ + # 1. 检查依赖条件 + if not GetDepend(depend): + return [] + + # 2. 处理源文件 + if isinstance(src, list): + # 过滤掉被移除的文件 + src = [s for s in src if s not in removed_src] + + # 3. 创建组对象 + group = {} + group['name'] = name + group['src'] = src + + # 4. 处理编译参数 + # 全局参数 + if 'CPPPATH' in parameters: + group['CPPPATH'] = parameters['CPPPATH'] + + # 本地参数(仅对当前组有效) + if 'LOCAL_CPPPATH' in parameters: + paths = parameters['LOCAL_CPPPATH'] + group['LOCAL_CPPPATH'] = [os.path.abspath(p) for p in paths] + + # 5. 注册到全局项目列表 + Projects.append(group) + + # 6. 返回SCons对象 + if src: + objs = Env.Object(src) + else: + objs = [] + + return objs +``` + +### 2. 依赖管理机制 + +#### 2.1 GetDepend 实现 + +```python +def GetDepend(depend): + """ + 检查依赖条件是否满足 + + 参数: + depend: 字符串或字符串列表 + + 返回: + True: 依赖满足 + False: 依赖不满足 + """ + # 1. 处理空依赖 + if not depend: + return True + + # 2. 处理字符串依赖 + if isinstance(depend, str): + return _CheckSingleDepend(depend) + + # 3. 处理列表依赖(AND关系) + if isinstance(depend, list): + for d in depend: + if not _CheckSingleDepend(d): + return False + return True + + return False + +def _CheckSingleDepend(depend): + """检查单个依赖""" + # 1. 检查是否在BuildOptions中定义 + if depend in BuildOptions: + # 2. 检查值是否为真 + return BuildOptions[depend] != '0' + return False +``` + +#### 2.2 依赖表达式支持 + +```python +# 支持的依赖表达式 +depend = 'RT_USING_SERIAL' # 单个依赖 +depend = ['RT_USING_LWIP', 'SAL'] # AND关系 +depend = '' # 无条件包含 + +# 高级用法 - 在SConscript中 +if GetDepend('RT_USING_LWIP'): + if GetDepend('RT_USING_LWIP_TCP'): + src += ['tcp.c'] + if GetDepend('RT_USING_LWIP_UDP'): + src += ['udp.c'] +``` + +### 3. 配置解析系统 + +#### 3.1 预处理器实现 + +```python +class PreProcessor: + """ + C预处理器实现,用于解析rtconfig.h + """ + def __init__(self): + self.cpp_namespace = {} + self.defines = {} + + def process_contents(self, contents): + """处理文件内容""" + lines = contents.split('\n') + + for line in lines: + # 处理 #define 指令 + if line.startswith('#define'): + self._process_define(line) + # 处理 #ifdef 等条件编译 + elif line.startswith('#ifdef'): + self._process_ifdef(line) + + def _process_define(self, line): + """处理宏定义""" + # #define RT_NAME_MAX 8 + parts = line.split(None, 2) + if len(parts) >= 2: + name = parts[1] + value = parts[2] if len(parts) > 2 else '1' + self.cpp_namespace[name] = value +``` + +#### 3.2 配置文件格式 + +**rtconfig.h 示例** +```c +/* RT-Thread 配置文件 */ +#ifndef RT_CONFIG_H__ +#define RT_CONFIG_H__ + +/* 内核配置 */ +#define RT_THREAD_PRIORITY_32 +#define RT_THREAD_PRIORITY_MAX 32 +#define RT_TICK_PER_SECOND 100 +#define RT_USING_TIMER_SOFT + +/* 组件配置 */ +#define RT_USING_DEVICE +#define RT_USING_SERIAL +#define RT_SERIAL_RB_BUFSZ 64 + +/* 条件配置 */ +#ifdef RT_USING_SERIAL + #define RT_SERIAL_USING_DMA +#endif + +#endif /* RT_CONFIG_H__ */ +``` + +### 4. 工具链适配层 + +#### 4.1 工具链抽象接口 + +```python +class ToolchainBase: + """工具链基类""" + def __init__(self): + self.name = '' + self.prefix = '' + self.suffix = '' + + def get_cc(self): + """获取C编译器""" + raise NotImplementedError + + def get_cflags(self): + """获取C编译选项""" + raise NotImplementedError + + def get_linkflags(self): + """获取链接选项""" + raise NotImplementedError +``` + +#### 4.2 GCC工具链实现 + +```python +class GccToolchain(ToolchainBase): + def __init__(self, prefix=''): + self.name = 'gcc' + self.prefix = prefix + self.suffix = '' + + def get_cc(self): + return self.prefix + 'gcc' + + def get_cflags(self): + flags = [] + # 基础选项 + flags += ['-Wall', '-g'] + # 优化选项 + if GetOption('optimization') == 'size': + flags += ['-Os'] + else: + flags += ['-O0'] + # 架构选项 + flags += ['-mcpu=cortex-m3', '-mthumb'] + return ' '.join(flags) +``` + +#### 4.3 Keil MDK适配 + +```python +class KeilToolchain(ToolchainBase): + def __init__(self): + self.name = 'keil' + + def setup_environment(self, env): + """设置Keil特定的环境变量""" + # 修改文件扩展名 + env['OBJSUFFIX'] = '.o' + env['LIBPREFIX'] = '' + env['LIBSUFFIX'] = '.lib' + + # 设置编译命令 + env['CC'] = 'armcc' + env['AS'] = 'armasm' + env['AR'] = 'armar' + env['LINK'] = 'armlink' + + # 设置命令格式 + env['ARCOM'] = '$AR --create $TARGET $SOURCES' +``` + +### 5. 项目生成器架构 + +#### 5.1 生成器基类 + +```python +class ProjectGenerator: + """项目生成器基类""" + def __init__(self, env, project): + self.env = env + self.project = project + self.template_dir = '' + + def generate(self): + """生成项目文件""" + self._prepare() + self._generate_project_file() + self._generate_workspace_file() + self._copy_template_files() + self._post_process() + + def _collect_source_files(self): + """收集源文件""" + sources = [] + for group in self.project: + sources.extend(group['src']) + return sources + + def _collect_include_paths(self): + """收集头文件路径""" + paths = [] + for group in self.project: + if 'CPPPATH' in group: + paths.extend(group['CPPPATH']) + return list(set(paths)) # 去重 +``` + +#### 5.2 VS Code生成器实现 + +```python +class VSCodeGenerator(ProjectGenerator): + """VS Code项目生成器""" + + def _generate_project_file(self): + """生成VS Code配置文件""" + # 创建.vscode目录 + vscode_dir = os.path.join(self.env['BSP_ROOT'], '.vscode') + if not os.path.exists(vscode_dir): + os.makedirs(vscode_dir) + + # 生成c_cpp_properties.json + self._generate_cpp_properties() + + # 生成tasks.json + self._generate_tasks() + + # 生成launch.json + self._generate_launch() + + def _generate_cpp_properties(self): + """生成IntelliSense配置""" + config = { + "configurations": [{ + "name": "RT-Thread", + "includePath": self._collect_include_paths(), + "defines": self._collect_defines(), + "compilerPath": self._get_compiler_path(), + "cStandard": "c99", + "cppStandard": "c++11", + "intelliSenseMode": "gcc-arm" + }], + "version": 4 + } + + # 写入文件 + file_path = os.path.join('.vscode', 'c_cpp_properties.json') + with open(file_path, 'w') as f: + json.dump(config, f, indent=4) +``` + +#### 5.3 Keil MDK5生成器 + +```python +class MDK5Generator(ProjectGenerator): + """Keil MDK5项目生成器""" + + def _generate_project_file(self): + """生成uvprojx文件""" + # 加载XML模板 + tree = etree.parse(self.template_file) + root = tree.getroot() + + # 更新目标配置 + self._update_target_options(root) + + # 添加文件组 + groups_node = root.find('.//Groups') + for group in self.project: + self._add_file_group(groups_node, group) + + # 保存项目文件 + tree.write('project.uvprojx', encoding='utf-8', + xml_declaration=True) + + def _add_file_group(self, parent, group): + """添加文件组""" + group_elem = etree.SubElement(parent, 'Group') + + # 组名 + name_elem = etree.SubElement(group_elem, 'GroupName') + name_elem.text = group['name'] + + # 文件列表 + files_elem = etree.SubElement(group_elem, 'Files') + for src in group['src']: + self._add_file(files_elem, src) +``` + +### 6. 编译数据库生成 + +#### 6.1 compile_commands.json生成 + +```python +def generate_compile_commands(env, project): + """ + 生成compile_commands.json用于代码分析工具 + """ + commands = [] + + for group in project: + for src in group['src']: + if src.endswith('.c') or src.endswith('.cpp'): + cmd = { + "directory": env['BSP_ROOT'], + "file": os.path.abspath(src), + "command": _generate_compile_command(env, src, group) + } + commands.append(cmd) + + # 写入文件 + with open('compile_commands.json', 'w') as f: + json.dump(commands, f, indent=2) + +def _generate_compile_command(env, src, group): + """生成单个文件的编译命令""" + cmd = [] + + # 编译器 + cmd.append(env['CC']) + + # 编译选项 + cmd.extend(env['CFLAGS'].split()) + + # 头文件路径 + for path in group.get('CPPPATH', []): + cmd.append('-I' + path) + + # 宏定义 + for define in group.get('CPPDEFINES', []): + if isinstance(define, tuple): + cmd.append('-D{}={}'.format(define[0], define[1])) + else: + cmd.append('-D' + define) + + # 源文件 + cmd.append(src) + + return ' '.join(cmd) +``` + +### 7. 分发系统实现 + +#### 7.1 分发包生成流程 + +```python +def MkDist(program, BSP_ROOT, RTT_ROOT, Env, project): + """生成分发包""" + # 1. 创建分发目录 + dist_name = os.path.basename(BSP_ROOT) + dist_dir = os.path.join(BSP_ROOT, 'dist', dist_name) + + # 2. 复制RT-Thread内核 + print('=> copy RT-Thread kernel') + copytree(os.path.join(RTT_ROOT, 'src'), + os.path.join(dist_dir, 'rt-thread', 'src')) + copytree(os.path.join(RTT_ROOT, 'include'), + os.path.join(dist_dir, 'rt-thread', 'include')) + + # 3. 复制使用的组件 + print('=> copy components') + for group in project: + _copy_group_files(group, dist_dir) + + # 4. 生成Kconfig文件 + _generate_kconfig(dist_dir, project) + + # 5. 打包 + make_zip(dist_dir, dist_name + '.zip') +``` + +#### 7.2 精简分发包生成 + +```python +def MkDist_Strip(program, BSP_ROOT, RTT_ROOT, Env): + """ + 基于compile_commands.json生成精简分发包 + 只包含实际使用的文件 + """ + # 1. 解析compile_commands.json + with open('compile_commands.json', 'r') as f: + commands = json.load(f) + + # 2. 提取使用的文件 + used_files = set() + for cmd in commands: + # 源文件 + used_files.add(cmd['file']) + + # 解析包含的头文件 + includes = _parse_includes(cmd['file'], cmd['command']) + used_files.update(includes) + + # 3. 复制文件 + for file in used_files: + _copy_with_structure(file, dist_dir) +``` + +## 构建流程详解 + +### 完整构建流程图 + +![process](./process.drawio.png) + +### 依赖解析流程 + +```python +def dependency_resolution_flow(): + """ + 依赖解析流程示例 + """ + # 1. 从rtconfig.h读取所有宏定义 + macros = parse_rtconfig_h() + # 例: {'RT_USING_SERIAL': '1', 'RT_USING_PIN': '1'} + + # 2. 处理单个组件 + for component in components: + # 3. 检查依赖条件 + if check_dependencies(component.depends, macros): + # 4. 包含组件 + include_component(component) + else: + # 5. 跳过组件 + skip_component(component) + + # 6. 递归处理子依赖 + resolve_sub_dependencies() +``` + +## 扩展机制 + +### 1. 添加新的工具链支持 + +```python +# 1. 在tgt_dict中添加映射 +tgt_dict['mycc'] = ('mycc', 'mycc') + +# 2. 创建tools/mycc.py +import os +from building import * + +def generate_project(env, project): + """生成项目文件""" + print("Generating MyCC project...") + + # 收集信息 + info = ProjectInfo(env, project) + + # 生成项目文件 + # ... + +# 3. 在rtconfig.py中配置 +CROSS_TOOL = 'mycc' +PLATFORM = 'mycc' +``` + +### 2. 添加自定义构建步骤 + +```python +# 在SConstruct或SConscript中 +def custom_builder(target, source, env): + """自定义构建器""" + # 执行自定义操作 + cmd = 'custom_tool -o {} {}'.format(target[0], source[0]) + os.system(cmd) + +# 注册构建器 +env['BUILDERS']['CustomBuild'] = Builder(action=custom_builder, + suffix='.out', + src_suffix='.in') + +# 使用构建器 +custom_out = env.CustomBuild('output.out', 'input.in') +``` + +### 3. 扩展配置解析器 + +```python +class ExtendedPreProcessor(PreProcessor): + """扩展的预处理器""" + + def __init__(self): + super().__init__() + self.custom_handlers = {} + + def register_handler(self, directive, handler): + """注册自定义指令处理器""" + self.custom_handlers[directive] = handler + + def process_line(self, line): + """处理单行""" + # 检查自定义指令 + for directive, handler in self.custom_handlers.items(): + if line.startswith(directive): + return handler(line) + + # 默认处理 + return super().process_line(line) +``` + +### 4. 插件系统实现 + +```python +class BuildPlugin: + """构建插件基类""" + + def __init__(self, name): + self.name = name + + def pre_build(self, env, project): + """构建前钩子""" + pass + + def post_build(self, env, project): + """构建后钩子""" + pass + + def configure(self, env): + """配置环境""" + pass + +# 插件管理器 +class PluginManager: + def __init__(self): + self.plugins = [] + + def register(self, plugin): + self.plugins.append(plugin) + + def run_pre_build(self, env, project): + for plugin in self.plugins: + plugin.pre_build(env, project) +``` + +## 性能优化 + +### 1. 构建缓存机制 + +```python +class BuildCache: + """构建缓存""" + + def __init__(self, cache_dir='.scache'): + self.cache_dir = cache_dir + self.cache_db = os.path.join(cache_dir, 'cache.db') + + def get_hash(self, file): + """计算文件哈希""" + import hashlib + with open(file, 'rb') as f: + return hashlib.md5(f.read()).hexdigest() + + def is_cached(self, source, target): + """检查是否已缓存""" + # 检查目标文件是否存在 + if not os.path.exists(target): + return False + + # 检查源文件是否更新 + source_hash = self.get_hash(source) + cached_hash = self.load_hash(source) + + return source_hash == cached_hash +``` + +### 2. 并行构建优化 + +```python +def optimize_parallel_build(env, project): + """优化并行构建""" + # 1. 分析依赖关系 + dep_graph = analyze_dependencies(project) + + # 2. 计算最优构建顺序 + build_order = topological_sort(dep_graph) + + # 3. 分组独立任务 + parallel_groups = [] + for level in build_order: + # 同一层级可以并行 + parallel_groups.append(level) + + # 4. 设置并行度 + import multiprocessing + num_jobs = multiprocessing.cpu_count() + env.SetOption('num_jobs', num_jobs) + + return parallel_groups +``` + +## 调试技巧 + +### 1. 构建日志分析 + +```python +# 启用详细日志 +def enable_build_logging(): + # 设置SCons日志 + env.SetOption('debug', 'explain') + + # 自定义日志 + class BuildLogger: + def __init__(self, logfile): + self.logfile = logfile + + def __call__(self, msg, *args): + with open(self.logfile, 'a') as f: + f.write(msg % args + '\n') + + logger = BuildLogger('build.log') + env['PRINT_CMD_LINE_FUNC'] = logger +``` + +### 2. 依赖关系可视化 + +```python +def visualize_dependencies(project): + """生成依赖关系图""" + import graphviz + + dot = graphviz.Digraph(comment='Dependencies') + + # 添加节点 + for group in project: + dot.node(group['name']) + + # 添加边 + for group in project: + for dep in group.get('depends', []): + if find_group(dep): + dot.edge(dep, group['name']) + + # 渲染 + dot.render('dependencies', format='png') +``` + +## 最佳实践 + +### 1. 模块化设计原则 + +- 每个功能模块独立的SConscript +- 明确的依赖关系声明 +- 避免循环依赖 +- 使用统一的命名规范 + +### 2. 性能优化建议 + +- 使用Glob谨慎,大目录下性能差 +- 合理设置并行编译数 +- 使用增量编译 +- 避免重复的文件扫描 + +### 3. 可维护性建议 + +- 添加充分的注释 +- 使用有意义的变量名 +- 遵循Python PEP8规范 +- 定期清理无用代码 + +### 4. 跨平台兼容性 + +- 使用os.path处理路径 +- 避免平台特定的命令 +- 测试多平台构建 +- 处理路径分隔符差异 + +## 总结 + +RT-Thread的构建系统是一个精心设计的模块化系统,通过清晰的架构和灵活的扩展机制,为嵌入式开发提供了强大的构建能力。理解其内部原理有助于: + +1. 更好地使用和优化构建流程 +2. 快速定位和解决构建问题 +3. 扩展支持新的工具链和平台 +4. 为项目定制构建流程 + +构建系统的核心价值在于将复杂的嵌入式构建过程标准化和自动化,让开发者能够专注于功能开发而不是构建配置。 \ No newline at end of file diff --git a/tools/hello/package.json b/tools/hello/package.json index 1e390882b13..6fce0b9b00b 100644 --- a/tools/hello/package.json +++ b/tools/hello/package.json @@ -1,20 +1,31 @@ { - "name": "hello", - "version": "1.0.0", - "description": "Hello World component for RT-Thread", - "author": "RT-Thread Development Team", - "license": "Apache-2.0", - "type": "rt-package", - "source_files": [ - "hello.c" - ], - "CPPPATH": [ + "name": "hello", + "description": "Hello World component for RT-Thread", + "type": "rt-thread-component", + "dependencies": [], + "defines": [ + "DEFINE_HELLO" + ], + "sources": [ + { + "dependencies": [], + "includes": [ "." - ], - "CPPDEFINES": [ - "HELLO" - ], - "depends": [ - "" - ] + ], + "files": [ + "hello.c" + ] + }, + { + "dependencies": [ + "HELLO_USING_HELPER" + ], + "includes": [ + "src" + ], + "files": [ + "src/helper.c" + ] + } + ] } \ No newline at end of file diff --git a/tools/hello/src/helper.c b/tools/hello/src/helper.c new file mode 100644 index 00000000000..4367d076a2d --- /dev/null +++ b/tools/hello/src/helper.c @@ -0,0 +1,15 @@ +/* + * Copyright (c) 2025 RT-Thread Development Team + * + * SPDX-License-Identifier: Apache-2.0 + * + * Change Logs: + * Date Author Notes + * 2025-08-03 Bernard First version + */ + +#include "helper.h" + +void hello_helper() +{ +} diff --git a/tools/hello/src/helper.h b/tools/hello/src/helper.h new file mode 100644 index 00000000000..bf8934217dc --- /dev/null +++ b/tools/hello/src/helper.h @@ -0,0 +1,16 @@ +/* + * Copyright (c) 2025 RT-Thread Development Team + * + * SPDX-License-Identifier: Apache-2.0 + * + * Change Logs: + * Date Author Notes + * 2025-08-03 Bernard First version + */ + +#ifndef __HELPER__H__ +#define __HELPER__H__ + +void hello_helper(); + +#endif //!__HELPER__H__ diff --git a/tools/ng/README.md b/tools/ng/README.md new file mode 100644 index 00000000000..021a796e491 --- /dev/null +++ b/tools/ng/README.md @@ -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 { + <> + +get_name(): str + +detect(): bool + +configure_environment(env) + } + + class ProjectGenerator { + <> + +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许可证。 \ No newline at end of file diff --git a/tools/ng/__init__.py b/tools/ng/__init__.py new file mode 100644 index 00000000000..4ce70feea3a --- /dev/null +++ b/tools/ng/__init__.py @@ -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' +] \ No newline at end of file diff --git a/tools/ng/adapter.py b/tools/ng/adapter.py new file mode 100644 index 00000000000..64a2a17dfc9 --- /dev/null +++ b/tools/ng/adapter.py @@ -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) \ No newline at end of file diff --git a/tools/ng/building_ng.py b/tools/ng/building_ng.py new file mode 100644 index 00000000000..09d8a8f7ecd --- /dev/null +++ b/tools/ng/building_ng.py @@ -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('_')] \ No newline at end of file diff --git a/tools/ng/config.py b/tools/ng/config.py new file mode 100644 index 00000000000..fbf54431ea3 --- /dev/null +++ b/tools/ng/config.py @@ -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 \ No newline at end of file diff --git a/tools/ng/core.py b/tools/ng/core.py new file mode 100644 index 00000000000..c1960862d8a --- /dev/null +++ b/tools/ng/core.py @@ -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) \ No newline at end of file diff --git a/tools/ng/environment.py b/tools/ng/environment.py new file mode 100644 index 00000000000..34232636831 --- /dev/null +++ b/tools/ng/environment.py @@ -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', '') \ No newline at end of file diff --git a/tools/ng/generator.py b/tools/ng/generator.py new file mode 100644 index 00000000000..6c252d84cbf --- /dev/null +++ b/tools/ng/generator.py @@ -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()) \ No newline at end of file diff --git a/tools/ng/integration_example.py b/tools/ng/integration_example.py new file mode 100644 index 00000000000..22b1bd5c2f2 --- /dev/null +++ b/tools/ng/integration_example.py @@ -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 +""" \ No newline at end of file diff --git a/tools/ng/project.py b/tools/ng/project.py new file mode 100644 index 00000000000..4bd3bfab501 --- /dev/null +++ b/tools/ng/project.py @@ -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() \ No newline at end of file diff --git a/tools/ng/toolchain.py b/tools/ng/toolchain.py new file mode 100644 index 00000000000..007ccf94ada --- /dev/null +++ b/tools/ng/toolchain.py @@ -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()) \ No newline at end of file diff --git a/tools/ng/utils.py b/tools/ng/utils.py new file mode 100644 index 00000000000..a0503ea481e --- /dev/null +++ b/tools/ng/utils.py @@ -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 \ No newline at end of file diff --git a/tools/package.py b/tools/package.py index f7a2ffa745d..37a53f47bd0 100644 --- a/tools/package.py +++ b/tools/package.py @@ -41,41 +41,89 @@ def ExtendPackageVar(package, var): def BuildPackage(package = None): if package is None: package = os.path.join(GetCurrentDir(), 'package.json') - - if not os.path.isfile(package): - print("%s/package.json not found" % GetCurrentDir()) - return [] - - f = open(package) - package_json = f.read() + elif os.path.isdir(package): + # support directory path + package = os.path.join(package, 'package.json') # get package.json path - cwd = os.path.dirname(package) - - package = json.loads(package_json) + cwd = os.path.dirname(os.path.abspath(package)) - # check package name - if 'name' not in package or 'type' not in package or package['type'] != 'rt-package': + if not os.path.isfile(package): + # silent return for conditional usage return [] - # get depends - depend = ExtendPackageVar(package, 'depends') - - src = [] - if 'source_files' in package: - for src_file in package['source_files']: - src_file = os.path.join(cwd, src_file) - src += Glob(src_file) - - CPPPATH = [] - if 'CPPPATH' in package: - for path in package['CPPPATH']: - if path.startswith('/') and os.path.isdir(path): - CPPPATH = CPPPATH + [path] - else: - CPPPATH = CPPPATH + [os.path.join(cwd, path)] - - CPPDEFINES = ExtendPackageVar(package, 'CPPDEFINES') + with open(package, 'r') as f: + package_json = f.read() + package = json.loads(package_json) + + # check package name + if 'name' not in package or 'type' not in package or package['type'] != 'rt-thread-component': + return [] + + # get depends + depend = [] + if 'dependencies' in package: + depend = ExtendPackageVar(package, 'dependencies') + + # check dependencies + if depend: + group_enable = False + for item in depend: + if GetDepend(item): + group_enable = True + break + if not group_enable: + return [] + + CPPDEFINES = [] + if 'defines' in package: + CPPDEFINES = ExtendPackageVar(package, 'defines') + + src = [] + CPPPATH = [] + if 'sources' in package: + src_depend = [] + src_CPPPATH = [] + for item in package['sources']: + if 'includes' in item: + includes = item['includes'] + for include in includes: + if include.startswith('/') and os.path.isdir(include): + src_CPPPATH = src_CPPPATH + [include] + else: + path = os.path.abspath(os.path.join(cwd, include)) + src_CPPPATH = src_CPPPATH + [path] + + if 'dependencies' in item: + src_depend = src_depend + ExtendPackageVar(item, 'dependencies') + + src_enable = False + if src_depend == []: + src_enable = True + else: + for d in src_depend: + if GetDepend(d): + src_enable = True + break + + if src_enable: + files = [] + src_files = [] + if 'files' in item: + files += ExtendPackageVar(item, 'files') + + for item in files: + # handle glob patterns relative to package.json directory + old_dir = os.getcwd() + os.chdir(cwd) + try: + src_files += Glob(item) + finally: + os.chdir(old_dir) + + src += src_files + + CPPPATH += src_CPPPATH objs = DefineGroup(package['name'], src, depend = depend, CPPPATH = CPPPATH, CPPDEFINES = CPPDEFINES)