NOTE: 今天主要是介紹概念, 所以沒有提供 Colab, 從明天開始, 會陸續把今天提到的內容實作出來~
今天要介紹的 target 是 CMake 的核心, 無論是 dependency 的管理, 測試, 安裝, 還是打包, 使用 target 都可以讓我們事半功倍, 並減少不預期的錯誤
不過在開始介紹 target 之前, 我們先來介紹一下 CMake 中的 Properties 是什麼, 有助於後面理解 target 的概念
Property, 或叫做 "屬性", 是用來 控制 CMake configure 和 build 的行為的變數
記得 Day 4 我們提過的 Variable Scope 嗎?
從執行 cmake
開始, CMake 會 parsing 我們指定的 source directory 和 build directory, 然後自動建立一些 scope, 比如
也可以將這些 scopes 當作是物件, 他們都有自己的一些 properties
通過設定這些 properties, CMake 才知道要做什麼操作
且 "通常下層某些 property 的預設值會從外層拿", 比如 target scope 會根據 directory scope 的 properties 初始化
當然, 不建議這樣初始化下層的 property, 太隱晦, 最好還是明確寫出來比較好~
大部分的 property 都可以透過 set_property()
和 get_property()
來分別設定和取值
但是如果特定 scope 有提供對應指令, 最好用他們提供的專屬指令設定
set_property(entitySpecific
[APPEND | APPEND_STRING]
PROPERTY propertyName values...
)
entitySpecific
GLOBAL
DIRECTORY [dir]
TARGET targets...
SOURCE sources... # Additional options with CMake 3.18
INSTALL files...
TEST tests...
CACHE vars...
GLOBAL
DIRECTORY
dir
就是現在的 source directorySOURCE
PROPERTY
propertyName
會是 CMake 定義好的某些 propertyset_property(TARGET MyApp1 MyApp2
PROPERTY MYPROJ_CUSTOM_PROP val1 val2 val3
)
APPEND
, APPEND_STRING
APPEND
會將值用分號分隔, 合併成 ListAPPEND_STRING
則會直接合併成字串get_property()
和 set_property() 差不多
get_property(resultVar entitySpecific
PROPERTY propertyName
[DEFINED | SET | BRIEF_DOCS | FULL_DOCS]
)
entitySpecific
set_property()
多了 VARIABLE
VARIABLE
的取值比較特別, 他的變數不是接在 VARIABLE
keyword 後面, 而是 propertyName
propertyName
的變數比較好理解DEFINED
define_property()
定義SET
BRIEF_DOCS
NOTFOUND
FULL_DOCS
NOTFOUND
以上除了 SET
之外, 除非有 call define_property()
, 否則都不會有值
define_property()
只定義 property (ex. 如何初始化 / 繼承等), 不會給值
define_property(entityType
PROPERTY propertyName
[INHERITED]
# Mandatory for CMake 3.22 and earlier
[BRIEF_DOCS briefDoc [moreBriefDocs...]]
[FULL_DOCS fullDoc [moreFullDocs...]]
# Requires CMake 3.23 or later
[INITIALIZE_FROM_VARIABLE variableName]
)
BRIEF_DOCS
, FULL_DOCS
get_property()
會用到, 沒啥用entityType
set_property()
一樣INHERITED
get_property()
向上查找, 如果上層路徑都沒有的話, 就去 GLOBAL
找VARIABLE
or CACHE
都不支援此 keyword
INITIALIZE_FROM_VARIABLE
TARGET
propertyCMAKE_
或 _CMAKE_
# Example of setting the variable, but it could instead be set by the user,
# or even left unset to initialize the property with an empty value
set(MYPROJ_SOMETOOL_OPTIONS --verbose)
define_property(TARGET PROPERTY MYPROJ_SOMETOOL_OPTIONS
INITIALIZE_FROM_VARIABLE MYPROJ_SOMETOOL_OPTIONS
)
CMake 有 pre-define 很多 properties, 可以先去官方文件查看
最外層的 scope (因為 cache variable 會影響到下次執行, 所以先不算)
這些 properties 通常是用來
get_cmake_property(resultVar property)
resultVar
property
VARIABLES
CACHE_VARIABLES
COMMANDS
MACROS
COMPONENTS
install()
沒錯, 資料夾也有屬性, 記得前幾天提供的 colab 嗎?
當 CMake 處理到包含 CMakeLists.txt
的資料夾時, 會初始化預設的變數, 這些變數會影響到 target property, 所以也會影響到我們 build code 行為
set_directory_properties(PROPERTIES prop1 val1 [prop2 val2] ...)
get_directory_property(resultVar [DIRECTORY dir] property)
就是 source files 的屬性, 不建議使用, 容易造成 build code 的效能變差
set_source_files_properties(file1 [file2...]
PROPERTIES
propertyName1 value1
[propertyName2 value2] ...
)
get_source_file_property(resultVar sourceFile propertyName)
常見的有以下幾個, target_
後的後綴都是 target 的 property 名稱, 也可以用上面的 set_property()
設定, 但不推薦
include
的路徑, 通常會放該 target 的 headersCache variable 的 properties 都是給 IDE 讀取顯示使用, 由於本系列不會介紹和 IDE 或 GUI 整合, 故不多做介紹, 不影響之後開發
嚴格來說, target scope 不屬於前面提到的 scope 的任何一層, target 其實是橫跨所有路徑的, 儘管他有些 properties 會用 directory property 對應的 property 當作預設值
而 Target 就是我們要 build 的最終目標, 包含
CMake 從 3.0 開始建議以 target 為中心設定各個 properties, 而非從上面提過的 global scope 或是 directory scope 繼承
下面先簡單列出常見的 targets 類型有哪些, 細節會留到之後對應的天數再介紹
add_executable(targetName [WIN32] [MACOSX_BUNDLE]
[EXCLUDE_FROM_ALL]
source1 [source2 ...]
)
WinMain()
而不是 main()
/SUBSYSTEM:WINDOWS
ALL
target (or ALL_BUILD
for Xcode) 會 build 所有資料夾下的檔案, 所以可以透過 EXCLUDE_FROM_ALL
來避免該目標 (資料夾) 被 buildSet this target property to a true (or false) value to exclude (or include) the target from the "all" target of the containing directory and its ancestors. If excluded, running e.g. make in the containing directory or its ancestors will not build the target by default.
add_executable()
也可以 reference 現有的目標產生 alias target
可執行檔
Day 11 會詳細介紹, 這邊先知道個概念即可
add_library(<name> [STATIC | SHARED | MODULE]
[EXCLUDE_FROM_ALL]
[<source>...])
.so
檔.dll
和對應的 .lib
檔.a
檔.lib
檔dlopen()
把他 load 進來嚴格來說, object libraries 不算是 library
因為他只是一堆 object files 的集合, CMake 3.12 前也不能 link
除非必要, 否則盡量不要使用, 容易讓人混淆
而且如果 object files 有重複的 symbols 會導致 link failed
但是 link 其他類型的 libraries 就不會有問題, linker 遇到重複的 symbols 時會用第一個出現的
所以可以的話用 static libraries 就好
add_library(<name> <type> IMPORTED [GLOBAL])
如其名, 我們 import 進來的 libraries 會被 CMake 納入已知的 targets 中, 我們就可以用 target_link_libraries(<name>)
找到他
add_library(<name> ALIAS <target>)
給 target 一個綽號, link 該綽號的 targets 和 link library 本名一樣, 適合用在需要提供相容性的情境
一樣, 這邊先簡單帶過, 會在 Day 19 詳細介紹
和上面提到的 imported libraries 很像, 不過更 general 一點, 除了外部 libraries 以外, 還有外部的套件 (packages), 比如 Gtest
如果該外部套件有提供 *Config.cmake
, 就可以用 find_package()
把外部套件的 targets 加進來
find_package(YourLibrary REQUIRED)
include_directories(${YourLibrary_INCLUDE_DIRS})
add_executable(MyApp main.cpp)
target_link_libraries(MyApp PRIVATE YourLibrary::YourLibrary)
如果該外部套件提供 *.pc
檔, 就可以用 pkg_check_modules()
或是 pkg_search_modules()
把 targets 拉進來
find_package(PkgConfig REQUIRED)
pkg_check_modules(YOURLIB REQUIRED YourLibrary)
include_directories(${YOURLIB_INCLUDE_DIRS})
link_directories(${YOURLIB_LIBRARY_DIRS})
add_executable(MyApp main.cpp)
target_link_libraries(MyApp ${YOURLIB_LIBRARIES})
甚至用更高級的 FetchContent
module 也可以
總之, 將外部套件拉進我們 CMake 的方法很多, 但常見的就是上面幾個
和上面的 imported targets 對應, 只是這次我們的角色從 "使用者" 變成了 "套件提供者", 讓其他 CMake 專案使用我們的 targets
因為有很多的細節需要注意, 比如上面提到的, 使用者可能會用不同的方式 import 我們的 targets, 以 find_package()
來說, 我們需要產生 MyLibraryConfig.cmake
檔; 以 pkg_check_modules()
來說, 我們要產生 MyLibrary.pc
檔; 如果使用者想從 github 上直接拉, 我們要將專案推到 github, 並提供更多檔案, 還需要有特定的專案架構
這些複雜的東西都會在 Day 20 到 Day 23 陸續介紹
Custom targets 也不是一般的 targets, 他是一個抽象的 target, 讓我們可以在 build code 的流程中使用
只要有人 depend 這個 target, 他底下的 command 就會優先被執行
讓我們可以更自由的控制 build code 的過程
add_custom_target(targetName
[ALL]
[command1 [args1...]]
[COMMAND command2 [args2...]]
[DEPENDS depends1...]
[BYPRODUCTS [files...]]
[WORKING_DIRECTORY dir]
[COMMENT comment]
[VERBATIM]
[USES_TERMINAL] # Requires CMake 3.2 or later
[JOB_POOL poolName] # Requires CMake 3.15 or later
[SOURCES source1 [source2...]]
)
ALL
ALL
/ all
target depends custom targetALL
/ all
是由 generators 決定, 給 CMake 擔心就好COMMAND
COMMAND
$<TARGET_FILE:...>
BYPRODUCTS
add_executable(Hasher hasher.cpp)
add_library(MyLib api.cpp)
add_custom_target(CreateHash
COMMAND Hasher $<TARGET_FILE:MyLib>
)
$<TARGET_FILE:…>
$<TARGET_LINKER_FILE:…>
$<TARGET_SONAME_FILE:…>
$<TARGET_PDB_FILE:…>
add_dependencies()
DEPENDS
VERBATIM
VERBATIM
CMakeLists.txt
的時候 escapeVERBATIM
WORKING_DIRECTORY
${CMAKE_CURRENT_BINARY_DIR}
了, 預設就是相對於他BYPRODUCTS
add_custom_command()
GENERATED
以追蹤 dependenciesCOMMENT
USES_TERMINAL
JOB_POOL
USES_TERMINAL
是丟給 console job pool, 這可以把 task 丟給任何 job pool
USES_TERMINAL
擇一SOURCES
呼~看完了這麼多種類的 targets, 接下來終於可以開始寫我們的第一個 CMake 專案啦!