翻译自https://crascit.com/2016/01/31/enhanced-source-file-handling-with-target_sources/

使用 target_sources() 提高源文件处理能力

在CMake项目中,通常存在从大量源文件 (source files) 构建的目标 (targets) 。这些文件可以分布在不同的子目录中,这些子目录本身可以嵌套在多个层次上。在此类项目中,传统方法通常要么在最顶层目录列出所有源文件,要么将源文件list储存于一个变量,并将其传递给 add_library(), add_executable() 等。在CMake 3.13中,引入了一个新的命令target_sources(),该命令提供了各种 target_xxx 命令中缺失的部分功能。虽然CMake文档简洁地描述了 target_sources() 的功能,但没有强调新命令的有用性以及它为何能更好地支持 CMake 项目:

  • 它可以产生更清晰、更简洁的 CMakeLists.txt 项目文件
  • 依赖信息 (dependency information) 可以在依赖实际发生的目录层次中得以指定
  • 源文件可以成为目标接口的一部分
  • 源文件可以添加到第三方项目的目标中,而无需修改第三方项目文件

在 target_sources() 出现前

通常,开发人员首先以非常简单的方式学习CMake,通过在 add_executable() 或 add_library() 命令本身中直接列出源文件来定义目标。如:

add_executable(myApp src1.cpp src2.cpp)

当源文件的数量越来越大,并且它们分布在多个子目录中(可能嵌套到多个级别)时,这就会变得很难处理。这样还导致必须(在 CMakeLists.txt 文件中)重复目录结构,这首先降低了将源文件结构化为目录的好处。

因此,许多开发人员做出的改进是,在子目录中用变量中建立源文件列表,并通过 include() 引入该变量。在 include 所有子目录后,调用 add_executable() 或 add_library(),但这次只传递变量,而不是显式的文件列表。这时顶层 CMakeLists.txt 文件有点像这样:

# The name of the included file could be anything,
# it doesn't have to be called CMakeLists.txt
include(foo/CMakeLists.txt)
include(bar/CMakeLists.txt)add_executable(myApp ${myApp_SOURCES})

其子目录文件的结构类似于:

list(APPEND myApp_SOURCES${CMAKE_CURRENT_LIST_DIR}/foo.cpp${CMAKE_CURRENT_LIST_DIR}/foo_p.cpp
)

这允许每个子目录仅定义其提供的源 (sources),并将嵌套的子目录委托给另一个 include()。这使得顶层 CMakeLists.txt 文件相当小,并且每个子目录中的 CMakeLists.txt 文件也趋于简单,只关注该目录中的内容。

除了显式地使用变量构建源文件列表这一方法,一些开发人员选择让CMake查找源文件并使用 file(GLOB_RECURSE …) 命令自动生成该变量的内容。虽然一开始这可能因其简洁性而非常吸引人,但这种技术有许多缺点,CMake文档不鼓励这种方法。尽管如此,这一方法经常被新接触CMake的开发人员使用,直到他们亲身体验到该方法带来的问题。

target_sources() :没有缺点,全是优点

上述方法的缺点可能不会立即显现。一个缺点是,源文件构建在变量中,然后该变量被传递到顶层 CMakeLists.txt 文件的add_executable() 或 add_library() 语句中。这种使用变量存储源文件列表的方法不是特别可靠。例如,如果在整个目录层次结构中构建了许多目标,变量的数量和命名可能会失控。这可以通过坚持某种命名约定来解决,但这取决于所有开发人员都知道并遵守该约定。此外,如果开发人员不小心在更深层目录中重复使用变量名,则源可能最终会添加到意外的目标中。CMake 通常对此不会发出任何类型的诊断消息,因为它不会知道您不打算这样做。

但是,使用变量的更大缺点可能是,它阻止了在深入到子目录时的 CMake 目标定义。这反过来意味着在子目录中也不能直接调用 target_compile_definitions(), target_compile_options(), target_include_directories() 或 target_link_libraries()。为了将编译器标识 (flags)、选项 (options)、头搜索路径 (header search paths) 和其他要链接的库 (libraries) 关联起来,必须定义更多的变量来将这些信息传递回顶层。在正确处理引用 (quoting) 时也必须格外小心。如果您想充分利用 PUBLIC, PRIVATE, INTERFACE 这些目标命令,仅仅一个目标所需的变量数量已经开始变得有点愚蠢。如果在项目的目录结构中定义了许多目标,那么可以想象变量数量将爆炸。

