CMakeLists.txt详解

CMakeLists.txt编写入门

借用黑格尔的名言“存在即合理”,既然CMakeList.txt被设计出来,就有它的一个道理!这样想来!我们内心对它的畏惧是不是就减少了呢!那~现在让我们从“它为什么存在”,“它是什么”以及“它怎么用”三个角度深刻剖析!


为什么存在?

我们刚开始学C++中的“Hello World”时,是通过用g++编译器对该cpp进行编译生成可执行文件(g++ main.cpp -o main)。当涉及大型项目,需要用到大量库的时候,采用这种编译方式是极其繁琐的。因此,能不能用一个脚本文件来编译源代码文件,于是Makefile作为一个自动化编译脚本应运而生。然而,它语法实在过于繁琐,如果能有更简单的配置文件就好,能自动化生成Makefile文件。所以,CMakeLists.txt(Cmake)就设计出来了。当然,这只是其中的部分原因,更重要的是有了它,我们能在不同操作系统运行(与操作系统解耦),也就是别人写的代码,也能在我的操作系统上编译运行。

看到这,大家的思路应该也清晰了,总结一下:CMakeLists.txt存在的原因是:

  1. 解决跨平台编译问题

  2. 使项目构建过程更简单、灵活

历史渊源:在早期,开发人员需要针对每个平台编写不同的构建系统脚本,例如Makefile(Unix-like系统)、Visual Studio项目文件(Windows)等。这导致了构建过程的繁琐和维护困难,特别是对于跨平台项目。为了解决这个问题,CMake在2000年由Kitware公司开发出来。

常见操作系统:(系统软件;硬件和软件的中介;提供管理硬件接口)

Window: 微软开发,用于个人电脑和服务器系统

Linux: 基于开源Linux内核的操作系统,有许多不同的发行版,如Ubuntu、Debian、Fedora、CentOS等

Unix: 多用户、多任务的操作系统家族,包括类似BSD、Solaris等不同的版本

macOS: 苹果开发,用于苹果的Mac系列电脑

iOS: 苹果 开发,用于iPhone、iPad和iPod Touch

Android: 谷歌开发,用于智能手机和平板电脑

系统脚本:一种计算机程序。(程序由指令组成,指令像命令)

Makefile:一种自动化编译的脚本文件,定义了项目的构建规则和依赖关系。

Make: 一种自动化构建和编译工具。Make根据这个Makefile来判断哪些文件需要重新编译,然后调用相应的编译器(例如g++)来完成编译过程。

Cmake: 一种跨平台的开源构建工具。

CMakeLists.txt: 是Cmake的脚本文件

关系:CMakeList.txt通过Cmake指令来生成Makefile文件,Makefile文件再通过make指令自动化编译C++源代码。

是什么?

CMakeLists: 是一个脚本文件,通过CMake指令编写,允许开发者灵活地配置和管理C/C++项目的构建过程。使用CMakeLists.txt文件,开发者可以实现一次编写,多平台编译的效果,方便地在不同操作系统和编译器上构建项目,从而提高项目的可移植性和开发效率。

CMake:是一个跨平台的构建工具,用于生成适用于不同操作系统和编译器的本地构建文件(如Makefile、Visual Studio项目文件等)。CMake使用一个名为"CMakeLists.txt"的脚本文件来描述项目的构建配置。通过CMake,开发者可以将项目的构建过程和依赖管理与特定的编译器和操作系统解耦,从而实现跨平台的构建支持。

怎么用?

先来个复杂的CMakeLists文件,先直观上感受一下:

