利用cmake构建C++项目

最近出来创业,没法用鹅厂的构建工具了,只能钻研一下开源的build工具,发觉cmake至少用来构建一个中小型项目还是挺方便的,通过写一些辅助脚本,也可以具备一定的自动化能力。

cmake支持外部编译,即在源码包外额外创建一个build目录,好处是不会污染整个源码目录,比较优雅。

$ mkdir build
$ cd build
$ cmake ..
$ make

基础功能

简单示例

先给一个CMakeLists.txt的例子

PROJECT(app)
ADD_EXECUTABLE(myapp
  main.cc
  classA.cc
)
TARGET_LINK_LIBRARIES(myapp
  sqlite my_ilb
)
ADD_SUBDIRECTORY(lib)

# lib/CMakeLists.txt
ADD_LIBRARY(my_lib
   my_lib.c
)

常用命令

隐式变量

<project_name>_SOURCE_DIR 工程代码路径,基本等同于CMAKE_SOURCE_DIRPROJECT_SOURCE_DIR <project_name>_BINARY_DIR 编译目标路径,基本等同于CMAKE_BINARY_DIRPROJECT_BINARY_DIR

指定编译目标路径

SET(EXECUTABLE_OUTPUT_PATH ${PROJECT_BINARY_DIR}/bin)
SET(LIBRARY_OUTPUT_PATH ${PROJECT_BINARY_DIR}/lib)

指定安装路径:

cmake -DCMAKE_INSTALL_PREFIX=/tmp/t2/usr .

或者安装时指定路径也可以:

make install DESTDIR=/tmp/t2/usr

生成库文件

ADD_LIBRARY(libname [SHARED|STATIC|MODULE] [EXCLUDE_FROM_ALL] source1 source2 ... sourceN)

比如,同时生成动态和静态库:

ADD_LIBRARY(hello ${LIBHELLO_SRC})
ADD_LIBRARY(hello_static STATIC ${LIBHELLO_SRC})        # 注意这里使用hello_static为了和动态库不重名
SET_TARGET_PROPERTIES(hello_static PROPERTIES OUTPUT_NAME "hello")      # 这里额外修改生成的libhello_static.a变为libhello.a

设置INC和LIB搜索路径(即make的-I-L参数)

INCLUDE_DIRECTORIES([AFTER|BEFORE] [SYSTEM] dir1 dir2 ...)
LINK_DIRECTORIES(directory1 directory2 ...)

也可以用环境变量 CMAKE_INCLUDE_PATHCMAKE_LIBRARY_PATH

链接库(动态库可以加.so,静态库加.a标识):

TARGET_LINK_LIBRARIES(target library1 library2 ...)

遍历一个目录下所有的源代码文件,并将文件列表存储在一个变量中,这个指令临时被用来自动构建源文件列表:

AUX_SOURCE_DIRECTORY(. DIR_SRCS)

编译信息

Debug模式

cmake -DCMAKE_BUILD_TYPE=Debug

打印更多编译信息

make VERBOSE=1

或者

cmake -DCMAKE_VERBOSE_MAKEFILE=ON .
make

或者,减少一点输出:

cmake -DCMAKE_RULE_MESSAGES=OFF -DCMAKE_VERBOSE_MAKEFILE=ON .
make --no-print-directory

导入依赖包

和Python、Java开发不同,C++开发一大痛点是没有很好的包管理器,如果想引入一个依赖的外部库做法总是很山寨,cmake这里倒是提供了不错的解决方案。分两步走:一是提供了Module机制,用于描述一个依赖包;二是提供了ExternalProject_Add可以方便的导入外部依赖包。

Module描述

以常用库GFlags为例,可以自定义 FindGFlags.cmake 来描述GFlags这个库的头文件和库的路径信息。

1)寻找头文件,如果找到会设置路径到GFLAGS_INCLUDE_DIR变量

find_path(GFLAGS_INCLUDE_DIR
  gflags/gflags.h
  HINTS
  /opt/local/include
  /usr/local/include
  /usr/include
  ${GFLAGS_ROOT_DIR}/src
  )

2)寻找库文件,如果找到会设置路径到GFLAGS_LIBRARY变量

