Modern Cmake

历史背景

CMake是一个构建系统生成器(build-system generator)。常见的构建系统,有Visual Studio,XCode,Make等等。CMake可以支持不同平台下构建系统的生成。
思考:ninja是构建系统吗

CMake的出现已经有接近20年的历史,它的发展过程也初步经历了三个阶段。

  • ~2000 (~v2.x) ,刚刚启动,过程式描述为主。
  • 2000~2014 (v3.0~) ,引入Target概念。
  • 2014~now (~v3.15),有了Target和Property的定义,更现代化。

概 述

现代化的CMake是围绕 Target 和 Property 来定义的,并且竭力避免出现变量variable的定义。Variable横行是典型CMake2.8时期的风格。现代版的CMake更像是在遵循OOP的规则,通过target来约束link、compile等相关属性的作用域。

Target 概念

旧版 CMake 2.0 主要是基于 directory 来构建,很多复用只能靠变量实现。Modern CMake 最大的改进是引入了 target,支持了对构建的闭包性和传播性的控制
,从而实现了构建可以模块化。

在 Modern CMake 中强烈推荐抛弃旧的 directory 方式,使用 target 的方式构建整个工程。

Target 分类

Target 中最核心的两个分类是:executable, library。

其中 executable 是可执行程序,在不同的操作系统会有不同的格式,同样一个工程内也可能需要生成多个可执行程序。 具体指令如下所示:

add_executable( [WIN32] [MACOSX_BUNDLE]
              [EXCLUDE_FROM_ALL]
              [source1] [source2 ...])

library 代表链接库,可以分为 share, static, object, module, interface 五个种类。

  • share 表示共享库,在编译构建过程中,需要链接但不会添加到最后的可执行文件中。共享库在程序运行中可以被动态加载和替换,当被多个程序使用时还可以在内存中被共享。如果期望 library 可以被独立的部署和替换的话,需要选择这种方式。
  • static 表示静态库,会在编译过程中被一起添加生成到可执行文件中。当静态库的实现发生变更时,必须要重新编译整个系统才可以使用。使用静态库的一个好处是,生成的可执行程序可以独立的运行,不再需要依赖这个静态库。
  • module 也是共享库的一种,CMake 中限制了 moudle 类型的 libray 不能被编译时链接,只能通过 dlopen 在运行时动态加载使用。
  • object 类型的库表示一组编译后的文件,并不会打包和链接。使用 object 类型的库可以避免一些大的源文件被重复的编译,提升编译效率。
  • interface 类型的并不会编译输出文件,代表一组接口文件,可以在编译构建中被其他 target 使用。使用 interface 类型的库可以把多个模块公共的接口头文件作为一个单独 target 来被引用,构建更加高效。

定义库具体指令如下:

add_library( [STATIC | SHARED | MODULE |OBJECT |INTERFACE] ...)

Target 闭包性

为了实现 target 闭包性,Modern CMake 实现 target 与 构建和使用中所有依赖建立绑定关系,从而可以拿来即用。正常情况下编译一个 target(可执行程序或者库)需要依赖如下所示:

  • 源文件列表,通过 target_sources 配置。
  • 头文件列表,通过 target_include_directories 配置。
  • 预编译宏,通过 target_compile_definition 配置。
  • 编译选项和特性,通过 target_compile_options,target_compile_features 配置。
  • 链接选项,通过 target_link_options 配置。

在 C/C++软件系统中,一个 target 中大部分的头文件是仅在模块内使用,为内部接口,仅有小一部分接口头文件是外部使用,称为对外接口。在软件设计过程中,要从高内聚低耦合的角度出发,去严格设计每个 target 的外部接口和内部接口。同样构建过程中,在链接不同 target 时也需要明确指明依赖的外部接口文件,从而提高编译构建的效率。

为了更好支持这个特性,Modern CMake 针对 target 引入两个概念:user requriement(用户依赖) 和 build requirement(编译依赖)。用户依赖表示 target 使用方需要的依赖,而编译依赖表示当前 target 编译构建时需要依赖。

