从入门到精通:CMakeLists.txt 完全指南

从入门到精通:CMakeLists.txt 完全指南

CMake 是一个跨平台的自动化构建系统,它使用名为 CMakeLists.txt 的配置文件来控制软件的编译过程。无论你是刚接触 CMake 的新手,还是希望提升 CMake 技能的中级开发者,这篇指南都将带你从基础到高级全面掌握 CMakeLists.txt 的编写技巧。

一、CMake 基础入门

1.1 什么是 CMake?

CMake 是一个跨平台的开源构建系统,它通过读取 CMakeLists.txt 文件中的指令来生成标准的构建文件(如 Unix 的 Makefile 或 Windows 的 Visual Studio 项目文件)。CMake 的主要优势在于:

  • 跨平台性:可以生成适用于不同操作系统和编译器的构建文件
  • 简化构建过程:自动处理依赖关系和编译顺序
  • 模块化设计:支持大型项目的模块化管理
  • 可扩展性:可以通过自定义命令和函数扩展功能

1.2 最简单的 CMakeLists.txt

让我们从一个最简单的 "Hello World" 项目开始:

cmake 复制代码
# 指定 CMake 的最低版本要求
cmake_minimum_required(VERSION 3.10)

# 设置项目名称
project(HelloWorld)

# 添加可执行文件
add_executable(HelloWorld main.cpp)

这个简单的 CMakeLists.txt 文件包含三个基本指令:

  1. cmake_minimum_required:指定构建此项目所需的最低 CMake 版本
  2. project:定义项目名称和相关信息
  3. add_executable:指定要生成的可执行文件和源文件

1.3 构建过程

使用这个 CMakeLists.txt 文件的典型构建流程是:

bash 复制代码
mkdir build  # 创建构建目录(推荐外部构建)
cd build     # 进入构建目录
cmake ..     # 生成构建系统
make         # 编译项目

这种"外部构建"的方式(在单独的 build 目录中构建)是推荐的做法,因为它不会污染源代码目录。

二、CMakeLists.txt 核心语法详解

2.1 基本指令深入

2.1.1 project() 指令

project() 指令不仅可以指定项目名称,还可以设置版本、描述和使用的编程语言:

cmake 复制代码
project(MyProject 
        VERSION 1.0.0 
        DESCRIPTION "A sample CMake project"
        LANGUAGES CXX)
  • VERSION:设置项目版本号
  • DESCRIPTION:项目描述信息
  • LANGUAGES:指定项目使用的编程语言(C 表示 C 语言,CXX 表示 C++)
2.1.2 添加可执行文件

add_executable() 指令用于生成可执行文件:

cmake 复制代码
add_executable(TargetName source1.cpp source2.cpp header1.h)

CMake 会自动识别 .cpp 文件为源文件,.h 文件为头文件。虽然头文件可以列出,但通常不需要,除非它们包含需要被 moc 或其他预处理器处理的代码。

2.1.3 添加库文件

使用 add_library() 可以创建库文件:

cmake 复制代码
add_library(LibraryName STATIC source1.cpp source2.cpp)

库的类型可以是:

  • STATIC:静态库(.a 或 .lib)
  • SHARED:动态库(.so 或 .dll)
  • MODULE:模块库(不被链接,但可能被运行时加载)

2.2 变量与属性

2.2.1 变量设置与使用

CMake 使用 set() 命令定义变量:

cmake 复制代码
set(MY_VARIABLE "Hello World")
set(SOURCE_FILES main.cpp utils.cpp)

使用 ${} 语法引用变量:

cmake 复制代码
message(STATUS "The value is: ${MY_VARIABLE}")
add_executable(MyApp ${SOURCE_FILES})
2.2.2 缓存变量

缓存变量会保存在 CMakeCache.txt 中,可以在后续构建中使用:

cmake 复制代码
set(MY_CACHE_VAR "DefaultValue" CACHE STRING "A description of this variable")

缓存变量可以在命令行通过 -D 选项设置:

bash 复制代码
cmake -DMY_CACHE_VAR="CustomValue" ..
2.2.3 环境变量

