Scons 构建工具

优质
小牛编辑
120浏览
2023-12-01

SCons 简介

SCons 是一套由 Python 语言编写的开源构建系统,类似于 GNU Make。它采用不同于通常 Makefile 文件的方式,而是使用 SConstruct 和 SConscript 文件来替代。这些文件也是 Python 脚本,能够使用标准的 Python 语法来编写。所以在 SConstruct、SConscript 文件中可以调用 Python 标准库进行各类复杂的处理,而不局限于 Makefile 设定的规则。

SCons 的网站上可以找到详细的 SCons 用户手册,本章节讲述 SCons 的基本用法,以及如何在 RT-Thread 中用好 SCons 工具。

什么是构建工具

构建工具 (software construction tool) 是一种软件,它可以根据一定的规则或指令,将源代码编译成可执行的二进制程序。这是构建工具最基本也是最重要的功能。实际上构建工具的功能不止于此,通常这些规则有一定的语法,并组织成文件。这些文件用来控制构建工具的行为,在完成软件构建之外,也可以做其他事情。

目前最流行的构建工具是 GNU Make。很多知名开源软件,如 Linux 内核就采用 Make 构建。Make 通过读取 Makefile 文件来检测文件的组织结构和依赖关系,并完成 Makefile 中所指定的命令。

由于历史原因,Makefile 的语法比较混乱,不利于初学者学习。此外在 Windows 平台上使用 Make 也不方便,需要安装 Cygwin 环境。为了克服 Make 的种种缺点,人们开发了其他构建工具,如 CMake 和 SCons 等。

RT-Thread 构建工具

RT-Thread 早期使用 Make/Makefile 构建。从 0.3.x 开始,RT-Thread 开发团队逐渐引入了 SCons 构建系统,引入 SCons 唯一的目是:使大家从复杂的 Makefile 配置、IDE 配置中脱离出来,把精力集中在 RT-Thread 功能开发上。

有些读者可能会有些疑惑,这里介绍的构建工具与 IDE 有什么不同呢?IDE 通过图形化界面的操作来完成构建。大部分 IDE 会根据用户所添加的源码生成类似 Makefile 或 SConscript 的脚本文件,在底层调用类似 Make 或 SCons 的工具来构建源码。

安装 SCons

在使用 SCons 系统前需要在 PC 主机中安装它,因为它是 Python 语言编写的,所以在使用 SCons 之前需要安装 Python 运行环境。

RT-Thread 提供的 Env 配置工具带有 SCons 和 Python,因此在 windows 平台使用 SCons 则不需要安装这两个软件。

在 Linux、BSD 环境中 Python 应该已经默认安装了,一般也是 2.x 版本系列的 Python 环境。这时只需要安装 SCons 即可,例如在 Ubuntu 中可以使用如下命令安装 SCons:

sudo apt-get install scons

SCons 基本功能

RT-Thread 构建系统支持多种编译器。目前支持的编译器包括 ARM GCC、MDK、IAR、VisualStudio、Visual DSP。主流的 ARM Cortex M0、M3、M4 平台,基本上 ARM GCC、MDK、IAR 都是支持的。有一些 BSP 可能仅支持一种,读者可以阅读该 BSP 目录下的 rtconfig.py 里的 CROSS_TOOL 选项查看当前支持的编译器。

如果是 ARM 平台的芯片,则可以使用 Env 工具,输入 scons 命令直接编译 BSP,这时候默认使用的是 ARM GCC 编译器,因为 Env 工具带有 ARM GCC 编译器。 如下图所示使用 scons 命令编译 stm32f10x-HAL BSP,后文讲解 SCons 也将基于这个 BSP。

使用 scons 命令编译 stm32f10x-HAL BSP

如果用户要使用其他的 BSP 已经支持的编译器编译工程,或者 BSP 为非 ARM 平台的芯片,那么不能直接使用 scons 命令编译工程,需要自己安装对应的编译器,并且指定使用的编译器路径。在编译工程前,可以在 Env 命令行界面使用下面的 2 个命令指定编译器为 MDK 和编译器路径为 MDK 的安装路径。

set RTT_CC=keil
set RTT_EXEC_PATH=C:/Keilv5

