CMake應用:模塊化及庫依賴

來源:公衆號【很酷的程序員/RealCoolEngineer】

當項目比較大的時候,往往需要將代碼劃分爲幾個模塊,可能還會分離出部分通用模塊,在多個項目之間同時使用;當然,也可能是依賴開源的第三方庫,在項目中包含第三方源代碼或者編譯好的庫文件。本文將會介紹CMake中如何模塊化地執行編譯,以及指定目標對相應庫文件的依賴。

在上一篇文章中,筆者介紹了一個比較完備的CMakeists.txt該如何書寫。往期文章可以關注文集:CMake,部分文章如下:

  1. CMake應用:基礎篇
  2. CMake應用:核心語法篇
  3. CMake應用:CMakeists.txt完全指南

但是上一篇文章介紹的CMakeLists.txt一般是在項目初期的樣子,隨着項目代碼原來越多,或者功能越來越多,代碼可能會分化出不同的功能模塊,並且有一些可能是多個項目通用的模塊,這時爲了更好地管理各個模塊,可以爲每個模塊都編寫一個CMakeLists.txt文件,然後在父級目錄中對不同編譯目標按需添加依賴。

本文着重介紹下面的內容:

  1. 模塊化管理構建系統(add_subdirectory)
  2. 導入編譯好的目標文件
  3. 添加庫依賴

一 模塊化構建

在前面的文章中介紹過,CMakeLists.txt是定義一個目錄(Source Tree)的構建系統的,所以對於模塊化構建,其實就是分別爲每一個子模塊目錄編寫一個CMakeLists.txt,在其父目錄中“導入”子目錄的構建系統生成對應的目標,以便在父目錄中使用。

下面仍以開源項目:https://gitee.com/RealCoolEngineer/cmake-template爲例,基於上一篇文章的狀態進行修改,本文對應的commit id爲:4bfb85b

假設項目目錄結構如下:

./cmake-template
├── CMakeLists.txt
├── src
│   └── c
│       ├── cmake_template_version.h
│       ├── cmake_template_version.h.in
│       ├── main.c
│       └── math
│           ├── add.c
│           ├── add.h
│           ├── minus.c
│           └── minus.h
└── test
    └── c
        ├── test_add.c
        └── test_minus.c

現在的編譯任務爲:

  1. 將math目錄視爲子模塊,爲其單獨定義構建系統
  2. 整個項目依賴math模塊的編譯結果,生成其他目標文件

1 定義子目錄的構建系統

只要是定義目錄的構建系統,都是在此目錄下創建一個CMakeLists.txt文件,其結構和語法在上一篇文章已經介紹的比較詳細。

因爲主要進行模塊的編譯工作,所以一般只需要編譯構建庫文件(靜態庫或者動態庫),以及針對該庫對外提供接口的一些單元測試即可,所以可以寫的比較簡單一些。

src/math目錄下新建CMakeLists.txt文件,內容如下:

cmake_minimum_required(VERSION 3.12)
project(CMakeTemplateMath VERSION 0.0.1 LANGUAGES C CXX)

aux_source_directory(. MATH_SRC)
message("MATH_SRC: ${MATH_SRC}")

add_library(math STATIC ${MATH_SRC})

如上代碼所示,對於子目錄(模塊),一般也有自己的project命令,同時如果有需要,也可以指定自己的版本號。

這裏使用了一個此前沒有提到的命令:aux_source_directory,該命令可以搜索指定目錄(第一個參數)下的所有源文件,將源文件的列表保存到指定的變量(第二個參數)。

2 包含子目錄

通過命令add_subdirectory包含一個子目錄的構建系統,其命令格式如下:

add_subdirectory(source_dir [binary_dir] [EXCLUDE_FROM_ALL])

其中source_dir就是要包含的目標目錄,該目錄下必須存在一個CMakeLists.txt文件,一般爲相對於當前CMakeLists.txt的目錄路徑,當然也可以是絕對路徑;

binary_dir是可選的參數,用於指定子構建系統輸出文件的路徑,相對於當前的Binary tree,同樣也可以是絕對路徑。
一般情況下,source_dir是當前目錄的子目錄,那麼binary_dir的值爲不做任何相對路徑展開的source_dir;但是如果source_dir不是當前目錄的子目錄,則必須指定binary_dir,這樣CMake才知道要將子構建系統的相關文件生成在哪個目錄下。