读取和使用环境变量:

cmake 复制代码
set(ENV{PATH} "$ENV{PATH}:/opt/local/bin")
message(STATUS "Current PATH: $ENV{PATH}")

2.3 控制流

2.3.1 条件语句
cmake 复制代码
if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
    message(STATUS "Running on Linux")
elseif(WIN32)
    message(STATUS "Running on Windows")
else()
    message(STATUS "Running on unknown system")
endif()

常用条件表达式:

  • DEFINED var:检查变量是否定义
  • EXISTS path:检查路径是否存在
  • file1 IS_NEWER_THAN file2:检查文件时间戳
  • version1 VERSION_LESS version2:版本比较
2.3.2 循环语句
cmake 复制代码
# foreach 循环
foreach(i RANGE 1 10)
    message(STATUS "Counter: ${i}")
endforeach()

# while 循环
set(i 0)
while(i LESS 10)
    message(STATUS "Counter: ${i}")
    math(EXPR i "${i} + 1")
endwhile()

2.4 文件操作

2.4.1 包含目录
cmake 复制代码
include_directories(${PROJECT_SOURCE_DIR}/include)

现代 CMake 更推荐使用 target_include_directories()

cmake 复制代码
target_include_directories(MyTarget PUBLIC include)
2.4.2 链接库
cmake 复制代码
target_link_libraries(MyTarget PUBLIC MyLibrary)

PUBLICPRIVATEINTERFACE 关键字控制依赖的传递性:

  • PRIVATE:仅当前目标使用
  • INTERFACE:仅依赖此目标的其他目标使用
  • PUBLIC:当前目标和其他依赖目标都使用
2.4.3 文件操作命令
cmake 复制代码
# 查找所有 .cpp 文件
file(GLOB SOURCES "src/*.cpp")

# 复制文件
file(COPY data DESTINATION ${CMAKE_BINARY_DIR})

# 读写文件
file(READ "${PROJECT_SOURCE_DIR}/VERSION" PROJECT_VERSION)
file(WRITE "${CMAKE_BINARY_DIR}/generated.h" "#define VERSION \"${PROJECT_VERSION}\"")

三、项目结构组织

3.1 基本项目结构

一个典型的 CMake 项目结构如下:

复制代码
MyProject/
├── CMakeLists.txt        # 顶层 CMake 配置
├── build/                # 构建目录(外部构建)
├── include/              # 公共头文件
│   └── MyLib/
│       └── header.h
├── src/                  # 源文件
│   ├── CMakeLists.txt    # 子目录 CMake 配置
│   ├── main.cpp
│   └── utils.cpp
└── tests/                # 测试代码
    └── CMakeLists.txt

3.2 子目录管理

使用 add_subdirectory() 将项目分解为多个子目录:

cmake 复制代码
# 顶层 CMakeLists.txt
cmake_minimum_required(VERSION 3.10)
project(MyProject)

add_subdirectory(src)    # 包含 src 子目录
add_subdirectory(tests)  # 包含 tests 子目录

每个子目录有自己的 CMakeLists.txt 文件。

3.3 源文件收集

有多种方式指定源文件:

  1. 显式列出所有源文件:
cmake 复制代码
add_executable(MyApp src/main.cpp src/utils.cpp)
  1. 使用 aux_source_directory 自动收集:
cmake 复制代码
aux_source_directory(. SRC_FILES)
add_executable(MyApp ${SRC_FILES})
  1. 使用 file(GLOB) 更灵活地匹配文件:
cmake 复制代码
file(GLOB SRC_FILES "src/*.cpp" "src/*.c")
add_executable(MyApp ${SRC_FILES})

注意:GLOB 不会自动检测新增文件,需要重新运行 CMake。

四、依赖管理与查找

4.1 查找系统库

使用 find_package 查找系统安装的库:

cmake 复制代码
find_package(Boost 1.70 REQUIRED COMPONENTS filesystem system)
target_link_libraries(MyTarget PUBLIC Boost::filesystem Boost::system)

REQUIRED 表示必须找到该包,否则报错。