SCons 基本命令

本节介绍 RT-Thread 中常用的 SCons 命令。SCons 不仅完成基本的编译,还可以生成 MDK/IAR/VS 工程。

scons

在 Env 命令行窗口进入要编译的 BSP 工程目录,然后使用此命令可以直接编译工程。如果执行过 scons 命令后修改了一些源文件,再次执行 scons 命令时,则 SCons 会进行增量编译,仅编译修改过的源文件并链接。

如果在 Windows 上执行 scons 输出以下的警告信息:

scons: warning: No version of Visual Studio compiler found - C/C++ compilers most likely not set correctly.

说明 scons 并没在你的机器上找到 Visual Studio 编译器,但实际上我们主要是针对设备开发,和 Windows 本地没什么关系,请直接忽略掉它。

scons 命令后面还可以增加一个 - s 参数,即命令 scons -s,和 scons 命令不同的是此命令不会打印具体的内部命令。

scons -c

清除编译目标。这个命令会清除执行 scons 时生成的临时文件和目标文件。

scons --target=XXX

如果使用 mdk/iar 来进行项目开发,当修改了 rtconfig.h 打开或者关闭某些组件时,需要使用以下命令中的其中一种重新生成对应的定制化的工程,然后在 mdk/iar 进行编译下载。

scons --target=iar
scons --target=mdk4
scons --target=mdk5

在命令行窗口进入要编译的 BSP 工程目录,使用 scons --target=mdk5 命令后会在 BSP 目录生成一个新的 MDK 工程文件名为 project.uvprojx。双击它打开,就可以使用 MDK 来编译、调试。使用 scons --target=iar 命令后则会生成一个新的 IAR 工程文件名为 project.eww。不习惯 SCons 的用户可以使用这种方式。如果打开 project.uvproj 失败,请删除 project.uvopt 后,重新生成工程。

在 bsp/simulator 下,可以使用下面的命令生成 vs2012 的工程或 vs2005 的工程。

scons --target=vs2012
Scons --target=vs2005

如果 BSP 目录下提供其他 IDE 工程的模板文件也可以使用此命令生成对应的新工程,比如 ua、vs、cb、cdk。

这个命令后面同样可以增加一个 -s 参数,如命令 scons –target=mdk5 -s,执行此命令时不会打印具体的内部命令。

注意事项

要生成 MDK 或者 IAR 的工程文件,前提条件是 BSP 目录存在一个工程模版文件,然后 scons 才会根据这份模版文件加入相关的源码,头文件搜索路径,编译参数,链接参数等。而至于这个工程是针对哪颗芯片的,则直接由这份工程模版文件指定。所以大多数情况下,这个模版文件是一份空的工程文件,用于辅助 SCons 生成 project.uvprojx 或者 project.eww。

scons -jN

多线程编译目标,在多核计算机上可以使用此命令加快编译速度。一般来说一颗 cpu 核心可以支持 2 个线程。双核机器上使用 scons -j4 命令即可。

注意事项

如果你只是想看看编译错误或警告,最好是不使用 - j 参数,这样错误信息不会因为多个文件并行编译而导致出错信息夹杂在一起。

scons --dist

搭建项目框架,使用此命令会在 BSP 目录下生成 dist 目录,这便是开发项目的目录结构,包含了RT-Thread源码及BSP相关工程,不相关的BSP文件夹及libcpu都会被移除,并且可以随意拷贝此工程到任何目录下使用。

scons --verbose

默认情况下,使用 scons 命令编译的输出不会显示编译参数,如下所示:

D:\repository\rt-thread\bsp\stm32f10x>scons
scons: Reading SConscript files ...
scons: done reading SConscript files.
scons: Building targets ...
scons: building associated VariantDir targets: build
CC build\applications\application.o
CC build\applications\startup.o
CC build\components\drivers\serial\serial.o
...

使用 scons –verbose 命令的效果如下:

