CMake实践

深入理解CMake(2):初步解读Caffe的CMake脚本

2019-03-03  本文已影响0人  BetterCV

预备说明

分析的是官方Caffe(https://github.com/BVLC/caffe)的CMake脚本,主要分析了根目录的CMakeLists.txt
Caffe代码的commit id为99bd99795dcdf0b1d3086a8d67ab1782a8a08383

所谓CMake脚本这里指的是CMakeLists.txt和xxx.cmake的统称。

$CAFFE_ROOT/CMakeLists.txt解读

cmake_minimum_required(VERSION 2.8.7)

设定cmake最低版本。高版本cmake提供更多的功能(例如cmake3.13开始提供target_link_directories())或解决bug(例如OpenMP的设定问题),低版本有更好的兼容性。VERSION必须大写,否则不识别而报错。非必须但常规都写。放在最开始一行。


if(POLICY CMP0046)
  cmake_policy(SET CMP0046 NEW)
endif()

cmake中也有if判断语句,需要配对的endif()。
POLICY是策略的意思,cmake中的poilcy用来在新版本的cmake中开启、关闭老版本中逐渐被放弃的功能特性:

Policies in CMake are used to preserve backward compatible behavior across multiple releases


project(Caffe C CXX)

project()指令,给工程起名字,很正常不过了。这列还写明了是C/C++工程,其实没必要写出来,因为CMake默认是开启了这两个的。
这句命令执行后,自动产生了5个变量:

image.png

自行实践验证下:


image.png
image.png
set(CAFFE_TARGET_VERSION "1.0.0" CACHE STRING "Caffe logical version")
set(CAFFE_TARGET_SOVERSION "1.0.0" CACHE STRING "Caffe soname version")

set()指令是设定变量的名字和取值,CACHE意思是缓存类型,是说在外部执行CMake时可以临时指定这一变量的新取值来覆盖cmake脚本中它的取值:CMAKE -Dvar_name=var_value

而最后面的双引号包起来的取值可以认为是”注释“。STRING是类型,不过据我目前看到和了解到的,CMake的变量99.9%是字符串类型,而且这个字符串类型变量和字符串数组类型毫无区分。

变量在定义的时候直接写名字,使用它的时候则需要用${VAR_NAME}的形式。此外还可以使用系统的环境变量,形式为$ENV{ENV_VAR_NAME},例如$ENV{PATH}$ENV{HOME}等。

除了缓存变量,option()指令设定的东西也可以被用CMake -Dxxx=ON的形式来覆盖。


add_definitions(-DCAFFE_VERSION=${CAFFE_TARGET_VERSION})

add_definitions()命令通常用来添加C/C++中的宏,例如:

在这里具体的作用是,设定CAFFE_VERSION这一C/C++宏的值为CAFFE_TARGET_VERSION变量的取值,而这一变量在前面分析过,它是缓存变量,有一个预设的默认值,也可以通过cmake .. -DCAFFE_TARGET_VERSION=x.y.z来指定为x.y.z


list(APPEND CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/cmake/Modules)

这里首先是list(APPEND VAR_NAME VAR_VALUE)这一用法,表示给变量VAR_NAME追加一个元素VAR_VALUE。虽然我写成VAR_NAME,但前面有提到,cmake中的变量几乎都是字符串或字符串数组,这里VAR_NAME你就当它是一个数组就好了,而当后续使用${VAR_NAME}时输出的是”整个数组的值“。(吐槽:这不就是字符串么?为什么用list这个名字呢?搞得像是在写不纯正的LIPS)

具体的说,这里是把项目根目录(CMakeLists.txt在项目根目录,${PROJECT_SOURCE_DIR}表示CMakeLists.txt所在目录)下的cmake/Modules子目录对应的路径值,追加到CMAKE_MODULE_PATH中;CMAKE_MODULE_PATH后续可能被include()find_package()等命令所使用。


include(ExternalProject)
include(GNUInstallDirs)

include()命令的作用:

具体的说,这里是把CMake安装包提供的ExternalProject.cmake(例如我的是/usr/local/share/cmake/Modules/ExternalProject.cmake)文件包含进来。ExternalProject,顾名思义,引入外部工程,各种第三方库什么的都可以考虑用它来弄;

GNUInstallDirs也是对应到CMake安装包提供的GNUInstallDirs.cmake文件,这个包具体细节还不太了解,可自行翻阅该文件。


include(cmake/Utils.cmake)
include(cmake/Targets.cmake)
include(cmake/Misc.cmake)
include(cmake/Summary.cmake)
include(cmake/ConfigGen.cmake)

这里是实打实的包含了在项目cmake子目录下的5各cmake脚本文件了,是Caffe作者们(注意,完整的Caffe不是Yangqing Jia一个人写的)提供的,粗略看了下:

这5个cmake脚本中具体的函数比较多,这里先放过,后续可能考虑逐一解读。


caffe_option(CPU_ONLY  "Build Caffe without CUDA support" OFF) # TODO: rename to USE_CUDA
caffe_option(USE_CUDNN "Build Caffe with cuDNN library support" ON IF NOT CPU_ONLY)
caffe_option(USE_NCCL "Build Caffe with NCCL library support" OFF)
caffe_option(BUILD_SHARED_LIBS "Build shared libraries" ON)
caffe_option(BUILD_python "Build Python wrapper" ON)
set(python_version "2" CACHE STRING "Specify which Python version to use")
caffe_option(BUILD_matlab "Build Matlab wrapper" OFF IF UNIX OR APPLE)
caffe_option(BUILD_docs   "Build documentation" ON IF UNIX OR APPLE)
caffe_option(BUILD_python_layer "Build the Caffe Python layer" ON)
caffe_option(USE_OPENCV "Build with OpenCV support" ON)
caffe_option(USE_LEVELDB "Build with levelDB" ON)
caffe_option(USE_LMDB "Build with lmdb" ON)
caffe_option(ALLOW_LMDB_NOLOCK "Allow MDB_NOLOCK when reading LMDB files (only if necessary)" OFF)
caffe_option(USE_OPENMP "Link with OpenMP (when your BLAS wants OpenMP and you get linker errors)" OFF)

# This code is taken from https://github.com/sh1r0/caffe-android-lib
caffe_option(USE_HDF5 "Build with hdf5" ON)

这里是设定各种option,也就是”开关“,然后后续根据开关的取值(布尔类型的变量,利用ifelse来判断),编写各自的构建规则。
其中caffe_option()cmake/Utils.cmake中定义的,它相比于cmake自带的option()命令,增加了可选的条件控制字段:

image.png

caffe_option()的具体实现还没有看懂,不过看一下所有用到的地方也都是很直观的:

image.png

具体的说,这里就是设定一些“高层级的编译选项开关”,比如是否编matlab接口、是否编python接口,是否用hdf5,是否用openmp,等等。


include(cmake/Dependencies.cmake)

这里是包含Dependencies.cmake,它里面配置了Caffe的绝大多数依赖库:

Boost
Threads
OpenMP
Google-glog
Google-gflags
Google-protobuf
HDF5
LMDB
LevelDB
Snappy
CUDA
OpenCV
BLAS
Python
Matlab
Doxygen

其中每一个依赖库库都直接(在Dependencies.cmake中)或间接(在各自的cmake脚本文件中)使用find_package()命令来查找包

使用find_package(),需要明确两点:

  1. find_package(Xxx)如果执行成功,则提供相应的Xxx_INCLUDE_DIRXxx_LIBRARY_DIR等变量,看起来挺方便,但其实并不是所有的库都提供了同样的变量后缀,其实都是由库的官方作者或第三方提供的xxx.cmake等脚本来得到的,依赖于生态。
  2. find_packge(Xxx)实际中往往是翻车重灾区。它其实有N大查找顺序,而CSDN上的博客中往往就瞎弄一个,你照搬后还是不行。具体例子:

这里暂时不逐一分析每一个包的find_package()情况,只需要注意如果某个包你安装了但是cmake却没有找到,那就需要在find_package()前进行设定,以及之后排查。


if(UNIX OR APPLE)
  set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fPIC -Wall")
endif()

通过设定CMAKE_CXX_FLAGS,cmake生成各自平台的makefile、.sln或xcodeproject文件时设定同样的CXXFLAGS给编译器。如果是.c文件,则由c编译器编译,对应的是CMAKE_C_FLAGS

这里的set()指令设定CMAKE_CXX_FLAGS的值,加入了两个新的flags:"-fPIC"和"-Wall"。实际上用list(APPEND CMAKE_CXX_FLAGS "-fPIC -Wall")是完全可以的。set()只不过是有时候可能考虑设定变量默认值的时候用一用。

-fPIC作用于编译阶段,告诉编译器产生与位置无关代码(Position-Independent Code),则产生的代码中,没有绝对地址,全部使用相对地址,故而代码可以被加载器加载到内存的任意位置,都可以正确的执行。这正是共享库所要求的,共享库被加载时,在内存的位置不是固定的。
-Wall则是开启所有警告。根据个人的开发经验,C编译器的警告不能完全忽视,有些wanring其实应当当做error来对待,例如:


caffe_set_caffe_link()

这里是设置Caffe_LINK这一变量,后续链接阶段会用到。它定义在cmake/Targets.cmake中:

image.png
可以看到,如果是编共享库(动态库),则就叫caffe;否则,则增加一些链接器的flags:-Wl是告诉编译器,后面紧跟的是链接器的flags而不是编译器的flags(现在的编译器往往是包含了调用连接器的步骤)。

这里的几个链接器参数,目前我没有细究过,具体看ld文档:https://ftp.gnu.org/old-gnu/Manuals/ld-2.9.1/html_node/ld_3.html


if(USE_libstdcpp)
  set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -stdlib=libstdc++")
  message("-- Warning: forcing libstdc++ (controlled by USE_libstdcpp option in cmake)")
endif()

USE_libstdcpp这个变量的含义:
在前面已经include(cmake/Dependencies.cmake)的情况下,Dependencies.cmake中的include(cmake/Cuda.cmake)使得Cuda的设定也被载入。而Cuda.cmake中的最后,判断如果当前操作系统是苹果系统并且>10.8、cuda版本小于7.0,那么使用libstdc++而不是libc++

image.png

这时候想起来还没毕业那会儿的一个新闻,说苹果移除了libstdc++而让大家换libc++的事情了,这个USE_libstdcpp就是这个意思了:如果cuda版本老(<7.0)并且OSX版本高(>10.8),就应该用libstdc++来兼容cuda。

这里还有一个小插曲:通常执行cmake后最前面会输出它所使用的C、C++编译器的可执行文件完整路径,然后一个同事的机器上把CXX环境变量设为/usr/bin/gcc,导致编译.cpp文件时是用CXX这一环境变量——也就是gcc——来编译.cpp文件。编译.cpp,如果是C++编译器来编译,链接阶段默认会把标准库链接进去,而现在是C编译器,没有明确指出要链接C++标准库,就会导致链接出问题,虽然他的CMakeLists.txt中曾经加入过libstdc++库,但是显然这很容易翻车,CXX环境变量不应该设定为/usr/bin/gcc


caffe_warnings_disable(CMAKE_CXX_FLAGS -Wno-sign-compare -Wno-uninitialized)

这里添加的编译器flags,是用来屏蔽特定类型的警告的。虽说眼不见心不烦,关掉后少些warning输出,但是0error0warning不应该是中级目标吗?


configure_file(cmake/Templates/caffe_config.h.in "${PROJECT_BINARY_DIR}/caffe_config.h")

这是设定configure file。configure_file()命令是把输入文件(第一个参数)里面的一些内容做替换(比如${var}@var@替换为具体的值,宏定义等),然后放到指定的输出文件(第二个参数)。其实还有其他没有列出的参数。

具体说,这里生成了build/caffe_config.h,里面define了几个变量:

image.png

set(Caffe_INCLUDE_DIR ${PROJECT_SOURCE_DIR}/include)
set(Caffe_SRC_DIR ${PROJECT_SOURCE_DIR}/src)
include_directories(${PROJECT_BINARY_DIR})

这里是设定两个自定义变量Caffe_INCLUDE_DIRCaffe_SRC_DIR的值,只不过它俩比较特殊,想想:如果以后别人find_package(Caffe),其实就需要其中的Caffe_INCLUDE_DIR的值。anyway,那些是后续export命令干的事情,这里忽略。

这里第三句include_directories()命令,把build目录加入到头文件搜索路径了,其实就是为了确保caffe_config.h能被正常include(就一个地方用到它):

image.png

# cuda_compile() does not have per-call dependencies or include pathes
# (cuda_compile() has per-call flags, but we set them here too for clarity)
#
# list(REMOVE_ITEM ...) invocations remove PRIVATE and PUBLIC keywords from collected    definitions and include pathes
if(HAVE_CUDA)
  # pass include pathes to cuda_include_directories()
  set(Caffe_ALL_INCLUDE_DIRS ${Caffe_INCLUDE_DIRS})
  list(REMOVE_ITEM Caffe_ALL_INCLUDE_DIRS PRIVATE PUBLIC)
  cuda_include_directories(${Caffe_INCLUDE_DIR} ${Caffe_SRC_DIR}                         ${Caffe_ALL_INCLUDE_DIRS})

  # add definitions to nvcc flags directly
  set(Caffe_ALL_DEFINITIONS ${Caffe_DEFINITIONS})
  list(REMOVE_ITEM Caffe_ALL_DEFINITIONS PRIVATE PUBLIC)
  list(APPEND CUDA_NVCC_FLAGS ${Caffe_ALL_DEFINITIONS})
endif()

擦亮眼睛:Caffe的cmake脚本中分别定义了Caffe_INCLUDE_DIRCaffe_INCLUDE_DIRS两个变量,只相差一个S,稍不留神容易混掉:不带S的值是$Caffe_ROOT/include,带S的值是各个依赖库的头文件搜索路径(在Dependencies.cmake中多次list(APPEND得到的。类似的,Caffe_DEFINITIONS也是在Dependencies.cmake中设定的。

这里判断出如果有CUDA的话就把Caffe_INCLUDE_DIRS变量中的PUBLICPRIVATE都去掉,把Caffe_DEFINITIONS中的PUBLICPRIVATE也去掉。

add_definitions()中添加的宏,用PUBLIC或PRIVATE修饰,有什么用?
以及,set()或list(APPEND来设定、更新的库名字,用PUBLIC、PRIVATE或INTERFACE修饰,有什么用?这里比较疑惑,尽管我找到了stack overflow上的这篇回答,但是仍然一头雾水:https://stackoverflow.com/questions/26037954/cmake-target-link-libraries-interface-dependencies

anyway,反正这里最后都做了list(REMOTE_ITEM操作,把PUBLICPRIVATE去掉了。


add_subdirectory(src/gtest)
add_subdirectory(src/caffe)
add_subdirectory(tools)
add_subdirectory(examples)
add_subdirectory(python)
add_subdirectory(matlab)
add_subdirectory(docs)

使用add_subdirectory(),意思是说把子目录中的CMakeLists.txt文件加载过来执行,从这个角度看似乎等同于include()命令。实则不然,因为它除了按给定目录名字后需要追加"/CMakeLists.txt"来构成完整路径外,往往都是包含一个target(类似于git中的submodule了),同时还可以设定别的一些参数:

粗略看看各个子目录都是做什么的:

add_custom_target(lint COMMAND ${CMAKE_COMMAND} -P ${PROJECT_SOURCE_DIR}/cmake/lint.cmake)

这里依然是定制的target,具体看来是调用scripts/cpplint.py(谷歌官方C++代码风格检查工具)来执行代码风格检查。(个人觉得G家的C++风格有一点不太好:缩进两个空格太少了,费眼睛,强烈建议和Visual Studio保持一致,用tab并且tab宽度为4个空格)。

所谓linter就是语法检查器,除了cpplint其实还可以用cpp_checkgccclang等,我的vim中配置的就是用cpp_checkgcc,不妨试试:https://github.com/zchrissirhcz/dotvim


if(BUILD_python)
  add_custom_target(pytest COMMAND python${python_version} -m unittest discover -s caffe/test WORKING_DIRECTORY ${PROJECT_SOURCE_DIR}/python )
  add_dependencies(pytest pycaffe)
endif()

如果开启了BUILD_python开关,那么执行一个定制的target(执行pytest)。
add_dependencies()意思是指定依赖关系,这里要求pycaffe目标完成后再执行pytest目标,因为pytest需要用到pycaffe生成的caffe模块。pycaffe在前面提到的add_subdirectory(python)中被构建。


configure_file(
    ${CMAKE_CURRENT_SOURCE_DIR}/cmake/Uninstall.cmake.in
    ${CMAKE_CURRENT_BINARY_DIR}/cmake/Uninstall.cmake
    IMMEDIATE @ONLY)

add_custom_target(uninstall
    COMMAND ${CMAKE_COMMAND} -P
    ${CMAKE_CURRENT_BINARY_DIR}/cmake/Uninstall.cmake)

这里是添加”uninstall"这一target,具体定制的target其实就是执行cmake/Uninstall.cmake脚本。这个脚本根据cmake/Uninstall.cmake.in做变量取值替换等来生成得到。


# ---[ Configuration summary
caffe_print_configuration_summary()

# ---[ Export configs generation
caffe_generate_export_configs()

在Caffe根目录的CMakeLists.txt的最后,是打印各种配置的总的情况,以及输出各种配置(后者其实包含了install()指令的调用)

(2019-03-03 00:31:09 本篇写之前觉得不难,但是断断续续分析下来竟然用了大半天时间,对于CMake的一些指令细节重新查过,发现之前的掌握确实还不够)

上一篇 下一篇

猜你喜欢

热点阅读