第一章:什么是 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')獲取這個返回值,并將其整合到整個項目的構建流程中 - 這種機制實現了模塊化的構建配置,每個子模塊負責定義自己的構建信息,再由父腳本統一組織,使大型項目的構建配置更加清晰和可維護