注意:以下建议和示例已从原始文章中更新,以说明 CMake 3.13.0 中添加的新功能

一个例子应该有助于强调为什么 target_sources() 会带来更鲁棒和简洁的 CMakeLists.txt 文件。假设我们有一个项目,有两个子目录 foo 和 bar。顶层 CMakeLists.txt 文件可以像这样简单:

cmake_minimum_required(VERSION 3.13)
project(MyProj)add_library(myLib "")add_subdirectory(foo)
add_subdirectory(bar)

add_library() 调用中的空引号是必需的,因为该命令需要源文件列表,即使该列表为空。如果有需要从当前顶层目录中添加的源,则可以在那里列出它们。

现在让我们假设 foo 子目录中的源文件使用一些名为 barry 的第三方库的功能。这需要 myLib 连接到 (link against) barry 库。为了便于讨论,我们还假设在构建 (build) myLib 时以及对于包含来自 myLib 的头 (headers) 的任何代码,我们都需要定义一个名为 USE_BARRY 的编译器符号 (symbol)。假设最低 CMake 版本为3.13.0,则 foo 子目录中的 CMakeLists.txt 文件如下所示:

target_sources(myLibPRIVATEfoo.cppfoo_p.cppfoo_p.hPUBLICfoo.h  # poor PUBLIC example, see discussion below for why
)find_library(BARRY_LIB barry)
# This call requires CMake 3.13 or later, see next section
target_link_libraries(myLib PUBLIC ${BARRY_LIB})target_compile_definitions(myLib PUBLIC USE_BARRY)
target_include_directories(myLib PUBLIC ${CMAKE_CURRENT_LIST_DIR})

在上述示例中,请注意 .h 头文件 (header files) 也被指定为源,而不仅仅是 .cpp 实现文件 (implementation files) 。作为源列出的头本身不会直接被编译,但添加它们对 IDE 来说有益,如 Visual Studio, Xcode, Qt Creator 等。这会导致这些头文件在 IDE 内的项目文件列表中列出,即使没有源文件通过 #include 引用它。这样可以使这些头在开发过程中更容易被找到,并可能有助于重构等。

PRIVATE 和 PUBLIC 关键字指出这些对应的源应在何处被使用。PRIVATE 表示这些源只应添加到 myLib,而 PUBLIC 表示这些源应添加到 myLib 和任何链接到myLib的对象中。INTERFACE 用于不应添加到 myLib 但应添加到任何链接到 myLib 的对象的源。实际上,源几乎总是 PRIVATE 的,因为它们通常不应该被添加到任何链接到该目标的相关内容中。只有头的接口库 (Header-only interface libraries) 是一个例外,因为源只能通过INTERFACE关键字添加到接口库中。不要将 PRIVATE, PUBLIC 和 INTERFACE 关键字与头是否是库的公共API的一部分混淆,这些关键字专门用于控制源添加到哪些目标。还有一些不太常见的情况,一些文件(例如资源、图像、数据文件)可能需要直接编译成链接到库的目标,以便在运行时找到它们。将这样的源用 PUBLIC 或 INTERFACE 关键字列出可以帮助解决此类情况。请注意,安装非私有源可能会有一些问题(我们将在下面进一步讨论这个话题)。

PRIVATE, PUBLIC 和 INTERFACE 的含义适用于其他 target_xxx() 命令,尽管更常见的情况是将对应内容视为非私有 (non-private)。上面的例子显示了指定 myLib 和任何链接到它的目标也需要链接到 barry 库是多么容易。类似地,仅通过一条 target_compile_definitions() 调用,myLib 和任何链接到 myLib 的对象都将有符号 USE_BARRY 的定义。最后,target_include_directories() 命令将 foo 子目录添加到 myLib 和任何链接到它的对象的头搜索路径中。因此,其他目录中需要#include “foo.h” 的任何其他源文件也将能够找到它。

为了说明这些 target_xxx() 命令的强大功能,让我们考虑一下 bar 子目录下的 CMakeLists.txt 文件可能的样子。在这种情况下,我们假设 bar 需要添加一些源文件,并且 bar 的一些源文件或头文件将包含 foo.h。

target_sources(myLibPRIVATEbar.cppbar.hgumby.cppgumby.h
)

