Cmake允许开发者编写一种平台无关的CMakeList.txt文件来定制整个编译流程,然后再根据目标用户的平台进一步生成所需的本地化Makefile和工程文件,如Unix的MakeFile或Windows的Visual Studio工程。从而做到"Write once, run everywhere"。
在linux平台下使用CMake生成的Makefile并编译的流程如下:
mkdir build && cd build
,然后执行命令cmake ..
生成Makefile,再执行命令make
。(建立build文件夹的目的是将编译文件和源文件分开)make
之后还可以再执行个sudo make install
命令,这个命令可选,作用是将需要的头文件和库文件拷贝到/usr目录下,有时是/usr/local目录下。make, make install的作用
CMake语法支持的数据类型是 字符串 和 字符串列表。
源文件main.cpp
如下:
#include
#include
/**
* power - Calculate the power of number.
* @param base: Base value.
* @param exponent: Exponent value.
*
* @return base raised to the power exponent.
*/
double power(double base, int exponent)
{
int result = base;
int i;
if (exponent == 0) {
return 1;
}
for(i = 1; i < exponent; ++i){
result = result * base;
}
return result;
}
int main(int argc, char *argv[])
{
if (argc < 3){
printf("Usage: %s base exponent \n", argv[0]);
return 1;
}
double base = atof(argv[1]);
int exponent = atoi(argv[2]);
double result = power(base, exponent);
printf("%g ^ %d is %g\n", base, exponent, result);
return 0;
}
编写CMakeLists.txt
# CMake 最低版本号要求
cmake_minimum_required (VERSION 2.8)
# 项目信息
project (Demo1)
# 指定生成目标
add_executable(Demo main.cpp)
CMakeLists.txt 的语法比较简单,由命令、注释和空格组成,其中命令是不区分大小写的。符号#
后面的内容是注释。命令由命令名称、小括号和参数组成。参数之间使用空格进行间隔。
对于上面的CMakeLists.txt文件,依次出现了几个命令:
cmake_minimum_required
:指定运行此配置文件所需的CMake的最低版本;project
:参数值是Demo1
,该命令表示项目的名称是Demo1
。add_executable
:将名为main.cpp
的源文件编译成一个名为Demo
的可执行文件。上面的例子只有单个源文件。现在假如把power
函数单独写进一个名为MathFunctions.cpp
的源文件里,使得这个工程变成如下的形式:
./Demo2
|
+--- main.cpp
|
+--- MathFunctions.cpp
|
+--- MathFunctions.h
CMakeLists.txt改成如下的形式:
# CMake 最低版本号要求
cmake_minimum_required(VERSION 2.8)
# 项目信息
project(Demo2)
# 指定生成目标
add_executable(Demo main.cpp MathFunctions.cpp)
唯一的改动只是在 add_executable
命令中增加了一个 MathFunctions.cpp
源文件。这样写当然没什么问题,但是如果源文件很多,把所有源文件的名字都加进去将是一件烦人的工作。更省事的方法是使用 aux_source_directory
命令,该命令会查找指定目录下的所有源文件,然后将结果存进指定变量名。其语法如下:
aux_source_directory( )
因此,可以修改CMakeLists.txt如下:
# CMake 最低版本号要求
cmake_minimum_required (VERSION 2.8)
# 项目信息
project (Demo2)
# 查找当前目录下的所有源文件,并将名称保存到 DIR_SRCS 变量
aux_source_directory(. DIR_SRCS)
# 指定生成目标
add_executable(Demo ${DIR_SRCS})
这样,CMake 会将当前目录所有源文件的文件名赋值给变量 DIR_SRCS
,再指示变量 DIR_SRCS
中的源文件需要编译成一个名称为 Demo 的可执行文件。
现在进一步将MathFunctions.h和MathFunctions.cpp文件移动到math目录下。
./Demo3
|
+--- main.cc
|
+--- math/
|
+--- MathFunctions.cc
|
+--- MathFunctions.h
对于这种情况,需要分别在项目根目录Demo3和math目录里各编写一个CMakeLists.txt文件。为了方便,我们可以将math目录里的文件编译成静态库再由main函数调用。
根目录中的 CMakeLists.txt :
# CMake 最低版本号要求
cmake_minimum_required (VERSION 2.8)
# 项目信息
project (Demo3)
# 查找当前目录下的所有源文件,并将名称保存到 DIR_SRCS 变量
aux_source_directory(. DIR_SRCS)
# 添加 math 子目录
add_subdirectory(math)
# 指定生成目标
add_executable(Demo main.cc)
# 添加链接库
target_link_libraries(Demo MathFunctions)
该文件添加了下面的内容: 第3行,使用命令 add_subdirectory
指明本项目包含一个子目录 math,这样 math 目录下的 CMakeLists.txt 文件和源代码也会被处理 。第6行,使用命令 target_link_libraries
指明可执行文件 main 需要连接一个名为 MathFunctions 的链接库 。
子目录中的 CMakeLists.txt:
# 查找当前目录下的所有源文件,并将名称保存到 DIR_LIB_SRCS 变量
aux_source_directory(. DIR_LIB_SRCS)
# 生成链接库
add_library (MathFunctions ${DIR_LIB_SRCS})
在该文件中使用命令 add_library
将 src 目录中的源文件编译为静态链接库。
规定cmake程序的最低版本。这行命令是可选的。但在有些情况下,如果CMakeLists.txt文件中使用了一些高版本cmake特有的一些命令的时候,就需要加上这一行,提醒用户升级到该版本之后再执行cmake
指定项目的名称。在cmake中有两个预定义的变量:
_BINARY_DIR以及
_SOURCE_DIR。
同时cmake还预定义了PROJECT_BINARY_DIR
和PROJECT_SOURCE_DIR
变量。PROJECT_BINARY_DIR
等同于
,PROJECT_SOURCE_DIR
等同于
。在实际的应用用,我强烈推荐使用PROJECT_BINARY_DIR
和PROJECT_SOURCE_DIR
变量,这样即使项目名称发生变化也不会影响CMakeLists.txt文件。
ADD_SUBDIRECTORY(source_dir [binary_dir] [EXCLUDE_FROM_ALL])
这个指令用于向当前工程添加存放源文件的子目录,并可以指定中间二进制和目标二进制存放的位置。
include 有点像 C++ 的 include,是把文件内容复制过来。
现阶段,只需要了解SET命令可以用来显式的定义变量即可。在后面CMakeCache.txt部分会有更详细的说明。
INCLUDE_DIRECTORIES类似gcc中的编译参数“-I”,指定编译过程中编译器搜索头文件的路径。当项目需要的头文件不在系统默认的搜索路径时,需要指定该路径。
SET(SRC_LIST main.cc
rpc/CRMasterCaller.cpp
rpc/CRNode_server.skeleton.cpp
rpc/Schd_constants.cpp
rpc/CRMaster.cpp
rpc/CRNode.cpp
rpc/Schd_types.cpp
task/TaskExecutor.cpp
task/TaskMoniter.cpp
util/Const.cpp
util/Globals.cc
util/utils.cc
util/Properties.cpp
)
ADD_EXECUTABLE(CRNode ${SRC_LIST})
ADD_EXECUTABLE定义了这个工程会生成一个文件名为 CRNode 的可执行文件,相关的源文件是 SRC_LIST 中定义的源文件列表。需要注意的是,这里的CRNode和之前的项目名称没有任何关系,可以任意定义。
我们可以通过 SET 指令重新定义 EXECUTABLE_OUTPUT_PATH
和 LIBRARY_OUTPUT_PATH
变量来指定最终的目标二进制的位置(指最终生成的CRNode可执行文件或者最终的共享库,而不包含编译生成的中间文件)。
命令如下:
SET(EXECUTABLE_OUTPUT_PATH ${PROJECT_BINARY_DIR}/bin)
SET(LIBRARY_OUTPUT_PATH ${PROJECT_BINARY_DIR}/lib)
需要注意的是,在哪里 ADD_EXECUTABLE 或 ADD_LIBRARY,如果需要改变目标存放路径,就在哪里加入上述的定义。
这句话指定在链接目标文件的时候需要链接的外部库,其效果类似gcc的编译参数“-l”,可以解决外部库的依赖问题。
find_package可以根据cmake内置的.cmake的脚本去找相应的库的模块,当然,内建了很多库的模块变量,调用了find_package之后,会有相应的变量“生成”有效。
比如调用了find_package(Qt5Widgets)
,就会有变量Qt5Widgets_FOUND
,Qt5Widgets_INCLUDE_DIRS
,Qt5Widgets_LIBRARIES
,Qt5Widgets_NEED_PREFIX
,Qt5Widgets_VERSION_STRING
相应的变量生效。
然后就可以在CMakeLists.txt里面使用上述的变量了,类似include_directories(${Qt5Widgets_INCLUDE_DIRS})
下面详细介绍一下find_package这个命令吧
当我们编译时需要使用第三方库时,我们需要知道三件事:
举个小例子:
project(helloworld)
add_executable(helloworld hello.c)
find_package (BZip2)
if (BZIP2_FOUND)
include_directories(${BZIP_INCLUDE_DIRS})
target_link_libraries (helloworld ${BZIP2_LIBRARIES})
endif (BZIP2_FOUND)
find_package()
是出错的重灾区,因此如果需要的话再从原理进行一下解读。下面是两种利用这个命令的方式,具体还可以看 finder 这部分。
(1)CMake有个系统变量CMAKE_ROOT
,通常比如说是 “/usr/local/share/cmake-3.5”。这个目录下的子目录是completions editors Help include Licenses Modules Templates
。
其中,Modules
文件夹中存在着大量的FindXXXX.cmake
文件,其中XXXX就是package名(注意:大小写敏感,比如“OpenCV”)。这个目录就是cmake默认根据package名找FindXXXX.cmake文件的目录。同时,如果系统中有其它的Modules目录来进行这种功能的扩展,则可以用CMAKE_MODULE_PATH
系统变量来记录。(开源项目或自己开发的库可以提供自己的 FindXXXX.cmake 放到你想放的Modules目录下。关于怎样生成 FindXXXX.cmake,这个还需要再学习下。)
(2)cmake有个环境变量CMAKE_PREFIX_PATH
用来注册开源项目的安装目录,它会在安装目录下搜索如下文件:XXXXConfig.cmake
和XXXXConfig.cmake
(同样,XXXX还是package名,并且大小写敏感)。这种方式下,开源项目生成自己的XXXXConfig.cmake
或XXXX-config.cmake
后,安装在自己目录下被使用。这两个文件怎样生成还需要再学习下。
在使用ROS时,编写CMakeLists.txt 会经常用到catkin_package这个宏命令。
例如:
find_package(catkin REQUIRED COMPONENTS
roscpp
actionlib
std_msgs
message_generation
actionlib_msgs
)
catkin_package(CATKIN_DEPENDS std_msgs actionlib actionlib_msgs roscpp)
根据教程的解释,这两个宏命令的区别是什么?
首先来看一下CMakeLists.txt中自动生成的注释对两个宏命令进行了解释:
## Find catkin macros and libraries
## if COMPONENTS list like find_package(catkin REQUIRED COMPONENTS xyz)
## is used, also find other catkin packages
find_package(catkin REQUIRED COMPONENTS ...
和
## The catkin_package macro generates cmake config files for your package
## Declare things to be passed to dependent projects
## INCLUDE_DIRS: uncomment this if you package contains header files
## LIBRARIES: libraries you create in this project that dependent projects also need
## CATKIN_DEPENDS: catkin_packages dependent projects also need
## DEPENDS: system dependencies of this project that dependent projects also need
catkin_package(...
find_package
是cmake中常见的宏命令,用于加载catkin宏和指定对其它ROS功能包的依赖关系。
catkin_package
的作用是声明要传递给依赖本项目的其它项目的内容,生成cmake配置文件。也就是说它对依赖于此功能包的其它功能包来说具有重要作用。
具体解读一下上面的例子:
构建此功能包需要依赖于
而依赖于此功能包的功能包同时需要具有以下依赖:
option( "This is a option for address" [initial value])
option() 提供选项让用户可以选择是ON or OFF,default是OFF
有两点需要注意:
1.option()具有很高的优先级,意思就是即使在subdirectory中定义option(),在全局都是有效的。
2.option命令和你本地是否存在编译缓存关系很大,所以如果有关于option的改变,那么你得清理 CMakeCache.txt 和 CMakeFiles 文件夹。
当编译一个需要使用第三方库的软件时,我们需要知道:
这是cmake的finder需要返回的最基本的信息。
接下来举个例子:
假如我们需要一个第三方库curl,那么我们的CMakeLists.txt需要指定头文件目录,和库文件,类似:
include_directories(/usr/include)
target_link_libraries(myprogram curl)
如果借助于cmake提供的finder会怎么样呢?如果要使用cmake的Modules目录下的FindCURL.cmake,相应的CMakeLists.txt文件:
find_package(CURL REQUIRED)
include_directories(${CURL_INCLUDE_DIR})
target_link_libraries(curltest ${CURL_LIBRARY})
或者
find_package(CURL)
if(CURL_FOUND)
include_directories(${CURL_INCLUDE_DIR})
target_link_libraries(curltest ${CURL_LIBRARY})
else(CURL_FOUND)
message(FATAL_ERROR "curl not found!")
endif(CURL_FOUND)
如果我们使用的finder,不是cmake自带的怎么办?
set(CMAKE_MODULE_PATH $(CMAKE_MODULE_PATH) "${CMAKE_SOURCE_DIR}/cmake/Modules/")
find_package
如何工作
find_package
将会在module路径下查找Find
。首先它搜索${CMAKE_MODULE_PATH}
中的所有路径,然后搜索
如果这个文件未找到,它将会查找
或
(在CMAKE_PREFIX_PATH
中查找)。这两个文件是库文件安装时自己安装的,将自己的路径硬编码到其中。
前者称为module模式,后者称为config模式。
每个模块一般会提供以下几个变量:
_FOUND
_INCLUDE_DIR
或_INCLUDES
_LIBRARY
或_LIBRARIES
或_LIBS
_DEFINITIONS
如何编写finder
find_package
探测本软件包依赖的第三方库(参数 QUIETLY 和 REQUIRED应该被传递)_INCLUDE_DIR
和_LIBRARY
(注意:单数而不是复数)_INCLUDE_DIRS
为 _INCLUDE_DIR_INCLUDE_DIRS ...
_LIBRARIES
为_LIBRARY_LIBRARIES ...
find_package_handle_standard_args()
设置 _FOUND
并打印或失败信息CMakeCache.txt
可以将其想象成一个配置文件(在UNIX环境下,我们可以认为它等价于传递给configure的参数)。
set(... CACHE ...)
设置的变量option()
提供的选项cmake . -D :=
定义变量cmake第一次运行时将会生成CMakeCache.txt文件,我们可以通过ccmake或cmake-gui或make edit_cache对其进行编辑。
对于命令行-D定义变量,-U用来删除变量(支持globbing_expr),比如cmake -U/*QT/*
将删除所有名字中带有QT的cache项。
变量与Cache
cmake的变量系统远比第一眼看上去复杂:
举个例子:
set(var1 13)
set(var1 13 ... CACHE ...)
set(var1 13 ... CACHE ... FORCE)
find_xxx
为了避免每次运行时都要进行头文件和库文件的探测,以及考虑到允许用户通过ccmake设置头文件路径和库文件的重要性,这些东西必须进行cache。
find_path
和find_library
会自动cache它们的变量,如果变量已经存在且是一个有效值(即不是 -NOTFOUND 或 undefined),它们将什么都不做。_FOUND
, _INCLUDE_DIRS
, _LIBRARIES
)不应该被cache。CMake变量按功能分主要有四种不同的类型:(1)提供信息的变量(53个) (2)改变行为的变量(23个) (3)描述系统的变量(24个) (4)控制构建过程的变量(22个)
PROJECT_SOURCE_DIR
CMAKE_CURRENT_SOURCE_DIR
当前处理的CMakeLists.txt所在的目录。
CMAKE_CURRENT_LIST_DIR
这是当前正在处理的列表文件的目录。—— 当CMake处理项目中的列表文件时,该变量的值始终被设置为当前正在处理的列表文件(CMAKE_CURRENT_LIST_FILE)所在的目录。 因此该变量的值具有动态范围。当CMake开始处理源文件中的命令时,它将此变量设置为此文件所在的目录。当CMake完成处理来自文件的命令时,它将恢复先前的值。
举个例子:如果目录项目中存在CMakeLists.txt并且包含以下指令include(src/CMakeLists.txt)
然后在处理src/CMakeLists.txt时,CMAKE_CURRENT_LIST_DIR
将引用project/src,而CMAKE_CURRENT_SOURCE_DIR
将指向外部目录项目。
CMAKE_
CMAKE_MODULE_PATH
set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${PROJECT_SOURCE_DIR}/cmake/Modules/")
CMAKE_LIBRARY_OUTPUT_DIRECTORY
CMAKE_ARCHIVE_OUTPUT_DIRECTORY
在CMake中定义和使用变量时,可以使用引号,也可以不使用引号,他们会产生不同的结果。
定义变量时不使用引号,使用变量时也不使用引号
set(TITLE learn cmake quotes!)
message(${TITLE})
输出:learncmakequotes!
可以看到字符串中间的空格没有了,实际上,当我们不使用引号定义变量时,相当于我们定义了一个包含多个成员的字符串列表。于是,当我们用message输出时,其实是挨着输出这些元素。我们可以用foreach验证结果:
foreach(e ${TITLE})
message(${e})
endforeach()
定义变量时不使用引号,使用变量时使用引号
对于上面的${TITLE}
变量,如果使用引号,也会有不同的结果。
message("${TITLE}")
输出:
learn;cmake;quotes!
${TITLE}
是一个数组,我们用"${TITLE}"
这种形式的时候,表示要让CMake把这个数组的所有值当成一个整体,而不是分散的个体。于是,为了保持数组的含义,又提供一个整体的表达形式,CMake就会用;把这数组的多个值连接起来。无论是在CMake还是在Shell中,用分号分割的字符串,形式上是一个字符串,但把它当成命令执行,就会被解析成多个用分号分割的部分。
对于单一的字符串变量(不包含特殊字符),用不用引号,结果都是一样的。
定义变量时使用引号,使用变量时使不使用引号效果都一样
set(TITLE "learn cmake quotes!")
message($(TITLE))
message("${TITLE}")
输出:
learn cmake quotes!
learn cmake quotes!
当使用引号时,这个值就是普通的字符层,不再是数组了。
总结
引号对于CMake的作用,主要就是在于当有空格的时候,区别一个变量是一个数组还是纯粹的字符串。在使用的时候,对于普通字符串,加不加引号没什么区别,而对于数组,加引号会将数组以分号间隔输出,而不加引号则是直接拼接数组。
首先对于一些常用的 build type, CMake 提供了一些内置的 compiler flag。可以通过设置CMAKE_BUILD_TYPE
(Release, Debug, RelWithDebInfo, MinSizeRel) 来选择 build type. (可以在CMakeLists.txt中设置,也可以用命令行设置)。每个buld type对应C++ flags是取决于编译器的,并且储存在CMAKE_CXX_FLAGS_
这个变量中。
如果你想查看build type对应着哪些C++ flags,可以按照如下的方法编写CMakeLists.txt。
message("CMAKE_CXX_FLAGS_DEBUG is ${CMAKE_CXX_FLAGS_DEBUG}")
message("CMAKE_CXX_FLAGS_RELEASE is ${CMAKE_CXX_FLAGS_RELEASE}")
message("CMAKE_CXX_FLAGS_RELWITHDEBINFO is ${CMAKE_CXX_FLAGS_RELWITHDEBINFO}")
message("CMAKE_CXX_FLAGS_MINSIZEREL is ${CMAKE_CXX_FLAGS_MINSIZEREL}")
如果Linux/gcc,则输出为:
CMAKE_CXX_FLAGS_DEBUG is -g
CMAKE_CXX_FLAGS_RELEASE is -O3 -DNDEBUG
CMAKE_CXX_FLAGS_RELWITHDEBINFO is -O2 -g -DNDEBUG
CMAKE_CXX_FLAGS_MINSIZEREL is -Os -DNDEBUG
如果我们想添加额外的compiler flags,有两种方式:
add_definitions()
CMAKE_CXX_FLAGS
或者 CMAKE_CXX_FLAGS_
(注意:CMAKE_CXX_FLAGS
is applied to both compiler and linker, while add_definitions()
is applied only to compiler.)
举个例子: 如果我们想额外添加两个compiler flag, -Wall
和 -std=c++11
, 下面也要分两种情况讨论。
add_definitions(-Wall -std=c++11)
set(CMAKE_CXX_FLAGS "-Wall -std=c++11")
CMAKE_CXX_FLAGS_DEBUG
set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -Wall -std=c++11")
https://www.cnblogs.com/zjutzz/p/6815342.html
https://www.hahack.com/codes/cmake/
https://www.cnblogs.com/ph829/p/4759124.html
https://blog.csdn.net/haluoluo211/article/details/80559341
https://blog.csdn.net/sunyoop/article/details/80022024
https://www.jianshu.com/p/46e9b8a6cb6a
https://blog.csdn.net/lcc816/article/details/82949880
https://answers.ros.org/question/58498/what-is-the-purpose-of-catkin_depends/