预备说明
分析的是官方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个变量:
-
PROJECT_NAME
,值等于Caffe
-
PROJECT_SOURCE_DIR
,是CMakeLists.txt
所在目录,通常是项目根目录(奇葩的项目比如protobuf,把CMakeLists.txt放在cmake子目录的也有) -
PROJECT_BINARY_DIR
,是执行cmake命令时所在的目录,通常是build
一类的用户自行创建的目录。 -
Caffe_SOURCE_DIR
,此时同PROJECT_SOURCE_DIR
-
Caffe_BINARY_DIR
,此时同PROJECT_BINARY_DIR
官方cmake文档对PROJECT_SOURCE_DIR
和PROJECT_BINARY_DIR
的解释很晦涩:
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++中的宏,例如:
-
add_defitions(-DCPY_ONLY)
,给编译器传递了预定义的宏CPU_ONLY
,相当于代码中增加了一句#define CPU_ONLY
-
add_defitions(-DMAX_PATH_LEN=256)
,则相当于#define MAX_PATH_LEN 256
根据文档,实际上add_definitions()
可以添加任意的编译器flags,只不过像添加头文件搜索路径等flags被交给include_directory()
等命令了。
在这里具体的作用是,设定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()
命令的作用:
- 包含文件,
- 或者,包含模块
所谓包含文件,例如include(utils.cmake)
,把当前路径下的utils.cmake
包含进来,基本等同于C/C++中的#include
指令。通常,include
文件的话文件应该是带有后缀名的。
所谓包含模块,比如include(xxx)
,是说在CMAKE_MODULE_PATH
变量对应的目录,或者CMake安装包自带的Modules目录(比如mac下brew装的cmake对应的是/usr/local/share/cmake/Modules
)里面寻找xxx.cmake
文件。注意,此时不需要写".cmake"这一后缀。
具体的说,这里是把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一个人写的)提供的,粗略看了下:
-
cmake/Utils.cmake
: 定义了一些通用的(适用于其他项目的)函数和宏,用于变量(数组)的打印、合并、去重、比较等(吐槽:cmake语法比较奇葩,相当一段时间之后我才发现它是lisp方式的语法,也就是函数(命令)是一等公民) -
cmake/Targets.cmake
: 定义了Caffe项目本身的一些函数和宏,例如源码文件组织、目录组织等。 -
cmake/Misc.cmake
:杂项,比较抠细节的一些设定,比如通常CMAKE_BUILD_TYPE
基本够用了,但是这里通过CMAKE_CONFIGURATION_TYPES
来辅助设定CMAKE_BUILD_TYPE
,等等 -
cmake/Summary.cmake
:定义了4个打印函数,用来打印Caffe的一些信息,执行CMake时会在终端输出,相比于散落在各个地方的message()
语句会更加系统一些 -
cmake/ConfigGen.cmake
: 整个caffe编译好之后,如果别的项目要用它,那它也应该用cmake脚本提供配置信息。
这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,也就是”开关“,然后后续根据开关的取值(布尔类型的变量,利用if
和else
来判断),编写各自的构建规则。
其中caffe_option()
是cmake/Utils.cmake
中定义的,它相比于cmake自带的option()
命令,增加了可选的条件控制字段:
caffe_option()
的具体实现还没有看懂,不过看一下所有用到的地方也都是很直观的:
具体的说,这里就是设定一些“高层级的编译选项开关”,比如是否编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()
,需要明确两点:
-
find_package(Xxx)
如果执行成功,则提供相应的Xxx_INCLUDE_DIR
、Xxx_LIBRARY_DIR
等变量,看起来挺方便,但其实并不是所有的库都提供了同样的变量后缀,其实都是由库的官方作者或第三方提供的xxx.cmake等脚本来得到的,依赖于生态。 -
find_packge(Xxx)
实际中往往是翻车重灾区。它其实有N大查找顺序,而CSDN上的博客中往往就瞎弄一个,你照搬后还是不行。具体例子:
- 系统包管理工具装的OpenCV不带contrib模块,想使用自行编译的OpenCV但是git clone下来的开源代码执行后找不到自己编译的OpenCV。其实只要知道N大查找顺序,设定CMAKE_PREFIX_PATH中包含OpenCV路径后基本都能找到。
- Caffe基于cmake编译,依赖于Boost,系统里用apt或brew装了Boost,同时也自行编译了高版本Boost,现在Caffe编译时cmake只认自行编译版的Boost,指定N大查找顺序也不能找到系统的Boost。切换已安装的多个Boost给CMake find_package(),这时候需要看看FindBoost.cmake是怎么写的,必须提供它里面说的字样的变量(表示include和lib的查找路径),才能让
find_package()
起作用。 - CMake编译安装了多个版本的Caffe(比如官方Caffe、SSD的Caffe),
~/.cmake
目录下会缓存一个caffe,而现在手头有一个做人脸检测的工程依赖Caffe,而你希望它用官方Caffe而不是SSD-Caffe,这个缓存目录很可能捣乱,这个我认为是某些项目比如Caffe的export输出是多余的,反而容易造成混淆。
这里暂时不逐一分析每一个包的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来对待,例如:
- 函数未定义而被使用(忘记#include头文件)
- 指针类型不兼容(incompatible)
都有可能引发seg fault,甚至bus error。
caffe_set_caffe_link()
这里是设置Caffe_LINK
这一变量,后续链接阶段会用到。它定义在cmake/Targets.cmake
中:
可以看到,如果是编共享库(动态库),则就叫
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++
:
这时候想起来还没毕业那会儿的一个新闻,说苹果移除了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了几个变量:
set(Caffe_INCLUDE_DIR ${PROJECT_SOURCE_DIR}/include)
set(Caffe_SRC_DIR ${PROJECT_SOURCE_DIR}/src)
include_directories(${PROJECT_BINARY_DIR})
这里是设定两个自定义变量Caffe_INCLUDE_DIR
和Caffe_SRC_DIR
的值,只不过它俩比较特殊,想想:如果以后别人find_package(Caffe)
,其实就需要其中的Caffe_INCLUDE_DIR
的值。anyway,那些是后续export
命令干的事情,这里忽略。
这里第三句include_directories()
命令,把build
目录加入到头文件搜索路径了,其实就是为了确保caffe_config.h
能被正常include(就一个地方用到它):
# 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_DIR
和Caffe_INCLUDE_DIRS
两个变量,只相差一个S
,稍不留神容易混掉:不带S的值是$Caffe_ROOT/include
,带S的值是各个依赖库的头文件搜索路径(在Dependencies.cmake
中多次list(APPEND
得到的。类似的,Caffe_DEFINITIONS
也是在Dependencies.cmake
中设定的。
这里判断出如果有CUDA的话就把Caffe_INCLUDE_DIRS
变量中的PUBLIC
和PRIVATE
都去掉,把Caffe_DEFINITIONS
中的PUBLIC
和PRIVATE
也去掉。
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
操作,把PUBLIC
和PRIVATE
去掉了。
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了),同时还可以设定别的一些参数:
- 指定
binary_dir
- 设定
EXCLUDE_FROM_ALL
,也就是”搞一个独立的子工程“,此时需要有project()
指令,并且不被包含在生成的.sln工程的ALL
目标中,需要单独构建。
粗略看看各个子目录都是做什么的:
-
src/gtest
,googletest的源码 -
src/caffe
,caffe的源码构建,因为前面做了很多操作(依赖库、路径,etc),这里写的就比较少。任务只有2个:构建一个叫做caffe
的库,以及test
。 -
tools
,这一子目录下每一个cpp文件都生成一个xxx.bin
的目标,而最常用的就是caffe训练接口build/caffe
这个可执行文件了。 -
examples
,这一子目录下有cpp_classification
的C++代码,以及mnist,cifar10,siamse这三个例子的数据转换的代码,这四个都是C++文件,每一个都被编译出一个可执行 -
python
,pycaffe接口,python/caffe/_caffe.cpp
编译出动态库 -
matlab
,matlab接口,./+caffe/private/caffe_.cpp
编译出?编译出一个定制的目标,至于是啥类型,也许是动态库吧,玩过matlab和C/C++混编的都知道,用mex编译C/C++为.mexa文件,然后matlab调用.mexa文件,其实就是动态库 -
docs
,文档,doxygen、jekyll都来了,以及拷贝每一个.ipynb文件。没错,add_custom_command()
能定制各种target,只要你把想要执行的shell脚本命令用cmake的语法来写就可以了,很强大。
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_check
、gcc
、clang
等,我的vim中配置的就是用cpp_check
和gcc
,不妨试试: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的一些指令细节重新查过,发现之前的掌握确实还不够)
网友评论