Professional CMake, A Practical Guide
--
## Chapter 16. Compiler And Linker Essentials (ehung)
上一章我們了解了各種 build type 和其對應的 compiler 和 linker 會如何運作, 這章會著重在 **如何透過 CMake 控制 compiler 和 linker 的行為**
在開始之前, 先來講古一下
CMake 早期 (CMake < 3.0) 的設計是傾向 global build (NOTE: 透過 CMake scope / directory scope variables 控制 build 的行為), 隨著 CMake 發展, 現在已經捨棄 global build 的開發模式, 轉向 target-centered build (NOTE: 即透過控制 target properties 建立 dependencies 和 build flow)
Target-centered build 的好處是
- 明確定義了 targets 間的關係
- 更容易控制 compiler 和 linker 的行為 (NOTE: by target)
### 16.1 Target Properties
如上所述, 我們可以用 target properties 來更好的控制 compiler / linker 的行為, 因此這節會介紹比較常用的幾個 target properties
:::warning
不建議直接修改以下提到的 properties, 建議透過 CMake 提供的 `target_XXX` function 設定
:::
#### 16.1.1. Compiler Flags
先從基本的 target properties 開始
`INCLUDE_DIRECTORIES`
- Header search paths
- List of directories (absolute path)
- Add compiler flags based on compiler type
- ex. `-I`, `/I`
- target property 繼承當前對應的 directory property 作為初始值
`COMPILE_DEFINITIONS`
- List of compiler definitions
- Add definitions to the command line based on compiler type
- ex. `VAR`, `VAR=VALUE` becomes `-DVAR...` or `/DVAR...`
- 當 target 建立時, 當前的 directory property 會加入對應的 target property, 組成最終的 compiler command line
`COMPILE_OPTIONS`
- List of compiler options
- Compiler flags neither header search paths nor definitions go here
- target property 繼承當前對應的 directory property 作為初始值
- De-duplicate duplicated compiler options
:::info
`COMPILER_FLAGS` (deprecated)
雖然用途和 `COMPILER_OPTIONS` 類似, 但 `COMPILER_FLAGS` 會被當作字串直接組 command line, 需要開發者自己 escape; 也不支援 generator expression
`COMPILER_OPTIONS` 則是 list, 且會先被 CMake escape / quoting 後才組 command line, 比較安全
:::
##### `INTERFACE_*` properties
上述三個 properties 都有對應的 `INTERFACE_*` 系列, 這些 properties 會被加到 consuming target 而非當前的 target, 所以 `INTERFACE_*` properties 又稱作 *usage requirements*; non-`INTERFACE_*` properties 則稱作 *build requirements*
大多數 target properties 都有對應的 usage requirements, 除了 `IMPORTED` 和 `INTERFACE`, 這兩個 properteis 只有 usage requirements, 會在 Chapter 19, *Target Types* 介紹
Section 16.8.2, "System Header Search Paths" 會介紹 `INTERFACE_INCLUDE_DIRECTORIES` 的其他用法
##### Generator expression
上述 target properties 也可以搭配 generator expression 使用, 比如想要針對特定 compiler 加入不同的 `COMPILE_OPTIONS`
也可以搭配 `INTERFACE_INCLUDE_DIRECTORIES` 來控制要傳給 consuming targets 的路徑, 這會在 Section 16.3.1, "Building, Installing And Exporting" 介紹
:::info
Source file properties
如果想要更精細的控制每個檔案的 build process, CMake 3.11 開始也提供了 source file level 的 properties
大多數特性都和前面介紹的 target properties 一樣, 除了 generator expression 的支援版本較落後
- `COMPILE_DEFINITIONS`: CMake 3.8
- o.w.: CMake 3.11
且 Xcode 不支援 source file properties, 所以 `$<CONFIG>`, `$<CONFIG:...>` 不能用在 source file 上
其餘部分請回顧 Section 10.5, "Source Properties" 討論過的, 使用 source file properties 會造成的效能問題~
:::
#### 16.1.2. Linker Flags
和 linker 相關的 target properties 與前面的 compiler flags 類似, 只是有些是在 CMake 3.13 才引進, 且只有部分 properties 支援 interface 和 generator expressions
`LINK_LIBRARIES`
- Support interface (`INTERFACE_LINK_LIBRARIES`)
- List of libraries target links to
- Library can be
- Absolute path
- Library name without prefix or suffix (ext, e.g. .a, .so, .dll)
- CMake library target (preferred)
- CMake 會根據平台組出 linker command
`LINK_OPTIONS`
- CMake >= 3.13
- Support interface (`INTERFACE_LINK_OPTIONS`)
- General linker flags
- target property 繼承當前對應的 directory property 作為初始值
- De-duplicate duplicated linker options
- 不支援 static libraries
- NOTE: interface 版沒有此限制
`STATIC_LIBRARY_OPTIONS`
- 和 `LIBRARY_OPTIONS` 類似, 但只能給 static library target 使用 (傳給 librarian / archiver)
`STATIC_LIBRARY_FLAGS`
:::info
`LINK_FLAGS` / `STATIC_LIBRARY_FLAGS` (deprecated)
和 `COMPILE_FLAGS` 狀況類似, 都會直接組成 linker command, 會遇到 escaping 的問題, 沒有 interface 且不支援 generator expression, 好消息是可以根據 config 設定: `LINK_FLAGS_<CONFIG>`, `STATIC_LIBRARY_FLAGS_MCONFIG>`
除非 CMake < 3.13, 否則一律建議用 `LINK_OPTIONS` / `STATIC_LIBRARY_OPTIONS`
:::
:::info
`LINK_INTERFACE_LIBRARIES` (deprecated)
在某些舊專案中, 你可能會看到 `LINK_INTERFACE_LIBRARIES` (CMake 2.8.12), 這在較新版本已經被 `INTERFACE_LINK_LIBRARIES` 取代
:::
另外, 將 properties 傳給 linker 通常會遇到一個問題: 不同 compiler 要用不同的參數表達傳給 linker 的 flags, 比如
- `gcc`: `-Wl,...,`
- `clang`: `-XLinker ...`
從 CMake 3.13 開始, 我們可以用 `LINKER:` 前綴將 linker flags 傳給 linker, 不用擔心 build machine 背後是用哪個 compiler
```
set_target_properties(Foo PROPERTIES
LINK_OPTIONS LINKER:-stats
)
```
Section 16.5, "De-duplicating Options" 詳細介紹 `LINKER:` 的用法~
#### 16.1.3. Sources
`SOURCES` property 的主要用途是列出 **non-compiled files**, 比如 soruces (.cpp, .c), headers (.h, .hpp), resources (dynamic generated files) 或其他檔案
用途
1. Generated headers
- Section 20.3, "Commands That Generate Files" 會介紹
- [How can I generate a source file during the build?](https://gitlab.kitware.com/cmake/community/-/wikis/FAQ#how-can-i-generate-a-source-file-during-the-build)
2. 和 IDE 整合
- 讓 IDE 能夠列出 target source files
3. 列出 interface libraries 的 headers
- 用 `INTERFACE_SOURCES`
`SOURCES`, `INTERFACE_SOURCES` 都支援 generator expressions
:::info
Link object library
CMake 3.14 前, target 無法直接 link object library, 需要用 `$<TARGET_OBJECTS:objectLib>` 加到 target `SOURCES` property
CMake 3.14 開始, target 直接 link object library 就好👍
:::
### 16.2 Target Properties Commands (kjchen)
如前面所述,通常我們不會去直接改動本章目前有提及的 `target properties`內容,CMake 有提供一系列方便快速的 command 協助改動,這些 command 也鼓勵明確指定 target 之間的依賴關係和傳遞 (transitive) 行為。
#### 16.2.1. Linking Libraries
回憶 [Section 4.3, Linking Target](https://hackmd.io/@ZGt0WcJQQ_enG8iTXTGNWw/SJQwchoO3/https%3A%2F%2Fhackmd.io%2F%40ZGt0WcJQQ_enG8iTXTGNWw%2FS1hkR2ou3#43-Function-composition),我們介紹了 `target_link_libraries()` 也同時解釋了如何用 `PRIVATE`, `PUBLIC`, `INTERFACE`表示 iner-target 的 dependency。在之前的解釋當中,主要專注在不同 target 之間的 dependency,但是現在有 target properties 的認知後,我們可以進一步的了解裡面更深層的不同。
```cpp!
target_link_libraries(targetName
<PRIVATE|PUBLIC|INTERFACE> item1 [item2 ...]
[<PRIVATE|PUBLIC|INTERFACE> item3 [item4 ...]]
...
)
```
##### PRIVATE
列在 `PRIVATE` 之後的東西(items)只會影響到 `targetName` 本身,並且(items)會被加入`LINK_LIBRARIES` target property
##### INTERFACE
這是 `PRIVATE` 補充(complement),列在 `INTERFACE` 之後的東西(items)會被加入 target's `INTERFACE_LINK_LIBRARIES` property。只要有連結到 `targetName` 的target,都會有這些列出的東西(items) 如同這些東西(items)列在自己的 `LINK_LIBRARIES` property 一樣
##### PUBLIC
相當於 `PRIVATE` 與 `INTERFACE` 的聯合效果
---
大多數的時候,開發者會覺得[Section 4.3, Linking Target](https://hackmd.io/@ZGt0WcJQQ_enG8iTXTGNWw/SJQwchoO3/https%3A%2F%2Fhackmd.io%2F%40ZGt0WcJQQ_enG8iTXTGNWw%2FS1hkR2ou3#43-Function-composition)介紹的方式比較直覺但是上述更精確的介紹可以幫助我們理解有不尋常 properties 的專案。上述的敘述剛好很類似於其他用來操弄 compiler 跟 linker flag 的`target_...()` command。事實上,他們也都有上述三種 keyword ,且涵義也可以類比。
在 CMake 3.12 以前,`target_link_libraries` 還不能對不同 directory 的 target 進行操作,`target_link_libraries` 需要在與 `add_executable()` 或是 `add_library()` 所在的同一個 directory 裡面被使用,有關該限制可以看 Section 43.5.1,CMake 3.13 後就移除了該限制
#### 16.2.2. Linker Options
CMake 3.13 新增了一個專用指令,用於指定 linker options。不同於在 `target_link_libraries()` 中指定 linker options,它允許專案更清晰、更準確地表達正在新增的linker options。這也避免了將 `LINK_LIBRARIES` 屬性填充為 linker flag,而是設定成相關 target properties。
```cpp!
target_link_options(targetName [BEFORE]
<PRIVATE|PUBLIC|INTERFACE> item1 [item2 ...]
[<PRIVATE|PUBLIC|INTERFACE> item3 [item4 ...]]
...
)
```
`target_link_options()` 會讓 `LINK_OPTIONS` target property 設定為`PRIVATE` 之後的 items,並將 `INTERFACE_LINK_OPTIONS` target property 設定為 `INTERFACE` 之後的items。正如所望,`PUBLIC` 之後的 items 將被添加到兩個 target property 中。由於這些屬性支持 generator expression,`target_link_options()` 也支持這種表達式。
通常情況下,每次 call `target_link_options()` 都會將指定的項目附加到相應的 target property 中。這使得以自然、漸進的方式添加多個選項變得容易。如果需要,可以使用 `BEFORE` 關鍵字,將列出的選項插入到目標屬性的現有內容之前。
即使 `targetName` 是 static library,也可以使用此命令,但請注意,`PRIVATE` 和 `PUBLIC` 後的 items 將填充 `LINK_OPTIONS`,而不是 `STATIC_LIBRARY_OPTIONS`。要填充 `STATIC_LIBRARY_OPTIONS`,唯一的選擇是直接使用 `set_property()` 或 `set_target_properties()` 修改目標屬性。對於 static library 目標,使用 `target_link_options()` 添加 `INTERFACE` 項目仍然很有用,因為 `INTERFACE_LINK_OPTIONS` 的內容將應用於 *consuming* target。
由於 `target_link_options()` 將項目添加到 `LINK_OPTIONS` 和 `INTERFACE_LINK_OPTIONS` 屬性中,該命令還支持將項目以 `LINKER:` 為前綴,以處理編譯器前端的差異。因此,[Section 16.1.2, “Linker Flags”](https://hackmd.io/rlqJTgzRTmC4_YLHvVXM6Q?both#1612-Linker-Flags)的範例可以更好地實作為:
```cpp!
target_link_options(Foo PRIVATE LINKER:-stats)
```
#### 16.2.3. Header Search Paths
有許多命令可用於管理 target’s compiler-related properties。將 directory 添加到編譯器的標頭搜索路徑是最常見的需求之一。
```cpp!
target_include_directories(targetName [AFTER|BEFORE] [SYSTEM]
<PRIVATE|PUBLIC|INTERFACE> dir1 [dir2 ...]
[<PRIVATE|PUBLIC|INTERFACE> dir3 [dir4 ...]]
...
)
```
`target_include_directories()` 命令將 header search path 添加到 `INCLUDE_DIRECTORIES` 和 `INTERFACE_INCLUDE_DIRECTORIES` target property 中。在 `PRIVATE` 關鍵字後面的 directory 將添加到 `INCLUDE_DIRECTORIES` target property中。在 `INTERFACE` 關鍵字後面的目錄將添加到 `INTERFACE_INCLUDE_DIRECTORIES` target property 中。在 `PUBLIC` 關鍵字後面的目錄將添加到 `INTERFACE` 屬性和非 `INTERFACE` 屬性中。見[Section 16.8.2, “System Header Search Paths”](https://hackmd.io/@ZGt0WcJQQ_enG8iTXTGNWw/HyBjDKrAT#1682-System-Header-Search-Paths)了解`SYSTEM` 關鍵字影響這些搜索路徑的使用方式。
`BEFORE` 關鍵字的作用與 `target_link_options()` 中的相同。它導致指定的目錄被插入到相應的屬性中而不是附加到末尾。CMake 3.20 添加了對 `AFTER` 關鍵字的支持以保持對稱性,但由於附加是默認行為,因此無需使用。
`target_include_directories()` 命令相比直接操作 target property 還提供了另一個優勢。專案不僅可以指定絕對路徑,還可以指定相對路徑。在下面討論的一個例外情況下,相對路徑將自動轉換為絕對路徑,將它們視為相對於當前源目錄。
由於 `target_include_directories()` 命令基本上只是填充相應的 target property,因此這些屬性的所有常用功能都適用。這意味著可以使用 generator expression,這是在安裝目標和創建 packages 變得更加重要的功能。見[Section 16.3.1, “Building, Installing And Exporting”](https://hackmd.io/@ZGt0WcJQQ_enG8iTXTGNWw/HyBjDKrAT#1631-Building-Installing-and-Exporting)以了解與該命令及其底層屬性經常一起使用的特定 generator expression 的討論。
#### 16.2.4. Compiler Defines
Compiler defines for a target 也有專用的指令,其格式如下:
```cpp!
target_compile_definitions(targetName
<PRIVATE|PUBLIC|INTERFACE> item1 [item2 ...]
[<PRIVATE|PUBLIC|INTERFACE> item3 [item4 ...]]
...
)
```
`target_compile_definitions()` 命令很直觀。每個項目的格式為 `VAR` 或 `VAR=VALUE`。`PRIVATE` 項目填充 `COMPILE_DEFINITIONS` target property,`INTERFACE` 項目填充 `INTERFACE_COMPILE_DEFINITIONS` target property,而 PUBLIC 項目則填充兩個target property。可以使用 generator expression。
#### 16.2.5. Compiler Options
如需增加 defines 以外的 compiler options,應該使用下述方法:
```cpp!
target_compile_options(targetName [BEFORE]
<PRIVATE|PUBLIC|INTERFACE> item1 [item2 ...]
[<PRIVATE|PUBLIC|INTERFACE> item3 [item4 ...]]
...
)
```
遵照前面的規律,`PRIVATE` 項目填充 `COMPILE_OPTIONS` target property,`INTERFACE` 項目填充 `INTERFACE_COMPILE_OPTIONS` target property ,而 `PUBLIC` 項目則填充前面所述的兩個 target property 。對於所有情況,每個項目都附加到現有的 target property 值,但可以使用 BEFORE 關鍵字來進行預置。在所有情況下都支持 generator expression。
#### 16.2.6. Source Files
將 sources files 直接添加到目標的最直接方法是在 `add_executable()` 或 `add_library()` 中列出它們。這將這些文件添加到目標的 `SOURCES` 屬性中。使用 CMake 3.1 或更高版本,可以使用 `target_sources()` 命令,在定義目標後向目標添加 sources files。此命令的工作方式與其他 `target_...()` 命令相同,支持相同的常見形式,見 [Section 35.5.1, “File Sets”](https://hackmd.io/@ZGt0WcJQQ_enG8iTXTGNWw/SJhhddjO6/https%3A%2F%2Fhackmd.io%2F%40ZGt0WcJQQ_enG8iTXTGNWw%2FBJOHTdidT%23Chapter-1-Introduction) ,裡面也討論了不同的形式
```cpp!
target_sources(targetName
<PRIVATE|PUBLIC|INTERFACE> file1 [file2 ...]
[<PRIVATE|PUBLIC|INTERFACE> file3 [file4 ...]]
...
)
```
`PRIVATE` sources 被添加到 `SOURCES` 屬性,`INTERFACE` sources 被添加到 `INTERFACE_SOURCES` 屬性,`PUBLIC` sources 被添加到這兩個屬性中。更實用的思考方式是,`PRIVATE` sources 被編譯到 `targetName` 中,`INTERFACE` sources 被添加到鏈接到 `targetName` 的任何內容中,`PUBLIC` sources 被添加到兩者中。在實踐中,除了`PRIVATE`以外的情況都很少見。[Section 19.2.4, “Interface Libraries”](https://hackmd.io/@ZGt0WcJQQ_enG8iTXTGNWw/SJhhddjO6/https%3A%2F%2Fhackmd.io%2F%40ZGt0WcJQQ_enG8iTXTGNWw%2FBJOHTdidT%23Chapter-1-Introduction) 討論了在支持 CMake 3.18 或更早版本時可能合理地使用 `INTERFACE` 列出頭文件的情況。
```cpp!
add_executable(MyApp main.cpp)
if(WIN32)
target_sources(MyApp PRIVATE eventloop_win.cpp)
else()
target_sources(MyApp PRIVATE eventloop_generic.cpp)
endif()
```
在 CMake 3.13 之前的 `target_sources()` 命令的一個特殊之處在於,如果使用相對路徑,則假定該路徑是相對於**要添加到的目標的 source directory**。這造成了一些問題。
第一個問題是,如果`INTERFACE` 下有相對路徑,那麼該路徑將被視為,**相對於使用該目標的目標**,而不是調用 `target_sources()` 的目標。顯然,這可能會建立錯誤的路徑,因此任何`non-PRIVATE` source 都需要使用絕對路徑來指定。
第二個問題是,當在定義目標的目錄之外的目錄中調用 `target_sources()` 時,相對路徑的行為變得不直觀。以先前一個需要在不同 platform 下編譯不同的 target 的例子來說:
```cpp!
// CMakeLists.txt
add_executable(MyApp main.cpp)
if(WIN32)
add_subdirectory(windows)
else()
add_subdirectory(generic)
endif()
```
```cpp!
// windows/CMakeLists.txt
// WARNING: Wrong file paths with CMake 3.12 or earlier
target_sources(MyApp PRIVATE eventloop_win.cpp)
```
```cpp!
// generic/CMakeLists.txt
// WARNING: Wrong file paths with CMake 3.12 or earlier
target_sources(MyApp PRIVATE eventloop_generic.cpp)
```
在上面的例子中,對 `target_sources()` 的調用旨在添加來自 `windows` 或 `generic` 子目錄的 sources。但是,在 CMake 3.12 或更早版本中,它們將被解釋為相對於定義 MyApp 目標的頂層目錄。
最簡單快速修正上述問題的方式就是加上 prefix files `${CMAKE_CURRENT_SOURCE_DIR}` or `${CMAKE_CURRENT_LIST_DIR}` 確保它們永遠正確的
```cpp!
// windows/CMakeLists.txt
target_sources(MyApp PRIVATE ${CMAKE_CURRENT_LIST_DIR}/eventloop_win.cpp)
```
必須為每個來源檔案添加 `${CMAKE_CURRENT_SOURCE_DIR}` 或 `${CMAKE_CURRENT_LIST_DIR} 前綴不方便,也不是特別直觀`。 為此,從 CMake 3.13 開始,行為已變更為將相對路徑視為相對於呼叫 `target_sources()` 時的 `CMAKE_CURRENT_SOURCE_DIR`,而不是目標 target 的來源目錄。 Policy CMP0076 提供了對舊行為的向後相容性。 如果可能的話,專案應將其最低 CMake 版本設定為 3.13 或更高版本,並改用新的 CMP0076 行為。
CMake 3.20 新增了使用 `target_sources()` 來對 custom target 加入 sources 的能力(見[Chapter 20, Custom Tasks](https://hackmd.io/@ZGt0WcJQQ_enG8iTXTGNWw/SJhhddjO6/https%3A%2F%2Fhackmd.io%2F%40ZGt0WcJQQ_enG8iTXTGNWw%2FBJOHTdidT%23Chapter-1-Introduction) 了解更多),在先前的 CMake 版本中,sources 只能透過 `add_custom_target()` 加入 custom target。
### 16.3 Target Property Contexts (amychen)
有些 expression 會根據不同 context 而被展開成不同內容, 例如在 building 是有內容的但 installing 是空內容, 或是 compiling 時是非空白的但 linking 是空內容.
#### 16.3.1 Building, Installing and Exporting
`COMPILE_OPTIONS`, `COMPILE_DEFINITIONS`, `INCLIDE_DIRECTORIES` 會影響編譯 target sources 時使用的 cmd line params. `INTERFACE_...` 版本則是在同樣的 build system 透過傳遞 usage requirement 給 target 使用者 來影響 cmd line. 類似情況也存在於 cmd lines with `LINK_OPTIONS` 和 `LINK_LIBRARIES`.
Target 還可以通過 `install()` cmd 安裝,以供單獨的構建使用 (Section 35.2).安裝一個 project 會提供 built binaries 和任何支持文件,可以在沒有接觸任何 project sources 或 build directory 下使用。project 可以直接安裝,也可以作為使用 cpack 生成包的一部分來執行安裝。
安裝時, target details 可以被 export 到獨立的檔案, 檔案定義了 imported targets, 表示創建已安裝內容的 original targets(Section 35.3). 這些 imported targets 定義 `INTERFACE...` properties, 傳遞訊息給安裝的使用者. 安裝時這些 exported targets 的產生被稱為 `install context`.
`$<BUILD_INTERFACE:…>` 和 `$<INSTALL_INTERFACE:…>` 可以被用在 target properties 來只給 build 或只給 install context 的 value:
```cpp=
add_library(SomeFuncs STATIC ...)
target_include_directories(SomeFuncs
PUBLIC
${CMAKE_CURRENT_BINARY_DIR}/somewhere
${MyProject_BINARY_DIR}/anotherDir
)
// `PUBLIC` 表示編譯 SomeFuncs 以及其他使用 SomeFuncs 的
// 都會使用這些 included directories
install(TARGETS SomeFuncs EXPORT Funcs DESTINATION ...)
// 安裝 SomeFuncs, 將 target details 導出到 Funcs
install(EXPORT Funcs DESTINATION ...)
// 將 Funcs 安裝到指定目錄, 會生成一個 Funcs.cmake,
// 包含 SomeFuncs target 的導入規則, 讓其他可以使用已安裝的 SomeFuncs
```
在 build 時,任何 linking 到 SomeFuncs 的東西都會將 somewhere 和 anotherDir 的絕對路徑添加到其 header search path. 當 SomeFuncs 被安裝時,它可能會被打包並部署到完全不同的機器上, somewhere 和 anotherDir 的路徑將不再有意義, 但上述例子仍然會將它們添加到使用者 target 的 header search path. 於是需要一個方法告訴他, build 時要用 xxx path, 而 install 時要用 yyy path, 也就是 `$<BUILD_INTERFACE:…>` 和 `$<INSTALL_INTERFACE:…>` generator
expressions:
```cpp=
target_include_directories(SomeFuncs
PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_BINARY_DIR}/somewhere>
$<BUILD_INTERFACE:${MyProject_BINARY_DIR}/anotherDir>
$<INSTALL_INTERFACE:include> # see below
)
```
`$<BUILD_INTERFACE:xxx` 會將 xxx 展開到 build context. 當被用在呼叫 `target_include_directories`, 展開會發生在檢查相對路徑之後, 因此 value 會被 evaluated 為絕對路徑, 或者 CMake 會有 error.
`$<INSTALL_INTERFACE:yyy>`: 確定 yyy 被加到 installed target. 在 install context 中, include directories 不一定要是絕對路徑, 通常會是相對路徑 (相對於 base install location). 一般來說會是 `include` directory 但不建議向上面例子一樣直接 hard-code 這個 directory. Section 35.1.1 會說明怎樣使用能提供更大的彈性.
還有別的狀況, project 可以在沒有任何安裝的情況下導出 target details for current build directory, 可以使用 `export()` command (Section 35.3). 獨立的 project 可以使用 exported targets 並直接在原 build directory 使用他們. 所有被包在 exported information 的路徑都指向原本的 build directory, 但導出的細節在其他方面與安裝 project 時提供的細節非常相似.
Exporting a target directly from the build directory 被認為是 build 和 install context 的一部份, 因此在這個狀況下展生 exported file 時, `$<BUILD_INTERFACE:zzz>` 和 `$<INSTALL_INTERFACE:zzz>` 會展開到 zzz. 這並不總是理想的情況。project 可能希望添加只應在 local build 時使用的 flag,但在target 被導出並被外部構建使用時不應該使用這些 flag(e.g. 一些特殊 warning flags 不應該出現在 project 外部使用者). CMake 3.26 新增對 `$<BUILD_LOCAL_INTERFACE:zzz>` generator expression 的支援. 產生 exported details時,它展開為空,而在與 target 相同的 build 直接使用時,它展開為 zzz。下面範例為常見的模式,flags 被收集到一個 interface library 且此 library 不應該被暴露給 project 外的任何東西:
```cpp=
add_library(SpecialFlags INTERFACE)
target_compile_options(SpecialFlags INTERFACE ...)
add_library(SomeFuncs STATIC ...)
target_link_libraries(SomeFuncs
PRIVATE
$<BUILD_LOCAL_INTERFACE:SpecialFlags>
)
```
在 SomeFuncs 的 local build, target links to SpecialFlags 以及特殊 flags 會被 applied. 但當 SomeFuncs 被 exported/installed, `$<BUILD_LOCAL_INTERFACE:SpecialFlags>` 展開則為空, 在 exported details 中不會提到 SpecialFlags. 如果沒有使用 `$<BUILD_LOCAL_INTERFACE:SpecialFlags>`, 因為 SomeFuncs 是 static, 在 linking 他的 private dependency 會被當作 public, 因為在 SomeFuncs 的 exported details 有 SpecialFlags, 則使用 SomeFuncs 的 project 也需要 export SpecialFlags.
#### 16.3.2 Compiling And Linking
CMake 3.27 之後支援額外的 generator expression `$>COMPILE_ONLY:...`. 可以用來限制 propagation of linking dependencies to comsumers. 通常使被使用在 `LINK_LIBRARIES` 或 `INTERFACE_LINK_LIBRARIES` target properties, 但實際上很少用. 他允許無視 inking requirements(linker options) 但可以傳遞 compilation requirements(compiler definitions, options, header search paths).
```cpp=
add_library(ExpensiveThing ...)
add_library(Algo ...)
target_link_libraries(Algo PUBLIC ExpensiveThing)
// It is known that SomeFuncs only uses parts of Algo that
// do not use ExpensiveThing in any way
add_library(SomeFuncs ...)
target_link_libraries(SomeFuncs
PRIVATE $<COMPILE_ONLY:Algo>
)
```
當你知道 `SomeFuncs` 只使用從 `Algo` headers 來的東西, 具體來說, 使用到的 `Algo` headers 是 fully inlined implementations, 且完全不需要使用到 `ExpensiveThing`, 因此 `SomeFuncs` 不需要 link to `Algo` 或 `ExpensiveThing`. 使用 `$<COMPILE_ONLY:...` 表示 `SomeFuncs` 可以在不 build `Algo`/`ExtensiveThing` 的情況下建置, 甚至 the inlined implementation in the header from `Algo` 有被更改過. 這可以大幅減少 iteration time 如果 `Algo`/`ExtensiveThing` 要花很久的時間來 build.
CMake 3.1 提供 `$<LINK_ONLY:...>`, 這是在target link requirements 應用於使用他的人,但應忽略 compilation requirements 的情況下使用的. projects 一般不會直接使用 `$<LINK_ONLY:...>`, 但他很常被 Cmake 用作 export static library targets 的一部分 (Section 23.2).
### 16.4 Directory Properties And Commands (michaelwang)
在 CMake 3.0 及之後的版本中,強烈建議使用 target properties 來指定 compiler 和 linker flags。
這是因為 target properties 才有能力可以定義當前 target 如何與「其他被 link 的target」互動。
在較早版本的 CMake 中,target properties 並不常見,通常 properties 是在 directory 層級指定的。這些 directory properties 和用於操作它們的 commands ,都**缺乏與它們基於目標的等效物相比的一致性 (?)**,這也是這些 properties/command 應該避免被使用的另一個原因。
> lack the consistency shown by their target-based equivalents
>> 應該是由於 target 與 directory 不永遠是一對一的關係,所以在現今主流的 target-based build 之下,更動 directory properties 無法保證確實改動到「想要改動的 target」,或是可能一不小心改動到多個 target。
儘管如此,由於許多線上教學和範例仍在使用這些 commands,開發人員至少應該熟悉 directory level 的 properties 和 commands。
#### `include_directories`
```cpp
include_directories([AFTER | BEFORE] [SYSTEM] dir1 [dir2...])
```
簡單來說,`include_directories()` 命令會將 `dir [dir2...]` 添加到當前 directory scope (和 child scope) 的 **"header file search path"**。
一般情況下,這些路徑會被 append 到現有的 directory list 中,但可以通過將 `CMAKE_INCLUDE_DIRECTORIES_BEFORE` 設置為 `true` 來更改此預設值。
我們也可以通過 `BEFORE` 和 `AFTER` 選項來控制每次呼叫的行為,明確指定該路徑應該如何處理。
設置 `CMAKE_INCLUDE_DIRECTORIES_BEFORE` 時應該保持謹慎,因為大多數開發人員可能會自然預設是 `AFTER` 的行為。
`SYSTEM` 關鍵字的效果在第 16.8.2 “系統 header file 搜索路徑”中討論。
`dir [dir2...]` 可以是相對/絕對路徑。
相對路徑會以相對於 current source directory 的方式進行轉換,變成絕對路徑。
`dir [dir2...]` 中也可以包含 generator expressions。
`include_directories()` 真正的實做細節其實比上方所述還複雜。
當呼叫他時,主要會有兩個效果:
1. `dir1 [dir2...]` 會被加入當前 CMakeLists.txt 檔案的 **directory property** ----`INCLUDE_DIRECTORIES`
這代表在這個資料夾或子資料夾底下被創建的 **target** 都會擁有這些 directory 作為他們的 `INCLUDE_DIRECTORIES` **target property**。
2. 即使是在呼叫 `include_directories()` 前,在當前的 CMakeLists.txt 檔案(或更準確地說,current directory scope)中創建的 target,也**會**將這些路徑添加到它們的 `INCLUDE_DIRECTORIES` target property 中。
這適用於當前的 CMakeLists.txt 檔案或通過 `include()` 引入的其他檔案中創建的 target,但不適用於在 parent directory 或 child directory scope 中創建的任何 target。
第二點的行為非常容易使開發人員傻眼。
所以為了避免混淆的狀況發生,當我們逼不得已需要呼叫`include_directories()` 時,我們傾向於在 `CMakeLists.txt` 中,任何 target 被建立前、或是任何子資料夾透過 `include()` 或 `add_subdirectory()` 加入之前,就先呼叫 `include_directories()`
#### `add_definitions()/remove_definitions()`
```cpp
add_definitions(-DSomeSymbol /DFoo=Value ...)
remove_definitions(-DSomeSymbol /DFoo=Value ...)
```
`add_definitions()` 和 `remove_definitions()` 會在 `COMPILE_DEFINITIONS` 這個 directory properties 中添加和移除 entries。
每個 entry 應以 -D 或 /D 開頭,這兩種是絕大多數 compiler 使用的前綴。
這個前綴先被 CMake 移除,之後才會儲存在 `COMPILE_DEFINITIONS` 這個 directory propreties 。
因此無論在哪個 compiler 或平台上 build project,使用哪個前綴其實都無所謂。
就如同 `include_directories()` 的特性一樣,這兩個 command 影響了所有在當下 `CMakeLists.txt` 創建的 target,包含那些在**呼叫前創造的 target**。
在 child directory scope 中創建的 target 只會在**呼叫後**被影響。
這是 CMake 使用 `COMPILE_DEFINITIONS` 這個 directory property 方式最直接造成的結果。
雖然並不推薦,但這些 commands 也可以拿來指定除了 definitions 以外其他的 compiler flags。
當 CMake 無法辨別一個看似是 compiler define 的東西時,他會將其放入 `COMPILE_OPTIONS` 這個 directory property。
這個行為是由於歷史因素而存在,但我們應該避免這種使用方式的出現。
(可以使用下方介紹的 `add_compile_options()`來替代他)
由於底層的 directory properties 支持 generator expressions,這兩個 command 也支持。
- 注意事項
- generator expressions 只能用於某個 entry 的 value,而不能用於名稱(即僅在 `-DVAR=VALUE` 中的 “=” 後面使用,或是完全不使用)。
這與 CMake 如何 parse 每個 item 是否是 compiler definition 有關。
- 還要注意,這些命令僅修改 **directory** properties,不影響`COMPILE_DEFINITIONS` **target** properties。
`add_definitions()` 存在一些缺點。
他要求每個項目前都加上 -D 或 /D 才能將其視為定義,這與其他 CMake 行為不一致。
事實上,如果省略前綴,此 command 會將該 item 視為一個 generic option,這與命令的名稱相悖 (add "definitions")。
此外,對於 generator expression 僅支持 VALUE 的限制也是要求使用前綴的結果。
而因此,CMake 3.12 推出了用以取代 `add_definitions()` 的新指令 ---- `add_compile_definitions()`
#### `add_compile_definitions()`
```cpp
add_compile_definitions(SomeSymbol Foo=Value ...)
```
新的 command 僅處理 compile definitions,不需要在每個項目前加上任何前綴,並且可以使用 generator expressions 而不僅限於 VALUE。
與 `target_compile_definitions()` 相同,該 command 的名稱與其對 definitions 的處理方式一致。
`add_compile_definitions()` 仍然會影響在同個 directory scope 內創建的所有 target,無論這些 target 是在呼叫 `add_compile_definitions()` 之前還是之後創建的,因為這是該 command 操作的底層 ---- `COMPILE_DEFINITIONS` directory properties 的特性,而不是 command 本身的特性。
#### add_compile_options()
```cpp
add_compile_options(opt1 [opt2 ...])
```
`add_compile_options()` 用於提供任意的 compiler 選項。
與 `include_directories()`、`add_definitions()`、`remove_definitions()` 和 `add_compile_definitions()` 不同,它的行為非常直觀和可預測。
每個給予 `add_compile_options()` 的選項都會被添加到 `COMPILE_OPTIONS` directory property 中。
隨後在當前 directory scope 及其下創建的每個 target 將在其自己的 `COMPILE_OPTIONS` target properties 中繼承這些選項。
**任何在呼叫之前創建的目標不受影響**。
這種行為與其他 directory properties commands 相比,更接近開發人員直觀期望的行為。
此外,因為 generator expression 可以用於 directory 和 target properties,因此也可以用於 `add_compile_options()`。
#### `link_libraries()/link_directories()`
```cpp
link_libraries(item1 [item2 ...]
[ [debug | optimized | general] item] ...
)
link_directories( [ BEFORE | AFTER ] dir1 [dir2 ...])
```
在早期的 CMake 版本中,這兩個 command 是告訴 CMake 將 library link 到其他 target 的主要方法。
在呼叫這些 command 後,它們會影響在當前 directory scope 及其下創建的所有target,但任何現有 target 都不受影響(即類似於 `add_compile_options()` 的行為)。
在 `link_libraries()` command 中指定的 item 可以是 CMake target、 library 名稱、 library 的完整路徑,甚至是 linker flag。
大致上
- 通過在 item 之前加上關鍵字 `debug`,可以使 item 僅適用於 Debug build。
- 通過在 item 之前加上關鍵字 `optimized`,可以使 item 適用於除 Debug 以外的所有 build types。
- 可以在 item 之前加上關鍵字 `general`,表示它適用於所有 build types。(Default)
這三個關鍵字僅影響其後的單個 item ,而不是下一個關鍵字出現前的所有 item。
**強烈不建議**使用這些關鍵字,因為 generator expressions 可以更好地控制何時添加 item 。
對於自定義的 build types,如果一個 build type 有在 global property `DEBUG_CONFIGURATIONS` 中列出,則將該 build type 視為 debug 配置。
通過 `link_directories()` 添加的 directory,只有在 CMake 的 link 目標是一個**純粹的 library name** 時,才會起到作用。
> The directories added by link_directories() only have an effect when CMake is given a **bare library name** to link to.
>> 意思應該是當 CMake 需要 link 到一個 library,但他沒有相關的路徑資訊 (只有名稱) 時,才會去 `link_directories()` 中尋找。
CMake 將提供的路徑添加到 linker command line,然後讓 linker 自行查找這些 library。
給定的 directory 需要是絕對路徑,儘管在 CMake 3.13 之前允許相對路徑(參見 CMP0081,該 policy 控制是否在遇到相對路徑時 CMake 會停止並顯示錯誤)。
`BEFORE` 和 `AFTER` 關鍵字是在 CMake 3.13 中添加的,具有與 `include_directories()` 相似的效果,如果不存在任何關鍵字,預設行為等效於 `AFTER`。
出於穩定性的考慮,當使用 `link_libraries()` 時,應提供完整的路徑或 CMake target 的名稱。
在這兩種情況下,都不需要提供 linker 額外的搜索路徑,linker 可以直接拿到確切的 library。
此外,一旦使用 `link_directories()` 添加了 linker 搜索 directory,projects 將沒有方便的方式來刪除該搜索路徑。
所以在一般情況下,應該避免使用這個 command。
CMake 3.13 還引入了 `add_link_options()` 命令。它與 `target_link_options()` 命令類似,不同之處在於它作用於 directory properties 而不是 target properties。
```cpp
add_link_options(item1 [item2...])
```
此 command 將 items 附加到 directory property ---- `LINK_OPTIONS`,該 directory property 用於初始化 current directory scope 及其下隨後創建的所有 target 的同名 target property。
與其他 directory level command一樣,通常應該避免使用 `add_link_options()`,而應該使用 target level 的 command。
### 16.5 De-duplicating Options
當 CMake 構建最終的 compiler 和 linker command line 時,它會對 flags 進行 de-duplicating。
這可以大大減少 command line 的長度,除了對實做有幫助外,也對開發人員試圖理解最終的 options 有好處。
但在某些情況下,我們可能不希望 de-duplicate。
例如,可能需要重復某個 option,並使用不同的第二個參數,比如使用 Clang 傳遞多個 linker option 的情況:
```cpp
# This won't work as expected
target_link_options(SomeTarget PRIVATE
-Xlinker -z
-Xlinker defs
)
```
經過 de-duplicate 後,第二個 `-Xlinker` 會被移除,導致不合理的 option `-Xlinker -z defs` 出現。
對 compiler 來說,這裡也有一個相似的例子:
```cpp
target_compile_options(SomeTarget PRIVATE
-Xassembler --keep
-Xassembler --no_esc
)
```
CMake 提供了一個 `SHELL:` 前綴,用以避免同組的 options 透過 de-duplicating 被拆開。
這個前綴從 CMake 3.12 支援 compiler options 並於 3.13 支援 linker options。
如果要強制兩個或多個 options 被視為同一個 group,他們必須加上`SHELL:` 前綴並作為 single quoted string 被傳入。
其中 options 也需由空白隔開。
```cpp
target_link_options(SomeTarget PRIVATE
"SHELL:-Xlinker -z"
"SHELL:-Xlinker defs"
)
target_compile_options(SomeTarget PRIVATE
"SHELL:-Xassembler --keep"
"SHELL:-Xassembler --no_esc"
)
```
對於 linker options,`LINKER:` 前綴會在 de-duplication 後展開。
他也可以與 `SHELL:` 合併使用。
下兩行指令具有相同的效果:
```cpp
target_link_options(SomeTarget PRIVATE "LINKER:-z,defs")
target_link_options(SomeTarget PRIVATE "LINKER:SHELL:-z defs")
```
當使用 Clang 時,`"LINKER:-z,defs"` 和 `"LINKER:SHELL:-z defs"` 都會被擴展成 `-Xlinker -z -Xlinker
defs`。
`-Xlinker` 並不會被 de-duplicate。
`SHELL:`、`LINKER:` 和 `LINKER:SHELL:` 前綴在 target property level 處理。
這意味著它們可以與任何操縱 target properties 的 command 一起使用。
`LINK_OPTIONS` 和 `INTERFACE_LINK_OPTIONS` 支持所有前綴。
`COMPILE_OPTIONS`、`INTERFACE_COMPILE_OPTIONS` 和 `STATIC_LIBRARY_OPTIONS` 僅支持 `SHELL:`。
由於所有這些 target properties 都是從同名的 directory properties 初始化的,因此這些 directory properties 也可以使用前綴。
### 16.6 Compiler And Linker Variables (mjtsai)
properties 是 project 內主要影響 compiler 與 linker flags 的方式,end user 無法直接去修改 properties,因此 project 的行為取決於 properties。
但在有些情況下,user 會想加上特別的 compiler 或 linker flags,像是增加 warning option,或是開啟特殊的 compiler feature,像是 sanitizer 或 debug 開關等等,為了達成目的,變數就這麼出現了。
CMake 提供了一些的變數,可以與這些由 directory、target、source file 提供的 properties 合併,這些變數通常是 cache variable,user 可以很容易去觀察修改他,但它們也能在 CMakeList.txt 被設置成一般的 variable (但一些 project 應該避免)。CMake 在第一次運行時會給予 cache variable 適當的預設值。
主要影響 compiler flags 的 property 有以下形式:
- `CMAKE_<LANG>_FLAGS`
- `CMAKE_<LANG>_FLAGS_<CONFIG>`
`<LANG>` 指的是 compile 的語言,常見的有 C, CXX, Fortran, Swift ... 等;
`<CONFIG>` 則是大寫的字串,代表著 build type,常見的有 DEBUG, RELEASE, RELWITHDEBINFO, MINISIZEREL。
第一種形式會影響到所有的 build type,包含 single configuration generator 且 CMAKE_BUILD_TYPE 為空值的時候 。第二種形式只會影響 <CONFIG> 所設定的 build type。舉例來說, C++ 以 Debug 去 build,會帶有來自 `CMAKE_CXX_FLAGS` 與 `CMAKE_CXX_FLAGS_DEBUG` 的 flag。
當遭遇第一個 `project()` 指令時,若它們的 cache variable 不存在,則會新增此 cache variable(這是簡單的說法,細節在第 24 章會介紹),而在 run 過後,我們就可以從 CMake GUI 中看到這些值。下面是特定 compiler 在 C++ 的預設值。
| CMAKE_CXX_FLAGS | |
| -------- | -------- |
| CMAKE_CXX_FLAGS_DEBUG | -g -O0 |
| CMAKE_CXX_FLAGS_RELEASE | -O3 -DNDEBUG |
| CMAKE_CXX_FLAGS_RELWITHDEBINFO | -O2 -g DNDEBUG |
| CMAKE_CXX_FLAGS_MINSIZEREL | -Os -DNDEBUG |
linker flags 也是一樣的邏輯,他們的形式如下:
- `CMAKE_<TARGETTYPE>_LINKER_FLAGS`
- `CMAKE_<TARGETTYPE>_LINKER_FLAGS_<CONFIG>`
`<TARGETTYPE>` 的值需為下列其中之一,其功能已在第 4 章有介紹。
- EXE
用 `add_executable()` 建立的 target
- SHARED
用 `add_library(name SHARED ...)` 所建立的 target,或是其他等同的形式,例如省略 `SHARED`,但卻設定 `BUILD_SHARED_LIBS` 變數為 true
- STATIC
用 `add_library(name STATIC ...)` 所建立的 target,或其他等同的形式,例如省略 `STATIC`,但卻設定 `BUILD_SHARED_LIBS` 變數為 false,或是未定義。
- MODULE
用 `add_library(name MODULE ...)` 所建立的 target
像 compiler flags 一樣,`CMAKE_<TARGETTYPE>_LINKER_FLAGS` 適用所有 build configuration,而 `CMAKE_<TARGETTYPE>_LINKER_FLAGS_<CONFIG>` 只適用指定的 CONFIG。對於一些平台來說,linker flags 為空字串是常見的事。
CMakue 教學與範例在 CMake 3.0 以前,常常透過頻繁的修改上述變數來改變 compiler 與 linker flags,這在 CMake 3.0 以前是常見的作法,但在 CMake 3.0,以 target 為中心的概念出現後,這種方式就變得不太好了,他們會造成一些常見的錯誤,以下列舉幾個常見的例子。
#### 16.6.1. Compiler And Linker Variables Are Single Strings, Not Lists
如果需要一次加多個 flag,則需要把他們以單個字串的形式加上,而非 list 形式。
CMake 對於帶有分號的 flag 變數(即 list),無法正確的處理。
```cpp=
# Wrong, list used instead of a string
set(CMAKE_CXX_FLAGS -Wall -Wextra)
# Correct, but see later sections for why appendin
set(CMAKE_CXX_FLAGS "-Wall -Wextra")
# Appending to existing flags the correct way (two methods)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra")
string(APPEND CMAKE_CXX_FLAGS " -Wall -Wextra")
```
#### 16.6.2. Distinguish Between Cache And Non-cache Variables
上述提到的變數都是 cache variable,而同名的 non-cache variable 可以被定義,且可以在當前 directory 或 children(add_subdirectory) scope 內,達成取代 cache variable 的效果。
若不是用上述的方法去更新 local variable,而是去強制更新 cache variable,則就可能會有問題。除了會讓 code 變得難以運作,若 user 是使用 CMAKE GUI 或類似的程式,build code 時,會讓 user 覺得自己在跟整個 project 做對抗。
```cpp=
# Case 1: Only has an effect if the variable isn't already in the cache
set(CMAKE_CXX_FLAGS "-Wall -Wextra" CACHE STRING "C++ flags")
# Case 2: Using FORCE to always update the cache variable, but this overwrites
# any changes a developer might make to the cache
set(CMAKE_CXX_FLAGS "-Wall -Wextra" CACHE STRING "C++ flags" FORCE)
# Case 3: FORCE + append = recipe for disaster (see discussion below)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra" CACHE STRING "C++ flags" FORCE)
```
- Case 1:
這是一個 CMake 新手常見的疏忽,沒有 Force 的情況下,`set` 只會在該 cache variable 未被定義的時候才會生效,因此在 CMake 第一次運行的情況下可能如預期般運作(該指令被放置在任何 `project()` 指令之前),但若該行後續有做一些其他的變動,則變動在接下來的 build 都不會有作用,因為該 cache variable 已經被定義好了。
- Case 2:
發現 cache variable 沒被更新後的常見反應就是加上 FORCE 來確保 cache variable 有被更新,但就會產生另一個問題,The cache is a
primary means for developers to change variables locally without having to edit project files.,因此當使用 FORCE 來單方面的更新 cache variable,則會導致之前 user 對 cache variable 的設定消失。
- Case 3:
這個方式是更有問題的,因為會造成每次運行 CMake 都會讓 flag 被再次 append,造成一個持續增長且重複的 flag。使用這個方法來更新 flags 不是一個好主意。
與其去拿掉 FORCE,正確的處理方式是去 `set` non-cache variable,如此一來便可安全的 append flag 到現有的 flags 上,保持 cache variable 沒有被變更。
```cpp=
# Preserves the cache variable contents, appends new flag
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra")
```
#### 16.6.3. Prefer Appending Over Replacing Flags
如上所述,developer 有時候想要在它們的 CMakeLists.txt 內改變 compiler flags,如下範例:
```cpp=
# Not ideal, discards any developer s
set(CMAKE_CXX_FLAGS "-Wall -Wextra")
```
這種方式覆蓋掉了 cache variable,後續 flag 就無法簡單的去調整他,而是得爬 code,找到這行並直接調整,如果是大型的 project,這件事就會很麻煩。如果可能,我們應該要用 append 的方式去增加現有的 flags。
上述方式,唯一合理的使用原因為,該 project 需要一組強制的 compiler 與 linker flags 參數,遇到這種情況,則建議盡可能把設置參數這件事移到 CmakeLists.txt 的最前面(top level),例如接在 `cmake_minimum_required` 之後,或是,更好的方式是,把參數移到 toolchain file 內 (第 24 章會介紹)。但要注意的是,當該 project 變成其他 project 的 child project 時,設置參數就不再是最 top level 的地方,因此這個方法就變得沒那麼適合。
#### 16.6.4. Understand When Variable Values Are Used
compiler 與 linker flag "採用"的時間點,是一個讓人感到疑惑的點。如以下範例。
```cpp=
# Save the original set of flags so we can restore them later
set(oldCxxFlags "${CMAKE_CXX_FLAGS}")
# This library has stringent build requirements, so
# enforce them just for it alone.
# WARNING: This doesn't do what it may appear to do!
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra")
add_library(StrictReq STATIC ...)
# Less strict requirements from here, so restore the
# original set of compiler flags
set(CMAKE_CXX_FLAGS "${oldCxxFlags}")
add_library(RelaxedReq STATIC ...)
```
上述結果是,`StrictReq` 的 build 並不會帶有 `-Wall -Wextra`。
我們直覺是,當 CMmake 運行到該處時,便採用當時的變數;但實際上是,該變數是採用該 directory scope 結束時的變數,即該 directory 內 CMakeLists.txt 運行到最後的值,這種差異可能會導致一些未預期的錯誤。
developer 若以為 compiler 與 linker variable 能夠隨改隨用,便會遇到上述的錯誤。另一個常見的錯誤就是在 target 建立後去 `include`,且 `include` 的檔案內有修改 compiler 與 linker variable,這會導致在 `include` 之前的 target 也受到影響。
這種 compiler 與 linker variable 延遲的特性,導致他們處於一種脆弱的狀態。為了避免這種錯誤發生,請盡量將 compiler 與 linker variable 的修改寫在 CMakeLists.txt 的最上方,以免發生"驚喜"。
### 16.7 Language-specific Compiler Flags
我們可以透過 generator expressions 替特定 target 的特定語言加上特定 compiler flags,範例如下(假設該 compiler 支援 -fno-exceptions flag),但也有一些限制存在。
```cpp=
target_compile_options(Foo PRIVATE
$<$<COMPILE_LANGUAGE:CXX>:-fno-exceptions>
)
```
不幸的是,上述例子在 Visual Studio 或 Xcode 是行不通的,這些 generator's implementations 不支援在 target level 針對不同語言使用不同 flag,而是根據 target 的 source,若有 C++ source,則將 target language 判斷為 C++,反之則為 C。這個行為在 compile options、compile definitions 與 include directories 都是。這個限制是為了避免 build 效率大幅下降,如果能接受 build 效率降低,則可以使用 source file property 來設定 compiler flags,範例如下:
```cpp=
add_executable(Foo src1.c src2.cpp)
set_property(SOURCE src2.cpp APPEND PROPERTY
COMPILE_OPTIONS -fno-exceptions
)
```
source file properties 也有它自己的限制,如第 10.5 章討論的,Xcode generator 無法支援特定 configuration 的寫法,因此 generator expression 需要避免使用 $<CONFIG>。
針對不同語言的限制,比較好的應用解法是,將它們分成不同 target,如此一來 compiler flag 就可以套用到整個 target 上,因此避免 build 效率下降的問題。
```cpp=
add_library(Foo_c src1.c)
add_executable(Foo src2.cpp)
target_link_libraries(Foo PRIVATE Foo_c)
target_compile_options(Foo PRIVATE -fno-exceptions)
```
還有一個沒那麼好的應用解法是,使用 CMAKE_<LANG>_FLAGS,這個變數 Visual Studio 與 Xcode 都有支援,但它們會套用到 directory scope 內的所有 target,建議 developer 能將它們單獨使用。
### 16.8 Compiler Option Abstractions (henrylin)
CMake 提供不同的方式實現對各種 compiler feature 的抽象,這其中有些是單純為了使用 compiler feature,也些則是為了讓特定 toolchain 的功能更方便利用。
需要注意的是當我們使用這些功能時,不應該將它們與直接傳遞 compiler flag 的方式混合在一起使用,一個專案最好是從完全使用 compiler option abstractions 或傳遞 compiler flag 之間選擇一種模式來開發就好,混合使用可能會導致 unexpected result 甚至是破壞整個 build 的流程。
[Chapter 39, Fetch Content](#Chapter39) 會詳細探討專案的 dependencies 是否應該與專案本體採用一樣的 abstraction
#### 16.8.1 Warnings As Errors
如果一個專案希望在建置時把所有 warning 都解決掉,其中一個辦法是把所有 warning 都視為 error,這樣可以鼓勵開發者在 merge 回 main branch 前解決所有的 warning
CMake 3.24 新增了 `COMPILE_WARNING_AS_ERROR` 這個 target property,當被設置為 true 時,該 target 就會視所有 warning 為 error。不是所有 compiler 都支援這個功能,但作者說至少主流的 compiler (e.g GNU, clang, MSVC) 都有實作
`COMPILE_WARNING_AS_ERROR` 這個 property 會被 `CMAKE_COMPILE_WARNING_AS_ERROR` 初始化。理想上,專案不應該直接去更改 `CMAKE_COMPILE_WARNING_AS_ERROR`,而是應該通過 cache variable 的方式來設定
```cpp=
cmake -DCMAKE_COMPILE_WARNING_AS_ERROR=YES ...
```
[Chpater 42.](#Chpater42) 會介紹的 CMake preset 是另外一個設定 `CMAKE_COMPILE_WARNING_AS_ERROR` 的好方法
在某些情況下,專案可能為因為 policy 而手動 hard-code 設定 `CMAKE_COMPILE_WARNING_AS_ERROR`,這時開發者仍然可以通過傳遞 `--compile-no-warning-as-error` 來關閉 "warning as error" 這個功能
```cpp=
cmake --compile-no-warning-as-error ...
```
#### 16.8.2 System Header Search Paths
大多數主流 compiler 都支援指定 system header search paths。這些路徑一般由系統預設提供(e.g Unix 上的 `/usr/include`),有時候,這個稱呼也會用於專案所使用 dependency 的 header search path
關於 system header path,部分 compiler 在某些 search path 的行為上會有所差異,例如
1. compiler 可能會先搜尋所有 non-system search path 完才搜尋 system 的
2. 當一個 header 被放在 system search path 時,compiler 可能會忽略所有從該 header 發出的 warning
3. compiler 在計算 file 的 dependency 時,可能會忽略那些 system search path 的 header
為了統一 CMake generators 的行為,CMake 在幾次版本中更新了下面的功能:
1. 從 CMake 3.12 開始,system search path 都會被放在 non-system 之後
2. 從 CMake 3.22 開始,使用 Visual Studio toolchain 時,`/external:W0` 會被加入到 default compiler flag 來去關掉 system header 的 warning (gcc & clang 預設都是關掉的)
CMake 預設把 **imported target** 的 header search path 當成是 system search path。[Section 19.2.3]() 會詳細介紹 imported target 是什麼,現在我們可以先把它當成是一個來自 project 外的 library 來看就好
假設在某個情況下,consumer (link 到某個 imported target 的 target) 不想把 imported target 的 header search path 當成 system,可以通過設定 consumer 的 target property `NO_SYSTEM_FROM_IMPORTED` 為 true 來達到這個目的
要注意的是這種設定方式會影響到該 consumer 的所有 imported target,所以一般會建議使用 CMake 3.25 引入的 `SYSTEM` property 來個別對 imported target 進行設定:
- 對 imported target,`SYSTEM` 預設為 true
- 對 non-imported target,`SYSTEM` 預設使用 directory property 的 `SYSTEM` (如果沒有設定就是 false)
如果一個資料夾下需要設定 `SYSTEM` 的 target 太多,可以考慮使用以下方式設定 `SYSTEM` directory property
```cpp=
# Vendored code stored directly in the project
add_subdirectory(third_party/somedep SYSTEM)
# External dependency downloaded and added to the build
include(FetchContent)
FetchContent_Declare(anotherdep
GIT_REPOSITORY ...
SYSTEM
)
FetchContent_MakeAvailable()
```
CMake 3.25 還提供了 target 在 installed 或是 exported 時的 `SYSTEM` 控制,通過設定 `EXPORT_NO_SYSTEM` property 來達成:
```cpp=
# This is an ordinary non-imported target during the build
add_library(MyThing ...)
set_target_properties(MyThing PROPERTIES
EXPORT_NO_SYSTEM TRUE
)
# It becomes an imported target in the installed location.
# Its SYSTEM property will be false when installed.
install(TARGETS MyThing EXPORT MyProj ...)
install(EXPORT MyProj ...)
export(EXPORT MyProj ...)
```
除了上述提供的方式,`target_include_directories()` 跟 `include_directories()` 也可以接受 `SYSTEM` 當 keyword 達成相似的效果,但要注意的是用這兩個方法設定的 `SYSTEM` 無法被 `SYSTEM` 或 `EXPORT_NO_SYSTEM` 覆蓋,所以一般不建議使用
#### 16.8.3 Runtime Library Selection (aaronliu)
使用 MSVC ABI 的編譯器時,必須選擇一個 runtime library。在 CMake 3.15 或更高版本中,可以透過 target property `MSVC_RUNTIME_LIBRARY` 來設定。有效的值包括:
- MultiThreaded
- MultiThreadedDLL
- MultiThreadedDebug
- MultiThreadedDebugDLL
DLL 結尾代表用 dynamic link,而沒有 DLL 的值代表使用 static link。而 Debug 結尾代表使用 debug version。當 `MSVC_RUNTIME_LIBRARY` 被設定,CMake 會自動選擇適合所使用編譯器的 flags。這樣一來,project 不必知道針對各種不同 toolchains 所需的不同選項。如果 `MSVC_RUNTIME_LIBRARY` 保持未設置狀態,CMake 將使用 generator expression `MultiThreaded$<$<CONFIG:Debug>:Debug>DLL` 作為 default value (Debug builds will use the debug runtime, and binaries will be dynamically linked.)。
當使用 CMake 3.14 或更早版本時,某些 MSVC toolchains 使用了類似的默認設置。但是,這並不是透過 `MSVC_RUNTIME_LIBRARY`,而是通過在 [16.6 Compiler And Linker Variables](https://hackmd.io/rlqJTgzRTmC4_YLHvVXM6Q?both#166-Compiler-And-Linker-Variables-mjtsai) 中討論的新增 raw flags 來實現的。這使得 project 修改默認行為變得更加困難,因為需要知道 toolchains 使用了哪些 flags 並進行修改。
為了讓 CMake 使用 `MSVC_RUNTIME_LIBRARY`,project 必須確保在呼叫第一個 `project()` 之前將 policy `CMP0091` 設定為 `NEW`。最簡單且最典型的方式是要求至少使用 CMake 3.15 或更高的版本:
```cmake
# Need at least CMake 3.15 to use MSVC_RUNTIME_LIBRARY
cmake_minimum_required(VERSION 3.15)
```
如果使用的 CMake 版本小於 3.15,CMake 將忽略 `MSVC_RUNTIME_LIBRARY`。
#### 16.8.4. Debug Information Format Selection
CMake’s default compiler flags 會影響 debug information format。例如像 `/Z7`、`/Zi` 或 `/ZI` 這樣的 MSVC toolchain flags 會改變 debug information 存儲的位置以及在 Visual Studio 中的 debugging 的行為。在 CMake 3.24 及更早版本中,這些 flags 是通過 [Section 16.6, “Compiler And Linker Variables”](https://hackmd.io/rlqJTgzRTmC4_YLHvVXM6Q?both#166-Compiler-And-Linker-Variables-mjtsai) 介紹的 `CMAKE_<LANG>_FLAGS_<CONFIG>` 裡來指定的。當開發人員需要不同於 default 的選項時,必須對這些變量進行 string-based 的 search-and-replace。這在很多方面都很不方便:
- String-based replacements 可能是不安全的
- 必須針對每種語言和配置的組合分別進行。
- 需要了解任何版本的 toolchain 所有可能的 flags。
CMake 3.25 新增 `MSVC_DEBUG_INFORMATION_FORMAT` target property 來決定為該 target 添加哪些 debug information 的 flags,但僅在第一次調用 `project()` 時且將 policy `CMP0141` 設為 `NEW` 的情況下適用。如果該 policy 未設置或設置為 `OLD`,則 `CMAKE_<LANG>_FLAGS_<CONFIG>` 的 default values 將包含 debug information 的 flags,在 CMake 3.24 及更早版本,`MSVC_DEBUG_INFORMATION_FORMAT` 屬性將被忽略。
`MSVC_DEBUG_INFORMATION_FORMAT` 必須是以下值之一或空字串:
- Embedded: 對於 MSVC toolchain,編譯器將直接把 debug information 包在每個 object file 中。(這對應於 `/Z7` flag)
- ProgramDatabase: 編譯器將 debug information 收集到單獨的 PDB 文件中(program database)。(對於 MSVC toolchain,這對應於 `/Zi` flag)
- EditAndContinue: 與 ProgramDatabase 類似,但額外支持 Visual Studio 的 “編輯並繼續” 功能。(對於 MSVC toolchain,這對應於 `/ZI` flag)
對於 target 為 MSVC ABI 的非 MSVC toolchain,並不是所有上述情況都受支援。例如當使用目Clang toolchain 時,唯一支援的值是 `Embedded`。
```cmake
# NOTE: Setting this property directly is discouraged.
set_target_properties(SomeTarget
CMAKE_MSVC_DEBUG_INFORMATION_FORMAT
$<$<CONFIG:Debug,RelWithDebInfo>:Embedded>
)
```
當未設置 `CMAKE_MSVC_DEBUG_INFORMATION_FORMAT` 時,CMake 將使用 default value,如果 toolchain 支援 ProgramDatabase,則 ProgramDatabase 為 default value。對於不支持 ProgramDatabase 的 toolchain,default value 將改為 Embedded。當 `CMAKE_MSVC_DEBUG_INFORMATION_FORMAT` 被明確設置為空字串時,將導致不添加任何與 debug 相關的 flags。
### 16.9 Recommended Practices
Projects 應該盡量使用 `target_link_libraries()` 命令來定義所有 targets 之間的依賴關係,它清楚地表達了 targets 之間的關係性。而不是 `link_libraries()` 或直接操作 target or directory properties。同樣,其他`target_...()` 命令提供了一種更清潔、更一致和更強大的方式來操作 compiler 和 linker flags。
CMake 3.13 引入了一些與 linker 選項相關的新命令和屬性,Projects 應優先使用新的`target_link_options()`命令,避免使用 `add_link_options()` 這種 directory level command。
CMake 3.13 還引入了一個新的 target level command `target_link_directories()`,這是對現有 `link_directories()` 的補充。出於穩定性原因,這兩個 target level command 都應避免使用。建議 Projects link 到 target names 或是使用 absolute paths to libraries。
以下指南可以幫助確定哪些方法適用於特定情況:
- 優先使用 `target_...()` 命令來描述 target 之間的關係,以及修改 compiler and linker 的行為。
- 避免使用 directory property commands。使用 `target_...()` 命令能夠使 project 結構更清晰,行為更可預測。如果必須使用 directory property commands,請盡早在 CMakeLists.txt 中使用,以避免產生不直觀的行為。
- 例如 `add_link_options()` 是 directory property commands,可能會對多個 target 產生影響。使用 `target_link_options()` 能夠針對特定 target 設置 link option,更加靈活和穩定。
- 避免直接操作影響 compiler 和 linker 行為的 target and directory properties。儘可能使用更專門的 target and directory specific commands。
```cmake
cmake_minimum_required(VERSION 3.12)
project(MyProject)
add_executable(MyExecutable main.cpp)
# 直接設置 TARGET 的編譯器標誌
set_property(TARGET MyExecutable PROPERTY COMPILE_FLAGS "-Wall -Wextra")
# 直接設置 TARGET 的連結器標誌
set_property(TARGET MyExecutable PROPERTY LINK_FLAGS "-Wl,--no-undefined")
```
```cmake
cmake_minimum_required(VERSION 3.12)
project(MyProject)
add_executable(MyExecutable main.cpp)
# 為 TARGET 設置編譯選項
target_compile_options(MyExecutable PRIVATE -Wall -Wextra)
# 為 TARGET 設置連結選項
target_link_options(MyExecutable PRIVATE -Wl,--no-undefined)
```
- 當 CMake 提供了編譯器或鏈接器功能的 abstraction 時,應優先使用該 abstraction,而不是添加原始的編譯器或鏈接器 flags。
-例如 `MSVC_DEBUG_INFORMATION_FORMAT` v.s. `/Z7`、`/Zi` 或 `/ZI` 這樣的 MSVC toolchain flags
- 請盡量避免修改各種 `CMAKE_…_FLAGS` variables,將這些保留給開發人員,他們可能希望隨時在本地進行更改。如果需要在整個 project 範圍內應用更改,請考慮在 project 的頂層使用一些策略性的目錄屬性命令
```cmake
# CMakeLists.txt
# 設置全局的編譯器標誌
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -O3")
```
開發人員應該熟悉 `PRIVATE`, `PUBLIC` and `INTERFACE` 關係的概念。它們是 `target_…()` 命令集的關鍵部分,並且在 project 的安裝和打包階段變得更加重要。儘管將所有東西都標記為`PUBLIC`可能很誘人,但這可能會暴露超出所需的依賴關係,且可能會影響構建時間。請優先將依賴關係設置為`PRIVATE`,只有在需要時才將其設置為 `PUBLIC`。
避免用 hard-coding 的方式將警告轉換為錯誤。讓這個選擇由 `CMAKE_COMPILE_WARNING_AS_ERROR` 變量決定(需要 CMake 3.24 或更新版本)。project 不應該設置這個變量,它是讓開發人員可以在命令行上設置的。
開發人員有時會傾向於使用 `SYSTEM` target property, 或在 `target_include_directories()` `include_directories()` 中使用 `SYSTEM` 關鍵字來靜音來自 header 的警告,而不是直接解決這些警告。如果這些 header 是項目的一部分,通常不應使用 `SYSTEM` 功能。一般來說,`SYSTEM` 是針對項目外部的路徑而設計的。
```cmake
cmake_minimum_required(VERSION 3.10)
project(MyProject)
# 添加頭文件路徑,並使用 SYSTEM 關鍵字來靜音警告
target_include_directories(MyTarget SYSTEM PRIVATE ${CMAKE_SOURCE_DIR}/include)
add_executable(MyTarget main.cpp)
```