在一些场景下我们需要编写一些库,并希望其他程序可以找到这些库并引用。
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目录结构如下