歷史背景
CMake是一個構建系統(tǒng)生成器(build-system generator)辕狰。常見的構建系統(tǒng)绢慢,有Visual Studio值朋,XCode腻贰,Make等等吁恍。CMake可以支持不同平臺下構建系統(tǒng)的生成。
思考:ninja是構建系統(tǒng)嗎
CMake的出現(xiàn)已經有接近20年的歷史播演,它的發(fā)展過程也初步經歷了三個階段冀瓦。
- ~2000 (~v2.x) ,剛剛啟動写烤,過程式描述為主翼闽。
- 2000~2014 (v3.0~) ,引入Target概念洲炊。
- 2014~now (~v3.15)感局,有了Target和Property的定義,更現(xiàn)代化暂衡。
概 述
現(xiàn)代化的CMake是圍繞 Target 和 Property 來定義的蓝厌,并且竭力避免出現(xiàn)變量variable的定義。Variable橫行是典型CMake2.8時期的風格」磐剑現(xiàn)代版的CMake更像是在遵循OOP的規(guī)則,通過target來約束link读恃、compile等相關屬性的作用域隧膘。
Target 概念
舊版 CMake 2.0 主要是基于 directory 來構建,很多復用只能靠變量實現(xiàn)寺惫。Modern CMake 最大的改進是引入了 target疹吃,支持了對構建的閉包性和傳播性的控制
,從而實現(xiàn)了構建可以模塊化西雀。
在 Modern CMake 中強烈推薦拋棄舊的 directory 方式萨驶,使用 target 的方式構建整個工程。
Target 分類
Target 中最核心的兩個分類是:executable, library艇肴。
其中 executable 是可執(zhí)行程序腔呜,在不同的操作系統(tǒng)會有不同的格式,同樣一個工程內也可能需要生成多個可執(zhí)行程序再悼。 具體指令如下所示:
add_executable(<name> [WIN32] [MACOSX_BUNDLE]
[EXCLUDE_FROM_ALL]
[source1] [source2 ...])
library 代表鏈接庫核畴,可以分為 share, static, object, module, interface 五個種類。
- share 表示共享庫冲九,在編譯構建過程中谤草,需要鏈接但不會添加到最后的可執(zhí)行文件中。共享庫在程序運行中可以被動態(tài)加載和替換,當被多個程序使用時還可以在內存中被共享丑孩。如果期望 library 可以被獨立的部署和替換的話冀宴,需要選擇這種方式。
- static 表示靜態(tài)庫温学,會在編譯過程中被一起添加生成到可執(zhí)行文件中略贮。當靜態(tài)庫的實現(xiàn)發(fā)生變更時,必須要重新編譯整個系統(tǒng)才可以使用枫浙。使用靜態(tài)庫的一個好處是刨肃,生成的可執(zhí)行程序可以獨立的運行,不再需要依賴這個靜態(tài)庫箩帚。
- module 也是共享庫的一種真友,CMake 中限制了 moudle 類型的 libray 不能被編譯時鏈接,只能通過 dlopen 在運行時動態(tài)加載使用紧帕。
- object 類型的庫表示一組編譯后的文件询筏,并不會打包和鏈接琅轧。使用 object 類型的庫可以避免一些大的源文件被重復的編譯,提升編譯效率。
- interface 類型的并不會編譯輸出文件夺鲜,代表一組接口文件,可以在編譯構建中被其他 target 使用恤左。使用 interface 類型的庫可以把多個模塊公共的接口頭文件作為一個單獨 target 來被引用蔓肯,構建更加高效。
定義庫具體指令如下:
add_library(<name> [STATIC | SHARED | MODULE |OBJECT |INTERFACE] ...)
Target 閉包性
為了實現(xiàn) target 閉包性丽柿,Modern CMake 實現(xiàn) target 與 構建和使用中所有依賴建立綁定關系恢准,從而可以拿來即用。正常情況下編譯一個 target(可執(zhí)行程序或者庫)需要依賴如下所示:
- 源文件列表甫题,通過 target_sources 配置馁筐。
- 頭文件列表,通過 target_include_directories 配置坠非。
- 預編譯宏敏沉,通過 target_compile_definition 配置。
- 編譯選項和特性炎码,通過 target_compile_options盟迟,target_compile_features 配置。
- 鏈接選項潦闲,通過 target_link_options 配置队萤。
在 C/C++軟件系統(tǒng)中,一個 target 中大部分的頭文件是僅在模塊內使用矫钓,為內部接口要尔,僅有小一部分接口頭文件是外部使用舍杜,稱為對外接口。在軟件設計過程中赵辕,要從高內聚低耦合的角度出發(fā)既绩,去嚴格設計每個 target 的外部接口和內部接口。同樣構建過程中还惠,在鏈接不同 target 時也需要明確指明依賴的外部接口文件饲握,從而提高編譯構建的效率。
為了更好支持這個特性蚕键,Modern CMake 針對 target 引入兩個概念:user requriement(用戶依賴) 和 build requirement(編譯依賴)救欧。用戶依賴表示 target 使用方需要的依賴,而編譯依賴表示當前 target 編譯構建時需要依賴锣光。
Modern CMake 增加了三個關鍵字 INTERFACE笆怠、PUBLIC、PRIVATE 分布表示不同作用域誊爹, 下面以添加頭文件依賴命令為例說明:
target_include_directories(<target> [SYSTEM] [BEFORE]
<INTERFACE|PUBLIC|PRIVATE> [items1...]
[<INTERFACE|PUBLIC|PRIVATE> [items2...] ...])
給 target 添加頭文件依賴路徑時:
- INTERFACE : 表示添加的頭文件路徑僅 target 的使用方需要蹬刷,編譯當前 target 并不需要。
- PRIVATE : 表示添加的頭文件路徑僅當前 target 編譯時使用频丘,其他 target 不需要办成。
- PUBLIC : 表示編譯時和鏈接該 target 都需要使用。
在 Modern CMake 中強烈建議為 target 添加依賴接口時搂漠,從使用者角度考慮寫明 INTERFACE迂卢, PRIVATE, PUBLIC。
在 Modern CMake 中推薦使用 target_sources 來添加源文件依賴桐汤,保持每個接口的職責單一而克。
Target 傳播性
當構建工程中 包含比較多的 libary 時,編譯和管理這些 Libary 之間的依賴就變得尤為重要惊科。在 Modern CMake 中,當給 Libary 定義用戶依賴和編譯依賴后亮钦,通過在 target_link_libraries 中定義與其他組件間的依賴關系, 就可以自動傳遞和推演 target 之間的所有編譯依賴馆截。
組件間的依賴關系定義命令如下:
target_link_libraries(<target>
<PRIVATE|PUBLIC|INTERFACE> <item>...
[<PRIVATE|PUBLIC|INTERFACE> <item>...]...)
- PRIVATE: 被依賴 libary 的 user requirement 的會變成當前 target 的 build requirement
- PUBLIC:被依賴 libary 的 user requirement 的會變成當前 target 的 build requirement 和 user requirement.
- INTERFACE:被依賴 libary 的 user requirement 的會變成當前 target 的 user requirement
充分利用 Modern CMake 強大的依賴傳遞功能,合理設計每個 target 間的依賴關系蜂莉。
如果把一個Target想象成一個對象(Object)蜡娶,會發(fā)現(xiàn)兩者的組織方式非常相似:
構造函數(shù):
- add_executable
- add_library
成員函數(shù):
- get_target_property()
- set_target_properties()
- get_property(TARGET)
- set_property(TARGET)
- target_compile_definitions()
- target_compile_features()
- target_compile_options()
- target_include_directories()
- target_link_libraries()
- target_sources()
成員變量:
- Target properties(太多)
知識點和principle
1、在現(xiàn)代IDE中的Multi-configuration
cmake -DCMAKE_BUILD_TYPE=Release .. // 在xcode或vs上不生效映穗,build type選擇后移至IDE中控制窖张,而非cmake階段。
cmake --build . --config release // Apple蚁滋、MSVC使用cmake命令行構建時release包時需要加上--config參數(shù)宿接,否則默認debug赘淮。
在現(xiàn)代IDE中,Build-type一般都不是在CMake config期間能確定的睦霎。如VS梢卸,XCode都支持Multi-configuration,具體使用Debug還是Release是在編譯時才確定副女,那如果Target的依賴路徑或者依賴庫需要區(qū)分Configuration來配置該怎么辦呢蛤高?在傳統(tǒng)CMake中是比較難辦的,target_link_libraries提供了一種手段碑幅,可以用debug和optimized來區(qū)分具體的庫名戴陡,而其他的編譯或鏈接設置則比較困難。在Modern CMake中沟涨,我們可以通過generator-expression來實現(xiàn)恤批。
generator-expression定義為$<...>的形式。該表達式的值有多種形式拷窜,而且支持嵌套使用:
條件表達式
$<condition:true_string> 當條件為1時开皿,表達式為true_string,否則為空
$<IF:condition,true_string,false_string> 當條件為1時,表達式為true_string篮昧,否則為false_string
變量表達式
$<TARGET_EXISTS:target> 當target存在為1赋荆,否則為0
$<CONFIG:cfg> 當config為cfg時為1,否則為0懊昨。這是非常高頻使用的一個表達式窄潭,可以通過它來區(qū)分Debug/Release等不同的config。如下例所示酵颁,通過嵌套使用上述兩個表達式嫉你,可以達到區(qū)分CONFIG來設置依賴庫路徑的目的。
target_link_directories(${PROJECT_NAME} PUBLIC
$<$<CONFIG:Debug>:${CONAN_LIB_DIRS_DEBUG}>
$<$<CONFIG:Release>:${CONAN_LIB_DIRS_RELEASE}>)
思考:假設target A依賴于TARGET B C D E躏惋,希望A和C是debug幽污,B D E是release,是否支持
2簿姨、Forget those commands:
- add_compile_options()
- include_directories()
- link_directories()
- link_libraries()
3距误、install和find_package
- install
install(TARGETS MyLib
EXPORT MyLibTargets
LIBRARY DESTINATION lib # 動態(tài)庫安裝路徑
ARCHIVE DESTINATION lib # 靜態(tài)庫安裝路徑
RUNTIME DESTINATION bin # 可執(zhí)行文件安裝路徑
PUBLIC_HEADER DESTINATION include # 頭文件安裝路徑
)
LIBRARY, ARCHIVE, RUNTIME, PUBLIC_HEADER是可選的,可以根據(jù)需要進行選擇扁位。 准潭、
DESTINATION后面的路徑可以自行制定,根目錄默認為CMAKE_INSTALL_PREFIX,可以試用set方法進行指定域仇,如果使用默認值的話刑然,Unix系統(tǒng)的默認值為 /usr/local, Windows的默認值為 c:/Program Files/${PROJECT_NAME}。
- find_package
SET(MyLib_DIR "${CMAKE_INSTALL_PREFIX}/lib/cmake/MyLib" CACHE FILEPATH "MyLib package." FORCE)
FIND_PACKAGE(MyLib REQUIRED CONFIG)
設置${PROJECT_NAME}_DIR暇务,F(xiàn)IND_PACKAGE使用CONFIG模式泼掠。
如果使用默認的FIND_PACKAGE module模式怔软,行為如何?
4武鲁、ExternalProjects
Create custom targets to build projects in external trees
INCLUDE(ExternalProject)
SET(JSONCPP_SOURCES_DIR ${THIRD_PARTY_DIR}/jsoncpp)
SET(JSONCPP_INSTALL_DIR ${CMAKE_CURRENT_BINARY_DIR}/third_party)
SET(JSONCPP_ROOT ${JSONCPP_INSTALL_DIR} CACHE FILEPATH "jsoncpp root directory." FORCE)
SET(JSONCPP_INCLUDE_DIR "${JSONCPP_INSTALL_DIR}/include" CACHE PATH "jsoncpp include directory." FORCE)
IF(WIN32)
SET(JSONCPP_LIBRARIES "${JSONCPP_INSTALL_DIR}/lib/jsoncpp.lib" CACHE FILEPATH "jsoncpp library." FORCE)
ELSE(WIN32)
SET(JSONCPP_LIBRARIES "${JSONCPP_INSTALL_DIR}/lib/libjsoncpp.a" CACHE FILEPATH "jsoncpp library." FORCE)
ENDIF(WIN32)
INCLUDE_DIRECTORIES(${JSONCPP_INCLUDE_DIR})
ExternalProject_Add(
extern_jsoncpp
${EXTERNAL_PROJECT_LOG_ARGS}
DEPENDS
SOURCE_DIR ${JSONCPP_SOURCES_DIR}
UPDATE_COMMAND ""
DOWNLOAD_COMMAND ""
${EXTERNAL_PROJECT_CMAKE_ARGS}
BUILD_BYPRODUCTS ${JSONCPP_LIBRARIES}
CMAKE_ARGS -DCMAKE_CXX_COMPILER=${CMAKE_CXX_COMPILER}
CMAKE_ARGS -DCMAKE_C_COMPILER=${CMAKE_C_COMPILER}
CMAKE_ARGS -DCMAKE_AR=${CMAKE_AR}
CMAKE_ARGS -DCMAKE_RANLIB=${CMAKE_RANLIB}
CMAKE_ARGS -DCMAKE_CXX_FLAGS=${CMAKE_CXX_FLAGS}
CMAKE_ARGS -DCMAKE_C_FLAGS=${CMAKE_C_FLAGS}
CMAKE_ARGS -DCMAKE_INSTALL_PREFIX=${JSONCPP_INSTALL_DIR}
-DBUILD_SHARED_LIBS=OFF
-DCMAKE_POSITION_INDEPENDENT_CODE=ON
-DCMAKE_MACOSX_RPATH=ON
CMAKE_ARGS -DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE}
CMAKE_CACHE_ARGS -DCMAKE_INSTALL_PREFIX:PATH=${JSONCPP_INSTALL_DIR}
-DCMAKE_POSITION_INDEPENDENT_CODE:BOOL=ON
-DCMAKE_BUILD_TYPE:STRING=${CMAKE_BUILD_TYPE}
)
ADD_LIBRARY(jsoncpp STATIC IMPORTED GLOBAL)
SET_PROPERTY(TARGET jsoncpp PROPERTY IMPORTED_LOCATION ${JSONCPP_LIBRARIES})
ADD_DEPENDENCIES(jsoncpp extern_jsoncpp)
ExternalProject_Add函數(shù)會創(chuàng)建target爽雄,除了編譯還會執(zhí)行download和update step。
思考:ExternalProject創(chuàng)建的target是否會集成其實際編譯工程內target的屬性沐鼠?
5挚瘟、cmake policies
CMake 在添加新特性后可能不會完全兼容舊的 CMake 版本,這導致了在新版本的 CMake 中使用舊的 CMakeLists 文件時可能會存在一些問題饲梭。策略的引入就是幫助用戶和開發(fā)者解決這些問題乘盖,它是 CMake 中用來改善向后兼容性和追蹤兼容性的一種機制。 CMake 中的所有策略都被賦予一個 CMPNNNN 格式的識別符憔涉,其中 NNNN 是一個整數(shù)值订框。策略通常既保留了用于保持舊版本兼容性的舊行為,又包含了讓用戶在新項目中優(yōu)先使用的正確的新行為兜叨。每個策略相關的文檔都會描述舊行為和新行為穿扳,以及引入該策略的原因。
- 設置策略
工程可以設置各種策略來選擇新的或舊的行為国旷。當 CMake 遇到會被特殊策略影響的用戶代碼時矛物,它會檢查工程是否設置了策略。如果沒有設置策略跪但,工程會使用舊行為履羞,并會給出警告要求項目作者設置工程的策略。
有許多方法設置一個策略的行為屡久,最快速的方式是設置所有的策略版本與編寫項目的 CMake 版本一致忆首,設置策略的版本會獲取所有指定的版本或更早的版本中引入的策略。所有指定的版本之后引入的策略會標記為未設置被环,這是為了輸出這些新策略合適的警告信息糙及。設置策略版本的命令為:
cmake_policy (VERSION major.minor[.patch[.tweak]])
cmake_policy (SET CMPNNNN OLD)
cmake_policy (SET CMPNNNN NEW)
使用 NEW 選項的 cmake_policy 命令明確告訴 CMake 使用策略的新行為。
A policy should almost never be set to OLD, except to silence warnings in an otherwise frozen or stable codebase, or temporarily as part of a larger migration path.
if (POLICY CMP0042)
cmake_policy (SET CMP0042 NEW)
endif (POLICY CMP0042)
if (POLICY CMP0063)
cmake_policy (SET CMP0063 NEW)
endif (POLICY CMP0063)
GLOG工程中的cmake
CMP0042
MACOSX_RPATH
is enabled by default.
CMake 2.8.12 and newer has support for using @rpath
in a target’s install name. This was enabled by setting the target property MACOSX_RPATH
. The @rpath
in an install name is a more flexible and powerful mechanism than @executable_path
or @loader_path
for locating shared libraries.
CMake 3.0 and later prefer this property to be ON by default. Projects wanting @rpath
in a target’s install name may remove any setting of the INSTALL_NAME_DIR
and CMAKE_INSTALL_NAME_DIR
variables.
This policy was introduced in CMake version 3.0. CMake version 3.0.2 warns when the policy is not set and uses OLD behavior. Use the cmake_policy command to set it to OLD or NEW explicitly.
思考:cmake policy是否支持用戶自定義筛欢?
6浸锨、使用target_sources而非GLOB
add_library(evolution"")
target_sources(evolution
PRIVATE
${CMAKE_CURRENT_LIST_DIR}/evolution.cpp
PUBLIC
${CMAKE_CURRENT_LIST_DIR}/evolution.hpp
)
VS
file(GLOB srcs "*.cpp")
add_library(${srcs })
OOP思想的modern cmake VS 面向過程
使用GLOB正則匹配后,增刪文件cmake系統(tǒng)無感知悴能,cmake官方強烈建議不使用GLOB的方式引入源文件揣钦。
提問
1雳灾、如果未指定<PRIVATE|PUBLIC|INTERFACE>漠酿,默認行為是?
target_link_libraries(hello-world PUBLIC hello)
target_include_directories(hello-world PUBLIC hello)
2谎亩、如何隔離某個target的編譯參數(shù)炒嘲?
3宇姚、一個target是否可以既是static,又是shared夫凸?
4浑劳、cmake是否同時支持多種generator輸出