Modern CMake 簡介

LiuYanYGZ發表於2024-08-23

摘自:https://zhuanlan.zhihu.com/p/76975231

Modern CMake 簡介

CMake是一個構建系統生成器(build-system generator)。常見的構建系統,有Visual Studio,XCode,Make等等。CMake可以支援不同平臺下構建系統的生成。

CMake的出現已經有接近20年的歷史,它的發展過程也初步經歷了三個階段。

  • ~2000 (~v2.x) ,剛剛啟動,過程式描述為主。
  • 2000~2014 (v3.0~) ,引入Target概念。
  • 2014~now (~v3.15),有了Target和Property的定義,更現代化。

概 述

現代化的CMake是圍繞 Target Property 來定義的,並且竭力避免出現變數variable的定義。Variable橫行是典型CMake2.8時期的風格。現代版的CMake更像是在遵循OOP的規則,透過target來約束link、compile等相關屬性的作用域。如果把一個Target想象成一個物件(Object),會發現兩者的組織方式非常相似:

  • 建構函式
    • add_executable
    • add_library
  • 成員函式:
    • 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(太多)

在Target中有兩個概念非常重要:Build-Requirements 和 Usage-Requirements。這兩個概念對於理解為什麼現代CMake會如此設計提供了指導意義。

  • Build-Requirements: 包含了所有構建Target必須的材料。如原始碼,include路徑,預編譯命令,連結依賴,編譯/連結選項,編譯/連結特性等。
  • Usage-Requirements:包含了所有使用Target必須的材料。如原始碼,include路徑,預編譯命令,連結依賴,編譯/連結選項,編譯/連結特性等。這些往往是當另一個Target需要使用當前target時,必須包含的依賴。

傳統的CMake和現代化的CMake的主要區別(非語法層面)如下圖所示。Traditioncal CMake在設定build-requirements和usage-requirements上都依賴手動輸入命令,並且人工維持其作用域(變數的作用域以目錄為單位)。而Modern CMake在設定上述requirement均以target為單位,所以在傳遞target屬性到其依賴的下游鏈條中更自動也更智慧。

Modern CMake 簡介

在Moden CMake中新增了不少關鍵字,其中最常見的是PUBLIC、PRIVATE、INTERFACE。

  • PRIVATE/INTERFACE/PUBLIC:定義了Target屬性的傳遞範圍。
    • PRIVATE: 表示Target的屬性只定義在當前Target中,任何依賴當前Target的Target不共享PRIVATE關鍵字下定義的屬性。
    • INTERFACE:表示Target的屬性不適用於其自身,而只適用於依賴其的Target。
    • PUBLIC:表示Target的屬性既是build-requirements也是usage-requirements。凡是依賴。凡是依賴於當前Target的Target都會共享本屬性。

解剖麻雀

我們來嘗試寫一個例項,看看在CMake v3.13及以後版本中的寫法如何。

HelloWorld
      |___ CMakeLists.txt
      |___ hello-exe
               |______ CMakeLists.txt
               |______ main.cpp
      |___ hello-lib
               |______ CMakeLists.txt
               |______ hello.hpp
               |______ hello.cpp

以這樣一個簡單的HelloWorld開啟有助於我們快速進入主題。這個專案結構很簡單,包含兩個子資料夾,hello-exe生成executable,hello-lib生成連結庫(動態)。

  • 我們先看下頂層CMakeLists的內容:
# HelloWorld/CMakeLists.txt
cmake_minimum_required(VERSION 3.14)

project(HelloWorld VERSION 1.0.0)

add_subdirectory(hello-lib)
add_subdirectory(hello-exe)

這裡沒有什麼值得多討論的,與傳統CMake一樣的寫法,定義project名稱,版本號,新增子資料夾。

  • 我們接著看hello-lib。首先看原始碼。
Modern CMake 簡介

原始碼比較簡單,只是定義一個hello_printer類,並在其cpp中定義成員函式print。請注意標頭檔案中的預編譯命令。這在VS中是非常常用的預編譯命令,用於匯出動態庫的符號。而當該庫被其他Target呼叫時,需要使用dllimport匯入符號。注意這條預編譯命令剛好符合build-requirement和usage-requirement的定義。對於hello-lib而言,定義DLL_EXPORT從而將DLL_API定義為_declspec(dllexport)是build-requirement,而對於該Target的呼叫者,需要的是不定義DLL_EXPORT。因而需要在定義compile_definitions 時將Dll_EXPORT放在PRIVATE關鍵詞下。

當其他Target使用hello-lib的時候,還需要知道hello.hpp的路徑。傳統的CMake寫法是透過在呼叫者的CMakeLists.txt中新增includedirectory來實現。但這種寫法會依賴庫之間的相對路徑,一旦調整路徑,所有的CMakeLists都將需要更新。在Modern CMake中不必如此,你只需要透過target_include_directories指定hello.hpp的路徑,將之納入INTERFACE(當然PUBLIC)也行。則呼叫者就可以得到該include路徑。

CMakeLists.txt 全文如下:

set(target_name "hello-lib")

add_library(${target_name}  SHARED
        hello.cpp
        hello.hpp
    )

target_include_directories(${target_name} INTERFACE ${CMAKE_CURRENT_SOURCE_DIR})
target_compile_definitions(${target_name} PRIVATE DLL_EXPORT)
  • 最後看下hello-exe。hello-exe中的CMakeLists.txt就可以比較簡單了:
add_executable(hello-exe main.cpp)
target_link_libraries(hello-exe PUBLIC hello-lib)

補充

Modern CMake中還有些有意思的知識點,這裡沒法一一覆蓋,只能稍稍展開。最有意思的點是generator-expression。在現代IDE中,Build-type一般都不是在CMake config期間能確定的。如VS,XCode都支援Multi-configuration,具體使用Debug還是Release是在編譯時才確定,那如果Target的依賴路徑或者依賴庫需要區分Configuration來配置該怎麼辦呢?在傳統CMake中是比較難辦的,target_link_libraries提供了一種手段,可以用debug和optimized來區分具體的庫名,而其他的編譯或連結設定則比較困難。在Modern CMake中,我們可以透過generator-expression來實現。

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。這是非常高頻使用的一個表示式,可以透過它來區分Debug/Release等不同的config。如下例所示,透過巢狀使用上述兩個表示式,可以達到區分CONFIG來設定依賴庫路徑的目的。
target_link_directories(${PROJECT_NAME} PUBLIC                                                                                                                                                                      
  $<$<CONFIG:Debug>:${CONAN_LIB_DIRS_DEBUG}>                                                                                                                                                                        
  $<$<CONFIG:Release>:${CONAN_LIB_DIRS_RELEASE}>) 
    • ... 太多了,不一一列舉。

以上是Modern CMake中常用的內容,還有些如IMPORTED,ALIAS暫時還沒用到,等用到再更新吧。

參考

  1. onqtam/awesome-cmake