cmake_minimum_required(VERSION 2.8.3)
project(lio_sam)set(CMAKE_BUILD_TYPE "Release")
set(CMAKE_CXX_FLAGS "-std=c++11")
set(CMAKE_CXX_FLAGS_RELEASE "-O3 -Wall -g -pthread")find_package(catkin REQUIRED COMPONENTS
  tf
  roscpp
  rospy
  cv_bridge
  # pcl library
  pcl_conversions
  # msgs
  std_msgs
  sensor_msgs
  geometry_msgs
  nav_msgs
  message_generation
  visualization_msgs
)find_package(OpenMP REQUIRED)
find_package(PCL REQUIRED QUIET)
find_package(OpenCV REQUIRED QUIET)
find_package(GTSAM REQUIRED QUIET)
find_package(Boost REQUIRED COMPONENTS timer)add_message_files(
  DIRECTORY msg
  FILES
  cloud_info.msg
)add_service_files(
  DIRECTORY srv
  FILES
  save_map.srv
)generate_messages(
  DEPENDENCIES
  geometry_msgs
  std_msgs
  nav_msgs
  sensor_msgs
)catkin_package(
  INCLUDE_DIRS include
  DEPENDS PCL GTSAM
​
  CATKIN_DEPENDS
  std_msgs
  nav_msgs
  geometry_msgs
  sensor_msgs
  message_runtime
  message_generation
  visualization_msgs
)# include directories
include_directories(
    include
    ${catkin_INCLUDE_DIRS}
    ${PCL_INCLUDE_DIRS}
  ${OpenCV_INCLUDE_DIRS}
    ${GTSAM_INCLUDE_DIR}
)# link directories
link_directories(
    include
    ${PCL_LIBRARY_DIRS}
  ${OpenCV_LIBRARY_DIRS}
  ${GTSAM_LIBRARY_DIRS}
)###########
## Build ##
############ Range Image Projection
add_executable(${PROJECT_NAME}_imageProjection src/imageProjection.cpp)
add_dependencies(${PROJECT_NAME}_imageProjection ${catkin_EXPORTED_TARGETS} ${PROJECT_NAME}_generate_messages_cpp)
target_link_libraries(${PROJECT_NAME}_imageProjection ${catkin_LIBRARIES} ${PCL_LIBRARIES} ${OpenCV_LIBRARIES})# Feature Association
add_executable(${PROJECT_NAME}_featureExtraction src/featureExtraction.cpp)
add_dependencies(${PROJECT_NAME}_featureExtraction ${catkin_EXPORTED_TARGETS} ${PROJECT_NAME}_generate_messages_cpp)
target_link_libraries(${PROJECT_NAME}_featureExtraction ${catkin_LIBRARIES} ${PCL_LIBRARIES} ${OpenCV_LIBRARIES})# Mapping Optimization
add_executable(${PROJECT_NAME}_mapOptmization src/mapOptmization.cpp)
add_dependencies(${PROJECT_NAME}_mapOptmization ${catkin_EXPORTED_TARGETS} ${PROJECT_NAME}_generate_messages_cpp)
target_compile_options(${PROJECT_NAME}_mapOptmization PRIVATE ${OpenMP_CXX_FLAGS})
target_link_libraries(${PROJECT_NAME}_mapOptmization Boost::timer ${catkin_LIBRARIES} ${PCL_LIBRARIES} ${OpenCV_LIBRARIES} ${OpenMP_CXX_FLAGS} gtsam)# IMU Preintegration
add_executable(${PROJECT_NAME}_imuPreintegration src/imuPreintegration.cpp)
target_link_libraries(${PROJECT_NAME}_imuPreintegration Boost::timer ${catkin_LIBRARIES} ${PCL_LIBRARIES} ${OpenCV_LIBRARIES} gtsam)

对于单个cpp文件,例如Hello World,CMakeLists.txt文件如下:

cmake_minimum_required(VERSION 2.8.3)
project(Main)
add_executable(main main.cpp)

在cmd终端输入下面指令即可运行:

mkdir build 
cd build
cmake ..
make
./main

当我们安装了比较低的CMake时,试图编译高版本CMake的CMakeLists脚本文件,CMake 将会检测到版本不匹配,并报告错误。这是因为高版本的 CMakeLists.txt 可能包含了较低版本 CMake 不支持的新特性和语法。那该如何提醒告知开发人员呢?于是下列指令便产生:

cmake_minimum_required(VERSION 2.8.3)

作用:指定项目构建所需的最低 CMake 版本。例如,cmake_minimum_required(VERSION 3.10) 表示项目需要 CMake 3.10 或更高版本才能构建,安装低于该版本的便报错!

