Skip to content

Latest commit

 

History

History
168 lines (144 loc) · 10.8 KB

how_to_write_an_cmakelist.md

File metadata and controls

168 lines (144 loc) · 10.8 KB

我们写的C/C++程序需是要经过编译器处理, 最终变为二进制文件才能被计算机识别的. 一般我们程序生成的二进制target分为可执行程序和库文件. 可执行程序是我们接触得最多的(比如windows下的.exe), 只能执行, 并且更具自己代码所决定的流程一套完整的走下来. 库文件中包含了许多方法和函数, 可以被其他target调用(比如我们用opencv库中的函数来处理图像).

gcc/g++ 是常用的编译器, 用来处理我们的c/c++程序并将其生成所需要的target. 假设我们写了一个hello_world.c, 并且想要将其生成可执行程序来运行,可以输入指令:

gcc -o hello hello_world.c

将会生成一个名为hello的可执行程序, 执行./hello就可以运行这个程序. 当代码文件较少时我们可以直接使用gcc生成target, 当文件较多时这样就很费时了. 于是就有了make工具来批处理文件, 调用gcc/g++来帮助我们生成target. 使用make工具需要编写规则文件Makefile(比如将哪些cpp文件生成可执行程序, 生成的target需要依赖哪些库等等), make工具将根据Makefile来批处理编译. 当项目工程比较大时, 直接编写Makefile还是比较复杂的, 于是就有了cmake工具来帮助我们自动生成Makefile. 使用cmake工具也需要写一个规则文件叫CMakeList.txt. 相对而言, 写CMakeList.txt是比较简单的了.


cmake工程的结构

一个基于cmake的c++工程的典型结构为:

project_name        # 工程根目录
└── src             # 存放源码目录
    ├── xxx.cpp
└── include         # 存放头文件的目录
    ├── xxx.hpp
├── CMakeList.txt   

编译的方法为:

cd project_name         # 进入工程根目录
mkdir build & cd build  # 建立一个build目录并进入
cmake ..                # 执行CMakeList文件, 生成Makefile
make                    # 根据生成的Makefile编译生成target
make install            # 可选, 安装文件到电脑

如何利用CMakeList生成target

假设我们有一个叫做basic_cmake_example的工程, 其结构和上面的典型结构一样. 其中src目录下有一个hello.cpp的源文件, 内容为:

#include <iostream>
int main() {
  std::cout << "hellow world!\n";
}

我们想要将其生成一个可执行文件, 那么CMakeList.txt可以写为:

cmake_minimum_required(VERSION 3.0)

project(hello)

add_executable(hello_world   
  src/hello.cpp)

按照上面的编译方法编译后, 将会在build目录生成一个名为hello_world可执行文件, 执行./hello_world, 将会在终端输出hello world

在上面的CMakeList中, 第1行cmake_minimum_required(VERSION 3.0)是必不可少的. 这条语句指定了所需的cmake的最低版本要求, 也就是说我们电脑上装的cmake版本要比这个数字高才能编译这个工程.(cmake版本可以通过cmake --version查看.) 第2行project(hello)也是比不可少的, 设定了这个工程的名字, 名字可以任意取. 可以很容易猜出来, 我们通过第3行的指令add_executable()生成了一个名为hello_world的可执行文件. 简单的add_executable指令为:

add_executable(<target_name>   
               <source-file1> <source-file2> ...)

其中<target_name>是生成的可执行文件名字, 可以任意取. 后面的参数需要填生成这个可执行文件所需要的所有源文件相对于CMakeList的路径.

相应的, 当我们想生成库文件时可以用指令add_library:

add_library(<target_name> [STATIC | SHARED | MODULE]
            <source-file1> <source-file2> ...)

这个指令和add_executable类似. 当指定STATIC时, 将生成静态链接库;当指定SHARED时, 将生成动态链接库. 指令默认生成静态链接库.

上面的例子中只用一个源文件生成了可执行文件, 并且不依赖于其它库文件. 假设我们的程序要依赖OPENCV, 使用OPENCV中的函数来处理图片呢? OPENCV库文件(一系列.so文件)被安装到了电脑上的其它位置, 如何才能让我们的程序和OPENCV发生关系, 并能够调用其中的函数呢? 这时CMakeList可以写为:

cmake_minimum_required(VERSION 3.0)

project(hello)

find_package(OpenCV REQUIRED)

include_directories(${OpenCV_INCLUDE_DIRS})

add_executable(hello_world   
  src/hello.cpp)

target_link_libraries(hello_world
  ${OpenCV_LIBRARIES})

在上面的CMakeList中, 我们使用find_package()来寻找OPENCV, 一旦找到的话, 就会在变量OpenCV_INCLUDE_DIRS 中存储OPENCV的头文件所在路径, 在变量OpenCV_LIBRARIES中存储OPENCV库文件所在目录. 我们可以使用message()指令来打印变量的值, 比如在find_package后面添加:

message("OpenCV_INCLUDE_DIRS = ${OpenCV_INCLUDE_DIRS}")
message("OpenCV_LIBRARIES = ${OpenCV_LIBRARIES}")

在执行cmake ..时, 会在终端中打印出上面两个变量存放的值. 如何用find_package找包请参看how to find an cmake package.

