《 Cmake实践》-阅读笔记
标签: 阅读笔记 Cmake
Github图书资源:https://github.com/Akagi201/learning-cmake/blob/master/docs/cmake-practice.pdf
Cmake在编译程序和项目管理上带给我们极大的便利,也正是从这本书中(作者称之为学习笔记和Tutorial)让我了解到了Find_Package等一系列命令,开始能看懂Cmake中的一些逻辑,而不是机械的Ctrl+C和Ctrl+V复制CMakeLists.txt(当然,我也会简单的find_package)。
这个笔记更像是-实验记录。
Cmake
- 开源,跨平台(Windows+Cmake,Linux+Cmake)
- 使用广泛(就我目前使用的工具OpenCV、PCL、ORB_SLAM,都是使用Cmake管理)
- 能够管理大型项目(这个目前没有体会到)
我的理解,cmake就是为生成Makefile的,可以帮助我们解决程序的依赖关系(包含头文件,链接库文件),特别是常用的开源库都在使用Cmake进行管理时,掌握这一工具就显得尤其重要了。
安装(小工具)
cmake-gui
Cmake已经集成在Ubuntu 系统中,不需要单独安装。
若在编译时需要输入参数,可以单独安装cmake-gui工具。这个工具让我们在编译时可以不用在命令行中手动输入一系列参数(OpenCV编译时需要指定contrib目录,编译nonfree模块等 -D……),同时也有Win下的版本,十分友好。
sudo apt-get install cmake-qt-gui
更换cmake版本
查看当前的cmake版本
cmake --version
替换当前cmake版本(先尝试将Cmakelists.txt中的cmake_minumum_required中的版本改变,可能就work了,也就不用这么折腾了)
https://blog.csdn.net/meng825/article/details/83341446
使用
cmake既然就是为了生成makefile,所以编译程序的整个过程就变成了:
cmake + make
最简单的Cmake
文件夹中存放源文件(main.cpp),CmakeLists.txt即可。
main.cpp
#include<iostream>
using namespace std;
int main(int argc,char** argv){
cout<<"Hello CMake"<<endl;
cout<<"Hello World"<<endl;
return 0;
}
CMakeLists.txt
cmake_minimum_required(VERSION 3.2) # cmake最小需求版本,如果当前版本低于此版本则报错
project(Cmake_begin) # 项目名称,可以使用 ${PROJECT_NAME}对该名称进行引用
add_executable(Cmake_begin main.cpp) # 生成可执行文件,等同于 g++ main.cpp -o Cmake_begin
# 上述也可以写成 add_executable(${PROJECT_NAME} main.cpp)
然后在命令行中 cmake .即可生成Makefile文件,接着进行make操作,就可以生成可执行文件。
执行make操作:
在文件夹下就可以找到 Cmake_begin 的可执行文件,在终端中输入 ./Cmake_begin 可以直接运行,输出结果。
在编译过程中,如果我们希望看到编译时的一些信息,可以在CMakeLists.txt中进行一些修改:
cmake_minimum_required(VERSION 3.2)
project(Cmake_begin)
#输出一些信息
message(STATUS "This is BINARY dir: " ${Cmake_begin_BINARY_DIR}) # 变量使用${变量名}的方式取值
# 也可以写成 message(STATUS "This is BINARY dir: ${Cmake_begin_BINARY_DIR}") 注意双引号的位置
message(STATUS "This is SOURCE dir: " ${Cmake_begin_SOURCE_DIR})
# 指令是大小写无关的,也可以写成: MESSAGE(STATUS "This is SOURCE dir: " ${Cmake_begin_SOURCE_DIR})
add_executable(Cmake_begin main.cpp)
cmake时输出的结果:
CMakeLists中语法解析
- message(STATUS | SEND_ERROR | FATAL_ERROR “message to display”)
通过终端向用户输出信息,分为三种类型:
- SEND_ERROR 产生错误,生成的过程被跳过
- STATUS 输出前缀为-的信息
- FATAL_ERROR 立即终止所有cmake过程
- project(project_name [CXX] [C] [Java]) # 指定工程的名称
定义工程名称时,同时定义了两个cmake变量
<project_name>_BINARY_DIR : 生成的中间文件及Makefile路径
<project_name>_SOURCE_DIR : 源代码路径
因为我们是在同一个目录下进行编译和生成的,所以上述两个变量都指向文件的根目录“/home/lucas/Blog/Cmake_Learn”,这种编译方式也被称为“内部构建”。
稍微复杂一些的Cmake
程序编译成功,意味着从0到1的突破,值得鼓励。但回顾源代码编译目录,编译所生成的中间文件与源文件都存放在根目录中,十分杂乱。为条理化编译,就需要“外部构建”的方式编译,即在build目录单独进行编译,src目录单独存放源文件,所有动作全部发生在编译目录,对原有的工程没有任何影响。
这样构建的整个工程就清晰简单。重新编辑CMakeLists.txt文档。
其中工程目录下的CMakeLists.txt为:
cmake_minimum_required(VERSION 3.2)
project(Cmake_begin)
#输出一些信息
message(STATUS "This is BINARY dir: ${Cmake_begin_BINARY_DIR}")
MESSAGE(STATUS "This is SOURCE dir: " ${Cmake_begin_SOURCE_DIR})
add_subdirectory(src bin)
src目录下的CMakeLists.txt为:
add_executable(hello main.cpp) # 不要怀疑,就一行
然后进入build目录,执行cmake …,其中 … 代表上一级目录中的CMakeLists.txt(即工程根目录下的主CMakeLists.txt)。
程序中的目录变成:
在build/bin下就生成了二进制的文件 Hello ,在终端中运行可以得到:
CMakeLists中语法解析
add_subdirectory(source_dir [binary_dir] [EXCLUDE_FROM_ALL])
这个命令在当前工程目录指定构建文件的源目录和生成的二进制的输出目录,其中最后一项参数[EXCLUDE_FROM_ALL]指代在编译过程中排除的目录,如example目录。
除此之外,我们还可以通过set命令,重新设置 输出目录:
set(EXECUTABLE_OUTPUT_PATH ${PROJECT_BINARY_DIR}/bin)
set(LIBRARY_OUTPUT_PATH ${PROJECT_BINARY_DIR}/lib)
由于采用的是“外部构建”的方式,因此** P R O J E C T B I N A R Y D I R ∗ ∗ 与 ∗ ∗ {PROJECT_BINARY_DIR}** 与 ** PROJECTBINARYDIR∗∗与∗∗{PROJECT_SOURCE_DIR}** 分别指代不同的目录。
安装(INSTALL)
目前我们已经编译成功,需要安装到系统中以便在编程的时候调用。
这里涉及到cmake中的install命令,分为多种形式(二进制、动态库、静态库以及文件、目录、脚本等),详见cmake-practice P16:
主目录中的CMakeLists.txt:
cmake_minimum_required(VERSION 3.2)
project(Cmake_begin)
#输出一些信息
message(STATUS "This is BINARY dir: ${Cmake_begin_BINARY_DIR}")
MESSAGE(STATUS "This is SOURCE dir: " ${Cmake_begin_SOURCE_DIR})
add_subdirectory(src bin)
install(FILES COPYRIGHT.md README.md DESTINATION share/loc/cmake/t2)
install(PROGRAMS runhello.sh DESTINATION bin)
install(DIRECTORY doc/ DESTINATION share/doc/cmake/t2)
执行:cmake -DCMAKE_INSTALL_PREFIX=/home/lucas/Blog/Cmake_learn/tmp …
然后 make
最后安装:make install
可以看到最后的输出信息,所有内容都安装到位,一切完美~
构建动态库与静态库
既然需要构建库文件,.cpp和.h文件必然就不可少,所以基本的文件目录就是:
其中工程目录(主目录)下的CMakeLists.txt文件为:
cmake_minimum_required(VERSION 3.2)
project(HELLOLIB)
add_subdirectory(lib)
lib目录下的文件:
CMakeLists.txt:
set(LIBHELO_SRC hellp.cpp hellp.h)
add_library(hellp SHARED ${LIBHELO_SRC})
hellp.cpp:
#include "hellp.h"
void HelloFunc(){
std::cout<<"Hello World\n"<<std::endl;
}
hellp.h:
#ifndef HELLO_H
#define HELLO_H
#include <iostream>
void HelloFunc();
#endif
同样采用“外部构建”的方式在build文件夹下执行 cmake + make 命令:
最后在build/lib下生成了libhellp.so共享库文件:
如果想控制库文件的输出目录,则可以使用set命令对库文件的输入位置进行设置:
set(LIBRARY_OUTPUT_PATH <路径>)
CMakeLists中的语法解析
add_library(libname [SHARED|STATIC|MODULE] [EXCLUDE_FROM_ALL] source1 source2 source3 ……sourceN)
值得注意的是,libname中,只需要写name。cmake会自动添加lib 的前缀,[SHARED|STATIC|MODULE]指代不同的库类型,SHARED为动态库,STATIC为静态库。
如果既需要构建动态库,又需要构建静态库,则需要使用set_target_properties命令对静态(动态)库名称重新设置,以生成同一名称的库文件,否则将只生成动态库。
如lib/CMakeLists.txt :
set(LIBHELO_SRC hellp.h hellp.cpp )
add_library(hellp SHARED ${LIBHELO_SRC})
add_library(hellp STATIC ${LIBHELO_SRC})
仅生成了一个libhellp.so,如果我们采用不同库名称,则可以生成两个库文件。
生成了libhellp.so 与 libhellp_static.a文件,但是……我们需要的是同名称的啊!
因此使用 set_target_properties:
其基本语法为:
set_target_properties(target1 target2 PROPERTIES prop1 value1 prop2 value2)
在lib/CMakeLists.txt中设置为:
set(LIBHELO_SRC hellp.h hellp.cpp )
add_library(hellp SHARED ${LIBHELO_SRC})
add_library(hellp_static STATIC ${LIBHELO_SRC})
set_target_properties(hellp_static PROPERTIES OUTPUT_NAME "hellp")
再次cmake + make后得到同名的静态库和动态库(libhellp.a与libhellp.so):
与之对应的就是get_target_property(var target property)
get_target_property(var target property)
在lib/CMakeLists.txt中
set(LIBHELO_SRC hellp.h hellp.cpp )
add_library(hellp SHARED ${LIBHELO_SRC})
add_library(hellp_static STATIC ${LIBHELO_SRC})
set_target_properties(hellp_static PROPERTIES OUTPUT_NAME "hellp")
get_target_property(OUTPUT_VALUE hellp_static OUTPUT_NAME)
MESSAGE(STATUS "This is the hello_static OUTPUT_NAME: ${OUTPUT_VALUE}")
输出表示为:
添加动态库的版本号:
依然是使用set_target_properties
set_target_properties(hellp PROPERTIES VERSION 1.2 SOVERSION 1)
其中VERSION指代动态库版本,SOVERSION指代API版本,然后重新cmake + make就生成了带有动态库版本号的库文件:
使用上一节说明的install操作,对生成的库文件进行安装:
向lib/CMakeLists.txt中添加内容(牢记:在哪里add_executable或add_library,如果需要改变目标存放路径,就在哪里加入上述的定义)
set(LIBHELO_SRC hellp.h hellp.cpp )
add_library(hellp SHARED ${LIBHELO_SRC})
add_library(hellp_static STATIC ${LIBHELO_SRC})
set_target_properties(hellp_static PROPERTIES OUTPUT_NAME "hellp")
get_target_property(OUTPUT_VALUE hellp_static OUTPUT_NAME)
MESSAGE(STATUS "This is the hello_static OUTPUT_NAME: ${OUTPUT_VALUE}")
set_target_properties(hellp PROPERTIES VERSION 1.2 SOVERSION 1)
#安装库文件
install(TARGETS hellp hellp_static
LIBRARY DESTINATION lib
ARCHIVE DESTINATION lib)
#安装头文件
install(FILES hellp.h DESTINATION include/hello)
最后,cmake + make一下:
安装一步到位:
使用外部共享库和头文件
首先建立工程目录下的文件:
根目录下的CMakeLists为:
cmake_minimum_required(VERSION 3.2)
project(NEWHELLO)
add_subdirectory(src)
src/CMakeLists.txt为:
add_executable(main main.cpp)
src/main.cpp为:
#include"hellp.h"
int main(){
HelloFunc();
return 0;
}
然后建立build目录,以“外部构建”的方式进行编译:
可以看到报错内容为找不到相应的头文件,这时候就需要两个Cmake命令:
include_directories([AFTER|BEFORE] [SYSTEM] dir1 dir2 ……)
target_link_libraries(target library1 <debug | optimized> library2 ……)
对外部的头文件和库文件进行链接。
在src/CMakeLists.txt中进行修改为:
include_directories(/home/lucas/Blog/Cmake_Learn/t3/tmp/include/hellp)
add_executable(main main.cpp)
target_link_libraries(main /home/lucas/Blog/Cmake_Learn/t3/tmp/lib/libhellp.so)
# 如果要链接静态库
# target_link_libraries(main /home/lucas/Blog/Cmake_Learn/t3/tmp/lib/libhellp.a)
构建成功,在build/src下生成了二进制文件main:
可以成功运行:
可以使用 ldd 命令查看 main中链接情况(链接动态库):
链接静态库(已经没有了libhellp):
Cmake常用命令
-
add_definitions() # 向C/C++编辑器添加-D定义
如果CMakeLists中添加了add_difinitions(-DENABLE_DEBUG),那么在代码中 #ifdef ENABLE_DEBUG #endif所包围的代码块就会生效。
-
add_dependencies(target-name depend-target1 depend-target2)
定义target依赖的其他target,确保在编译本target之前,其他的target已经被构建。
-
aux_source_directory() # 作用是将一个目录下的所有文件保存为一个变量,方便去调用
作用是发现一个目录下所有的源代码文件并将列表存储在一个变量中,这个指令临时被用来自动构建源文件列表。
如:
aux_source_directory(. SRC_LIST) add_executable(main ${SRC_LIST})
更多命令说明详见:《Cmake Practice》
模块的使用以及自定义
在编译过程中,我们经常使用的命令为 :find_package(pkg-name),比如find_package(OpenCV),这实际上是查找FindOpenCV.cmake的过程,这个文件中就详细定义了OpenCV中库文件与头文件的目录,下图是FindGLEW.cmake
对于每一个Package模块,都会预定义三个变量:
<name>_FOUND # 是否找到该模块
<name>_INCLUDE_DIR # 头文件目录
<name>_LIBRARY # 库文件目录
对于命令find_package命令:
find_package(<name> [major.minor] [QUIET] [NO_MODULE] [[REQUIRED | COMPONENTS] [componets ……]])
QUIET参数是控制 Cmake中的变量 _FIND_QUIETLY 为 True 或 False 的控制变量。
REQUIRED 是控制该共享库是否为必须,如果设置为必须却未找到,则终止编译并报错。
对于外部的模块 ,可以使用set命令让工程找到该模块的头文件或者库目录:
set(CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/cmake)
SLAM及三维重建中常用的模块
Eigen
eigen只有头文件,所以只需要include就可以:
include_directories( "usr/include/eigen3" )
OpenCV
find_package(OpenCV REQUIRED)
# 输出一些基本信息
message(STATUS "OpenCV library status:")
message(STATUS " OpenCV Version: ${OpenCV_VERSION}")
message(STATUS " library:${OpenCV_LIBS}")
message(STATUS " include path:${OpenCV_INCLUDE_DIRS}")
# 包含头文件
include_directories(${OpenCV_INCLUDE_DIRS})
# 链接库目录
target_link_libraries(${PROJECT_NAME} ${OpenCV_LIBS})
Pangolin
find_package(Pangolin)
# 包含头文件
include_directories(${Pangolin_INCLUDE_DIRS})
# 链接库目录
target_link_libraries(${PROJECT_NAME} ${Pangolin_LIBRARIES})
PCL
find_package(PCL REQUIRED COMPONENT common io)
# 包含头文件
include_directories(${PCL_INCLUDE_DIRS})
# 添加编译器控制变量
add_definitions(${PCL_DEFINITIONS})
# 链接库目录
target_link_libraries(project_name ${PCL_LIBRARIES})
Ceres
find_package(Ceres REQUIRED)
# 包含头文件
include_directories(${CERES_INCLUDE_DIRS})
# 链接库目录
target_link_libraries(project_name ${CERES_LIBRARIES})
G2O
find_package(G2O REQUIRED)
# 包含头文件
include_directories(${G20_INCLUDE_DIRS})
# 链接库目录
target_link_libraries(project_name g2o_core g2o_stuff g2o_types_sba g2o_csparse_extension)