# 5.6 探究編譯和鏈接命令
**NOTE**:*此示例代碼可以在 https://github.com/dev-cafe/cmake-cookbook/tree/v1.0/chapter-5/recipe-06 中找到,其中包含一個C++例子。該示例在CMake 3.9版(或更高版本)中是有效的,并且已經在GNU/Linux、macOS和Windows上進行過測試。代碼庫還有一個與CMake 3.5兼容的示例。*
生成構建系統期間最常見的操作,是試圖評估在哪種系統上構建項目。這意味著要找出哪些功能工作,哪些不工作,并相應地調整項目的編譯。使用的方法是查詢依賴項是否被滿足的信號,或者在代碼庫中是否啟用工作區。接下來的幾個示例,將展示如何使用CMake執行這些操作。我們將特別討論以下事宜:
1. 如何確保代碼能成功編譯為可執行文件。
2. 如何確保編譯器理解相應的標志。
3. 如何確保特定代碼能成功編譯為運行可執行程序。
## 準備工作
示例將展示如何使用來自對應的` Check<LANG>SourceCompiles.cmake`標準模塊的`check_<lang>_source_compiles`函數,以評估給定編譯器是否可以將預定義的代碼編譯成可執行文件。該命令可幫助你確定:
* 編譯器支持所需的特性。
* 鏈接器工作正常,并理解特定的標志。
* 可以使用`find_package`找到的包含目錄和庫。
本示例中,我們將展示如何檢測OpenMP 4.5標準的循環特性,以便在C++可執行文件中使用。使用一個C++源文件,來探測編譯器是否支持這樣的特性。CMake提供了一個附加命令`try_compile`來探究編譯。本示例將展示,如何使用這兩種方法。
**TIPS**:*可以使用CMake命令行界面來獲取關于特定模塊(` cmake --help-module <module-name>`)和命令(`cmake --help-command <command-name>`)的文檔。示例中,` cmake --help-module CheckCXXSourceCompiles`將把`check_cxx_source_compiles`函數的文檔輸出到屏幕上,而` cmake --help-command try_compile`將對`try_compile`命令執行相同的操作。*
## 具體實施
我們將同時使用`try_compile`和`check_cxx_source_compiles`,并比較這兩個命令的工作方式:
1. 創建一個C++11工程:
```cmake
cmake_minimum_required(VERSION 3.9 FATAL_ERROR)
project(recipe-06 LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_EXTENSIONS OFF)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
```
2. 查找編譯器支持的OpenMP:
```cmake
find_package(OpenMP)
if(OpenMP_FOUND)
# ... <- the steps below will be placed here
else()
message(STATUS "OpenMP not found: no test for taskloop is run")
endif()
```
3. 如果找到OpenMP,再檢查所需的特性是否可用。為此,設置了一個臨時目錄,`try_compile`將在這個目錄下來生成中間文件。我們把它放在前面步驟中引入的`if`語句中:
```cmake
set(_scratch_dir ${CMAKE_CURRENT_BINARY_DIR}/omp_try_compile)
```
4. 調用`try_compile`生成一個小項目,以嘗試編譯源文件`taskloop.cpp`。編譯成功或失敗的狀態,將保存到`omp_taskloop_test_1`變量中。需要為這個示例編譯設置適當的編譯器標志、包括目錄和鏈接庫。因為使用導入的目標`OpenMP::OpenMP_CXX`,所以只需將`LINK_LIBRARIES`選項設置為`try_compile`即可。如果編譯成功,則任務循環特性可用,我們為用戶打印一條消息:
```cmake
try_compile(
omp_taskloop_test_1
${_scratch_dir}
SOURCES
${CMAKE_CURRENT_SOURCE_DIR}/taskloop.cpp
LINK_LIBRARIES
OpenMP::OpenMP_CXX
)
message(STATUS "Result of try_compile: ${omp_taskloop_test_1}")
```
5. 要使用`check_cxx_source_compiles`函數,需要包含`CheckCXXSourceCompiles.cmake`模塊文件。其他語言也有類似的模塊文件,C(`CheckCSourceCompiles.cmake`)和Fortran(`CheckFortranSourceCompiles.cmake`):
```cmake
include(CheckCXXSourceCompiles)
```
6. 我們復制源文件的內容,通過` file(READ ...)`命令讀取內容到一個變量中,試圖編譯和連接這個變量:
```cmake
file(READ ${CMAKE_CURRENT_SOURCE_DIR}/taskloop.cpp _snippet)
```
7. 我們設置了`CMAKE_REQUIRED_LIBRARIES`。這對于下一步正確調用編譯器是必需的。注意使用導入的`OpenMP::OpenMP_CXX`目標,它還將設置正確的編譯器標志和包含目錄:
```cmake
set(CMAKE_REQUIRED_LIBRARIES OpenMP::OpenMP_CXX)
```
8. 使用代碼片段作為參數,調用`check_cxx_source_compiles`函數。檢查結果將保存到`omp_taskloop_test_2`變量中:
```cmake
check_cxx_source_compiles("${_snippet}" omp_taskloop_test_2)
```
9. 調用`check_cxx_source_compiles`并向用戶打印消息之前,我們取消了變量的設置:
```cmake
unset(CMAKE_REQUIRED_LIBRARIES)
message(STATUS "Result of check_cxx_source_compiles: ${omp_taskloop_test_2}"
```
10. 最后,進行測試:
```shell
$ mkdir -p build
$ cd build
$ cmake ..
-- ...
-- Found OpenMP_CXX: -fopenmp (found version "4.5")
-- Found OpenMP: TRUE (found version "4.5")
-- Result of try_compile: TRUE
-- Performing Test omp_taskloop_test_2
-- Performing Test omp_taskloop_test_2 - Success
-- Result of check_cxx_source_compiles: 1
```
## 工作原理
`try_compile`和`check_cxx_source_compiles`都將編譯源文件,并將其鏈接到可執行文件中。如果這些操作成功,那么輸出變量`omp_task_loop_test_1`(前者)和`omp_task_loop_test_2`(后者)將被設置為`TRUE`。然而,這兩個命令實現的方式略有不同。`check_<lang>_source_compiles`命令是`try_compile`命令的簡化包裝。因此,它提供了一個接口:
1. 要編譯的代碼片段必須作為CMake變量傳入。大多數情況下,這意味著必須使用`file(READ ...)`來讀取文件。然后,代碼片段被保存到構建目錄的`CMakeFiles/CMakeTmp`子目錄中。
2. 微調編譯和鏈接,必須通過設置以下CMake變量進行:
* CMAKE_REQUIRED_FLAGS:設置編譯器標志。
* CMAKE_REQUIRED_DEFINITIONS:設置預編譯宏。
* CMAKE_REQUIRED_INCLUDES:設置包含目錄列表。
* CMAKE_REQUIRED_LIBRARIES:設置可執行目標能夠連接的庫列表。
3. 調用`check_<lang>_compiles_function`之后,必須手動取消對這些變量的設置,以確保后續使用中,不會保留當前內容。
**NOTE**:*使用CMake 3.9中可以對于OpenMP目標進行導入,但是目前的配置也可以使用CMake的早期版本,通過手動為`check_cxx_source_compiles`設置所需的標志和庫:`set(CMAKE_REQUIRED_FLAGS ${OpenMP_CXX_FLAGS})`和`set(CMAKE_REQUIRED_LIBRARIES ${OpenMP_CXX_LIBRARIES})`。*
**TIPS**:*Fortran下,CMake代碼的格式通常是固定的,但也有意外情況。為了處理這些意外,需要為`check_fortran_source_compiles`設置`-ffree-form`編譯標志。可以通過`set(CMAKE_REQUIRED_FLAGS “-ffree-form")`實現。*
這個接口反映了:測試編譯是通過,在CMake調用中直接生成和執行構建和連接命令來執行的。
命令`try_compile`提供了更完整的接口和兩種不同的操作模式:
1. 以一個完整的CMake項目作為輸入,并基于它的`CMakeLists.txt`配置、構建和鏈接。這種操作模式提供了更好的靈活性,因為要編譯項目的復雜度是可以選擇的。
2. 提供了源文件,和用于包含目錄、鏈接庫和編譯器標志的配置選項。
因此,`try_compile`基于在項目上調用CMake,其中`CMakeLists.txt`已經存在(在第一種操作模式中),或者基于傳遞給`try_compile`的參數動態生成文件。
## 更多信息
本示例中概述的類型檢查并不總是萬無一失的,并且可能產生假陽性和假陰性。作為一個例子,可以嘗試注釋掉包含`CMAKE_REQUIRED_LIBRARIES`的行。運行這個例子仍然會報告“成功”,這是因為編譯器將忽略OpenMP的`pragma`字段。
當返回了錯誤的結果時,應該怎么做?構建目錄的`CMakeFiles`子目錄中的`CMakeOutput.log`和`CMakeError.log`文件會提供一些線索。它們記錄了CMake運行的操作的標準輸出和標準錯誤。如果懷疑結果有誤,應該通過搜索保存編譯檢查結果的變量集來檢查前者。如果你懷疑有誤報,你應該檢查后者。
調試`try_compile`需要一些注意事項。即使檢查不成功,CMake也會刪除由該命令生成的所有文件。幸運的是,`debug-trycompile`將阻止CMake進行刪除。如果你的代碼中有多個`try_compile`調用,一次只能調試一個:
1. 運行CMake,不使用`--debug-trycompile`,將運行所有`try_compile`命令,并清理它們的執行目錄和文件。
2. 從CMake緩存中刪除保存檢查結果的變量。緩存保存到`CMakeCache.txt`文件中。要清除變量的內容,可以使用`-U `的CLI開關,后面跟著變量的名稱,它將被解釋為一個全局表達式,因此可以使用`*`和`?`:
```shell
$ cmake -U <variable-name>
```
3. 再次運行CMake,使用`--debug-trycompile`。只有清除緩存的檢查才會重新運行。這次不會清理執行目錄和文件。
**TIPS**:*`try_compile`提供了靈活和干凈的接口,特別是當編譯的代碼不是一個簡短的代碼時。我們建議在測試編譯時,小代碼片段時使用`check_<lang>_source_compile`。其他情況下,選擇`try_compile`。*
- Introduction
- 前言
- 第0章 配置環境
- 0.1 獲取代碼
- 0.2 Docker鏡像
- 0.3 安裝必要的軟件
- 0.4 測試環境
- 0.5 上報問題并提出改進建議
- 第1章 從可執行文件到庫
- 1.1 將單個源文件編譯為可執行文件
- 1.2 切換生成器
- 1.3 構建和鏈接靜態庫和動態庫
- 1.4 用條件句控制編譯
- 1.5 向用戶顯示選項
- 1.6 指定編譯器
- 1.7 切換構建類型
- 1.8 設置編譯器選項
- 1.9 為語言設定標準
- 1.10 使用控制流
- 第2章 檢測環境
- 2.1 檢測操作系統
- 2.2 處理與平臺相關的源代碼
- 2.3 處理與編譯器相關的源代碼
- 2.4 檢測處理器體系結構
- 2.5 檢測處理器指令集
- 2.6 為Eigen庫使能向量化
- 第3章 檢測外部庫和程序
- 3.1 檢測Python解釋器
- 3.2 檢測Python庫
- 3.3 檢測Python模塊和包
- 3.4 檢測BLAS和LAPACK數學庫
- 3.5 檢測OpenMP的并行環境
- 3.6 檢測MPI的并行環境
- 3.7 檢測Eigen庫
- 3.8 檢測Boost庫
- 3.9 檢測外部庫:Ⅰ. 使用pkg-config
- 3.10 檢測外部庫:Ⅱ. 自定義find模塊
- 第4章 創建和運行測試
- 4.1 創建一個簡單的單元測試
- 4.2 使用Catch2庫進行單元測試
- 4.3 使用Google Test庫進行單元測試
- 4.4 使用Boost Test進行單元測試
- 4.5 使用動態分析來檢測內存缺陷
- 4.6 預期測試失敗
- 4.7 使用超時測試運行時間過長的測試
- 4.8 并行測試
- 4.9 運行測試子集
- 4.10 使用測試固件
- 第5章 配置時和構建時的操作
- 5.1 使用平臺無關的文件操作
- 5.2 配置時運行自定義命令
- 5.3 構建時運行自定義命令:Ⅰ. 使用add_custom_command
- 5.4 構建時運行自定義命令:Ⅱ. 使用add_custom_target
- 5.5 構建時為特定目標運行自定義命令
- 5.6 探究編譯和鏈接命令
- 5.7 探究編譯器標志命令
- 5.8 探究可執行命令
- 5.9 使用生成器表達式微調配置和編譯
- 第6章 生成源碼
- 6.1 配置時生成源碼
- 6.2 使用Python在配置時生成源碼
- 6.3 構建時使用Python生成源碼
- 6.4 記錄項目版本信息以便報告
- 6.5 從文件中記錄項目版本
- 6.6 配置時記錄Git Hash值
- 6.7 構建時記錄Git Hash值
- 第7章 構建項目
- 7.1 使用函數和宏重用代碼
- 7.2 將CMake源代碼分成模塊
- 7.3 編寫函數來測試和設置編譯器標志
- 7.4 用指定參數定義函數或宏
- 7.5 重新定義函數和宏
- 7.6 使用廢棄函數、宏和變量
- 7.7 add_subdirectory的限定范圍
- 7.8 使用target_sources避免全局變量
- 7.9 組織Fortran項目
- 第8章 超級構建模式
- 8.1 使用超級構建模式
- 8.2 使用超級構建管理依賴項:Ⅰ.Boost庫
- 8.3 使用超級構建管理依賴項:Ⅱ.FFTW庫
- 8.4 使用超級構建管理依賴項:Ⅲ.Google Test框架
- 8.5 使用超級構建支持項目
- 第9章 語言混合項目
- 9.1 使用C/C++庫構建Fortran項目
- 9.2 使用Fortran庫構建C/C++項目
- 9.3 使用Cython構建C++和Python項目
- 9.4 使用Boost.Python構建C++和Python項目
- 9.5 使用pybind11構建C++和Python項目
- 9.6 使用Python CFFI混合C,C++,Fortran和Python
- 第10章 編寫安裝程序
- 10.1 安裝項目
- 10.2 生成輸出頭文件
- 10.3 輸出目標
- 10.4 安裝超級構建
- 第11章 打包項目
- 11.1 生成源代碼和二進制包
- 11.2 通過PyPI發布使用CMake/pybind11構建的C++/Python項目
- 11.3 通過PyPI發布使用CMake/CFFI構建C/Fortran/Python項目
- 11.4 以Conda包的形式發布一個簡單的項目
- 11.5 將Conda包作為依賴項發布給項目
- 第12章 構建文檔
- 12.1 使用Doxygen構建文檔
- 12.2 使用Sphinx構建文檔
- 12.3 結合Doxygen和Sphinx
- 第13章 選擇生成器和交叉編譯
- 13.1 使用CMake構建Visual Studio 2017項目
- 13.2 交叉編譯hello world示例
- 13.3 使用OpenMP并行化交叉編譯Windows二進制文件
- 第14章 測試面板
- 14.1 將測試部署到CDash
- 14.2 CDash顯示測試覆蓋率
- 14.3 使用AddressSanifier向CDash報告內存缺陷
- 14.4 使用ThreadSaniiser向CDash報告數據爭用
- 第15章 使用CMake構建已有項目
- 15.1 如何開始遷移項目
- 15.2 生成文件并編寫平臺檢查
- 15.3 檢測所需的鏈接和依賴關系
- 15.4 復制編譯標志
- 15.5 移植測試
- 15.6 移植安裝目標
- 15.7 進一步遷移的措施
- 15.8 項目轉換為CMake的常見問題
- 第16章 可能感興趣的書
- 16.1 留下評論——讓其他讀者知道你的想法