背景

最近在一个C++项目中尝试Bazel编译,编译依赖方式确实写着比较舒服和直观,但最后链接出来的二进制文件在执行时报Segment error,但用CMake编译出来的二进制文件就可以成功执行,Bazel编译的问题无从下手。另外,Bazel无法从系统目录查找头文件,这就不能忍了,有人建议从cc_toolchain_config.bzl查找问题,但toolchain实在是有点麻烦,就暂时放弃Bazel,继续使用CMake了。Bazel里提供的git_repositry等从外部源自动下载编译依赖的方式很好用,所以就思考在CMake里是不是也有类似的东西呢。之前使用CMake时,第三方依赖都是手动先在本地安装好,后来查找到了CMake里提供了类似Bazel的命令,那就是ExternalProject,不过这个命令只管下载编译等操作,但git_repositry更好使一些,它可以根据依赖自动判断是不是下载,而ExternalProject就没这么丝滑了,所以本文记录下在CMake怎样基于ExternalProject打造git_repositry那种丝滑的体验。

FindPackageHandleStandardArgs

在具体方案之前,先看一下CMake里提供的这个函数,在后面的实现中它可是非常重要哦。

This module provides a function intended to be used in Find Modules implementing find_package(<PackageName>) calls. It handles the REQUIRED, QUIET and version-related arguments of find_package. It also sets the <PackageName>_FOUND variable. The package is considered found if all variables listed contain valid results, e.g. valid filepaths.

我们在CMake里查找包时使用find_package(<PackageName>),它判断一个包是否找到就是利用了FindPackageHandleStandardArgs。这个函数会判断几个关键变量是否有正确的值,如果都经过了验证,就设置<PackageName>_FOUND,既找到了包,否则就没找到。它有两个函数定义,一个简单的是这样滴:
find_package_handle_standard_args(<PackageName>
(DEFAULT_MSG|<custom-failure-message>)
<required-var>...
)

简单的例子,
FIND_PACKAGE_HANDLE_STANDARD_ARGS(Gflags DEFAULT_MSG GFLAGS_INCLUDE_DIR GFLAGS_LIBRARY)

通过判断GFLAGS_INCLUDE_DIRGFLAGS_LIBRARY是否有正确值来断定GFlags是否找到,这里的GFLAGS_INCLUDE_DIRGFLAGS_LIBRARY当然是我们自己设定喽,比如我们这么设定它们,如果搜到了头文件和库文件,就设置正确路径,否则就没值嘛。注意,这里我们最好不要用GFLAGS_LIBIARIES这种变量,这种我们是用来作为链接依赖库的变量的呀,同理,头文件那个变量也一样哦。
find_path(GFLAGS_INCLUDE_DIR gflags/gflags.h /usr/local/include)
find_library(GFLAGS_LIBRARY gflags HINTS /usr/local/lib)

具体方案

我们比较常用的一种第三方库依赖的方式是这样嘛,先在本地系统目录搜下,如果找到就用,如果找不到就自动去下载编译。所以这里我们还是以GFlags为例,分两步:

  • 从系统目录查找GFlags,如果找到则设置GFLAGS_INCLUDE_DIRGFLAGS_LIBRARY
  • 如果上一步没找到,那么从某个地方下载Gflags,本地编译安装后,再设置GFLAGS_INCLUDE_DIRGFLAGS_LIBRARY

我们把这两步定义成两个宏DO_FIND_GFLAGS_SYSTEMDO_FIND_GFLAGS_DOWNLOAD,然后条件判断调用就行啦,像这样:

if(NOT GFLAGS_FOUND)
DO_FIND_GFLAGS_SYSTEM()
endif()

if(NOT GFLAGS_FOUND)
DO_FIND_GFLAGS_DOWNLOAD()
endif()

DO_FIND_GFLAGS_SYSTEM比较简单哦,我们直接给出代码:

macro(DO_FIND_GFLAGS_SYSTEM)
find_path(GFLAGS_INCLUDE_DIR gflags/gflags.h
PATHS /usr/local/include /usr/include
)
message("GFLAGS_INCLUDE_DIR: " ${GFLAGS_INCLUDE_DIR})
find_library(GFLAGS_LIBRARY
NAMES gflags
PATHS /usr/local/lib /usr/local/lib64 /usr/lib /usr/lib64
)
message("GFLAGS_LIBRARY: " ${GFLAGS_LIBRARY})
FIND_PACKAGE_HANDLE_STANDARD_ARGS(Gflags DEFAULT_MSG
GFLAGS_INCLUDE_DIR GFLAGS_LIBRARY
)
set(GFLAGS_LIBRARIES ${GFLAGS_LIBRARY})
set(GFLAGS_INCLUDE_DIRS ${GFLAGS_INCLUDE_DIR})
mark_as_advanced(GFLAGS_LIBRARIES GFLAGS_INCLUDE_DIRS)
endmacro()