Modern CMake 增加了三个关键字 INTERFACE、PUBLIC、PRIVATE 分布表示不同作用域, 下面以添加头文件依赖命令为例说明:

target_include_directories( [SYSTEM] [BEFORE]
   [items1...]
  [ [items2...] ...])

给 target 添加头文件依赖路径时:

  • INTERFACE : 表示添加的头文件路径仅 target 的使用方需要,编译当前 target 并不需要。
  • PRIVATE : 表示添加的头文件路径仅当前 target 编译时使用,其他 target 不需要。
  • PUBLIC : 表示编译时和链接该 target 都需要使用。

在 Modern CMake 中强烈建议为 target 添加依赖接口时,从使用者角度考虑写明 INTERFACE, PRIVATE, PUBLIC。

在 Modern CMake 中推荐使用 target_sources 来添加源文件依赖,保持每个接口的职责单一。

Target 传播性

当构建工程中 包含比较多的 libary 时,编译和管理这些 Libary 之间的依赖就变得尤为重要。在 Modern CMake 中,当给 Libary 定义用户依赖和编译依赖后,通过在 target_link_libraries 中定义与其他组件间的依赖关系, 就可以自动传递和推演 target 之间的所有编译依赖。

组件间的依赖关系定义命令如下:

target_link_libraries(
                       ...
                     [ ...]...)
  • PRIVATE: 被依赖 libary 的 user requirement 的会变成当前 target 的 build requirement
  • PUBLIC:被依赖 libary 的 user requirement 的会变成当前 target 的 build requirement 和 user requirement.
  • INTERFACE:被依赖 libary 的 user requirement 的会变成当前 target 的 user requirement
    充分利用 Modern CMake 强大的依赖传递功能,合理设计每个 target 间的依赖关系。

如果把一个Target想象成一个对象(Object),会发现两者的组织方式非常相似:

构造函数:

  • add_executable
  • add_library

成员函数:

  • get_target_property()
  • set_target_properties()
  • get_property(TARGET)
  • set_property(TARGET)
  • target_compile_definitions()
  • target_compile_features()
  • target_compile_options()
  • target_include_directories()
  • target_link_libraries()
  • target_sources()

成员变量:

  • Target properties(太多)

知识点和principle

1、在现代IDE中的Multi-configuration

cmake -DCMAKE_BUILD_TYPE=Release .. // 在xcode或vs上不生效,build type选择后移至IDE中控制,而非cmake阶段。
cmake --build . --config release // Apple、MSVC使用cmake命令行构建时release包时需要加上--config参数,否则默认debug。
在现代IDE中,Build-type一般都不是在CMake config期间能确定的。如VS,XCode都支持Multi-configuration,具体使用Debug还是Release是在编译时才确定,那如果Target的依赖路径或者依赖库需要区分Configuration来配置该怎么办呢?在传统CMake中是比较难办的,target_link_libraries提供了一种手段,可以用debug和optimized来区分具体的库名,而其他的编译或链接设置则比较困难。在Modern CMake中,我们可以通过generator-expression来实现。

generator-expression定义为$<...>的形式。该表达式的值有多种形式,而且支持嵌套使用:

条件表达式
$ 当条件为1时,表达式为true_string,否则为空
$ 当条件为1时,表达式为true_string,否则为false_string
变量表达式
$ 当target存在为1,否则为0
$ 当config为cfg时为1,否则为0。这是非常高频使用的一个表达式,可以通过它来区分Debug/Release等不同的config。如下例所示,通过嵌套使用上述两个表达式,可以达到区分CONFIG来设置依赖库路径的目的。
target_link_directories(${PROJECT_NAME} PUBLIC                                                                                                                                                                      
  $<$:${CONAN_LIB_DIRS_DEBUG}>                                                                                                                                                                        
  $<$:${CONAN_LIB_DIRS_RELEASE}>) 

