借用黑格尔的名言“存在即合理”,既然CMakeList.txt被设计出来,就有它的一个道理!这样想来!我们内心对它的畏惧是不是就减少了呢!那~现在让我们从“它为什么存在”,“它是什么”以及“它怎么用”三个角度深刻剖析!
我们刚开始学C++中的“Hello World”时,是通过用g++编译器对该cpp进行编译生成可执行文件(g++ main.cpp -o main)。当涉及大型项目,需要用到大量库的时候,采用这种编译方式是极其繁琐的。因此,能不能用一个脚本文件来编译源代码文件,于是Makefile作为一个自动化编译脚本应运而生。然而,它语法实在过于繁琐,如果能有更简单的配置文件就好,能自动化生成Makefile文件。所以,CMakeLists.txt(Cmake)就设计出来了。当然,这只是其中的部分原因,更重要的是有了它,我们能在不同操作系统运行(与操作系统解耦),也就是别人写的代码,也能在我的操作系统上编译运行。
看到这,大家的思路应该也清晰了,总结一下:CMakeLists.txt存在的原因是:
解决跨平台编译问题
使项目构建过程更简单、灵活
历史渊源:在早期,开发人员需要针对每个平台编写不同的构建系统脚本,例如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_DIR
、CMAKE_BINARY_DIR
等,用于指示项目的源代码目录和构建目录的路径。常见内置变量如下:
CMAKE_SOURCE_DIR
:当前 CMakeLists.txt 所在的源码目录的根路径。
CMAKE_BINARY_DIR
:构建目录的根路径,即构建生成的可执行文件、库和其他构建输出的存放位置。
CMAKE_CURRENT_SOURCE_DIR
:当前处理的 CMakeLists.txt 所在的源码目录的路径。
CMAKE_CURRENT_BINARY_DIR
:当前处理的 CMakeLists.txt 所在的构建目录的路径。
CMAKE_CURRENT_LIST_DIR
:当前处理的 CMakeLists.txt 所在的路径(源码目录或构建目录)。
CMAKE_CURRENT_LIST_LINE
:当前正在处理的 CMakeLists.txt 的行号。
CMAKE_MODULE_PATH
:一个用于指定额外的 CMake 模块(.cmake 文件)的搜索路径的列表。
CMAKE_INCLUDE_CURRENT_DIR
:如果设置为 ON
,则在构建过程中自动将当前处理的 CMakeLists.txt 所在的目录添加到包含路径中。
CMAKE_LIBRARY_OUTPUT_DIRECTORY
:库文件的输出目录。
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
作用:将该库与其他目标(如可执行文件或其他库)进行链接;它怎么找到我指定的库文件呢?
CMake 会首先在当前构建目录中查找要链接的库文件。这是因为 add_library()
命令生成的库文件默认会位于当前构建目录中。
如果在 target_link_libraries()
命令中直接指定了库的绝对路径或相对路径,CMake 将会使用这个路径来链接库文件,而不再进行其他查找。
库连接查找顺序:
如果在前两步中找不到要链接的库文件,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_PATH
,CPLUS_INCLUDE_PATH
和 CPATH
指定的目录,最后搜索编译器内定的目录。
接着上面的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_libraries
和add_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
有两种搜索模式,分别是Module
和Config
模式
默认采用Module
,该模式在CMAKE_MODULE_PAHT
和CMAKE_ROOT
变量对应的路径下搜索,前者默认为空,后者则为CMake
安装目录下,我电脑为/usr/share/cmake-3.16
;
若前一种模式未搜索到,则使用Config
模式搜索,搜索方式如下:
查找
的CMake变量或者环境变量,默认为空;
查找名为CMAKE_PREFIX_PATH
、CMAKE_FRAMEWORK_PATH
、CMAKE_APPBUNDLE_PATH
的CMake变量或环境变量路径,默认均为空;
搜索PATH
变量下各路径,先找该路径下是否有
或
的模块文件,如果该路径如果以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
若没有继续找下列目录:
/(lib/|lib|share)/cmake/*/
/(lib/|lib|share)/*/
/(lib/|lib|share)/*/(cmake|CMake)/
其中,
表示系统架构,例如ubuntu系统下一般是::/usr/lib/x86_64-linux-gnu
,(lib/
表示可选路径,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_DIRS
为OpenCV
库头文件包含路径,OpenCV_LIBS
为OpenCV
链接库路径。这样一来,我们就可以使用在前面说的命令中使用opencv的头文件变量和链接库变量了。
总结:库在采用cmake
编译时,会生成xxxConfig.cmake
文件(xxx表示库名),该文件中定义了变量xxx_INCLUDE_DIRS
和xxx_LIBRARIES
的路径。也就是只要找到xxxConfig.cmake
文件,我们就能包含头文件以及链接库了。所以find_package
就是为此而诞生的,根据上述搜索方法,顺利找到xxxConfig.cmake
配置文件后,会将配置文件的路径定义给OpenCV_DIR
。如此一来,每个库的”三兄弟“路径关系便很清晰了。
注:
使用find_package
(包名,REQUIRED)这里的包名是严格区分大小写的;例如OpenCV
写成opencv
便找不到OpenCVConfig.cmake
配置文件了,再如Eigen3写成eigen3也是找不到eigen3库的;
包的三兄弟${xxx_DIR}
、${xxx_INCLUDE_DIRS}
和${xxx_LIBS}
也区分大小的;例如${OpenCV_libs}
和${oPEN_CV_LIBS}
是不一样的,链接时会报错,t,通过测试发现xxx写库名或者xxx全部大写是能找到包的;
如果编译代码时,发现xxxConfig.cmake
找不到,说明该库没有编译(默认已安装该库且库大小写没写错),因为编译该库会生成该库的配置文件,一旦在CMakeLists
中使用find_package
命令,根据以上介绍内容绝对能找到!因此,回过头找到该库单独编译一下或者放到准备编译某代码的同一路径下一起编译!
实战:在SLAM诸多算法中,在CMakeLists
文件常常看到这一句find_package(catkin REQUIRED COMPONENTS tf roscpp ...)
,这该如何理解呢?首先根据上述方法,我在PATH路径下,即:/opt/ros/noetic/share/catkin/cmake/
路径下,找到catkinConfig.cmake
文件,该文件中定义了catkin_LIBRARIES
和catkin_INCLUDE_DIRS
,现在大哥都找到了,各小弟找到有难度吗?