armcc -o build\src\mempool.o -c --device DARMSTM --apcs=interwork -ID:/Keil/ARM/
RV31/INC -g -O0 -DUSE_STDPERIPH_DRIVER -DSTM32F10X_HD -Iapplications -IF:\Projec
t\git\rt-thread\applications -I. -IF:\Project\git\rt-thread -Idrivers -IF:\Proje
ct\git\rt-thread\drivers -ILibraries\STM32F10x_StdPeriph_Driver\inc -IF:\Project
\git\rt-thread\Libraries\STM32F10x_StdPeriph_Driver\inc -ILibraries\STM32_USB-FS
-Device_Driver\inc -IF:\Project\git\rt-thread\Libraries\STM32_USB-FS-Device_Driv
er\inc -ILibraries\CMSIS\CM3\DeviceSupport\ST\STM32F10x -IF:\Project\git\rt-thre
...

SCons 进阶

SCons 使用 SConscript 和 SConstruct 文件来组织源码结构,通常来说一个项目只有一 SConstruct,但是会有多个 SConscript。一般情况下,每个存放有源代码的子目录下都会放置一个 SConscript。

为了使 RT-Thread 更好的支持多种编译器,以及方便的调整编译参数,RT-Thread 为每个 BSP 单独创建了一个名为 rtconfig.py 的文件。因此每一个 RT-Thread BSP 目录下都会存在下面三个文件:rtconfig.py、SConstruct 和 SConscript,它们控制 BSP 的编译。一个 BSP 中只有一个 SConstruct 文件,但是却会有多个 SConscript 文件,可以说 SConscript 文件是组织源码的主力军。

RT-Thread 大部分源码文件夹下也存在 SConscript 文件,这些文件会被 BSP 目录下的 SConscript 文件 “找到” 从而将 rtconfig.h 中定义的宏对应的源代码加入到编译器中来。后文将以 stm32f10x-HAL BSP 为例,讲解 SCons 是如何构建工程。

SCons 内置函数

如果想要将自己的一些源代码加入到 SCons 编译环境中,一般可以创建或修改已有 SConscript 文件。SConscript 文件可以控制源码文件的加入,并且可以指定文件的 Group(与 MDK/IAR 等 IDE 中的 Group 的概念类似)。

SCons 提供了很多内置函数可以帮助我们快速添加源码程序,利用这些函数,再配合一些简单的 Python 语句我们就能随心所欲向项目中添加或者删除源码。下面将简单介绍一些常用函数。

GetCurrentDir()

获取当前路径。

Glob('*.c')

获取当前目录下的所有 C 文件。修改参数的值为其他后缀就可以匹配当前目录下的所有某类型的文件。

GetDepend(macro)

该函数定义在 tools 目录下的脚本文件中,它会从 rtconfig.h 文件读取配置信息,其参数为 rtconfig.h 中的宏名。如果 rtconfig.h 打开了某个宏,则这个方法(函数)返回真,否则返回假。

Split(str)

将字符串 str 分割成一个列表 list。

DefineGroup(name, src, depend,**parameters)

这是 RT-Thread 基于 SCons 扩展的一个方法(函数)。DefineGroup 用于定义一个组件。组件可以是一个目录(下的文件或子目录),也是后续一些 IDE 工程文件中的一个 Group 或文件夹。

DefineGroup() 函数的参数描述:

参数描述
nameGroup 的名字
srcGroup 中包含的文件,一般指的是 C/C++ 源文件。方便起见,也能够通过 Glob 函数采用通配符的方式列出 SConscript 文件所在目录中匹配的文件
dependGroup 编译时所依赖的选项(例如 FinSH 组件依赖于 RT_USING_FINSH 宏定义)。编译选项一般指 rtconfig.h 中定义的 RT_USING_xxx 宏。当在 rtconfig.h 配置文件中定义了相应宏时,那么这个 Group 才会被加入到编译环境中进行编译。如果依赖的宏并没在 rtconfig.h 中被定义,那么这个 Group 将不会被加入编译。相类似的,在使用 scons 生成为 IDE 工程文件时,如果依赖的宏未被定义,相应的 Group 也不会在工程文件中出现
parameters配置其他参数,可取值见下表,实际使用时不需要配置所有参数

parameters 可加入的参数:

参数描述
CCFLAGSC 源文件编译参数
CPPPATH头文件路径
CPPDEFINES链接时参数
LIBRARY包含此参数,则会将组件生成的目标文件打包成库文件

