CMake 完全指南:第三章 - CMakeLists.txt 详解 - 项目的蓝图

326 阅读5分钟

一、 项目的心脏:project() 命令详解

在第二章,我们使用了最简单的project(HelloCMake LANGUAGES CXX)。现在,让我们解锁它的全部潜力。

  1. 基础语法与核心作用:

    project(<PROJECT-NAME>
            [VERSION <min>[.<patch>[.<tweak>[.<suffix>]]]
            [DESCRIPTION <project-description-string>]
            [HOMEPAGE_URL <url-string>]
            [LANGUAGES <language-name>...])
    
    • <PROJECT-NAME> (必填):  给你的项目起个名字!这个名字会设置一系列重要的内置变量(稍后详解)。

    • VERSION (可选):  为项目指定版本号(例如 VERSION 1.0.0)。CMake 3.12+ 支持。设置后,会定义相关版本变量。

    • DESCRIPTION (可选):  提供项目的简短描述(例如 DESCRIPTION "My Awesome Application")。CMake 3.9+ 支持。

    • HOMEPAGE_URL (可选):  设置项目的主页URL(例如 HOMEPAGE_URL "https://github.com/me/myproject")。CMake 3.12+ 支持。

    • LANGUAGES (可选但强烈推荐):  明确项目使用的编程语言。常用值:

      • C: C语言项目
      • CXX: C++语言项目 (最常见)
      • CUDA: CUDA项目
      • FortranASM等。可以指定多个,如 LANGUAGES C CXX如果不指定,CMake默认会启用 C 和 CXX  显式指定更清晰。
  2. project() 设置的关键内置变量:
    执行 project() 命令后,CMake 会自动定义一组非常有用的变量,这些变量在整个 CMakeLists.txt 及其子目录中都可用(作用域后续讨论):

    • PROJECT_NAME:  项目的名称(即你传入的 <PROJECT-NAME>)。
    • <PROJECT-NAME>_SOURCE_DIR / PROJECT_SOURCE_DIR:  项目源代码的根目录。  即包含顶层 CMakeLists.txt 的目录。这是最常用的变量之一。例如:${PROJECT_SOURCE_DIR}/include
    • <PROJECT-NAME>_BINARY_DIR / PROJECT_BINARY_DIR:  项目构建的根目录。  即你运行 cmake 命令时所在的目录(通常是 build 目录)。这是最常用的变量之一。
    • PROJECT_VERSION<PROJECT-NAME>_VERSION:  项目的完整版本号(如果指定了 VERSION)。
    • PROJECT_VERSION_MAJOR<PROJECT-NAME>_VERSION_MAJOR:  主版本号。
    • PROJECT_VERSION_MINOR<PROJECT-NAME>_VERSION_MINOR:  次版本号。
    • PROJECT_VERSION_PATCH<PROJECT-NAME>_VERSION_PATCH:  修订号。
    • PROJECT_VERSION_TWEAK<PROJECT-NAME>_VERSION_TWEAK:  微调号(较少用)。
    • PROJECT_DESCRIPTION<PROJECT-NAME>_DESCRIPTION:  项目描述(如果指定了 DESCRIPTION)。
    • PROJECT_HOMEPAGE_URL<PROJECT-NAME>_HOMEPAGE_URL:  项目主页URL(如果指定了 HOMEPAGE_URL)。
    • CMAKE_PROJECT_NAME:  当前正在处理的最顶层项目的名称(在多项目配置中与 PROJECT_NAME 可能不同)。
    • 编译器相关变量 (检测后设置):  CMAKE_C_COMPILERCMAKE_CXX_COMPILERCMAKE_C_FLAGSCMAKE_CXX_FLAGS 等。这些通常在 project() 调用时或之后被检测和设置。
  3. 实践示例:

    cmake_minimum_required(VERSION 3.12) # 需要支持 VERSION/DESCRIPTION
    
    project(MySuperApp
        VERSION 2.1.3
        DESCRIPTION "A revolutionary application that does amazing things"
        HOMEPAGE_URL "https://github.com/awesomecoder/mysuperapp"
        LANGUAGES CXX
    )
    
    message(STATUS "Project Name: ${PROJECT_NAME}")
    message(STATUS "Project Version: ${PROJECT_VERSION}")
    message(STATUS "Project Description: ${PROJECT_DESCRIPTION}")
    message(STATUS "Source Dir: ${PROJECT_SOURCE_DIR}")
    message(STATUS "Build Dir: ${PROJECT_BINARY_DIR}")
    

    运行 cmake 时,你会看到这些变量的值被打印出来。这些变量在后续配置头文件、安装路径、打包信息等方面极其有用。

二、 构建模块:创建库 (add_library())

除了可执行文件 (add_executable),库是代码复用和项目组织的基石。add_library() 让你轻松创建各种类型的库。

  1. 基础语法与库类型:

    add_library(<name> [STATIC | SHARED | MODULE]
                [EXCLUDE_FROM_ALL]
                [<source>...])
    
    • <name> (必填):  你要创建的库的目标名称 (Target Name) 。这个名字将在后续的 target_link_libraries() 等命令中使用。建议保持唯一性!

    • 库类型 (可选,但强烈建议指定):

      • STATIC:  创建静态库 (Static Library) 。在Linux/macOS上通常生成 .a 文件,在Windows上生成 .lib 文件。特点:  代码在链接时被直接复制到最终的可执行文件或库中。运行时不再需要静态库文件。生成的文件较大,但发布简单(不需要附带库文件)。
      • SHARED:  创建动态库/共享库 (Shared Library / Dynamic Link Library - DLL) 。在Linux/macOS上生成 .so (Shared Object) 文件,在Windows上生成 .dll (及配套的 .lib 导入库)。特点:  代码被复制到最终程序。程序运行时动态加载共享库。生成的可执行文件较小,但发布时需要附带相应的共享库文件。支持运行时更新(热插拔)。
      • MODULE:  一种特殊的动态库,通常不作为其他目标的直接依赖,而是在运行时被动态加载(例如插件系统)。在Linux/macOS上类似 .so,Windows上类似 .dll。使用相对较少。
      • 不指定类型:  CMake会根据 BUILD_SHARED_LIBS 变量的值决定默认类型。如果 BUILD_SHARED_LIBS 为 ON,默认创建 SHARED 库;如果为 OFF (默认值),创建 STATIC 库。最佳实践:总是显式指定 STATIC 或 SHARED  避免歧义。
    • EXCLUDE_FROM_ALL (可选):  如果指定,该库不会被默认构建(即运行 cmake --build . 或 make 时不会构建)。需要显式指定目标构建(如 cmake --build . --target MyLib 或 make MyLib)。用于构建可选的或测试用的库。

    • <source>... (必填):  构建这个库所需的源文件列表(.cpp.c.cu 等)。可以列出多个文件,也可以使用变量或生成器表达式(后续介绍)。

  2. 创建库的示例:
    场景:  假设我们的项目 MySuperApp 包含一个数学工具库 MathUtils 和一个主程序。
    目录结构 (简化):

    MySuperApp/
    ├── CMakeLists.txt        # 顶层
    ├── app/
    │   ├── CMakeLists.txt    # 子目录 (可选,演示作用域)
    │   └── main.cpp
    └── math/
        ├── CMakeLists.txt    # 子目录 (可选)
        ├── math_utils.h
        └── math_utils.cpp
    

    顶层 CMakeLists.txt (创建静态库示例):

    cmake_minimum_required(VERSION 3.12)
    project(MySuperApp VERSION 1.0 LANGUAGES CXX)
    
    # 添加 math 子目录。CMake会进入 math/ 并处理其 CMakeLists.txt
    add_subdirectory(math)
    
    # 添加 app 子目录
    add_subdirectory(app)
    

    math/CMakeLists.txt (创建 MathUtils 静态库):

    # 在 math 目录下创建一个名为 MathUtils 的 STATIC 库
    # 源文件:当前目录下的 math_utils.cpp
    # 头文件 math_utils.h 不需要在这里列出(用于编译),但它的位置需要告知使用者(后续章节)
    add_library(MathUtils STATIC math_utils.cpp)
    
    # (可选) 设置库目标属性,例如包含路径(后续章节详解)
    # target_include_directories(MathUtils PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
    

    app/CMakeLists.txt (创建链接库的可执行文件):

    # 创建一个名为 MySuperApp 的可执行文件,源文件是 main.cpp
    add_executable(MySuperApp main.cpp)
    
    # 告诉 CMake:MySuperApp 可执行文件需要链接到 MathUtils 库
    # 这是初步写法,现代CMake更推荐使用 target_link_libraries 的 PUBLIC/PRIVATE/INTERFACE (第四章详解)
    target_link_libraries(MySuperApp PRIVATE MathUtils)
    

    构建过程:

    1. mkdir build && cd build

    2. cmake ..

    3. cmake --build .
      结果:

    • 在 build/math/ (或类似路径) 下生成 libMathUtils.a (Linux/macOS) 或 MathUtils.lib (Windows)。
    • 在 build/app/ 下生成 MySuperApp 可执行文件,该文件内部包含了 MathUtils 库的代码(因为是静态链接)。
  3. 静态库 vs 动态库的选择:

    • 使用静态库 (STATIC):

      • 优点:  最终程序独立发布,不依赖外部库文件;程序启动可能稍快(无加载开销)。
      • 缺点:  可执行文件体积大;如果多个程序使用同一个库,内存中会有多份库代码拷贝;库更新需要重新编译链接整个程序。
      • 适用场景:  小型工具、嵌入式系统、要求独立发布的程序、不希望用户管理依赖。
    • 使用动态库 (SHARED):

      • 优点:  可执行文件体积小;多个程序可共享内存中的同一份库代码,节省内存;库可以独立更新(需注意ABI兼容性)。
      • 缺点:  发布程序时需要附带相应的动态库文件(或确保目标系统已安装);运行时加载有轻微开销;存在“DLL Hell”(依赖冲突)风险(需管理好版本)。
      • 适用场景:  大型应用、公共库(如系统API)、插件系统、需要热更新的场景。

三、 CMake的神经:变量 (set()) 与作用域

变量是CMake脚本中存储信息、控制流程的核心机制。理解变量的设置作用域至关重要。

  1. 设置变量:set() 命令

    set(<variable> <value>... [CACHE <type> <docstring> [FORCE]] [PARENT_SCOPE])
    
    • 基本形式 (设置普通变量):

      set(MY_VARIABLE "Hello, CMake Variables!")
      set(SOURCE_FILES main.cpp utils.cpp helper.cpp)
      
      • <variable>: 变量名。CMake变量名区分大小写!  通常使用大写字母和下划线命名(如 MY_VARPROJECT_SOURCES)。
      • <value>...: 变量的值。可以是一个值,也可以是多个值组成的列表。如果包含空格,值需要用双引号括起来 ("value with space")。多个值会组成一个分号(;)分隔的列表
    • 访问变量:${<variable>}

      message(STATUS "MY_VARIABLE is: ${MY_VARIABLE}")
      add_executable(MyApp ${SOURCE_FILES}) # 展开SOURCE_FILES列表
      
    • 取消设置变量:unset()

      unset(MY_VARIABLE) # 删除变量 MY_VARIABLE
      
  2. 变量作用域:代码执行的“领地”
    CMake变量的可见性由其被定义的位置决定,这就是作用域。主要作用域类型:

    • 1. 目录作用域 (Directory Scope - 最常见):

      • 变量在定义它的 CMakeLists.txt 文件及其下级子目录的 CMakeLists.txt 文件中可见。

      • add_subdirectory() 的影响:  当父目录使用 add_subdirectory(subdir) 进入子目录 subdir 时:

        • 父目录定义的普通变量会复制一份到子目录作用域(子目录的修改不会影响父目录的副本)。
        • 子目录定义的普通变量在其自身作用域可见,但不会自动向上传递到父目录。
      • 示例 (project() 设置的内置变量如 PROJECT_SOURCE_DIR 也是目录作用域):

        # 顶层 CMakeLists.txt
        set(TOP_LEVEL_VAR "I'm at the top")
        message("Top (Before subdir): TOP_LEVEL_VAR=${TOP_LEVEL_VAR}") # 输出: I'm at the top
        
        add_subdirectory(subdir)
        
        message("Top (After subdir): TOP_LEVEL_VAR=${TOP_LEVEL_VAR}") # 输出: I'm at the top (未被子目录修改)
        
        # subdir/CMakeLists.txt
        message("Subdir (Start): TOP_LEVEL_VAR=${TOP_LEVEL_VAR}") # 输出: I'm at the top (父目录变量的副本)
        
        set(TOP_LEVEL_VAR "Modified in subdir") # 修改的是子目录作用域内的副本
        set(SUB_VAR "I'm only in subdir")
        
        message("Subdir (End): TOP_LEVEL_VAR=${TOP_LEVEL_VAR}") # 输出: Modified in subdir
        
        # 回到顶层 CMakeLists.txt
        message("Top (After subdir): SUB_VAR=${SUB_VAR}") # 输出: SUB_VAR= (空! 子目录变量不可见)
        
    • 2. 函数作用域 (Function Scope):

      • 在 function() ... endfunction() 中定义的变量,默认只在该函数内部可见。

      • 函数参数 (ARGVARGN) 也属于函数作用域。

      • PARENT_SCOPE 关键字:  如果需要在函数内部修改调用者作用域(通常是定义函数的目录作用域)的变量,必须使用 set(... PARENT_SCOPE)

        function(my_function)
            set(INSIDE_FUNC "Local Value") # 只在函数内可见
            set(OUTSIDE_VAR "Changed Inside" PARENT_SCOPE) # 修改调用者的 OUTSIDE_VAR
        endfunction()
        
        set(OUTSIDE_VAR "Original")
        my_function()
        message("OUTSIDE_VAR=${OUTSIDE_VAR}") # 输出: Changed Inside
        message("INSIDE_FUNC=${INSIDE_FUNC}") # 输出: (空! 函数内变量不可见)
        
    • 3. 持久缓存作用域 (Cache Scope):

      • 使用 set(... CACHE ...) 设置的变量称为缓存变量 (Cache Variables)

      • 它们存储在 CMakeCache.txt 文件中(位于构建目录)。即使你修改了 CMakeLists.txt 并重新运行 cmake,这些变量的值默认会保留(除非你显式删除缓存或使用 FORCE 覆盖)。

      • 缓存变量在整个项目(所有目录、函数)中全局可见

      • 语法:

        set(<variable> <value> CACHE <type> <docstring> [FORCE])
        
        • <type>: 变量类型,主要影响GUI工具中的显示。常用类型:

          • BOOL (ON/OFF): 布尔值 (复选框)
          • FILEPATH: 文件路径 (文件选择对话框)
          • PATH: 目录路径 (目录选择对话框)
          • STRING: 字符串 (文本输入框)
          • INTERNAL: 内部变量 (通常不在GUI中显示)
        • <docstring>: 变量的描述文本,会在GUI工具和 cmake -L 等命令中显示。

        • FORCE: 强制覆盖缓存中已存在的值。慎用!

      • 示例 (定义一个开关选项):

        option(ENABLE_FEATURE_X "Enable the experimental Feature X" OFF) # option() 本质是 set(... CACHE BOOL ...)
        set(INSTALL_PREFIX "/usr/local" CACHE PATH "Where to install the software")
        
      • 访问缓存变量:  同样使用 ${<variable>}。如果存在同名的普通变量,普通变量优先。要强制访问缓存变量,使用 $CACHE{<variable>} (CMake 3.13+)。

  3. 环境变量:

    • 访问系统环境变量使用 $ENV{<VARNAME>}
    • 设置环境变量只在当前CMake进程及其生成的子进程(如构建命令)中有效,使用 set(ENV{<VARNAME>} <value>)
    message("PATH is: $ENV{PATH}")
    set(ENV{TEMP_DIR} "/tmp/my_temp") # 仅影响CMake进程及后续构建命令
    

四、 本章小结

本章我们深入探索了 CMakeLists.txt 的三大核心构件:

  1. project() 命令:

    • 定义了项目名称、版本、描述、主页和语言。
    • 设置了关键的内置目录变量 PROJECT_SOURCE_DIRPROJECT_BINARY_DIR 和版本变量。
    • 是CMake配置的起点。
  2. add_library() 命令:

    • 用于创建静态库 (STATIC)、共享库 (SHARED) 或模块库 (MODULE)。
    • 定义了库目标 (<name>) 及其源文件。
    • 静态库 vs 动态库的选择取决于项目需求(大小、独立性、更新策略)。
    • 库目标是现代CMake管理依赖的基础。
  3. 变量系统:

    • set() 用于设置变量,unset() 用于取消设置,${} 用于访问。

    • 作用域是核心概念:

      • 目录作用域:  变量在定义目录及其子目录可见(add_subdirectory 会复制父变量到子作用域)。
      • 函数作用域:  变量只在函数内部可见,修改父作用域需 PARENT_SCOPE
      • 缓存作用域:  set(... CACHE ...) 定义的变量全局可见,持久存储在 CMakeCache.txt 中。
    • 环境变量通过 $ENV{...} 访问和设置。