翻译自https://crascit.com/2016/01/31/enhanced-source-file-handling-with-target_sources/
使用 target_sources() 提高源文件处理能力
在CMake项目中,通常存在从大量源文件 (source files) 构建 (build) 的目标 (targets)。这些文件可以分布在不同的子目录中,这些子目录本身可以嵌套在多个层次上。在此类项目中,传统方法通常要么在最顶层目录列出所有源文件,要么将源文件列表储存于一个变量,并将其传递给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 文件有点像这样:
# 被 include() 包含的文件的文件名可以任取,不必为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 这些目标命令,仅仅一个目标所需的变量数量已经开始变得有点愚蠢。如果在项目的目录结构中定义了许多目标,那么可以想象变量数量将爆炸。
一个例子应该有助于强调为什么 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 库。为了便于讨论,我们还假设在构建 myLib 时以及对于包含来自 myLib 的头 (headers) 的任何代码,我们都需要定义一个名为 USE_BARRY 的编译器符号 (symbol)。假设最低 CMake 版本为3.13.0,则 foo 子目录中的 CMakeLists.txt 文件如下所示:
target_sources(myLib
PRIVATE
foo.cpp
foo_p.cpp
foo_p.h
PUBLIC
foo.h # 一个不太恰当的 PUBLIC 示例,随后将讨论这么说的原因
)
find_library(BARRY_LIB barry)
# 这个命令需要 CMake 版本最低为3.13
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 关键字列出可以帮助解决此类情况。请注意,安装 (install) 非私有源可能会有一些问题(我们将在下面进一步讨论这个话题)。
PRIVATE, PUBLIC 和 INTERFACE 的含义适用于其他 target_xxx()
命令,尽管更常见的情况是将对应内容视为非私有 (non-private)。上面的例子显示了指定 myLib 和任何链接到它的目标也需要链接到 barry 库是多么容易。类似地,仅通过一条 target_compile_definitions()
调用,myLib 和任何链接到 myLib 的对象都将有符号 USE_BARRY 的定义。最后,target_include_directories()
命令将 foo 子目录添加到 myLib 和任何链接到它的对象的头搜索路径中。因此,任何其他目录中需要包含 foo.h 的源文件也将能够找到它。
为了说明这些 target_xxx()
命令的强大功能,让我们考虑一下 bar 子目录下的 CMakeLists.txt 文件可能的样子。在这种情况下,我们假设 bar 需要添加一些源文件,并且 bar 的一些源文件或头文件将包含 foo.h。
target_sources(myLib
PRIVATE
bar.cpp
bar.h
gumby.cpp
gumby.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_link_libraries()
以外的其他 target_...()
命令都没有这种限制。我们很快就会解决这一点。
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(myLib
PRIVATE
${CMAKE_CURRENT_LIST_DIR}/foo.cpp
${CMAKE_CURRENT_LIST_DIR}/foo_p.cpp
${CMAKE_CURRENT_LIST_DIR}/foo_p.h
PUBLIC
${CMAKE_CURRENT_LIST_DIR}/foo.h
)
这既不太方便,可读性也较差,因此定义辅助函数可能会有所帮助,这也适用于早期的 CMake 版本。CMake 对保持向后兼容性有很强的要求,因此处理相对路径的行为变化由策略 CMP0076 来保护。我们可以在辅助函数中利用这一点:
# 注意:这个辅助函数默认对源文件不使用生成器表达式
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 scope
cmake_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 behavior
unset(_srcList)
get_target_property(_targetSourceDir ${target} SOURCE_DIR)
foreach(src ${ARGN})
if(NOT src STREQUAL "PRIVATE" AND
NOT src STREQUAL "PUBLIC" AND
NOT src STREQUAL "INTERFACE" AND
NOT IS_ABSOLUTE "${src}")
# Relative path to source, prepend relative to where target was defined
file(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(myLib
PRIVATE
foo.cpp
foo_p.cpp
foo_p.h
PUBLIC
foo.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 "")
# 使用 include() 来避免创建新的目录范围,这样在这些目录中
# 即可使用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(myHeaderOnly
PUBLIC
$<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 直接合并到你的主构建中)。这可以用于从外部项目添加头和图片等,而不会影响目标的构建方式。对于真正有冒险精神的人,您甚至可以使用此技术为弱符号添加自己的实现,以便您的实现覆盖外部项目目标通常使用的实现。这可能有助于测试,或提供特定函数的更高效实现等。