SConscript(dirs,variant_dir,duplicate)

读取新的 SConscript 文件,SConscript() 函数的参数描述如下所示:

参数描述
dirsSConscript 文件路径
variant_dir指定生成的目标文件的存放路径
duiplicate设定是否拷贝或链接源文件到 variant_dir

SConscript 示例

下面我们将以几个 SConscript 为例讲解 scons 构建工具的使用方法。

SConscript 示例 1

我们先从 stm32f10x-HAL BSP 目录下的 SConcript 文件开始讲解,这个文件管理 BSP 下面的所有其他 SConscript 文件,内容如下所示。

import os
cwd = str(Dir('#'))
objs = []
list = os.listdir(cwd)
for d in list:
    path = os.path.join(cwd, d)
       if os.path.isfile(os.path.join(path, 'SConscript')):
        objs = objs + SConscript(os.path.join(d, 'SConscript'))
Return('objs')
  • import os: 导入 Python 系统编程 os 模块,可以调用 os 模块提供的函数用于处理文件和目录。

  • cwd = str(Dir('#')): 获取工程的顶级目录并赋值给字符串变量 cwd,也就是工程的 SConstruct 所在的目录,在这里它的效果与 cwd = GetCurrentDir() 相同。

  • objs = []: 定义了一个空的 list 型变量 objs。

  • list = os.listdir(cwd): 得到当前目录下的所有子目录,并保存到变量 list 中。

  • 随后是一个 python 的 for 循环,这个 for 循环会遍历一遍 BSP 的所有子目录并运行这些子目录的 SConscript 文件。具体操作是取出一个当前目录的子目录,利用 os.path.join(cwd,d) 拼接成一个完整路径,然后判断这个子目录是否存在一个名为 SConscript 的文件,若存在则执行 objs = objs + SConscript(os.path.join(d,'SConscript'))。 这一句中使用了 SCons 提供的一个内置函数 SConscript(),它可以读入一个新的 SConscript 文件,并将 SConscript 文件中所指明的源码加入到了源码编译列表 objs 中来。

通过这个 SConscript 文件,BSP 工程所需要的源代码就被加入了编译列表中。

SConscript 示例 2

那么 stm32f10x-HAL BSP 其他的 SConcript 文件又是怎样的呢?我们再看一下 drivers 目录下 SConcript 文件,这个文件将管理 drivers 目录下面的源代码。drivers 目录用于存放根据 RT-Thread 提供的驱动框架实现的底层驱动代码。

Import('rtconfig')
from building import *

cwd = GetCurrentDir()

# add the general drivers.
src = Split("""
board.c
stm32f1xx_it.c
""")

if GetDepend(['RT_USING_PIN']):
    src += ['drv_gpio.c']
if GetDepend(['RT_USING_SERIAL']):
    src += ['drv_usart.c']
if GetDepend(['RT_USING_SPI']):
    src += ['drv_spi.c']
if GetDepend(['RT_USING_USB_DEVICE']):
    src += ['drv_usb.c']
if GetDepend(['RT_USING_SDCARD']):
    src += ['drv_sdcard.c']

if rtconfig.CROSS_TOOL == 'gcc':
    src += ['gcc_startup.s']

CPPPATH = [cwd]

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

Return('group')
  • Import('rtconfig'): 导入 rtconfig 对象,后面用到的 rtconfig.CROSS_TOOL 定义在这个 rtconfig 模块。

  • from building import *: 把 building 模块的所有内容全都导入到当前模块,后面用到的 DefineGroup 定义在这个模块。

  • cwd = GetCurrentDir(): 获得当前路径并保存到字符串变量 cwd 中。

后面一行使用 Split() 函数来将一个文件字符串分割成一个列表,其效果等价于

src = ['board.c','stm32f1xx_it.c']

后面使用了 if 判断和 GetDepend() 检查 rtconfig.h 中的某个宏是否打开,如果打开,则使用 src += [src_name] 来往列表变量 src 中追加源代码文件。

  • CPPPATH = [cwd]: 将当前路径保存到一个列表变量 CPPPATH 中。