find_library(GFLAGS_LIBRARY
  NAMES gflags
  HINTS
  /usr
  /usr/local
  PATH_SUFFIXES
  x86_64-linux-gnu
  i386-linux-gnu
  lib64
  lib)

3)导入辅助函数find_package_handle_standard_args,功能是判断上面的两个变量GFLAGS_INCLUDE_DIRGFLAGS_LIBRARY是否有值。如果都有值的话,会设置GFLAGS_FOUND供后续调用使用(这种调用哪怕传入的name是小写gflags也会变为大写,另外一种FOUND_VAR的调用方式会保持原来的case)。

include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(gflags
  DEFAULT_MSG
  GFLAGS_INCLUDE_DIR GFLAGS_LIBRARY)

4)如果已经安装了gflags,可以设置头文件和库文件的路径供后续使用。

if (GFLAGS_FOUND)
  set(GFLAGS_LIBRARIES ${GFLAGS_LIBRARY})
  set(GFLAGS_INCLUDE_DIRS ${GFLAGS_INCLUDE_DIR})

  string(REGEX REPLACE "/libgflags.so" "" GFLAGS_LIBRARY_DIR ${GFLAGS_LIBRARIES})

  include_directories(${GFLAGS_INCLUDE_DIR})
  link_directories(${GFLAGS_LIBRARY_DIR})

  mark_as_advanced(GFLAGS_LIBRARIES GFLAGS_INCLUDE_DIRS)
endif(GFLAGS_FOUND)

自动安装依赖包

结合Module和find_libray函数,可以查询本地是否的确安装了相应的包:

set(gflags_RELEASE 2.1.2)
find_package(GFlags)
if (NOT GFLAGS_FOUND)
  message (STATUS " gflags library has not been found.")
  message (STATUS " gflags will be downloaded and built automatically ")
  message (STATUS " when doing 'make'. ")

如果本地没有安装可以通过ExternalProject_Add获取外部资源,包括url/git/svn/cvs等,自动下载并编译安装。

目标路径这里只设置PREFIX即可,其他可以使用默认路径:

TMP_DIR = <prefix>/tmp
STAMP_DIR = <prefix>/src/<name>-stamp
DOWNLOAD_DIR = <prefix>/src
SOURCE_DIR = <prefix> /src/<name>
BINARY_DIR = <prefix>/src/<name>-build
INSTALL_DIR = <prefix>

注意CMAKE_ARGS可以自定义一些编译选项,否则会使用全局设置的编译选项。

建议把UPDATE_COMMAND显式设置为空,否则如果是svn/cvs这种代码仓库,每次会去update代码。

同时我们也不需要安装,可以把INSTALL_COMMAND设置为空。

ExternalProject_Add(
  gflags-${gflags_RELEASE}         # 建议这里的name加上版本号,只在这里使用,不暴露出去用
  PREFIX ${CMAKE_CURRENT_BINARY_DIR}/gflags-${gflags_RELEASE}
  GIT_REPOSITORY https://github.com/gflags/gflags.git
  GIT_TAG v${gflags_RELEASE}
  CMAKE_ARGS -DBUILD_SHARED_LIBS=ON -DBUILD_STATIC_LIBS=ON -DBUILD_gflags_nothreads_LIB=OFF -DCMAKE_CX X_COMPILER=${CMAKE_CXX_COMPILER}
  BUILD_COMMAND make
  UPDATE_COMMAND ""
  PATCH_COMMAND ""
  INSTALL_DIR /path/to/install
  INSTALL_COMMAND make install
  )

可以获取外部项目的一些属性,如下把目标路径和源码路径保存到binary_dirsource_dir变量里。

ExternalProject_Get_Property(gflags-${gflags_RELEASE} binary_dir)
ExternalProject_Get_Property(gflags-${gflags_RELEASE} source_dir)
ExternalProject_Get_Property(gflags-${gflags_RELEASE} install_dir)  # 其实就是 INSTALL_DIR

利用上面取得的路径,设置最终gflags的头文件路径和库文件路径。可以供外部调用。

注意这里的路径不同代码项目一般是不同的,比如gflags的头文件用${binary_dir}/include,而glog的头文件位置是在${binary_dir}/src

CACHE PATH参数表示该参数会被cache住(比如第一次从命令行设置了该参数,第二次命令行调用没带该参数,该参数也能生效)。

