CMake是跨平台编译工具,比make更高级一些。其编译的主要工作是生成CMakeLists.txt文件,然后根据该文件生成Makefile,最后调用make来生成可执行程序或者动态库。所以基本步骤就只有两步
- cmake生成Makefile件
- make执行编译工作
1. 常用语法
1.1. 消息输出
message("Hello world!")1.2. 设置变量
cmake 并没有类,但是我们可以通过定义一组变量,该组变量以相同的前缀来模拟类,并使用嵌套的${}来引用这些变量。例如:
set(LEO_NAME "Leo Lee") file(GLOB_RECURSE FLANN_LIBRARIES ${THIRDPARTY}/flann-1.9.1/lib/lib*)其中:
set用于显示地指定变量,它是静态的,在语句中写的是什么就是什么
file是动态的,它到系统路径中去找文件,找到了即进行赋值
1.3. 算术运算
math(EXPR MY_SUM "1 + 1") math(EXPR DOUBLE_SUM "${MY_SUM} * 2")1.4. 流程控制命令
流程控制命令包括if/endif和while/endwhile。
如,判断当前环境是否为 win32:
if (WIN32) message("You're running CMake on Windows.") else () message("You aren't running CMake on Windows.") endif()例如,使用while/endwhile循环打印出所有小于 20 的斐波那契数列:
set(A "1") set(B "1") while(${A} LESS "20") message("${A}") # 打印 A math(EXPR T "${A} + ${B}") # 计算 A + B 的值,并存储在变量 T set(A "${B}") # 设置变量 A 的值为 B set(B "${T}") # 设置变量 B 的值为 T endwhile()在 cmake 中,变量与数字进行比较的语法与其他语言不同,例如上面的小于比较,就使用了LESS,其他比较条件的使用。
输出:
1
1
2
3
5
8
13
1.5. 列表:分号分隔的字符串
直接以例子说明。以下例子中,将三个参数传递给math:
set(ARGS "EXPR;T;1 + 1") math(${ARGS}) # 等价于 math(EXPR T "1 + 1") message(${T})如果引用变量${}外部添加双引号,则 cmake 会将整个字符串作为一个参数,并保留分号:
set(ARGS "EXPR;T;1 + 1") message("${ARGS}")输出:
EXPR;T;1 + 1
如果${}不带双引号,例如:
set(ARGS "EXPR;T;1 + 1") message(${ARGS})则输出:
EXPRT1 + 1
如果有两个以上的参数传递给set命令,则它们会被分号连接,然后传递给指定的变量:
set(MY_LIST Please visit leehao.me) message("${MY_LIST}")输出:
Please;visit;leehao.me
可以使用 list 命令处理列表:
set(MY_LIST Please visit url leehao.me) list(REMOVE_ITEM MY_LIST "url") message("${MY_LIST}")输出:
Please;visit;leehao.me
可以使用foreach/endforeach处理列表,迭代除第一个参数外的列表的所有项,并将每项赋值给第一个参数变量:
foreach(ARG Please visit url leehao.me) message("${ARG}") endforeach()输出:
Please
visit
url
leehao.me
1.6. 定义函数
在 cmake 中,可以使用function/endfunction来定义一个函数,例如下面的函数将参数值 * 2 后输出:
function(doubleIt VALUE) math(EXPR RESULT "${VALUE} * 2") message("${RESULT}") endfunction() doubleIt("4")输出:
8
函数中定义的变量不会影响调用方的作用域,如果需要返回值,可以将变量传递给函数,然后使用set命令,并指定PARENT_SCOPE参数:
function(doubleIt VARNAME VALUE) math(EXPR RESULT "${VALUE} * 2") set(${VARNAME} "${RESULT}" PARENT_SCOPE) # 设置返回值 endfunction() doubleIt(RESULT "4") # RESULT 变量存储函数的返回值 message("${RESULT}") # 输出:8cmake 中,使用macro/endmacro定义宏。与函数不同,宏内改变变量的值会影响调用方的作用域:
macro(doubleIt VARNAME VALUE) math(EXPR ${VARNAME} "${VALUE} * 2") endmacro() doubleIt(RESULT "4") message("${RESULT}")1.7. CMAKE_CXX_FLAGS设置
# set compiler # -O2 or -O3 优化 # -fPIC 产生与位置无关代码 # -g 调试信息 # -Wall 开启所有的编译警告 # -ffast-math -ffast-math -use_fast_math 浮点数计算加速 # -fdiagnostics-color 颜色设置 set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -O2 -fPIC -std=c++14 -g -Wall -ffast-math -ffast-math -use_fast_math -fdiagnostics-color=auto")1.8. 警告
- -w
禁止编译警告的打印。这个警告不建议使用。大约2012年底,公司代码进行一次大重构,另外从Codeblock集成开发环境转向Makefile管理,Makefile里面默认使用了-w,因而代码一直没有警告,今年个别项目开发中发现一些代码笔误导致的BUG,而这些问题可以从编译警告中知道。前几个月,领导安排我来fix这些警告。为了自己,为了后人,不建议使用-w选项。
- -Werror
将所有的警告当成错误处理。此选项谨慎建议加上。有的开源库警告很多(大名鼎鼎的ffmpeg也有很多警告呢),一一改掉耗时耗人力,必要性也不大。最后,公司代码加入了一个开源库,里面有很多代码警告,可能领导又安排我来fix了。
- -Wfatal-errors
遇到第一个错误就停止,减少查找错误时间。建议加上。很多人遇到错误,没有意识到从第一个开始排查。不管是编译错误,还是程序运行出错,从最开始的错误查起,是个好的做法。
-Wall开启“所有”的警告。强烈建议加上,并推荐该选项成为共识。如case语句没有default处理,有符号、无符号处理,未使用变量(特别是函数有大量未使用的数组,占用栈空间,测试发现,开辟一个未使用的8MB的数组,程序有coredump),用%d来打印地址,或%s打印int值等,都可以发出警告。
- -Wextra
除-Wall外其它的警告,建议加上。
在GCC编译时,加上必要的警告选项,可以避免很多低级错误引发的问题,我就在实际工程代码中遇到用“==”来赋值,我自己写的代码也出现过把“=”当成判断的。但是,有些错误却不是用GCC选项能解决的。比如一般项目都会自定义调试信息打印函数,但在处理可变参数类型时,往往不注意。
1.9. 位置无关代码
PIC就是position independent code,告诉编译器产生与位置无关代码。即产生的代码中,没有绝对地址,全部使用相对地址,故而代码可以被加载器加载到内存的任意位置,都可以正确的执行。这正是共享库所要求的,共享库被加载时,在内存的位置不是固定的。
PIC使.so文件的代码段变为真正意义上的共享。如果不加-fPIC,则加载.so文件的代码段时,代码段引用的数据对象需要重定位,重定位会修改代码段的内容,这就造成每个使用这个.so文件代码段的进程在内核里都会生成这个.so文件代码段的copy。每个copy都不一样,取决于这个.so文件代码段和数据段内存映射的位置。
不加fPIC编译出来的so,是要再加载时根据加载到的位置再次重定位的。(因为它里面的代码并不是位置无关代码)
如果被多个应用程序共同使用,那么它们必须每个程序维护一份so的代码副本了。(因为so被每个程序加载的位置都不同,显然这些重定位后的代码也不同,当然不能共享)
我们总是用fPIC来生成so,也从来不用fPIC来生成a。
fPIC与动态链接可以说基本没有关系,libc.so一样可以不用fPIC编译,只是这样的so必须要在加载到用户程序的地址空间时重定向所有表目。
因此,不用fPIC编译so并不总是不好。
如果你满足以下4个需求/条件:
- 该库可能需要经常更新
- 该库需要非常高的效率(尤其是有很多全局量的使用时)
- 该库并不很大
- 该库基本不需要被多个应用程序共享
如果用没有加这个参数的编译后的共享库,也可以使用的话,可能是两个原因:
- gcc默认开启-fPIC选项
- loader使你的代码位置无关
从GCC来看,shared应该是包含fPIC选项的,但似乎不是所以系统都支持,所以最好显式加上fPIC选项。
1.10. 链接
target_link_libraries(hello A B.a C.so)在上面的命令中,libA.so可能依赖于libB.a和libC.so,如果顺序有错,链接时会报错。还有一点,B.a会告诉CMake优先使用静态链接库libB.a,C.so会告诉CMake优先使用动态链接库libC.so,也可直接使用库文件的相对路径或绝对路径。使用绝对路径的好处在于,当依赖的库被更新时,make的时候也会重新链接。
gcc中库的链接顺序是从右往左进行,所以要把最基础实现的库放在最后,这样左边的lib就可以调用右边的lib中的代码。同时,当一个函数的实现代码在多个lib都存在时,最左边的lib代码最后link,所以也将最终保存下来。
2. RPATH
2.1. 什么是RPATH
在Linux环境下,使用动态链接的程序在运行时会自动链接ld.so这个库(OS X上是dyld),然后通过ld.so来查找链接其它的库。而RPATH就是编译的时候链接到执行文件的链接库路径。OS X在RPATH的设置上和Linux还是有点出入的,OS X的RPATH采用的是绝对路径。
ld.so搜索路径的优先级是这样的:
RPATH,编译链接时加入-rpath参数指明所谓的RUNPATH,这样可执行文件(或者依赖其他动态链接库的动态链接库)就能告诉ld.so到哪里去搜索对应的动态链接库了。LD_LIBRARY_PATH,对于没有设定RPATH的可执行文件或者动态链接库,我们可以用LD_LIBRARY_PATH这个环境变量通知ld.so往哪里查找链接库。/etc/ld.so.conf,系统对ld.so的路径配置文件。/usr/lib、/lib和/usr/local/lib,系统默认路径。
2.2. Cmake和RPATH
在分发程序的时候,执行文件使用的链接库在系统内不一定会有,或者自带了的版本不对,一般都会在程序文件夹内都会附带相应的链接库,所以最好还是把RPATH加上。Cmake对RPATH提供了很多选项支持,我们一般只关注这几个变量就好了:
CMAKE_SKIP_BUILD_RPATH、CMAKE_BUILD_WITH_INSTALL_RPATH、CMAKE_INSTALL_RPATH和CMAKE_INSTALL_RPATH_USE_LINK_PATH。
2.3. 默认RPATH设置
set(CMAKE_SKIP_BUILD_RPATH FALSE) # 编译时加上RPATH set(CMAKE_BUILD_WITH_INSTALL_RPATH FALSE) # 编译时RPATH不使用安装的RPATH set(CMAKE_INSTALL_RPATH "") # 安装RPATH为空 set(CMAKE_INSTALL_RPATH_USE_LINK_PATH FALSE) # 安装的执行文件不加上RPATHCmake在默认情况下,make install会把安装的执行文件的RPATH删掉的,所以就会出现上面我执行安装好的执行文件报错的问题。
2.4. 加上完整的RPATH
Cmake的默认设置我们肯定是不能使用的,我们需要一个安装的时候也要带上RPATH的设置
set(INSTALL_LIB_DIR "${PROJECT_BINARY_DIR}/lib") # 假设安装目录在编译目录的lib子目录内 set(CMAKE_SKIP_BUILD_RPATH FALSE) set(CMAKE_BUILD_WITH_INSTALL_RPATH FALSE) set(CMAKE_INSTALL_RPATH "${CMAKE_INSTALL_PREFIX}/lib") set(CMAKE_INSTALL_RPATH_USE_LINK_PATH TRUE) # 确保链接库不在系统默认安装的目录上时更改到项目lib上 list(FIND CMAKE_PLATFORM_IMPLICIT_LINK_DIRECTORIES ${CMAKE_INSTALL_RPATH} isSystemDir) if("${isSystemDir}" STREQUAL "-1") set(CMAKE_INSTALL_RPATH "${INSTALL_LIB_DIR}") endif("${isSystemDir}" STREQUAL "-1")3. 示例
project(test) # version requirement cmake_minimum_required(VERSION 2.8) if(CMAKE_BUILD_TYPE STREQUAL "Debug") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -O0 -fPIC -std=c++17 -g -Wall -fdiagnostics-color=auto") else() set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -O3 -fPIC -std=c++17 -Wall -fdiagnostics-color=auto") endif() # set path of cmake files set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${PROJECT_SOURCE_DIR}/modules) # set path of executable files set(EXECUTABLE_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/bin) # set path of library set(LIBRARY_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/lib) # find third party libraries find_package(Boost COMPONENTS thread filesystem date_time system REQUIRED) find_package(Eigen3 3.4 REQUIRED) find_package(OpenCV REQUIRED) find_package(glog REQUIRED) find_package(GTSAM REQUIRED) find_package(Ceres REQUIRED) find_package(PCL REQUIRED) # report path of third party libraries message(${EIGEN3_INCLUDE_DIR}) # header files include_directories(${Boost_INCLUDE_DIR}) include_directories(${EIGEN3_INCLUDE_DIR}) include_directories(${OpenCV_INCLUDE_DIRS}) include_directories(${CERES_INCLUDE_DIRS}) include_directories(${GLOG_INCLUDE_DIRS}) include_directories(${PCL_INCLUDE_DIRS}) # set source files AUX_SOURCE_DIRECTORY(${CMAKE_CURRENT_SOURCE_DIR}/src SOURCE_FILES) add_library(${PROJECT_NAME} SHARED ${SOURCE_FILES}) target_link_libraries( ${PROJECT_NAME} ${PCL_LIBRARIES} ${Boost_LIBRARIES} ${OpenCV_LIBS} ${CERES_LIBRARIES} glog ${libLAS_LIBRARIES} ) add_executable(${PROJECT_NAME} main/test.cpp) # link libraries target_link_libraries(${PROJECT_NAME} ${Boost_LIBRARIES} glog ${CERES_LIBRARIES})如果需要链接自己在其它工程下编译的链接库,则需加下下列代码
# 指定链接库目录,链接库的名为common link_directories(${CMAKE_SOURCE_DIR}/../common/lib/) link_libraries(common.so) set(${PROJECT_NAME} common )参考文献
CMake 语言 15 分钟入门教程_Leo的博客-CSDN博客_cmake语言
gcc编译参数-fPIC的一些问题
GCC编译警告选项的学习_李迟的专栏-CSDN博客
[CMAKE] 详解CMakeLists.txt文件 - VictoKu - 博客园
CMake命令target_link_libraries链接库的顺序_zhujianwei31415的专栏-CSDN博客_target_link_libraries
CMAKE和RPATH_章志强的专栏-CSDN博客_cmake rpath