最后一行使用 DefineGroup 创建一个名为 Drivers 的组,这个组也就对应 MDK 或者 IAR 中的分组。这个组的源代码文件为 src 指定的文件,depend 为空表示该组不依赖任何 rtconfig.h 的宏。

CPPPATH =CPPPATH 表示将当前路径添加到系统的头文件路径中。左边的 CPPPATH 是 DefineGroup 中内置参数,表示头文件路径。右边的 CPPPATH 是本文件上面一行定义的。这样我们就可以在其他源码中引用 drivers 目录下的头文件了。

SConscript 示例 3

我们再看一下 applications 目录下的 SConcript 文件,这个文件将管理 applications 目录下面的源代码,用于存放用户自己的应用代码。

from building import *

cwd = GetCurrentDir()
src = Glob('*.c')
CPPPATH = [cwd, str(Dir('#'))]

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

Return('group')

src = Glob('*.c'): 得到当前目录下所有的 C 文件。

CPPPATH = [cwd, str(Dir('#'))]: 将当前路径和工程的 SConstruct 所在的路径保存到列表变量 CPPPATH 中。

最后一行使用 DefineGroup 创建一个名为 Applications 的组。这个组的源代码文件为 src 指定的文件,depend 为空表示该组不依赖任何 rtconfig.h 的宏,并将 CPPPATH 保存的路径添加到了系统头文件搜索路径中。这样 applications 目录和 stm32f10x-HAL BSP 目录里面的头文件在源代码的其他地方就可以引用了。

总结:这个源程序会将当前目录下的所有 c 程序加入到组 Applications 中,因此如果在这个目录下增加或者删除文件,就可以将文件加入工程或者从工程中删除。它适用于批量添加源码文件。

SConscript 示例 4

下面是 RT-Thread 源代码 component/finsh/SConscript 文件的内容,这个文件将管理 finsh 目录下面的源代码。

Import('rtconfig')
from building import *

cwd     = GetCurrentDir()
src     = Split('''
shell.c
symbol.c
cmd.c
''')

fsh_src = Split('''
finsh_compiler.c
finsh_error.c
finsh_heap.c
finsh_init.c
finsh_node.c
finsh_ops.c
finsh_parser.c
finsh_var.c
finsh_vm.c
finsh_token.c
''')

msh_src = Split('''
msh.c
msh_cmd.c
msh_file.c
''')

CPPPATH = [cwd]
if rtconfig.CROSS_TOOL == 'keil':
    LINKFLAGS = '--keep *.o(FSymTab)'

    if not GetDepend('FINSH_USING_MSH_ONLY'):
        LINKFLAGS = LINKFLAGS + '--keep *.o(VSymTab)'
else:
    LINKFLAGS = ''

if GetDepend('FINSH_USING_MSH'):
    src = src + msh_src
if not GetDepend('FINSH_USING_MSH_ONLY'):
    src = src + fsh_src

group = DefineGroup('finsh', src, depend = ['RT_USING_FINSH'], CPPPATH = CPPPATH, LINKFLAGS = LINKFLAGS)

Return('group')

我们来看一下文件中第一个 Python 条件判断语句的内容,如果编译工具是 keil,则变量 LINKFLAGS = '--keep *.o(FSymTab)' 否则置空。

DefinGroup 同样将 finsh 目录下的 src 指定的文件创建为 finsh 组。depend = ['RT_USING_FINSH'] 表示这个组依赖 rtconfig.h 中的宏 RT_USING_FINSH。当 rtconfig.h 中打开宏 RT_USING_FINSH 时,finsh 组内的源码才会被实际编译,否则 SCons 不会编译。

然后将 finsh 目录加入到系统头文件目录中,这样我们就可以在其他源码中引用 finsh 目录下的头文件。

LINKFLAGS = LINKFLAGS 的含义与 CPPPATH = CPPPATH 类似。左边的 LINKFLAGS 表示链接参数,右边的 LINKFLAGS 则是前面 if else 语句所定义的值。也就是给工程指定链接参数。

使用 SCons 管理工程

前面小节对 RT-Thread 源代码的相关 SConscript 做了详细讲解,大家也应该知道了 SConscript 文件的一些常见写法,本小节将指导大家如何使用 SCons 管理自己的工程。

添加应用代码

