参考博客:
1. cmake核心知识点整理
2. cmake - 编译
3. CMake教程
4. Cmake使用教程(看这一篇就够了)(这篇详细的介绍了cmake的安装、创建和使用过程)
5. CMake 入门实战
6. CMake&CMakeList.txt
0、cmake基础知识
0.1 什么是cmake,与make区别
在各种开源项目中,经常会发现项目中除了代码源文件,还包含了 CMakeList.txt、 Makefile 文件,在项目的编译时候需要用到的命令有 cmake 、 make。我们本次想搞清楚他们之前的关系以及CMakeList的语法规则。
正常情况下,我们编写程序的大体流程为:
1)用编辑器(vim、emacs等)编写源代码文件(.h、.cpp等);
2)用编译器编译代码生成目标文件(.o等);
3)用链接器连接目标文件生成可执行文件(.exe等)。
一个程序在编写时,可能需要编写很多的代码文件,以及依赖很多第三方的库。在这种情况下,手动依次编译每个文件会变的非常麻烦,效率低下。
make
是一个自动化的批量编译工具,可以实现用一个命令构建整个工程的目的。但是其执行需要依赖一个规则文件,这个文件就是 Makefile
。 Makefile
文件里详细描述了构建的细节(文件的依赖关系,编译的先后顺序等)。
对于一个大工程来说,编写 Makefile 文件也是一项非常复杂的事情。
cmake
(Cross-platform Make)是一个可以自动生成 Makefile
文件的工具,当然它不只能生成 Makefile
,还能跨平台生成主流IDE(VS, xcode…)构建工程所需的 project 文件。 cmake
的执行同样需要依赖规则文件,这个文件就是 CMakeLists.txt
。
所以,为了能够把一堆c,cc,cpp,h,hpp代码变成可运行的库或者程序,核心还是Makefile,CMakefiles.txt纯粹是为了降低不懂Unix体系的人去编译C/C++代码做的一个自动系统和环境检测和设置工具,同时帮你生成Makefile。
0.2 语法特性介绍
基本语法格式:指令(参数 1 参数 2…)
参数使用括弧括起
参数之间使用空格或分号分开
指令是大小写无关的,参数和变量是大小写相关的
set(HELLO hello.cpp)
add_executable(hello main.cpp hello.cpp)
ADD_EXECUTABLE(hello main.cpp ${HELLO})
变量使用${}方式取值,但是在 IF 控制语句中是直接使用变量名
1、指定编译器并同时设置编译选项
1.1 cmake_minimum_required
- 指定CMake的最小版本要求
//cmake_minimum_required(VERSION versionNumber )
//CMake最小版本要求为2.8.3
cmake_minimum_required(VERSION 2.8.3)
1.2 project
- 定义工程名称,并可指定工程支持的语言
//project(projectname [CXX] [C] [Java])
//指定工程名为HELLOWORLD
project(HELLOWORLD)
1.3 set
- 显式的定义变量
//set(VAR [VALUE] [CACHE TYPE DOCSTRING [FORCE]])
//定义SRC变量,其值为main.cpp hello.cpp
set(SRC sayhello.cpp hello.cpp)
//设置输出文件位置
//设置运行时目标文件(exe、dll)的输出位置,原来的输出文件demo.exe是在build\Debug下,指定其生成路径改为build\exe\Debug下
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/exe)
message(${CMAKE_RUNTIME_OUTPUT_DIRECTORY})
//设置存档目标文件(lib、a)的输出位置
set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib)
set(CMAKE_CXX_COMPILER "clang++" ) # 显示指定使用的C++编译器
set(CMAKE_CXX_FLAGS "-std=c++11") # c++11
set(CMAKE_CXX_FLAGS "-g") # 调试信息
set(CMAKE_CXX_FLAGS "-Wall") # 开启所有警告
set(CMAKE_CXX_FLAGS_DEBUG "-O0" ) # 调试包不优化
set(CMAKE_CXX_FLAGS_RELEASE "-O2 -DNDEBUG " ) # release包优化
CMAKE_CXX_FLAGS
设置的编译选项只会对g++有效,其他编译器不生效。
当然我们也可以通过add_compile_options()
设置,但是通过add_compile_options
会对所有编译器
生效,如:
add_compile_options(-std=c++11) //在编译C代码时就会产生警告信息
2、常用指令
2.1 add_compile_options
- 添加编译参数
语法:add_compile_options(
//添加编译参数 -Wall -std=c++11
add_compile_options(-Wall -std=c++11 -O2)
2.2 add_executable
- 生成可执行文件
语法:add_executable(exename source1 source2 … sourceN)
//编译main.cpp生成可执行文件main
add_executable(main main.cpp)
2.3 option
定义一个开关,相当于1个bool值
# 语法:option(变量值 "帮助信息" [变量值])
# 作用:定义一个开关,相当于1个bool值
# 说明:变量值为 ON 或 OFF ,默认为 OFF
//如果开关被打开,输出当前日期;否则不输出
option(DATE_ENABLE "version date" ON)
if(DATE_ENABLE)
set(data "2022.09.09")
endif()
2.4 configure_file
将输入文件进行替换并生成输出文件
# 语法:configure_file(输入文件 输出文件)
# 作用:将输入文件进行替换并生成输出文件
# 说明:输入文件中形如 @VAR@ 或 ${VAR} 的字符串会被替换为这些变量的当前值,如果未定义则被替换为空字符串。
//添加config file,输入文件是config.h.in输出文件是config.h
configure_file(config.h.in config.h)
2.5 add_subdirectory
添加源文件目录
# 语法:add_subdirectory(source_dir [binary_dir] [EXCLUDE_FROM_ALL])
# 说明:source_dir指定源文件目录的位置,binary_dir 指定编译结果存放的位置,该参数可以不写。
# 作用:向当前工程添加存放源文件的子目录,并可以指定中间二进制和目标二进制存放的位置
//添加src子目录,src中需有一个CMakeLists.txt
add_subdirectory(src)
2.6 aux_source_directory
- 发现一个目录下所有的源代码文件并将列表存储在一个变量中,这个指令临时被用来自动构建源文件列表
语法:aux_source_directory(dir VARIABLE)
//定义SRC变量,其值为当前目录下所有的源代码文件
aux_source_directory(. SRC)
2.7 include_directories
指定所有目标的头文件路径
# 语法:include_directories(dir1 [dir2 ...])
# 作用:指定所有目标的头文件添加路径。如果不写会导致无法找到头文件,编译无法过去。
# 说明:目录会被添加到当前文件的 INCLUDE_DIRECTORIES 属性中
//将/usr/include/myincludefolder 和 ./include 添加到头文件搜索路径
include_directories(/usr/include/myincludefolder ./include)
2.8 target_include_directories
指定一个目标的头文件路径
# 语法:target_include_directories(target 名
<INTERFACE | PUBLIC | PRIVATE> [items1...]
<INTERFACE | PUBLIC | PRIVATE> [items2...] ...)
# 作用:指定一个目标的头文件路径。推荐使用target_include_directories,因为有些目标不需要添加路径,使用include_directories(dir1 [dir2 ...])会增加编译时间。
# 说明:目标文件有 INCLUDE_DIRECTORIES 和 INTERFACE_INCLUDE_DIRECTORIES 两个属性:
# ①INCLUDE_DIRECTORIES:对内头文件目录
# ②INTERFACE_INCLUDE_DIRECTORIES,对外头文件目录
//指定目标的头文件路径
target_include_directories(HelloWorld PUBLIC "${PROJECT_BINARY_DIR}")
2.9 link_directories
- 向工程添加多个特定的库文件搜索路径 —>相当于指定g++编译器的-L参
# 语法:**link_directories(dir1 dir2 …) **
//将/usr/lib/mylibfolder 和 ./lib 添加到库文件搜索路径
link_directories(/usr/lib/mylibfolder ./lib)
2.10 message()
输出某个信息:
//其中输出类型可选择:STATUS(输出前缀为-的信息),SEND_ERROR(产生错误),FATAL_ERROR(终止cmake过程)
message(STATUS "messages")
2.11 file()
对文件和文件夹的操作:比如搜索文件、打开文件、写入文件
# 语法:file(GLOB <variable> [LIST_DIRECTORIES true[false] [RELATIVE <path> ] [CONFIGURE_DEPENDS] [<globbing-expression> ...])
# 作用:主要用于匹配规则在指定的目录内匹配到所需要的文
# 说明:LIST_DIRECTORIES true[false]: 如果为false,目录将会被省略,默认情况下返回是带目录
RELATIVE <path>: 相对路径<path> 返回的结果将不是绝对路径,而是将绝对路径中的<path>部分去掉,返回相对路径
CONFIGURE_DEPENDS:如果该标记位设置,在主构建系统检查目标添加逻辑,必便在构建时重新运行标记的GLOB命令
<globbing-expression>:匹配表达式,表达式类似与正则匹配
//寻找当前路径下的cpp文件,且返回的结果中为/public/home的相对路径
file(GLOB TEST_RESULT LIST_DIRECT true RELATIVE /public/home *.cpp)
//如果要创建文件夹路径:
file(MAKE_DIRECTORY ${xxx}) #创建某个路径文件夹
3、编译库文件
3.1 cmake可以通过add_library
利用源文件生成
动态和静态库文件,指令如下:
add_library(xxx1 SHARED xxx.cpp xxxx.cpp) ##命令根据xxx.cpp和xxxx.cpp生成动态库文件libxxx1.so
add_library(xxx2 STATIC xxx.cpp xxxx.cpp) ##命令根据xxx.cpp和xxxx.cpp生成静态库文件libxxx1.a
add_library(xxx3 MODULE xxx.cpp xxxx.cpp) ##命令根据xxx.cpp和xxxx.cpp生成中间文件libxxx3,该文件不会被加载到其他地方使用
注意:库名称不需要写前缀lib,系统会自动在给出的库名称前面加lib.
库文件生成后,我们需要对其属性进行设置,如重置库文件的名称,设置库文件的版本号等,这些需要通过set_target_properties
命令实现:
// 创建动态库
add_library(algorithms SHARED ${src_path})
// 创建动态链接库:库名称libalgorithms, SHARED表示为.so动态链接库,src_path是.cpp文件所在路径
// 创建静态库
set_target_properties(hello PROPERTIES CLEAN_DIRECT_OUTPUT 1)
// 设置不清除同名动态库hello.so
set_target_properties(hello_static PROPERTIES CLEAN_DIRECT_OUTPUT 1)
// 设置不清除同名静态库hello.a
set_target_properties(hello_static PROPERTIES OUTPUT_NAME "hello")
// 设置静态库的输出名称为hello,从而即使跟动态库重名也能实现
//设置动态库的版本号
set_target_properties(hello PROPERTIES VERSION 1.2 SOVERSION 1)
//VERSION指代动态库版本,SOVERSION指代API版本
//注意:add_library除了可以生产库文件之外,还可以生成目标文件,但不打包成lib命令如下
add_library(objlib OBJECT <src>...)
//这种库只编译源文件生成目标文件,但是不把这些目标文件打包进一个lib。
//当其他的库或者目标文件要使用这些目标文件的时候,会以这样的形式来添加,objlib是这个库的名字
add_library(... $<TARGET_OBJECTS:objlib> ...)
add_executable(... $<TARGET_OBJECTS:objlib> ...)
3.2 使用已知路径
的外部现成
的库文件
实测发现默认的.so路径只有一个/usr/lib,也就是库文件如果不在这个路径下则需要手动链接。
有两个语句都可以实现链接库文件:
link_libraries(path)
是在生成可执行文件之前就指定库文件路径(必须放在add_executable()的前边)。
target_link_libraries(main path)
是在生成target可执行文件之后链接(必须放在add_executable()的后边)
两者的区别就是在定义链接文件时 是在可执行文件生成之前还是之后,都可以实现功能。应尽可能首选target_link_libraries
命令
# 语法:link_libraries([item1 [item2 [...]]]
[[debug|optimized|general] <item>] ...)
# 作用:用于将库链接到稍后添加的所有targets
# 说明:指定在通过诸如add_executable或add_library等命令链接稍后在当前目录或更低目录中创建的任何targets时要使用的库或标志。
add_library(add SHARED ${CMAKE_CURRENT_SOURCE_DIR}/source/add.cpp) #将会在build目录下生成libadd.so
add_library(subtraction SHARED ${CMAKE_CURRENT_SOURCE_DIR}/source/subtraction.cpp) #将会在build目录下生成libsubtraction.so
link_libraries(add subtraction)
add_executable(sample_add ${CMAKE_CURRENT_SOURCE_DIR}/samples/sample_add.cpp)
add_library(libxxx4 <SHARED|STATIC|MODULE|UNKNOWN> IMPORTED)
//IMPORTED 表明此库在工程之外,是target_link_libraries的方便形式。
//外部库的详细信息通过set_target_properties设置
//其中最重要的就是 IMPORTED_LOCATION 属性,它指定外部库的位置。
add_library(libxxx4 STATIC IMPORTED)
set_target_properties(libxxx4 PROPERTIES IMPORTED_LOCATION /path/to/libboost_system.a)
//libxxx4其实就是libboost_system.a
target_link_libraries(wang libxxx4)
//其实上述3条命令等价于
target_link_libraries(wang /path/to/libboost_system.a)
如果已知头文件库文件路径,可直接用
target_include_directories()
和target_link_libraries()
进行添加。
如果不知道头文件库文件路径,可以有另外2个方法: 一种采用find_package()
相当于变量赋值然后用target_include_directories()
和target_link_libraries()
设置即可;
另一种采用find_library()
和find_path()
。
3.3 寻找外部依赖库find_package()
在一个大型项目中,免不了需要导入很多外部依赖库,比如一个项目需要使用到opencv库,我们需要知道头文件的位置,库文件的位置以及库文件的名称,此时我们就需要find_package命令
find_package
: 是一种查找头文件和库文件的方法,是针对第三方库的常用方法(比如CUDA/opencv)。
如果要使用find_package()查找第三方库的头文件和链接库文件路径:注意:采用find_package()
命令cmake的模块**查找顺序**
是:先在变量${CMAKE_MODULE_PATH}
查找,然后在/usr/shared/cmake/Modules/
里边查找。
find_package(CUDA REQUIRED) # 查找某个第三方库的cmake module,比如CUDA代表的就是FindCUDA.cmake这个module
find_package(OpenCV REQUIRED) # 多个库分别查找, 然后统一加到include_directories和link_libraries即可
target_include_directories(tensorrt PUBLIC ${CUDA_INCLUDE_DIRS} ${TENSORRT_INCLUDE_DIR})
target_link_libraries(tensorrt ${CUDA_LIBRARIES} ${TENSORRT_LIBRARY} ${CUDA_CUBLAS_LIBRARIES} ${CUDA_cudart_static_LIBRARY} ${OpenCV_LIBS})
如果要查看cmake所自带支持的所有module和内容,就在如下路径中:
/usr/shared/cmake/Modules/ # 这个路径下所有FindXXXX.cmake都是cmake module文件(大部分是以Find开头,也有不是这么开头的)
如果要为自己写的库定义一个cmake module,则本质上就是先自己查找好头文件、库文件路径,然后欧放到某几个变量中。并且cmake统一规定这几个变量的写法:name_FOUND, name_INCLUDE_DIR, name_LIBRARY
,从而只要知道某些库的module关键字(一般大写),然后运行find_package(关键字),然后就能得到两个变量:关键字_INCLUDE_DIR, 关键字_LIBRARY,从而就可以用include_directories(), link_libraries()进行设置了。
# 如下是一个名为FindTEST.cmake的module的写法: 关键字是TEST
# 先找到自己安装的头文件(test.h): 为了避免安装时prefix路径设置不同,
# 这里同时在两个默认存放头文件的路径寻找,一个是所有用户路径,一个是登录用户路径。
//表示寻找test.h文件,找到则把路径赋值给变量TEST_INCLUDE_DIR
find_path(TEST_INCLUDE_DIR test.h /usr/include/mytest
/usr/local/include/mytest)
# 再找到自己安装的动态库(libmytest.so): 为了避免安装时prefix路径设置不同,这里也同时在两个路径搜索
//NAMES是关键字,表示寻找名称为mytest(实际名称libmytest.so)的头文件,PATH也是关键字,表示在接下来2个路径中寻找
find_library(TEST_LIBRARY NAMES mytest PATH /usr/lib
usr/local/lib)
if(TEST_INCLUDE_DIR AND TEST_LIBRARY) # 如果找到则设置标志
set(TEST_FOUND TRUE)
endif(TEST_INCLUDE_DIR AND TEST_LIBRARY)
3.4 find_library()
和find_path()
查找库文件和头文件
# 语法:find_library(var name HINTS path PATH_SUFFIXES suff1 suff2)
# 作用:搜索名称为name的库文件(实际名称是libname.so),找到后存入var中,并可以带多个关键参数
# 说明:其中HINTS关键参数代表搜索路径,PATH_SUFFIXES代表路径后缀
find_library(_NVINFER_LIB nvinfer HINTS ${TRT_LIB} PATH_SUFFIXES lib lib64)
举例如下:
find_path(dbms_path
NAMES db_xxx.h
PATHS /home/dongfang/cmake_example/find_path
DOC "this is a test for find_path")
查找路径PATH(/home/dongfang/cmake_example/find_path)下是否有db_xxx.h文件,如果有则将"/home/dongfang/cmake_example/find_path"存储在变量dbms_path中
举例如下:
find_library(dbms_library
NAMES libDBMS.so
PATHS /home/dongfang/cmake_example/find_path
DOC "this is a test for find_path")
查找路径PATH(/home/dongfang/cmake_example/find_path)下是否有libDBMS.so文件,如果有则将"/home/dongfang/cmake_example/find_path"存储在变量dbms_library中
在经过查找头文件和库文件路径之后,我们需要对下游负责,明确告知下游头文件路径和库文件路径是否准确找到,可使用如下命令:
include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(DBMS dbms_path dbms_library) //如果dbms_path或者dbms_library无值,直接对下游报错
4、安装
不管是库文件还是目标文件,编出来之后,都需要将其放到一定的位置,方便其他目标文件使用,此时就需要install命令
。
install
可以生成的目标文件包括3种:RUNTIME
是可执行文件, LIBRARY
是动态库, ARCHIVE
是静态库
可以指定安装路径,采用关键字DESTINATION
接路径,注意如果是/
开头的路径就是绝对路径,如果不是斜杠开头则默认基于CMAKE_INSTALL_PREFIX
,也就是{CMAKE_INSTALL_PREFIX}/相对路径
**注意在clion中如果要安装,还需要手动进入项目cmake-build-debug文件夹,执行sudo make install执行,否则clion不会自动帮你安装。
// 安装bin/lib库文件或者头文件:
install(TARGETS MyLib
EXPORT MyLibTargets
LIBRARY DESTINATION lib # 动态库安装路径
ARCHIVE DESTINATION libstatic # 静态库安装路径
RUNTIME DESTINATION bin # 可执行文件安装路径
PUBLIC_HEADER DESTINATION include # 头文件安装路径
)
//安装头文件:
install(FILES hello.h DESTINATION include/hello) //安装头文件
// 安装sh头文件:
install(PROGRAMS runhello.sh DESTINATION bin)// 安装sh文件
install(TARGETS)
仅适用于使用 add_executable()
或 add_library()
创建的目标。对于使用自定义目标生成的文件,使用 install(FILES)
add_custom_target(
main ALL
DEPENDS ${CMAKE_CURRENT_BINARY_DIR}/program
)
add_custom_command(
OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/program
COMMAND ${CMAKE_CURRENT_SOURCE_DIR}/go.sh ${PROJECT_NAME}
)
install(FILES ${CMAKE_CURRENT_BINARY_DIR}/program DESTINATION /usr/local/lib/)
5、编译流程
5.1 构建流程
在 linux 平台下使用 CMake 构建C/C++工程的流程如下:
手动编写 CmakeLists.txt。
执行命令 cmake PATH生成 Makefile ( PATH 是顶层CMakeLists.txt 所在的目录 )。
执行命令make 进行编译。
// important tips
. // 表示当前目录
./ // 表示当前目录
.. // 表示上级目录
../ // 表示上级目录
5.2 两种构建方式
内部构建(in-source build):不推荐使用
内部构建会在同级目录下产生一大堆中间文件,这些中间文件并不是我们最终所需要的,和工程源文件放在一起会显得杂乱无章。
## 内部构建
//在当前目录下,编译本目录的CMakeLists.txt,生成Makefile和其他文件
cmake .
//执行make命令,生成target
make
外部构建(out-of-source build):推荐使用
将编译输出文件与源文件放到不同目录中
## 外部构建
//1. 在当前目录下,创建build文件夹
mkdir build
//2. 进入到build文件夹
cd build
//3. 编译上级目录的CMakeLists.txt,生成Makefile和其他文件
cmake ..
//4. 执行make命令,生成target
make