Edit online

SConstruct

4 Dec 2024
Read time: 8 minute(s)

SConstruct,一般简称为 SCons,全称为 Software Construction Tool,是一个用 Python 编写的开源构建工具,旨在改进和自动化软件构建过程。SConstruct 提供了一种更简单、更可靠且高效的替代传统构建系统如 GNU Make 的方案。

Baremetal 使用 SConstruct 来实现自动化构建。

基本用法和特点

SCons 使用 SConstructSConscript 文件来组织源码结构。

在大型项目中,通常会有一个 SConstruct 和多个 SConscript 文件,分别位于含有源代码的子目录中。原则上每个存放有源代码的子目录下都会放置一个 SConscript,但譬如 BSP 的驱动开发等会集合所有的驱动源码到一个 SConscript 中。

SCons 具有以下特点:

  • SCons 使用 Python 脚本做为配置文件,具有强大的表达式力和灵活性,允许执行复杂的逻辑和操作。

  • Python 的跨平台特性使得 SCons 自然支持多平台,可以无缝地在 Windows、Linux 及其他操作系统上运行,无需修改构建脚本。

  • 自动检测源文件和头文件之间的依赖关系,确保只有在必要时才重新构建项目。

  • 支持多种编程语言,包括 C, C++, D, Java, Fortran, Yacc, Lex, Qt,SWIG 以及 Tex/Latex, 扩展性好,支持用户自扩展编程语言。

  • 支持 make -j 风格的并行构建,允许同时运行多个工作,以利用多核处理器的优势,提高构建速度。SCons 管理并行任务时会考虑文件层次结构和依赖性,避免构建错误。

  • 使用 Autoconf 风格自动查找系统上的头文件,函数库,函数和类型定义,简化了跨系统构建配置的复杂性。

  • 基于 MD5 识别构建文件是否发生变化,比传统的时间戳更精准和安全。

环境安装

  • Windows

    Baremetal 具有小体积,良好的兼容性,以及快速的特点,尤其是在命令行的开发和构建方面,推荐在 Windows 系统上进行操作。

    Windows 下的对应工具存放在 luban-lite/tools/env/tools 目录中,不需要安装。


    win-sdk-tools

  • Linux
    在 Linux 系统上设置 Baremetal 固件构建环境,需要安装关键的依赖包,包括:
    注: 推荐使用 apt-get 命令直接安装目标软件和依赖。
    1. python2 + SCons

      安装 SCons
      sudo apt-get install scons
    2. python3 + pycryptodomex

      安装 pycryptodomex

      pycryptodomex 是一个 python 编写的加密包,Baremetal 中有源码包可以进行编译安装

      sudo apt install pip
      cd tools/env/local_pkgs/
      tar xvf pycryptodomex-3.11.0.tar.gz
      cd pycryptodomex-3.11.0
      sudo python3 setup.py install

SConstruct

Baremetal 的 SConstruct 脚本放置在 SDK 根目录下,负责配置编译逻辑、设置全局环境变量,并允许用户添加自定义的私有环境变量 (ENV),以便实现高度定制化的构建配置:
  • 配置编译逻辑:通过定义一系列的规则和依赖关系,SConstruct 脚本可以自动化编译流程。

    相关规则可以指定如何从源代码生成目标文件,包括编译器选择、编译选项及链接过程。用户可以在 SConstruct 中定义不同的编译配置,例如调试版本和发布版本,每种配置可以有不同的编译器选项和优化设置,从而满足不同环境下的构建需求。

  • 设置全局环境变量:SConstruct 提供了设置全局环境变量的功能,如编译器路径、包含目录等,确保构建过程能在正确的环境下执行。

    通过调整环境变量,Baremetal 可以适应不同的操作系统和工具链,提高构建系统的灵活性和兼容性。

import os
import sys

# Luban-Lite root directory
AIC_ROOT = os.path.normpath(os.getcwd())

