CMake 的基本操作
CMake 编译可执行文件
一个打印 hello world 的 cpp 文件,通过 CMake 将它编译成可执行文件。
在 cpp 的同一目录下创建 CMakeLists.txt 文件,内容如下:
1 | # 指定 CMake 使用版本 |
其中,通过 cmake_minimum_required 方法指定 CMake 使用版本,通过 project 指定工程名。 而 add_executable 就是指定最后编译的可执行文件名称和需要编译的 cpp 文件,如果工程很大,有多个 cpp 文件,那么都要把它们添加进来。 定义了 CMake 文件之后,就可以开始编译构建了。 CMake 在构建工程时会生成许多临时文件,避免让这些临时文件污染代码,一般会把它们放到一个单独的目录中。 操作步骤如下:
1 | # 在 cpp 目录下创建 build 目录 |
在 build 目录中可以找到最终生成的可执行文件。
这就是 CMake 的一个简单操作,将 cpp 编译成可执行文件,但在 Android 中,大多数场景都是把 cpp 编译成库文件。
CMake 编译静态库和动态库
同样还是一个 cpp 文件和一个 CMake 文件,cpp 文件内容为打印字符串的函数:
1 | #include <iostream> |
同时,CMake 文件也要做相应更改:
1 | cmake_minimum_required(VERSION 3.12) |
通过 add_library 指定要编译的库的名称,以及动态库还是静态库,还有要编译的文件。
最后同样地执行构建,在 build 目录下可以看到生成的库文件。
到这里,就基本可以使用 CMake 来构建 C/C++ 工程了。
CMake 基本语法
熟悉了上面的基本操作之后,就必然会遇到以下的问题了:
如果要参与编译的 C/C++ 文件很多,难道每个都要手动添加嘛? 可以把编译好的可执行文件或者库自动放到指定位置嘛? 可以把编译好的库指定版本号嘛?
带着这些问题,还是要继续深入学习 CMake 的相关语法,最好的学习材料就是 官网文档 了。 为了避免直接看官方文档时一头雾水,这里列举一些常用的语法命令。
PROJECT(工程名字)
这条指令会自动创建两个变量:
<projectname>_BINARY_DIR
(二进制文件保存路径)<projectname>_SOURCE_DIR
(源代码路径)
cmake系统也帮助我们预定义了PROJECT_BINARY_DIR和PROJECT_SOURCE_DIR其值与上述对应相等
注释与大小写
在前面就已经用到了 CMake 注释了,每一行的开头 # 代表注释。
另外,CMake 的所有语法指令是不区分大小写的。
变量定义与消息打印
通过 set 来定义变量:
1 | # 变量名为 var,值为 hello |
当需要引用变量时,在变量名外面加上 ${} 符合来引用变量
1 | # 引用 var 变量 |
还可以通过 message 在命令行中输出打印内容。
1 | set(var hello) |
在IF等语句中,是直接使用变量名而不通过${}取值。
数学和字符串操作
数学操作
CMake 中通过 math 来实现数学操作。
1 | # math 使用,EXPR 为大小 |
1 | math(EXPR var "1+1") |
math 支持 +, -, *, /, %, |, &, ^, ~, <<, >>
等操作,和 C 语言中大致相同。
字符串操作
CMake 通过 string 来实现字符串的操作,这波操作有很多,包括将字符串全部大写、全部小写、求字符串长度、查找与替换等操作。
1 | set(var "this is string") |
另外,通过空白或者分隔符号可以表示字符串序列。
1 | set(foo this is a list) // 实际内容为字符串序列 |
当字符串中需要用到空白或者分隔符时,再用双括号””表示为同一个字符串内容。
1 | set(foo "this is a list") // 实际内容为一个字符串 |
文件操作
Make 中通过 file 来实现文件操作,包括文件读写、下载文件、文件重命名等。
1 | # 文件重命名 |
在文件的操作中,还有两个很重要的指令 GLOB 和 GLOB_RECURSE 。
1 | # GLOB 的使用 |
其中,GLOB 指令会将所有匹配 *.cpp 表达式的文件组成一个列表,并保存在 ROOT_SOURCE 变量中。 而 GLOB_RECURSE 指令和 GLOB 类似,但是它会遍历匹配目录的所有文件以及子目录下面的文件。 使用 GLOB 和 GLOB_RECURSE 有好处,就是当添加需要编译的文件时,不用再一个一个手动添加了,同一目录下的内容都被包含在对应变量中了,但也有弊端,就是新建了文件,但是 CMake 并没有改变,导致在编译时也会重新产生构建文件,要解决这个问题,就是动一动 CMake,让编译器检测到它有改变就好了。
INSTALL指令
安装的需要有两种,一种是从代码编译后直接make install安装,一种是打包时的指定目录安装。
这里需要引入一个新的cmake 指令 INSTALL和一个非常有用的变量CMAKE_INSTALL_PREFIX。
CMAKE_INSTALL_PREFIX变量类似于configure脚本的 –prefix,常见的使用方法看起来是这个样子:
cmake -DCMAKE_INSTALL_PREFIX=/usr .
INSTALL指令用于定义安装规则,安装的内容可以包括目标二进制、动态库、静态库以及文件、目录、脚本等。INSTALL指令包含了各种安装类型,我们需要一个个分开解释:
目标文件的安装
1 | INSTALL(TARGETS targets... |
参数中的TARGETS后面跟的就是我们通过ADD_EXECUTABLE或者ADD_LIBRARY定义的目标文件,可能是可执行二进制、动态库、静态库。目标类型也就相对应的有三种,ARCHIVE特指静态库,LIBRARY特指动态库,RUNTIME特指可执行目标二进制。
DESTINATION定义了安装的路径,如果路径以/开头,那么指的是绝对路径,这时候CMAKE_INSTALL_PREFIX其实就无效了。如果你希望使用CMAKE_INSTALL_PREFIX来定义安装路径,就要写成相对路径,即不要以/开头,那么安装后的路径就是 ${CMAKE_INSTALL_PREFIX}/<DESTINATION定义的路径>
举个简单的例子:
1 | INSTALL(TARGETS myrun mylib mystaticlib |
上面的例子会将:
- 可执行二进制myrun安装到${CMAKE_INSTALL_PREFIX}/bin目录
- 动态库libmylib安装到${CMAKE_INSTALL_PREFIX}/lib目录
- 静态库libmystaticlib安装到${CMAKE_INSTALL_PREFIX}/libstatic目录
特别注意的是你不需要关心TARGETS具体生成的路径,只需要写上TARGETS名称就可以
了。
普通文件的安装
1
2
3
4
5INSTALL(FILES files... DESTINATION <dir>
[PERMISSIONS permissions...]
[CONFIGURATIONS [Debug|Release|...]]
[COMPONENT <component>]
[RENAME <name>] [OPTIONAL])
可用于安装一般文件,并可以指定访问权限,文件名是此指令所在路径下的相对路径。 如果默认不定义权限PERMISSIONS,安装后的权限为,OWNER_WRITE,OWNER_READ, GROUP_READ,和WORLD_READ,即644权限。
非目标文件的可执行程序安装(比如脚本之类)
1 | INSTALL(PROGRAMS files... DESTINATION <dir> |
跟上面的FILES指令使用方法一样,唯一的不同是安装后权限为: OWNER_EXECUTE, GROUP_EXECUTE, 和WORLD_EXECUTE,即755权限
目录的安装
1 | INSTALL(DIRECTORY dirs... DESTINATION <dir> |
这里主要介绍其中的DIRECTORY、PATTERN以及PERMISSIONS参数。
- DIRECTORY后面连接的是所在Source目录的相对路径,但务必注意: abc和abc/有很大的区别。 abc意味着abc这个目录会安装在目标路径下; abc/意味着abc这个目录的内容会被安装在目标路径下; 如果目录名不以/结尾,那么这个目录将被安装为目标路径下的abc,如果目录名以/结尾, 代表将这个目录中的内容安装到目标路径,但不包括这个目录本身。
- PATTERN用于使用正则表达式进行过滤,
- PERMISSIONS用于指定PATTERN过滤后的文件权限。
我们来看一个例子:
1 | INSTALL(DIRECTORY icons scripts/ DESTINATION share/myproj |
这条指令的执行结果是:
将icons目录安装到 <prefix>/share/myproj
,将scripts/
中的内容安装到 <prefix>/share/myproj
不包含目录名为CVS的目录,对于scripts/*文件指定权限为 OWNER_EXECUTE
OWNER_WRITE OWNER_READ GROUP_EXECUTE GROUP_READ.
安装时cmake脚本的执行
1 | INSTALL([[SCRIPT <file>] [CODE <code>]] [...]) |
把相对路径转换成绝对路径
1 | get_filename_component(<VAR> <FileName> |
Set to the absolute path of
- ABSOLUTE = Full path to file REALPATH = Full path to existing file with symlinks resolved
- If the provided
is a relative path, it is evaluated relative to the given base directory . If no base directory is provided, the default base directory will be CMAKE_CURRENT_SOURCE_DIR. - Paths are returned with forward slashes and have no trailing slashes. If the optional CACHE argument is specified, the result variable is added to the cache.
To convert an absolute path to a file into a relative paths, you might use the file command:
1 | file(RELATIVE_PATH <variable> <directory> <file>) |
Compute the relative path from a <directory>
to a <file>
and store it in the <variable>
.
1 | file(RELATIVE_PATH buildDirRelFilePath "${CMAKE_BINARY_DIR}" "${myFile}") |
预定义的常量
在 CMake 中有许多预定义的常量,使用好这些常量能起到事半功倍的效果。
CMAKE_BINARY_DIR
PROJECT_BINARY_DIR
< projectname >_BINARY_DIR 这三个变量指代的内容是一致的,如果是in-source编译,指得就是工程顶层目录;如果是out-of-source编译,指的是工程编译发生的目录。PROJECT_BINARY_DIR跟其它指令稍有区别,目前可以认为它们是一致的。
CMAKE_SOURCE_DIR
PROJECT_SOURCE_DIR
< projectname >_SOURCE_DIR 这三个变量指代的内容是一致的,不论采用何种编译方式,都是工程顶层目录。也就是在in-source编译时,他跟CMAKE_BINARY_DIR等变量一致。PROJECT_SOURCE_DIR跟其它指令稍有区别,目前可以认为它们是一致的。 (out-of-source build与in-source build相对,指是否在CMakeLists.txt所在目录进行编译。)
CMAKE_CURRENT_SOURCE_DIR : 前处理的CMakeLists.txt所在的路径,比如上面我们提到的src子目录。
CMAKE_CURRRENT_BINARY_DIR 如果是in-source编译,它跟CMAKE_CURRENT_SOURCE_DIR一致;如果是out-of-source编译,指的是target编译目录。使用ADD_SUBDIRECTORY(src bin)可以更改这个变量的值。使用SET(EXECUTABLE_OUTPUT_PATH <新路径>)并不会对这个变量造成影响,它仅仅修改了最终目标文件存放的路径。
CMAKE_CURRENT_LIST_FILE : 输出调用这个变量的CMakeLists.txt的完整路径
CMAKE_CURRENT_LIST_LINE : 输出这个变量所在的行
CMAKE_MODULE_PATH : 这个变量用来定义自己的cmake模块所在的路径。如果工程比较复杂,有可能会自己编写一些cmake模块,这些cmake模块是随工程发布的,为了让cmake在处理CMakeLists.txt时找到这些模块,你需要通过SET指令将cmake模块路径设置一下。比如SET(CMAKE_MODULE_PATH,${PROJECT_SOURCE_DIR}/cmake) 这时候就可以通过INCLUDE指令来调用自己的模块了。
EXECUTABLE_OUTPUT_PATH : 新定义最终结果的存放目录
LIBRARY_OUTPUT_PATH : 新定义最终结果的存放目录
PROJECT_SOURCE_DIR : 指当前工程的路径
PROJECT_NAME : 返回通过PROJECT指令定义的项目名称
比如,在 add_library 中需要指定 cpp 文件的路径,以 CMAKE_CURRENT_SOURCE_DIR 为基准,指定 cpp 相对它的路径就好了。
1 | # 利用预定义的常量来指定文件路径 |
平台相关的常量
CMake 能够用来在 Window、Linux、Mac 平台下进行编译,在它的内部也定义了和这些平台相关的变量。
列举一些常见的:
- WIN32:如果编译的目标系统是 Window,那么 WIN32 为 True 。
- UNIX:如果编译的目标系统是 Unix 或者类 Unix 也就是 Linux ,那么 UNIX 为 True 。
- MSVC:如果编译器是 Window 上的 Visual C++ 之类的,那么 MSVC 为 True 。
- ANDROID:如果目标系统是 Android ,那么 ANDROID 为 1 。
- APPLE:如果目标系统是 APPLE ,那么 APPLE 为 1 。
有了这些常量做区分,就可以在一份 CMake 文件中编写不同平台的编译选项。
1 | if(WIN32){ |
toolChain脚本中设置的几个重要变量
- CMAKE_SYSTEM_NAME: 即你目标机target所在的操作系统名称,比如ARM或者Linux你就需要写”Linux”,如果Windows平台你就写”Windows”,如果你的嵌入式平台没有相关OS你即需要写成”Generic”,只有当CMAKE_SYSTEM_NAME这个变量被设置了,CMake才认为此时正在交叉编译,它会额外设置一个变量CMAKE_CROSSCOMPILING为TRUE.
- CMAKE_C_COMPILER: 顾名思义,即C语言编译器,这里可以将变量设置成完整路径或者文件名,设置成完整路径有一个好处就是CMake会去这个路径下去寻找编译相关的其他工具比如linker,binutils等,如果你写的文件名带有arm-elf等等前缀,CMake会识别到并且去寻找相关的交叉编译器。
- CMAKE_CXX_COMPILER: 同上,此时代表的是C++编译器。
- CMAKE_FIND_ROOT_PATH: 指定了一个或者多个优先于其他搜索路径的搜索路径。比如你设置了/opt/arm/,所有的Find_xxx.cmake都会优先根据这个路径下的/usr/lib,/lib等进行查找,然后才会去你自己的/usr/lib和/lib进行查找,如果你有一些库是不被包含在/opt/arm里面的,你也可以显示指定多个值给CMAKE_FIND_ROOT_PATH,比如
set(CMAKE_FIND_ROOT_PATH /opt/arm /opt/inst)
该变量能够有效地重新定位在给定位置下进行搜索的根路径。该变量默认为空。当使用交叉编译时,该变量十分有用:用该变量指向目标环境的根目录,然后CMake将会在那里查找。 - CMAKE_FIND_ROOT_PATH_MODE_PROGRAM: 对FIND_PROGRAM()起作用,有三种取值,NEVER,ONLY,BOTH,第一个表示不在你CMAKE_FIND_ROOT_PATH下进行查找,第二个表示只在这个路径下查找,第三个表示先查找这个路径,再查找全局路径,对于这个变量来说,一般都是调用宿主机的程序,所以一般都设置成NEVER
- CMAKE_FIND_ROOT_PATH_MODE_LIBRARY: 对FIND_LIBRARY()起作用,表示在链接的时候的库的相关选项,因此这里需要设置成ONLY来保证我们的库是在交叉环境中找的.
- CMAKE_FIND_ROOT_PATH_MODE_INCLUDE: 对FIND_PATH()和FIND_FILE()起作用,一般来说也是ONLY,如果你想改变,一般也是在相关的FIND命令中增加option来改变局部设置,有NO_CMAKE_FIND_ROOT_PATH,ONLY_CMAKE_FIND_ROOT_PATH,BOTH_CMAKE_FIND_ROOT_PATH
- BOOST_ROOT:对于需要boost库的用户来说,相关的boost库路径配置也需要设置,因此这里的路径即ARM下的boost路径,里面有include和lib。
- QT_QMAKE_EXECUTABLE: 对于Qt用户来说,需要更改相关的qmake命令切换成嵌入式版本,因此这里需要指定成相应的qmake路径(指定到qmake本身)
系统信息
- CMAKE_MAJOR_VERSION,CMAKE主版本号,比如2.4.6中的2
- CMAKE_MINOR_VERSION,CMAKE次版本号,比如2.4.6中的4
- CMAKE_PATCH_VERSION,CMAKE补丁等级,比如2.4.6 中的6
- CMAKE_SYSTEM,系统名称,比如Linux-2.6.22
- CMAKE_SYSTEM_NAME,不包含版本的系统名,比如Linux
- CMAKE_SYSTEM_VERSION,系统版本,比如2.6.22
- CMAKE_SYSTEM_PROCESSOR,处理器名称,比如i686.
- UNIX,在所有的类UNIX平台为TRUE,包括OS X和cygwin
- WIN32,在所有的win32平台为TRUE,包括cygwin
主要的开关选项:
- CMAKE_ALLOW_LOOSE_LOOP_CONSTRUCTS,用来控制IF ELSE语句的书写方式
- BUILD_SHARED_LIBS 这个开关用来控制默认的库编译方式,如果不进行设置,使用ADD_LIBRARY并没有指定库类型的情况下,默认编译生成的库都是静态库。如果SET(BUILD_SHARED_LIBS ON)后,默认生成的为动态库。
- CMAKE_C_FLAGS 设置C编译选项,也可以通过指令ADD_DEFINITIONS()添加。
- CMAKE_CXX_FLAGS 设置C++编译选项,也可以通过指令ADD_DEFINITIONS()添加。
函数、宏、流程控制和选项 等命令
具体参考cmake-commands ,这里面包括了很多重要且常见的指令。
简单示例 CMake 中的函数操作:
1 | function(add a b) |
其中,function 为定义函数,第一个参数为函数名称,后面为函数参数。
在调用函数时,参数之间用空格隔开,不要用逗号。
宏的使用与函数使用有点类似:
1 | macro(del a b) |
在流程控制方面,CMake 也提供了 if、else 这样的操作:
1 | set(num 0) |
其中,CMake 提供了 AND、OR、NOT、LESS、EQUAL 等等这样的操作来对数据进行判断,比如 AND 就是要求两边同为 True 才行。
另外 CMake 还提供了循环迭代的操作:
1 | set(stringList this is string list) |
CMake 还提供了一个 option 指令。
可以通过它来给 CMake 定义一些全局选项:
1 | option(ENABLE_SHARED "Build shared libraries" TRUE) |
可能会觉得 option 无非就是一个 True or False 的标志位,可以用变量来代替,但使用变量的话,还得添加 ${} 来表示变量,而使用 option 直接引用名称就好了。
CMake 阅读实践
明白了上述的 CMake 语法以及从官网去查找陌生的指令意思,就基本上可以看懂大部分的 CMake 文件了。 这里举两个开源库的例子:
- glm 是一个用来实现矩阵计算的,在 OpenGL 的开发中会用到。
- CMakeLists.txt 地址在 这里
- libjpeg-turbo 是用来进行图片压缩的,在 Android 底层就是用的它。
- CMakeLists.txt 地址在 这里
这两个例子中大量用到了前面所讲的内容,可以试着读一读增加熟练度。
为编译的库设置属性
接下来再回到用 CMake 编译动态库的话题上,毕竟 Android NDK 开发也主要是用来编译库了,当编译完 so 之后,我们可以对它做一些操作。
通过 set_target_properties 来给编译的库设定相关属性内容,函数原型如下:
1 | set_target_properties(target1 target2 ... |
比如,要将编译的库改个名称:
1 | set_target_properties(native-lib PROPERTIES OUTPUT_NAME "testlib" ) |
更多的属性内容可以参考 官方文档。
不过,这里面有一些属性设定无效,在 Android Studio 上试了无效,在 CLion 上反而可以,当然也可能是我使用姿势不对。
比如,实现动态库的版本号:
1 | set_target_properties(native-lib PROPERTIES VERSION 1.2 SOVERSION 1 ) |
对于已经编译好的动态库,想要把它导入进来,也需要用到一个属性。
比如编译的 FFmpeg 动态库,
1 | # 使用 IMPORTED 表示导入库 |
链接到其他的库
如果编译了多个库,并且想库与库之间进行链接,那么就要通过 target_link_libraries 。
1 | target_link_libraries( native-lib |
在 Android 底层也提供了一些 so 库供上层链接使用,也要通过上面的方式来链接,比如最常见的就是 log 库打印日志。 如果要链接自己编译的多个库文件,首先要保证每个库的代码都对应一个 CMakeLists.txt 文件,这个 CMakeLists.txt 文件指定当前要编译的库的信息。 然后在当前库的 CMakeLists.txt 文件中通过 ADD_SUBDIRECTORY 将其他库的目录添加进来,这样才能够链接到。
1 | ADD_SUBDIRECTORY(src/main/cpp/turbojpeg) |
ADD_SUBDIRECTORY(src_dir [binary_dir] [EXCLUDE_FROM_ALL])
向当前工程添加存放源文件的子目录,并可以指定中间二进制和目标二进制的存放位置
EXCLUDE_FROM_ALL含义:将这个目录从编译过程中排除
SET(EXECUTABLE_OUTPUT_PATH${PROJECT_BINARY_DIR}/bin)更改生成的可执行文件路径
SET(LIBRARY_OUTPUT_PATH${PROJECT_BINARY_DIR}/lib)更改生成的库文件路径
添加头文件
在使用的时候有一个容易忽略的步骤就是添加头文件,通过 include_directories 指令把头文件目录包含进来。
这样就可以直接使用 #include "header.h"
的方式包含头文件,而不用 #include "path/path/header.h"
这样添加路径的方式来包含。
区分debug、release版本
建立debug/release两目录,分别在其中执行cmake -D CMAKE_BUILD_TYPE=Debug(或Release),需要编译不同版本时进入不同目录执行make即可:
1 | Debug版会使用参数-g; |
另一种设置方法——例如DEBUG版设置编译参数DDEBUG
1 | IF(DEBUG_mode) |
在执行cmake时增加参数即可,例如cmake -D DEBUG_mode=ON
设置条件编译
例如debug版设置编译选项DEBUG,并且更改不应改变CMakelist.txt 使用option command,eg:
1 | option(DEBUG_mode"ON for debug or OFF for release" ON) |
使其生效的方法:首先cmake生成makefile,然后make edit_cache编辑编译选项;Linux下会打开一个文本框,可以更改,改完后再make生成目标文件——emacs不支持make edit_cache; 局限:这种方法不能直接设置生成的makefile,而是必须使用命令在make前设置参数;对于debug、release版本,相当于需要两个目录,分别先cmake一次,然后分别makeedit_cache一次; 期望的效果:在执行cmake时直接通过参数指定一个开关项,生成相应的makefile。