前文提到过 BSP 下的 Applications 文件夹用于存放用户自己的应用代码,目前只有一个 main.c 文件。如果用户的应用代码不是很多,建议相关源文件都放在这个文件夹下面。在 Applications 文件夹下新增了 2 个简单的文件 hello.c 和 hello.h,内容如下所示。

/* file: hello.h */

#ifndef _HELLO_H_
#define _HELLO_H_

int hello_world(void);

#endif /* _HELLO_H_ */

/* file: hello.c */
#include <stdio.h>
#include <finsh.h>
#include <rtthread.h>

int hello_world(void)
{
    rt_kprintf("Hello, world!\n");

    return 0;
}

MSH_CMD_EXPORT(hello_world, Hello world!)

applications 目录下的 SConcript 文件会把当前目录下的所有源文件都添加到工程中。需要使用 scons --target=xxx 命令才会把新增的 2 个文件添加到工程项目中。注意每次新增文件都要重新生成工程。

添加模块

前文提到在自己源代码文件不多的情况下,建议所有源代码文件都放在 applications 文件夹里面。如果用户源代码很多了,并且想创建自己的工程模块,或者需要使用自己获取的其他模块,怎么做会比较合适呢?

同样以上文提到的 hello.c 和 hello.h 为例,这两个文件将会放到一个单独的文件夹里管理,并且在 MDK 工程文件里有自己的分组,且可以通过 menuconfig 选择是否使用这个模块。在 BSP 下新增 hello 文件夹。

新增 hello 文件夹

大家注意到文件夹里多了一个 SConscript 文件,如果想要将自己的一些源代码加入到 SCons 编译环境中,一般可以创建或修改已有的 SConscript 文件。参考上文对 RT-Thread 源代码的一些对 SConscript 文件的分析,这个新增的 hello 模块 SConscript 文件内容如下所示:

from building import *

cwd          = GetCurrentDir()
include_path = [cwd]
src          = []

if GetDepend(['RT_USING_HELLO']):
    src += ['hello.c']

group = DefineGroup('hello', src, depend = [''], CPPPATH = include_path)

Return('group')

通过上面几行简单的代码,就创建了一个新组 hello,并且可以通过宏定义控制要加入到组里面的源文件,还将这个组所在的目录添加到了系统头文件路径中。那么自定义宏 RT_USING_HELLO 又是通过怎样的方式定义呢?这里要介绍一个新的文件 Kconfig。Kconfig 用来配置内核,使用 Env 配置系统时使用的 menuconfig 命令生成的配置界面就依赖 Kconfig 文件。menuconfig 命令通过读取工程的各个 Kconfig 文件,生成配置界面供用户配置内核,最后所有配置相关的宏定义都会自动保存到 BSP 目录里的 rtconfig.h 文件中,每一个 BSP 都有一个 rtconfig.h 文件,也就是这个 BSP 的配置信息。

在 stm32f10x-HAL BSP 目录下已经有了关于这个 BSP 的 Kconfig 文件,我们可以基于这个文件添加自己需要的配置选项。关于 hello 模块添加了如下配置选项,# 号后面为注释。

hello 模块相关配置选项

使用 Env 工具进入 stm32f10x-HAL BSP 目录后,使用 menuconfig 命令在主页面最下面就可以看到新增的 hello 模块的配置菜单,进入菜单后如下图所示。

hello 模块配置菜单

还可以修改 hello value 的值。

修改 hello value 的值

保存配置后退出配置界面,打开 stm32f10x-HAL BSP 目录下的 rtconfig.h 文件可以看到 hello 模块的配置信息已经有了。

hello 模块相关宏定义

注意:每次 menuconfig 配置完成后都要使用 scons --target=XXX 命令生成新工程。

因为 rtconfig.h 中已经定义了 RT_USING_HELLO 宏,所以新生成工程时就会把 hello.c 的源文件添加到新工程中。

上面只是简单列举了在 Kconfig 文件中添加自己模块的配置选项,用户还可以参考《Env 用户手册》,里面也有对配置选项修改和添加的讲解,也可以自己百度查看 Kconfig 的相关文档,实现其他更复杂的配置选项。

添加库

