Android-NDK/JNI

CMake入门和大型工程管理

2020-02-24  本文已影响0人  啊呀哟嘿

最近在负责一个大型工程的CMake编译系统管理,整理一些工作过程中积累下来的知识片段和技巧。CMake是一个跨平台的编译工具。

基本操作

通过编写CMakeLists.txt指挥cmake进行构建和编译。
通常我们会在根目录新建一个build文件夹,然后依次执行:

cmake ..
make
make install

其中cmake命令主要任务是按照CMakeLists.txt编写的规则生成MakeFile,而make会按照MakeFile进行编译、汇编和链接,从而生成可执行文件或者库文件。make install则是将编译好的文件安装到指定的目录。
CMake常用的命令或函数包括:

当代CMake理念

参考1: https://kubasejdak.com/modern-cmake-is-like-inheritance
翻译自: https://pabloariasal.github.io/2018/02/19/its-time-to-do-cmake-right/

一些人士指出,CMake应该是基于Targets目标和Properties属性的,应有面向对象的思想。
目标指的当然就是library和executable。目标的属性则具有两种不同的作用域:INTERFACE(接口)和PRIVATE(私有)。私有属性适用于构建目标本身时内部使用,而接口属性则是由目标的使用者在外部使用的。也就是说,接口属性定义了使用要求,而私有属性则定义了目标本身的构建要求。
此外,属性也可以被定义为PUBLIC(公有),当且仅当其既是私有又是接口。
比如,假如一个工程里有如下文件:

libjsonutils
├── CMakeLists.txt
├── include
│   └── jsonutils
│       └── json_utils.h
├── src
│   ├── file_utils.h
│   └── json_utils.cpp
└── test
    ├── CMakeLists.txt
    └── src
        └── test_main.cpp

我们注意到,include/中有json_utils.h头文件,这是我们想对外暴露的公共文件;而src/中有额外的头文件file_utils.h,这个文件仅在构建中使用,不想对外暴露。这两个头文件都应该在构建的时候被包含(include) ;另一方面,jsontuils的使用者又仅仅需要知道公开的头文件,因此INTERFACE_INCLUDE_DIRS只需要包含include/,而没有src/
为此,可以在CMakeLists.txt使用如下代码(这里使用了CMake的generator expression特性):

add_library(JSONUtils src/json_utils.cpp)
target_include_directories(JSONUtils
    PUBLIC 
        $<INSTALL_INTERFACE:include>    
        $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
    PRIVATE
        ${CMAKE_CURRENT_SOURCE_DIR}/src
)

对于目标的依赖项,同样有INTERFACEPRIVATE的区分。
比如:

find_package(Boost 1.55 REQUIRED COMPONENTS regex)
find_package(RapidJSON 1.0 REQUIRED MODULE)

target_link_libraries(JSONUtils
    PUBLIC
        Boost::boost RapidJSON::RapidJSON
    PRIVATE
        Boost::regex
)

这种情况,rapidjson和Boost::boost都应当被定义成接口类型的依赖,并被传递到目标的使用者那边,因为用户所导入的头文件中调用了这两个库的工具。这意味着JSONUtils的用户不仅需要JSONUtils的接口属性,同时也需要其接口类型的依赖的接口属性(在我们的情况下,定义了boost和rapidjson的公共头文件),甚至接口类型的依赖的接口类型的依赖的接口属性,等等。
对于CMake而言,它会将Boost::boostRapidJSON::RapidJson的所有接口属性添加到JSONUtils的接口属性中。这意味着JSONUtils的用户会传递获取依赖链条上所有的接口属性。
另一方面Boost::regex则仅在我们目标的内部使用,并且可以作为私有依赖。这种情况下,Boost::regex的接口属性会被添加到JSONUtils的私有属性中,而不会传递到用户那里。

导入目标

当我们执行find_package(Boost 1.55 REQUIRED COMPONENTS regex)的时候,CMake实际执行了FindBoost.cmake脚本,并由此导入了目标Boost::boostBoost::regex,这是为什么我们能通过target_link_libraries()来依赖这些目标。
然而部分第三方库并不那么守规矩,比如RapidJSON的RapidJSONConfig.cmake:

get_filename_component(RAPIDJSON_CMAKE_DIR "${CMAKE_CURRENT_LIST_FILE}" PATH)
set(RAPIDJSON_INCLUDE_DIRS "/usr/include")
message(STATUS "RapidJSON found. Headers: ${RAPIDJSON_INCLUDE_DIRS}")

它实际上并没有定义目标,只是定义了RAPIDJSON_INCLUDE_DIRS一个变量。
这种情况,我们可以自己编写FindRapidJSON.cmake文件:

# FindRapidJSON.cmake
#
# Finds the rapidjson library
#
# This will define the following variables
#
#    RapidJSON_FOUND
#    RapidJSON_INCLUDE_DIRS
#
# and the following imported targets
#
#     RapidJSON::RapidJSON
#
# Author: Pablo Arias - pabloariasal@gmail.com

find_package(PkgConfig)
pkg_check_modules(PC_RapidJSON QUIET RapidJSON)

find_path(RapidJSON_INCLUDE_DIR
    NAMES rapidjson.h
    PATHS ${PC_RapidJSON_INCLUDE_DIRS}
    PATH_SUFFIXES rapidjson
)