include_directories()中填库头文件目录的绝对路径, 或者是相对于当前CMakeList.txt位置的相对路径, 这样我们在程序中就能直接使用相对路径包含头文件. 举个例子, 假设OpenCV的头文件目录为/usr/local/include/opencv3.3.1/opencv/, 如果不使用include_directories(), 我们在程序中包含opencv头文件的方式为:#include </usr/local/include/opencv3.3.1/opencv/cv.hpp>. 而使用了include_directories()后, 我们在程序中包含头文件的方式可以简单的写成:#include <cv.hpp>.

target_link_libraries()的作用是为target链接上所需要的库文件. 一般头文件中包含了函数的声明, 库文件中包含了函数的实现. 如果不链接到相应的库文件, 那么就无法调用函数的具体实现, 会报undefined reference ...错误. target_link_libraries()的第一个参数为target名字, 应该要与add_executableadd_library中的名字一致;后面的参数为所需库文件的绝对路径.

以上就是一个基本的CMakeList.txt文件的写法. simple_cmake_example提供了一个简单的例程. 例程中生成了一个叫point库和一个叫simple_cmake_example的可执行文件. 可执行程序中调用了opencv库显示一张图片.


如何写一个基于ROS的CMakeList

一个ros工程目录的基本结构为:

workspace_name   # 工作空间目录
    └── devel    # 编译后自动生成该目录. 生成的target存放在该目录
    └── src   # 源码目录
        └── package1    # 包目录
            └── src     # 存放源码目录
                ├── xxx.cpp
            └── include    # 存放头文件的目录
                ├── xxx.hpp
            ├── CMakeList.txt   
            ├── package.xml  
        └── package2    # 包目录
         ...
        └── packagen    # 包目录

可以看到, 一个ros package就是上面介绍的一个cmake c++工程, 只不过多了一个package.xml文件. 另外, 一个ros package必须放在workspace目录下的src里才行.

编译ros包的基本指令:

cd <workspace_folder> # 进入工作空间目录
catkin build <package-name>

更多关于ros的基本概念请先参看ros官方教程.

ROS package的CMakeList与普通CMakeList的写法基本是一样的, 普通CMakeList支持的语法, ros CMakeList都支持. 只不过ros对cmake进行了封装, 增加了几条指令. 和普通的CMakeList相比, 这里主要关心2个问题: 1) 如何找到其他的ros package作为库使用(找普通的library方法不变). 2) 如何让自己写的ros package能够被其他ros package找到使用.

如何使用其他的ros包

寻找ros包同样也使用find_package()指令, 不过有些许不同:

find_package(catkin REQUIRED COMPONENTS
  <package1>
  <package2>
  ...
  <packagen>)

可以看到, 就算有n个ROS包, 也可以使用1个find_package()来找. 所有的ROS包都将作为catkin的components, 这些包的头文件存储在变量catkin_INCLUDE_DIRS中, 库文件都存储在变量catkin_LIBRARIES中. 找ROS包除了在CMakeList.txt中使用find_package, 还需要在package.xml文件中添加:

<depend>package1<depend>
...
<depend>packagen<depend>

假设工作空间下已经有一个package-A. 现在我想写一个package-B, 需要使用package-A中的函数. 此时需要在package-A的CMakeList.txt中添加:

find_package(catkin REQUIRED package-A)

include_directories(${catkin_INCLUDE_DIRS})

add_executable(<target_name>
  xxx.cpp ...)
target_link_libraries(<target-name>
  ${catkin_LIBRARIES})

然后在package-Bpackage.xml文件中写入:

<depend>package-A<depend>

这样就能在package-B的代码中包含package-A的头文件并使用其中的函数了.

ROS包一定是位于某个工作空间中的(可以是其他工作空间), 每一个工作空间都有一个setup.bash文件, 要想这个工作空间中的包能被find_package()找到, 必须先在终端执行 source setup.bash 命令来设定相应的CMAKE PATH变量

如何让自己的ROS包能被其他包调用

想要让自己写的ros包能被其他ros包顺利调用, 需要在生成target的指令之前添加:

catkin_package(
   INCLUDE_DIRS <自己包的头文件所在相对路径(相对于CMakeList.txt)>
   LIBRARIES    <自己包会生成的库的名字>
   CATKIN_DEPENDS <自己包所依赖的其他ros包的名字>
   DEPENDS <自己包所依赖的其他非ROS库的名字>)
  • 如果INCLUDE_DIRS不填, 则其他ros包无法找到这个包的头文件;
  • 如果LIBRARIES不填, 则其他包会找不到这个包生成的库文件, 会出现undefined reference error: ...;
  • CATKIN_DEPENDS/DEPENDS的作用在于: 当其他包调用这个包时, 不需要再用find_package()再去寻找一遍相同的依赖库. 举个例子, 假如我们自己写的packae_A中依赖了OpenCV, 如果在catkin_package()中写了DEPENDS OpenCV, 那么在其他包中使用find_package找 package_A时, 会自动加入OpenCV库的依赖, 而不需要再使用find_package(OpenCV REQUIRED)寻找OpenCV.

更详细的关于ROS CMakeList的知识, 参考官网ROS CMakeList.

例程simple_ros_cmake_example中展示了一个基本的ROS 版CMakeList写法.这个包读取一张图片并发布成占据栅格在rviz中显示,同时订阅rviz发布的2D Nav Goal信息.