(只简单列出源文件,省略其他内容)
所有的工作都已经在 foo 目录中完成了,所以我们在这里没有什么可做的了。这突出了使用 target_sources() 的最大优点之一,即依赖项 (dependencies) 可以在最相关的地方列出,而所有其他目录都不需要关心。这种依赖细节的本地化使得整个项目中的 CMakeLists.txt 文件更鲁棒、更简洁。如果没有 target_sources(),我们将无法以这种方式使用 target_compile_definitions(), target_compile_options(), target_include_directories() 或 target_link_libraries(),因为当我们进入每个子目录时,目标 myLib 并没有被定义。

支持 CMake 3.12 及更早版本

以上关于使用 CMake 3.13.0 或更高版本的内容与该版本中删除了限制有关。在3.13.0之前,target_link_libraries() 只能被在同一目录范围内创建的目标调用。这意味着在 foo 子目录的示例中,target_link_libraries(myLib …) 调用将导致 CMake 3.12 或更早版本出错,因为 myLib 目标是在父作用域中创建的。没有其他 target_…() 命令有这种限制,只有 target_link_libraries() 有。我们很快就会谈到这一点。

CMake 3.13.0 中的另一个变化是 target_sources() 如何将相对路径对应到源文件。在 CMake 3.12 或更早版本中,相对路径被视为相对于该被添加源的目标路径。这是不直观的,因此 CMake 3.13.0 改为将相对路径视为相对于当前源目录。如果项目将 3.13.0 设置为其最低 CMake 版本要求,则默认会自动获取新行为 (behavior)。

对于需要支持 CMake 3.12 或更早版本的项目,他们可以使用源文件的绝对路径以避免行为的变化,并避免任何策略警告 (policy warnings)。例如:

target_sources(myLibPRIVATE${CMAKE_CURRENT_LIST_DIR}/foo.cpp${CMAKE_CURRENT_LIST_DIR}/foo_p.cpp${CMAKE_CURRENT_LIST_DIR}/foo_p.hPUBLIC${CMAKE_CURRENT_LIST_DIR}/foo.h
)

这既不太方便,可读性也较差,因此定义辅助函数可能会有所帮助,但这也适用于早期的 CMake 版本。CMake 对保持向后兼容性有很强的要求,因此处理相对路径的行为变化由策略 CMP0076 来保护。我们可以在辅助函数中利用这一点:

# NOTE: This helper function assumes no generator expressions are used
#       for the source files
function(target_sources_local target)if(POLICY CMP0076)# New behavior is available, so just forward to it by ensuring# that we have the policy set to request the new behavior, but# don't change the policy setting for the calling scopecmake_policy(PUSH)cmake_policy(SET CMP0076 NEW)target_sources(${target} ${ARGN})cmake_policy(POP)return()endif()# Must be using CMake 3.12 or earlier, so simulate the new behaviorunset(_srcList)get_target_property(_targetSourceDir ${target} SOURCE_DIR)foreach(src ${ARGN})if(NOT src STREQUAL "PRIVATE" ANDNOT src STREQUAL "PUBLIC" ANDNOT src STREQUAL "INTERFACE" ANDNOT IS_ABSOLUTE "${src}")# Relative path to source, prepend relative to where target was definedfile(RELATIVE_PATH src "${_targetSourceDir}" "${CMAKE_CURRENT_LIST_DIR}/${src}")endif()list(APPEND _srcList ${src})endforeach()target_sources(${target} ${_srcList})
endfunction()

现在,我们可以像调用内置命令一样调用上述辅助函数,即使使用 CMake 3.12 或更早版本,也可以获得 CMake 3.13 的行为:

target_sources_local(myLibPRIVATEfoo.cppfoo_p.cppfoo_p.hPUBLICfoo.h
)

使用 CMake 3.12 或更早版本时,使用 target_link_libraries() 绕过限制会更困难。可以选择将 target_link_libraries() 调用语句移动到定义目标的目录,或者使用 include() 而不是 add_subdirectory() 避免创建新的目录范围。后者只需要更改顶层 CMakeLists.txt 文件为类似以下内容(这是本文在为 CMake 3.13.0 更新之前建议的原始方法):

cmake_minimum_required(VERSION 3.1)
project(MyProj)add_library(myLib "")# Using include() avoids creating a new directory scope, so these directories
# are able to call target_link_libraries(myLib ...)
include(foo/CMakeLists.txt)
include(bar/CMakeLists.txt)

