iT邦幫忙

2023 iThome 鐵人賽

DAY 8
0

本日內容

  • Properties
  • Target 是什麼?
  • 有哪些 Target?

NOTE: 今天主要是介紹概念, 所以沒有提供 Colab, 從明天開始, 會陸續把今天提到的內容實作出來~

今天要介紹的 target 是 CMake 的核心, 無論是 dependency 的管理, 測試, 安裝, 還是打包, 使用 target 都可以讓我們事半功倍, 並減少不預期的錯誤

不過在開始介紹 target 之前, 我們先來介紹一下 CMake 中的 Properties 是什麼, 有助於後面理解 target 的概念

Properties

Property, 或叫做 "屬性", 是用來 控制 CMake configure 和 build 的行為的變數
記得 Day 4 我們提過的 Variable Scope 嗎?
從執行 cmake 開始, CMake 會 parsing 我們指定的 source directory 和 build directory, 然後自動建立一些 scope, 比如

  • Global scope
  • Directory scope
  • Source sope
  • Target scope
  • Cache variable 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
      • build itself
    • DIRECTORY
      • 如果沒給 dir 就是現在的 source directory
    • SOURCE
      • CMake 3.18 開始有新的 options 能用
  • PROPERTY
    • 通常 propertyName 會是 CMake 定義好的某些 property
    • 也可能是自定義的 property, 建議用 project name 作為 prefix
    set_property(TARGET MyApp1 MyApp2
      PROPERTY MYPROJ_CUSTOM_PROP val1 val2 val3
    ) 
    
  • APPEND, APPEND_STRING
    • APPEND 會將值用分號分隔, 合併成 List
    • APPEND_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
    • 可以想成是從現在 scope 的變數中拿出 propertyName 的變數比較好理解
  • DEFINED
    • 檢查該 property 是否有用 define_property() 定義
  • SET
    • 檢查 property 是否有初始化
  • BRIEF_DOCS
    • 撈出 brief documentation string
    • 沒有的話會拿到 NOTFOUND
  • FULL_DOCS
    • 撈出完整的 documentation
    • 沒有的話會拿到 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() 會用到, 沒啥用
    • 以後可能會 deprecate
  • entityType
    • set_property() 一樣
  • INHERITED
    • get_property() 向上查找, 如果上層路徑都沒有的話, 就去 GLOBAL
    • VARIABLE or CACHE 都不支援此 keyword
      • 因為這兩個 entity type 預設會找 parent directory 了
  • INITIALIZE_FROM_VARIABLE
    • CMake >= 3.23
    • 定義的同時初始化 property
    • 只能用在 TARGET property
    • 變數名稱不能有前綴 CMAKE__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, 可以先去官方文件查看

Global Properties

最外層的 scope (因為 cache variable 會影響到下次執行, 所以先不算)
這些 properties 通常是用來

  • 控制 build tool 的行為
  • 控制 project 架構
  • 提供 build-level 的資訊
get_cmake_property(resultVar property)
  • resultVar
    • 存 global property 的值
  • property
    • name of global property
    • 也可以是 pseudo properties
      • VARIABLES
        • list of non-cache variables
      • CACHE_VARIABLES
        • list of cache variables
      • COMMANDS
        • list of commands, macros, functions
        • commands 只會是 CMake pre-defined 的
        • 拿到的 names 可能和原本定義的不一樣
      • MACROS
        • 只回傳 list of macro names
        • 拿到的 names 可能和原本定義的不一樣
      • COMPONENTS
        • list of components defined by install()
        • Day 20 會詳細介紹

Directory Properties

沒錯, 資料夾也有屬性, 記得前幾天提供的 colab 嗎?
當 CMake 處理到包含 CMakeLists.txt 的資料夾時, 會初始化預設的變數, 這些變數會影響到 target property, 所以也會影響到我們 build code 行為

set_directory_properties(PROPERTIES prop1 val1 [prop2 val2] ...)
get_directory_property(resultVar [DIRECTORY dir] property)

Source Properties

就是 source files 的屬性, 不建議使用, 容易造成 build code 的效能變差

