預備說明
分析的是官方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
自行實踐驗證下:
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的一些指令細節重新查過,發現之前的掌握確實還不夠)