CMAKE
CMAKE 💯 #
基础 #
简介 #
gccgcc是用来编译c文件的编译器。简单来说就是给他输入一个(或几个)c文件,它可以输出一个可执行文件。当你的程序只有一个源文件时,直接就可以用gcc命令编译它。但是当你的程序包含很多个源文件时,用gcc命令逐个去编译时,容易混乱而且工作量大。
make因此出现了make工具。它本身没有编译和链接功能。但是它可以按照
Makefile
文件中的命令调用gcc来编译工程。有了make,我们就不用一个一个文件去编译,只要把他们的编译指令都写在Makefile中,然后make就可以一键完成。cmake为了进一步简化Makefile的书写,出现了
cmake
。cmake根据CMakeLists.txt文件生成项目的makefile,而且写法也大大简化。总的来说,CMake,make,gcc组成一个工具链,让我们在构建项目时只需要编写源文件以及CmakeLists.txt即可。它的流程如下:graph LR A(编写CMakeLists.txt)-->B((Cmake)) B-->C(Makefile等配置文件) C-->D((make)) D-->F((gcc)) E(源文件)-->F F-->G(可执行程序)基本规则 #
变量 #
使用
${}
方式取值,变量名区分大小写(指令名不区分大小写)定义一个变量可以用SET指令:这条语句就是定一个一个名字叫
valname
的变量,它的值是main.c src1.c src2.c
;相当于是valname=main.c src1.c src2.c
系统预定义的变量
在我们使用PROJECT指令指定工程名后,CMake会自动生成两个变量;可以在 CMakeCache.txt 中看到定义。
- ${pname}_BINARY_DIR: 可执行程序目录,比如
step1_BINARY_DIR
,其实就是执行cmake命令时的当前目录,比如在build/外部编译,那么它就是build的绝对路径 - ${pname}_SOURCE_DIR: 源文件目录,比如
step1_SOURCE_DIR
,就是主CmakeLists.txt所在的绝对路径。
我们可以使用他们。同时系统还会帮我们预定义两个与工程名无关的变量,他们的内容与上述两个变量相同,但是好处是变量名不会因为工程名改变而改变,因此推荐使用:
- PROJECT_BINARY_DIR : 可执行程序目录
- PROJECT_SOURCE_DIR : 源文件目录
指令 #
指令就是CMake里的函数。它的名称不区分大小写,调用的格式如下: * 指令名(参数1,参数2,参数3) 参数之间用空格或者分号隔开。 如果参数中包含括号,可以使用双引号将参数分开,如下所示:
使用 #
CMake命令用于生成中间文件,使用时要传入CmakeLists.txt文件所在的目录。如果在主目录下直接生成配置文件,内部构建,这样做的坏处是使代码文件与中间文件相互混淆,因此最好的做法时外部构件。
如下所示:在主目录下- 工程目录结构
最终安装工程时,如下操作:
具体指令 #
最低CMake版本(每个CmakeLists.txt文件都必须有)
CMAKE_MINIMUM_PREQUIRED(VERSION 2.6)
2. > [?] 指定工程名PROJECT(PRJNAME)
3. > [?] 输出提示信息MESSAGE(STATUS "This is binary dir" ${PROJECT_BINARY_DIR})
第一个参数表示输出的信息类型,有三中选项: * STATUS: 输出前缀为–的信息。 * SEND_ERROR: 产生错误,生成过程中被跳过 * FATAL_ERROR: 重要错误,CMake进城终止。
添加要编译的可执行程序
ADD_EXECUTABLE(hello main.c src1.c)
# hello为一个target 等效于在makefile里写指定子目录
由于子目录src下也有CMakeLists.txt,是用来编译源代码的,如果不在主目录下的cmakelists里面声明子目录,就不会包含这部分代码,因此主目录的文件中需要添加:
ADD_SUBDIRECTORY(src bin exclude)
* src: 将源文件存放的src目录加入工程,相对于主目录,也既PROJECT_SOURCE_DIR
* bin: 将编译输出到bin,若不指定则输出到src中。此处的bin是相对于编译时所在目录,也即PROJECT_BINARY_DIR
* exclude: 编译时排出的目录注意:采用外部编译时,例如在build目录下面
camke ..
,那么输出目录就会以build/
作为相对路径的参考,因此会在build/下生成一个bin/目录来存放二进制文件,若没有指定bin,则会生成build/src目录。更改二进制输出目录
除了用命令指定二进制文件输出目录,还可以通过更改路径变量来指定目录。指定可执行文件或共享库输出目录的变量分别是 * EXECUTABLE_OUTPUT_PATH * LIBRARY_OUTPUT_PATH
使用SET命令来更改他们:
指定安装目录
(1)makefile中如何写安装目录
常见到命令行中运行 ./configure -prefix=/usr,其实就是修改
PREFIX
(2)Cmake中如何写安装目录
CMAKE_INSTALL_PREFIX:安装目录的默认前缀,作用类似于 PREFIX,常见的用法如:
cmake -CMAKE_INSTALL_PREFIX=/usr
,只要安装目录不是以/开头,则会默认是以该目录为前缀。如果未定义,则默认为/usr/local
。在主目录的CMakeLists.txt中添加SET语句来设置它,也可以执行cmake时设置。INSTALL:指令用于定义安装规则,安装的内容包括:二进制,动态库,静态库及文件,目录,脚本等,使的格式为:
- 安装二进制文件
例子:
- 普通文件的安装(其实就是mv+chmod)
- 非目标文件的可执行程序(脚本)安装
- 目录的安装
例子:
- 安装时CMake脚本的执行
常用变量 #
一、cmake变量的引用方式 #
使用\({}进行变量的引用。在 IF 等语句中,是直接使用变量名而不通过\){}取值
二、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 跟其他指令稍有区别,现在,你可以理解为他们是一致的。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 模块路径设置一下。比如这时候你就可以通过 INCLUDE 指令来调用自己的模块了。
EXECUTABLE_OUTPUT_PATH | LIBRARY_OUTPUT_PATH
分别用来重新定义最终结果的存放目录,前面我们已经提到了这两个变量。PROJECT_NAME
返回通过 PROJECT 指令定义的项目名称。三、cmake调用环境变量的方式 #
使用
$ENV{NAME}
指令就可以调用系统的环境变量了。设置环境变量的方式是:SET(ENV{变量名} 值)
1. CMAKE_INCLUDE_CURRENT_DIR 自动添加 CMAKE_CURRENT_BINARY_DIR 和 CMAKE_CURRENT_SOURCE_DIR 到当前处理的 CMakeLists.txt。相当于在每个 CMakeLists.txt 加入:shell INCLUDE_DIRECTORIES(${CMAKE_CURRENT_BINARY_DIR} ${CMAKE_CURRENT_SOURCE_DIR})
2. CMAKE_INCLUDE_DIRECTORIES_PROJECT_BEFORE 将工程提供的头文件目录始终至于系统头文件目录的前面,当你定义的头文件确实跟系统发生冲突时可以提供一些帮助。 3. CMAKE_INCLUDE_PATH 和 CMAKE_LIBRARY_PATH 我们在上一节已经提及。四、系统信息 #
- 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()添加。
常用指令 #
一、基本指令 #
1.
ADD_DEFINITIONS
向 C/C++编译器添加-D 定义,比如:如果你的代码中定义了这个,则
#ifdef ENABLE_DEBUG #endif
代码块就会生效。如果要添加其他的编译器开关,可以通过 CMAKE_C_FLAGS 变量和 CMAKE_CXX_FLAGS 变量设置。2.
ADD_DEPENDENCIES
定义 target 依赖的其他 target,确保在编译本 target 之前,其他的 target 已经被构建。3.
ADD_EXECUTABLE、ADD_LIBRARY、ADD_SUBDIRECTORY
添加可执行程序,库和子目录4.
ADD_TEST 与 ENABLE_TESTING
ENABLE_TESTING
指令用来控制 Makefile 是否构建 test 目标,涉及工程所有目录。语法很简单,没有任何参数ENABLE_TESTING()
,一般情况这个指令放在工程的主CMakeLists.txt 中。ADD_TEST
指令的语法是:testname 是自定义的 test 名称,Exename 可以是构建的目标文件也可以是外部脚本等等。后面连接传递给可执行文件的参数。如果没有在同一个 CMakeLists.txt 中打开ENABLE_TESTING()指令,任何 ADD_TEST 都是无效的。比如我们前面的 Helloworld 例子,可以在工程主 CMakeLists.txt 中添加ADD_TEST(mytest ${PROJECT_BINARY_DIR}/bin/main)ENABLE_TESTING()生成 Makefile 后,就可以运行 make test 来执行测试了。
5.
AUX_SOURCE_DIRECTORY
基本语法是:作用是发现一个目录下所有的源代码文件并将列表存储在一个变量中,这个指令临时被用来自动构建源文件列表,因为目前 cmake 还不能自动发现新添加的源文件。比如
你也可以通过后面提到的 FOREACH 指令来处理这个 LIST
6.
CMAKE_MINIMUM_REQUIRED
其语法为 CMAKE_MINIMUM_REQUIRED(VERSION versionNumber [FATAL_ERROR])比如 CMAKE_MINIMUM_REQUIRED(VERSION 2.5 FATAL_ERROR)如果 cmake 版本小与 2.5,则出现严重错误,整个过程中止。7.
EXEC_PROGRAM
在 CMakeLists.txt 处理过程中执行命令,并不会在生成的 Makefile 中执行。具体语法为:用于在指定的目录运行某个程序,通过 ARGS 添加参数,如果要获取输出和返回值,可通过OUTPUT_VARIABLE 和 RETURN_VALUE 分别定义两个变量.这个指令可以帮助你在 CMakeLists.txt 处理过程中支持任何命令,比如根据系统情况去修改代码文件等等。举个简单的例子,我们要在 src 目录执行 ls 命令,并把结果和返回值存下来。可以直接在 src/CMakeLists.txt 中添加:
在 cmake 生成 Makefile 的过程中,就会执行 ls 命令,如果返回 0,则说明成功执行,那么就输出 ls *.c 的结果。关于 IF 语句,后面的控制指令会提到。
8.
FILE 指令
文件操作指令,基本语法为:这里的语法都比较简单,不在展开介绍了。
9.
INCLUDE 指令
用来载入 CMakeLists.txt 文件,也用于载入预定义的 cmake 模块.OPTIONAL 参数的作用是文件不存在也不会产生错误。你可以指定载入一个文件,如果定义的是一个模块,那么将在 CMAKE_MODULE_PATH 中搜索这个模块并载入。载入的内容将在处理到 INCLUDE 语句时直接执行。
10.
FIND_<\>指令
FIND_系列指令主要包含以下指令:用来调用预定义在 CMAKE_MODULE_PATH 下的 Find<name>.cmake 模块,你也可以自己定义 Find<name>模块,通过 SET(CMAKE_MODULE_PATH dir)将其放入工程的某个目录中供工程使用,我们在后面的章节会详细介绍 FIND_PACKAGE 的使用方法和 Find 模块的编写。FIND_LIBRARY 示例:
二、控制指令: #
1.
IF 指令
- ##### (1) 基本语法为:shell IF(expression_r_r) # THEN section. COMMAND1(ARGS ...) COMMAND2(ARGS ...) ... ELSE(expression_r_r) # ELSE section. COMMAND1(ARGS ...) COMMAND2(ARGS ...) ... ENDIF(expression_r_r)
另外一个指令是ELSEIF
,总体把握一个原则,凡是出现 IF 的地方一定要有对应的ENDIF.出现 ELSEIF 的地方,ENDIF 是可选的。- ##### (2) 表达式的使用方法如下: ```shell IF(var) # 如果变量不是:空,0,N, NO, OFF, FALSE, NOTFOUND 或<var>_NOTFOUND 时,表达式为真。 IF(NOT var) # 与上述条件相反。 IF(var1 AND var2) # 当两个变量都为真是为真。 IF(var1 OR var2) # 当两个变量其中一个为真时为真。 IF(COMMAND cmd) # 当给定的 cmd 确实是命令并可以调用是为真。 IF(EXISTS dir)、IF(EXISTS file) # 当目录名或者文件名存在时为真。 IF(file1 IS_NEWER_THAN file2) # 当 file1 比 file2 新,或者 file1/file2 其中有一个不存在时为真,文件名请使用完整路径。 IF(IS_DIRECTORY dirname) # 当 dirname 是目录时,为真。 IF(DEFINED variable) # 如果变量被定义,为真。 ``` 正则表达式 ```shell IF(variable MATCHES regex)、IF(string MATCHES regex) # 当给定的变量或者字符串能够匹配正则表达式 regex 时为真。比如: IF("hello" MATCHES "ell") MESSAGE("true") ENDIF("hello" MATCHES "ell") ``` 数字比较表达式 ```shell IF(variable LESS number) IF(string LESS number) IF(variable GREATER number) IF(string GREATER number) IF(variable EQUAL number) IF(string EQUAL number) ``` 按照字母序的排列进行比较 ```shell IF(variable STRLESS string) IF(string STRLESS string) IF(variable STRGREATER string) IF(string STRGREATER string) IF(variable STREQUAL string) IF(string STREQUAL string) ``` 一个小例子,用来判断平台差异: ```shell IF(WIN32) MESSAGE(STATUS “This is windows.”) #作一些 Windows 相关的操作 ELSE(WIN32) MESSAGE(STATUS “This is not windows”) #作一些非 Windows 相关的操作 ENDIF(WIN32) ### 上述代码用来控制在不同的平台进行不同的控制,但是,阅读起来却并不是那么舒服, ## ELSE(WIN32)之类的语句很容易引起歧义。 ## 这就用到了我们在“常用变量”一节提到的 CMAKE_ALLOW_LOOSE_LOOP_CONSTRUCTS 开关: SET(CMAKE_ALLOW_LOOSE_LOOP_CONSTRUCTS ON) # 这时候就可以写成: IF(WIN32) ELSE() ENDIF() ### 如果配合 ELSEIF 使用,可能的写法是这样: IF(WIN32) #do something related to WIN32 ELSEIF(UNIX) #do something related to UNIX ELSEIF(APPLE) #do something related to APPLE ENDIF(WIN32) ```
2.
WHILE指令
WHILE 指令的语法是:其真假判断条件可以参考 IF 指令。
3.
FOREACH指令
FOREACH 指令的使用方法有三种形式: - ##### (1)列表shell FOREACH(loop_var arg1 arg2 ...) COMMAND1(ARGS ...) COMMAND2(ARGS ...) ... ENDFOREACH(loop_var)
像我们前面使用的 AUX_SOURCE_DIRECTORY 的例子shell AUX_SOURCE_DIRECTORY(. SRC_LIST) FOREACH(F ${SRC_LIST}) MESSAGE(${F}) ENDFOREACH(F)
- ##### (2)范围shell FOREACH(loop_var RANGE total) ... ENDFOREACH(loop_var)
从 0 到 total 以1为步进 举例如下:shell FOREACH(VAR RANGE 3) MESSAGE(${VAR}) ENDFOREACH(VAR)
最终得到的输出是: 0 1 2 3- ##### (3)范围和步进 ```shell FOREACH(loop_var RANGE start stop [step]) ... ENDFOREACH(loop_var) ``` 从 start 开始到 stop 结束,以 step 为步进,</br> 举例如下: ```cmake FOREACH(A RANGE 5 15 3) MESSAGE(${A}) ENDFOREACH(A) ``` 最终得到的结果是: 5 8 11 14 这个指令需要注意的是,直到遇到 ENDFOREACH 指令,整个语句块才会得到真正的执行。
补充 #
Reference #
- https://cmake.org/cmake/help/latest/
- https://cmake.org/cmake/help/latest/guide/tutorial/index.html
- https://cmake.org/cmake/help/latest/command/project.html#command:project
- https://cmake.org/cmake/help/latest/manual/cmake-variables.7.html
- https://www.gnu.org/software/make/manual/make.html
- https://github.com/Liuyvjin/notebook/blob/master/Cmake/Cmake%E5%9F%BA%E7%A1%80.md
- https://github.com/Liuyvjin/notebook/blob/master/Cmake/Cmake%E5%B8%B8%E7%94%A8%E5%8F%98%E9%87%8F.md
- https://github.com/12302-bak/practise-demo 练习DEMO(整合三方lib以及子模块依赖lib)