告别CMake配置噩梦:从入门到精通的现代CMake示例项目教程

告别CMake配置噩梦:从入门到精通的现代CMake示例项目教程

【免费下载链接】cmake-examples A collection of as simple as possible, modern CMake projects 【免费下载链接】cmake-examples 项目地址: https://gitcode.com/gh_mirrors/cma/cmake-examples

引言:你是否也曾陷入CMake的困境?

作为C/C++开发者,你是否经历过以下场景:花费数小时配置CMakeLists.txt却依然无法正确构建项目?在静态库、动态库和头文件库之间切换时感到困惑?不知道如何正确安装和导出自己的库供其他项目使用?如果你对这些问题的回答是肯定的,那么本文正是为你准备的。

通过阅读本文,你将获得:

  • 对现代CMake核心概念的清晰理解
  • 构建不同类型库(头文件库、静态库、动态库)的实战经验
  • 掌握CMake项目的安装和导出方法
  • 学会使用find_package命令轻松集成第三方库
  • 解决跨平台构建中的常见问题

CMake示例项目概述

cmake-examples项目是一个收集了尽可能简单的现代CMake项目的仓库,专注于展示如何正确配置、构建和安装C/C++库。项目结构清晰,包含多个示例,从简单的头文件库到复杂的依赖管理,覆盖了日常开发中常见的CMake使用场景。

项目结构概览

cmake-examples/
├── examples/
│   ├── core/                # 核心示例
│   │   ├── header-only/     # 头文件库示例
│   │   ├── static/          # 静态库示例
│   │   ├── shared/          # 动态库示例
│   │   └── shared-export/   # 带导出头文件的动态库示例
│   └── more/                # 更多高级示例
│       ├── components/      # 组件示例
│       ├── external-project-add/  # 外部项目添加示例
│       ├── fetch-content/   # FetchContent示例
│       └── ...
├── installing/              # 安装相关文档
└── README.md                # 项目总览

核心示例介绍

核心示例包含四个独立的文件夹,每个文件夹都包含一个库和一个使用该库的应用程序:

  1. header-only: 头文件库示例,最简单的库类型
  2. static: 静态库示例
  3. shared: 动态库示例
  4. shared-export: 使用generate_export_header命令的动态库示例

推荐按照上述顺序学习这些示例,因为头文件库是最简单的,且包含了最详细的注释。每个示例的CMakeLists.txt文件都包含了大量注释,解释每个命令的作用和用途。

快速上手:构建和运行示例

基本构建流程

所有示例都遵循相似的构建流程,以下是基本步骤:

  1. 配置项目:生成构建文件
  2. 构建项目:编译源代码
  3. 安装库(可选):将库安装到系统或自定义位置
  4. 运行应用程序:测试构建结果

构建头文件库示例

以header-only示例为例,展示完整的构建流程:

# 进入头文件库目录
cd examples/core/header-only/library

# 配置项目,指定安装目录
cmake -S . -B build -DCMAKE_INSTALL_PREFIX=install

# 构建并安装库
cmake --build build --target install

# 进入应用程序目录
cd ../application

# 配置应用程序,指定库的安装位置
cmake -S . -B build -DCMAKE_PREFIX_PATH=$(pwd)/../library/install

# 构建应用程序
cmake --build build

# 运行应用程序
./build/calculator-app

Windows系统注意事项

在Windows系统上,命令略有不同:

:: 配置应用程序
cmake -S . -B build -DCMAKE_PREFIX_PATH=%cd%/../library/install

:: 运行应用程序
.\build\Debug\calculator-app.exe

注意:在Windows上安装到默认位置(如C:\Program Files\<lib>)时,需要以管理员身份运行命令提示符,否则可能会遇到权限错误。

CMake核心概念解析

现代CMake的核心思想

现代CMake(3.0+)引入了许多新特性,主要思想包括:

  • 基于目标(Target)的设计:一切围绕目标进行,目标之间通过依赖关系连接
  • 接口(Interface)与构建(Build)分离:清晰区分编译时和安装时的设置
  • 生成器表达式(Generator Expressions):提供条件化配置的强大能力
  • 导入(Imported)和导出(Exported)目标:简化库的安装和使用