4.2 自定义查找模块

如果 CMake 没有提供某个库的查找模块,可以自己编写 FindXXX.cmake

cmake 复制代码
# FindMyLib.cmake
find_path(MYLIB_INCLUDE_DIR mylib.h HINTS /usr/local/include)
find_library(MYLIB_LIBRARY NAMES mylib HINTS /usr/local/lib)

include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(MyLib DEFAULT_MSG MYLIB_LIBRARY MYLIB_INCLUDE_DIR)

if(MyLib_FOUND)
    set(MyLib_LIBRARIES ${MYLIB_LIBRARY})
    set(MyLib_INCLUDE_DIRS ${MYLIB_INCLUDE_DIR})
endif()

然后在 CMakeLists.txt 中使用:

cmake 复制代码
list(APPEND CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake")
find_package(MyLib REQUIRED)
target_include_directories(MyTarget PUBLIC ${MyLib_INCLUDE_DIRS})
target_link_libraries(MyTarget PUBLIC ${MyLib_LIBRARIES})

4.3 第三方依赖管理

现代 CMake 项目常用这些方法管理第三方依赖:

  1. Git 子模块
bash 复制代码
git submodule add https://github.com/xxx/yyy.git extern/yyy

然后在 CMakeLists.txt 中:

cmake 复制代码
add_subdirectory(extern/yyy)
target_link_libraries(MyTarget PUBLIC yyy)
  1. FetchContent(CMake 3.11+):
cmake 复制代码
include(FetchContent)
FetchContent_Declare(
  googletest
  GIT_REPOSITORY https://github.com/google/googletest.git
  GIT_TAG release-1.11.0
)
FetchContent_MakeAvailable(googletest)
target_link_libraries(MyTarget PRIVATE gtest_main)
  1. ExternalProject(更复杂但更灵活):
cmake 复制代码
include(ExternalProject)
ExternalProject_Add(
  MyExternalLib
  URL "http://example.com/mylib.tar.gz"
  CONFIGURE_COMMAND ""
  BUILD_COMMAND ""
  INSTALL_COMMAND ""
)

五、高级特性与技巧

5.1 生成器表达式

生成器表达式允许在生成构建系统时进行条件判断,常用于平台特定设置:

cmake 复制代码
target_compile_definitions(MyTarget
  PUBLIC $<$<CONFIG:Debug>:DEBUG_MODE=1>
         $<$<CXX_COMPILER_ID:GNU>:EXTRA_FEATURE=1>
)

常用生成器表达式:

  • $<CONFIG:cfg>:如果配置是 cfg 则为 1
  • $<PLATFORM_ID:platform>:平台匹配检查
  • $<COMPILE_LANGUAGE:lang>:编译语言检查

5.2 自定义命令与目标

cmake 复制代码
# 自定义命令
add_custom_command(
  OUTPUT generated.cpp
  COMMAND generator.py ${CMAKE_CURRENT_SOURCE_DIR}/input.txt > generated.cpp
  DEPENDS generator.py input.txt
)

# 自定义目标
add_custom_target(
  GenerateDocs ALL
  COMMAND doxygen Doxyfile
  WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
  COMMENT "Generating documentation"
)

5.3 交叉编译

设置交叉编译工具链:

cmake 复制代码
# toolchain.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 /path/to/sysroot)
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)

然后使用:

bash 复制代码
cmake -DCMAKE_TOOLCHAIN_FILE=toolchain.cmake ..

5.4 测试与打包

5.4.1 添加测试
cmake 复制代码
enable_testing()

add_test(NAME MyTest1
         COMMAND MyTestExe --test1
         WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR})

add_test(NAME MyTest2
         COMMAND MyTestExe --test2)

可以使用 CTest 运行测试:

bash 复制代码
ctest -V  # 运行所有测试并显示详细输出
5.4.2 安装规则
cmake 复制代码
install(TARGETS MyTarget
        RUNTIME DESTINATION bin
        LIBRARY DESTINATION lib
        ARCHIVE DESTINATION lib/static)