思考:假设target A依赖于TARGET B C D E,希望A和C是debug,B D E是release,是否支持

2、Forget those commands:
  • add_compile_options()
  • include_directories()
  • link_directories()
  • link_libraries()
3、install和find_package
  • 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}。

  • find_package
SET(MyLib_DIR "${CMAKE_INSTALL_PREFIX}/lib/cmake/MyLib" CACHE FILEPATH "MyLib package." FORCE)
FIND_PACKAGE(MyLib REQUIRED CONFIG)

设置${PROJECT_NAME}_DIR,FIND_PACKAGE使用CONFIG模式。
如果使用默认的FIND_PACKAGE module模式,行为如何?

4、ExternalProjects

Create custom targets to build projects in external trees

INCLUDE(ExternalProject)

SET(JSONCPP_SOURCES_DIR ${THIRD_PARTY_DIR}/jsoncpp)
SET(JSONCPP_INSTALL_DIR ${CMAKE_CURRENT_BINARY_DIR}/third_party)
SET(JSONCPP_ROOT ${JSONCPP_INSTALL_DIR} CACHE FILEPATH "jsoncpp root directory." FORCE)
SET(JSONCPP_INCLUDE_DIR "${JSONCPP_INSTALL_DIR}/include" CACHE PATH "jsoncpp include directory." FORCE)

IF(WIN32)
  SET(JSONCPP_LIBRARIES "${JSONCPP_INSTALL_DIR}/lib/jsoncpp.lib" CACHE FILEPATH "jsoncpp library." FORCE)
ELSE(WIN32)
  SET(JSONCPP_LIBRARIES "${JSONCPP_INSTALL_DIR}/lib/libjsoncpp.a" CACHE FILEPATH "jsoncpp library." FORCE)
ENDIF(WIN32)

INCLUDE_DIRECTORIES(${JSONCPP_INCLUDE_DIR})

ExternalProject_Add(
    extern_jsoncpp
    ${EXTERNAL_PROJECT_LOG_ARGS}
    DEPENDS
    SOURCE_DIR       ${JSONCPP_SOURCES_DIR}
    UPDATE_COMMAND   ""
    DOWNLOAD_COMMAND ""
    ${EXTERNAL_PROJECT_CMAKE_ARGS}
    BUILD_BYPRODUCTS ${JSONCPP_LIBRARIES}
    CMAKE_ARGS      -DCMAKE_CXX_COMPILER=${CMAKE_CXX_COMPILER}
    CMAKE_ARGS      -DCMAKE_C_COMPILER=${CMAKE_C_COMPILER}
    CMAKE_ARGS      -DCMAKE_AR=${CMAKE_AR}
    CMAKE_ARGS      -DCMAKE_RANLIB=${CMAKE_RANLIB}
    CMAKE_ARGS      -DCMAKE_CXX_FLAGS=${CMAKE_CXX_FLAGS}
    CMAKE_ARGS      -DCMAKE_C_FLAGS=${CMAKE_C_FLAGS}
    CMAKE_ARGS      -DCMAKE_INSTALL_PREFIX=${JSONCPP_INSTALL_DIR}
                    -DBUILD_SHARED_LIBS=OFF
                    -DCMAKE_POSITION_INDEPENDENT_CODE=ON
                    -DCMAKE_MACOSX_RPATH=ON
    CMAKE_ARGS      -DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE}
    CMAKE_CACHE_ARGS -DCMAKE_INSTALL_PREFIX:PATH=${JSONCPP_INSTALL_DIR}
                     -DCMAKE_POSITION_INDEPENDENT_CODE:BOOL=ON
                     -DCMAKE_BUILD_TYPE:STRING=${CMAKE_BUILD_TYPE}
)