万物皆有名字,于是project(name)诞生了,它是给咱们项目取的名字。

project(Main)

当你在命令行中运行CMake命令时,CMake会读取项目根目录下的CMakeLists.txt文件,并解析其中的内容。在这个过程中,CMake会自动生成一些内置变量,例如CMAKE_SOURCE_DIRCMAKE_BINARY_DIR等,用于指示项目的源代码目录和构建目录的路径。常见内置变量如下:

  1. CMAKE_SOURCE_DIR:当前 CMakeLists.txt 所在的源码目录的根路径。

  2. CMAKE_BINARY_DIR:构建目录的根路径,即构建生成的可执行文件、库和其他构建输出的存放位置。

  3. CMAKE_CURRENT_SOURCE_DIR:当前处理的 CMakeLists.txt 所在的源码目录的路径。

  4. CMAKE_CURRENT_BINARY_DIR:当前处理的 CMakeLists.txt 所在的构建目录的路径。

  5. CMAKE_CURRENT_LIST_DIR:当前处理的 CMakeLists.txt 所在的路径(源码目录或构建目录)。

  6. CMAKE_CURRENT_LIST_LINE:当前正在处理的 CMakeLists.txt 的行号。

  7. CMAKE_MODULE_PATH:一个用于指定额外的 CMake 模块(.cmake 文件)的搜索路径的列表。

  8. CMAKE_INCLUDE_CURRENT_DIR:如果设置为 ON,则在构建过程中自动将当前处理的 CMakeLists.txt 所在的目录添加到包含路径中。

  9. CMAKE_LIBRARY_OUTPUT_DIRECTORY:库文件的输出目录。

  10. CMAKE_RUNTIME_OUTPUT_DIRECTORY:可执行文件的输出目录

第三行指令add_executable是将一个或多个源文件编译成可执行文件,有了它我们就能将C++生成可执行文件了

add_executable(可执行文件名 cpp文件)

假设现在我们自己定义了一个库并且使用它,我们该如何编写CMakeLists文件?

Main函数:

#include 
#include "Hello.h"
using namespace std;
int main(int argc, const char * const *argv){
  Hello();
  return 0;
}

自定义库:

#include 
void Hello(){
    std::cout<<"Hello World"<<std::endl;
}

库头文件:

#ifndef HELLO_H_
#define HELLO_H_
void Hello();
#endif

文件组织形式:

先端上CMakeLists结果:

cmake_minimum_required(VERSION 3.0)
project(main)
add_library(shard_hello Hello.cpp)
add_executable(${PROJECT_NAME} main.cpp)
target_link_libraries(${PROJECT_NAME} shard_hello)

分别多了add_library()target_link_libraries(),它俩有啥用呢?

add_library(shard_hello Hello.cpp)
add_library(库名 cpp文件名)

add_library作用:将Hello.cpp文件生成shard_hello 库文件,这个库文件会生成在哪文件夹里,默认是生成在build文件夹下,也是执行cmake的文件夹;

target_link_libraries(可执行文件名 库的路径绝对或相对)

target_link_libraries作用:将该库与其他目标(如可执行文件或其他库)进行链接;它怎么找到我指定的库文件呢?

  1. CMake 会首先在当前构建目录中查找要链接的库文件。这是因为 add_library() 命令生成的库文件默认会位于当前构建目录中。

  2. 如果在 target_link_libraries() 命令中直接指定了库的绝对路径或相对路径,CMake 将会使用这个路径来链接库文件,而不再进行其他查找。

  3. 库连接查找顺序:

    • 如果在前两步中找不到要链接的库文件,CMake 将按照默认的库文件搜索路径进行查找。这些默认搜索路径可能包括系统标准路径和其他指定的路径。

    • 系统标准路径:CMake 会在系统预定义的标准路径中查找库文件,这些路径通常是编译器和操作系统默认的库搜索路径。例如,在 Linux 上,通常会在 /usr/lib/usr/local/lib 等目录中查找。

    • CMAKE_LIBRARY_PATH 变量:您可以在 CMakeLists.txt 文件中使用 set() 命令设置 CMAKE_LIBRARY_PATH 变量,指定额外的库文件搜索路径。CMake 会在这些路径中查找库文件。

    • CMAKE_PREFIX_PATH 变量:这个变量通常用于指定第三方库的安装路径。CMake 会在 CMAKE_PREFIX_PATH 变量指定的路径中查找库文件。

    • 如果前面的步骤中找不到要链接的库文件,CMake 将根据库名字及库文件后缀来查找。CMake 会依次查找不同后缀的库文件(如 .lib.a.dll 等),直到找到匹配的库文件。

