构建系统
CMake与构建工具
概述
C语言项目从源代码到可执行文件需要经过编译、链接等步骤。当项目规模增大、源文件增多、需要跨平台支持时,手动执行 gcc 命令变得不切实际。构建系统(Build System)负责自动化管理编译流程,解决依赖关系,并支持跨平台构建。CMake 是目前C/C++项目最主流的构建系统,此外还有 Make、Ninja、Meson 等工具。
基础概念
编译流程
C语言项目从源码到可执行文件的基本流程:
- 预处理:展开宏、处理条件编译、包含头文件(
.c->.i) - 编译:将预处理后的代码翻译为汇编代码(
.i->.s) - 汇编:将汇编代码翻译为目标文件(
.s->.o) - 链接:将目标文件和库文件合并为可执行文件(
.o-> 可执行文件)
构建系统的作用
- 自动检测源文件之间的依赖关系
- 只重新编译发生变化的文件(增量构建)
- 管理编译选项(优化级别、警告、标准版本)
- 处理第三方库的查找和链接
- 支持跨平台构建(生成不同平台的构建文件)
常见构建工具
| 工具 | 说明 |
|---|---|
| Make | 最经典的构建工具,使用 Makefile |
| CMake | 元构建系统,生成 Makefile 或其他构建文件 |
| Ninja | 专注于速度的底层构建工具 |
| Meson | 新一代构建系统,使用 Python 风格语法 |
| Autotools | GNU 项目的传统构建系统 |
快速上手
最简单的 CMake 项目
项目结构:
myproject/
CMakeLists.txt
src/
main.c
CMakeLists.txt:
# 最低CMake版本要求
cmake_minimum_required(VERSION 3.20)
# 项目名称和语言
project(MyProject C)
# 设置C标准
set(CMAKE_C_STANDARD 17)
set(CMAKE_C_STANDARD_REQUIRED ON)
# 添加可执行文件
add_executable(program src/main.c)
构建命令:
# 创建构建目录并进入
mkdir build && cd build
# 配置项目(生成构建文件)
cmake ..
# 编译项目
cmake --build .
使用 Make 直接构建
# Makefile 示例
CC = gcc
CFLAGS = -Wall -Wextra -std=c17 -O2
TARGET = program
SRCS = main.c utils.c
OBJS = $(SRCS:.c=.o)
# 默认目标
all: $(TARGET)
# 链接
$(TARGET): $(OBJS)
$(CC) $(CFLAGS) -o $@ $^
# 编译
%.o: %.c
$(CC) $(CFLAGS) -c -o $@ $<
# 清理
clean:
rm -f $(OBJS) $(TARGET)
# 声明伪目标
.PHONY: all clean
构建命令:
make # 编译
make clean # 清理
详细用法
多源文件项目
myproject/
CMakeLists.txt
include/
utils.h
src/
main.c
utils.c
cmake_minimum_required(VERSION 3.20)
project(MyProject C)
set(CMAKE_C_STANDARD 17)
# 添加可执行文件,包含多个源文件
add_executable(program
src/main.c
src/utils.c
)
# 添加头文件搜索路径
target_include_directories(program PRIVATE include)
# 添加编译选项
target_compile_options(program PRIVATE -Wall -Wextra -Wpedantic)
构建类型与优化
# 默认构建类型(可在命令行覆盖)
if(NOT CMAKE_BUILD_TYPE)
set(CMAKE_BUILD_TYPE Release)
endif()
# 不同构建类型的编译选项
# Debug: -g -O0(调试信息,无优化)
# Release: -O3 -DNDEBUG(最大优化,去除断言)
# RelWithDebInfo: -O2 -g -DNDEBUG(优化加调试信息)
# MinSizeRel: -Os -DNDEBUG(最小体积优化)
# 为 Debug 模式添加额外选项
target_compile_options(program PRIVATE
$<$<CONFIG:Debug>:-g3 -O0>
$<$<CONFIG:Release>:-O3>
)
命令行指定构建类型:
cmake -DCMAKE_BUILD_TYPE=Debug ..
cmake --build .
查找和使用外部库
cmake_minimum_required(VERSION 3.20)
project(MathApp C)
set(CMAKE_C_STANDARD 17)
add_executable(mathapp src/main.c)
# 查找数学库
find_library(MATH_LIBRARY m)
if(MATH_LIBRARY)
target_link_libraries(mathapp PRIVATE ${MATH_LIBRARY})
endif()
# 查找 pthread 库
find_package(Threads REQUIRED)
target_link_libraries(mathapp PRIVATE Threads::Threads)
# 查找自定义库(如 libcurl)
find_package(CURL REQUIRED)
if(CURL_FOUND)
target_include_directories(mathapp PRIVATE ${CURL_INCLUDE_DIRS})
target_link_libraries(mathapp PRIVATE ${CURL_LIBRARIES})
endif()
安装规则
# 安装可执行文件到 bin 目录
install(TARGETS program
RUNTIME DESTINATION bin
)
# 安装头文件到 include 目录
install(FILES include/utils.h
DESTINATION include
)
# 安装库文件
install(TARGETS mylib
LIBRARY DESTINATION lib
ARCHIVE DESTINATION lib
)
安装命令:
cmake --install . --prefix /usr/local
常见场景
场景一:构建静态库和动态库
cmake_minimum_required(VERSION 3.20)
project(MyLib C)
set(CMAKE_C_STANDARD 17)
# 库的源文件
set(LIB_SOURCES
src/string_utils.c
src/file_utils.c
src/math_utils.c
)
# 构建静态库
add_library(mylib_static STATIC ${LIB_SOURCES})
target_include_directories(mylib_static PUBLIC include)
# 构建动态库
add_library(mylib_shared SHARED ${LIB_SOURCES})
target_include_directories(mylib_shared PUBLIC include)
set_target_properties(mylib_shared PROPERTIES
OUTPUT_NAME "mylib" # 输出文件名
VERSION 1.2.0 # 版本号
SOVERSION 1 # ABI 版本
)
# 构建使用该库的测试程序
add_executable(test_program tests/test.c)
target_link_libraries(test_program PRIVATE mylib_static)
场景二:多目录大型项目
project/
CMakeLists.txt # 顶层
app/
CMakeLists.txt # 应用子目录
main.c
lib/
CMakeLists.txt # 库子目录
core/
core.c
core.h
utils/
utils.c
utils.h
tests/
CMakeLists.txt # 测试子目录
test_core.c
顶层 CMakeLists.txt:
cmake_minimum_required(VERSION 3.20)
project(MyProject C)
set(CMAKE_C_STANDARD 17)
# 添加子目录
add_subdirectory(lib)
add_subdirectory(app)
# 可选:添加测试
option(BUILD_TESTS "构建测试" ON)
if(BUILD_TESTS)
enable_testing()
add_subdirectory(tests)
endif()
lib/CMakeLists.txt:
# 构建核心库
add_library(core core/core.c)
target_include_directories(core PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/core)
# 构建工具库
add_library(utils utils/utils.c)
target_include_directories(utils PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/utils)
target_link_libraries(utils PUBLIC core)
app/CMakeLists.txt:
add_executable(myapp main.c)
target_link_libraries(myapp PRIVATE core utils)
tests/CMakeLists.txt:
add_executable(test_core test_core.c)
target_link_libraries(test_core PRIVATE core)
# 添加测试
add_test(NAME test_core COMMAND test_core)
场景三:交叉编译
# toolchain-arm.cmake - 交叉编译工具链文件
set(CMAKE_SYSTEM_NAME Linux)
set(CMAKE_SYSTEM_PROCESSOR arm)
# 指定交叉编译器
set(CMAKE_C_COMPILER arm-linux-gnueabihf-gcc)
set(CMAKE_CXX_COMPILER arm-linux-gnueabihf-g++)
# 搜索路径设置
set(CMAKE_FIND_ROOT_PATH /usr/arm-linux-gnueabihf)
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
使用工具链文件构建:
cmake -DCMAKE_TOOLCHAIN_FILE=toolchain-arm.cmake ..
cmake --build .
注意事项
构建目录不要放在源码目录中
CMake 推荐使用”外部构建”(out-of-source build),将构建产物放在单独的目录中,避免污染源码:
# 正确:在单独的目录中构建
mkdir build && cd build
cmake ..
# 错误:在源码目录中直接构建(会生成大量临时文件)
cmake .
CMake 版本兼容性
cmake_minimum_required 指定的版本会影响CMake的行为策略。建议使用较新的版本(3.20+),并注意不同版本之间的策略变化。
target_link_libraries 的作用域
# PUBLIC: 依赖传递给使用此目标的其他目标
# PRIVATE: 依赖仅用于此目标,不传递
# INTERFACE: 依赖仅传递给使用此目标的其他目标,不用于此目标本身
target_link_libraries(myapp
PUBLIC core # core 的头文件对 myapp 的使用者也可见
PRIVATE utils # utils 仅 myapp 内部使用
)
避免使用过时的CMake命令
# 过时写法(不推荐)
include_directories(include)
link_libraries(m)
add_definitions(-DDEBUG)
# 推荐写法(基于目标)
target_include_directories(myapp PRIVATE include)
target_link_libraries(myapp PRIVATE m)
target_compile_definitions(myapp PRIVATE DEBUG)
进阶用法
使用 FetchContent 管理第三方依赖
cmake_minimum_required(VERSION 3.20)
project(MyApp C)
include(FetchContent)
# 自动下载并构建 cJSON 库
FetchContent_Declare(
cjson
GIT_REPOSITORY https://github.com/DaveGamble/cJSON.git
GIT_TAG v1.7.17
)
FetchContent_MakeAvailable(cjson)
add_executable(myapp src/main.c)
target_link_libraries(myapp PRIVATE cjson)
自定义命令和生成代码
# 在构建前生成版本信息头文件
set(VERSION_FILE ${CMAKE_BINARY_DIR}/generated/version.h)
add_custom_command(
OUTPUT ${VERSION_FILE}
COMMAND ${CMAKE_COMMAND}
-DVERSION_FILE=${VERSION_FILE}
-DPROJECT_VERSION=${PROJECT_VERSION}
-P ${CMAKE_SOURCE_DIR}/cmake/generate_version.cmake
DEPENDS ${CMAKE_SOURCE_DIR}/cmake/generate_version.cmake
COMMENT "生成版本信息头文件"
)
# 将生成的文件加入构建
add_executable(myapp
src/main.c
${VERSION_FILE}
)
target_include_directories(myapp PRIVATE ${CMAKE_BINARY_DIR}/generated)
使用 CMake Presets
CMake Presets(预设)允许在 CMakePresets.json 中定义常用的配置,简化构建命令:
{
"version": 3,
"configurePresets": [
{
"name": "debug",
"binaryDir": "${sourceDir}/build/debug",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Debug",
"CMAKE_C_FLAGS": "-g3 -O0 -Wall"
}
},
{
"name": "release",
"binaryDir": "${sourceDir}/build/release",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Release",
"CMAKE_C_FLAGS": "-O3 -Wall"
}
}
],
"buildPresets": [
{
"name": "debug",
"configurePreset": "debug"
},
{
"name": "release",
"configurePreset": "release"
}
]
}
使用预设构建:
cmake --preset debug
cmake --build --preset debug
使用 Ninja 加速构建
Ninja 是一个专注于构建速度的工具,比 Make 更快:
# 安装 Ninja
# Ubuntu: sudo apt install ninja-build
# macOS: brew install ninja
# 使用 Ninja 生成器
cmake -G Ninja ..
cmake --build .
# 或在预设中指定
# "generator": "Ninja"