ADD_LIBRARY(jsoncpp STATIC IMPORTED GLOBAL)
SET_PROPERTY(TARGET jsoncpp PROPERTY IMPORTED_LOCATION ${JSONCPP_LIBRARIES})
ADD_DEPENDENCIES(jsoncpp extern_jsoncpp)

ExternalProject_Add函数会创建target,除了编译还会执行download和update step。
思考:ExternalProject创建的target是否会集成其实际编译工程内target的属性?

5、cmake policies

CMake 在添加新特性后可能不会完全兼容旧的 CMake 版本,这导致了在新版本的 CMake 中使用旧的 CMakeLists 文件时可能会存在一些问题。策略的引入就是帮助用户和开发者解决这些问题,它是 CMake 中用来改善向后兼容性和追踪兼容性的一种机制。 CMake 中的所有策略都被赋予一个 CMPNNNN 格式的识别符,其中 NNNN 是一个整数值。策略通常既保留了用于保持旧版本兼容性的旧行为,又包含了让用户在新项目中优先使用的正确的新行为。每个策略相关的文档都会描述旧行为和新行为,以及引入该策略的原因。

  • 设置策略
    工程可以设置各种策略来选择新的或旧的行为。当 CMake 遇到会被特殊策略影响的用户代码时,它会检查工程是否设置了策略。如果没有设置策略,工程会使用旧行为,并会给出警告要求项目作者设置工程的策略。
      有许多方法设置一个策略的行为,最快速的方式是设置所有的策略版本与编写项目的 CMake 版本一致,设置策略的版本会获取所有指定的版本或更早的版本中引入的策略。所有指定的版本之后引入的策略会标记为未设置,这是为了输出这些新策略合适的警告信息。设置策略版本的命令为:
cmake_policy (VERSION major.minor[.patch[.tweak]])
cmake_policy (SET CMPNNNN OLD) 
cmake_policy (SET CMPNNNN NEW)

使用 NEW 选项的 cmake_policy 命令明确告诉 CMake 使用策略的新行为。
A policy should almost never be set to OLD, except to silence warnings in an otherwise frozen or stable codebase, or temporarily as part of a larger migration path.

if (POLICY CMP0042)
  cmake_policy (SET CMP0042 NEW)
endif (POLICY CMP0042)

if (POLICY CMP0063)
  cmake_policy (SET CMP0063 NEW)
endif (POLICY CMP0063)

GLOG工程中的cmake
CMP0042
MACOSX_RPATH is enabled by default.
CMake 2.8.12 and newer has support for using @rpath in a target’s install name. This was enabled by setting the target property MACOSX_RPATH. The @rpath in an install name is a more flexible and powerful mechanism than @executable_path or @loader_path for locating shared libraries.
CMake 3.0 and later prefer this property to be ON by default. Projects wanting @rpath in a target’s install name may remove any setting of the INSTALL_NAME_DIR and CMAKE_INSTALL_NAME_DIR variables.
This policy was introduced in CMake version 3.0. CMake version 3.0.2 warns when the policy is not set and uses OLD behavior. Use the cmake_policy command to set it to OLD or NEW explicitly.
思考:cmake policy是否支持用户自定义?

6、使用target_sources而非GLOB
add_library(evolution"")
target_sources(evolution
  PRIVATE
      ${CMAKE_CURRENT_LIST_DIR}/evolution.cpp
  PUBLIC
      ${CMAKE_CURRENT_LIST_DIR}/evolution.hpp
  )

VS

file(GLOB srcs "*.cpp")
add_library(${srcs })

OOP思想的modern cmake VS 面向过程
使用GLOB正则匹配后,增删文件cmake系统无感知,cmake官方强烈建议不使用GLOB的方式引入源文件。

提问

1、如果未指定,默认行为是?

target_link_libraries(hello-world PUBLIC hello)
target_include_directories(hello-world PUBLIC hello)

2、如何隔离某个target的编译参数?
3、一个target是否可以既是static,又是shared?
4、cmake是否同时支持多种generator输出

你可能感兴趣的:(Modern Cmake)