美文网首页Android-NDK/JNI
CMake入门和大型工程管理

CMake入门和大型工程管理

作者: 啊呀哟嘿 | 来源:发表于2020-02-24 21:23 被阅读0次

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

    基本操作

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

    cmake ..
    make
    make install
    

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

    • 定义项目:
      project(myProject C CXX):该命令会影响PROJECT_SOURCE_DIRPROJECT_BINARY_DIRPROJECT_NAME等变量。另外要注意的是,对于多个project嵌套的情况,CMAKE_PROJECT_NAME是当前CMakeLists.txt文件回溯至最顶层CMakeLists.txt文件中所在位置之前所定义的最后一个project的名字。
      cmake_minimum_required(VERSION 3.0):指出进行编译所需要的CMake最低版本,如果不指定的话系统会自己指定一个,但是也会扔出一个warning

    • 搜索源文件:
      file(<GLOB|GLOB_RECURSE> <variable> <pattern>):按照正则表达式搜索路径下的文件,比如file(GLOB SRC_LIST "./src/*.cpp")
      aux_source_directory(<dir> <variable>):搜索文件内所有的源文件。

    • 添加编译目标:
      add_library(mylib [STATIC|SHARED] ${SRC_LIST})
      add_executable(myexe ${SRC_LIST})

    • 添加头文件目录:
      include_directories(<items>):为该位置之后的target链接头文件目录(不推荐)。
      target_include_directories(<target> <PUBLIC|INTERFACE|PRIVATE]> <items>):为特定的目标链接头文件目录。

    • 添加依赖库:
      link_libraries(<items>):为该位置之后的target链接依赖库。
      target_link_libraries(<target> <items>):为特定的目标链接依赖库。
      这里,常见的依赖库可能是以下几种情况:

      1. 在此次编译的工程里添加的目标,给出目标名;
      2. 外部库,给出路径和库文件全名;
      3. 外部库,通过find_package()等命令搜索到的。

      对于find_package(XXX),该命令本身并不直接去进行搜索,而是通过特定路径下的FindXXX.cmake或XXXConfig.cmake文件来定位头文件和库文件的位置,分别被称为Module模式和Config模式。该命令会定义一个XXX_FOUND变量,如果成功找到,该变量为真,同时会定义XXX_INCLUDE_DIRXXX_LIBRARIES两个变量,用于link和include。

    • 添加子目录:
      add_subdirectories(<dir>):子目录中要有CMakeLists.txt文件,否则会报错。

    • 包含其他cmake文件:
      include(./path/to/tool.cmake)
      set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ./path/to),随后include(tool)
      该命令相当于将tool.cmake的内容直接包含进来。

    • 定义变量:
      set(<variable> <value>... [PARENT_SCOPE])
      set(<variable> <value>... CACHE <type> <docstring> [FORCE])
      其中CACHE会将变量定义在缓存文件CMakeCache.txt里,可以在下次编译的时候读取。

    • 作用域:
      add_subdirectories(<dir>)会创建一个子作用域,里面可以使用父作用域里定义的变量,但里面定义的变量在父作用域不可见,同样,在子作用域修改父作用域里的变量不会影响父作用域。function()同样会产生一个子作用域。若想让子作用域里的定义或者修改在父作用域可见,需要使用PARENT_SCOPE标记。
      相对地,macro()include()不会产生子作用域。

    • 选项:
      add_option(MY_OPTION <ON|OFF>):会定义一个选项。在使用cmake命令时,可以通过-D改变选项的值。比如cmake .. -DMY_OPTION=ON

    • 编译选项:
      add_compile_options(-std=c++11)
      如果想要指定具体的编译器的选项,可以使用make_cxx_flags()cmake_c_flags()

    • 与源文件的交互:
      configure_file(XXX.in XXX.XX)会读入一个文件,处理后输入到新的位置。一方面,会替换掉#XXX或者@XXX@定义的内容。另一方面,会将文件里的#cmakedefine VAR …替换为#define VAR …或者/* #undef VAR */

    • 字符串操作、循环、判断、文件/变量存在判断等
      这些命令同样有用,请参考网络资料。

    当代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。

    相关文章

      网友评论

        本文标题:CMake入门和大型工程管理

        本文链接:https://www.haomeiwen.com/subject/fiatfhtx.html