文章目录

  • Cmake简介
    • 单个源文件
    • 多个源文件
      • 同一目录,多个源文件
      • 多个目录,多个源文件
    • 各种命令详解
    • cmake_minimum_required
    • project
    • add_subdirectory
    • include
    • set
    • include_directories
    • add_executable
    • target_link_libraries
        • find_package
        • catkin_package 和 find_package 的区别
        • option
        • add_definitions
  • finder
  • CMakeCache.txt
  • 变量
  • CMake中引号的用法
  • compiler flag
  • Reference

Cmake简介

Cmake允许开发者编写一种平台无关的CMakeList.txt文件来定制整个编译流程,然后再根据目标用户的平台进一步生成所需的本地化Makefile和工程文件,如Unix的MakeFile或Windows的Visual Studio工程。从而做到"Write once, run everywhere"。

  1. 在linux平台下使用CMake生成的Makefile并编译的流程如下:
  2. 在同一个目录下编写CMake配置文件CMakeLists.txt和源文件
  3. mkdir build && cd build,然后执行命令cmake ..生成Makefile,再执行命令make。(建立build文件夹的目的是将编译文件和源文件分开)
    make之后还可以再执行个sudo make install命令,这个命令可选,作用是将需要的头文件和库文件拷贝到/usr目录下,有时是/usr/local目录下。

make, make install的作用

CMake语法支持的数据类型是 字符串  字符串列表

  • 单个源文件

源文件main.cpp如下:

#include <stdio.h>
#include <stdlib.h>
/**
 * 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文件,依次出现了几个命令:

  1. cmake_minimum_required:指定运行此配置文件所需的CMake的最低版本;
  2. project:参数值是Demo1,该命令表示项目的名称是Demo1
  3. 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(<dir> <variable>)

因此,可以修改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 目录中的源文件编译为静态链接库。

各种命令详解

  1. cmake_minimum_required

规定cmake程序的最低版本。这行命令是可选的。但在有些情况下,如果CMakeLists.txt文件中使用了一些高版本cmake特有的一些命令的时候,就需要加上这一行,提醒用户升级到该版本之后再执行cmake

  1. project


指定项目的名称。在cmake中有两个预定义的变量:<project name>_BINARY_DIR以及<project name>_SOURCE_DIR。
同时cmake还预定义了PROJECT_BINARY_DIR和PROJECT_SOURCE_DIR变量。PROJECT_BINARY_DIR等同于<project name>_BINARY_DIR,PROJECT_SOURCE_DIR等同于<project name>_SOURCE_DIR。在实际的应用用,我强烈推荐使用PROJECT_BINARY_DIR和PROJECT_SOURCE_DIR变量,这样即使项目名称发生变化也不会影响CMakeLists.txt文件。

  1. add_subdirectory

ADD_SUBDIRECTORY(source_dir [binary_dir] [EXCLUDE_FROM_ALL])这个指令用于向当前工程添加存放源文件的子目录,并可以指定中间二进制和目标二进制存放的位置

  1. include

include 有点像 C++ 的 include,是把文件内容复制过来。

  1. set

现阶段,只需要了解SET命令可以用来显式的定义变量即可。在后面CMakeCache.txt部分会有更详细的说明。

  1. include_directories

    INCLUDE_DIRECTORIES类似gcc中的编译参数“-I”,指定编译过程中编译器搜索头文件的路径。当项目需要的头文件不在系统默认的搜索路径时,需要指定该路径。

  2. add_executable

    e.g.
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,如果需要改变目标存放路径,就在哪里加入上述的定义。

7.target_link_libraries
这句话指定在链接目标文件的时候需要链接的外部库,其效果类似gcc的编译参数“-l”,可以解决外部库的依赖问题。

8.find_package

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这个命令吧
当我们编译时需要使用第三方库时,我们需要知道三件事:

  1. 去哪儿找头文件(.h)
  2. 去哪儿找库文件(.so/.dll/.lib/.dylib/…)
  3. 库文件的名字

举个小例子:

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后,安装在自己目录下被使用。这两个文件怎样生成还需要再学习下。

  1. catkin_package 和 find_package 的区别

在使用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配置文件。也就是说它对依赖于此功能包的其它功能包来说具有重要作用。

具体解读一下上面的例子:
构建此功能包需要依赖于

  • roscpp
  • actionlib
  • std_msgs
  • message_generation
  • actionlib_msgs

而依赖于此功能包的功能包同时需要具有以下依赖:

  • std_msgs
  • actionlib
  • actionlib_msgs
  • roscpp
  1. option

    option(<option_variable> "This is a option for address" [initial value])
    

option() 提供选项让用户可以选择是ON or OFF,default是OFF
有两点需要注意:
1.option()具有很高的优先级,意思就是即使在subdirectory中定义option(),在全局都是有效的。
2.option命令和你本地是否存在编译缓存关系很大,所以如果有关于option的改变,那么你得清理 CMakeCache.txt 和 CMakeFiles 文件夹。

11.add_definitions
为源文件的编译添加由-D定义的标志
The documentation suggests that it was intended for setting preprocessor definitions, but in practice many people use it for compiler flags.

finder

当编译一个需要使用第三方库的软件时,我们需要知道:
在这里插入图片描述

这是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自带的怎么办?

  • 放置位置:工程根目录下的cmake/Modules/
  • 然后在CMakeLists.txt中添加set(CMAKE_MODULE_PATH $(CMAKE_MODULE_PATH) "${CMAKE_SOURCE_DIR}/cmake/Modules/")

find_package如何工作
find_package将会在module路径下查找Find<name>.cmake。首先它搜索${CMAKE_MODULE_PATH}中的所有路径,然后搜索<CMAKE_ROOT>/share/cmake-x.y/Modules/
如果这个文件未找到,它将会查找<Name>Config.cmake或<lower-case-name>-config.cmake(在CMAKE_PREFIX_PATH中查找)。这两个文件是库文件安装时自己安装的,将自己的路径硬编码到其中。
前者称为module模式,后者称为config模式。
每个模块一般会提供以下几个变量:

    • <name>_FOUND
    • <name>_INCLUDE_DIR<name>_INCLUDES
    • <name>_LIBRARY<name>_LIBRARIES<name>_LIBS
    • <name>_DEFINITIONS
  • 如何编写finder

    • 首先使用find_package探测本软件包依赖的第三方库(参数 QUIETLY 和 REQUIRED应该被传递)
    • 如果 pkg-config 可用,则可以用其去探测include/library路径
    • 分别使用 find_path 和 find_library 查找头文件和库文件
      • pkg-config 提供的路径仅作为参考
      • CMake有很多硬编码的路径
      • 结果放在<name>_INCLUDE_DIR<name>_LIBRARY(注意:单数而不是复数)
    • 设置 <name>_INCLUDE_DIRS  <name>_INCLUDE_DIR<dependency1>_INCLUDE_DIRS ...
    • 设置 <name>_LIBRARIES 
      <name>_LIBRARY<dependency1>_LIBRARIES ...
    • 依赖使用复数,包自身使用单数形式(由find_path和find_library提供)
    • 调用宏 find_package_handle_standard_args() 设置 <name>_FOUND 并打印或失败信息

CMakeCache.txt

  • CMakeCache.txt
    可以将其想象成一个配置文件(在UNIX环境下,我们可以认为它等价于传递给configure的参数)。

  • CMakeLists.txt中通过set(... CACHE ...)设置的变量
  • CMakeLists.txt中的option()提供的选项
  • CMakeLists.txt中find_package()等find命令引入变量
  • 命令行cmake . -D <var>:<type>=<value>定义变量
    cmake第一次运行时将会生成CMakeCache.txt文件,我们可以通过ccmake或cmake-gui或make edit_cache对其进行编辑。
    对于命令行-D定义变量,-U用来删除变量(支持globbing_expr),比如cmake -U/*QT/*将删除所有名字中带有QT的cache项。
  • 变量与Cache

    cmake的变量系统远比第一眼看上去复杂:

    • 有些变量被cache,有些则不被cache
    • 被cache的变量
      • 有的不能通过ccmake等进行编辑(internal)
    • 有的(带有描述和类型)可以被编辑(external)
          • 有的只在ccmake的advanced模式出现

      举个例子:

      • set(var1 13)
        • 变量var1被设置成13
        • 如果var1在cache中已经存在,则该命令不会去overwrite cache中的值
      • set(var1 13 ... CACHE ...)
        • 如果cache中存在该变量,使用cache中变量
          • 如果cache中不存在,则将该值写入cache
        • set(var1 13 ... CACHE ... FORCE)
          • 不论cache中是否存在,始终使用该值
      • find_xxx
        为了避免每次运行时都要进行头文件和库文件的探测,以及考虑到允许用户通过ccmake设置头文件路径和库文件的重要性,这些东西必须进行cache。

        find_path和find_library会自动cache它们的变量,如果变量已经存在且是一个有效值(即不是 -NOTFOUND 或 undefined),它们将什么都不做。
        另一方面,模块查找时输出的变量(<name>_FOUND, <name>_INCLUDE_DIRS, <name>_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中引号的用法

    在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的作用,主要就是在于当有空格的时候,区别一个变量是一个数组还是纯粹的字符串。在使用的时候,对于普通字符串,加不加引号没什么区别,而对于数组,加引号会将数组以分号间隔输出,而不加引号则是直接拼接数组。

    compiler flag


    首先对于一些常用的 build type, CMake 提供了一些内置的 compiler flag。可以通过设置CMAKE_BUILD_TYPE (Release, Debug, RelWithDebInfo, MinSizeRel) 来选择 build type. (可以在CMakeLists.txt中设置,也可以用命令行设置)。每个buld type对应C++ flags是取决于编译器的,并且储存在CMAKE_CXX_FLAGS_<buildType>这个变量中。

    如果你想查看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,有两种方式:

    1. 使用 add_definitions()
    2. 设置 CMAKE_CXX_FLAGS 或者 CMAKE_CXX_FLAGS_<buildType>
    
    

    (注意:CMAKE_CXX_FLAGS is applied to both compiler and linker, while add_definitions() is applied only to compiler.)

    举个例子: 如果我们想额外添加两个compiler flag, -Wall 和 -std=c++11, 下面也要分两种情况讨论。


    如果我们想让这些flags对所有的build types都有效

    1. add_definitions(-Wall -std=c++11)
    2. set(CMAKE_CXX_FLAGS "-Wall -std=c++11")
    • 如果我们想给某些特定的build type添加additional compiler flags
      例如:CMAKE_CXX_FLAGS_DEBUG
      set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -Wall -std=c++11")

    Reference
    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/