[CMAKE]tutorial
0 概述
在软件开发的世界里,构建系统扮演着至关重要的角色,尤其是对于那些涉及多个源文件、库依赖以及跨平台支持的复杂项目。随着项目规模的增长,手动编译和管理这些依赖关系变得既耗时又容易出错。这就是CMake作为一款强大的构建系统生成器所发挥重要作用的地方。本篇博客参考Cmake官方教程。所有的代码放在。
1 Basic
创建一个基本的 CMake 项目
构建一个从单个源代码文件生成的可执行文件是CMake项目的最简形式。对于这类基础项目,只需编写一个包含三个命令的CMakeLists.txt
文件即可。
尽管CMake支持使用大写、小写或混合大小写的命令,但推荐使用小写命令格式。
关键步骤
指定CMake最低版本
每个项目的顶层
CMakeLists.txt
文件首先需要通过cmake_minimum_required()
命令指定所需的最小CMake版本。这一步骤确保了所使用的CMake函数与相应版本兼容,并设置了策略环境。1
cmake_minimum_required(VERSION 3.10)
设置项目名称
接下来,使用
project()
命令定义项目名称。此命令应在cmake_minimum_required()
之后立即调用,且对任何项目都是必需的。此外,它还可用于设定其他项目级信息,如编程语言和版本号等。1
project(MyProject)
创建可执行文件
最后,使用
add_executable()
命令告知CMake根据指定的源代码文件创建可执行文件。1
add_executable(MyExecutable main.cpp)
构建和运行项目
首先,使用cmake
命令行工具或cmake-gui
来配置项目。以命令行为例,进入CMake源代码树的特定教程目录,并创建一个专门用于构建的目录:
1 |
|
接着,在新建的构建目录中运行cmake
以配置项目并生成本地构建系统:
1 |
|
配置完成后,调用生成的构建系统来编译和链接项目。这可以通过以下命令完成:
1 |
|
对于多配置生成器(如Visual Studio),需先导航至对应的子目录,例如Debug
或Release
,再执行相应的构建命令:
1 |
|
成功构建项目后,即可尝试运行新构建的应用程序。
1 |
|
指定c++标准
在CMake中,存在一系列以CMAKE_
为前缀的特殊变量,它们或是由CMake内部创建,或是在项目配置后对CMake具有特定意义。为了避免与这些预定义变量发生冲突,在为自定义项目创建新变量时,应避免使用CMAKE_
作为变量名的开头。
其中,两个重要的用户可设置变量是CMAKE_CXX_STANDARD
和CMAKE_CXX_STANDARD_REQUIRED
。这两个变量协同工作,用于指定构建项目所必需的C++标准版本:
CMAKE_CXX_STANDARD
:指定项目所需的C++标准版本(例如,98、11、14、17等)。CMAKE_CXX_STANDARD_REQUIRED
:设定为TRUE
时,表示所指定的C++标准是必须的。如果编译器不支持该标准,则构建过程将失败。
示例配置如下:
1 |
|
添加版本号和配置的头文件
在某些场景下,将CMakeLists.txt中定义的变量应用于源代码是十分必要的,例如打印项目的版本号。为此,可以采用配置头文件的方法实现。
首先,创建一个包含占位符变量(如 @VAR@
格式)的模板文件。然后,通过CMake的 configure_file()
命令,将此模板文件复制为目标文件,并用CMakeLists.txt中对应变量的实际值替换所有占位符。
这种方法的优势在于维护了一个单一的真实数据源,避免了手动更新多个位置所带来的错误风险。
configure_file命令
configure_file
命令用于将一个文件复制到另一个位置,并在复制过程中对文件内容进行必要的转换。该命令常用于生成配置头文件或定制安装脚本。
命令格式
1 |
|
<input>
:指定要读取和转换的源文件。<output>
:指定目标文件的位置及名称。
选项说明
NO_SOURCE_PERMISSIONS
:不复制源文件的权限;这是默认行为。USE_SOURCE_PERMISSIONS
:使用源文件的权限设置目标文件权限。FILE_PERMISSIONS
:明确指定目标文件的权限。COPYONLY
:仅执行文件复制,不进行任何内容替换。ESCAPE_QUOTES
:转义文本中的引号。@ONLY
:仅替换形式为@VAR@
的变量引用。NEWLINE_STYLE
:设置输出文件的换行符风格,支持UNIX、DOS、WIN32、LF、CRLF。
使用场景
当你需要在项目构建期间动态生成配置文件时,configure_file
是理想选择。例如,它可用于生成包含版本信息或编译时间戳的头文件,从而确保这些值在每次构建时都能准确反映项目的当前状态。
修改输入文件后,构建系统会自动重新运行CMake以更新配置文件,确保所有更改得到正确应用。只有当文件内容实际发生变化时,才会更新目标文件的时间戳,避免不必要的重新编译。
<PROJECT-NAME>_VERSION_MAJOR
和 <PROJECT-NAME>_VERSION_MINOR
<PROJECT-NAME>_VERSION_MAJOR
和 <PROJECT-NAME>_VERSION_MINOR
是预定义变量,用于表示项目主版本号和次版本号。由 project()
命令设置。
定义与使用
在CMake项目的上下文中,project()
命令不仅初始化了项目的基本信息,还设置了多个与项目相关的变量。其中,<PROJECT-NAME>_VERSION
包含了项目的完整版本号(如“2.5.1”),而 <PROJECT-NAME>_VERSION_MAJOR
则提取并存储该版本号的第一个部分,即主版本号(例如,在版本号“2.5.1”中,“2”为主版本号); <PROJECT-NAME>_VERSION_MINOR
则具体指代该版本号的次版本编号(例如,在版本号“2.5.1”中,“5”为次版本号)。
示例
假设在一个CMakeLists.txt文件中有如下声明:
1 |
|
执行上述命令后,将自动生成以下变量:
MyProject_VERSION_MAJOR
的值为3
MyProject_VERSION_MINOR
的值为2
应用场景
MyProject_VERSION_MAJOR
对于需要根据项目的主版本号来调整编译选项、链接库或生成文档等操作特别有用。例如,可以根据主版本号的变化引入不兼容的API更改或是重大功能更新,从而帮助开发者更好地管理项目的生命周期和依赖关系。
次版本号通常用于标记新功能的引入或显著改进而不破坏现有功能的兼容性。因此,<PROJECT-NAME>_VERSION_MINOR
对于需要根据项目的次版本号来调整编译选项、依赖管理或文档生成等操作特别有用。例如,可以基于次版本号的变化来决定是否启用某些实验性功能或者更新特定的开发工具链。这有助于开发者更好地维护和升级项目。
target_include_directories
函数
target_include_directories
是 CMake 中用于向特定目标添加包含目录的命令。它允许开发者指定在编译给定目标时应使用的头文件搜索路径。
基本语法
1 |
|
<target>
: 必须是之前通过add_executable()
或add_library()
定义的目标,不能为别名目标。[SYSTEM]
: 标识这些目录是否应被视为系统包含目录,在某些平台上可能影响编译器行为(如抑制警告)。[AFTER|BEFORE]
: 控制追加或前置包含目录的方式。<INTERFACE|PUBLIC|PRIVATE>
: 指定范围,决定哪些依赖于该目标的其他目标可以访问这些包含目录。PRIVATE: 仅当前目标使用。
PUBLIC: 当前目标及其依赖者均可访问。
INTERFACE: 仅对依赖于该目标的其他目标有效,自身不使用。
例如:
1 |
|
注意事项
- 使用生成器表达式
$<...>
可以根据构建上下文动态调整包含目录路径。 - 对于导入目标,自 CMake 3.11 起支持设置 INTERFACE 项。
- 在创建可重定位的软件包时,避免硬编码依赖项的绝对路径到
INTERFACE_INCLUDE_DIRECTORIES
属性中,以免限制包的灵活性和可移植性。
2 添加库
创建一个库
在CMake项目中添加一个库,可以通过add_library()
命令指定构成该库的源文件。为了更好地组织代码,通常会将源文件分布在多个子目录中。以下是如何高效地完成这些步骤的方法。
添加库
首先,在库所属的子目录下创建一个CMakeLists.txt
文件,并使用add_library()
定义库:
1 |
|
这里MyLibrary
是新创建库的名称,后面跟随的是构成库的所有源文件路径。
组织项目结构
如果项目包含多个子目录,可以利用add_subdirectory()
命令从顶层CMakeLists.txt
文件中添加这些子目录:
1 |
|
这样,CMake就会处理libs/MyLibrary
目录下的CMakeLists.txt
文件,从而将这个子项目集成到整个构建过程中。
配置库的包含目录与链接
一旦库被创建,需要配置其包含目录以及与其他目标的链接关系。这通过target_include_directories()
和target_link_libraries()
实现:
1 |
|
此处,MyExecutable
是希望链接MyLibrary
的可执行文件的目标名称。通过PUBLIC
关键字,确保了任何依赖于MyLibrary
的目标都能够访问指定的包含目录;而通过PRIVATE
关键字链接MyLibrary
,表示这种链接关系仅对MyExecutable
有效,不对其他依赖它的目标公开。
add_library()
add_library()
用于定义一个新的库目标。它告诉CMake如何从一组源文件构建出一个库。其基本语法如下:
1 |
|
<name>
是库的名字。- 可以指定库的类型:静态库(STATIC)、共享库(SHARED)或模块库(MODULE)。如果不指定,默认为静态库。
[<source>...]
表示构成这个库的所有源文件。
add_subdirectory()
add_subdirectory()
允许将子目录添加到构建过程中。这对于组织大型项目特别有用。其语法为:
1 |
|
source_dir
是要添加的子目录路径。binary_dir
(可选)指定输出目录。如果未提供,则默认与source_dir
相同。EXCLUDE_FROM_ALL
(可选)标记此子目录不包含在默认构建中。
target_link_libraries()
target_link_libraries()
用于指定哪些库需要链接到指定的目标上。语法如下:
1 |
|
<target>
指定了要链接的库的目标名称。- 后续参数可以是库名或其他目标,还可以通过
debug
,optimized
, 或general
关键字来指定针对不同构建配置链接不同的库。
PROJECT_SOURCE_DIR
PROJECT_SOURCE_DIR
是一个预定义的CMake变量,指向当前处理的顶层CMakeLists.txt文件所在的目录。这对于引用项目根目录下的文件非常有用。例如:
1 |
|
添加选项
为了在 MathFunctions
库中提供一个选项,允许开发人员选择是使用自定义平方根实现还是标准实现,可以利用 CMake 的 option()
命令。此命令不仅提供了灵活性,还通过缓存机制简化了配置过程。以下是具体步骤和代码示例:
添加选项
首先,在 CMakeLists.txt
文件中使用 option()
命令来创建一个可选的布尔变量。例如,可以创建一个名为 USE_CUSTOM_SQRT
的选项。
1 |
|
这里:
USE_CUSTOM_SQRT
是选项的名称。"Use custom implementation of square root"
是该选项的帮助字符串,描述了它的用途。ON
表示默认启用此选项。可以根据需要设置为OFF
。
根据选项调整构建逻辑
接下来,基于 USE_CUSTOM_SQRT
选项的状态,我们可以决定是否包含自定义平方根实现或标准实现。
1 |
|
在此示例中:
- 如果
USE_CUSTOM_SQRT
被设为ON
,则将使用mysqrt.cxx
文件作为平方根的实现。 - 如果
USE_CUSTOM_SQRT
被设为OFF
,则使用standard_sqrt.cxx
文件中的标准实现。
配置和生成项目
完成上述配置后,可以在首次运行 cmake
配置其构建目录时,通过命令行参数 -DUSE_CUSTOM_SQRT=[ON|OFF]
来指定希望使用的平方根实现方式。一旦设置,这个值会被存储在 CMake 缓存中,因此不需要每次重新配置都进行设置。
if()
if()
命令用于在 CMake 脚本中进行条件判断,允许根据不同的条件执行不同的代码块。它支持多种条件表达式,包括变量检查、布尔运算等。
if()
可以与变量、逻辑运算符(如AND
,OR
,NOT
)结合使用。- 条件表达式可以用来检测选项值、环境变量、文件存在性等多种情况。
option()
option()
命令用于定义用户可选的选项,这些选项可以在配置项目时通过 -D
参数设定,并且其值会被缓存以便于后续构建过程中复用。
示例:
1 |
|
- 第一个参数是选项名称。
- 第二个参数是帮助信息,描述该选项的作用。
- 第三个参数是默认值(
ON
或OFF
),表示是否启用该选项。
target_compile_definitions()
target_compile_definitions()
命令用于为目标添加预处理定义,这使得可以在编译时根据不同的目标设置不同的宏定义。
示例:
1 |
|
- 第一个参数是要应用定义的目标名称。
- 第二个参数指定定义的应用范围,可以是
PRIVATE
、PUBLIC
或INTERFACE
,分别对应仅当前目标、当前目标及其依赖项、以及仅依赖当前目标的目标。 - 后续参数是具体的宏定义名称,可以根据需要添加多个定义。
结合使用
这三个命令经常被组合起来使用,以实现更加灵活和可控的构建配置。例如,通过 option()
定义用户选项,利用 if()
根据选项值执行不同分支的代码逻辑,最后通过 target_compile_definitions()
针对特定目标添加或修改编译期定义。