如果要往工程中添加一个额外的库,需要注意不同的工具链对二进制库的命名。

  • ARMCC 工具链下的库名称应该是 xxx.lib,一个以 .lib 为后缀的文件。
  • IAR 工具链下的库名称应该是 xxx.a,一个以 .a 为后缀的文件。
  • GCC 工具链下的库名称应该是 libxxx.a,一个以 .a 为后缀的文件,并且有 lib 前缀。

ARMCC / IAR 工具链下,若添加库名为 libabc.lib / libabc_iar.a 时,在指定库时指定全名 libabc。

GCC 工具链比较特殊,它识别的是 libxxx.a 这样的库名称,若添加库名为 libabc.a 时,在指定库时是指定 abc,而不是 libabc。

例如,/libs 下有以下库文件需要添加:

libabc_keil.lib
libabc_iar.a
libabc_gcc.a

则对应的 SConscript 如下:

Import('rtconfig')
from building import *

cwd = GetCurrentDir()
src = Split('''
''')

LIBPATH = [cwd + '/libs']             # LIBPATH 指定库的路径,表示库的搜索路径是当前目录下的'libs'目录

if rtconfig.CROSS_TOOL == 'gcc':
    LIBS = ['abc_gcc']                    # GCC 下 LIBS 指定库的名称 
elif rtconfig.CROSS_TOOL == 'keil':
    LIBS = ['libabc_keil']                # ARMCC 下 LIBS 指定库的名称
else:
    LIBS = ['libabc_iar']                 # IAR 下 LIBS 指定库的名称

group = DefineGroup('ABC', src, depend = [''], LIBS = LIBS, LIBPATH=LIBPATH)

Return('group')

编译器选项

rtconfig.py 是一个 RT-Thread 标准的编译器配置文件,控制了大部分编译选项,是一个使用 python 语言编写的脚本文件,主要用于完成以下工作:

  • 指定编译器(从支持的多个编译器中选择一个你现在使用的编译器)。

  • 指定编译器参数,如编译选项、链接选项等。

当我们使用 scons 命令编译工程时,就会按照 rtconfig.py 的编译器配置选项编译工程。下面的代码为 stm32f10x-HAL BSP 目录下 rtconfig.py 的部分代码。

import os

# toolchains options
ARCH='arm'
CPU='cortex-m3'
CROSS_TOOL='gcc'

if os.getenv('RTT_CC'):
    CROSS_TOOL = os.getenv('RTT_CC')

# cross_tool provides the cross compiler
# EXEC_PATH is the compiler execute path, for example, CodeSourcery, Keil MDK, IAR

if  CROSS_TOOL == 'gcc':
    PLATFORM    = 'gcc'
    EXEC_PATH   = '/usr/local/gcc-arm-none-eabi-5_4-2016q3/bin/'
elif CROSS_TOOL == 'keil':
    PLATFORM    = 'armcc'
    EXEC_PATH   = 'C:/Keilv5'
elif CROSS_TOOL == 'iar':
    PLATFORM    = 'iar'
    EXEC_PATH   = 'C:/Program Files/IAR Systems/Embedded Workbench 6.0 Evaluation'

if os.getenv('RTT_EXEC_PATH'):
    EXEC_PATH = os.getenv('RTT_EXEC_PATH')

BUILD = 'debug'

if PLATFORM == 'gcc':
    # toolchains
    PREFIX = 'arm-none-eabi-'
    CC = PREFIX + 'gcc'
    AS = PREFIX + 'gcc'
    AR = PREFIX + 'ar'
    LINK = PREFIX + 'gcc'
    TARGET_EXT = 'elf'
    SIZE = PREFIX + 'size'
    OBJDUMP = PREFIX + 'objdump'
    OBJCPY = PREFIX + 'objcopy'

    DEVICE = '-mcpu=cortex-m3 -mthumb -ffunction-sections -fdata-sections'
    CFLAGS = DEVICE
    AFLAGS = '-c' + DEVICE + '-x assembler-with-cpp'
    LFLAGS = DEVICE + '-Wl,--gc-sections,-Map=rtthread-stm32.map,-cref,-u,Reset_Handler -T stm32_rom.ld'

