WhyCMake?
先回答括號中的問題:被逼的。
這三個字是認真的。
不管CMake是否是一個優秀的構建工具,不管你是否認同CMake,都無法否認CMake目前是C++的defactobuildsystem[1]。
所以在社區以及生態的影響下,使用CMake作為構建工具的項目會越來越多。
一個佐證是,即使是微軟、Google以及Facebook這三家公司都有自己的C++構建系統,他們開源的項目仍支持使用CMake構建;并且CMake是除了官方構建系統之外的推薦構建系統。
如何學習CMake
首先反對認為CMake不需要入門的觀點:
cmake還需要入門?這種工具性質的東西,你只要用到哪里學到哪里就行了,如果需要入門那就說明這個工具做的不怎么樣。
這句話后半句是符合事實的,CMake這個工具不僅是做的不怎么樣,而且做的....一言難盡....以至于這個問題下就有回答說不要跳CMake這個坑的;更多的吐槽可以參考如何評價CMake這個問題。
所以自CMakev3.0開始(嚴格的來說是v3.2[2]),社區開始了浩浩蕩蕩的ModernCMake的運動,試圖通過引入一些新的特性并搭配推薦做法來提升用戶的生活質量。
嗯,另一個廣泛喊著modernization的社區剛好是C++社區...
又恰好這倆的現代化用法的相同之處在于需要摒棄老的、傳統的觀念&使用方法,使用新的、現代化的慣用法。
打一個可能不太準的比喻:說CMake用到哪學到哪就行了和說C++用到哪學到哪就行了一樣;如果你本身對這塊領域已經很有經驗,那么這樣做是沒問題的;但是對于一個新人來說,無異于讓他自己去踩雷。
另一個上來就直接扔給對方一個CMakedocumentation讓他學的做法也是非常糟糕的;這就好比一個新手說要學習C++,你直接朝他扔了cppreference...
學習(Modern)CMake的合適路徑
0x00起手式
這里假設題主以及其他想入門CMake的人像我一樣鶸,下面是我個人總結的比較適合的學習路徑。
首先默念三遍并記住口訣:
Declareatarget
Declaretarget'straits
It'sallaboutt
然后clonehttps://github.com/ttroy50/cmake-examples這個項目到本地,把里面的
01-basic(跳過E-installing,因為和依賴有關,后面會說)
02-sub-projects
兩個目錄認真的學習一遍,最好自己能夠動手跟著做一遍。
每學習完一個小節,把前面的三句口訣復習一下
每遇到一個不認識的命令,在EffectiveModernCMake這個頁面里搜索一下,看看這個命令是否取代了某個老命令。
cmake-examples這個項目大概是我在Github上能找到的最符合“深入淺出”這四個字的。不僅例子符合ModernCMake,并且每個步驟都會有詳細的注釋來解釋“是什么,為什么”。
例如第一節的HelloCMake,只有短短的幾行:
#SettheminimumversionofCMakethatcanbeused
#Tofindthecmakeversionrun
#$cmake--version
cmake_minimum_required(VERSION3.5)
#Settheprojectname
project(hello_cmake)
#Addanexecutable
add_executable(hello_cmakemain.cpp)
而StaticLibrary一節,也只包含了最核心的內容:
cmake_minimum_required(VERSION3.5)
project(hello_library)
############################################################
#Createalibrary
############################################################
#Generatethestaticlibraryfromthelibrarysources
add_library(hello_librarySTATIC
src/Hello.cpp
)
target_include_directories(hello_library
PUBLIC
${PROJECT_SOURCE_DIR}/include
)
############################################################
#Createanexecutable
############################################################
#Addanexecutablewiththeabovesources
add_executable(hello_binary
src/main.cpp
)
#linkthenewhello_librarytargetwiththehello_binarytarget
target_link_libraries(hello_binary
PRIVATE
hello_library
)
考慮到我給這個項目也提過PR,這里也算有私心吧XD。
而之所以先看01和02是因為這兩個目錄里的內容已經涵蓋了至少80%的使用場景;一下學太多很容易出現遭遇知識洪水的無力感而早早放棄。
對于入門學習來說,知識是螺旋上升的;同時照著優秀的例子learningbydoing可以很好緩解恐懼感。這里算是給藍色的回答提供教材。
EffectiveModernCMake這里的作用類似于C++那幾本Effective。
0x01緊跟步伐&拓展
前面講過,CMake目前正處于現代化運動當中,新版本的迭代也非常快(最新版已經是v3.16了,并且已經內建支持了PCH),因此有一些之前認為是modernpractice的做法可能在后續的版本已經顯得不那么好了。(別緊張,前面說過知識是螺旋式上升的)
所以如果你還想在這個方面有所深入,最好的方式就看各種會議的talk,包括他們的slides。
這里個人比較推薦的幾個有:
EffectiveCMake:arandomseletionofbestpractices--DanielPfeifer
EmbracingModernCMake:HowtorecognizeandusemodernCMakeinterfaces--StephenKellyATDublinC++Meetup
ModernCMakeformodulardesign--MathieuRopertATCppCon-2017
MoreModernCMake:WorkingWithCMake3.12AndLater--DenizBahadirATMeetingC++2018
對應的slides和演講的視頻都可以搜到。
建議:不要試圖一次全看懂,遇到不理解或者不認可的可以先跳過
如果你發現上面的幾個slides的推薦做法有沖突也別緊張,說不定就是moremodern對modern的一次修正呢。
這里順帶舉個例子來說明一下moderncmake發展的有多快。
因為CMake的變量很容易泄露到其他作用域去,所以ModernCMake有一個慣用法就是AvoidUnnecessaryVariables。對于項目的源文件,不依賴變量直接加到target上,同時可以通過generatorexpression設置不同平臺的文件:
add_executable(hello_cmakemain.cpp)
target_source(hello_cmake
PRIVATE
foo.h
foo.cpp
$<$<BOOL:${WIN32}>:
#forWindows
>
$<$<NOT:$<BOOL:${WIN32}>>:
#forPOSIX
>
)
在v3.11之前,add_xxx()定義一個target時,一定要有一個文件(sourceparameter),但是自v3.11開始這個約束被去掉了。所有源碼文件可以通過target_source()引入,避免出現創建library時有一個源碼文件孤零零的放在add_library()中顯得很不協調。
0x02第三方依賴引入
如何引入&管理第三方依賴是一個時常能夠在C++社區引發廣泛討論的話題。
眾所周知,迄今為止C++至少有89種構建系統以及至少64種依賴管理方案,這還是建立在連module都沒有情況下。
ModernCMake下引入第三方依賴目前整體上主要有兩種方式。
第一種是Install&FindPackage
安裝可以有多種途徑,包括不僅限于:Linux系統的各包管理器;vcpkg或者conan這樣單獨包管理器;甚至自己cmakebuild&install。
安裝好第三方庫之后就可以在CMake種使用find_package()引入依賴。
好處是,CMake編寫方便,以及可以使用不支持CMake的第三方庫
但是這種方式的缺點也很明顯:操作復雜度和二進制模塊的ABI問題。
注:headeronly的庫也可以安裝,但是不影響以下討論。
操作復雜度:
使用系統包管理器安裝的庫版本都比較老,想要新的且指定版本的庫需要費工夫
vcpkg不支持庫版本指定,一不小心所有依賴都更新了;想一想沒有gomod甚至沒有vendor機制前的gopackage
conan倒是支持版本控制,庫更新的也勤快,并且可以針對項目單獨指定依賴;但是庫少是真的,并且對CMake使用上是侵入式的
另外一個蛋疼的是ABI。如果你安裝的依賴庫是希望被不同的項目直接使用,那么你遲早會掉到這個坑里。
在Windows上使用MSVC作為編譯器的話,閉著眼睛都能發現:
Debug和Releasebuild是無法兼容鏈接在一起的
/MT和/MD是無法鏈接在一起的
X86和X64是無法鏈接在一起的
甚至有時候不同minor版本的構建也是無法鏈接在一起的(官方保證ABI但是幫同事解決鏈接問題時又遇到)
那么請問你應該安裝的二進制是哪個配伍的呢?
Linux上坑稍微少一點,但是如果庫作者自己不注意,或者安裝的時候flags手抖了,也容易出現問題。
哦,注意你的GCC是不是開了CXX_11_ABI....
當然你也可以像vcpkg一樣把允許所有的組合下二進制都構建安裝一遍...
但是這里還有一個坑:多個ABI版本的二進制庫如果要共存,需要庫作者仔細謹慎的編寫Install模塊!
前面推薦的slides里有不少關于如何正確編寫Export&Install的內容;以及,你現在可以把cmake-examples里的例子看完了:-)。
如果依賴不怎么經常變化的話,通過dockerimage把第三方包都裝好,感覺上可能會比較省事?
第二種方式是源碼依賴。
源碼依賴的意思是說,你可以訪問依賴的完整源代碼,因此構建的時候是第三方依賴先構建,然后再構建你的工程。
注意,源碼依賴模式下,第三方依賴一樣也可以是靜態庫、動態庫。
優點是,你可以精細控制整個構建過程;因為工具鏈&參數是統一的,所以沒有ABI的問題。
缺點有如下幾個:
構建時間長,因為第三方依賴也要構建;同時不同的項目如果依賴同一個庫,需要分別構建。
需要依賴庫支持CMake,或者有人為依賴庫提供好CMake工程描述
源碼依賴在CMake中通過add_subdirectory()添加依賴源碼文件夾實現。
也可以通過ExternalProjectAdd以及v3.11開始提供的FetchContentModule來完成自動化。
Google是源碼依賴的擁躉,他們開源的abseil-cpp、grpc-cpp等都推薦使用源碼依賴的方式進行構建。
研究過Chromium的人想必這輩子都忘不了depot_tools和gclient....
這里單獨提一下FetchContentModule。
這個模塊是v3.11開始有的,并且在v3.14得到了進一步完善。它的核心功能是幫你從指定的VCS地址(比如gitrepo,SVNrepo)或者URL下載依賴,并自動通過add_subdirectory()進入你的工程。
并且這一切發生在configurationstage。
grpc-cpp的官方文檔就提供了使用FetchContent來引入的例子:
include(FetchContent)
FetchContent_Declare(
gRPC
GIT_REPOSITORYhttps://github.com/grpc/grpc
GIT_TAGv1.25.0
)
FetchContent_MakeAvailable(gRPC)
add_executable(my_exemy_exe.cc)
target_link_libraries(my_exegrpc++)
如果你決定使用FetchContent,不妨考慮一下CPM這個CMake擴展。
CPM在FetchContent和傳統的Packagedependency的基礎之上做了很多整合,支持:
指定一個全局的三方源碼依賴緩存文件夾,避免多個項目重復拉取同一個版本的依賴源碼
通過參數fallback到find_package()的方式使用localpackage。
依賴的options控制
最后,希望能在有生之年看到defactopackagemanager出現的那天。。