CMake(六):使⽤⼦⽬录
对于简单的项⽬,将所有内容保存在⼀个⽬录中是可以的,但是⼤多数实际项⽬倾向于将它们的⽂件分割到多个⽬录中。通常可以到不同的⽂件类型或分组在各⾃的⽬录下的独⽴模块,或者将属于逻辑功能组的⽂件放在项⽬⽬录层次结构的各⾃部分中。虽然⽬录结构可能由开发⼈员对项⽬的看法驱动,但项⽬的结构⽅式也会影响构建系统。
在任何多⽬录项⽬中,两个基本的CMake命令是add_subdirectory()和include()。这些命令将来⾃另⼀个⽂件或⽬录的内容引⼊到构建中,允许构建逻辑分布在⽬录层次结构中,⽽不是强制所有内容都在最顶层定义。这样做有很多好处:
构建逻辑是本地化的,这意味着构建的特征可以在它们最相关的⽬录中定义。
构建可以由⼦组件组成,⼦组件的定义独⽴于使⽤它们的顶级项⽬。这对于使⽤git⼦模块或嵌⼊第三⽅源代码树的项⽬来说尤为重要。
因为⽬录可以是⾃包含的,所以仅仅通过选择是否在该⽬录中添加就可以打开或关闭构建的部分。
add_subdirectory()和include()具有⾮常不同的特征,因此了解两者的优缺点是很重要的。
6.1 add_subdirectory()
add_subdirectory()命令允许项⽬将另⼀个⽬录带⼊构建。该⽬录必须有⾃⼰的⽂件,该⽂件将在add_subdirectory()被调⽤的地⽅进⾏处理,并在项⽬的构建树中为它创建⼀个相应的⽬录。
add_subdirectory(sourceDir [ binaryDir ] [ EXCLUDE_FROM_ALL ])
sourceDir不⼀定是源树中的⼦⽬录,尽管它通常是。可以添加任何⽬录,sourceDir可以指定为绝对路径或相对路径,后者相对于当前源⽬录。绝对路径通常只在添加主源代码树之外的⽬录时才需要。
通常,binaryDir不需要指定。省略时,CMake会在构建树中创建⼀个与sourceDir同名的⽬录。如果sourceDir包含任何路径组件,它们将被镜像到CMake创建的binaryDir中。或者,binaryDir可以显式地指定为绝对路径或相对路径,后者相对于当前⼆进制⽬录(稍后将更详细地讨论)求值。如果sourceDir是源树之外的⼀个路径,CMake需要指定binaryDir,因为相应的相对路径不能再被⾃动构造。
可选的EXCLUDE_FROM_ALL关键字⽤于控制在添加的⼦⽬录中定义的⽬标在默认情况下是否应该包含在项⽬的ALL⽬标中。不幸的是,对于⼀些CMake版本和项⽬⽣成器,它并不总是像预期的那样⼯作,甚⾄会导致构建破裂。
(1)Source和Binary⽬录变量
有时,开发⼈员需要知道与当前源⽬录对应的构建⽬录的位置,例如当在运⾏时需要复制⽂件或者执⾏
⾃定义构建任务时。使⽤
add_subdirectory(),源代码树和构建树的⽬录结构可以任意复杂。甚⾄可以在同⼀个源代码树中使⽤多个构建树。因此,开发⼈员需要CMake的帮助来确定感兴趣的⽬录。为此,CMake提供了许多变量来跟踪当前正在处理的⽂件的源⽬录和⼆进制⽬录。当CMake处理每个⽂件时,以下只读变量会⾃动更新。它们总是包含绝对路径。
CMAKE_SOURCE_DIR
源码树的最顶层⽬录(也就是⽂件所在的地⽅)。这个变量永远不会改变它的值。
CMAKE_BINARY_DIR
构建树的最顶层⽬录。这个变量永远不会改变它的值。
CMAKE_CURRENT_SOURCE_DIR
CMake正在处理的⽂件所在的⽬录。每次在add_subdirectory()调⽤的结果中处理新⽂件时,它都会更新,并在完成对该⽬录的处理后再次恢复。
CMAKE_CURRENT_BINARY_DIR
当前CMake正在处理的⽂件对应的构建⽬录。每次调⽤add_subdirectory()时它都会改变,并在add_subdirectory()返回时再次恢复。
⼀个例⼦应该有助于演⽰这种⾏为:
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在⼦作⽤域中仍然可见。
④在设置childdvar之前,它在⼦作⽤域中仍然是未定义的。
⑤myVar在⼦范围内被修改。
⑥childVar已被设置在⼦范围内。
⑦当处理返回到⽗作⽤域时,myVar仍然拥有调⽤add_subdirectory()之前的值。⼦作⽤域中对myVar的修改对⽗作⽤域是不可见的。
⑧childVar是在⼦作⽤域中定义的,因此它对⽗对象不可见,计算结果为空字符串。
上述变量作⽤域的⾏为突出了add_subdirectory()的⼀个重要特征。它允许添加的⽬录更改它想要的任何变量,⽽不影响调⽤作⽤域中的变量。这有助于将调⽤范围与可能不想要的更改隔离开来。
但是,有时候,希望在添加的⽬录中对变量进⾏的更改对调⽤者是可见的。例如,该⽬录可能负责收集⼀组源⽂件名,并将其作为⽂件列表向上传递给⽗⽬录。这就是set()命令中PARENT_SCOPE关键字的作⽤。当使⽤PARENT_SCOPE时,所设置的变量是⽗作⽤域中的变量,⽽不是当前作⽤域中的变量。重要的是,这并不意味着同时在⽗范围和当前范围中设置变量。稍微修改⼀下前⾯的例
⼦,PARENT_SCOPE的效果就很明显了:
<
message("Child (before): myVar = ${myVar}")
set(myVar bar PARENT_SCOPE)
message("Child (after): myVar = ${myVar}")
sub_
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 ②
①⼦作⽤域中的myVar不受set()调⽤的影响,因为关键字PARENT_SCOPE告诉CMake修改⽗作⽤域中的myVar,⽽不是本地的myVar。
②⽗类的myVar被⼦作⽤域中的set()调⽤修改了。
因为使⽤PARENT_SCOPE可以防⽌任何同名的局部变量被该命令修改,所以如果局部作⽤域不重⽤与⽗变量相同的变量名,则可以减少误导。在上⾯的例⼦中,⼀组更清晰的命令是:
set(localVar bar)
set(myVar ${localVar} PARENT_SCOPE)
显然,上⾯的例⼦很简单,但是对于实际的项⽬来说,在最终设置⽗类的myVar变量之前,可能会有很多命令帮助建⽴localVar的值。
受范围影响的不仅仅是变量,策略和⼀些属性在这⽅⾯也与变量有类似的⾏为。对于策略,每个add_subdirectory()调⽤都会创建⼀个新的范围,在此范围内可以进⾏策略更改,⽽不会影响⽗策略的设置。类似地,可以在⼦⽬录的⽂件中设置⽬录属性,这对⽗⽬录的⽬录属性没有影响。
6.2 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()需要⼀个⽬录,并在该⽬录中查⽂件。传递给include()的⽂件名通常扩展名为.cmake,但可以是任何名称。
include()没有引⼊新的变量范围,⽽add_subdirectory()引⼊了。
默认情况下,这两个命令都引⼊了⼀个新的策略范围,但是可以使⽤NO_POLICY_SCOPE选项告诉include()命令不要这样做(add_subdirectory()没有这样的选项)。
CMAKE_CURRENT_SOURCE_DIR和CMAKE_CURRENT_BINARY_DIR变量的值在处理由include()命名的⽂件时不会改变,然⽽它们在add_subdirectory()中会改变。
include()命令的第⼆种形式具有完全不同的⽬的。它⽤于加载命名的模块,除了第⼀点之外,上述所有观点都适⽤于第⼆种形式。
因为当include()被调⽤时,CMAKE_CURRENT_SOURCE_DIR的值不会改变,所以包含的⽂件似乎很难计算出它所在的⽬录。
CMAKE_CURRENT_SOURCE_DIR将包含调⽤include()的⽂件的位置,⽽不是包含包含⽂件的⽬录。此外,与⽂件名总是为的add_subdirectory()不同,当使⽤include()时,⽂件的名称可以是任何东西,所以所包含的⽂件很难确定⾃⼰的名称。为了解决这样的情况,CMake提供了⼀组额外的变量:
CMAKE_CURRENT_LIST_DIR
类似于CMAKE_CURRENT_SOURCE_DIR,除了它会在处理包含的⽂件时更新。这是在需要处理当前⽂件的⽬录时使⽤的变量,不管它是如何被添加到构建中的。它总是保持⼀条绝对路径。
CMAKE_CURRENT_LIST_FILE
总是给出当前正在处理的⽂件的名称。它总是保存⽂件的绝对路径,⽽不仅仅是⽂件名。
CMAKE_CURRENT_LIST_LINE
保存当前正在处理的⽂件的⾏号。这个变量很少需要,但是在⼀些调试场景中可能会被证明是有⽤的。
需要注意的是,上述三个变量适⽤于任何CMake处理的⽂件,⽽不仅仅是那些include()命令的⽂件。即使是通过add_subdirectory()拉⼊的⽂件,它们的值也与上⾯描述的相同,在这种情况下,CMAKE_CURRENT_LIST_DIR将与
CMAKE_CURRENT_SOURCE_DIR具有相同的值。下⾯的例⼦演⽰了这种⾏为:
<
add_subdirectory(subdir)
message("====")
include()
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/
CMAKE_CURRENT_LIST_LINE = 5
====
cmake如何使用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/
CMAKE_CURRENT_LIST_LINE = 5
上⾯的例⼦还突出了include()命令的另⼀个有趣的特性。它可以⽤于包含先前构建中已经包含的⽂件的内容。如果⼤型复杂项⽬的不同⼦⽬录都想在项⽬公共区域的某个⽂件中使⽤CMake代码,那么它们都可以独⽴地include()该⽂件。
6.3 早期终⽌处理
在某些情况下,项⽬可能希望停⽌处理当前⽂件的剩余部分,并将控制权返回给调⽤者。return()命令可以完全⽤于此⽬的,但请注意,它不能向调⽤者返回值。它的唯⼀作⽤是结束当前作⽤域的处理。如果
不是在函数内部调⽤,return()将结束对当前⽂件的处理,⽆论它是通过include()还是add_subdirectory()引⼊的。
如前⼀节所述,项⽬的不同部分可能包括来⾃多个位置的相同⽂件。有时,最好检查这个⽂件,只包含该⽂件⼀次,并尽早返回后续包含的内容,以防⽌多次重新处理该⽂件。这与C/ C++头⽂件的情况⾮常相似,通常会看到类似形式的include guard被使⽤:
if(DEFINED cool_stuff_include_guard)
return()
endif()
set(cool_stuff_include_guard 1)
# ...
在CMake 3.10或更⾼版本中,可以使⽤类似于C/ c++中的#pragma once的专⽤命令来更简洁、更健壮地表达这⼀点:
include_guard()

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。