其中 CFLAGS 是 C 文件的编译选项,AFLAGS 则是汇编文件的编译选项,LFLAGS 是链接选项。BUILD 变量控制代码优化的级别。默认 BUILD 变量取值为'debug',即使用 debug 方式编译,优化级别 0。如果将这个变量修改为其他值,就会使用优化级别 2 编译。下面几种都是可行的写法(总之只要不是'debug'就可以了)。

BUILD = ''
BUILD = 'release'
BUILD = 'hello, world'

建议在开发阶段都使用 debug 方式编译,不开优化,等产品稳定之后再考虑优化。

关于这些选项的具体含义需要参考编译器手册,如上面使用的 armcc 是 MDK 的底层编译器。其编译选项的含义在 MDK help 中有详细说明。

前文提到过如果用户执行 scons 命令时希望使用其他编译器编译工程,可以在 Env 的命令行端使用相关命令指定编译器和编译器路径。但是这样修改只对当前的 Env 进程有效,再次打开时又需要重新使用命令设置,我们可以直接修改 rtconfig.py 文件达到永久配置编译器的目的。一般来说,我们只需要修改 CROSS_TOOL 和下面的 EXEC_PATH 两个选项。

  • CROSS_TOOL:指定编译器。可选的值有 keil、gcc、iar,浏览 rtconfig.py 可以查看当前 BSP 所支持的编译器。如果您的机器上安装了 MDK,那么可以将 CROSS_TOOL 修改为 keil,则使用 MDK 编译工程。

  • EXEC_PATH:编译器的安装路径。这里有两点需要注意:

安装编译器时(如 MDK、GNU GCC、IAR 等),不要安装到带有中文或者空格的路径中。否则,某些解析路径时会出现错误。有些程序默认会安装到 C:\Program Files 目录下,中间带有空格。建议安装时选择其他路径,养成良好的开发习惯。

修改 EXEC_PATH 时,需要注意路径的格式。在 windows 平台上,默认的路径分割符号是反斜杠 “\”,而这个符号在 C 语言以及 Python 中都是用于转义字符的。所以修改路径时,可以将“\” 改为 “/”,或者在前面加 r(python 特有的语法,表示原始数据)。

假如某编译器安装位置为 D:\Dir1\Dir2 下。下面几种是正确的写法:

  • EXEC_PATH = r'D:\Dir1\Dir2' 注意,字符串前带有 r,则可正常使用 “\”

  • EXEC_PATH = 'D:/Dir1/Dir2' 注意,改用 “/”,前面没有 r。

  • EXEC_PATH = 'D:\\Dir1\\Dir2' 注意,这里使用 “\” 的转义性来转义 “\” 自己。

  • 这是错误的写法:EXEC_PATH = 'D:\Dir1\Dir2'

如果 rtconfig.py 文件有以下代码,在配置自己的编译器时请将下列代码注释掉。

if os.getenv('RTT_CC'):
    CROSS_TOOL = os.getenv('RTT_CC')
... ...
if os.getenv('RTT_EXEC_PATH'):
    EXEC_PATH = os.getenv('RTT_EXEC_PATH')

上面 2 个 if 判断会设置 CROSS_TOOL 和 EXEC_PATH 为 Env 的默认值。

编译器配置完成之后,我们就可以使用 SCons 来编译 RT-Thread 的 BSP 了。在 BSP 目录打开命令行窗口,执行 scons 命令就会启动编译过程。

RT-Thread 辅助编译脚本

在 RT-Thread 源代码的 tools 目录下存放有 RT-Thread 自己定义的一些辅助编译的脚本,例如用于自动生成 RT-Thread 针对一些 IDE 集成开发环境的工程文件。其中最主要的是 building.py 脚本。

SCons 更多使用

对于复杂、大型的系统,显然不仅仅是一个目录下的几个文件就可以搞定的,很可能是由数个文件夹一级一级组合而成。

在 SCons 中,可以编写 SConscript 脚本文件来编译这些相对独立目录中的文件,同时也可以使用 SCons 中的 Export 和 Import 函数在 SConstruct 与 SConscript 文件之间共享数据(也就是 Python 中的一个对象数据)。更多 SCons 的使用方法请参考 SCons 官方文档