目录

CMake子目录

CMake提供两个命令add_subdirectory和include,将另一个文件或目录中的内容包含到构建中,允许构建逻辑分布在目录层次中,而不是所有东西都在最顶层定义。优点如下:

  • 构建逻辑是本地化的,这意味着构建的特性可以在与之最相关的目录中定义。
  • 构建可以由子组件组成,这些子组件的定义独立于使用它们的顶级项目。
  • 因为目录是独立的,所以只需选择是否添加该目录,就可以打开或关闭部分构建。

add_subdirectory

add_subdirectory命令将一个目录包含到构建中,并在构建目录中为其创建一个相应的目录。该目录必须有自己的CMakeLists.txt文件,它将在调用add_subdirectory时被处理。

1
add_subdirectory(sourceDir [binaryDir] [EXCLUDE_FROM_ALL])

sourceDir不一定是源码树中的目录,任何目录都可以被添加,sourceDir可以是绝对路径或相对路径,后者是相对于当前源目录的,通常只有添加源码树之外的目录时才需要绝对路径。

如果省略binaryDir,CMake会在构建目录中创建一个与sourceDir同名的目录。如果sourceDir是源码树之外的路径,则binaryDir路径也需要指定。

EXCLUDE_FROM_ALL关键字用来控制子目录中定义的目标是否应该包含在默认的ALL目标中。

目录相关变量

CMake提供了一些变量,用于跟踪当前正在处理的CMakeLists.txt文件的源码目录和二进制目录。这些只读变量会在CMake处理每个文件时自动更新,并且总是包含绝对路径。

CMAKE_SOURCE_DIR

源码树最顶层的目录,这个变量的值永远不会改变。

CMAKE_BINARY_DIR

构建树最顶层的目录,这个变量的值永远不会改变。

CMAKE_CURRENT_SOURCE_DIR

目前正在被CMake处理的CMakeLists.txt文件所在的目录。每次调用add_subdirectory都会被更新,并在该目录处理完后再次被恢复。

CMAKE_CURRENT_BINARY_DIR

当前被 CMake 处理的 CMakeLists.txt 文件对应的构建目录。每次调用add_subdirectory都会被更新,并在该目录处理完后再次被恢复。

举个例子,顶层CMakeLists.txt如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
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如下:

1
2
3
4
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}")

会得到以下输出:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
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:

1
2
3
4
5
6
7
8
set(myVar foo)
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

1
2
3
4
5
6
7
8
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}")

输出如下:

1
2
3
4
5
6
7
8
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 =

然而有些时候可能需要在子目录中能够修改父目录的变量,这就是set命令中PARENT_SCOPE关键字的目的。当使用PARENT_SCOPE时,被设置的变量是父作用域中的变量,并不会修改当前作用域中同名的变量。举个例子,顶层CMakeLists.txt如下:

1
2
3
4
set(myVar foo)
message("Parent (before): myVar = ${myVar}")
add_subdirectory(subdir)
message("Parent (after): myVar = ${myVar}")

subdir/CMakeLists.txt

1
2
3
message("Child (before): myVar = ${myVar}")
set(myVar bar PARENT_SCOPE)
message("Child (after): myVar = ${myVar}")

输出如下:

1
2
3
4
Parent (before): myVar = foo
Child (before): myVar = foo
Child (after): myVar = foo
Parent (after): myVar = bar

受作用域影响的不仅仅是变量,策略和一些属性也有类似于变量的行为。就策略而言,每个add_subdirectory的调用都会创建一个新的作用域,在这个作用域中可以进行策略的改变,而不影响父目录的策略设置。类似地,有些目录属性可以在子目录的CMakeLists.txt文件中设置,而对父目录的属性没有影响。

include

CMake提供的另一种包含其它文件的方法是include命令,它有以下两种形式:

1
2
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命令不要这样做。
  • 在处理include包含的文件时,CMAKE_CURRENT_SOURCE_DIR和CMAKE_CURRENT_BINARY_DIR变量的值不会改变,而在add_subdirectory时,它们会改变。

include命令的第二种形式有一个完全不同的目的。除了上面的第一点之外,其他的都适用于第二种形式。由于CMAKE_CURRENT_SOURCE_DIR的值在调用include时不会改变,CMake提供了一组额外的变量:

  • CMAKE_CURRENT_LIST_DIR:当前正在处理的目录的绝对路径。
  • CMAKE_CURRENT_LIST_FILE:当前正在处理的文件的绝对路径。
  • CMAKE_CURRENT_LIST_LINE:当前正在处理的文件的行号。

上述三个变量对任何被CMake处理的文件都有效,而不仅仅include命令包含的文件。举个例子,顶层CMakeLists.txt如下:

1
2
3
add_subdirectory(subdir)
message("====")
include(subdir/CMakeLists.txt)

subdir/CMakeLists.txt

1
2
3
4
5
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}")

输出如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
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

return

在某些情况下,可能希望提前停止处理当前文件,return命令正好可以用于这个目的。它的唯一作用是结束对当前作用域的处理。如果不是从一个函数内部调用,return将结束对当前文件的处理。

一个文件可能被一个项目的不同部分包含多次。为防止多次处理该文件,只包含一次文件,后续包含可以提前返回:

1
2
3
4
5
6
if(DEFINED cool_stuff_include_guard)
	return()
endif()

set(cool_stuff_include_guard 1)
# ...

在CMake 3.10或更高版本中,有一个专门的命令用于包含保护:

1
include_guard([DIRECTORY|GLOBAL])

如果文件在项目的其他地方已经被处理过,GLOBAL将结束对该文件的处理。DIRECTORY只在当前目录或下级目录检查是否处理过当前文件。