目录
SCons 从入门到精通详细总结
一、SCons 概述与核心概念
1.1 什么是 SCons
SCons 是一个基于 Python 的软件构建工具,使用 Python 脚本描述构建过程,是 Make、CMake 的替代品。
核心特点:
- 跨平台(Windows、Linux、macOS)
- 依赖关系自动分析
- 内置支持多种编程语言
- 使用 Python 语法,功能强大灵活
1.2 安装 SCons
# 使用 pip 安装
pip install scons
# 验证安装
scons --version
1.3 项目结构
project/
├── SConstruct # 主构建文件
├── SConscript # 子目录构建文件
├── src/ # 源代码
└── build/ # 构建输出(可选)
二、基础入门
2.1 最简单的 SConstruct
# 构建单个 C 程序
Program('hello.c')
# 指定输出名称
Program('hello', 'hello.c')
# 构建多个源文件
Program('app', ['main.c', 'util.c', 'config.c'])
2.2 基本构建目标
# 编译对象文件
Object('hello.o', 'hello.c')
# 创建静态库
StaticLibrary('libutils.a', ['util1.c', 'util2.c'])
# 创建动态库
SharedLibrary('libutils.so', ['util1.c', 'util2.c'])
# 链接库到可执行文件
Program('app',
['main.c'],
LIBS=['utils', 'm'],
LIBPATH=['./lib']
)
2.3 环境变量
# 创建构建环境
env = Environment()
# 设置编译选项
env.Append(CCFLAGS=['-O2', '-g', '-Wall'])
env.Append(LINKFLAGS=['-lm'])
# 使用环境构建
env.Program('hello', 'hello.c')
# 克隆并修改环境
debug_env = env.Clone()
debug_env.Append(CCFLAGS=['-DDEBUG', '-O0'])
debug_env.Program('hello_debug', 'hello.c')
三、中级特性
3.1 依赖与扫描器
# 显式依赖声明
hello = Program('hello', 'hello.c')
Depends(hello, 'header.h')
# 自定义扫描器(查找 #include)
def custom_scanner(node, env, path):
# 分析文件中的依赖
contents = node.get_text_contents()
includes = []
# 简单正则匹配 #include
import re
for match in re.finditer(r'#include\s+["<]([^">]+)[">]', contents):
includes.append(match.group(1))
return includes
# 创建扫描器
scanner = Scanner(
function=custom_scanner,
skeys=['.c', '.cpp']
)
# 添加到环境
env.Append(SCANNERS=scanner)
3.2 多目录项目
# SConstruct(主文件)
env = Environment()
# 导出环境变量
Export('env')
# 调用子目录的 SConscript
SConscript('src/SConscript', variant_dir='build/src', duplicate=0)
SConscript('lib/SConscript', variant_dir='build/lib', duplicate=0)
# src/SConscript(子文件)
Import('env')
# 构建本目录的目标
env.Program('app', Glob('*.c'))
3.3 条件构建
# 根据平台设置
if env['PLATFORM'] == 'win32':
env.Append(LIBS=['ws2_32'])
env.Append(CCFLAGS=['/D_CRT_SECURE_NO_WARNINGS'])
else:
env.Append(LIBS=['pthread'])
env.Append(CCFLAGS=['-pthread'])
# 根据命令行参数
AddOption('--debug', dest='debug', action='store_true',
default=False, help='build debug version')
if GetOption('debug'):
env.Append(CCFLAGS=['-g', '-DDEBUG'])
else:
env.Append(CCFLAGS=['-O3'])
3.4 自定义构建器
# 定义自定义命令
def generate_doxygen(source, target, env, for_signature):
return f"doxygen {source[0].path}"
# 创建自定义构建器
doxygen_builder = Builder(
action=generate_doxygen,
suffix='.html',
src_suffix='.doxyfile'
)
# 添加到环境
env.Append(BUILDERS={'Doxygen': doxygen_builder})
# 使用自定义构建器
env.Doxygen('docs/html', 'Doxyfile')
四、高级特性
4.1 文件与目录操作
# 文件操作
src_files = Glob('src/*.c')
header_files = Glob('include/*.h')
# 文件过滤
c_files = [f for f in src_files if not f.name.endswith('_test.c')]
# 目录创建
env.Command('build/obj', None, Mkdir('$TARGET'))
# 文件复制
env.Command('config.ini', 'config.ini.default',
Copy('$TARGET', '$SOURCE'))
# 查找文件
lib_path = Dir('#/lib') # 项目根目录下的 lib
src_dir = Dir('src')
4.2 构建前后动作
# 构建前执行
def pre_build_message(target, source, env):
print(f"Building {target[0]} from {len(source)} sources")
env['BUILDERS']['Program'].add_pre_action(pre_build_message)
# 构建后执行
def post_build_message(target, source, env):
import os
size = os.path.getsize(target[0].path)
print(f"Built {target[0].name} ({size} bytes)")
env['BUILDERS']['Program'].add_post_action(post_build_message)
4.3 并行构建控制
# 设置并行构建参数
SetOption('num_jobs', 4) # 或通过命令行 scons -j4
# 显式依赖控制
env.Serialize(['big_target1', 'big_target2']) # 顺序执行
# 独立构建目标
env.SeparateBuild(['test1', 'test2'])
4.4 缓存与增量构建
# 启用缓存
CacheDir('cache')
# 强制重建
env.Decider('MD5-timestamp') # 默认,同时检查MD5和时间戳
# env.Decider('MD5') # 只检查MD5
# env.Decider('timestamp-newer') # 只检查时间戳
# 清理特定目标
Clean('hello', 'hello.o')
# 自定义清理规则
def custom_clean(target, source, env):
import shutil
shutil.rmtree('temp_files', ignore_errors=True)
env.AddPostAction('all', custom_clean)
五、实际项目配置示例
5.1 C/C++ 项目完整示例
# SConstruct
import os
# 创建基础环境
env = Environment()
# 平台特定配置
platform = env['PLATFORM']
if platform == 'win32':
env.Append(CCFLAGS=['/EHsc', '/W3'])
env['CC'] = 'cl' # 使用 MSVC
elif platform in ['posix', 'darwin']:
env.Append(CCFLAGS=['-std=c++11', '-Wall', '-Wextra'])
env['CC'] = 'g++' # 使用 GCC
# 自定义配置选项
AddOption('--prefix', dest='prefix', type='string',
default='/usr/local', help='安装路径')
AddOption('--build-type', dest='build_type', type='choice',
choices=['debug', 'release', 'profile'],
default='release', help='构建类型')
# 根据构建类型配置
build_type = GetOption('build_type')
if build_type == 'debug':
env.Append(CCFLAGS=['-g', '-O0', '-DDEBUG'])
elif build_type == 'release':
env.Append(CCFLAGS=['-O3', '-DNDEBUG'])
elif build_type == 'profile':
env.Append(CCFLAGS=['-pg', '-O2'])
# 添加包含路径
env.Append(CPPPATH=['include', 'third_party/include'])
# 导出环境
Export('env')
# 构建子目录
SConscript('src/SConscript', variant_dir='build/src', duplicate=0)
SConscript('test/SConscript', variant_dir='build/test', duplicate=0)
# 安装目标
def install_target(env, target):
prefix = GetOption('prefix')
return env.Install(os.path.join(prefix, 'bin'), target)
# 默认目标
Default(['build/src/app', 'build/src/lib'])
5.2 自动化测试集成
# 测试运行器
def run_tests(target, source, env):
import subprocess
import sys
test_programs = [str(s) for s in source]
all_passed = True
for test in test_programs:
print(f"\nRunning {test}...")
result = subprocess.run([test], capture_output=True, text=True)
if result.returncode == 0:
print(f"✓ {test} passed")
else:
print(f"✗ {test} failed")
print(result.stderr)
all_passed = False
if not all_passed:
sys.exit(1)
# 创建测试运行构建器
test_runner = Builder(
action=run_tests,
suffix='.tested',
src_suffix=''
)
env.Append(BUILDERS={'TestRunner': test_runner})
# 构建并运行测试
test_programs = ['test1', 'test2', 'test3']
test_targets = [env.Program(t, [t + '.c']) for t in test_programs]
env.TestRunner('all_tests.tested', test_targets)
# 添加测试到默认目标
Alias('test', 'all_tests.tested')
Default('test')
六、最佳实践与技巧
6.1 性能优化
# 1. 使用 variant_dir 避免污染源码目录
SConscript('src/SConscript', variant_dir='build', duplicate=0)
# 2. 启用缓存
CacheDir('~/.scons_cache')
# 3. 限制扫描范围
env['CPPPATH'] = ['include'] # 明确的包含路径
# 4. 使用 Decider 优化
env.Decider('MD5') # 如果时间戳不可靠
# 5. 避免在 SConstruct 中执行耗时代码
def expensive_computation():
# 延迟执行,只在需要时计算
pass
# 使用惰性评估
env['EXPENSIVE_DATA'] = expensive_computation
6.2 模块化组织
# tools/__init__.py
def setup_cpp_env(env):
"""C++ 环境配置"""
env.Append(CXXFLAGS=['-std=c++17'])
return env
def setup_test_env(env):
"""测试环境配置"""
env.Append(LIBS=['gtest', 'gmock'])
return env
# SConstruct
import tools
env = Environment()
env = tools.setup_cpp_env(env)
# 根据不同需求创建环境变体
test_env = env.Clone()
test_env = tools.setup_test_env(test_env)
6.3 错误处理与调试
# 调试模式
SetOption('debug', 'explain') # 解释为什么重建
# SetOption('debug', 'presub') # 显示预处理后的命令
# SetOption('debug', 'stacktrace') # 显示堆栈跟踪
# 自定义错误处理
def handle_error(error):
print(f"Build error: {error}")
# 发送通知等
return True # 继续构建
env['BUILD_ERROR_FUNC'] = handle_error
# 验证构建参数
def validate_build(env):
if 'CC' not in env:
raise Exception("Compiler not set")
return True
AddPreAction('all', validate_build)
6.4 跨平台支持策略
def detect_tools(env):
"""自动检测可用工具链"""
import sys
if sys.platform == 'win32':
# 尝试查找 MSVC
for version in ['14.0', '12.0', '10.0']:
if FindMSVCCurrentVersion(version):
env.Tool('msvc')
return 'msvc'
# 回退到 MinGW
env.Tool('mingw')
return 'mingw'
else:
# Unix-like 系统
env.Tool('gcc')
return 'gcc'
# 工具链配置字典
toolchains = {
'gcc': {'CC': 'gcc', 'CXX': 'g++'},
'clang': {'CC': 'clang', 'CXX': 'clang++'},
'msvc': {'CC': 'cl', 'CXX': 'cl'},
}
def setup_toolchain(env, toolchain_name):
"""配置指定工具链"""
if toolchain_name in toolchains:
env.Replace(**toolchains[toolchain_name])
return env
七、扩展与集成
7.1 集成第三方构建系统
# 集成 CMake
def cmake_build(target, source, env, for_signature):
return (
f"cmake -B {target[0].dir} -S {source[0].dir} && "
f"cmake --build {target[0].dir}"
)
env['BUILDERS']['CMake'] = Builder(action=cmake_build)
# 集成 Autotools
def autotools_build(target, source, env):
import subprocess
srcdir = source[0].dir.abspath
builddir = target[0].dir.abspath
subprocess.run(['autoreconf', '-i'], cwd=srcdir)
subprocess.run([f'{srcdir}/configure', f'--prefix={builddir}'],
cwd=builddir)
subprocess.run(['make', 'install'], cwd=builddir)
env.Command('third_party/lib', 'third_party/src', autotools_build)
7.2 插件系统
# plugin/scons_extensions.py
class CodeCoverage:
def __init__(self, env):
self.env = env
def enable(self):
self.env.Append(CCFLAGS=['--coverage'])
self.env.Append(LINKFLAGS=['--coverage'])
# 添加覆盖率报告生成
self.env.Command('coverage.info', '*', self._generate_coverage)
def _generate_coverage(self, target, source, env):
# 生成覆盖率报告
pass
# SConstruct 中使用
import plugin.scons_extensions as ext
env = Environment()
cov = ext.CodeCoverage(env)
cov.enable()
7.3 持续集成集成
# ci_helper.py
import json
import os
class CIHelper:
def __init__(self, env):
self.env = env
self.ci_data = {}
def record_build_info(self):
"""记录构建信息"""
self.ci_data['timestamp'] = os.environ.get('CI_BUILD_TIMESTAMP', '')
self.ci_data['commit'] = os.environ.get('CI_COMMIT_SHA', '')
# 保存到文件
with open('build_info.json', 'w') as f:
json.dump(self.ci_data, f)
def upload_artifacts(self, patterns):
"""上传构建产物"""
import glob
artifacts = []
for pattern in patterns:
artifacts.extend(glob.glob(pattern))
# 这里可以实现上传逻辑
return artifacts
# 在构建过程中使用
ci = CIHelper(env)
ci.record_build_info()
# 构建后上传
if GetOption('ci_mode'):
PostAction('all', lambda t, s, e: ci.upload_artifacts(['*.exe', '*.so', '*.a']))
八、完整项目示例
8.1 企业级项目配置
"""
企业级 SConstruct 模板
支持:多配置、多平台、测试、打包、文档
"""
import os
import sys
import hashlib
from pathlib import Path
# ========== 初始化 ==========
env = Environment(ENV=os.environ)
# ========== 配置选项 ==========
AddOption('--build-dir', dest='build_dir', type='string',
default='build', help='构建目录')
AddOption('--config', dest='config', type='choice',
choices=['debug', 'release', 'sanitize', 'coverage'],
default='debug', help='构建配置')
AddOption('--toolchain', dest='toolchain', type='string',
default='auto', help='工具链 (gcc, clang, msvc)')
AddOption('--install-prefix', dest='install_prefix', type='string',
default='/usr/local', help='安装前缀')
# ========== 工具链配置 ==========
def setup_toolchain(env, toolchain):
"""配置工具链"""
if toolchain == 'auto':
# 自动检测
if sys.platform == 'win32':
env.Tool('msvc')
else:
env.Tool('gcc')
elif toolchain == 'clang':
env['CC'] = 'clang'
env['CXX'] = 'clang++'
elif toolchain == 'gcc':
env['CC'] = 'gcc'
env['CXX'] = 'g++'
elif toolchain == 'msvc':
env.Tool('msvc')
return env
env = setup_toolchain(env, GetOption('toolchain'))
# ========== 构建配置 ==========
config = GetOption('config')
build_dir = GetOption('build_dir')
config_flags = {
'debug': {'CCFLAGS': ['-O0', '-g', '-DDEBUG']},
'release': {'CCFLAGS': ['-O3', '-DNDEBUG', '-flto']},
'sanitize': {'CCFLAGS': ['-fsanitize=address,undefined', '-g']},
'coverage': {'CCFLAGS': ['--coverage', '-O0', '-g'],
'LINKFLAGS': ['--coverage']}
}
env.Replace(**config_flags.get(config, {}))
# ========== 平台特定配置 ==========
if env['PLATFORM'] == 'win32':
env.Append(CCFLAGS=['/EHsc', '/W4'])
env.Append(LINKFLAGS=['/SUBSYSTEM:CONSOLE'])
else:
env.Append(CCFLAGS=['-Wall', '-Wextra', '-Wpedantic',
'-fvisibility=hidden'])
env.Append(LINKFLAGS=['-pthread'])
# ========== 项目配置 ==========
env.Append(CPPPATH=['include', 'src'])
env.Append(LIBPATH=['lib'])
# 版本信息
version = '1.0.0'
env.Append(CPPDEFINES=[('VERSION', f'\\"{version}\\"')])
# ========== 构建规则 ==========
# 导出环境
Export('env')
# 构建源码
src_targets = SConscript('src/SConscript',
variant_dir=os.path.join(build_dir, 'src'),
duplicate=0,
exports=['env', 'config'])
# 构建测试
if config != 'release': # 发布版本不构建测试
test_targets = SConscript('tests/SConscript',
variant_dir=os.path.join(build_dir, 'tests'),
duplicate=0,
exports=['env'])
else:
test_targets = []
# ========== 安装规则 ==========
def install_project(env, targets, prefix):
"""安装项目文件"""
install_actions = []
for target in targets:
if str(target).endswith(('.exe', '')): # 可执行文件
bin_dir = os.path.join(prefix, 'bin')
install_actions.append(env.Install(bin_dir, target))
elif str(target).endswith(('.so', '.dylib', '.dll')): # 动态库
lib_dir = os.path.join(prefix, 'lib')
install_actions.append(env.Install(lib_dir, target))
elif str(target).endswith('.a'): # 静态库
lib_dir = os.path.join(prefix, 'lib')
install_actions.append(env.Install(lib_dir, target))
return install_actions
install_prefix = GetOption('install_prefix')
install_targets = install_project(env, src_targets, install_prefix)
# ========== 辅助命令 ==========
# 清理命令
def full_clean():
"""完全清理"""
import shutil
if os.path.exists(build_dir):
shutil.rmtree(build_dir)
print("Clean complete")
AddOption('--full-clean', dest='full_clean', action='store_true',
default=False, help='完全清理构建目录')
if GetOption('full_clean'):
full_clean()
sys.exit(0)
# ========== 默认目标 ==========
# 构建所有目标
all_targets = src_targets + test_targets
Default(all_targets)
# ========== 别名 ==========
# 创建常用别名
Alias('build', all_targets)
Alias('test', test_targets)
Alias('install', install_targets)
# ========== 构建信息 ==========
def print_build_info():
"""打印构建信息"""
print("\n" + "="*50)
print("Build Configuration")
print("="*50)
print(f"Platform: {env['PLATFORM']}")
print(f"Toolchain: {env.get('CXX', 'unknown')}")
print(f"Config: {config}")
print(f"Build dir: {build_dir}")
print(f"Version: {version}")
print("="*50 + "\n")
# 构建前打印信息
PreAction('build', print_build_info)
# ========== 构建后处理 ==========
def post_build_checks(target, source, env):
"""构建后检查"""
import subprocess
# 检查可执行文件
for t in target:
if str(t).endswith(('.exe', '')) and os.path.exists(str(t)):
# 运行基本检查
try:
result = subprocess.run([str(t), '--version'],
capture_output=True, text=True)
if result.returncode == 0:
print(f"✓ {t.name} built successfully")
except:
pass
# 生成构建哈希
build_hash = hashlib.sha256(str(target).encode()).hexdigest()[:8]
with open(os.path.join(build_dir, 'build.hash'), 'w') as f:
f.write(build_hash)
return 0
PostAction('build', post_build_checks)
print("SCons build system initialized.")
九、常见问题与解决方案
9.1 性能问题
# 问题:构建缓慢
# 解决方案:
# 1. 启用缓存
CacheDir('/tmp/scons_cache')
# 2. 使用 variant_dir 分离源码和构建文件
SConscript('src/SConscript', variant_dir='build', duplicate=0)
# 3. 优化扫描器
env.Decider('MD5') # 使用 MD5 而非时间戳
# 4. 并行构建
SetOption('num_jobs', os.cpu_count())
# 5. 避免全局扫描
env['CPPPATH'] = ['include'] # 明确指定包含路径
9.2 依赖问题
# 问题:依赖检测不准确
# 解决方案:
# 1. 显式声明依赖
hello = Program('hello', 'hello.c')
Depends(hello, 'config.h')
# 2. 自定义扫描器
def header_scanner(node, env, path):
# 自定义头文件扫描逻辑
pass
env.Append(SCANNERS=Scanner(header_scanner))
# 3. 使用 SideEffect 处理生成的头文件
generated_header = env.Command('generated.h', 'template.h', generate_header)
SideEffect('generated.h', generated_header)
9.3 跨平台问题
# 问题:跨平台兼容性
# 解决方案:
def platform_specific_setup(env):
"""平台特定设置"""
platform = env['PLATFORM']
if platform == 'win32':
# Windows 设置
env['PROGSUFFIX'] = '.exe'
env['SHLIBSUFFIX'] = '.dll'
env.Append(LIBS=['ws2_32', 'advapi32'])
elif platform == 'darwin':
# macOS 设置
env['SHLIBSUFFIX'] = '.dylib'
env.Append(FRAMEWORKS=['Cocoa'])
else:
# Linux/Unix 设置
env['SHLIBSUFFIX'] = '.so'
env.Append(LIBS=['dl', 'pthread'])
return env
env = platform_specific_setup(env)
十、学习资源与进阶
10.1 官方资源
10.2 调试技巧
# 调试命令
scons --debug=explain # 解释为什么重建目标
scons --debug=presub # 显示预处理后的命令
scons --debug=stacktrace # 显示错误堆栈
scons --debug=time # 显示时间统计
scons -Q # 安静模式,只显示命令
# 生成构建图
scons --tree=all # 显示依赖树
10.3 进阶主题
- 分布式构建:结合 distcc、icecream
- 增量打包:集成 CPack、RPM 构建
- 容器化构建:Docker 集成
- IDE 集成:生成 compile_commands.json
- 构建服务器:集成 Jenkins、GitLab CI
总结
SCons 是一个强大而灵活的构建系统,通过 Python 脚本提供完整的构建控制。从简单项目到复杂企业级应用,SCons 都能提供良好的支持。关键是要理解其核心概念:
- 环境:构建配置的容器
- 构建器:执行特定构建操作
- 扫描器:自动检测依赖
- 变量:控制构建过程
掌握 SCons 需要实践和经验积累,建议从简单项目开始,逐步添加复杂功能。记住,良好的构建系统应该:
- 可重复:每次构建结果一致
- 高效:增量构建快速
- 可维护:配置清晰易懂
- 可扩展:易于添加新功能
通过合理设计 SConstruct 和 SConscript 文件,你可以创建出既强大又易于维护的构建系统。

5249

被折叠的 条评论
为什么被折叠?



