概述
本文是知乎上HAN Bing写的CMake实战指南,加上一部分个人的理解和实践。
使用find_package引入外部依赖包
通过Cmake内置模块引入依赖包
为了方便我们在项目中引入外部依赖包,cmake官方为我们预定义了许多寻找依赖包的Module,他们存储在path_to_your_cmake/share/cmake-<version>/Modules目录下。每个以Find.cmake命名的文件都可以帮我们找到一个包。我们也可以在官方文档中查看到哪些库官方已经为我们定义好了,我们可以直接使用find_package函数进行引用官方文档:Find Modules。
以curl库为例,假设我们项目需要引入这个库,从网站中请求网页到本地,我们看到官方已经定义好了FindCURL.cmake。所以我们在CMakeLists.txt中可以直接用find_pakcage进行引用。
find_package(CURL)
add_executable(curltest curltest.cc)
if(CURL_FOUND)
target_include_directories(clib PRIVATE ${CURL_INCLUDE_DIR})
target_link_libraries(curltest ${CURL_LIBRARY})
else(CURL_FOUND)
message(FATAL_ERROR ”CURL library not found”)
endif(CURL_FOUND)
对于系统预定义的 Find<LibaryName>.cmake
模块,使用方法一般如上例所示。
每一个模块都会定义以下几个变量
<LibaryName>_FOUND
<LibaryName>_INCLUDE_DIR or <LibaryName>_INCLUDES
<LibaryName>_LIBRARY or <LibaryName>_LIBRARIES
你可以通过<LibaryName>_FOUND
来判断模块是否被找到,如果没有找到,按照工程的需要关闭 某些特性、给出提醒或者中止编译,上面的例子就是报出致命错误并终止构建。 如果<LibaryName>_FOUND
为真,则将<LibaryName>_INCLUDE_DIR
加入 INCLUDE_DIRECTORIES,
通过find_package引入非官方的库(该方式只对支持cmake编译安装的库有效)
假设此时我们需要引入glog库来进行日志的记录,我们在Module目录下并没有找到 FindGlog.cmake。所以我们需要自行安装glog库,再进行引用。
安装
# clone该项目
git clone https://github.com/google/glog.git
# 切换到需要的版本
cd glog
git checkout v0.40
# 根据官网的指南进行安装
cmake -H. -Bbuild -G "Unix Makefiles"
cmake --build build
cmake --build build --target install
此时我们便可以通过与引入curl库一样的方式引入glog库了
find_package(glog)
add_executable(glogtest glogtest.cc)
if(GLOG_FOUND)
# 由于glog在连接时将头文件直接链接到了库里面,所以这里不用显示调用target_include_directories
target_link_libraries(glogtest glog::glog)
else(GLOG_FOUND)
message(FATAL_ERROR ”glog library not found”)
endif(GLOG_FOUND)
Module模式与Config模式
通过上文我们了解了通过Cmake引入依赖库的基本用法。知其然也要知其所以然,find_package对我们来说是一个黑盒子,那么它是具体通过什么方式来查找到我们依赖的库文件的路径的呢。到这里我们就不得不聊到find_package的两种模式,一种是Module模式,也就是我们引入curl库的方式。另一种叫做Config模式,也就是引入glog库的模式。下面我们来详细介绍着两种方式的运行机制。
在Module模式中,cmake需要找到一个叫做Find<LibraryName>.cmake
的文件。这个文件负责找到库所在的路径,为我们的项目引入头文件路径和库文件路径。cmake搜索这个文件的路径有两个,一个是上文提到的cmake安装目录下的share/cmake-<version>/Modules
目录,另一个使我们指定的CMAKE_MODULE_PATH
的所在目录。
如果Module模式搜索失败,没有找到对应的Find<LibraryName>.cmake
文件,则转入Config模式进行搜索。它主要通过<LibraryName>Config.cmake
or <lower-case-package-name>-config.cmake
这两个文件来引入我们需要的库。以我们刚刚安装的glog库为例,在我们安装之后,它在/usr/local/lib/cmake/glog/
目录下生成了glog-config.cmake
文件,而/usr/local/lib/cmake/<LibraryName>/
正是find_package函数的搜索路径之一。(find_package的搜索路径是一系列的集合,而且在linux,windows,mac上都会有所区别,需要的可以参考官方文档find_package)
由以上的例子可以看到,对于原生支持Cmake编译和安装的库通常会安装Config模式的配置文件到对应目录,这个配置文件直接配置了头文件库文件的路径以及各种cmake变量供find_package使用。而对于非由cmake编译的项目,我们通常会编写一个Find<LibraryName>.cmake
,通过脚本来获取头文件、库文件等信息。通常,原生支持cmake的项目库安装时会拷贝一份XXXConfig.cmake到系统目录中,因此在没有显式指定搜索路径时也可以顺利找到。
编写自己的Find<LibraryName>.cmake
模块
假设我们编写了一个新的函数库,我们希望别的项目可以通过find_package对它进行引用我们应该怎么办呢。
我们在当前目录下新建一个ModuleMode
的文件夹,在里面我们编写一个计算两个整数之和的一个简单的函数库。库函数以手工编写Makefile的方式进行安装,库文件安装在/usr/lib目录下,头文件放在/usr/include目录下。其中的Makefile文件如下:
# 1、准备工作,编译方式、目标文件名、依赖库路径的定义。
CC = g++
CFLAGS := -Wall -O3 -std=c++11
OBJS = libadd.o #.o文件与.cpp文件同名
LIB = libadd.so # 目标文件名
INCLUDE = ./ # 头文件目录
HEADER = libadd.h # 头文件
all : $(LIB)
# 2. 生成.o文件
$(OBJS) : libadd.cc
$(CC) $(CFLAGS) -I ./ -fpic -c $< -o $@
# 3. 生成动态库文件
$(LIB) : $(OBJS)
rm -f $@
g++ $(OBJS) -shared -o $@
rm -f $(OBJS)
# 4. 删除中间过程生成的文件
clean:
rm -f $(OBJS) $(TARGET) $(LIB)
# 5.安装文件
install:
cp $(LIB) /usr/lib
cp $(HEADER) /usr/include
编译安装
make
sudo make install
接下来我们回到我们的Cmake项目中来,在cmake
文件夹下新建一个FindAdd.cmake的文件。我们的目标是找到库的头文件所在目录和共享库文件的所在位置。
# 在指定目录下寻找头文件和动态库文件的位置,可以指定多个目标路径
find_path(ADD_INCLUDE_DIR libadd.h /usr/include/ /usr/local/include ${CMAKE_SOURCE_DIR}/ModuleMode)
find_library(ADD_LIBRARY NAMES add PATHS /usr/lib/add /usr/local/lib/add ${CMAKE_SOURCE_DIR}/ModuleMode)
if (ADD_INCLUDE_DIR AND ADD_LIBRARY)
set(ADD_FOUND TRUE)
endif (ADD_INCLUDE_DIR AND ADD_LIBRARY)
这时我们便可以像引用curl一样引入我们自定义的库了。
在CMakeLists.txt中添加
# 将项目目录下的cmake文件夹加入到CMAKE_MODULE_PATH中,让find_pakcage能够找到我们自定义的函数库
set(CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake;${CMAKE_MODULE_PATH}")
add_executable(addtest addtest.cc)
find_package(ADD)
if(ADD_FOUND)
target_include_directories(addtest PRIVATE ${ADD_INCLUDE_DIR})
target_link_libraries(addtest ${ADD_LIBRARY})
else(ADD_FOUND)
message(FATAL_ERROR "ADD library not found")
endif(ADD_FOUND)
引入外部项目
本节主要介绍如何引入外部项目源码作为自己项目的Library,与FindPackage引入头文件编译好的库不同的是,本节介绍的方法直接将第三方库源码引入到项目中,编译自己的项目时也会连同第三方库的源码一同编译。特别是当我们使用git等工具引入代码时,我们可以很方便地控制第三方代码的版本,防止本地安装的库文件版本与项目存在冲突。
通过Submodle的方式引入
克隆spdlog作为项目的子项目
git submodule add https://github.com/gabime/spdlog.git
本项目已经添加了submodule,所以在项目根目录执行以下命令初始化
git submodule init
git submodule update
切换到我们需要的版本
git checkout v1.4.2
我们已经clone好了,现在只需要将spdlog作为subdirectory加入CMakeLists.txt当中就行了
project(ImportExternalProject)
cmake_minimum_required(VERSION 3.5)
add_definitions(-std=c++11) # 指定采用c++11进行编译(spdlog需要c++11)
add_subdirectory(spdlog)
使用FetchContent (CMake 3.11+)
使用FetchContent的步骤总结起来就是:
- 使用FetchContent_Declare(MyName) 获取项目。可以是一个URL也可以是一个Git仓库。
- 使用FetchContent_GetProperties(MyName) 获取我们需要的变量MyName_*。
- 使用add_subdirectory(${MyName_SOURCE_DIR} ${MyName_BINARY_DIR})引入项目。
在cmake3.14版本,官方又为我们提供了更方便的FetchContent_MakeAvailable方法,将步骤2,3集成在了一起。为了兼容3.11版本,我们可以把它封装成一个宏,这样我们就可以统一使用FetchContent_MakeAvailable方法了。
# Campatible with cmake 3.11 and above.
macro(FetchContent_MakeAvailable NAME)
FetchContent_GetProperties(${NAME})
if(NOT ${NAME}_POPULATED)
FetchContent_Populate(${NAME})
add_subdirectory(${${NAME}_SOURCE_DIR} ${${NAME}_BINARY_DIR})
endif()
endmacro()macro(FetchContent_MakeAvailable NAME)
FetchContent_GetProperties(${NAME})
if(NOT ${NAME}_POPULATED)
FetchContent_Populate(${NAME})
add_subdirectory(${${NAME}_SOURCE_DIR} ${${NAME}_BINARY_DIR})
endif()
endmacro()
而后的第三方库cmake目录可以这么写:
# 添加第三方依赖包
include(FetchContent)
# FetchContent_MakeAvailable was not added until CMake 3.14
if(${CMAKE_VERSION} VERSION_LESS 3.14)
include(add_FetchContent_MakeAvailable.cmake)
endif()
set(SPDLOG_GIT_TAG v1.4.1) # 指定版本
set(SPDLOG_GIT_URL https://github.com/gabime/spdlog.git) # 指定git仓库地址
FetchContent_Declare(
spdlog
GIT_REPOSITORY ${SPDLOG_GIT_URL}
GIT_TAG ${SPDLOG_GIT_TAG}
)
FetchContent_MakeAvailable(spdlog)
在CMakeLists.txt中,包含对应的第三方cmake,便可将spdlog作为library来使用了
project(ImportExternalProject)
cmake_minimum_required(VERSION 3.14)
add_definitions(-std=c++11) # 指定采用c++11进行编译(spdlog需要c++11)
add_executable(test_spdlog testspdlog.cc)
set(CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake;${CMAKE_MODULE_PATH}")
include(spdlog2)
target_link_libraries(test_spdlog PRIVATE spdlog)
安装
本节主要介绍如何将项目生成的库文件、头文件、可执行文件或相关文件等安装到指定位置(系统目录,或发行包目录)。在cmake中,这主要是通过install
方法在CMakeLists.txt中配置,make install
命令安装相关文件来实现的。
编写一个简单的库
编写一个计算整数和浮点数之和的库函数mymath
mymath.h
#ifndef MYMATH_H
#define MYMATH_H
int add(int, int);
double add(double, double);
#endif
mymath.cpp
#include "mymath.h"
int add(int a, int b){
return a+b;
}
double add(double a, double b){
return a+b;
}
可执行程序mymathApp.cpp
#include <iostream>
#include "mymath.h"
using namespace std;
int main(int argc, char const *argv[])
{
double a = add(1.1, 1.1);
int b = add(1, 1);
cout << "1.1加1.1等于" << a <<endl;
cout << "1加1等于" << b <<endl;
return 0;
}
编写CMakeLists
在CMakeLists中添加配置
cmake_minimum_required(VERSION 3.0)
project(Installation VERSION 1.0)
# 如果想生成静态库,使用下面的语句
# add_library(mymath mymath.cc)
# target_include_directories(mymath PUBLIC ${CMAKE_SOURCE_DIR}/include)
# 如果想生成动态库,使用下面的语句
add_library(mymath SHARED mymath.cc)
target_include_directories(mymath PRIVATE ${CMAKE_SOURCE_DIR}/include)
set_target_properties(mymath PROPERTIES PUBLIC_HEADER ${CMAKE_SOURCE_DIR}/include/mymath.h)
# 生成可执行文件
add_executable(mymathapp mymathApp.cc)
target_link_libraries(mymathapp mymath)
target_include_directories(mymathapp PRIVATE ${CMAKE_SOURCE_DIR}/include)
接下来我们为生成的target配置安装目录。install
方法的基础用法如下
install(TARGETS MyLib
EXPORT MyLibTargets
LIBRARY DESTINATION lib # 动态库安装路径
ARCHIVE DESTINATION lib # 静态库安装路径
RUNTIME DESTINATION bin # 可执行文件安装路径
PUBLIC_HEADER DESTINATION include # 头文件安装路径
)
LIBRARY, ARCHIVE, RUNTIME, PUBLIC_HEADER是可选的,可以根据需要进行选择。 DESTINATION后面的路径可以自行制定,根目录默认为CMAKE_INSTALL_PREFIX
,可以试用set
方法进行指定,如果使用默认值的话,Unix系统的默认值为 /usr/local
, Windows的默认值为 c:/Program Files/${PROJECT_NAME}
。比如字linux系统下若LIBRARY的安装路径指定为lib
,即为/usr/local/lib
。所以要安装mymath mymathapp
我们可以这样写
# 将库文件,可执行文件,头文件安装到指定目录
install(TARGETS mymath mymathapp
EXPORT MyMathTargets
LIBRARY DESTINATION lib
ARCHIVE DESTINATION lib
RUNTIME DESTINATION bin
PUBLIC_HEADER DESTINATION include
)
他人如果使用我们编写的函数库,安装完成后,希望可以通过find_package
方法进行引用,这时我们需要怎么做呢。
首先我们需要生成一个MyMathConfigVersion.cmake
的文件来声明版本信息
# 写入库的版本信息
include(CMakePackageConfigHelpers)
write_basic_package_version_file(
MyMathConfigVersion.cmake
VERSION ${PACKAGE_VERSION}
COMPATIBILITY AnyNewerVersion # 表示该函数库向下兼容
)
其中PACKAGE_VERSION
便是我们在CMakeLists.txt
开头project(Installation VERSION 1.0)
中声明的版本号
第二步我们将前面EXPORT MyMathTargets
的信息写入到MyLibTargets.cmake
文件中, 该文件存放目录为${CMAKE_INSTALL_PREFIX}/lib/cmake/MyMath
install(EXPORT MyMathTargets
FILE MyLibTargets.cmake
NAMESPACE MyMath::
DESTINATION lib/cmake/MyLib
)
最后我们在源代码目录新建一个MyMathConfig.cmake.in
文件,用于获取配置过程中的变量,并寻找项目依赖包。如果不一来外部项目的话,可以直接include MyMathTargets.cmake
文件
include(CMakeFindDependencyMacro)
# 如果想要获取Config阶段的变量,可以使用这个
# set(my-config-var @my-config-var@)
# 如果你的项目需要依赖其他的库,可以使用下面语句,用法与find_package相同
# find_dependency(MYDEP REQUIRED)
# Any extra setup
# Add the targets file
include("${CMAKE_CURRENT_LIST_DIR}/MyMathTargets.cmake")
最后在CMakeLists.txt文件中,配置生成MyMathTargets.cmake
文件,并一同安装到${CMAKE_INSTALL_PREFIX}/lib/cmake/MyMath
目录中。
configure_file(MyMathConfig.cmake.in MyMathConfig.cmake @ONLY)
install(FILES "${CMAKE_CURRENT_BINARY_DIR}/MyMathConfig.cmake"
"${CMAKE_CURRENT_BINARY_DIR}/MyMathConfigVersion.cmake"
DESTINATION lib/cmake/MyMath
)
最后我们在其他项目中,就可以使用
find_package(MyMath 1.0)
target_linked_library(otherapp MyMath::mymath)
来引用我们的函数库了。