假设我们现在自己生成了库文件怎么办呢?下面是将Hello.cpp生成库文件Hello.a:

g++ -c Hello.cpp -o Hello.o
ar rcs Hello.a Hello.o    

这时候我们可以使用link_librarie,例如:

link_libraries("C:\\Users\\zhouwei\\Desktop\\c++\\Hello.a")

link_libraries("C:/Users/zhouwei/Desktop/c++/Hello.a")

在window下,上面的路径加不加双引号都可以的。

注:

静态库:库的代码会在编译时被复制并链接到可执行文件中,形成一个独立的可执行文件。静态库的优点是在运行时不需要依赖外部的库文件,使得可执行文件独立于系统环境运行。

动态库:库的代码在编译时不会被复制到可执行文件中,而是在运行时由操作系统加载到内存中,多个程序可以共享同一个动态库。动态库的优点是节省了磁盘空间,并且在多个程序之间共享,减少了系统资源的浪费。

window系统静态库和动态库后缀分别为.lib.dll;

linux系统里静态库和动态库后缀分别为.a.so,

C++中window绝对路径表示:例如:

std::ifstream file1("C:\\Users\\user\\data\\example.txt");

C++中window相对路径表示:例如:

std::ifstream file1(".\\data\\example.txt"); 

C++中linux绝对路径表示,例如:

std::ifstream file1("/home/user/data/example.txt");

C++中linux相对路径表示,例如:

std::ifstream file1("./data/example.txt"); 

不过,仅有.a 或.so 库文件的 话,我们并不知道它里头的函数到底是什么,调用的形式又是什么样的。为了让别人(或者自己)使用这个库,我们需要提供一个头文件,说明这些库里都有些什么。因此,对于库的使用者,只要拿到了头文件和库文件,就可以调用这个库了。

说到这,不得不提一下include_directories这个命令;

当我们在main函数中引入#include "Hello.h"头文件之后,它是怎么找到该头文件呢?

首先它会在源码所在文件夹,即main.cpp所在的文件夹中查找,然后搜索-I指定的目录,接着搜索环境变量C_INCLUDE_PATHCPLUS_INCLUDE_PATHCPATH 指定的目录,最后搜索编译器内定的目录。

接着上面的Hello程序,我们将Hello.h头文件单独放入到include文件中,此时编译失败,无法找到该头文件,该怎么办呢?

两种解决方案:

方案一:

头文件改为如下,在CMakeLists.txt不写include_directories命令;

#include "include/Hello.h"

方案二:

在CMakeLists.txt写include_directories命令

include_dirctories(include)

总结:

include_directories(dir)等同于-I指令,用于指定头文件搜索路径,当include自己搜索不到头文件时,我们就可以使用它了,其中dir表示相对路径。


上面将CMakeLists.txt中基本的命令说完了,回想一下下面命令的用法呀~

include_directories,add_libraries,target_link_libraries()link_librariesadd_executable

接下来我们学习一下find_package指令,用法如下:

find_package(package_name [version] [EXACT] [QUIET] [REQUIRED] [COMPONENTS components...])

这里的package_name是区分大小写的(例如把OpenCV写成opencv找不到该文件),version指定查找库的版本,EXACT表示指定了该标准,则查找的库版本必须和指定版本一致,QUIET表示不输出查找信息,REQUIRED表示找不到就报错,COMPONENTS components...指定查找的库的组件。

那是如何查找的呢?

find_package有两种搜索模式,分别是ModuleConfig模式