set(RapidJSON_VERSION ${PC_RapidJSON_VERSION})

mark_as_advanced(RapidJSON_FOUND RapidJSON_INCLUDE_DIR RapidJSON_VERSION)

include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(RapidJSON
    REQUIRED_VARS RapidJSON_INCLUDE_DIR
    VERSION_VAR RapidJSON_VERSION
)

if(RapidJSON_FOUND)
    set(RapidJSON_INCLUDE_DIRS ${RapidJSON_INCLUDE_DIR})
endif()

if(RapidJSON_FOUND AND NOT TARGET RapidJSON::RapidJSON)
    add_library(RapidJSON::RapidJSON INTERFACE IMPORTED)
    set_target_properties(RapidJSON::RapidJSON PROPERTIES
        INTERFACE_INCLUDE_DIRECTORIES "${RapidJSON_INCLUDE_DIR}"
    )
endif()

导出自己的库

如果想让自己的工程能够被别人通过简单的命令使用:

find_package(JSONUtils 1.0 REQUIRED)
target_link_libraries(example JSONUtils::JSONUtils)

我们需要做两件事:首先,需要导出目标JSONUtils::JSONUtils;随后,需要允许下游应用find_package(JSONUtils)的时候能够导入这个目标。
首先我们要将目标导出到一个能够导入目标的JSONUtilsTargets.cmake

include(GNUInstallDirs)
install(TARGETS JSONUtils
    EXPORT jsonutils-targets
    LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
    ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
)

install(EXPORT jsonutils-targets
  FILE
    JSONUtilsTargets.cmake
  NAMESPACE
    JSONUtils::
  DESTINATION
    ${CMAKE_INSTALL_LIBDIR}/cmake/JSONUtils
)

这样,我们安装了一个JSONUtilsTargets.cmake文件,这里面包含了导入JSONUtils的命令,只需要在别的文件中使用这个文件就可以导入。
下一步,我们制作一个JSONUtilsConfig.cmake

get_filename_component(JSONUtils_CMAKE_DIR "${CMAKE_CURRENT_LIST_FILE}" PATH)
include(CMakeFindDependencyMacro)

find_dependency(Boost 1.55 REQUIRED COMPONENTS regex)
find_dependency(RapidJSON 1.0 REQUIRED MODULE)

if(NOT TARGET JSONUtils::JSONUtils)
    include("${JSONUtils_CMAKE_DIR}/JSONUtilsTargets.cmake")
endif()

大型工程

在第一部分介绍的都是基本命令,对于大型工程来说,会用到一些不太常用的概念或者功能。

什么是Project?

对于大型工程来说,project的概念变得更为重要。通常来说,简单的工程只需要有一个project,而对于复杂的工程,有可能会出现project的嵌套。
Project通常指的是一个逻辑上相对独立、完整,能够独立编译的集合。通常来说,如果某一个CMakeLists.txt文件中出现了project()命令,那你应该能以该文件所在的目录为根目录进行一次完整的编译。
https://stackoverflow.com/questions/26878379/in-cmake-what-is-a-project)该命令也会如上文所说的,影响CMAKE_PROJECT_NAME等变量的值。

文件组织

文件组织方式就见仁见智了。不过通常来说,为了方便cmake的管理,建议以modules的形式扁平地组织,并且在每个module中设置有限的文件层次。比如说我们有一个moduleA,其下面有src、include和test三个目录,而在include目录下面,再根据具体的功能分为不同的目录,再下一级就只有头文件。
这样在添加头文件目录的时候,统一添加为*/moduleA/include,而在源文件或者其他头文件包含的时候,可以从include下一级目录开始:#include "abc/a.hpp"

模块下的CMakeLists.txt

在一个模块下,可以遵循以下规律编写CMakeLists.txt:

  1. 设置内部模块依赖
  2. 搜索内部依赖模块的头文件和库文件
  3. 设置项目内第三方模块依赖
  4. 搜索项目内第三方模块依赖库的头文件和库文件
  5. 设置和搜索本地的外部依赖库
  6. 添加编译目标
  7. 包含头文件目录、链接库文件
  8. 设置安装规则(比如一些配置文件)
  9. 设置单元测试

头文件暴露

有的时候,有些头文件只供内部使用,不想暴露在install后的头文件目录里。那就将其放在src路径下。

依赖顺序管理

CMake中链接库的顺序是a依赖b,那么b放在a的后面。
例如目标test依赖a库、b库, a库又依赖b库,那么顺序如下:
target_link_libraries(test a b)
另外,假如目标test依赖a库, a库又依赖b库,但test不直接依赖b库,那么test不用链接b库。
如果在一个工程中有多个target,那么可以用add_dependencies(<target> [<target-dependency>]...)命令,来定义依赖关系。这样CMake会首先编译被依赖的目标,随后再编译依赖的目标。

INTERFACE|PUBLIC|PRIVATE

INTERFACE|PUBLIC|PRIVATE

如何调试

nm -a <target>命令查看符号表。
如果出现

Undefined symbols for architecture x86_64:
  "_main"

可能是在没有main的cpp文件定义add_executable。
构造函数和析构函数声明了就要定义,要么用default。

上一篇 下一篇

猜你喜欢

热点阅读