历史背景
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输出