set_source_files_properties(file1 [file2...]
  PROPERTIES
  propertyName1 value1
  [propertyName2 value2] ...
)
get_source_file_property(resultVar sourceFile propertyName)

Target Properties

常見的有以下幾個, target_ 後的後綴都是 target 的 property 名稱, 也可以用上面的 set_property() 設定, 但不推薦

  • target_link_libraries
    • 要 link 的 libraries
  • target_link_options
    • 給 linker 的 options
  • target_include_directories
    • include 的路徑, 通常會放該 target 的 headers
  • target_compile_options
    • 給 compiler 的 options
  • target_compile_definitions
    • 給 compiler 的 definitions
  • target_sources
    • 通常是給 IDE 顯示 header files 才用

Cache Variable Properties

Cache variable 的 properties 都是給 IDE 讀取顯示使用, 由於本系列不會介紹和 IDE 或 GUI 整合, 故不多做介紹, 不影響之後開發

Target 是什麼?

嚴格來說, target scope 不屬於前面提到的 scope 的任何一層, target 其實是橫跨所有路徑的, 儘管他有些 properties 會用 directory property 對應的 property 當作預設值

而 Target 就是我們要 build 的最終目標, 包含

CMake 從 3.0 開始建議以 target 為中心設定各個 properties, 而非從上面提過的 global scope 或是 directory scope 繼承

有哪些 Target?

下面先簡單列出常見的 targets 類型有哪些, 細節會留到之後對應的天數再介紹

Executables

add_executable(targetName [WIN32] [MACOSX_BUNDLE]
  [EXCLUDE_FROM_ALL]
  source1 [source2 ...]
)
  • 不給任何 options 的話, 預設會 build ELF
  • WIN32
    • Windows 上會 build 出 Windows GUI app
    • 主程式入口點為 WinMain() 而不是 main()
    • link 的時候會自動帶上 /SUBSYSTEM:WINDOWS
    • 其他平台自動忽略
  • MACOSX_BUNDLE
    • 有點誤導, 其實所有 Apple 平台皆可 (MacOS, iOS, etc.)
    • 在 Apple 平台上會 build 出 app bundles
    • 產出的檔案會依據平台有所不同
      • MacOS: 產出符合 Apple 要求的資料夾架構
      • iOS: 全部攤平
  • EXCLUDE_FROM_ALL
    • CMake 預設有個 ALL target (or ALL_BUILD for Xcode) 會 build 所有資料夾下的檔案, 所以可以透過 EXCLUDE_FROM_ALL 來避免該目標 (資料夾) 被 build
    • 補充: Doc - EXCLUDE_FROM_ALL

      Set 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

可執行檔

Libraries

Day 11 會詳細介紹, 這邊先知道個概念即可

Normal Libraries

add_library(<name> [STATIC | SHARED | MODULE]
            [EXCLUDE_FROM_ALL]
            [<source>...])
  • Shared libraries
    • Symbols 會被其他 targets 共用
    • 根據不同平台會包出不同檔案, 比如
      • Unix: .so
      • Windows: .dll 和對應的 .lib
  • Static libraries
    • Symbols 會加入到 link 他的 targets
    • 一樣根據不同平台會包出不同檔案
      • Unix: .a
      • Windoes: .lib
  • Module libraries
    • 作為 plugin 使用, 不會被其他 targets link, 而是 runtime 的時候被 load 進來使用, 比如可以用 Linux 的 dlopen() 把他 load 進來

Object Libraries

嚴格來說, object libraries 不算是 library
因為他只是一堆 object files 的集合, CMake 3.12 前也不能 link

除非必要, 否則盡量不要使用, 容易讓人混淆
而且如果 object files 有重複的 symbols 會導致 link failed
但是 link 其他類型的 libraries 就不會有問題, linker 遇到重複的 symbols 時會用第一個出現的

所以可以的話用 static libraries 就好

Imported Libraries

add_library(<name> <type> IMPORTED [GLOBAL])

如其名, 我們 import 進來的 libraries 會被 CMake 納入已知的 targets 中, 我們就可以用 target_link_libraries(<name>) 找到他

Alias Libraries

add_library(<name> ALIAS <target>)

給 target 一個綽號, link 該綽號的 targets 和 link library 本名一樣, 適合用在需要提供相容性的情境