辅助函数 target_sources_local() 的定义方式使得它可以在 foo 或任何其他目录中工作,无论我们使用的是 add_subdirectory() 还是 include() 。

大多数开发人员发现 add_subdirectory() 更自然,它确实倾向于更直观地处理变量,如 CMAKE_CURRENT_SOURCE_DIR, CMAKE_CURRENT_BINARY_DIR 等。因此,如果子目录不需要调用 target_link_libraries(),则首选使用 add_subdirectory() 方法,而不是上述 include() 解决方法。

安装的复杂性

指定 PRIVATE 源相对容易,几乎没有困难。每个源文件的位置都很清楚,只需要在构建中考虑。任何 PUBLIC 或 INTERFACE 源都会产生额外的必须加以考虑的因素。在构建中,非私有源的路径和私有源的处理方式一样,但是当项目安装后,无论是在项目自身的构建中,还是在使用已安装项目的任何内容的构建中,非私有路径都必须有意义。用于项目自身构建的路径将根据执行构建的硬件平台和构建过程所在的目录来确定。一旦安装,这些目录可能无法访问,因此需要将路径替换为对已安装的文件集有意义的其他路径。这可以通过使用 BUILD_INTERFACE 和 INSTALL_INTERFACE 生成器表达式实现。以下示例显示了如何在两个不同的上下文中为同一文件提供不同的路径:

target_sources(myHeaderOnlyPUBLIC$<BUILD_INTERFACE:${CMAKE_CURRENT_LIST_DIR}/algo.h>$<INSTALL_INTERFACE:include/algo.h>
)
install(FILES algo.h DESTINATION include)

虽然上述方法解决了构建/安装的差异,但它也有一个缺点,即要求我们再次将每个 BUILD_INTERFACE 路径拼写为绝对路径。我们不能再使用前面定义的 target_sources_local() 辅助函数。这是支持安装非私有源文件的成本。项目应权衡便利性的损失和复杂性的增加与通过使源非私有化而获得的预期收益。

关键点

退一步说,target_sources() 为我们做的是通过允许我们直接使用 CMake 目标来消除对变量的需要。这为我们提供了以下关键优势:

  • 它允许尽早定义 CMake 目标,使得在目标被定义后,可以在任何使用 add_subdirectory() 命令引入的子目录中调用各种其他 target_xxx 命令
  • 子目录不会无意中将源添加到错误的目标
  • 依赖信息可以在引入这些依赖的地方得到充分、可靠的定义。PRIVATE, PUBLIC 和 INTERFACE 关键字可以精确控制这些依赖项的特性,它们还可以更好地促进与能够利用这些信息的IDE环境的集成。

还应记住以下几点:

  • 非私有源需要更详细、更不方便的语法,因此请考虑将其变为非私有是否值得
  • 如果需要支持 CMake 3.12 或更早版本,则需要将任何 target_link_libraries() 调用移动到与其操作的目标相同的目录,或者使用 include() 而不是 add_subdirectory() 来避免引入新的目录范围。尽可能选择前者,因为它对开发人员来说可能更直观。

与基于变量的方法相比,target_sources() 命令还有一个独特的优势。它允许将额外的源添加到目标,而不管它们(指源还是目标?)在哪里定义(导入的目标除外)。
当使用 add_subdirectory() 或 include() 将外部项目中的代码合并到一个构建中时,这尤其有用(请参阅前面的一篇文章,介绍如何使用这些命令将GoogleTest直接合并到您的主构建中)。这可以用于从外部项目添加头、图像等,而不会影响目标的构建方式。一个大胆的做法是,您甚至可以使用此技术为弱符号添加自己的实现,以便您的实现覆盖外部项目目标通常使用的实现。这可能在测试期间有用,或提供特定函数的更高效实现等。