如果指定了EXCLUDE_FROM_ALL選項,在子路徑下的目標默認不會被包含到父路徑的ALL目標裏,並且也會被排除在IDE工程文件之外。但是,如果在父級項目顯式聲明依賴子目錄的目標文件,那麼對應的目標文件還是會被構建以滿足父級項目的依賴需求。

綜上,可以修改cmake-template項目根目錄下的CMakeLists.txt文件,將原來的如下內容:

# Build math lib
add_library(math STATIC ${MATH_LIB_SRC})

修改爲:

add_subdirectory(src/c/math)

構建的靜態庫的名字依舊是math,所以在編譯demo目標時,鏈接的庫的名字不用修改:

# Build demo executable
add_executable(demo src/c/main.c)
target_link_libraries(demo math)

此時構建和編譯的命令沒有任何改變:

➜ cmake-template # cmake -B cmake-build
➜ cmake-template # cmake --build cmake-build

上面的命令指定父項目的生成路徑(Binary tree)爲cmake-build,那麼子模塊(math)的生成路徑爲cmake-build/src/c/math,也就是說binary_dirsrc/c/math,等同於source_dir

二 導入編譯好的目標文件

在前面介紹的命令add_subdirectory其實是相當於通過源文件來構建項目所依賴的目標文件,但是CMake也可以通過命令來導入已經編譯好的目標文件。

1 導入庫文件

使用add_library命令,通過指定IMPORTED選項表明這是一個導入的庫文件,通過設置其屬性指明其路徑:

add_library(math STATIC IMPORTED)
set_property(TARGET math PROPERTY
             IMPORTED_LOCATION "./lib/libmath.a")

對於庫文件的路徑,也可以使用find_library命令來查找,比如在lib目錄下查找math的Realse和Debug版本:

find_library(LIB_MATH_DEBUG mathd HINTS "./lib")
find_library(LIB_MATH_RELEASE math HINTS "./lib")

對於不同的編譯類型,可以通過IMPORTED_LOCATION_<CONFIG>來指明不同編譯類型對應的庫文件路徑:

add_library(math STATIC IMPORTED GLOBAL)
set_target_properties(math PROPERTIES
  IMPORTED_LOCATION "${LIB_MATH_RELEASE}"
  IMPORTED_LOCATION_DEBUG "${LIB_MATH_DEBUG}"
  IMPORTED_CONFIGURATIONS "RELEASE;DEBUG"
)

導入成功以後,就可以將該庫鏈接到其他目標上,但是導入的目標不可以被install

這裏以導入靜態庫爲例,導入動態庫或其他類型也是類似的操作,只需要將文件類型STATIC修改成對應的文件類型即可。

2 導入可執行文件

這個不是那麼常用,爲了文章完整性,順便提一下。是和導入庫文件類似的:

add_executable(demo IMPORTED)
set_property(TARGET demo PROPERTY
             IMPORTED_LOCATION "./bin/demo")

三 庫依賴

這裏主要着重介紹一下target_link_libraries命令的幾個關鍵字:

  1. PRIVATE
  2. INTERFACE
  3. PUBLIC

這三個關鍵字的主要作用是指定的是目標文件依賴項的使用範圍(scope),所以可以專門瞭解一下。

假設某個項目中存在兩個動態鏈接庫:動態鏈接庫liball.so、動態鏈接庫libsub.so

對於PRIVATE關鍵字,使用的情形爲:liball.so使用libsub.so,但是liball.so不對外暴露libsub.so的接口:

target_link_libraries(all PRIVATE sub)
target_include_directories(all PRIVATE sub)

對於INTERFACE關鍵字,使用的情形爲:liball.so沒有使用libsub.so,但是liball.so對外暴露libsub.so的接口,也就是liball.so的頭文件包含了libsub.so的頭文件,在其它目標使用liball.so的功能的時候,可能必須要使用libsub.so的功能:

target_link_libraries(all INTERFACE sub)
target_include_directories(all INTERFACE sub)

對於PUBLIC關鍵字(PUBLIC=PRIVATE+INTERFACE),使用的情形爲:liball.so使用libsub.so,並且liball.so對外暴露libsub.so的接口:

target_link_libraries(all PUBLIC sub)
target_include_directories(all PUBLIC sub)

這裏的內容可以有個大概瞭解即可,隨着後續深入使用,自然會水到渠成。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章