先从系统目录查找头文件和库文件,如果找到就设置GFLAGS_INCLUDE_DIRGFLAGS_LIBRARY,然后调用FIND_PACKAGE_HANDLE_STANDARD_ARGS来通过我们设置的两个变量来断定GFlags到底找没找到,如果找到它会自动设置GFLAGS_FOUNDTrue,那第二个宏,就是先下载再编译的那个,就不用执行喽。最后,我们设定GFLAGS_INCLUDE_DIRSGFLAGS_LIBRARIES供我们的主程序作为头文件路径和库依赖路径使用呗。

第二个宏,我们终于用到开头提到的ExternalProject这个牛逼玩意了^_^

The ExternalProject_Add() function creates a custom target to drive download, update/patch, configure, build, install and test steps of an external project.

太多内容,咱就直接贴用法再讲解吧。
macro(DO_FIND_GFLAGS_DOWNLOAD)
include(ExternalProject)
ExternalProject_Add(
Gflags
URL https://github.com/gflags/gflags/archive/v2.2.1.zip
URL_HASH SHA256=4e44b69e709c826734dbbbd5208f61888a2faf63f239d73d8ba0011b2dccc97a
UPDATE_COMMAND ""
CONFIGURE_COMMAND cmake -DCMAKE_INSTALL_PREFIX=${GFLAGS_ROOT_DIR} -DBUILD_SHARED_LIBS=ON -DBUILD_STATIC_LIBS=ON -DGFLAGS_NAMESPACE=google .
BUILD_COMMAND make
BUILD_IN_SOURCE true
INSTALL_COMMAND make install
INSTALL_DIR ${GFLAGS_ROOT_DIR}
)

ExternalProject_Get_Property(Gflags INSTALL_DIR)
set(GFLAGS_INCLUDE_DIR ${INSTALL_DIR}/include)
message("GFLAGS_INCLUDE_DIR: " ${GFLAGS_INCLUDE_DIR})
set(GFLAGS_LIBRARY ${INSTALL_DIR}/lib/${LIBRARY_PREFIX}gflags${LIBRARY_SUFFIX})
message("GFLAGS_LIBRARY: " ${GFLAGS_LIBRARY})

FIND_PACKAGE_HANDLE_STANDARD_ARGS(Gflags DEFAULT_MSG
GFLAGS_INCLUDE_DIR GFLAGS_LIBRARY
)
set(GFLAGS_LIBRARIES ${GFLAGS_LIBRARY})
set(GFLAGS_INCLUDE_DIRS ${GFLAGS_INCLUDE_DIR})
mark_as_advanced(GFLAGS_LIBRARIES GFLAGS_INCLUDE_DIRS)
endmacro()

ExternalProject_Addgayhub上下载gflags源码,然后通过一系列编译安装操作,本地某临时目录就安装好GFlags了。里面几个参数CONFIGURE_COMMANDBUILD_COMMANDINSTALL_COMMAND都是项目常用的流程,还是比较舒服滴。安装好后,我们通过ExternalProject_Get_Property获取到真实安装路径,就可以像上面第一步那样设置GFLAGS_INCLUDE_DIRGFLAGS_LIBRARY啦,后面都一样一样的。

回顾

是不是很简单?那咱总结下里面的关键点吧。

  1. 先使用find_pathfind_library查找系统头文件和库,找到了设置GFLAGS_INCLUDE_DIRGFLAGS_LIBRARY变量
  2. 本地没找到,就用ExternalProject_Add从某地下载安装,然后设置GFLAGS_INCLUDE_DIRGFLAGS_LIBRARY变量
  3. 使用FIND_PACKAGE_HANDLE_STANDARD_ARGS来验证上面两个变量,验证没问题则GFLAGS_FOUNDTrue

那主程序那边怎么用呢?上面那些东西都保存为third_party/FindGflags.cmake文件,在我们的CMakeLists.txt里把它导入就行啦。

# 导入FindGflags.cmake
list(APPEND CMAKE_MODULE_PATH ${CMAKE_SOURCE_DIR}/third_party)

# 查找gflags
find_package(Gflags)

# 主程序头文件搜索路径
include_directories(
${CMAKE_CURRENT_SOURCE_DIR}
${GFLAGS_INCLUDE_DIRS}
)

# 主目标
add_executable(hello hello.cc)
target_link_libraries(
hello
${GFLAGS_LIBRARIES}
)

最后,几个常用的第三方库都用这种方式实现了下,主要是我自己用的哦。

https://github.com/formath/cmake_third_party