默认采用Module,该模式在CMAKE_MODULE_PAHTCMAKE_ROOT变量对应的路径下搜索,前者默认为空,后者则为CMake安装目录下,我电脑为/usr/share/cmake-3.16;

若前一种模式未搜索到,则使用Config模式搜索,搜索方式如下:

  1. 查找_DIR的CMake变量或者环境变量,默认为空;

  2. 查找名为CMAKE_PREFIX_PATHCMAKE_FRAMEWORK_PATHCMAKE_APPBUNDLE_PATH的CMake变量或环境变量路径,默认均为空;

  3. 搜索PATH变量下各路径,先找该路径下是否有Config.cmake-config.cmake的模块文件,如果该路径如果以bin或sbin结尾,则自动回退到上一级目录得到根目录中找。

    $ echo $PATH
    /home/zhanghm/.local/bin:/usr/local/cuda-10.1/bin:/opt/ros/melodic/bin:/home/zhanghm/.local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin
    
  4. 若没有继续找下列目录:

    /(lib/|lib|share)/cmake/*/
    /(lib/|lib|share)/*/ 
    /(lib/|lib|share)/*/(cmake|CMake)/

    其中,表示系统架构,例如ubuntu系统下一般是::/usr/lib/x86_64-linux-gnu(lib/|lib|share)表示可选路径,*表示不区分大小。举个例子若在ubuntu系统下查找opencv库,会查找/usr/lib/x86_64-linux-gnu/OpenCV//usr/lib/x86_64-linux-gnu/cmake/OpenCV//usr/lib/x86_64-linux-gnu/lib/share/OpenCV//usr/lib/x86_64-linux-gnu/share/OpenCV/等。

    在我ubuntu系统的电脑上,我通过该方法,顺利在/usr/lib/x86_64-linux-gnu/cmake/OpenCV找到opencvConfig.cmake文件,该文件里定义了变量`OpenCV_INCLUDE_DIRSOpenCV库头文件包含路径,OpenCV_LIBSOpenCV链接库路径。这样一来,我们就可以使用在前面说的命令中使用opencv的头文件变量和链接库变量了。

总结:库在采用cmake编译时,会生成xxxConfig.cmake文件(xxx表示库名),该文件中定义了变量xxx_INCLUDE_DIRSxxx_LIBRARIES的路径。也就是只要找到xxxConfig.cmake文件,我们就能包含头文件以及链接库了。所以find_package就是为此而诞生的,根据上述搜索方法,顺利找到xxxConfig.cmake配置文件后,会将配置文件的路径定义给OpenCV_DIR。如此一来,每个库的”三兄弟“路径关系便很清晰了。

注:

  1. 使用find_package(包名,REQUIRED)这里的包名是严格区分大小写的;例如OpenCV写成opencv便找不到OpenCVConfig.cmake配置文件了,再如Eigen3写成eigen3也是找不到eigen3库的;

  2. 包的三兄弟${xxx_DIR}${xxx_INCLUDE_DIRS}${xxx_LIBS}也区分大小的;例如${OpenCV_libs}${oPEN_CV_LIBS}是不一样的,链接时会报错,t,通过测试发现xxx写库名或者xxx全部大写是能找到包的;

  3. 如果编译代码时,发现xxxConfig.cmake找不到,说明该库没有编译(默认已安装该库且库大小写没写错),因为编译该库会生成该库的配置文件,一旦在CMakeLists中使用find_package命令,根据以上介绍内容绝对能找到!因此,回过头找到该库单独编译一下或者放到准备编译某代码的同一路径下一起编译!

实战:在SLAM诸多算法中,在CMakeLists文件常常看到这一句find_package(catkin REQUIRED COMPONENTS tf roscpp ...),这该如何理解呢?首先根据上述方法,我在PATH路径下,即:/opt/ros/noetic/share/catkin/cmake/路径下,找到catkinConfig.cmake文件,该文件中定义了catkin_LIBRARIEScatkin_INCLUDE_DIRS,现在大哥都找到了,各小弟找到有难度吗?

你可能感兴趣的:(c++)