目标(Target)详解

在现代CMake中,目标是构建系统的基本单元,可以是可执行文件、库或自定义目标。目标具有属性,可以通过各种命令设置和修改。

常用目标类型
目标类型命令描述
可执行文件add_executable创建可执行文件目标
静态库add_library(STATIC)创建静态库目标
动态库add_library(SHARED)创建动态库目标
模块库add_library(MODULE)创建模块库目标(可动态加载)
接口库add_library(INTERFACE)创建接口库目标(仅头文件)
对象库add_library(OBJECT)创建对象库目标(用于组合目标文件)
目标属性设置

常用的目标属性设置命令:

# 设置包含目录
target_include_directories(target PRIVATE|PUBLIC|INTERFACE dirs...)

# 设置链接库
target_link_libraries(target PRIVATE|PUBLIC|INTERFACE libraries...)

# 设置编译特性
target_compile_features(target PRIVATE|PUBLIC|INTERFACE features...)

# 设置编译选项
target_compile_options(target PRIVATE|PUBLIC|INTERFACE options...)

# 设置编译定义
target_compile_definitions(target PRIVATE|PUBLIC|INTERFACE definitions...)

PRIVATE/PUBLIC/INTERFACE的区别

  • PRIVATE:仅影响当前目标
  • PUBLIC:影响当前目标及其依赖者
  • INTERFACE:仅影响依赖者

生成器表达式(Generator Expressions)

生成器表达式是CMake 3.0引入的强大特性,允许在生成构建系统时进行条件化处理。

常用生成器表达式
表达式描述
$CONFIG:Debug仅在Debug配置时为真
$CONFIG:Release仅在Release配置时为真
$<BUILD_INTERFACE:dir>仅在构建时使用的目录
$<INSTALL_INTERFACE:dir>仅在安装时使用的目录
$<TARGET_FILE:target>目标文件的路径
$<TARGET_NAME:target>目标的名称
$<0:...>始终为假
$<1:...>始终为真
使用示例
target_include_directories(
    mylib
    PUBLIC
        $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
        $<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>
)

这个例子设置了库的包含目录,在构建时使用源代码目录下的include文件夹,而在安装时使用系统的包含目录。

构建不同类型的库

头文件库(Header-Only Library)

头文件库是最简单的库类型,所有代码都在头文件中实现,不需要编译。

头文件库的CMakeLists.txt
cmake_minimum_required(VERSION 3.14)
project(calculator LANGUAGES CXX)

# 创建接口库(头文件库)
add_library(calculator INTERFACE)
add_library(calculator::calculator ALIAS calculator)

# 设置C++标准
target_compile_features(calculator INTERFACE cxx_std_11)

# 设置包含目录
target_include_directories(
    calculator
    INTERFACE
        $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
        $<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>
)

# 安装头文件
install(
    DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/include/calculator
    DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/calculator
)

# 安装目标和导出
install(
    TARGETS calculator
    EXPORT calculator-config
    INCLUDES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}
)

install(
    EXPORT calculator-config
    NAMESPACE calculator::
    DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/calculator
)
关键命令解析
  1. add_library(calculator INTERFACE): 创建一个接口库目标,用于头文件库
  2. add_library(calculator::calculator ALIAS calculator): 创建一个命名空间别名,便于其他项目引用
  3. target_compile_features: 指定库所需的C++标准特性
  4. target_include_directories: 设置包含目录,使用生成器表达式区分构建和安装时的路径
  5. install(DIRECTORY ...): 安装头文件到系统包含目录
  6. install(TARGETS ...): 安装目标并导出配置
  7. install(EXPORT ...): 安装导出配置,使其他项目可以通过find_package找到该库

静态库(Static Library)

静态库是编译后的目标文件的归档,在链接时被完整地复制到可执行文件中。

静态库的CMakeLists.txt关键部分
# 创建静态库
add_library(calculator-static STATIC)
add_library(calculator::static ALIAS calculator-static)

# 添加源文件
target_sources(
    calculator-static
    PRIVATE
        src/calculator.cpp
)

# 设置包含目录
target_include_directories(
    calculator-static
    PUBLIC
        $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
        $<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>
)

