这篇文章探讨了所谓的现代CMake的概念,它主张放弃传统的基于变量的方法,用于基于所谓目标的更结构化模型。我的目的是展示如何使用“新”(> = 3.0.0)功能将您的CMake系统重塑为更加可维护和直观的替代方案,实际上是有意义的。
这里介绍的许多概念都源于Daniel Pfeifer的杰作Effective CMake。
find_package(Boost 1.55 COMPONENTS asio)
list(APPEND INCLUDE_DIRS ${BOOST_INCLUDE_DIRS})
list(APPEND LIBRARIES ${BOOST_LIBRARIES})
include_directories(${INCLUDE_DIRS})
link_libraries(${LIBRARIES})
上面这个例子不要学。 这在很多方面都是错误的。 你只是盲目地将东西扔进一堆包含目录和编译器标志。 没有结构。 没有透明度。 更不用说像include_directories这样的函数在目录级别工作并应用于范围中定义的所有实体。
上面说到的甚至不是真正的问题。
你如何处理传递依赖? 链接的顺序怎么样? 你需要自己照顾好自己。 您需要自己处理依赖项依赖关系,那很费脑子。
CMake开发人员看到了上述问题并引入了新式的语言功能,使您可以更好地构建项目。 现代CMake的重点是关注目标和属性的。 从概念上讲,这并不复杂。
目标为您的应用程序的组件建模。 可执行文件是目标,库是目标。 您的应用程序就是使用这些依赖于彼此并相互使用的目标集合老构建的。
目标有属性。 目标的属性是它构建的源文件,它需要的编译器选项,它链接的库。 在现代CMake中,您可以创建一系列需要的目标并在其上定义必要的属性。
目标属性在两个范围之一中定义:INTERFACE和PRIVATE。
私有属性在内部用于构建目标,而接口属性由目标用户在外部使用。换句话说,接口属性描述了使用目标的要求,而私有属性描述了构建目标的要求。
接口属性具有前缀,INTERFACE_前置于它们。
例如,属性COMPILE_OPTIONS包含了对构建目标时要传递给编译器的选项列表。例如,如果必须在启用所有警告的情况下构建目标,则此列表应包含选项-Wall。这是一个私有财产,仅在构建目标时使用,不会以任何方式影响其用户。
另一方面,属性INTERFACE_COMPILE_FEATURES存储编译器在构建目标用户时必须支持哪些功能。例如,如果库的公共头文件包含可变参数函数模板,则此属性应包含特征cxx_variadic_templates。这指示CMake必须由理解可变参数模板的编译器构建包括此标头的应用程序。
属性也可以指定为PUBLIC。公共属性在PRIVATE和INTERFACE中都定义。
通过一个例子可以更好地理解所有这些。
想象一下,您正在编写一个json工具库libjsonutils,它从提供的位置解析json文件。 Json文件可以位于本地文件系统上,也可以通过某些URL访问。
该库具有以下结构:
libjsonutils
├── CMakeLists.txt
├── include
│ └── jsonutils
│ └── json_utils.h
├── src
│ ├── file_utils.h
│ └── json_utils.cpp
└── test
├── CMakeLists.txt
└── src
└── test_main.cpp
我们有一个公开的头文件,我们定义了loadJson()
函数:
boost :: optional
此函数接收到json的URL或文件路径,并将其作为rapidjson对象加载。如果出现问题,将返回boost :: none
。
让我们开始编写jsonutil的CMakeLists.txt:
cmake_minimum_required(VERSION 3.5)
project(libjsonutils VERSION 1.0.0 LANGUAGES CXX)
第一步是创建我们的库目标:
add_library(JSONUtils src/json_utils.cpp)
现在让我们在目标上定义一些属性。从include目录开始
target_include_directories(JSONUtils
PUBLIC
$
$
PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/src
)
我们的头文件位于两个不同的位置:src /里面,它包含一个名为file_utils.h的实用程序头,而在include /中,我们的公开头文件json_utils.h在里面。为了构建我们的库,我们需要两个位置的所有头文件(json_utils.cpp包含两者),因此INCLUDE_DIRS必须包含src /,以及include /。
另一方面,jsonutils的用户只需要知道公共头json_utils.h的位置,因此INTERFACE_INCLUDE_DIRS只需要包含include /,而不是src /。
但是仍然存在问题。在构建jsonutils时,include /位于/ home/ xxx/libjsonutils/include/,但在安装我们的库之后,它将在$ {CMAKE_INSTALL_PREFIX} / include /下。因此,此目录的位置需要根据我们是否正在构建或安装库而有所不同。为了解决这个问题,我们使用生成器表达式,它根据情况设置正确的路径。
我们现在可以继续在目标上定义额外的属性。 例如,将警告视为错误可能是有益的:
target_compile_options(JSONUtils PRIVATE -Werror)
鉴于我们使用constexpr和auto,我们可以将语言标准设置为c ++ 11:
target_compile_features(JSONUtils PRIVATE cxx_std_11)
请注意,没有理由手动将-std = c ++ 11
附加到CMAKE_CXX_FLAGS
,让CMake为您执行此操作!
让我们考虑一下我们的依赖关系。首先,我们需要boost
,因为我们使用boost::optional
。另外,为了弄清楚传递的字符串是否是一个URL,我们必须对一些正则表达式进行评估,所以我们需要boost :: regex
(是的,我知道c ++ 11引入了正则表达式实用程序)。其次,我们需要rapidjson
。
在CMake中,target_link_libraries
用于建模目标之间的依赖关系。
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
)
目标的依赖关系(也就是链接库)只是另一个属性,在INTERFACE或PRIVATE范围内定义。在我们的例子中,rapidjson
和 boost optional
在目标Boost :: boost
中定义)必须是接口依赖并传播给用户,因为它们用于在客户端导入的公共头中。
这意味着JSONUtils的用户不仅需要JSONUtil的接口属性,还需要其接口依赖项的接口属性(在这种情况下定义boost和rapidjson的公共头),以及依赖项的依赖项等。
但是CMake如何解决这个问题呢?很简单,它将Boost :: boost和RapidJSON :: RapidJSON的所有接口属性添加到相应的JSONUtil自己的接口属性中。这意味着JSONUtils的用户将传递性地接收依赖链中所有目标的接口属性。
另一方面,Boost :: regex仅在内部使用,可以是私有依赖。这里,Boost :: regexes接口属性将附加到相应的JSONUtil的私有属性,并且不会传播给用户。
这就是现代CMake。
请注意,Boost :: boost
和RapidJSON :: RapidJSON
本身就是目标。但他们来自哪里?没错,目标可以输出。导出的目标可以稍后导入到其他项目中。就像我们使用这两个一样.
当我们调用find_package(Boost 1.55 REQUIRED COMPONENTS regex)
时,CMake将执行FindBoost.cmake
,其中将导入目标Boost :: boost
和Boost :: regex
,允许我们通过target_link_libraries()
依赖它们。
我们的项目具有结构,因为它们是作为封装目标的集合构建的,CMake处理我们的传递要求。
让我们尝试构建jsonutils:
CMake Error at CMakeLists.txt:9
Target "JSONUtils" links to target "RapidJSON::RapidJSON" but the target was not found.
找不到导入的目标RapidJSON :: RapidJSON,因为RapidJSONConfig.cmake没有创建它。让我们来看看rapidjson在我的arch linux系统上安装的配置中做了什么:
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的情况下,单个变量设置为指向其包含目录。这正是我们不想要的,我们不想要变量,我们想要目标!
就我而言,70%的依赖项没有在查找模块或配置中定义任何目标。现实情况是,CMake的使用是一种无政府状态。规则很少,灵活性太强。我们需要标准做法,我们需要指导方针。我们有C ++的设计模式,为什么不用于CMake?
那么在这些情况下你能做些什么呢?
Daniel Pfeifer建议库开发人员报告这种错误。我同意。上游应该支持下游的现代基于目标的设计。问问自己:你真的需要这种依赖吗?有没有替代品支持现代cmake使用?
但是,可能没有其他选择可以让你选,职能自己动手并自己编写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
#
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()
这是一个非常简单的查找模块,它在系统中查找rapidjson的头文件,并创建我们需要的导入目标RapidJSON :: RapidJSON。我使用INTERFACE来表明这个“库”实际上不是一个库,因为没有相应的.a或.so,但只是定义了使用要求。
我们希望jsonutils集成在下游的基于目标的构建系统中。这意味着他们要使用jsonutils所需要做的就是:
find_package(JSONUtils 1.0 REQUIRED)
target_link_libraries(example JSONUtils :: JSONUtils)
为实现这一目标,我们需要做两件事。首先,我们需要导出目标JSONUtils :: JSONUtils。第二,当下游调用find_package(JSONUtils)
时,我们需要导入该目标,即从我们的JSONUtilsConfig.cmake中调用。
让我们首先将目标导出到导入它的JSONUtilsTargets.cmake脚本。首先,我们需要安装库本身(实际的.a或.so文件):
include(GNUInstallDirs)
install(TARGETS JSONUtils
EXPORT jsonutils-export
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
)
在CMake中,使用EXPORT参数将已安装的目标注册导出。因此,导出(Exports)只是一组可以导出和安装的目标。在这里,我们告诉CMake安装我们的库并在导出jsonutils-export中注册目标。
然后我们可以继续安装上面定义的导出:
install(EXPORT jsonutils-targets
FILE
JSONUtilsTargets.cmake
NAMESPACE
JSONUtils::
DESTINATION
${CMAKE_INSTALL_LIBDIR}/cmake/JSONUtils
)
这将安装导入脚本JSONUtilsTargets.cmake,当包含在其他脚本中时,将加载导出jsonutils-export中定义的目标。通过使用NAMESPACE参数,我们告诉CMake将前缀JSONUtils ::添加到导入的所有目标。
请记住,当客户端调用find_package(JSONUtils)时,CMake将查找并执行JSONUtilsConfig.cmake。
因此我们的目标JSONUtils :: JSONUtils已导入并可供客户端使用,我们需要在配置文件中加载JSONUtilsTargets.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()
请注意,JSONUtilsTargets.cmake包含以下代码:
add_library(JSONUtils::JSONUtils STATIC IMPORTED)
set_target_properties(JSONUtils::JSONUtils PROPERTIES
INTERFACE_INCLUDE_DIRECTORIES "${_IMPORT_PREFIX}/include"
INTERFACE_LINK_LIBRARIES "Boost::boost;RapidJSON::RapidJSON;\$"
)
由于此脚本引用了boost和rapidjson中的目标,因此需要在JSONUtilsConfig.cmake中包含JSONUtilsTargets.cmake之前导入它们。
这就是我们需要在JSONUtilsConfig.cmake中调用find_dependency()的原因:确保下游安装了所有必需的依赖项,并在我们的JSONUtilsTargets.cmake中引用所需的目标之前导入它们。