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(<name> [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(<name> [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(<target> [SYSTEM] [BEFORE]
  <INTERFACE|PUBLIC|PRIVATE> [items1...]
  [<INTERFACE|PUBLIC|PRIVATE> [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(<target>
                      <PRIVATE|PUBLIC|INTERFACE> <item>...
                     [<PRIVATE|PUBLIC|INTERFACE> <item>...]...)
  • 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定义为$<...>的形式。该表达式的值有多种形式,而且支持嵌套使用:

条件表达式
$<condition:true_string> 当条件为1时,表达式为true_string,否则为空
$<IF:condition,true_string,false_string> 当条件为1时,表达式为true_string,否则为false_string
变量表达式
$<TARGET_EXISTS:target> 当target存在为1,否则为0
$<CONFIG:cfg> 当config为cfg时为1,否则为0。这是非常高频使用的一个表达式,可以通过它来区分Debug/Release等不同的config。如下例所示,通过嵌套使用上述两个表达式,可以达到区分CONFIG来设置依赖库路径的目的。
target_link_directories(${PROJECT_NAME} PUBLIC                                                                                                                                                                      
  $<$<CONFIG:Debug>:${CONAN_LIB_DIRS_DEBUG}>                                                                                                                                                                        
  $<$<CONFIG:Release>:${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、如果未指定<PRIVATE|PUBLIC|INTERFACE>,默认行为是?

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

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,542评论 6 504
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,822评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,912评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,449评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,500评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,370评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,193评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,074评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,505评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,722评论 3 335
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,841评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,569评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,168评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,783评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,918评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,962评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,781评论 2 354

推荐阅读更多精彩内容

  • CMake 是一个开源的跨平台自动化建构系统,是目前最主流的 C/C++语言构建工具。CMake3.0 之后引入很...
    尉刚强阅读 10,807评论 3 11
  • CMake is a great tool for managing a C++ system’s build. ...
    XBruce阅读 591评论 0 1
  • 前言 相信每个人都写过CMakeLists,然而,“一千个读者心中有一千个哈姆雷特”,一千个程序员也能写出一千种C...
    奇林的徒步学园阅读 2,608评论 0 1
  • 前言 相信每个人都写过CMakeLists,然而,“一千个读者心中有一千个哈姆雷特”,一千个程序员也能写出一千种C...
    金戈大王阅读 5,837评论 0 2
  • 最近在负责一个大型工程的CMake编译系统管理,整理一些工作过程中积累下来的知识片段和技巧。CMake是一个跨平台...
    啊呀哟嘿阅读 8,833评论 0 2