# 设置编译特性
target_compile_features(calculator-static PUBLIC cxx_std_11)

# 安装库文件
install(
    TARGETS calculator-static
    EXPORT calculator-static-config
    ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
    LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
    RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
    INCLUDES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}
)
静态库与头文件库的主要区别
  1. 使用STATIC关键字创建静态库目标
  2. 需要指定源文件(.cpp文件)
  3. 安装时不仅安装头文件,还要安装库文件(.a或.lib)

动态库(Shared Library)

动态库在运行时被加载,多个程序可以共享同一个库文件,节省内存和磁盘空间。

动态库的CMakeLists.txt关键部分
# 创建动态库
add_library(calculator-shared SHARED)
add_library(calculator::shared ALIAS calculator-shared)

# 添加源文件
target_sources(
    calculator-shared
    PRIVATE
        src/calculator.cpp
)

# 设置包含目录
target_include_directories(
    calculator-shared
    PUBLIC
        $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
        $<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>
)

# 设置编译特性
target_compile_features(calculator-shared PUBLIC cxx_std_11)

# 设置动态库版本
set_target_properties(
    calculator-shared
    PROPERTIES
        VERSION ${PROJECT_VERSION}
        SOVERSION ${PROJECT_VERSION_MAJOR}
        DEBUG_POSTFIX d
)

# 安装库文件
install(
    TARGETS calculator-shared
    EXPORT calculator-shared-config
    ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
    LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
    RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
    INCLUDES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}
)
动态库特有的设置
  1. 使用SHARED关键字创建动态库目标
  2. 设置版本信息:VERSION和SOVERSION属性
  3. 设置DEBUG_POSTFIX属性,为调试版本添加后缀(通常是"d")

使用generate_export_header的动态库

在Windows上创建动态库时,需要使用特定的宏来标记导出和导入的函数。CMake提供了generate_export_header命令来自动生成这些宏。

带导出头文件的动态库CMakeLists.txt关键部分
# 包含GenerateExportHeader模块
include(GenerateExportHeader)

# 创建动态库
add_library(calculator-shared-export SHARED)
add_library(calculator::shared-export ALIAS calculator-shared-export)

# 生成导出头文件
generate_export_header(
    calculator-shared-export
    BASE_NAME calculator
    EXPORT_MACRO_NAME CALCULATOR_EXPORT
    EXPORT_FILE_NAME include/calculator-shared-export/calculator_export.h
    STATIC_DEFINE CALCULATOR_STATIC
)

# 添加源文件
target_sources(
    calculator-shared-export
    PRIVATE
        src/calculator.cpp
        ${CMAKE_CURRENT_BINARY_DIR}/include/calculator-shared-export/calculator_export.h
)

# 设置包含目录,添加二进制目录以包含生成的导出头文件
target_include_directories(
    calculator-shared-export
    PUBLIC
        $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
        $<BUILD_INTERFACE:${CMAKE_CURRENT_BINARY_DIR}/include>
        $<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>
)
关键命令解析
  1. include(GenerateExportHeader): 包含生成导出头文件的模块
  2. generate_export_header: 生成导出头文件,包含导出和导入宏
  3. 添加生成的头文件到target_sources: 确保生成的头文件被正确处理
  4. 添加二进制目录到包含目录: 使编译器能够找到生成的导出头文件

安装和使用库

安装库到系统目录

安装库到系统目录可以让系统上的所有用户和项目都能使用该库。默认的安装路径如下:

操作系统安装前缀包含目录库目录
Linux/usr/local/usr/local/include/usr/local/lib
macOS/usr/local/usr/local/include/usr/local/lib
WindowsC:\Program Files<project>C:\Program Files<project>\includeC:\Program Files<project>\lib
安装命令
# 在库的构建目录中
cmake --build build --target install

注意:在Linux和macOS上安装到默认系统目录可能需要管理员权限(使用sudo),在Windows上可能需要以管理员身份运行命令提示符。

安装到自定义位置

在开发过程中,将库安装到自定义位置通常更方便,可以避免干扰系统库。

# 配置时指定安装前缀
cmake -S . -B build -DCMAKE_INSTALL_PREFIX=path/to/custom/install

