在一些場景下我們需要編寫一些庫,并希望其他程序可以找到這些庫并引用。
CMake采用package這個概念來解決這個問題。
關于CMake的find_package文章有很多,但這些文章的內容大多不直觀講了一堆講不到點子上,讓人看了一頭霧水。因此我想通過本文從實用角度出發介紹一下CMake的package概念。
CMake關于Package的官方文檔:https://cmake.org/cmake/help/latest/manual/cmake-packages.7.html
CMake的Package概念
簡單理解Package是有Component組成的,而Component包含了庫和頭文件。如:
find_package(Qt5 5.1.0 REQUIRED Widgets Xml Sql)
上面代碼中Qt5
是package,5.1.0
是package的版本號,REQUIRED
表示必須要找到,Widgets
、Xml
、Sql
是Component名稱。因此上面代碼的意思是“必須找到版本為5.1.0的Qt5 Package的名為Widgets、Xml和Sql的Components”。
target_link_libraries(Foo PRIVATE Qt5::Widgets Qt5::Xml Qt5::Sql)
上面代碼的意思是在編譯目標Foo是鏈接Qt5
中Widgets
、Xml
、Sql
這三個Component提供的庫。其實Qt5::Widgets
、Qt5::Xml
、Qt5::Sql
就是庫的別名,因此上面代碼也等價于
target_link_libraries(Foo PRIVATE qt5wdgets qtxml qt5sql)
最終參與編譯的是libqt5wdgets.a
、libqt5xml.a
、libqt5sql.a
這三個庫。上面三個庫名是我瞎編的可能與實際不符。
關于Config模式和Module模式
很多文章都提到了find_package
有兩種查詢package的模式,一種是Config模式,另一種是Module模式。這里就不再贅述了,因為對實際使用毫無益處。只需要知道如果庫也是用CMake構建的就是Config模式;如果庫不是CMake構建的就用Module模式。又因為在團隊內部通常構建工具都是統一的,而且現在CMake非常流行,因此本文自關心Config模式。
自定義package
經過以上的介紹,應該清楚所謂的Component只不過是庫的別名而已。因此我們自定義package,實際上就是編寫一套CMake文件,這些文件里定義了庫的頭文件路徑、庫文件路徑,并為庫文件起個別名即可。
以上工作并不需要我們親自編寫,因為CMake已經給我們提供了EXPORT
工具,我們只需要使用EXPORT
便可以完成上面的工作。
下面是我自己編寫的一個Demo。foo_lib是一個庫也就是自定義的package,foo_app是一個應用通過find_package查找foo_lib并引用。install是它倆的安裝路徑。
foo_lib
foo_lib中包含四個文件"foo_lib.h"、“foo_lib.cpp”、“CMakeLists.txt”、“FooLibConfig.cmake.in”
foo_lib.h
foo_lib.h作為庫的頭文件,這只提供一個方法foo_lib_func()
聲明
#pragma
void foo_lib_func();
foo_lib.cpp
foo_lib.cpp作為庫的源代碼文件,實現foo_lib_func()
#include <stdio.h>
#include "foo_lib.h"void foo_lib_func()
{printf("My name is foo lib");
}
CMakeLists.txt
CMakeLists.txt 是CMake的構建文件,也是我們要講解的重點
cmake_minimum_required(VERSION 3.5)project(FooLib VERSION 1.0.0 LANGUAGES CXX)add_library(${PROJECT_NAME} SHARED ${CMAKE_CURRENT_SOURCE_DIR}/foo_lib.cpp)install(TARGETS ${PROJECT_NAME}EXPORT ${PROJECT_NAME}TargetsLIBRARY DESTINATION libINCLUDES DESTINATION include
)install(FILES foo_lib.h DESTINATION include
)install(EXPORT ${PROJECT_NAME}TargetsFILE ${PROJECT_NAME}Targets.cmakeDESTINATION "${CMAKE_INSTALL_PREFIX}/lib/cmake/${PROJECT_NAME}"NAMESPACE ${PROJECT_NAME}::
)include(CMakePackageConfigHelpers)
configure_package_config_file("${CMAKE_CURRENT_SOURCE_DIR}/${PROJECT_NAME}Config.cmake.in""${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}Config.cmake"INSTALL_DESTINATION "${CMAKE_INSTALL_PREFIX}/lib/cmake/${PROJECT_NAME}"
)install(FILES "${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}Config.cmake"DESTINATION "${CMAKE_INSTALL_PREFIX}/lib/cmake/${PROJECT_NAME}"
)
- cmake_minimum_required 規定了cmake的版本
- project 定義了項目屬性
- add_library 表示以項目名編譯一個動態庫
- 第一個 install
第一個install 中最關鍵的是EXPORT。意思是當前的TARGETS要向外導出。導出的名稱為${PROJECT_NAME}Targets
- 第二個 install
第二個install用來安裝頭文件。雖然在第一個install中寫了INCLUDES DESTINATION include
,但是并沒有真正安裝頭文件,原因很簡單因為CMake無法自動推斷出哪些頭文件需要安裝。 - 第三個 install
第三個 install的作用是導出Targets文件。第一個install的EXPORT只是表示要導出${PROJECT_NAME}Targets
,但是導出到哪并沒有指明。因此需要第三個install指明導出文件為${PROJECT_NAME}Targets.cmake
,導出的位置為"${CMAKE_INSTALL_PREFIX}/lib/cmake/${PROJECT_NAME}"
,導出的命名空間為${PROJECT_NAME}::
上文說的庫頭文件路徑,庫文件路徑,庫文件別名 都會自動生成并保存在
${PROJECT_NAME}Targets.cmake
中 - include(CMakePackageConfigHelpers)
為了引入configure_package_config_file
方法 - configure_package_config_file
configure_package_config_file
的所用是生成${PROJECT_NAME}Config.cmake
。這個文件名是固定的,調用find_package(xxx)
時,cmake就會找xxxConfig.cmake
文件并引入。到這就很好理解了,我們只要在${PROJECT_NAME}Config.cmake
中引用${PROJECT_NAME}Targets.cmake
就可以把庫頭文件路徑、庫文件路徑、庫的別名等信息導入了。 - 第四個install
第四個個install的作用是將${PROJECT_NAME}Config.cmake
按照到指定路徑,好讓find_package(xxx)
能找到。
FooLibConfig.cmake.in
@PACKAGE_INIT@
include(CMakeFindDependencyMacro)
# find_dependency(xxx)
include("${CMAKE_CURRENT_LIST_DIR}/@PROJECT_NAME@Targets.cmake")
- @PACKAGE_INIT@ 是固定寫法,這部分內容會被
configure_package_config_file
替換。 - include(CMakeFindDependencyMacro) 和 find_dependency(xxx)
如果你的庫還引用了其他庫需要在這里追加,如include(CMakeFindDependencyMacro) find_dependency(Qt5) find_dependency(Boost)
include("${CMAKE_CURRENT_LIST_DIR}/@PROJECT_NAME@Targets.cmake")
作用是引用${PROJECT_NAME}Targets.cmake
編譯,現在在foo_lib下執行以下指令
mkdir build
cd build
cmake .. -DCMAKE_INSTALL_PREFIX=../../install
make install
執行完成后在install目錄結構如下
感興趣的話可以看看FooLibConfig.cmake和FooLibTargets.cmake的內容,就可以看到庫的路徑、名稱和別名了。
使用自定義Package
foo_app只有兩個文件main.cpp
和CMakeLists.txt
main.cpp
這個文件不必多說
#include "foo_lib.h"int main(int argc, char** argv)
{foo_lib_func();
}
CMakeLists.txt
cmake_minimum_required(VERSION 3.5)
project(FooApp VERSION 1.0.0)
find_package(FooLib REQUIRED)
add_executable(${PROJECT_NAME} ${CMAKE_CURRENT_SOURCE_DIR}/main.cpp)
target_link_libraries(${PROJECT_NAME} PRIVATE FooLib::FooLib)
install(TARGETS ${PROJECT_NAME} DESTINATION ${CMAKE_INSTALL_PREFIX}/bin)
這里關鍵的兩步是
- find_package(FooLib REQUIRED) 查找FooLib庫
- target_link_libraries(${PROJECT_NAME} PRIVATE FooLib::FooLib) 編譯時鏈接FooLib庫
編譯
在foo_app目錄下執行
mkdir build
cd build
cmake .. -DCMAKE_INSTALL_PREFIX=../../install
make install
執行完后install目錄結構如下