# luban-lite custom scripts
aic_script_path = os.path.join(AIC_ROOT, 'tools/scripts/')
sys.path.append(aic_script_path)
from aic_build import *
chk_prj_config(AIC_ROOT)
PRJ_CHIP,PRJ_BOARD,PRJ_KERNEL,PRJ_APP,PRJ_DEFCONFIG_NAME,PRJ_CUSTOM_LDS,MKIMAGE_POST_ACTION = get_prj_config(AIC_ROOT)
PRJ_NAME = PRJ_DEFCONFIG_NAME.replace('_defconfig','')
PRJ_OUT_DIR = 'output/' + PRJ_NAME + '/images/'
AIC_SCRIPT_DIR = aic_script_path
AIC_COMMON_DIR = os.path.join(AIC_ROOT, 'bsp/artinchip/sys/' + PRJ_CHIP)
AIC_PACK_DIR = os.path.join(AIC_ROOT, 'target/' + PRJ_CHIP + '/' + PRJ_BOARD + '/pack/')

# Var tranfer to SConscript
Export('AIC_ROOT')
Export('AIC_SCRIPT_DIR')
Export('AIC_COMMON_DIR')
Export('AIC_PACK_DIR')
Export('PRJ_CHIP')
Export('PRJ_BOARD')
Export('PRJ_KERNEL')
Export('PRJ_APP')
Export('PRJ_NAME')
Export('PRJ_DEFCONFIG_NAME')
Export('PRJ_OUT_DIR')
# Var tranfer to Kconfig 'option env=xxx'
os.environ["AIC_ROOT"]           = AIC_ROOT
os.environ["AIC_SCRIPT_DIR"]     = AIC_SCRIPT_DIR
os.environ["AIC_COMMON_DIR"]     = AIC_COMMON_DIR
os.environ["AIC_PACK_DIR"]       = AIC_PACK_DIR
os.environ["PRJ_CHIP"]           = PRJ_CHIP
os.environ["PRJ_BOARD"]          = PRJ_BOARD
os.environ["PRJ_KERNEL"]         = PRJ_KERNEL
os.environ["PRJ_APP"]            = PRJ_APP
os.environ["PRJ_NAME"]           = PRJ_NAME
os.environ["PRJ_DEFCONFIG_NAME"] = PRJ_DEFCONFIG_NAME
os.environ["PRJ_OUT_DIR"]        = PRJ_OUT_DIR

# rtconfig
chip_path = os.path.join(AIC_ROOT, 'bsp/artinchip/sys/' + PRJ_CHIP)
sys.path.append(chip_path)
import rtconfig

# RTT_ROOT
if os.getenv('RTT_ROOT'):
    RTT_ROOT = os.getenv('RTT_ROOT')
else:
    RTT_ROOT = os.path.join(AIC_ROOT, 'kernel/rt-thread/')
os.environ["RTT_ROOT"]           = RTT_ROOT
sys.path.append(os.path.join(RTT_ROOT, 'tools'))
from building import *

# ENV_ROOT
if os.getenv('ENV_ROOT') is None:
    ENV_ROOT = RTT_ROOT + '/../../tools/env'
    os.environ["ENV_ROOT"] =  ENV_ROOT

# TARGET
TARGET = PRJ_OUT_DIR + rtconfig.SOC + '.' + rtconfig.TARGET_EXT

rtconfig.LFLAGS += ' -T ' + ld

# add post action
rtconfig.POST_ACTION += MKIMAGE_POST_ACTION

# create env
env  = Environment(tools = ['mingw'],
AS   = rtconfig.AS,   ASFLAGS   = rtconfig.AFLAGS,
CC   = rtconfig.CC,   CFLAGS   = rtconfig.CFLAGS,
CXX  = rtconfig.CXX,  CXXFLAGS  = rtconfig.CXXFLAGS,
AR   = rtconfig.AR,   ARFLAGS   = '-rc',
LINK = rtconfig.LINK, LINKFLAGS = rtconfig.LFLAGS)
env.PrependENVPath('PATH', rtconfig.EXEC_PATH)