CMake - 使用 target_sources() 提高源文件处理能力相关推荐

  1. 【CMake】Android Studio 中使用 CMake 编译单个 C++ 源文件 ( 常用的 CMake 命令解析 )

    文章目录 一.Android Studio 中使用 CMake 编译单个 C++ 源文件 二.cmake_minimum_required 命令设置最小 CMake 版本 三.project 命令设置 ...

  2. 计算机网络基本概述,数据通信、资源共享、增加数据可靠性、提高系统处理能力

    一.计算机网络 硬件方面: 软件方面: 实现资源共享.信息传递 二.计算机网络的功能 数据通信.资源共享.增加数据可靠性.提高系统处理能力 三.计算机网络的发展 60年代:分组交换 70-80年代:T ...

  3. 用CMake编译运行在网上下载的源文件src

    参考:http://blog.csdn.net/yiqiudream/article/details/51885698 (一).怎么用CMake打开下载的源文件? 工具:下载CMake --> ...

  4. cmake教程5-macro宏定义以及传递参数给源文件

    引入在C++程序中我们经常见到如下,两个问题: 1. 输出当前程序的版本号 2. 通过cmake添加macro宏定义 出入到源文件,例如在编译opencv/caffe的时候,我们通过cmake -DU ...

  5. CMake Cookbook精要

    CMake允许您描述如何在所有主要硬件和操作系统上配置.构建和安装项目,无论是构建可执行文件.库还是两者. CTest允许您定义测试.测试套件,并设置它们的执行方式. CPack为您的所有打包需求提供 ...

  6. CMake 入门与进阶

    目录 cmake 简介 cmake 和Makefile cmake 的下载 cmake 的使用方法 示例一:单个源文件 示例二:多个源文件 示例三:生成库文件 示例四:将源文件组织到不同的目录 示例五 ...

  7. 【使用CMake组织C++工程】2:CMake 常用命令和变量

    前言 前面的文章介绍了一个最简单的CMake工程,这篇文章将介绍一个稍微复杂一些的CMake工程,结合这个工程总结一下在组织一个C/C++工程时最为常用的一些CMake命令和变量.对于涉及到的命令和变 ...

  8. Windows下VTK6.0.0安装详解(CMake使用说明)

    操作系统:Windows7,用到工具:Visual studio.CMake. 1.准备工作 VTK下载: 下载最新VTK稳定版(6.0.0,截至2013年7月)http://www.vtk.org/ ...

  9. CMake 常用命令和变量

    前言 前面的文章介绍了一个最简单的CMake工程,这篇文章将介绍一个稍微复杂一些的CMake工程,结合这个工程总结一下在组织一个C/C++工程时最为常用的一些CMake命令和变量.对于涉及到的命令和变 ...

最新文章

  1. R语言ggplot2可视化在分面图(facet_grid)的条形图上添加计数(count)或者百分比(percent)标签实战
  2. python【力扣LeetCode算法题库】面试题13- 机器人的运动范围(BFS)
  3. 神经网络与机器学习 笔记—反向传播算法(BP)
  4. json数据转换成表格_电子表格会让您失望吗? 将行数据转换为JSON树很容易。
  5. session和token的区别
  6. 【JSOI2008】【bzoj1012】最大数maxnumber
  7. 系统学习深度学习(十一)--dropout,dropconect
  8. mybatis传参——parameterType
  9. perl语言入门级练习记录23章
  10. cad灯具图标_灯具在CAD中怎么表示出来 都代表哪种灯 谢谢
  11. 批量tracert脚本
  12. shell综合练习(二)
  13. ubuntun16 上rtl 8723be 安装
  14. 18岁、20岁、23岁、25岁、28岁、30岁
  15. 二元置信椭圆r语言_R语言 第4章 初级绘图(6)
  16. 谈悲观、执著、超脱——周国平
  17. Linux服务器互信
  18. Linux系统根目录详解
  19. Vue 中 css scoped 样式穿透 ( stylus[] / sass / less[/deep/] )
  20. 鸿蒙系统 智能手表,wear os智能手表和鸿蒙系统智能手表对比

热门文章

  1. UIKit 框架讲解
  2. centos 7的firewalld防火墙配置IP伪装和端口转发(内附配置案例)
  3. VNCTF2023 WP
  4. c语言坐标海伦公式,C语言:用海伦公式求三角形面积 , C语言编程问题,利用海伦公式求三角形面积...
  5. 基于PaddleDetection实现人流量统计人体检测
  6. linux用户密码管理,Linux_详解Linux中的用户密码管理命令passwd和change,passwd 修改用户密码参数 nbsp - phpStudy...
  7. 从公司管理到IT审计(ZT)
  8. 社区人员登记管理系统
  9. Persecond for Mac(延时摄影视频制作工具)
  10. 别人不知道的小众手机APP,今天偷偷告诉你!