set(GFLAGS_INCLUDE_DIRS ${binary_dir}/include CACHE PATH "Local Gflags headers")
set(GFLAGS_LIBRARY_PATH ${binary_dir}/lib )

set(GFLAGS_BUILD_DIR ${binary_dir}) # to compile glog

我们拿到编译完后的信息之后,可以新建一个library的名字供外部使用,这里取名gflags(去掉了版本号)。

由于编译已经完成了,只要导入编译完的库就好了。

说明:

  • add_library(... IMPORTED) ,表示只是导入一个已经存在的库。
  • set_target_properties(... PROPERTIES IMPORTED_LOCATION ...),设置该库的具体路径。
  • add_dependencies(...) 该导入库依赖之前的外部库。
set(GFLAGS_LIBRARIES gflags)
add_library(gflags UNKNOWN IMPORTED)
set_target_properties(gflags PROPERTIES IMPORTED_LOCATION ${GFLAGS_LIBRARY_PATH}/libgflags.a)
add_dependencies(gflags gflags-${gflags_RELEASE})

引入头文件路径和库文件路径。

  # file(GLOB GFLAGS_SHARED_LIBRARIES "${binary_dir}/libgflags${CMAKE_SHARED_LIBRARY_SUFFIX}*")
  include_directories(${GFLAGS_INCLUDE_DIRS})
  link_directories(${GFLAGS_LIBRARY_PATH})
endif(NOT GFLAGS_FOUND)

导入外部依赖

示例:glog不同于gflags,它是用的autotools工具链,并且还依赖gflags,所以需要做一些额外处理。

这里需要额外创建一个定制的configure_with_gflags文件,用来配置它所依赖的gflags参数。

file(WRITE ${GLOG_CONFIGURE_TMP}
    "#!/bin/sh
export CPPFLAGS=-I${GFLAGS_INCLUDE_DIRS}
export LDFLAGS=-L${GFLAGS_LIBRARY_PATH}
export LIBS=-lgflags
${GLOG_SOURCE_DIR}/configure --with-gflags=${GFLAGS_BUILD_DIR}")

这里给configure_with_gflags文件添加指向属性。

file(COPY ${GLOG_CONFIGURE_TMP}
  DESTINATION ${GLOG_PREFIX}
  FILE_PERMISSIONS
  OWNER_READ OWNER_WRITE OWNER_EXECUTE GROUP_READ GROUP_EXECUTE WORLD_READ WORLD_EXECUTE)

TODO:感觉上面的步骤也可以用ExternalProject_Add_Step完成?

添加额外的CONFIGURE_COMMAND参数,表明该外部项目采用autotools工具链。

ExternalProject_Add(
  glog-${glog_RELEASE}
  DEPENDS gflags
  PREFIX ${GLOG_PREFIX}
  GIT_REPOSITORY https://github.com/google/glog.git
  GIT_TAG v${glog_RELEASE}
  CONFIGURE_COMMAND ${GLOG_CONFIGURE} --prefix=<INSTALL_DIR>
  BUILD_COMMAND make
  UPDATE_COMMAND ""
  PATCH_COMMAND ""
  INSTALL_COMMAND ""
  )

ExternalProject_Get_Property(glog-${glog_RELEASE} binary_dir)
ExternalProject_Get_Property(glog-${glog_RELEASE} source_dir)

# set(GLOG_INCLUDE_DIRS ${binary_dir}/src CACHE PATH "Local glog headers")
set(GLOG_INCLUDE_DIRS ${binary_dir}/src CACHE PATH "Local glog headers")
set(GLOG_LIBRARY_PATH ${binary_dir}/.libs )

如果需要在编译完再做一些额外的工作(这里是需要把log_severity.h函数复制到相应位置),可以调用如下函数。 注意设置DEPENDEES <build>,表示该步骤依赖之前的某步build。

【注】如果依赖前面的build必须用这种方式,才能正确的处理依赖关系。比如,如果直接调用file(COPY...)命令肯定不行。

# WORKAROUND log_severity.h is missing
ExternalProject_Add_Step(
  glog-${glog_RELEASE} workaround
  COMMAND cp ${source_dir}/src/glog/log_severity.h ${GLOG_INCLUDE_DIRS}/glog
  DEPENDEES build
  )