來源:公衆號【很酷的程序員/RealCoolEngineer】
當項目比較大的時候,往往需要將代碼劃分爲幾個模塊,可能還會分離出部分通用模塊,在多個項目之間同時使用;當然,也可能是依賴開源的第三方庫,在項目中包含第三方源代碼或者編譯好的庫文件。本文將會介紹CMake中如何模塊化地執行編譯,以及指定目標對相應庫文件的依賴。
在上一篇文章中,筆者介紹了一個比較完備的CMakeists.txt
該如何書寫。往期文章可以關注文集:CMake,部分文章如下:
但是上一篇文章介紹的CMakeLists.txt
一般是在項目初期的樣子,隨着項目代碼原來越多,或者功能越來越多,代碼可能會分化出不同的功能模塊,並且有一些可能是多個項目通用的模塊,這時爲了更好地管理各個模塊,可以爲每個模塊都編寫一個CMakeLists.txt
文件,然後在父級目錄中對不同編譯目標按需添加依賴。
本文着重介紹下面的內容:
- 模塊化管理構建系統(add_subdirectory)
- 導入編譯好的目標文件
- 添加庫依賴
一 模塊化構建
在前面的文章中介紹過,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
現在的編譯任務爲:
- 將math目錄視爲子模塊,爲其單獨定義構建系統
- 整個項目依賴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_dir
爲src/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
命令的幾個關鍵字:
- PRIVATE
- INTERFACE
- 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)
這裏的內容可以有個大概瞭解即可,隨着後續深入使用,自然會水到渠成。