install(DIRECTORY include/ DESTINATION include)
install(FILES README.md DESTINATION doc)
5.4.3 打包
cmake 复制代码
include(InstallRequiredSystemLibraries)
set(CPACK_PACKAGE_VENDOR "My Company")
set(CPACK_PACKAGE_VERSION ${PROJECT_VERSION})
set(CPACK_SOURCE_GENERATOR "TGZ")
include(CPack)

生成包:

bash 复制代码
make package      # 生成二进制包
make package_source  # 生成源码包

六、现代 CMake 最佳实践

6.1 目标为中心的设计

现代 CMake 强调以目标(target)为中心的构建方式,每个库或可执行文件都是一个目标,明确指定其属性:

cmake 复制代码
add_library(MyLibrary STATIC src/lib.cpp)
target_include_directories(MyLibrary PUBLIC include)
target_compile_features(MyLibrary PUBLIC cxx_std_17)
target_link_libraries(MyLibrary PUBLIC SomeOtherLib)

6.2 作用域控制

正确使用 PRIVATEPUBLICINTERFACE 控制依赖传递:

cmake 复制代码
# MyLibrary 的 CMakeLists.txt
target_include_directories(MyLibrary
  PUBLIC include      # 使用者和被使用者都需要
  PRIVATE src        # 仅实现需要
  INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}/interface  # 仅使用者需要
)

6.3 避免全局设置

避免使用全局设置如 include_directories()link_directories(),而是使用目标特定的命令。

6.4 模块化设计

将大型项目分解为多个逻辑组件,每个组件有自己的 CMakeLists.txt:

复制代码
components/
├── core/
│   ├── CMakeLists.txt
│   ├── include/
│   └── src/
└── gui/
    ├── CMakeLists.txt
    ├── include/
    └── src/

6.5 工具链兼容性

编写可移植的 CMake 脚本:

cmake 复制代码
if(MSVC)
  target_compile_options(MyTarget PRIVATE /W4 /WX)
else()
  target_compile_options(MyTarget PRIVATE -Wall -Wextra -pedantic)
endif()

七、常见问题与解决方案

7.1 头文件找不到

问题:编译时报告头文件找不到。

解决方案

  1. 使用 target_include_directories() 明确指定包含路径
  2. 确保路径正确,使用绝对路径或相对于 CMAKE_CURRENT_SOURCE_DIR 的路径
  3. 检查拼写错误

7.2 库链接失败

问题:链接时报告未定义的引用。

解决方案

  1. 确保 target_link_libraries() 指定了所有需要的库
  2. 检查库文件路径是否正确
  3. 确保库的顺序正确(被依赖的库放在后面)

7.3 跨平台问题

问题:在 Windows 上工作正常,但在 Linux 上失败。