# 构建并安装
cmake --build build --target install

使用已安装的库

使用find_package命令可以轻松地在另一个项目中使用已安装的库。

应用程序的CMakeLists.txt
cmake_minimum_required(VERSION 3.14)
project(calculator-app LANGUAGES CXX)

# 查找已安装的库
find_package(calculator CONFIG REQUIRED)

# 创建可执行文件
add_executable(calculator-app)

# 添加源文件
target_sources(
    calculator-app
    PRIVATE
        main.cpp
)

# 设置C++标准
target_compile_features(calculator-app PRIVATE cxx_std_11)

# 链接库
target_link_libraries(
    calculator-app
    PRIVATE
        calculator::calculator  # 使用命名空间别名
)
配置时指定自定义安装位置

如果库安装在非标准位置,需要在配置时指定CMAKE_PREFIX_PATH:

# 在应用程序目录中
cmake -S . -B build -DCMAKE_PREFIX_PATH=path/to/custom/install

高级示例:组件和依赖管理

组件示例

components示例展示了如何创建包含多个组件的库,允许用户只链接他们需要的组件。

组件库的CMakeLists.txt关键部分
# 创建一个接口库作为主库
add_library(phrases INTERFACE)
add_library(phrases::phrases ALIAS phrases)

# 创建各个组件
add_library(phrases-hello)
add_library(phrases::hello ALIAS phrases-hello)

add_library(phrases-goodbye)
add_library(phrases::goodbye ALIAS phrases-goodbye)

add_library(phrases-hey)
add_library(phrases::hey ALIAS phrases-hey)

# 将组件链接到主库
target_link_libraries(
    phrases
    INTERFACE
        phrases::hello
        phrases::goodbye
        phrases::hey
)

# 导出配置,包含组件信息
install(
    EXPORT phrases-config
    NAMESPACE phrases::
    DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/phrases
    COMPONENT phrases
)
使用组件库
# 查找库,可以指定需要的组件
find_package(phrases CONFIG REQUIRED COMPONENTS hello goodbye)

# 链接特定组件
target_link_libraries(
    myapp
    PRIVATE
        phrases::hello
        phrases::goodbye
)

ExternalProject_Add示例

external-project-add示例展示了如何使用ExternalProject_Add模块来管理外部依赖项。

include(ExternalProject)

# 添加外部项目
ExternalProject_Add(
    external_catch2
    PREFIX ${CMAKE_CURRENT_BINARY_DIR}/external/catch2
    GIT_REPOSITORY https://github.com/catchorg/Catch2.git
    GIT_TAG v2.13.6
    CMAKE_ARGS
        -DCMAKE_INSTALL_PREFIX=${CMAKE_CURRENT_BINARY_DIR}/external/catch2/install
        -DCMAKE_CXX_COMPILER=${CMAKE_CXX_COMPILER}
    UPDATE_COMMAND ""
    TEST_COMMAND ""
)

# 导入外部项目的目标
ExternalProject_Get_Property(external_catch2 install_dir)
set(CATCH2_INSTALL_DIR ${install_dir})

# 加载外部项目的配置
find_package(Catch2 CONFIG PATHS ${CATCH2_INSTALL_DIR}/lib/cmake/Catch2)

FetchContent示例

FetchContent是CMake 3.11引入的模块,提供了另一种管理外部依赖的方式,在配置阶段获取依赖项。

include(FetchContent)

# 声明要获取的内容
FetchContent_Declare(
    catch2
    GIT_REPOSITORY https://github.com/catchorg/Catch2.git
    GIT_TAG v2.13.6
)

# 获取内容并使其可用
FetchContent_MakeAvailable(catch2)

# 现在可以直接链接Catch2的目标
target_link_libraries(
    mytest
    PRIVATE
        Catch2::Catch2
)

CMake实用技巧和最佳实践

减少cd操作

使用-S-B选项可以在不改变当前工作目录的情况下指定源目录和构建目录:

# 从任何位置配置项目
cmake -S /path/to/source -B /path/to/build

# 构建项目
cmake --build /path/to/build

生成compile_commands.json