# add --start-group and --end-group for GNU GCC
env['LINKCOM'] = '$LINK -o $TARGET $LINKFLAGS $__RPATH $SOURCES $_LIBDIRFLAGS -Wl,--start-group $_LIBFLAGS -Wl,--end-group'
env['ASCOM'] = env['ASPPCOM']

# signature database
env.SConsignFile(PRJ_OUT_DIR + ".sconsign.dblite")

Export('RTT_ROOT')
Export('rtconfig')

# Var tranfer to building.py
env['AIC_ROOT']                  = AIC_ROOT
env['AIC_SCRIPT_DIR']            = AIC_SCRIPT_DIR
env['AIC_COMMON_DIR']            = AIC_COMMON_DIR
env['AIC_PACK_DIR']              = AIC_PACK_DIR
env['PRJ_CHIP']                  = PRJ_CHIP
env['PRJ_BOARD']                 = PRJ_BOARD
env['PRJ_KERNEL']                = PRJ_KERNEL
env['PRJ_NAME']                  = PRJ_NAME
env['PRJ_APP']                   = PRJ_APP
env['PRJ_DEFCONFIG_NAME']        = PRJ_DEFCONFIG_NAME
env['PRJ_OUT_DIR']               = PRJ_OUT_DIR

# prepare building environment
objs = PrepareBuilding(env, RTT_ROOT, has_libcpu=False)

# make a building
DoBuilding(TARGET, objs)

SConscript

SConscript 是 SCons 构建系统的配置文件,用于定义构建任务和依赖关系,以支持复杂的构建过程。SConscript 脚本位于源代码树的子目录中,每个 SConscript 文件可以描述该目录下的所有构建任务,从而简化构建流程的管理和维护。

一些常用的 SConscript 方法有:

  • 使用 Program 生成可执行文件

    Program 用于生成可执行文件的示例如下:

    Program('hello.c')          编译 hello.c 可执行文件,根据系统自动生成(hello.exe on Windows; hello on POSIX)
    Program('hello','hello.c')  指定 Output 文件名(hello.exe on Windows; hello on POSIX)
    Program(['hello.c', 'file1.c', 'file2.c']) 编译多个文件,Output 文件名以第一个文件命名
    Program(source = "hello.c",target = "hello")
    Program(target = "hello" , source = "hello.c")
    Program('hello', Split('hello.c file1.c file2.c')) 编译多个文件
    
    Program(Glob("*.c"))
    src = ["hello.c","foo.c"];
    Program(src)
  • 使用 Object 生成目标文件

    Object 用于生成目标文件的示例如下:

    Object('hello.c') 编译 hello.c 目标文件,根据系统自动生成(hello.obj on Windows; hello.o on POSIX)
    
  • 使用 Library 生成静态/动态库文件

    Library 用于生成静态/动态库文件

    Library('foo', ['f1.c', 'f2.c', 'f3.c']) 编译 library
    SharedLibrary('foo', ['f1.c', 'f2.c', 'f3.c']) 编译 shared library
    StaticLibrary('bar', ['f4.c', 'f5.c', 'f6.c']) 编译 static library
    
    库的使用:
    
    Program('prog.c', LIBS=['foo', 'bar'], LIBPATH='.') 连接库,不需加后缀或是前缀
    
  • 使用 Depends 明确依赖关系

    Depends 用于明确依赖关系

    Depends(hello,'other_file')//hello 依赖于 other_file

驱动程序的 SConscript 示例

以下是一个典型的驱动程序的 SConscript 示例,用于构建一个 UART 驱动程序。SConscript 文件示例定义了一个名为 aic_osal 的构建组,包含了 UART 驱动程序的所有源文件,并根据需要设置了头文件路径和编译选项。详情如下:

Import('AIC_ROOT')
Import('PRJ_KERNEL')
from building import *

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