解决方案

  1. 使用 if(WIN32)if(UNIX) 等条件语句处理平台差异
  2. 避免使用平台特定的路径分隔符(总是使用 /
  3. 使用 CMAKE_CXX_COMPILER_ID 检查编译器

7.4 构建速度慢

问题:大型项目构建时间过长。

解决方案

  1. 使用 ccache 缓存编译结果
  2. 启用并行构建:make -j8cmake --build . --parallel 8
  3. 减少不必要的依赖和包含

7.5 调试 CMake

技巧

  1. 使用 message() 打印变量值
  2. 添加 --trace--trace-expand 选项查看详细执行过程
  3. 使用 cmake --graphviz=graph.dot 生成依赖图

八、实战项目示例

8.1 基础项目结构

让我们看一个完整的基础项目结构:

复制代码
MyApp/
├── CMakeLists.txt
├── build/
├── include/
│   └── MyApp/
│       ├── utils.h
│       └── config.h
├── src/
│   ├── CMakeLists.txt
│   ├── main.cpp
│   └── utils.cpp
└── tests/
    ├── CMakeLists.txt
    └── test_utils.cpp

顶层 CMakeLists.txt:

cmake 复制代码
cmake_minimum_required(VERSION 3.10)
project(MyApp VERSION 1.0.0 LANGUAGES CXX)

# 设置 C++ 标准
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# 添加子目录
add_subdirectory(src)
add_subdirectory(tests)

src/CMakeLists.txt:

cmake 复制代码
# 添加库
add_library(MyLib STATIC utils.cpp)
target_include_directories(MyLib PUBLIC ../include)

# 添加可执行文件
add_executable(MyApp main.cpp)
target_link_libraries(MyApp PRIVATE MyLib)

tests/CMakeLists.txt:

cmake 复制代码
# 启用测试
enable_testing()

# 添加测试可执行文件
add_executable(TestUtils test_utils.cpp)
target_link_libraries(TestUtils PRIVATE MyLib)

# 添加测试用例
add_test(NAME TestUtils COMMAND TestUtils)

8.2 使用外部依赖的项目

更复杂的项目可能依赖第三方库:

cmake 复制代码
cmake_minimum_required(VERSION 3.12)
project(AdvancedApp)

# 查找依赖
find_package(Boost 1.70 REQUIRED COMPONENTS system filesystem)
find_package(OpenCV REQUIRED)
find_package(Threads REQUIRED)

# 添加项目目标
add_executable(AdvancedApp main.cpp)
target_link_libraries(AdvancedApp PRIVATE 
    Boost::system 
    Boost::filesystem 
    OpenCV::OpenCV 
    Threads::Threads
)

# 安装规则
install(TARGETS AdvancedApp DESTINATION bin)
install(FILES config.xml DESTINATION etc/AdvancedApp)

九、总结与进阶学习资源

9.1 CMake 学习路径

  1. 初学者

    • 掌握基本命令:project(), add_executable(), target_link_libraries()
    • 理解变量和作用域
    • 学习简单的项目组织
  2. 中级开发者

    • 掌握现代 CMake 目标为中心的方法
    • 学习依赖管理和查找
    • 理解生成器表达式
  3. 高级开发者

    • 掌握交叉编译和工具链文件
    • 学习编写复杂的自定义命令和目标
    • 理解 CMake 内部机制和模块开发

9.2 推荐资源

  1. 官方文档

  2. 书籍

    • 《Professional CMake: A Practical Guide》
    • 《CMake Cookbook》
  3. 在线教程

  4. 开源项目参考

    • 学习知名开源项目(如 KDE、VTK)的 CMake 配置
    • 参考 GitHub 上的现代 CMake 模板项目

9.3 结语

CMake 是一个功能强大但学习曲线较陡的工具。掌握 CMake 不仅能提高你的构建系统技能,还能让你更好地理解软件项目的组织和管理。从简单的单文件项目开始,逐步尝试更复杂的场景,最终你将能够驾驭任何规模的 CMake 项目。

记住,好的 CMake 脚本应该是:

  • 模块化:易于维护和扩展
  • 可移植:能在不同平台和编译器上工作
  • 高效:最小化不必要的重新构建
  • 明确:清晰地表达项目的结构和依赖关系
相关推荐
Fanche40428 分钟前
MySQL 8 自动安装脚本(CentOS-7 系统)
linux·运维·数据库·mysql·centos
W_kiven1 小时前
Centos安装Dockers+Postgresql13+Postgis3.1
linux·运维·docker·postgresql·centos
liulilittle2 小时前
FTTR 全屋光纤架构分享
linux·服务器·网络·ip·通信·光纤·fttr
镰圈量化5 小时前
Django 实现服务器主动给客户端发送消息的几种常见方式及其区别
服务器·django·sqlite
SuperW7 小时前
Linux学习——UDP
linux·学习·udp
菜狗想要变强7 小时前
Linux驱动开发--异步通知与异步I/O
linux·运维·驱动开发
SuperW8 小时前
Linux学习——IO多路复用知识
linux·服务器·学习
搬码临时工8 小时前
路由器转发规则设置方法步骤,内网服务器端口怎么让异地连接访问的实现
服务器·网络·智能路由器·内网穿透·端口映射·外网访问
CopyLower8 小时前
Spring Boot的优点:赋能现代Java开发的利器
java·linux·spring boot