(六)子目录使用
子目录
简单项目中将所有文件置于同一目录没有问题,但是实际过程中通常会存在多个目录,不同目录存放各自类型相同或功能相关的文件。当存在多个目录时,项目结构会各不相同,而这也会影响构建的过程。
在多目录项目中 CMake 会用到两个基础命令 add_subdirectory 和 include()。这些命令会将其他文件或目录中的内容引入构建过程中,从而使得构建逻辑能够分布在各目录层级结构中,而非强制所有内容都定义在最顶层。这种方式有以下好处:
- 构建逻辑更趋于本地化,意味着构建特性可以定义在与它们联系更为紧密的位置;
- 构建可以由独立于调用它们的顶层项目而定义的子组件组成。如果一个项目使用了诸如 Git 子模块或嵌入第三方源代码,这就会变得非常有用。
- 因为目录可以独立存在,所以只需决定是否将某个目录包含进来,就很容易地开启或关闭构建过程中的某些部分。
add_subdirectory() 和 include() 具有截然不同的特性,因此了解这两者的优缺点非常重要。
add_subdirectory()
add_subdirectory() 命令允许一个项目将另一个目录纳入构建过程。该目录必须有自己的 CMakeLists.txt 文件,该文件将在调用 add_subdirectory() 时进行处理。在项目的构建树中,会创建一个与之对应的目录。
add_subdirectory(sourceDir [binaryDir]
[EXCLUDE_FROM_ALL]
[SYSTEM]# Requires CMake 3.25 or later
)
源目录不一定是源文件夹树中的子目录,虽然通常是这样。任何目录都可以添加进来,其中 sourceDir 可以用绝对路径或相对路径来指定,后者是以当前源目录为基准的相对路径。绝对路径通常只在添加位于主源文件夹树之外的目录时才需要使用。
通常情况下,无需指定 binaryDir。若未指定,则 CMake 会在构建目录中创建一个与 sourceDir 名称相同的目录。如果 sourceDir 包含任何路径组件,这些组件也会在 CMake 创建的 binaryDir 中对应创建。或者,binaryDir 可以明确指定为绝对路径或相对路径,后者将以当前二进制目录为基准进行解析(稍后会对此进行更详细的说明)。如果 sourceDir 是位于源树之外的路径,CMake 就需要指定 binaryDir,因为此时 CMake无法定位相应的相对路径。
可选的“EXCLUDE_FROM_ALL”关键字旨在控制在要添加的子目录中定义的目标,默认情况下是否应包含在项目的“ALL”目标中。然而,对于某些 CMake 版本和项目生成器而言,该关键字的使用效果并不总是如预期那样,甚至可能导致构建失败。
源/目标目录变量
有时,开发人员需要知道与当前源目录相对应的构建目录的位置,例如在复制运行时所需的文件或执行自定义构建任务时。使用 add_subdirectory() 时,源目录和构建目录的目录结构可能会非常复杂。甚至可能使用同一个源目录创建多个构建目录。因此,开发人员需要 CMake 提供的一些辅助功能来确定这些关键目录。CMake 提供了多个变量来跟踪当前正在处理的 CMakeLists.txt 文件的源目录和二进制目录。以下这些只读变量会在 CMake 处理每个文件时自动更新。且它们总是包含绝对路径。
CMAKE_SOURCE_DIR
源代码树的最顶层目录(即最顶层的 CMakeLists.txt 文件所在的目录)。此变量的值永远不会改变。
CMAKE_BINARY_DIR
构建树的最顶层目录。此变量的值永远不会改变。
CMAKE_CURRENT_SOURCE_DIR
CMake 正在处理的 CMakeLists.txt 文件的目录。每次执行 add_subdirectory() 调用处理新文件时,该目录都会被更新,并且在该目录的处理完成后会再次恢复原状。
CMAKE_CURRENT_BINARY_DIR
与当前正在被 CMake 处理的 CMakeLists.txt 文件相对应的构建目录。每次调用 add_subdirectory() 时,该目录都会发生变化,并且在 add_subdirectory() 返回时会再次恢复。
举例最外层 CMakeLists.txt 文件:
cmake_minimum_required(VERSION 3.0)
project(MyApp)
message("top: CMAKE_SOURCE_DIR = ${CMAKE_SOURCE_DIR}")
message("top: CMAKE_BINARY_DIR = ${CMAKE_BINARY_DIR}")
message("top: CMAKE_CURRENT_SOURCE_DIR = ${CMAKE_CURRENT_SOURCE_DIR}")
message("top: CMAKE_CURRENT_BINARY_DIR = ${CMAKE_CURRENT_BINARY_DIR}")
add_subdirectory(mysub)
message("top: CMAKE_CURRENT_SOURCE_DIR = ${CMAKE_CURRENT_SOURCE_DIR}")
message("top: CMAKE_CURRENT_BINARY_DIR = ${CMAKE_CURRENT_BINARY_DIR}")
次级目录 mysub/CMakeLists.txt:
message("mysub: CMAKE_SOURCE_DIR = ${CMAKE_SOURCE_DIR}")
message("mysub: CMAKE_BINARY_DIR = ${CMAKE_BINARY_DIR}")
message("mysub: CMAKE_CURRENT_SOURCE_DIR = ${CMAKE_CURRENT_SOURCE_DIR}")
message("mysub: CMAKE_CURRENT_BINARY_DIR = ${CMAKE_CURRENT_BINARY_DIR}")
针对于以上示例,如果顶层 CMakeLists.txt 位于 /somewhere/src,构建目录位于 /somewhere/build,则会输出以下内容:
top: CMAKE_SOURCE_DIR = /somewhere/src
top: CMAKE_BINARY_DIR = /somewhere/build
top: CMAKE_CURRENT_SOURCE_DIR = /somewhere/src
top: CMAKE_CURRENT_BINARY_DIR = /somewhere/build
mysub: CMAKE_SOURCE_DIR = /somewhere/src
mysub: CMAKE_BINARY_DIR = /somewhere/build
mysub: CMAKE_CURRENT_SOURCE_DIR = /somewhere/src/mysub
mysub: CMAKE_CURRENT_BINARY_DIR = /somewhere/build/mysub
top: CMAKE_CURRENT_SOURCE_DIR = /somewhere/src
top: CMAKE_CURRENT_BINARY_DIR = /somewhere/build
作用域
调用 add_subdirectory() 函数的一个作用是,CMake 会为处理该子目录的 CMakeLists.txt 文件创建一个新的作用域。
该新作用域的行为类似于调用作用域的“子作用域”,与 block() 命令创建局部子作用域的方式类似。其效果也非常相似:
- 在调用范围内定义的所有变量在进入子目录的子作用域时都会被复制进去。
- 在子目录的子作用域中创建的任何新变量都不会对调用作用域可见。
- 在子目录的子作用域中对变量所做的任何更改都仅对该子作用域有效。
- 在子目录的子作用域中取消变量的设置并不会在调用作用域中取消该变量的设置。
请看如下详细示例:
CMakeLists.txt
set(myVar foo)
message("Parent
message("Parent (before): myVar = ${myVar}")
message("Parent (before): childVar = ${childVar}")
add_subdirectory(subdir)
message("Parent (after): myVar = ${myVar}")
message("Parent (after): childVar = ${childVar}")
subdir/CMakeLists.txt:
message("Child (before): myVar = ${myVar}")
message("Child (before): childVar = ${childVar}")
set(myVar bar)
set(childVar fuzz)
message("Child (after): myVar = ${myVar}")
message("Child (after): childVar = ${childVar}")
输出结果:
Parent (before): myVar = foo ①
Parent (before): childVar = ②
Child (before): myVar = foo ③
Child (before): childVar = ④
Child (after): myVar = bar ⑤
Child (after): childVar = fuzz ⑥
Parent (after): myVar = foo ⑦
Parent (after): childVar = ⑧
① myVar 在父级级别被定义。
② childVar 未在父级级别被定义,因此其值为一个空字符串。
③ myVar 在子作用域中仍然可见。
④ 在设置 childVar 之前,它在子作用域中仍未定义。
⑤ myVar 在子作用域中被修改。
⑥ childVar 在子作用域中被设置。
⑦ 在处理返回到父级作用域时,myVar 仍保留调用 add_subdirectory() 之前的状态。子作用域中对 myVar 的修改对父级不可见。
⑧ childVar 在子作用域中被定义,因此对父级不可见,并且其值为一个空字符串。
上述变量作用域的设定方式凸显了 add_subdirectory() 函数的一个重要特性。它使得添加的目录能够自由设定其想要的任何变量,而不会影响调用作用域中的变量。这有助于将调用作用域与可能产生的不希望出现的变化隔离开来。
如第 5.4 节“作用域块”所述,使用 PARENT_SCOPE 关键字可以与 set() 或 unset() 命令一起使用,以在父作用域而非当前作用域中更改或取消变量的设置。对于由 add_subdirectory() 创建的子作用域,其工作方式与此相同:
CMakeLists.txt:
set(myVar foo)
message("Parent (before): myVar = ${myVar}")
add_subdirectory(subdir)
message("Parent (after): myVar = ${myVar}")
subdir/CMakeLists.txt:
message("Child (before): myVar = ${myVar}")
set(myVar bar PARENT_SCOPE)
message("Child (after): myVar = ${myVar}")
输出:
Parent (before): myVar = foo
Child (before): myVar = foo
Child (after): myVar = foo ①
Parent (after): myVar = bar ②
①The myVar in the child scope is not affected by the set() call because the PARENT_SCOPE keyword tells CMake to modify the parent’s myVar, not the local one.
②The parent’s myVar has been modified by the set() call in the child scope.
由于使用“PARENT_SCOPE”可以防止同名的局部变量被该命令所修改,因此如果局部作用域不使用与父作用域相同的变量名称,那么这种做法可能会更不容易造成混淆。在上述示例中,更清晰的一组命令应该是:
subdir/CMakeLists.txt:
set(localVar bar)
set(myVar ${localVar} PARENT_SCOPE)
显然,上述内容只是一个简单的示例。但在实际项目中,可能会有许多命令在最终设置父类的 myVar 变量之前,对 localVar 的值起到一定的提升作用。
不仅变量会受到作用域的影响,一些策略以及某些属性在这一方面也有与变量类似的行为。就策略而言,每次调用 add_subdirectory() 都会在一个新的作用域中创建,这样就可以进行策略更改而不会影响父目录的策略设置。同样,也有可以在子目录的 CMakeLists.txt 文件中设置的目录属性,这些属性对父目录的属性不会产生任何影响。
调用 project() 时
有时会出现这样一个问题:在子目录的 CMakeLists.txt 文件中是否需要调用 project() 函数。在大多数情况下,这样做并非必要且也不可取,但却是允许的。唯一必须调用 project() 函数的地方是在最顶层的 CMakeLists.txt 文件中。在读取顶层的 CMakeLists.txt 文件时,CMake 会扫描该文件的内容,寻找对 project() 的调用。如果没有找到这样的调用,CMake 会发出警告,并插入一个带有默认 C 和 C++ 语言启用的内部调用 project()。项目永远不应依赖这种机制,而应始终自行显式地调用 project()。请注意,仅仅通过包装函数调用 project() 或通过 add_subdirectory() 或 include() 读取文件的方式是不够的,最顶层的 CMakeLists.txt 文件必须直接调用 project()。
在子目录中调用 project() 函数通常不会造成什么问题,但可能会导致 CMake 需要生成额外的文件。在大多数情况下,这些额外的 project() 调用和生成的文件只是无用的干扰信息,但在某些情况下它们可能有用。当使用 Visual Studio 项目生成器时,每个 project() 命令都会创建一个相关的解决方案文件。通常,开发人员会加载与最顶层的 project() 调用相对应的解决方案文件(该解决方案文件将位于构建目录的顶部)。这个顶级解决方案文件包含了项目中的所有目标。对于子目录中的任何 project() 调用所生成的解决方案文件,将包含一个更精简的视图,仅包含该目录范围及以下的目标,以及它们所依赖的构建其余部分的其他目标。开发人员可以加载这些子解决方案,而不是顶级解决方案,以获得项目更精简的视图,使他们能够专注于目标集中的较小子集。对于具有许多目标的非常大型项目,这尤其有用。
Xcode 生成器的运作方式也类似,每次调用 project() 时都会创建一个 Xcode 项目。这些 Xcode 项目可以加载以获得类似的精简视图,但与 Visual Studio 生成器不同的是,它们不包含从该目录范围之外或其之下构建目标的逻辑。开发者有责任确保来自该精简视图之外的任何所需内容已经构建完成。实际上,这意味着在切换到精简的 Xcode 项目之前,可能需要先加载并构建顶层项目。
include()
CMake 还提供了另一种从其他目录引入内容的方法,即“include()”命令,该命令有两种形式:
include(fileName [OPTIONAL] [RESULT_VARIABLE myVar] [NO_POLICY_SCOPE])
include(module [OPTIONAL] [RESULT_VARIABLE myVar] [NO_POLICY_SCOPE])
第一种形式在某种程度上类似于 add_subdirectory() 函数,但也有重要的区别:
-
include() 函数期望传入一个要读取的文件的名称,而 add_subdirectory() 函数则期望一个目录,并会在该目录内查找 CMakeLists.txt 文件。传入到 include() 函数中的文件名通常具有 .cmake 扩展名,但也可以是任何其他扩展名。
-
include() 不会引入新的变量作用域,而 add_subdirectory() 却会。
-
这两条命令默认都会引入一个新的策略范围,但 include() 命令可以通过添加 NO_POLICY_SCOPE 选项来避免这样做(而 add_subdirectory() 没有这样的选项)。
-
在处理由 include() 函数传入的文件时,CMAKE_CURRENT_SOURCE_DIR 和 CMAKE_CURRENT_BINARY_DIR 这两个变量的值不会发生变化;但对于 add_subdirectory() 而言,这些变量的值则会发生变化。这一情况将在稍后进行更详细的讨论。
“include()”命令的第二种形式有着完全不同的用途。它用于加载指定的模块。上述除第一点之外的所有要点对于这种第二种形式同样适用。
由于在调用 include() 时,CMAKE_CURRENT_SOURCE_DIR 的值不会发生变化,因此被包含的文件似乎难以确定其所在的目录。CMAKE_CURRENT_SOURCE_DIR 会包含调用 include() 时所从的文件的位置,而不是包含被包含文件所在的目录。此外,与 add_subdirectory() 不同,后者中的 fileName 总是为 CMakeLists.txt,而使用 include() 时文件名可以是任意内容,因此被包含的文件难以确定自身的名称。为了解决这类问题,CMake 提供了一组额外的变量:
CMAKE_CURRENT_LIST_DIR
与 CMAKE_CURRENT_SOURCE_DIR 类似,但其会在处理包含文件时进行更新。此变量用于在需要当前正在处理的文件所在目录的情况下使用,无论该目录是如何被添加到构建过程中的。它始终会保存绝对路径。
CMAKE_CURRENT_LIST_FILE
始终会显示当前正在处理的文件的名称。它始终保存的是文件的绝对路径,而不仅仅是文件名。
CMAKE_CURRENT_LIST_LINE
保存当前正在处理的文件的行数。此变量通常并不常用,但在某些调试场景中可能会派上用场。
请注意,上述这三个变量适用于任何由 CMake 处理的文件,而不仅仅是通过 include() 命令引入的文件。它们的值与上述描述的完全相同,即使是在通过 add_subdirectory() 引入的 CMakeLists.txt 文件中也是如此,在这种情况下,CMAKE_CURRENT_LIST_DIR 的值与 CMAKE_CURRENT_SOURCE_DIR 的值相同。以下示例展示了这种行为:
CMakeLists.txt:
add_subdirectory(subdir)
message("")
include(subdir/CMakeLists.txt)
subdir/CMakeLists.txt:
message("CMAKE_CURRENT_SOURCE_DIR = ${CMAKE_CURRENT_SOURCE_DIR}")
message("CMAKE_CURRENT_BINARY_DIR = ${CMAKE_CURRENT_BINARY_DIR}")
message("CMAKE_CURRENT_LIST_DIR = ${CMAKE_CURRENT_LIST_DIR}")
message("CMAKE_CURRENT_LIST_FILE = ${CMAKE_CURRENT_LIST_FILE}")
message("CMAKE_CURRENT_LIST_LINE = ${CMAKE_CURRENT_LIST_LINE}")
输出:
CMAKE_CURRENT_SOURCE_DIR = /somewhere/src/subdir
CMAKE_CURRENT_BINARY_DIR = /somewhere/build/subdir
CMAKE_CURRENT_LIST_DIR = /somewhere/src/subdir
CMAKE_CURRENT_LIST_FILE = /somewhere/src/subdir/CMakeLists.txt
CMAKE_CURRENT_LIST_LINE = 5
CMAKE_CURRENT_SOURCE_DIR = /somewhere/src
CMAKE_CURRENT_BINARY_DIR = /somewhere/build
CMAKE_CURRENT_LIST_DIR = /somewhere/src/subdir
CMAKE_CURRENT_LIST_FILE = /somewhere/src/subdir/CMakeLists.txt
CMAKE_CURRENT_LIST_LINE = 5
上述示例还突显了 include() 命令的另一个有趣特性。它可用于包含先前在构建过程中已包含的文件中的内容。如果一个大型复杂项目中的不同子目录都希望在项目中某个共同区域的某个文件中使用 CMake 代码,那么它们都可以独立地使用 include() 命令来包含该文件。
项目相关变量
正如后续章节中将会看到的那样,各种场景都需要相对于源目录或构建目录中的某个位置的路径。考虑这样一个例子:一个项目需要一个位于其顶层源目录中的文件的路径。根据前面提到的源目录和二进制目录变量,CMAKE_SOURCE_DIR 似乎是一个很自然的选择,可以使用像 ${CMAKE_SOURCE_DIR}/someFile 这样的路径。但考虑一下,如果该项目后来通过将它引入父项目的构建目录(通过 add_subdirectory())而被纳入另一个父项目中,会发生什么情况。它可以作为 git 子模块使用。原来项目源树的顶部现在是父项目源树中的一个子目录。CMAKE_SOURCE_DIR 现在指向父项目的顶部,所以文件路径将指向错误的目录。对于 CMAKE_BINARY_DIR 也存在类似的陷阱。
上述这种情况在在线教程和较旧的项目中出现得相当频繁,但其实很容易避免。project() 命令会设置一些变量,这些变量能以一种更可靠的方式定义相对于目录层次结构中位置的路径。在至少调用过一次 project() 之后,以下这些变量将会可用:
PROJECT_SOURCE_DIR
当前作用域或任何父作用域中最近一次调用 project() 时的源目录。项目名称(即 project() 命令所接收的第一个参数)与此无关。
PROJECT_BINARY_DIR
与由 PROJECT_SOURCE_DIR 定义的源目录相对应的构建目录。
PROJECT_NAME_SOURCE_DIR
当前作用域或任何父作用域中最近一次调用 project(projectName) 所使用的源目录。此目录与特定的项目名称相关联,因此也与对 project() 的特定调用相关联。
PROJECT_NAME_BINARY_DIR
与由 PROJECT_NAME_SOURCE_DIR 定义的源目录相对应的构建目录。
以下示例展示了如何使用这些变量(…_BINARY_DIR 变量的使用方式与所示的…_SOURCE_DIR 变量的使用方式类似)。
CMakeLists.txt:
cmake_minimum_required(VERSION 3.0)
project(topLevel)
message("Top level:")
message(" PROJECT_SOURCE_DIR = ${PROJECT_SOURCE_DIR}")
message(" topLevel_SOURCE_DIR = ${topLevel_SOURCE_DIR}")
add_subdirectory(child)
child/CMakeLists.txt:
message("Child:")
message(" PROJECT_SOURCE_DIR (before) = ${PROJECT_SOURCE_DIR}")
project(child)
message(" PROJECT_SOURCE_DIR (after) = ${PROJECT_SOURCE_DIR}")
message(" child_SOURCE_DIR = ${child_SOURCE_DIR}")
add_subdirectory(grandchild)
child/grandchild/CMakeLists.txt:
message("Grandchild:")
message(" PROJECT_SOURCE_DIR = ${PROJECT_SOURCE_DIR}")
message(" child_SOURCE_DIR = ${child_SOURCE_DIR}")
message(" topLevel_SOURCE_DIR = ${topLevel_SOURCE_DIR}")
在该项目层次结构的顶层运行 cmake 会得到类似于以下的输出:
Top level:
PROJECT_SOURCE_DIR=/somewhere/src
topLevel_SOURCE_DIR=/somewhere/src
Child:
PROJECT_SOURCE_DIR(before)=/somewhere/src
PROJECT_SOURCE_DIR(after)=/somewhere/src/child
child_SOURCE_DIR=/somewhere/src/child
Grandchild:
PROJECT_SOURCE_DIR=/somewhere/src/child
child_SOURCE_DIR=/somewhere/src/child
topLevel_SOURCE_DIR=/somewhere/src
上述示例展示了项目相关变量的多功能性。这些变量可以在目录层次结构的任何位置使用,以可靠地指向项目中的任何其他目录。对于本节开头所讨论的场景,使用 ${PROJECT_SOURCE_DIR}/someFile 或者也许是 ${projectName_SOURCE_DIR}/someFile 而不是 ${CMAKE_SOURCE_DIR}/someFile,将确保 someFile 的路径是正确的,无论项目是独立构建还是被纳入更大的项目层次结构中。
某些层级化的构建安排使得项目既可以独立构建,也可以作为更大父项目的组成部分进行构建。该项目的某些部分只有在处于构建的顶层时才有意义,例如设置打包支持。一个项目可以通过比较 CMAKE_SOURCE_DIR 的值与 CMAKE_CURRENT_SOURCE_DIR 的值来检测自己是否处于顶层位置。如果它们相等,那么当前目录范围必定是源树的顶层。
if(CMAKE_CURRENT_SOURCE_DIR STREQUAL CMAKE_SOURCE_DIR)
add_subdirectory(packaging)
endif()
上述技术已被所有版本的 CMake 支持,并且是一种非常常见的模式。在 CMake 3.21 或更高版本中,提供了专门的 PROJECT_IS_TOP_LEVEL 变量,它可以达到相同的效果,但其意图表述得更加清晰:
# Requires CMake 3.21 or later
if(PROJECT_IS_TOP_LEVEL)
add_subdirectory(packaging)
endif()
如果当前目录范围或更高层级中最近一次调用 project() 的操作是在顶级的 CMakeLists.txt 文件中进行的,那么 PROJECT_IS_TOP_LEVEL 的值将为真。CMake 3.21 或更高版本还为每次调用 project() 定义了一个类似的变量 _IS_TOP_LEVEL。它被创建为缓存变量,因此可以从任何目录范围中读取。 对应于所关注的 project() 命令的名称。当当前范围和所关注项目的范围之间可能存在中间的 project() 调用时,这个替代变量非常有用。
提前结束处理流程
在某些情况下,项目可能需要停止处理当前文件的剩余部分,并将控制权返回给调用者。此时可以使用“return()”命令来实现这一目的。如果不在函数内部调用“return()”,那么无论该文件是通过“include()”还是“add_subdirectory()”引入的,它都会结束当前文件的处理。
在 CMake 3.24 及更早版本中,return() 命令无法向调用者返回任何值。从 CMake 3.25 开始,return() 接受一个 PROPAGATE 关键字,其与 block() 命令的相同关键字具有相似之处。在 PROPAGATE 关键字之后列出的变量将在控制返回的范围内进行更新。从历史上看,return() 命令过去会忽略其接收到的所有参数。因此,如果使用 PROPAGATE 关键字,必须将 CMP0140 策略设置为 NEW,以表明旧行为不再适用。
CMakeLists.txt:
set(x 1)
set(y 2)
add_subdirectory(subdir)
# Here, x will have the value 3 and y will be unset
subdir/CMakeLists.txt:
# This ensures that we have a version of CMake that supports
# PROPAGATE and that the CMP0140 policy is set to NEW.
cmake_minimum_required(VERSION 3.25)
set(x 3)
unset(y)
return(PROPAGATE x y)
有两个涉及变量传播以及与“block()”命令交互的案例值得特别指出。这两个案例都是由于“return()”命令会更新其返回到的作用域中的变量这一事实所导致的。对于这两个案例中的第一个,如果返回到的该作用域位于一个“block”内部,那么受影响的将是该“block”所对应的作用域。
CMakeLists.txt:
set(x 1)
set(y 2)
block()
add_subdirectory(subdir)
# Here, x will have the value 3 and y will be unset
endblock()
# Here, x is 1 and y is 2
另一个需要强调的情况则更为有趣。如果 return() 语句本身位于一个代码块内部,那么该代码块不会影响变量向返回到的范围的传递。
CMakeLists.txt:
set(x 1)
set(y 2)
add_subdirectory(subdir)
# Here, x will have the value 3 and y will be unset
subdir/CMakeLists.txt:
cmake_minimum_required(VERSION 3.25)
# This block does not affect the propagation of x and y to
# the parent CMakeLists.txt file's scope
block()
set(x 3)
unset(y)
return(PROPAGATE x y)
endblock()
关于在目录范围内使用“返回(传播)”这一操作,有必要给出一点提醒。虽然这看起来像是将信息传递回父级范围的一种颇具吸引力的方式,但这种使用方法并不符合 CMake 最佳实践中以目标为中心的更集中式方法。将变量传播到父级范围会使项目结构更类似于传统的基于变量的方法。这些方法众所周知是脆弱的,并且缺乏以目标为中心方法的灵活性和表达力。不过,当从函数中返回值时,使用“返回(传播)”进行变量传播可能是合适的。
return() 命令并非是提前结束文件处理的唯一方法。正如前一节所述,一个项目的不同部分可能会从多个地方引用同一个文件。有时,为了确保只包含该文件一次,并且在后续引用时提前结束处理以避免多次重复处理该文件,这种情况是很有必要的。这与 C 和 C++ 头文件的情况非常相似。因此,通常会看到类似的包含保护机制被使用:
if(DEFINED cool_stuff_include_guard)
return()
endif()
set(cool_stuff_include_guard 1)#
...
在 CMake 3.10 或更高版本中,可以更简洁、更可靠地通过一个专用命令来实现这一功能,该命令的行为类似于 C 和 C++ 中的 #pragma once:
include_guard()
与手动编写 if-endif 代码相比,这种方式更加可靠,因为它会自动处理保护变量的名称。该命令还接受一个可选的关键字参数 DIRECTORY 或 GLOBAL,用于指定在何处检查文件是否已被之前处理过。不过,在大多数情况下,这些关键字不太可能被使用。如果未指定任何参数,将默认假设变量作用域,并且其效果与上述的 if-endif 代码完全相同。GLOBAL 保证如果文件在项目中的其他任何地方已被处理过,则命令会结束对该文件的处理(即忽略变量作用域)。DIRECTORY 仅在当前目录作用域及其以下范围内检查是否已进行过先前处理。
建议
在使用 add_subdirectory() 还是使用 include() 来将另一个目录纳入构建过程中时,究竟哪种选择才是最佳的,并非总是显而易见的。一方面,add_subdirectory() 更简单,并且在保持目录相对独立方面做得更好,因为它会创建自己的作用域。另一方面,一些 CMake 命令存在限制,仅允许它们在当前文件作用域内操作,所以 include() 在这些情况下表现得更好。
一般来说,大多数简单的项目最好还是优先使用 add_subdirectory() 而不是 include()。这样做能更清晰地定义项目,使给定目录的 CMakeLists.txt 更专注于该目录自身需要定义的内容。遵循这一策略将有助于在整个项目中更好地保持信息的局部性,并且只会在需要且能带来有益效果的地方引入复杂性。这并不是说 include() 自身比 add_subdirectory() 更复杂,但使用 include() 通常会导致文件路径需要更明确地指定,因为 CMake 所认为的当前源目录并非所包含文件的目录。在较新的 CMake 版本中,许多与从不同目录调用某些命令相关的限制也已被取消,这进一步支持了优先使用 add_subdirectory() 的观点。
无论使用 add_subdirectory()、include() 还是两者结合,通常来说,CMAKE_CURRENT_LIST_DIR 变量都比 CMAKE_CURRENT_SOURCE_DIR 更为理想的选择。尽早养成使用 CMAKE_CURRENT_LIST_DIR 的习惯,当项目变得复杂时,就能更轻松地在 add_subdirectory() 和 include() 之间切换,并且能够将整个目录移动以重新组织项目结构。
在可能的情况下,请避免使用 CMAKE_SOURCE_DIR 和 CMAKE_BINARY_DIR 这两个变量,因为它们通常会破坏项目被纳入更大项目层级结构的能力。在绝大多数情况下,PROJECT_SOURCE_DIR 和 PROJECT_BINARY_DIR,或者它们各自特定于项目的等效变量 PROJECT_NAME_SOURCE_DIR 和 PROJECT_NAME_BINARY_DIR 更是适合使用的变量。
在使用 return() 语句结束当前文件处理的过程中,切勿使用 PROPAGATE 关键字。将变量传播到父文件的做法违反了首选将信息附加到目标而非通过变量传递细节的最佳实践。
如果该项目需要 CMake 3.10 或更高版本,那么在需要避免多次包含同一文件的情况下,建议使用不带参数的 include_guard() 命令,而非使用显式的 if-endif 块。
避免在每个子目录的 CMakeLists.txt 文件中随意调用 project() 函数的做法。只有当某个子目录可以被视为一个相对独立的项目时,才考虑将其 CMakeLists.txt 文件中加入一个 project() 命令。除非整个构建的生成目标数量非常多,否则在除顶层 CMakeLists.txt 文件之外的任何地方调用 project() 函数都是不必要的。

浙公网安备 33010602011771号