if GetDepend('DRIVER_DRV_EN'):
    CPPPATH.append(cwd + '/include/drv')
if GetDepend('DRIVER_HAL_EN'):
    CPPPATH.append(cwd + '/include/hal')
    CPPPATH.append(cwd + '/include/uapi')

# UART driver
if GetDepend('AIC_UART_DRV'):
    if GetDepend('DRIVER_DRV_EN'):
        src += Glob('drv/uart/*.c')
    if GetDepend('DRIVER_HAL_EN'):
        src += Glob('hal/uart/*.c')

LOCAL_CCFLAGS += ' -O0'

//DefineGroup(name, src, depend,**parameters)
group = DefineGroup('aic_osal', src, depend=[''], CPPPATH=CPPPATH, LOCAL_CCFLAGS=LOCAL_CCFLAGS)

Return('group')
上述 SConscript 示例代码主要完成以下任务:
  1. 导入必要的变量和模块。
  2. 获取当前目录并设置源文件列表 src
  3. 根据条件判断是否包含特定的头文件路径到 CPPPATH 中。
  4. 如果启用了 UART 驱动程序,根据条件将相应的源文件添加到 src 列表中。
  5. 添加编译选项 -O0LOCAL_CCFLAGS
  6. 使用 DefineGroup 函数创建一个名为 aic_osal 的构建组,其中包含源文件、依赖项、头文件路径和编译选项。
  7. 返回创建的构建组。

应用程序的 SConscript 示例

以下是一个典型的应用程序的 SConscript 示例,用于构建一个多媒体播放器:

Import('AIC_ROOT')
Import('PRJ_KERNEL')
from building import *

cwd = GetCurrentDir()
path = [cwd + '/include']
path += [cwd + '/base/include']
path += [cwd + '/ge/include']

path += [cwd + '/ve/include']
path += [cwd + '../../../bsp/artinchip/include/uapi']
path += [cwd + '/mpp_test']

if GetDepend(['AIC_MPP_PLAYER_INTERFACE']):
    #audio decoder
    path += [cwd + '/middle_media/audio_decoder/include']
    path += [cwd + '/middle_media/audio_decoder/decoder']

    #base
    path += [cwd + '/middle_media/base/include']
    path += [cwd + '/middle_media/base/parser/mov']
    path += [cwd + '/middle_media/base/parser/rawdata']
    path += [cwd + '/middle_media/base/stream/file']


src = []
CPPDEFINES = []

# mpp
if GetDepend(['LPKG_MPP']):
    src += Glob('./base/memory/*.c')
    src += Glob('./ge/*.c')
    src += Glob('./fb/*.c')
    src += Glob('ve/decoder/*.c')
    src += Glob('ve/common/*.c')
    src += Glob('ve/decoder/jpeg/*.c')
    src += Glob('ve/decoder/png/*.c')
    src += Glob('ve/decoder/h264/*.c')
    src += Glob('./mpp_test/*.c')


if GetDepend(['AIC_MPP_PLAYER_INTERFACE']):
    #audio decoder
    src += Glob('middle_media/audio_decoder/decoder/*.c')
    src += Glob('middle_media/audio_decoder/decoder/mp3/mp3_decoder.c')

//DefineGroup(name, src, depend,**parameters)
group = DefineGroup('mpp', src, depend = [''], CPPPATH = path, CPPDEFINES = CPPDEFINES)

Return('group')
上述 SConscript 示例代码主要完成以下任务:
  1. 导入必要的变量和模块。
  2. 获取当前目录并设置头文件路径列表 path
  3. 根据条件判断是否包含特定的头文件路径到 path 中。
  4. 初始化源文件列表 src 和编译选项列表 CPPDEFINES
  5. 如果启用了多媒体播放器接口,根据条件将相应的源文件添加到 src 列表中。
  6. 使用 DefineGroup 函数创建一个名为 mpp 的构建组,其中包含源文件、依赖项、头文件路径和编译选项。
  7. 返回创建的构建组。