SConscript编写指南.md 20 KB

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. 基本结构

# 导入构建模块
from building import *

# 获取当前目录
cwd = GetCurrentDir()

# 定义源文件
src = ['main.c', 'app.c']

# 定义头文件路径
CPPPATH = [cwd]

# 定义组
group = DefineGroup('Applications', src, depend = [''], CPPPATH = CPPPATH)

# 返回组对象
Return('group')

2. 导入building模块

from building import *

这行代码导入了RT-Thread构建系统的所有函数,包括:

  • GetCurrentDir() - 获取当前目录
  • DefineGroup() - 定义组件组
  • GetDepend() - 检查依赖
  • Glob() - 文件通配符匹配
  • SrcRemove() - 移除源文件
  • DoBuilding() - 执行构建

3. 源文件定义

手动指定文件列表

src = ['file1.c', 'file2.c', 'file3.c']

使用通配符

src = Glob('*.c')           # 当前目录所有.c文件
src = Glob('src/*.c')       # src子目录所有.c文件
src = Glob('**/*.c')        # 递归所有子目录的.c文件

混合使用

src = ['main.c'] + Glob('drivers/*.c')

4. 条件编译

基于宏定义

src = ['common.c']

if GetDepend('RT_USING_SERIAL'):
    src += ['serial.c']

if GetDepend('RT_USING_I2C'):
    src += ['i2c.c']

基于平台

import rtconfig

if rtconfig.PLATFORM == 'gcc':
    src += ['gcc_specific.c']
elif rtconfig.PLATFORM == 'armcc':
    src += ['keil_specific.c']

5. 移除文件

src = Glob('*.c')
SrcRemove(src, ['test.c', 'debug.c'])  # 移除不需要的文件

常用模式

1. 简单组件模式

最基础的SConscript模式:

from building import *

src = Glob('*.c')
CPPPATH = [GetCurrentDir()]

group = DefineGroup('MyComponent', src, depend = ['RT_USING_MYCOMPONENT'], 
                    CPPPATH = CPPPATH)

Return('group')

2. 条件编译模式

根据配置选项包含不同的源文件:

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. 多目录组织模式

处理复杂的目录结构:

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:

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. 库文件链接模式

链接预编译的库文件:

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. 本地编译选项

为特定模块设置独立的编译选项:

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. 动态源文件生成

在构建时生成源文件:

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. 复杂依赖处理

处理复杂的依赖关系:

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. 平台特定实现

根据不同平台包含不同实现:

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. 第三方库集成

集成第三方库的模板:

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:

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. 依赖管理最佳实践

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. 错误处理

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. 性能优化

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. 文档化

"""
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

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

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

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

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?

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: 如何处理可选的依赖?

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: 如何支持多个工具链?

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: 如何处理生成的代码?

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: 如何组织大型项目?

# 主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项目的构建提供坚实的基础。