CMake构建系统入门

CMake是现代C/C++项目构建的事实标准。从KDE4到LLVM,从OpenCV到TensorFlow,无数知名项目都在使用CMake管理构建流程。本文将从基础概念到高级技巧,全面解析CMake构建系统。

一、CMake概述

1.1 什么是CMake

CMake(Cross-platform Make)是一个开源的跨平台构建系统生成器。它不直接构建项目,而是生成平台特定的构建文件:

平台 生成的构建文件
Linux/Unix Makefile
Windows Visual Studio项目文件(.sln)
macOS Xcode项目
跨平台 Ninja文件

1.2 为什么需要CMake

传统makefile的痛点

  • 编写复杂,容易出错
  • 平台相关性,移植困难
  • 依赖管理麻烦
  • 大型项目维护成本高

CMake的优势

  1. 跨平台:一份配置,多平台构建
  2. 开源自软件:BSD许可证
  3. 高效:比autotools快40%(KDE官方数据)
  4. 可扩展:支持自定义模块
  5. 简化流程:cmake + make两步构建

1.3 CMake构建流程

CMakeLists.txt → cmake → 平台构建文件 → make/编译器 → 可执行文件

典型工作流

# 1. 编写CMakeLists.txt

# 2. 生成构建文件
cmake .

# 3. 编译
make

# 4. 运行
./myapp

推荐的外部构建方式

mkdir build && cd build
cmake ..
make

二、CMake基础语法

2.1 语法特点

  • 变量引用:使用 ${} 取值,IF语句中直接使用变量名
  • 指令格式command(arg1 arg2 ...),参数用空格或分号分隔
  • 大小写:指令不区分大小写,变量区分大小写,推荐指令全大写
  • 注释:使用 #

2.2 第一个CMakeLists.txt

# 指定CMake最低版本
cmake_minimum_required(VERSION 3.10)

# 定义项目名称和语言
project(HelloWorld C CXX)

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

2.3 常用变量

CMake预定义了许多变量,掌握它们是高效使用CMake的关键:

路径相关变量

变量 说明
PROJECT_SOURCE_DIR 项目根目录
PROJECT_BINARY_DIR 构建输出目录
CMAKE_CURRENT_SOURCE_DIR 当前CMakeLists.txt所在目录
CMAKE_SOURCE_DIR 顶层CMakeLists.txt所在目录
CMAKE_BINARY_DIR 构建根目录

输出目录变量

# 设置可执行文件输出位置
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR}/bin)

# 设置动态库输出位置
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR}/lib)

# 设置静态库输出位置
set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR}/lib)

编译相关变量

# C++标准
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# 编译类型
set(CMAKE_BUILD_TYPE Release)  # Debug, Release, RelWithDebInfo

# 编译选项
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -O2")

三、常用指令详解

3.1 project() - 定义项目

# 基本形式
project(MyProject)

# 指定版本
project(MyProject VERSION 1.0.0)

# 指定语言
project(MyProject C CXX)

# 指定描述和主页
project(MyProject
    VERSION 1.0.0
    DESCRIPTION "My awesome project"
    LANGUAGES CXX
)

3.2 add_executable() - 添加可执行目标

# 基本形式
add_executable(myapp main.cpp)

# 多文件
add_executable(myapp 
    main.cpp
    utils.cpp
    network.cpp
)

# 使用变量
set(SOURCES main.cpp utils.cpp)
add_executable(myapp ${SOURCES})

3.3 add_library() - 添加库目标

# 静态库
add_library(mylib STATIC lib.cpp)

# 动态库
add_library(mylib SHARED lib.cpp)

# 默认(根据BUILD_SHARED_LIBS变量决定)
add_library(mylib lib.cpp)

3.4 target_link_libraries() - 链接库

add_executable(myapp main.cpp)
target_link_libraries(myapp 
    PRIVATE mylib      # 仅当前目标使用
    PUBLIC sharedlib   # 当前目标和依赖者都使用
    INTERFACE headeronly  # 仅依赖者使用
)

