SConscript 脚本入门教程
第一章:什么是 SCons 和 SConscript?
核心概念
SCons 是一个现代化的构建工具,用于自动化软件构建过程,类似于 Make 但功能更强大、语法更简洁。
- SConstruct:是 SCons 的主配置文件,通常在项目根目录,相当于 Makefile
- SConscript:是子配置文件,用于组织大型项目,可被 SConstruct 或其他 SConscript 包含
- 构建过程:将源代码转换为可执行文件的过程(编译、链接等)
SCons 使用 Python 语法,因此如果你熟悉 Python,学习 SCons 会更容易。
第一个示例
创建一个最简单的 SConscript:
# 编译hello.c并生成可执行文件hello
Program('hello.c')
这个简单的脚本告诉 SCons:编译 hello.c 文件并生成同名可执行文件。
练习题
- 什么是 SCons?它的主要作用是什么?
- SConstruct 和 SConscript 有什么区别?
- 写出一个 SConscript 脚本,用于编译 main.c 生成名为 app 的可执行文件。
答案详解
1.SCons 是一个构建工具,主要作用是自动化软件的编译、链接等构建过程,简化项目管理,提高开发效率。与传统的 Make 相比,它语法更简洁,功能更强大,且跨平台性更好。
2.区别在于:
- SConstruct 是主配置文件,是 SCons 的入口点
- SConscript 是子配置文件,用于组织大型项目
- 一个项目通常有一个 SConstruct,可能有多个 SConscript
- SConstruct 可以包含并执行 SConscript
3.实现代码:
# 编译main.c生成名为app的可执行文件
Program('app', ['main.c'])
这里Program
是 SCons 的一个构建器 (builder),第一个参数是输出文件名,第二个参数是源文件列表。
第二章:SConscript 基本语法
核心概念
SConscript 使用 Python 语法,同时提供了一些 SCons 特有的函数和变量:
- 构建器 (Builder):如
Program
(生成可执行文件)、Library
(生成库文件) 等 - 路径处理:
GetCurrentDir()
获取当前目录 - 文件匹配:
Glob()
匹配符合模式的文件 - 依赖管理:处理文件间的依赖关系
常用函数
Program(target, sources)
:编译源文件生成可执行文件
Program('myapp', ['main.c', 'utils.c'])
Library(target, sources)
:生成库文件
Library('mylib', ['func1.c', 'func2.c'])
GetCurrentDir()
:获取当前目录路径
current_dir = GetCurrentDir()
print(f"当前目录: {current_dir}")
Glob(pattern)
:匹配文件
# 获取所有.c文件
c_files = Glob('*.c')
# 获取src目录下所有.c文件
src_files = Glob('src/*.c')
Return(value)
:返回值给父脚本
src_files = Glob('*.c')
Return('src_files') # 将src_files返回给包含此脚本的父脚本
练习题
1.请写出一段 SConscript 代码,实现以下功能:
获取当前目录下所有的.c 文件
获取 src 目录下所有的.c 文件
将这两部分文件合并到一个列表中
打印出收集到的所有文件路径
2.
编写一个完整的 SConscript 脚本,实现以下功能:
编译 math 目录下所有的.c 文件生成名为 libmath 的静态库
编译 main.c 文件,并链接 libmath 库生成名为 calculator 的可执行文件
确保编译器能找到 math 目录下的头文件
答案详解
1. 实现代码:
# 获取当前目录下所有的.c文件
current_c_files = Glob('*.c')# 获取src目录下所有的.c文件
src_c_files = Glob('src/*.c')# 合并两个文件列表
all_c_files = current_c_files + src_c_files# 打印收集到的文件路径
print("收集到的C源文件:")
for file in all_c_files:print(f"- {file}")
解析:
Glob(pattern)
是 SCons 提供的文件匹配函数,用于查找符合模式的文件*.c
表示匹配当前目录下所有以.c 结尾的文件src/*.c
表示匹配 src 子目录下所有以.c 结尾的文件+
运算符用于合并两个列表(Python 语法)- 通过 for 循环遍历并打印所有文件路径,方便查看收集结果
2.实现代码:
# 编译math目录下所有.c文件生成libmath静态库
# Library是SCons的库文件构建器,第一个参数是库名,第二个是源文件
math_library = Library('math', Glob('math/*.c'))# 编译main.c并链接libmath库生成calculator可执行文件
# Program是SCons的可执行文件构建器
# CPPPATH指定头文件搜索路径,确保编译器能找到math目录下的头文件
# LIBS指定要链接的库文件
Program(target='calculator', # 输出的可执行文件名source=['main.c'], # 源文件CPPPATH=['math'], # 头文件搜索路径LIBS=['math'] # 要链接的库
)
解析:
Library('math', Glob('math/*.c'))
:生成名为 libmath 的静态库(在 Linux 下实际生成 libmath.a,Windows 下生成 math.lib)Glob('math/*.c')
:自动收集 math 目录下所有的 C 源文件CPPPATH=['math']
:告诉编译器在 math 目录中查找头文件(相当于 gcc 的 - I 选项)LIBS=['math']
:指定链接名为 math 的库(SCons 会自动处理库文件的路径和命名规则)- 这种分离编译的好处是:如果 math 目录下的文件没有修改,再次构建时不会重新编译,提高构建效率
第三章:变量和配置
核心概念
在 SConscript 中,变量用于存储配置信息、文件列表、编译选项等:
- 路径变量:存储目录路径,如源文件目录、头文件目录
- 文件列表变量:存储源文件列表
- 编译选项变量:如
CPPPATH
(头文件搜索路径)、CFLAGS
(C 编译选项) - 条件变量:根据不同平台或配置定义不同的变量值
示例代码
# 定义变量
cwd = GetCurrentDir() # 当前目录
src_files = [] # 源文件列表
inc_paths = [cwd, cwd + '/include'] # 头文件路径# 添加源文件
src_files += Glob('*.c')
src_files += Glob('src/*.c')# 设置编译选项
CPPPATH = inc_paths # 头文件搜索路径
CFLAGS = '-Wall -O2' # C编译选项# 根据条件修改变量
import os
if os.name == 'nt': # Windows系统CFLAGS += ' -DWIN32'
else: # 类Unix系统CFLAGS += ' -DUNIX'# 使用变量
Program('myapp', src_files, CPPPATH=CPPPATH, CFLAGS=CFLAGS)
练习题
1.解释以下变量的含义和作用:CPPPATH
、CFLAGS
、LIBS
、LIBPATH
。
2.编写一个 SConscript 脚本,实现以下功能:
- 定义一个变量存储当前目录路径
- 定义源文件列表,包含当前目录和 src 子目录下的所有.c 文件
- 定义头文件搜索路径,包含当前目录、include 目录和 src/include 目录
- 为 GCC 编译器设置编译选项:开启所有警告、将警告视为错误、优化级别为 O2
- 为 Windows 系统添加宏定义
_WIN32
,为 Linux 系统添加宏定义_LINUX
- 编译生成名为 myapp 的可执行文件
答案详解
1.各变量含义和作用:
CPPPATH
:C 预处理器的头文件搜索路径列表。告诉编译器去哪里查找#include
指令引用的头文件。
示例:CPPPATH=['include', 'src/include']
相当于 gcc 的-Iinclude -Isrc/include
选项。
CFLAGS
:C 编译器的编译选项。用于设置警告级别、优化级别、宏定义等。
示例:CFLAGS='-Wall -O2'
表示开启所有警告并使用 O2 级优化。
LIBS
:需要链接的库文件列表。指定程序运行时依赖的库。
示例:LIBS=['m', 'pthread']
表示链接数学库和线程库。
LIBPATH
:库文件的搜索路径列表。告诉链接器去哪里查找需要链接的库文件。
示例:LIBPATH=['lib', '/usr/local/lib']
相当于 gcc 的-Llib -L/usr/local/lib
选项。
2. 实现代码:
# 导入os模块用于判断操作系统类型
import os# 定义当前目录路径变量
current_dir = GetCurrentDir()# 定义源文件列表:当前目录和src子目录下的所有.c文件
source_files = Glob('*.c') + Glob('src/*.c')# 定义头文件搜索路径
include_paths = [current_dir, # 当前目录current_dir + '/include', # include目录current_dir + '/src/include' # src/include目录
]# 初始化编译选项
compile_flags = '-Wall -Werror -O2' # 开启所有警告、警告视为错误、O2优化# 根据操作系统添加不同的宏定义
if os.name == 'nt':# Windows系统,添加_WIN32宏定义compile_flags += ' -D_WIN32'
else:# Linux或类Unix系统,添加_LINUX宏定义compile_flags += ' -D_LINUX'# 编译生成myapp可执行文件
Program(target='myapp', # 目标可执行文件名source=source_files, # 源文件列表CPPPATH=include_paths, # 头文件搜索路径CFLAGS=compile_flags # 编译选项
)
解析:
GetCurrentDir()
是 SCons 提供的函数,用于获取当前脚本所在的目录路径Glob('*.c')
和Glob('src/*.c')
分别获取当前目录和 src 目录下的所有.c 文件,+
运算符将两个列表合并include_paths
列表包含了所有需要搜索头文件的目录,确保编译器能找到所有#include
的文件compile_flags
变量集合了所有编译选项,-Wall
开启所有警告,-Werror
将警告视为错误,-O2
设置优化级别-D
选项用于定义宏,在预处理阶段生效,代码中可以通过#ifdef _WIN32
等条件编译指令实现跨平台逻辑os.name
是 Python 的 os 模块提供的变量,用于判断操作系统类型('nt' 表示 Windows,'posix' 表示 Linux/Unix 等)
第四章:条件判断和依赖管理
核心概念
- 条件判断:根据不同平台、架构或配置选项执行不同的构建逻辑
- 依赖管理:处理代码中的依赖关系,如特定功能依赖于某个宏定义
- 平台相关配置:为不同 CPU 架构、操作系统定制构建选项
常用函数和语法
GetDepend(dependencies)
:检查是否存在特定的依赖项
# 检查是否定义了RT_USING_SMP宏
if GetDepend(['RT_USING_SMP']):print("启用了SMP支持")
字典用于存储平台 / 架构相关配置
# 支持的架构和CPU
support_arch = {"arm": ["cortex-m3", "cortex-m4"],"risc-v": ["na900"]
}
条件性添加源文件
src = []
# 根据架构添加不同的源文件
if arch == "arm":src += Glob('arch/arm/*.c')
elif arch == "risc-v":src += Glob('arch/risc-v/*.c')
示例代码
# 导入必要的模块和配置
import os
from building import *# 获取平台和架构信息
platform = rtconfig.PLATFORM
arch = rtconfig.ARCH
cpu = rtconfig.CPU# 初始化变量
cwd = GetCurrentDir()
src = []
CPPPATH = [cwd, cwd + '/include']# 定义支持的架构和CPU
support_arch = {"arm": ["cortex-m3", "cortex-m4"],"risc-v": ["na900"]
}# 根据CPU类型设置不同的源文件
if arch in support_arch.keys() and cpu in support_arch[arch]:# 添加对应架构和CPU的源文件src += Glob('arch/' + arch + '/' + cpu + '/*.c')# 添加公共架构代码src += Glob('arch/' + arch + '/common/*.c')# 添加通用代码src += Glob('*.c')# 设置头文件路径CPPPATH.append(cwd + '/arch/' + arch + '/' + cpu)# 根据配置选项移除不需要的文件
if not GetDepend('RT_USING_MEMORY_PROTECTION'):# 移除内存保护相关文件SrcRemove(src, ['mpu.c'])# 定义编译选项
LOCAL_CFLAGS = '-Wall'# 创建构建组并返回
group = DefineGroup('core', src, depend = ['RT_USING_CORE'], CPPPATH = CPPPATH, LOCAL_CFLAGS = LOCAL_CFLAGS)Return('group')
答案详解
1. GetDepend()
函数详解:
作用:检查是否存在指定的依赖项,主要用于判断是否定义了特定的宏或配置选项。
参数格式:接受一个列表作为参数,列表中包含要检查的依赖项名称(字符串类型)。
示例:GetDepend(['RT_USING_SMP', 'RT_USING_MMU'])
返回值:返回一个布尔值(True 或 False)。如果所有指定的依赖项都存在,则返回 True;否则返回 False。
使用场景:
根据宏定义决定是否包含某些源文件
启用或禁用特定功能模块
为不同配置提供不同的编译选项
示例代码:
# 检查是否定义了RT_USING_MEMORY_PROTECTION宏
if GetDepend(['RT_USING_MEMORY_PROTECTION']):# 如果定义了,则添加内存保护相关的源文件src += ['mpu.c', 'memory_protect.c']
else:# 如果未定义,则添加普通内存管理文件src += ['memory.c']
这段代码根据是否启用内存保护功能,选择不同的源文件进行编译,实现了条件性构建。
实现代码:
# 定义不同编译器对应的启动文件
startup_files = {'gcc': 'startup_gcc.s','armcc': 'startup_armcc.s','iccarm': 'startup_iccarm.s'
}# 假设这些变量是从配置中获取的
platform = rtconfig.PLATFORM # 当前使用的编译器
arch = rtconfig.ARCH # 当前架构# 初始化源文件列表
src = []# 选择默认启动文件
if platform in startup_files.keys():selected_startup = startup_files[platform]# 如果是ARM架构且启用了SMP,则使用带smp的启动文件if arch == "arm" and GetDepend(['RT_USING_SMP']):# 替换文件名,添加_smp后缀(如startup_gcc.s -> startup_gcc_smp.s)selected_startup = selected_startup.replace('.s', '_smp.s')# 将选择的启动文件添加到源文件列表src.append(selected_startup)
else:# 如果编译器不被支持,打印警告信息print(f"警告:不支持的编译器 {platform},未添加启动文件")# 可以继续添加其他源文件
src += Glob('*.c')
解析:
startup_files
字典使用键值对存储不同编译器对应的启动文件,便于根据编译器类型快速查找platform in startup_files.keys()
用于检查当前编译器是否在支持的列表中,避免使用未定义的启动文件GetDepend(['RT_USING_SMP'])
检查是否启用了 SMP(对称多处理)功能,这是嵌入式系统中常见的配置选项selected_startup.replace('.s', '_smp.s')
通过字符串替换生成带 smp 的启动文件名,避免重复编写条件判断src.append(selected_startup)
将选择好的启动文件添加到源文件列表,参与后续的编译过程- 最后的
src += Glob('*.c')
将其他 C 源文件添加到列表中,完成源文件的收集
第五章:项目实战与综合应用
核心概念
综合前面所学的知识,我们可以理解和编写更复杂的 SConscript 脚本,主要包括:
- 项目结构组织
- 多平台 / 架构支持
- 条件编译和配置管理
- 构建组定义和返回
示例解析
让我们解析你提供的示例脚本,理解其工作原理:
from building import *
import os# 获取平台、架构和CPU信息
platform = rtconfig.PLATFORM
arch = rtconfig.ARCH
cpu = rtconfig.CPU# 初始化变量
cwd = GetCurrentDir()
src = []
CPPPATH = [cwd]# 定义支持的架构和CPU
support_arch = {"arm": ["cortex-m3", "cortex-m4", "cortex-m7", "cortex-a", "cortex-r5", "cortex-r52", "cortex-m33"],"aarch64":["cortex-a"],"risc-v": ["na900"],"arc": ["em"],"arch_tricore": ["arch_tc3", "arch_tc4"],"RH850": ["rh850g3kh","rh850g4mh"],
}# 定义不同编译器对应的汇编文件
platform_file = {'armcc': 'rvds.S', 'gcc': 'gcc.S', 'iccarm': 'iar.S', 'mw': 'mw_gcc.S', 'armclang': 'rvds.S', 'ghs':'osa_ghs.S'
}# 根据CPU类型和配置修改汇编文件
if cpu == "cortex-m4":if GetDepend(['RT_USING_SMP']):platform_file[platform] = 'gcc_smp.S'else:platform_file[platform] = 'gcc.S'if cpu == "cortex-m3":if GetDepend(['RT_USING_SMP']):platform_file[platform] = 'iar_smp.S'else:platform_file[platform] = 'iar.S'if cpu == "cortex-m33":if GetDepend(['RT_USING_SMP']):platform_file[platform] = 'gcc_smp.S'else:platform_file[platform] = 'gcc.S'# 处理risc-v架构的CPU
if arch == 'risc-v':rv64 = ['virt64', 'c906']if cpu in rv64:cpu = 'rv64'# 根据平台和架构添加源文件
if platform in platform_file.keys(): # 检查是否支持当前平台if arch in support_arch.keys() and cpu in support_arch[arch]:# 汇编文件路径asm_path = 'arch/' + arch + '/' + cpu + '/*' + platform_file[platform]# 公共架构代码路径arch_common = 'arch/' + arch + '/' + 'common/*.c'# 添加所有源文件src += Glob('*.c') + Glob(asm_path) + Glob(arch_common)src += Glob('arch/' + arch + '/' + cpu + '/*.c')# 设置头文件路径CPPPATH = [cwd, cwd + '/arch/' + arch + '/' + cpu, cwd + '/include']# 处理特殊架构
if arch == "arch_tricore":src = ['arch/arch_tricore/osa_tricore.c']src += Glob('*.c')CPPPATH = [cwd, cwd + '/include']# 移除不需要的文件
if not GetDepend('RT_USING_MEMORY_PROTECTION'):SrcRemove(src, ['osa_mpu.c'])# 设置编译选项
LOCAL_CFLAGS = ''
if rtconfig.PLATFORM in ['gcc']: # 仅对GCC设置LOCAL_CFLAGS = ' -Wall -Werror'# 定义构建组并返回
group = DefineGroup('osa', src, depend = ['RT_USING_OSA'], CPPPATH = CPPPATH, LOCAL_CFLAGS = LOCAL_CFLAGS)Return('group')
这个脚本的主要功能:
- 根据不同的 CPU 架构、类型和编译器选择合适的源文件
- 处理特殊的架构配置
- 根据是否启用内存保护功能决定是否包含相关文件
- 为 GCC 编译器设置特定的编译选项
- 定义一个名为 'osa' 的构建组并返回
练习题
1.在提供的示例脚本中,有这样一段代码:
if not GetDepend('RT_USING_MEMORY_PROTECTION'):SrcRemove(src, ['osa_mpu.c'])
请详细解释这段代码的作用、使用场景和工作原理。
2.如何修改示例脚本,使其支持一种新的 CPU 架构 "risc-v" 的 "c910" 型号?需要修改哪些部分?为什么?
3.解释DefineGroup()
函数的各个参数的含义,并说明为什么在示例脚本的最后要使用Return('group')
。
答案详解
1. 代码解析:
if not GetDepend('RT_USING_MEMORY_PROTECTION'):SrcRemove(src, ['osa_mpu.c'])
作用:当未启用内存保护功能时,从源文件列表中移除内存保护相关的文件osa_mpu.c
。
使用场景:这是条件编译的典型应用,用于根据不同的功能配置包含或排除特定文件。在嵌入式系统中,内存保护(MPU)通常是可选功能,不是所有硬件平台都支持,也不是所有项目都需要。
工作原理:
GetDepend('RT_USING_MEMORY_PROTECTION')
检查是否定义了RT_USING_MEMORY_PROTECTION
宏(通常在配置文件中定义)
not
表示取反,如果未定义该宏(即不启用内存保护),则执行下面的语句
SrcRemove(src, ['osa_mpu.c'])
是 SCons 提供的函数,用于从src
列表中移除osa_mpu.c
文件
这样,在后续的编译过程中,osa_mpu.c
就不会被编译,避免了不必要的代码和可能的编译错误
好处:通过这种方式,可以用一个代码库支持不同的功能配置,无需为不同配置维护多个代码分支,提高了代码的复用性和可维护性。
2. 支持新 CPU 架构 "risc-v" 的 "c910" 型号的修改:
需要修改以下几个部分:
# 1. 在support_arch字典中添加c910支持
support_arch = {# ... 其他架构保持不变"risc-v": ["na900", "c910"], # 添加c910到risc-v支持列表# ... 其他架构保持不变
}# 2. 在risc-v架构处理部分添加c910的支持
if arch == 'risc-v':rv64 = ['virt64', 'c906', 'c910'] # 添加c910到rv64列表if cpu in rv64:cpu = 'rv64' # 将c910归类为rv64架构# 3. 为c910添加特定的汇编文件配置(如果需要)
if arch == 'risc-v' and cpu == 'c910':# 根据是否启用SMP选择不同的汇编文件if GetDepend(['RT_USING_SMP']):platform_file[platform] = 'gcc_riscv_c910_smp.S'else:platform_file[platform] = 'gcc_riscv_c910.S'
修改原因:
- 第 1 处修改:
support_arch
字典定义了脚本支持的架构和 CPU 型号,添加 c910 才能让脚本识别并处理这个 CPU 型号 - 第 2 处修改:risc-v 架构有 32 位和 64 位之分,c910 是 64 位 CPU,需要归类到 rv64 中以使用正确的编译选项
- 第 3 处修改:不同的 CPU 可能需要不同的汇编启动文件或硬件相关代码,这部分修改确保为 c910 选择正确的汇编文件
3. DefineGroup()
函数及Return('group')
解析:
DefineGroup()
函数用于定义一个构建组,其参数含义如下:
group = DefineGroup('osa', # 第一个参数:构建组的名称,用于标识这个模块src, # 第二个参数:源文件列表,包含该模块所有需要编译的文件depend = ['RT_USING_OSA'], # depend参数:构建依赖条件,只有定义了RT_USING_OSA才会构建这个组CPPPATH = CPPPATH, # CPPPATH参数:头文件搜索路径,供编译器查找头文件LOCAL_CFLAGS = LOCAL_CFLAGS # LOCAL_CFLAGS参数:该模块专用的编译选项
)
'osa'
:构建组的名称,通常与模块名一致,便于在构建系统中识别和引用
src
:该模块所有源文件的列表,包含 C 文件、汇编文件等
depend = ['RT_USING_OSA']
:指定构建该模块的前提条件,只有当RT_USING_OSA
宏被定义时,才会编译这个模块
CPPPATH
:指定该模块所需的头文件路径,确保编译器能找到所有需要的头文件
LOCAL_CFLAGS
:该模块专用的编译选项,不会影响其他模块
Return('group')
的作用:
- SConscript 作为子脚本,需要将定义好的构建组返回给调用它的父脚本(通常是 SConstruct 或其他 SConscript)
Return('group')
表示将group
变量的值传递给父脚本- 父脚本可以通过
env.SConscript('path/to/SConscript')
获取这个返回值,并将其整合到整个项目的构建流程中 - 这种机制实现了模块化的构建配置,每个子模块负责定义自己的构建信息,再由父脚本统一组织,使大型项目的构建配置更加清晰和可维护