Imported targets

一樣, 這邊先簡單帶過, 會在 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 的方法很多, 但常見的就是上面幾個

Exported targets

和上面的 imported targets 對應, 只是這次我們的角色從 "使用者" 變成了 "套件提供者", 讓其他 CMake 專案使用我們的 targets

因為有很多的細節需要注意, 比如上面提到的, 使用者可能會用不同的方式 import 我們的 targets, 以 find_package() 來說, 我們需要產生 MyLibraryConfig.cmake 檔; 以 pkg_check_modules() 來說, 我們要產生 MyLibrary.pc 檔; 如果使用者想從 github 上直接拉, 我們要將專案推到 github, 並提供更多檔案, 還需要有特定的專案架構

這些複雜的東西都會在 Day 20Day 23 陸續介紹

Custom targets

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...]]
)
  • 可以在 build stage 執行的指令
  • ALL
    • ALL / all target depends custom target
    • ALL / all 是由 generators 決定, 給 CMake 擔心就好
  • COMMAND
    • 建議第一行指令也要加上 COMMAND
    • 可以用來執行
      • 系統上的 script 或 executable
      • CMake 產生的 executable targets
        • 會把 executable target 在 build time 轉成對應路徑
        • (不建議) 但如果是被當作參數就不會自動轉, 要搭配 $<TARGET_FILE:...>
          • CMake 3.20 開始建議用 BYPRODUCTS
        • 所以 不要 寫死路徑
      
      add_executable(Hasher hasher.cpp)
      add_library(MyLib api.cpp)
      
      add_custom_target(CreateHash
        COMMAND Hasher $<TARGET_FILE:MyLib>
      )
      
    • 如果 executable target 被 custom target 使用, CMake 會自動建立 dependency, 先把 executable target build 好才會執行指令
    • 其他能建立 dependency 的 generator expressions
      • $<TARGET_FILE:…>
      • $<TARGET_LINKER_FILE:…>
      • $<TARGET_SONAME_FILE:…>
      • $<TARGET_PDB_FILE:…>
    • add_dependencies()
      • 要 depend 上述以外其他類型的 target 可以用該指令
    • DEPENDS
      • 僅適用 depend files 的情況
      • 且要用絕對路徑
    • 不要依賴任何 shell 特性
      • ex. variable substitution, redirection, etc.
      • 也不要自己 escape commands, 可以用下面的 VERBATIM
  • VERBATIM
    • 確保只有 CMake 會在 parsing CMakeLists.txt 的時候 escape
    • 只要懷疑需要 escape, 就用 VERBATIM
  • WORKING_DIRECTORY
    • 預設路徑是 binary directory
    • 可以是相對路徑或絕對路徑, 相對路徑是相對於 binary directory
      • 所以就不需要 ${CMAKE_CURRENT_BINARY_DIR} 了, 預設就是相對於他
  • BYPRODUCTS
    • 不建議用
      • 建議用 add_custom_command()
    • 乍看有點難理解, 但其實就是隨 custom target 指令產生的檔案
    • CMake 會把這些檔案標成 GENERATED 以追蹤 dependencies
  • COMMENT
    • 字面意思, 會在執行 custom target command 前印 log
  • USES_TERMINAL
    • 直接存取 console
      • 互動式 input 很有用
    • Ninja 使用會塞到 console job pool 去 buffer
      • 可以給 IDE 查看
  • JOB_POOL
    • 只對有 job pool 功能的 build tool 有用, 比如 Ninja
    • 相較於 USES_TERMINAL 是丟給 console job pool, 這可以把 task 丟給任何 job pool
      • 只能和 USES_TERMINAL 擇一
  • SOURCES
    • 主要是讓 IDE 能列出檔案給我們看, 像是 custom target depends 哪些 source files
      • 比如建個 dummy custom target, 僅為了顯示檔案

預告

呼~看完了這麼多種類的 targets, 接下來終於可以開始寫我們的第一個 CMake 專案啦!


上一篇
[Day 7] 如何 Debug?
下一篇
[Day 9] 第一個 CMake 專案!
系列文
30 天 CMake 跨平台之旅30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言