From 009ee1944f1f99e2c33bfc847dab6b911c0de2d6 Mon Sep 17 00:00:00 2001 From: bernard Date: Fri, 1 Aug 2025 21:33:50 +0800 Subject: [PATCH 1/3] [Tools] Add documents for tools script; Add NG for tools --- tools/docs/README.md | 162 +++ ...26\345\206\231\346\214\207\345\215\227.md" | 948 ++++++++++++++++++ ...77\347\224\250\346\214\207\345\215\227.md" | 643 ++++++++++++ ...00\346\234\257\345\216\237\347\220\206.md" | 912 +++++++++++++++++ tools/ng/README.md | 345 +++++++ tools/ng/__init__.py | 25 + tools/ng/adapter.py | 218 ++++ tools/ng/building_ng.py | 115 +++ tools/ng/config.py | 297 ++++++ tools/ng/core.py | 176 ++++ tools/ng/environment.py | 304 ++++++ tools/ng/generator.py | 368 +++++++ tools/ng/integration_example.py | 178 ++++ tools/ng/project.py | 260 +++++ tools/ng/toolchain.py | 396 ++++++++ tools/ng/utils.py | 339 +++++++ 16 files changed, 5686 insertions(+) create mode 100644 tools/docs/README.md create mode 100644 "tools/docs/SConscript\347\274\226\345\206\231\346\214\207\345\215\227.md" create mode 100644 "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" create mode 100644 "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" create mode 100644 tools/ng/README.md create mode 100644 tools/ng/__init__.py create mode 100644 tools/ng/adapter.py create mode 100644 tools/ng/building_ng.py create mode 100644 tools/ng/config.py create mode 100644 tools/ng/core.py create mode 100644 tools/ng/environment.py create mode 100644 tools/ng/generator.py create mode 100644 tools/ng/integration_example.py create mode 100644 tools/ng/project.py create mode 100644 tools/ng/toolchain.py create mode 100644 tools/ng/utils.py diff --git a/tools/docs/README.md b/tools/docs/README.md new file mode 100644 index 00000000000..295f6a4c030 --- /dev/null +++ b/tools/docs/README.md @@ -0,0 +1,162 @@ +# 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**: 检查依赖的核心函数 + +## 构建系统架构图 + +``` +┌─────────────────────────────────────┐ +│ 用户命令 (scons) │ +└──────────────┬──────────────────────┘ + │ +┌──────────────▼──────────────────────┐ +│ SConstruct (主脚本) │ +│ ┌─────────────────────┐ │ +│ │ PrepareBuilding() │ │ +│ │ 环境初始化 │ │ +│ └──────────┬──────────┘ │ +└────────────────┼───────────────────┘ + │ +┌────────────────▼────────────────────┐ +│ building.py │ +│ ┌──────────┬──────────┐ │ +│ │ 组件收集 │ 依赖处理 │ │ +│ └──────────┴──────────┘ │ +└─────────────────────────────────────┘ + │ + ┌────────┴────────┐ + │ │ +┌───────▼──────┐ ┌────────▼────────┐ +│ SConscript │ │ rtconfig.h │ +│ 组件脚本 │ │ 功能配置 │ +└──────────────┘ └─────────────────┘ +``` + +## 主要特性 + +✅ **多工具链支持** +- 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/\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..1e54f1d4d98 --- /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,643 @@ +# RT-Thread 构建系统使用指南 + +## 目录 + +1. [概述](#概述) +2. [快速开始](#快速开始) +3. [命令行选项详解](#命令行选项详解) +4. [工具链配置](#工具链配置) +5. [项目生成](#项目生成) +6. [软件包管理](#软件包管理) +7. [高级功能](#高级功能) +8. [常见问题](#常见问题) + +## 概述 + +RT-Thread使用基于SCons的构建系统,提供了统一的跨平台构建体验。构建系统支持: + +- 多种编译器和IDE(GCC、Keil、IAR、VS Code等) +- 模块化的组件管理 +- 灵活的配置系统 +- 自动化的依赖处理 +- 软件包管理功能 + +### 系统架构图 + +``` +┌─────────────────────────────────────────────────────────────┐ +│ 用户命令 │ +│ (scons, scons --target=xxx) │ +└─────────────────────┬───────────────────────────────────────┘ + │ +┌─────────────────────▼───────────────────────────────────────┐ +│ SConstruct │ +│ (BSP根目录构建脚本) │ +└─────────────────────┬───────────────────────────────────────┘ + │ +┌─────────────────────▼───────────────────────────────────────┐ +│ building.py │ +│ (核心构建引擎和函数库) │ +├─────────────────────┬───────────────────────────────────────┤ +│ PrepareBuilding() │ DefineGroup() │ DoBuilding() │ +│ 环境初始化 │ 组件定义 │ 执行构建 │ +└─────────────────────┴─────────┬─────────────────────────────┘ + │ + ┌───────────────────────┼───────────────────────┐ + │ │ │ +┌───────▼────────┐ ┌────────▼────────┐ ┌────────▼────────┐ +│ SConscript │ │ rtconfig.py │ │ rtconfig.h │ +│ (组件脚本) │ │ (工具链配置) │ │ (功能配置) │ +└────────────────┘ └─────────────────┘ └─────────────────┘ +``` + +## 快速开始 + +### 基本编译流程 + +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..d0a9ac0aaae --- /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,912 @@ +# RT-Thread 构建系统技术原理 + +## 目录 + +1. [系统架构设计](#系统架构设计) +2. [核心模块分析](#核心模块分析) +3. [构建流程详解](#构建流程详解) +4. [依赖管理机制](#依赖管理机制) +5. [工具链适配层](#工具链适配层) +6. [项目生成器架构](#项目生成器架构) +7. [配置系统实现](#配置系统实现) +8. [扩展机制](#扩展机制) + +## 系统架构设计 + +### 整体架构图 + +``` +┌────────────────────────────────────────────────────────────────────┐ +│ 用户接口层 │ +│ ┌─────────────┐ ┌──────────────┐ ┌─────────────┐ ┌──────────┐│ +│ │ 命令行接口 │ │ menuconfig │ │ 环境变量 │ │ 配置文件 ││ +│ │ (scons) │ │ (Kconfig) │ │ (RTT_xxx) │ │ (.config)││ +│ └──────┬──────┘ └──────┬───────┘ └──────┬──────┘ └─────┬────┘│ +└─────────┼─────────────────┼──────────────────┼───────────────┼─────┘ + │ │ │ │ +┌─────────▼─────────────────▼──────────────────▼───────────────▼─────┐ +│ 构建引擎层 │ +│ ┌─────────────────────────────────────────────────────────────┐ │ +│ │ building.py │ │ +│ ├─────────────┬───────────────┬─────────────┬────────────────┤ │ +│ │ 环境准备 │ 组件收集 │ 依赖处理 │ 构建执行 │ │ +│ │ Prepare │ DefineGroup │ GetDepend │ DoBuilding │ │ +│ └─────────────┴───────────────┴─────────────┴────────────────┘ │ +└────────────────────────────────────────────────────────────────────┘ + │ │ │ │ +┌─────────▼─────────────────▼──────────────────▼───────────────▼─────┐ +│ 工具支撑层 │ +│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌─────────┐ ┌──────┐│ +│ │ utils.py │ │options.py│ │package.py│ │mkdist.py│ │预处理││ +│ │ 工具函数 │ │命令选项 │ │包管理 │ │分发打包 │ │器 ││ +│ └──────────┘ └──────────┘ └──────────┘ └─────────┘ └──────┘│ +└────────────────────────────────────────────────────────────────────┘ + │ │ │ │ +┌─────────▼─────────────────▼──────────────────▼───────────────▼─────┐ +│ 目标生成器层 │ +│ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────────┐ │ +│ │keil.py │ │iar.py │ │gcc.py │ │vsc.py │ │cmake.py等 │ │ +│ └────────┘ └────────┘ └────────┘ └────────┘ └────────────┘ │ +└────────────────────────────────────────────────────────────────────┘ +``` + +### 设计原则 + +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) +``` + +## 构建流程详解 + +### 完整构建流程图 + +``` +┌──────────────┐ +│ 用户输入 │ +│ scons │ +└──────┬───────┘ + │ +┌──────▼───────┐ +│ SConstruct │ ← 读取rtconfig.py +│ 主脚本 │ ← 调用PrepareBuilding +└──────┬───────┘ + │ +┌──────▼───────┐ +│ 环境初始化 │ +│ ·设置路径 │ +│ ·检测工具链 │ +│ ·解析配置 │ +└──────┬───────┘ + │ +┌──────▼───────┐ +│ 递归处理 │ +│ SConscript │ ← 调用DefineGroup +│ ·收集源文件 │ ← 检查依赖GetDepend +│ ·设置参数 │ +└──────┬───────┘ + │ +┌──────▼───────┐ +│ 构建执行 │ +│ DoBuilding │ +│ ·合并对象 │ +│ ·链接程序 │ +└──────┬───────┘ + │ +┌──────▼───────┐ +│ 后处理 │ +│ ·生成bin文件 │ +│ ·显示大小 │ +│ ·自定义动作 │ +└──────────────┘ +``` + +### 依赖解析流程 + +```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/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..3646cfad7da --- /dev/null +++ b/tools/ng/environment.py @@ -0,0 +1,304 @@ +# -*- 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 or directory containing it + + Returns: + List of build objects + """ + import json + + # Find package.json + if package_path is None: + package_path = 'package.json' + elif os.path.isdir(package_path): + package_path = os.path.join(package_path, 'package.json') + + if not os.path.exists(package_path): + env.GetContext().logger.error(f"Package file not found: {package_path}") + return [] + + # Load package definition + with open(package_path, 'r') as f: + package = json.load(f) + + # Process package + name = package.get('name', 'unnamed') + dependencies = package.get('dependencies', {}) + + # Check main dependency + if 'RT_USING_' + name.upper() not in dependencies: + main_depend = 'RT_USING_' + name.upper().replace('-', '_') + else: + main_depend = list(dependencies.keys())[0] + + if not env.GetDepend(main_depend): + return [] + + # Collect sources + src = [] + include_paths = [] + + sources = package.get('sources', {}) + for category, config in sources.items(): + # Check condition + condition = config.get('condition') + if condition and not eval(condition, {'env': env, 'GetDepend': env.GetDepend}): + continue + + # Add source files + source_files = config.get('source_files', []) + for pattern in source_files: + src.extend(env.Glob(pattern)) + + # Add header paths + header_path = config.get('header_path', []) + include_paths.extend(header_path) + + # Create group + return env.DefineGroup(name, src, depend=main_depend, CPPPATH=include_paths) + + @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 From 93271df2cb4237041a54d29b1c726adf8353880f Mon Sep 17 00:00:00 2001 From: bernard Date: Fri, 1 Aug 2025 22:46:14 +0800 Subject: [PATCH 2/3] [Tools] Update images --- tools/docs/README.md | 28 +------ tools/docs/guide_arch.drawio.png | Bin 0 -> 46166 bytes tools/docs/process.drawio.png | Bin 0 -> 58609 bytes tools/docs/readme_arch.drawio.png | Bin 0 -> 34692 bytes tools/docs/tech_arch.drawio.png | Bin 0 -> 50781 bytes ...77\347\224\250\346\214\207\345\215\227.md" | 27 +------ ...00\346\234\257\345\216\237\347\220\206.md" | 75 +----------------- 7 files changed, 4 insertions(+), 126 deletions(-) create mode 100644 tools/docs/guide_arch.drawio.png create mode 100644 tools/docs/process.drawio.png create mode 100644 tools/docs/readme_arch.drawio.png create mode 100644 tools/docs/tech_arch.drawio.png diff --git a/tools/docs/README.md b/tools/docs/README.md index 295f6a4c030..a1c34073cb3 100644 --- a/tools/docs/README.md +++ b/tools/docs/README.md @@ -69,33 +69,7 @@ pkgs --list # 列出已安装包 ## 构建系统架构图 -``` -┌─────────────────────────────────────┐ -│ 用户命令 (scons) │ -└──────────────┬──────────────────────┘ - │ -┌──────────────▼──────────────────────┐ -│ SConstruct (主脚本) │ -│ ┌─────────────────────┐ │ -│ │ PrepareBuilding() │ │ -│ │ 环境初始化 │ │ -│ └──────────┬──────────┘ │ -└────────────────┼───────────────────┘ - │ -┌────────────────▼────────────────────┐ -│ building.py │ -│ ┌──────────┬──────────┐ │ -│ │ 组件收集 │ 依赖处理 │ │ -│ └──────────┴──────────┘ │ -└─────────────────────────────────────┘ - │ - ┌────────┴────────┐ - │ │ -┌───────▼──────┐ ┌────────▼────────┐ -│ SConscript │ │ rtconfig.h │ -│ 组件脚本 │ │ 功能配置 │ -└──────────────┘ └─────────────────┘ -``` +![arch](./readme_arch.drawio.png) ## 主要特性 diff --git a/tools/docs/guide_arch.drawio.png b/tools/docs/guide_arch.drawio.png new file mode 100644 index 0000000000000000000000000000000000000000..d2e49457441f79dde2be654dab687de8216e90a2 GIT binary patch literal 46166 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 literal 0 HcmV?d00001 diff --git a/tools/docs/process.drawio.png b/tools/docs/process.drawio.png new file mode 100644 index 0000000000000000000000000000000000000000..af03d7593e7b29688dcb6bf619b346128e26cc4e GIT binary patch literal 58609 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 literal 0 HcmV?d00001 diff --git a/tools/docs/tech_arch.drawio.png b/tools/docs/tech_arch.drawio.png new file mode 100644 index 0000000000000000000000000000000000000000..901f665168df2873543ce2f9d69909ffdf51a4b2 GIT binary patch literal 50781 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 Date: Sun, 3 Aug 2025 03:57:54 +0800 Subject: [PATCH 3/3] [Feature][Tools] Add support for package.json, refactor BuildPackage function to handle new format. --- tools/docs/package-json-support.md | 128 +++++++++++++++++++++++++++++ tools/hello/package.json | 45 ++++++---- tools/hello/src/helper.c | 15 ++++ tools/hello/src/helper.h | 16 ++++ tools/ng/environment.py | 84 +++++++++---------- tools/package.py | 108 +++++++++++++++++------- 6 files changed, 304 insertions(+), 92 deletions(-) create mode 100644 tools/docs/package-json-support.md create mode 100644 tools/hello/src/helper.c create mode 100644 tools/hello/src/helper.h 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/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/environment.py b/tools/ng/environment.py index 3646cfad7da..34232636831 100644 --- a/tools/ng/environment.py +++ b/tools/ng/environment.py @@ -170,62 +170,56 @@ def BuildPackage(env, package_path: str = None) -> List: Args: env: SCons Environment - package_path: Path to package.json or directory containing it + package_path: Path to package.json. If None, looks for package.json in current directory. Returns: List of build objects """ - import json + # Import the existing package module + import sys + import os - # Find package.json - if package_path is None: - package_path = 'package.json' - elif os.path.isdir(package_path): - package_path = os.path.join(package_path, 'package.json') - - if not os.path.exists(package_path): - env.GetContext().logger.error(f"Package file not found: {package_path}") - return [] - - # Load package definition - with open(package_path, 'r') as f: - package = json.load(f) - - # Process package - name = package.get('name', 'unnamed') - dependencies = package.get('dependencies', {}) + # Get the building module path + building_path = os.path.dirname(os.path.abspath(__file__)) + tools_path = os.path.dirname(building_path) - # Check main dependency - if 'RT_USING_' + name.upper() not in dependencies: - main_depend = 'RT_USING_' + name.upper().replace('-', '_') - else: - main_depend = list(dependencies.keys())[0] + # 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 - if not env.GetDepend(main_depend): - return [] + # BuildPackage uses global functions, so we need to set up the context + # Save current directory + current_dir = os.getcwd() - # Collect sources - src = [] - include_paths = [] - - sources = package.get('sources', {}) - for category, config in sources.items(): - # Check condition - condition = config.get('condition') - if condition and not eval(condition, {'env': env, 'GetDepend': env.GetDepend}): - continue + # 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) - # Add source files - source_files = config.get('source_files', []) - for pattern in source_files: - src.extend(env.Glob(pattern)) + os.chdir(work_dir) + + try: + # Call the original BuildPackage + result = build_package_func(package_path) + finally: + # Restore directory + os.chdir(current_dir) - # Add header paths - header_path = config.get('header_path', []) - include_paths.extend(header_path) + return result - # Create group - return env.DefineGroup(name, src, depend=main_depend, CPPPATH=include_paths) + 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]: 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)