可见性说明

  • PRIVATE:仅当前目标使用
  • PUBLIC:当前目标和依赖者都使用
  • INTERFACE:仅依赖者使用

3.5 include_directories() - 添加头文件目录

# 全局添加(不推荐)
include_directories(${PROJECT_SOURCE_DIR}/include)

# 针对目标添加(推荐)
target_include_directories(myapp 
    PRIVATE 
        ${PROJECT_SOURCE_DIR}/include
        ${PROJECT_SOURCE_DIR}/src
)

3.6 link_directories() - 添加库搜索路径

# 全局添加(不推荐,需在add_executable之前)
link_directories(${PROJECT_SOURCE_DIR}/lib)

# 针对目标添加(推荐)
target_link_directories(myapp PRIVATE ${PROJECT_SOURCE_DIR}/lib)

3.7 set() - 设置变量

# 设置变量
set(MY_VAR "hello")

# 设置列表
set(SOURCES 
    main.cpp
    utils.cpp
    network.cpp
)

# 设置缓存变量(可被cmake命令行修改)
set(MY_OPTION ON CACHE BOOL "Enable my option")

# 设置环境变量
set(ENV{PATH} "/usr/local/bin:$ENV{PATH}")

四、文件操作与源文件管理

4.1 file()命令

# 递归收集所有.cpp文件
file(GLOB_RECURSE CPP_SOURCES 
    ${PROJECT_SOURCE_DIR}/src/*.cpp
)

# 收集当前目录的.cpp文件(不递归)
file(GLOB CPP_SOURCES *.cpp)

# 复制文件
file(COPY ${PROJECT_SOURCE_DIR}/config.txt 
     DESTINATION ${PROJECT_BINARY_DIR})

# 创建目录
file(MAKE_DIRECTORY ${PROJECT_BINARY_DIR}/output)

4.2 aux_source_directory()

# 收集指定目录下的源文件(不递归)
aux_source_directory(${PROJECT_SOURCE_DIR}/src SOURCES)

add_executable(myapp ${SOURCES})

注意aux_source_directory只收集当前目录,不会递归子目录。

4.3 list()操作

# 定义列表
set(MY_LIST a b c d)

# 追加元素
list(APPEND MY_LIST e f)

# 删除元素
list(REMOVE_ITEM MY_LIST c)

# 获取长度
list(LENGTH MY_LIST LEN)

# 访问元素
list(GET MY_LIST 0 FIRST_ITEM)

# 排序
list(SORT MY_LIST)

# 示例:从源文件列表中删除特定文件
file(GLOB_RECURSE CPPS ${PROJECT_SOURCE_DIR}/src/*.cpp)
list(REMOVE_ITEM CPPS 
    ${PROJECT_SOURCE_DIR}/src/main.cpp
)

五、条件判断与循环

5.1 if()条件判断

# 基本形式
if(DEFINED MY_VAR)
    message(STATUS "MY_VAR is defined")
endif()

# 比较操作
if(MY_VAR EQUAL 10)
    # ...
elseif(MY_VAR LESS 5)
    # ...
else()
    # ...
endif()

# 逻辑操作
if(WIN32 AND NOT CYGWIN)
    # Windows原生环境
endif()

# 字符串比较
if(CMAKE_BUILD_TYPE STREQUAL "Debug")
    # 调试模式
endif()

# 检查变量是否定义
if(DEFINED MY_VAR)
    # 已定义
endif()

# 检查变量真假
if(MY_VAR)
    # 非空、非0、非FALSE
endif()

5.2 循环

# foreach循环
foreach(FILE ${SOURCES})
    message(STATUS "Processing ${FILE}")
endforeach()

# 带范围的循环
foreach(i RANGE 1 10)
    message(STATUS "i = ${i}")
endforeach()

# while循环
set(COUNT 0)
while(COUNT LESS 5)
    message(STATUS "Count: ${COUNT}")
    math(EXPR COUNT "${COUNT} + 1")
endwhile()

六、函数与宏

6.1 function()函数

# 定义函数
function(add_my_executable TARGET_NAME)
    # ARGN包含所有额外参数
    add_executable(${TARGET_NAME} ${ARGN})
    target_link_libraries(${TARGET_NAME} PRIVATE mylib)
    message(STATUS "Created target: ${TARGET_NAME}")
endfunction()

# 调用
add_my_executable(myapp main.cpp utils.cpp)

6.2 macro()宏

# 定义宏
macro(print_info VAR)
    message(STATUS "${VAR} = ${${VAR}}")
endmacro()

# 调用
set(MY_VAR "hello")
print_info(MY_VAR)

函数与宏的区别

  • 函数有自己的作用域,宏在调用者作用域执行
  • 函数内修改变量不影响外部,宏会影响
  • 推荐使用函数,作用域更清晰

七、依赖管理

7.1 find_package()查找依赖

# 查找必需依赖
find_package(OpenCV REQUIRED)

# 查找可选依赖
find_package(Boost COMPONENTS filesystem system)

# 使用依赖
if(OpenCV_FOUND)
    target_include_directories(myapp PRIVATE ${OpenCV_INCLUDE_DIRS})
    target_link_libraries(myapp PRIVATE ${OpenCV_LIBS})
endif()

# 指定查找路径
set(OpenCV_DIR "/path/to/opencv")
find_package(OpenCV REQUIRED)

常见变量

  • <Package>_FOUND:是否找到
  • <Package>_INCLUDE_DIRS:头文件目录
  • <Package>_LIBRARIES:库文件
  • <Package>_VERSION:版本号

7.2 FetchContent下载依赖

CMake 3.11+提供了现代化的依赖管理方式:

include(FetchContent)

# 声明依赖
FetchContent_Declare(
    googletest
    GIT_REPOSITORY https://github.com/google/googletest.git
    GIT_TAG release-1.12.1
)

# 获取依赖
FetchContent_MakeAvailable(googletest)

# 使用依赖
enable_testing()
add_executable(mytest test.cpp)
target_link_libraries(mytest PRIVATE GTest::gtest_main)

7.3 add_subdirectory()添加子项目

# 添加子目录
add_subdirectory(lib/mylib)

# 指定二进制输出目录
add_subdirectory(lib/mylib ${PROJECT_BINARY_DIR}/mylib_build)

# 子项目的CMakeLists.txt
add_library(mylib STATIC lib.cpp)
target_include_directories(mylib PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include)

# 主项目使用
target_link_libraries(myapp PRIVATE mylib)

7.4 include()包含其他CMake文件

# 包含自定义CMake模块
include(${PROJECT_SOURCE_DIR}/cmake/Utils.cmake)

# 包含CMake自带的模块
include(CMakePrintHelpers)
cmake_print_variables(CMAKE_CXX_COMPILER)

八、编译选项与定义

8.1 target_compile_definitions()

# 添加编译宏定义
target_compile_definitions(myapp
    PRIVATE
        DEBUG_MODE=1
        PROJECT_NAME="MyApp"
    PUBLIC
        API_EXPORTS
)

# 等价于编译选项 -DDEBUG_MODE=1 -DPROJECT_NAME="MyApp"

8.2 target_compile_options()

# 添加编译选项
target_compile_options(myapp
    PRIVATE
        -Wall
        -Wextra
        $<$<CONFIG:Debug>:-g -O0>    # 仅Debug模式
        $<$<CONFIG:Release>:-O3>     # 仅Release模式
)

8.3 target_compile_features()

# 要求特定C++特性
target_compile_features(myapp PRIVATE cxx_std_17)

九、安装与打包

9.1 install()安装规则

# 安装可执行文件
install(TARGETS myapp
    RUNTIME DESTINATION bin
)

# 安装库文件
install(TARGETS mylib
    LIBRARY DESTINATION lib
    ARCHIVE DESTINATION lib
)

# 安装头文件
install(DIRECTORY include/
    DESTINATION include
    FILES_MATCHING PATTERN "*.h"
)

# 安装配置文件
install(FILES config/settings.json
    DESTINATION etc/myapp
)

9.2 CPack打包

# 包含CPack模块
include(CPack)

# 设置包信息
set(CPACK_PACKAGE_NAME "MyApp")
set(CPACK_PACKAGE_VERSION "1.0.0")
set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "My awesome application")
set(CPACK_PACKAGE_VENDOR "MyCompany")

# 设置生成器
set(CPACK_GENERATOR "DEB;RPM;TGZ")

# DEB包特定设置
set(CPACK_DEBIAN_PACKAGE_DEPENDS "libc6 (>= 2.17)")

# 生成包
# cpack

十、实战示例

10.1 多目录项目结构

MyProject/
├── CMakeLists.txt
├── include/
│   └── myapp.h
├── src/
│   ├── main.cpp
│   ├── utils.cpp
│   └── network.cpp
├── lib/
│   └── thirdparty/
│       └── CMakeLists.txt
└── tests/
    └── CMakeLists.txt

根CMakeLists.txt

cmake_minimum_required(VERSION 3.10)
project(MyProject VERSION 1.0.0 LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}/bin)

# 收集源文件
file(GLOB_RECURSE SOURCES ${PROJECT_SOURCE_DIR}/src/*.cpp)

# 创建可执行文件
add_executable(myapp ${SOURCES})

target_include_directories(myapp 
    PRIVATE 
        ${PROJECT_SOURCE_DIR}/include
        ${PROJECT_SOURCE_DIR}/src
)

# 添加第三方库
add_subdirectory(lib/thirdparty)
target_link_libraries(myapp PRIVATE thirdparty)

# 启用测试
enable_testing()
add_subdirectory(tests)

10.2 条件编译

# 添加选项
option(ENABLE_CUDA "Enable CUDA support" OFF)
option(BUILD_TESTS "Build test programs" ON)

if(ENABLE_CUDA)
    find_package(CUDA REQUIRED)
    add_definitions(-DUSE_CUDA)
    target_link_libraries(myapp PRIVATE ${CUDA_LIBRARIES})
endif()

if(BUILD_TESTS)
    add_subdirectory(tests)
endif()

10.3 自定义CMake模块

cmake/FindMyLib.cmake

find_path(MYLIB_INCLUDE_DIR mylib.h
    PATHS /usr/local/include /usr/include
)

find_library(MYLIB_LIBRARY NAMES mylib
    PATHS /usr/local/lib /usr/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()

mark_as_advanced(MYLIB_INCLUDE_DIR MYLIB_LIBRARY)

使用

list(APPEND CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/cmake)
find_package(MyLib REQUIRED)
target_link_libraries(myapp PRIVATE ${MYLIB_LIBRARIES})

十一、最佳实践

1. 使用现代CMake(目标导向)

# 旧方式(不推荐)
include_directories(/path/to/include)
link_libraries(mylib)

# 新方式(推荐)
target_include_directories(myapp PRIVATE /path/to/include)
target_link_libraries(myapp PRIVATE mylib)

2. 外部构建

mkdir build && cd build
cmake ..
make

3. 使用target_*系列命令

优先使用 target_* 命令而非全局命令,作用域更清晰。

4. 合理组织项目结构

project/
├── CMakeLists.txt
├── cmake/          # 自定义模块
├── include/        # 公共头文件
├── src/            # 源代码
├── tests/          # 测试
└── docs/           # 文档

5. 使用变量简化配置

set(INCLUDE_DIR ${PROJECT_SOURCE_DIR}/include)
set(SRC_DIR ${PROJECT_SOURCE_DIR}/src)
Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