compile_commands.json文件包含了编译器命令信息,对许多开发工具(如Clang-Tidy、cppcheck等)非常有用:

cmake -S . -B build -DCMAKE_EXPORT_COMPILE_COMMANDS=ON

注意:CMAKE_EXPORT_COMPILE_COMMANDS仅支持Make和Ninja生成器。

设置编译选项的现代方法

现代CMake推荐使用target_compile_options和生成器表达式来设置编译选项,以确保选项只应用于特定目标和配置:

target_compile_options(
    mytarget
    PRIVATE
        $<$<CXX_COMPILER_ID:MSVC>:/W4 /WX>
        $<$<NOT:$<CXX_COMPILER_ID:MSVC>>:-Wall -Wextra -Wpedantic -Werror>
)

项目结构最佳实践

推荐的项目结构如下:

library-name/
├── include/
│   └── library-name/
│       └── header.h
├── src/
│   └── source.cpp
├── tests/
│   └── test.cpp
└── CMakeLists.txt

这种结构确保了包含路径的一致性,无论是使用find_package还是FetchContent:

#include "library-name/header.h"

常见问题解答

Q: 如何在CMake中设置C++标准?

A: 推荐使用target_compile_features命令来指定所需的C++标准特性:

target_compile_features(mytarget PRIVATE cxx_std_17)

或者,对于需要支持旧版本CMake的项目,可以使用CMAKE_CXX_STANDARD变量:

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)

Q: 如何处理跨平台路径问题?

A: CMake提供了几个变量和函数来处理跨平台路径问题:

  • 使用${CMAKE_INSTALL_INCLUDEDIR}${CMAKE_INSTALL_LIBDIR}等变量代替硬编码路径
  • 使用file(TO_CMAKE_PATH ...)函数转换路径
  • 使用target_sources代替直接操作源文件列表

Q: 如何在CMake中添加编译定义?

A: 使用target_compile_definitions命令:

target_compile_definitions(
    mytarget
    PRIVATE
        DEBUG_MODE
        VERSION="${PROJECT_VERSION}"
    PUBLIC
        $<$<PLATFORM_ID:Windows>:WINDOWS_BUILD>
)

也可以在配置时通过命令行添加:

cmake -S . -B build -DMY_DEFINE=1

然后在CMakeLists.txt中使用:

target_compile_definitions(
    mytarget
    PRIVATE
        $<$<BOOL:${MY_DEFINE}>:MY_DEFINE>
)

Q: 如何查看CMake生成的详细信息?

A: 可以使用--trace选项来获取CMake执行的详细跟踪信息:

cmake --trace -S . -B build

或者使用--debug-output选项获取调试输出:

cmake --debug-output -S . -B build

总结与展望

通过本文的学习,你应该已经掌握了现代CMake的核心概念和使用方法,包括:

  • 构建不同类型的库(头文件库、静态库、动态库)
  • 使用生成器表达式处理条件化配置
  • 安装和导出库,使其他项目可以轻松使用
  • 管理外部依赖项
  • 遵循CMake最佳实践

CMake是一个强大而复杂的构建系统,本文只是触及了表面。要成为CMake专家,还需要不断实践和探索。建议参考以下资源继续学习:

  1. CMake官方文档
  2. Modern CMake教程
  3. Professional CMake: A Practical Guide

希望本文能够帮助你摆脱CMake配置的困扰,让构建过程变得更加高效和愉快!

参考资料

  1. CMake官方文档: https://cmake.org/cmake/help/latest/
  2. CLIUtils现代CMake介绍: https://cliutils.gitlab.io/modern-cmake/
  3. It's Time To Do CMake Right: https://pabloariasal.github.io/2018/02/19/its-time-to-do-cmake-right/
  4. Effective Modern CMake: https://gist.github.com/mbinna/c61dbb39bca0e4fb7d1f73b0d66a4fd1
  5. CMake FetchContent vs. ExternalProject: https://www.scivision.dev/cmake-fetchcontent-vs-external-project/

【免费下载链接】cmake-examples A collection of as simple as possible, modern CMake projects 【免费下载链接】cmake-examples 项目地址: https://gitcode.com/gh_mirrors/cma/cmake-examples

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值