CMake入门
第一部分:基本例程
1. hello-cmake
文件树:
|
|
CMakeLists.txt
|
|
1.1 命令作用解析
|
|
CMake构建包含一个项目名称,上面的命令会自动生成一些变量,在使用多个项目时引用某些变量会更加容易。比如生成了:PROJECT_NAME
这个变量。PROJECT_NAME
是变量名,${PROJECT_NAME}
是变量值,值为hello_cmake
。
|
|
add_executable()
命令指定某些源文件生成可执行文件,本例中是main.cpp。add_executable()
函数的第一个参数是可执行文件名,第二个参数是要编译的源文件列表。
1.2 生成与工程同名的二进制文件
|
|
project(hello_cmake)
函数执行时会生成一个变量,是PROJECT_NAME
,${PROJECT_NAME}
表示PROJECT_NAME
变量的值为hello_cmake
,所以把${PROJECT_NAME}
用在add_executable()
里可以生成可执行文件名字叫hello_cmake
。
1.3 外部构建与内部构建
外部构建(推荐):
使用外部构建,我们可以创建一个可以位于文件系统上任何位置的构建文件夹。所有临时构建和目标文件都位于此目录中,以保持源代码树的整洁。
运行下述代码,新建build构建文件夹,并运行cmake命令:
|
|
内部构建:
内部构件将所有临时文件和源文件生成到一起,没有build,临时文件会和源代码文件在一起。(不推荐)
2 包含头文件(hello-headers)
文件树:
|
|
CMakeLists.txt
|
|
2.1 各种可用变量
CMake语法指定了许多变量,可用于帮助你在项目或源代码树中找到有用的目录。 其中一些包括:
Variable | Info |
---|---|
CMAKE_SOURCE_DIR | 根源代码目录,工程顶层目录。暂认为就是PROJECT_SOURCE_DIR |
CMAKE_CURRENT_SOURCE_DIR | 当前处理的 CMakeLists.txt 所在的路径 |
PROJECT_SOURCE_DIR | 工程顶层目录 |
CMAKE_BINARY_DIR | 运行cmake的目录。外部构建时就是build目录 |
CMAKE_CURRENT_BINARY_DIR | The build directory you are currently in.当前所在build目录 |
PROJECT_BINARY_DIR | 暂认为就是CMAKE_BINARY_DIR |
想仔细体会一下,可以在CMakeLists中,利用message()命令输出一下这些变量。
另外,这些变量不仅可以在CMakeLists中使用,同样可以在源代码.cpp中使用。
2.2 源文件变量(不建议!)
创建一个包含源文件的变量,以便于将其轻松添加到多个命令中,例如add_executable()函数。
|
|
在SOURCES
变量中设置特定文件名的另一种方法是使用GLOB命令使用通配符模式匹配来查找文件。file(GLOB SOURCES "src/*.cpp")
使用*这个通配符,表示所有.cpp结尾的文件都会包含到这个SOURCES
变量。
对于modern CMake,不建议对源文件使用变量。不建议使用glob。
相反,通常直接在add_xxx函数中声明源文件。
这对于glob命令尤其重要,如果添加新的源文件,这些命令可能不会始终为你显示正确的结果。在CMake中指定源文件的最佳方法是明确列出它们。
2.3 包含目录
当你有其他需要包含的文件夹(文件夹里有头文件)时,可以使用以下命令使编译器知道它们:target_include_directories()
。 编译此目标时,这将使用-I
标志将这些目录添加到编译器中,例如 -I /目录/路径
。
|
|
PRIVATE
标识符指定包含的范围。这对库很重要,将在下一个示例中进行说明。
2.4 详细输出
在前面的示例中,运行make命令时,输出仅显示构建状态。 要查看用于调试目的的完整输出,可以在运行make时添加VERBOSE = 1
标志。
下面的代码是使用VERBOSE
的命令
|
|
其中VERBOSE = 1
可以省略。
3. 包含静态库(Static-Library)
文件树:
|
|
CMakeLists.txt
|
|
3.1 创建静态库
add_library()
函数用于从某些源文件创建一个库,默认生成在构建文件夹。写法如下:
|
|
在add_library
调用中包含了源文件,用于创建名称为libhello_library.a
的静态库。
如前面的示例所述,将源文件直接传递给add_library调用,这是modern CMake的建议。(而不是先把Hello.cpp赋给一个变量)
3.2 添加头文件所在的目录
使用target_include_directories()
添加了一个目录,这个目录是库所包含的头文件的目录,并设置库属性为PUBLIC
。
|
|
使用这个函数后,这个目录会在以下情况被调用:
- 编译这个库的时候;
- 因为这个库
hello_library
由Hello.cpp生成,Hello.cpp中函数的定义在Hello.h中,Hello.h在这个include目录下,所以显然编译这个库的时候,这个目录会用到; - 编译链接到这个库
hello_library
的任何其他目标(库或者可执行文件);
private pubic interface的范围详解:
- PRIVATE - 目录被添加到目标(库)的包含路径中。
- INTERFACE - 目录没有被添加到目标(库)的包含路径中,而是链接了这个库的其他目标(库或者可执行程序)包含路径中
- PUBLIC - 目录既被添加到目标(库)的包含路径中,同时添加到了链接了这个库的其他目标(库或者可执行程序)的包含路径中
也就是说,根据库是否包含这个路径,以及调用了这个库的其他目标是否包含这个路径,可以分为三种scope。
建议:
- 对于公共的头文件,最好在include文件夹下建立子目录。
- 传递给函数target_include_directories()的目录,应该是所有包含目录的根目录,然后在这个根目录下建立不同的文件夹,分别写头文件。
这样使用的时候,不需要写${PROJECT_SOURCE_DIR}/include
,而是直接选择对应的文件夹里对应头文件。下面是例子:#include "static/Hello.h"
而不是#include "Hello.h"
使用此方法意味着在项目中使用多个库时,头文件名冲突的可能性较小。
3.3 链接库
创建将使用这个库的可执行文件时,必须告知编译器需要用到这个库。 可以使用target_link_libraries()
函数完成此操作。add_executable()
连接源文件,target_link_libraries()
连接库文件。
|
|
这告诉CMake在链接期间将hello_library
链接到hello_binary
可执行文件。 同时,这个被链接的库如果有INTERFACE
或者PUBLIC
属性的包含目录,那么,这个包含目录也会被传递( propagate )给这个可执行文件。
对于target_link_libraries(hello_binary PRIVATE hello_library)
这个命令中的scope关键字,private,public以及interface
可以举例理解:
public是说,你的这个工程如果被link了,那你的target_link_libraries指定的lib也会被链; private是说,你link的这些libs不会被暴露出去。
比如你的工程B是个dll,public连接了C, D 这个时候你的A.exe要链接B,那么它也会链接C和D 如果B是private链接了C, D 那么A链B的时候,不会链C和D
那么,A.exe链接B的时候,其实也有public和private的选项,但是因为没有其他东西链接A,所以不起作用。 这个主要是针对其它工程链自己的设置
对于hello_binary
,它不是库,所以不会被链接。直接private自己用这个库就行。
4. 包含动态库(Shared-Library)
文件树:
|
|
CMakeLists.txt
|
|
4.1 创建动态库
add_library()
函数用于从某些源文件创建一个动态库,默认生成在构建文件夹。写法如下:
|
|
在add_library
调用中包含了源文件,用于创建名称为libhello_library.so
的动态库。
如前面的示例所述,将源文件直接传递给add_library调用,这是modern CMake的建议。(而不是先把Hello.cpp赋给一个变量)
4.2 创建别名目标
顾名思义,别名目标是在只读上下文中可以代替真实目标名称的替代名称。
|
|
如下所示,当你将目标链接到其他目标时,使用别名可以引用目标。
链接共享库与链接静态库相同。 创建可执行文件时,请使用target_link_libraries()
函数指向你的库。
|
|
这告诉CMake使用别名目标名称将hello_library链接到hello_binary可执行文件。
5. 设置构建类型(build-type)
文件树:
|
|
CMakeLists.txt
|
|
5.1 构建级别
CMake具有许多内置的构建配置,可用于编译工程。 这些配置指定了代码优化的级别,以及调试信息是否包含在二进制文件中。
这些优化级别,主要有:
- Release:不可以打断点调试,程序开发完成后发行使用的版本,占的体积小。它对代码做了优化,因此速度会非常快,在编译器中使用命令:
-O3 -DNDEBUG
可选择此版本。 - Debug:调试的版本,体积大。在编译器中使用命令:
-g
可选择此版本。 - MinSizeRel:最小体积版本。在编译器中使用命令:
-Os -DNDEBUG
可选择此版本。 - RelWithDebInfo:既优化又能调试。在编译器中使用命令:
-O2 -g -DNDEBUG
可选择此版本。
5.2 设置级别的方式
CMake命令行中:
在命令行运行CMake的时候, 使用cmake命令行的-D
选项配置编译类型
|
|
CMake中设置默认的构建级别:
CMake提供的默认构建类型是不进行优化的构建级别。对于某些项目,需要自己设置默认的构建类型,以便不必记住进行设置。
具体语法接下来介绍
set()命令:
该命令可以为普通变量、缓存变量、环境变量赋值。
可以设置零个或多个参数。多个参数将以分号分隔的列表形式加入,以形成要设置的实际变量值。零参数将导致未设置普通变量。见unset()
命令显式取消设置变量。
所以此处学习SET命令需要分为设置普通变量,缓存变量以及环境变量三种类别来学习。
正常变量
|
|
设置的变量值 作用域属于整个 CMakeLists.txt 文件。(一个工程可能有多个CMakeLists.txt)
当这个语句中加入PARENT_SCOPE
后,表示要设置的变量是父目录中的CMakeLists.txt设置的变量。
比如有如下目录树:
|
|
并且在顶层的CMakeLists.txt中包含了src目录:add_subdirectory(src)
那么,顶层的CMakeLists.txt就是父目录。如果父目录中有变量Bang,在子目录中可以直接使用(比如用message输出Bang,值是父目录中设置的值)并且利用set()修改该变量Bang的值,但是如果希望在该子CMakeLists.txt对该变量做出的修改能够得到保留,那么就需要在set()命令中加入PARENT_SCOPE
这个变量。当然,如果父目录中本身没有这个变量,子目录中仍然使用了PARENT_SCOPE
,那么出了这个作用域后,该变量仍然不会存在。
这里举一个实际的例子:
|
|
我们建立一个项目结构如上:
|
|
|
|
执行如下:
|
|
|
|
从这里来看我们发现在执行父级CmakeLists.txt的内容时,会输出子目录的内容,而在执行子目录的CmakeLists.txt时则只会输出自己的内容。
CACHE变量
完整语句如下:
|
|
- 首先什么是CACHE变量,就是在运行cmake的时候,变量的值可能会被缓存到一份文件里即build命令下的CMakeCache.txt,当你重新运行cmake的时候,那些变量会默认使用这个缓存里的值。这个变量是全局变量,整个CMake工程都可以使用该变量。
- 在这个文件里,只要运行
cmake ..
命令,自动会出现一些值,比如CMAKE_INSTALL_PREFIX
,如果设置set(CMAKE_INSTALL_PREFIX "/usr")
,虽然CACHE缓存文件里还有这个CMAKE_INSTALL_PREFIX
变量,但是因为我们显示得设置了一个名为CMAKE_INSTALL_PREFIX
的正常变量,所以之后使用CMAKE_INSTALL_PREFIX
,值是我们设置的正常变量的值。 - 如果加上CACHE关键字,则设置的这个变量会被写入缓存文件中(但如果本身缓存文件中有这个变量,则不会覆盖缓存中的变量)。只有加上
FORCE
关键字,这个被写入文件的值会覆盖之前文件中存在的同名变量。 - 加上CACHE关键字,和是必需的。
被 CMake GUI 用来选择一个窗口,让用户设置值。可以有5种选项。其中一个是STRING ,弹出提示消息
- 为BOOL,则为布尔ON/OFF值。 cmake-gui(1)) 提供一个复选框。
- 为FILEPATH,则为磁盘上文件的路径。 cmake-gui(1)) 提供一个文件对话框。
- 为 PATH ,则为磁盘上目录的路径。 cmake-gui(1)) 提供一个文件对话框。
- 为 STRING ,则为一行文字。 cmake-gui(1)) 提供文本字段或下拉选择(如果 STRINGS 设置了缓存条目属性。)
- 为 INTERNAL ,则为一行文字。 cmake-gui(1))不显示内部条目。它们可用于在运行之间持久存储变量。使用此类型暗含FORCE。
比如:
|
|
这句话,就是强制在缓存文件中覆盖CMAKE_BUILD_TYPE
这个变量,将这个变量设置为RelWithDebInfo
。而STRING
“Choose the type of build.“参数在使用cmake-gui的时候起作用,在界面上会出现一个下拉框供给用户选择来设置CMAKE_BUILD_TYPE
变量。里的一行文字作为提示。
但是这个下拉框里的内容,需要使用随后的set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS "Debug" "Release" "MinSizeRel" "RelWithDebInfo")
这个命令来设置。也就是所谓的设置string缓存条目属性。
环境变量
|
|
设置一个 Environment Variable
到给定值。随后的调用$ENV{<varible>}
将返回此新值。
此命令仅影响当前的CMake进程,不影响调用CMake的进程,也不影响整个系统环境,也不影响后续构建或测试过程的环境。
如果在空字符串之后ENV{}
或如果没有参数``,则此命令将清除环境变量的任何现有值。
之后``的参数将被忽略。如果发现其他参数,则会发出作者警告。
6. 设置编译方式(Compile-Flags)
首先说一下什么是编译标志(或者 叫编译选项)。可执行文件的生成离不开编译和链接,那么如何编译,比如编译时使用C++的哪一个标准?这些编译设置都在CMAKE_CXX_FLAGS变量中。(C语言编译选项是CMAKE_C_FLAGS)
文件树:
|
|
CMakeLists.txt
|
|
6.1 设置每个目标编译标志
在现代CMake中设置C ++标志的推荐方法是专门针对某个目标(target)设置标志,可以通过target_compile_definitions()函数设置某个目标的编译标志。
|
|
如果这个目标是一个库(cmake_examples_compile_flags),编译器在编译目标时添加定义-DEX3
,并且选择了范围PUBLIC
或INTERFACE
,该定义-DEX3
也将包含在链接此目标(cmake_examples_compile_flags)的所有可执行文件中。 注意,本语句使用了PRIVATE,所以编译选项不会传递。
对于编译器选项,还可以使用target_compile_options()
函数。
|
|
是给 target 添加编译选项, target 指的是由 add_executable()
产生的可执行文件或 add_library()
添加进来的库。<INTERFACE|PUBLIC|PRIVATE>
指的是[items...]
选项可以传播的范围,PUBLIC
and INTERFACE
会传播 <target>
的 INTERFACE_COMPILE_DEFINITIONS
属性, PRIVATE
and PUBLIC
会传播 target 的 COMPILE_DEFINITIONS
属性。
6.2 设置默认编译标志
默认的CMAKE_CXX_FLAGS
为空或包含适用于构建类型的标志。 要设置其他默认编译标志,如下使用:
|
|
强制设置默认C++编译标志变量为缓存变量,该缓存变量被定义在文件中,相当于全局变量,源文件中也可以使用这个变量。这个变量原本包含的参数仍然存在,只是添加了EX2。
CACHE STRING "Set C++ Compiler Flags" FORCE
命令是为了强制将CMAKE_CXX_FLAGS
变量 放到CMakeCache.txt文件中
"${CMAKE_CXX_FLAGS} -DEX2"
这个字符串可以保留原有的CMAKE_CXX_FLAGS
中的参数,额外添加了一个EX2
参数。注意写法:空格,并且参数前加了-D
类似设置CMAKE_CXX_FLAGS
,还可以设置其他选项:
- 设置C编译标志:
CMAKE_C_FLAGS
- 设置链接标志:
CMAKE_LINKER_FLAGS
6.3 设置CMake标志
与构建类型类似,可以使用以下方法设置全局C 编译器标志。
- 利用ccmake或者gui
- 在cmake命令行中:
cmake .. -DCMAKE_CXX_FLAGS="-DEX3"
6.4 区别
- 6.2方法的设置
CMAKE_C_FLAGS
和CMAKE_CXX_FLAGS
将为该目录或所有包含的子目录中的所有目标全局设置一个编译器标志。现在不建议使用该方法,首选使用target_compile_definitions
函数。 - 6.1方法是被建议的,只为这个目标设置编译选项。
- 6.3设置的也是全局编译器选项。
7. 包含第三方库(Including-Third-Party-Library)
文件树:
|
|
CMakeLists.txt
|
|
几乎所有不平凡的项目都将要求包含第三方库,头文件或程序。 CMake支持使用find_package()
函数查找这些工具的路径。 这将从CMAKE_MODULE_PATH
中的文件夹列表中搜索格式为“FindXXX.cmake”的CMake模块。 在linux上,默认搜索路径将是/usr/share/cmake/Modules
。在我的系统上,这包括对大约142个通用第三方库的支持。
此示例要求将Boost库安装在默认系统位置。
7.1 Finding a Package
如上所述,find_package()
函数将从CMAKE_MODULE_PATH
中的文件夹列表中搜索“FindXXX.cmake”中的CMake模块。 find_package
参数的确切格式取决于要查找的模块。这通常记录在FindXXX.cmake文件的顶部。
|
|
参数:
- Boost-库名称。 这是用于查找模块文件FindBoost.cmake的一部分
- 1.46.1 - 需要的boost库最低版本
- REQUIRED - 告诉模块这是必需的,如果找不到会报错
- COMPONENTS - 要查找的库列表。从后面的参数代表的库里找boost
可以使用更多参数,也可以使用其他变量。 在后面的示例中提供了更复杂的设置。
7.2 Checking if the package is found
大多数被包含的包将设置变量XXX_FOUND,该变量可用于检查软件包在系统上是否可用。
在此示例中,变量为Boost_FOUND:
|
|
7.3 Exported Variables
找到包后,它会自动导出变量,这些变量可以通知用户在哪里可以找到库,头文件或可执行文件。 与XXX_FOUND变量类似,它们与包绑定在一起,通常记录在FindXXX.cmake文件的顶部。
本例中的变量:
|
|
在某些情况下,你还可以通过使用ccmake或cmake-gui检查缓存来检查这些变量。
7.4 Alias/Imported targets别名/导入目标
大多数modern CMake库在其模块文件中导出别名目标。 导入目标的好处是它们也可以填充包含目录和链接的库。 例如,从CMake v3.5开始,Boost模块支持此功能。 与使用自己的别名目标相似,模块中的别名可以使引用找到的目标变得更加容易。 对于Boost,所有目标均使用Boost ::标识符,然后使用子系统名称导出。 例如,你可以使用:
- Boost::boost for header only libraries
- Boost::system for the boost system library.
- Boost::filesystem for filesystem library.
与你自己的目标一样,这些目标包括它们的依赖关系,因此与Boost::filesystem
链接将自动添加Boost::boost
和Boost::system
依赖关系。
要链接到导入的目标,可以使用以下命令:
|
|
7.5 Non-alias targets
尽管大多数现代库都使用导入的目标,但并非所有模块都已更新。 如果未更新库,则通常会发现以下可用变量:
- xxx_INCLUDE_DIRS - 指向库的包含目录的变量。
- xxx_LIBRARY - 指向库路径的变量。
然后可以将它们添加到你的target_include_directories
和target_link_libraries
中,如下所示:
|
|
第二部分:子目录中使用多个CMake文件
许多大型项目由不同的库和二进制文件组成。本文利用多个CMakeLists.txt文件组织这些库和文件。
包括的示例是:
- basic - 此基本示例包括一个静态库,一个仅有头文件的库和一个可执行文件。
12. 使用子工程CMake(subproject-CMake)
本示例说明如何包含子项目。 顶级CMakeLists.txt调用子目录中的CMakeLists.txt来创建以下内容:
- sublibrary1 - 一个静态库
- sublibrary2 - 只有头文件的库
- subbinary - 一个可执行文件
文件树:
|
|
CMakeLists.txt,最高层的CMakeLists.txt。
|
|
subbinary/CMakeLists.txt,生成可执行文件的CMakeLists.txt。
|
|
sublibrary1/CMakeLists.txt,生成静态库的CMakeLists.txt。
|
|
sublibrary2/CMakeLists.txt,生成仅有头文件的库的CMakeLists.txt。
|
|
在此示例中,我已将头文件移至每个项目include目录下的子文件夹,而将目标include保留为根include文件夹。 这是防止文件名冲突的一个好主意,因为你必须包括以下文件:
|
|
如果你为其他用户安装库,则默认安装位置为/usr/local/include/sublib1/sublib1.h。
12.1 添加子目录
CMakeLists.txt文件可以包含和调用包含CMakeLists.txt文件的子目录。
|
|
12.2 引用子项目目录
使用project()
命令创建项目时,CMake将自动创建许多变量,这些变量可用于引用有关该项目的详细信息。 这些变量然后可以由其他子项目或主项目使用。例如,要引用你可以使用的其他项目的源目录。
|
|
CMake中有一些变量会自动创建:
Variable | Info |
---|---|
PROJECT_NAME | 当前project()设置的项目的名称。 |
CMAKE_PROJECT_NAME | 由project()命令设置的第一个项目的名称,即顶层项目。 |
PROJECT_SOURCE_DIR | 当前项目的源文件目录。 |
PROJECT_BINARY_DIR | 当前项目的构建目录。 |
name_SOURCE_DIR | 在此示例中,创建的源目录为 sublibrary1_SOURCE_DIR , sublibrary2_SOURCE_DIR , and subbinary_SOURCE_DIR |
name_BINARY_DIR | 本工程的二进制目录是sublibrary1_BINARY_DIR , sublibrary2_BINARY_DIR 和 subbinary_BINARY_DIR |
12.3 Header only Libraries
如果你有一个库被创建为仅头文件的库,则cmake支持INTERFACE目标,以允许创建没有任何构建输出的目标。 可以从here找到更多详细信息
|
|
创建目标时,你还可以使用INTERFACE范围包含该目标的目录。 INTERFACE范围用于制定在链接此目标的任何库中使用的目标需求,但在目标本身的编译中不使用。
|
|
12.4 引用子项目中的库
如果子项目创建了一个库,则其他项目可以通过在target_link_libraries()命令中调用该项目的名称来引用该库。 这意味着你不必引用新库的完整路径,而是将其添加为依赖项。
|
|
或者,你可以创建一个别名目标,该目标允许你在上下文(其实就是某个目标的绰号)中引用该目标。
|
|
To reference the alias, just it as follows:
|
|
12.5 包含子项目中的目录
从cmake v3开始从子项目添加库时,无需将项目include目录添加到二进制文件的include目录中。
创建库时,这由target_include_directories()
命令中的作用域控制。 在此示例中,因为子二进制可执行文件链接了sublibrary1
和sublibrary2
库,所以当它们与库的PUBLIC
和INTERFACE
范围一起导出时,它将自动包含${sublibrary1_SOURCE_DIR}/inc
和${sublibrary2_SOURCE_DIR}/inc
文件夹。(这个地方设及到了PUBLIC
和INTERFACE
的使用)