CMake(六):使用子目录

2023-05-16

对于简单的项目,将所有内容保存在一个目录中是可以的,但是大多数实际项目倾向于将它们的文件分割到多个目录中。通常可以找到不同的文件类型或分组在各自的目录下的独立模块,或者将属于逻辑功能组的文件放在项目目录层次结构的各自部分中。虽然目录结构可能由开发人员对项目的看法驱动,但项目的结构方式也会影响构建系统。

在任何多目录项目中,两个基本的CMake命令是add_subdirectory()和include()。这些命令将来自另一个文件或目录的内容引入到构建中,允许构建逻辑分布在目录层次结构中,而不是强制所有内容都在最顶层定义。这样做有很多好处:

  • 构建逻辑是本地化的,这意味着构建的特征可以在它们最相关的目录中定义。
  • 构建可以由子组件组成,子组件的定义独立于使用它们的顶级项目。这对于使用git子模块或嵌入第三方源代码树的项目来说尤为重要。
  • 因为目录可以是自包含的,所以仅仅通过选择是否在该目录中添加就可以打开或关闭构建的部分。

add_subdirectory()和include()具有非常不同的特征,因此了解两者的优缺点是很重要的。

6.1 add_subdirectory()

add_subdirectory()命令允许项目将另一个目录带入构建。该目录必须有自己的CMakeLists.txt文件,该文件将在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提供了许多变量来跟踪当前正在处理的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()返回时再次恢复。

一个例子应该有助于演示这种行为:

Top level 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}")

sub_dir/CMakeLists.txt

message("sub_dir: CMAKE_SOURCE_DIR = ${CMAKE_SOURCE_DIR}")
message("sub_dir: CMAKE_BINARY_DIR = ${CMAKE_BINARY_DIR}")
message("sub_dir: CMAKE_CURRENT_SOURCE_DIR = ${CMAKE_CURRENT_SOURCE_DIR}")
message("sub_dir: CMAKE_CURRENT_BINARY_DIR = ${CMAKE_CURRENT_BINARY_DIR}")

image-20220112101913126

(2)范围

调用add_subdirectory()的效果之一是,CMake为处理该目录的CMakeLists.txt文件创建了一个新的作用域。这个新的作用域就像调用作用域的子作用域,有很多效果:

  • 调用作用域中定义的所有变量对子作用域都是可见的,子作用域可以像读取其他变量一样读取它们的值。
  • 在子作用域中创建的任何新变量对调用作用域都不可见。
  • 对子作用域中的变量的任何更改都是该子作用域中的局部变量。即使该变量存在于调用作用域中,调用作用域的变量也保持不变。在子作用域中修改的变量就像一个新变量,在处理离开子作用域中时丢弃该变量。

换句话说,在进入子作用域时,它会接收到那个时间点上在调用作用域中定义的所有变量的副本。对子变量的任何更改都将在子变量的副本上执行,而不会改变调用者的变量。下面的例子最能说明这种行为:

CMakeLists.txt

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}")

sub_dir2/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在子作用域中仍然可见。

​ ④在设置childdvar之前,它在子作用域中仍然是未定义的。

​ ⑤myVar在子范围内被修改。

​ ⑥childVar已被设置在子范围内。

​ ⑦当处理返回到父作用域时,myVar仍然拥有调用add_subdirectory()之前的值。子作用域中对myVar的修改对父作用域是不可见的。

​ ⑧childVar是在子作用域中定义的,因此它对父对象不可见,计算结果为空字符串。

上述变量作用域的行为突出了add_subdirectory()的一个重要特征。它允许添加的目录更改它想要的任何变量,而不影响调用作用域中的变量。这有助于将调用范围与可能不想要的更改隔离开来。

但是,有时候,希望在添加的目录中对变量进行的更改对调用者是可见的。例如,该目录可能负责收集一组源文件名,并将其作为文件列表向上传递给父目录。这就是set()命令中PARENT_SCOPE关键字的作用。当使用PARENT_SCOPE时,所设置的变量是父作用域中的变量,而不是当前作用域中的变量。重要的是,这并不意味着同时在父范围和当前范围中设置变量。稍微修改一下前面的例子,PARENT_SCOPE的效果就很明显了:

CMakeLists.txt

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

sub_dir3/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 ②

​ ①子作用域中的myVar不受set()调用的影响,因为关键字PARENT_SCOPE告诉CMake修改父作用域中的myVar,而不是本地的myVar。

​ ②父类的myVar被子作用域中的set()调用修改了。

因为使用PARENT_SCOPE可以防止任何同名的局部变量被该命令修改,所以如果局部作用域不重用与父变量相同的变量名,则可以减少误导。在上面的例子中,一组更清晰的命令是:

subdir/CMakeLists.txt

set(localVar bar)
set(myVar ${localVar} PARENT_SCOPE)

显然,上面的例子很简单,但是对于实际的项目来说,在最终设置父类的myVar变量之前,可能会有很多命令帮助建立localVar的值。

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

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()需要一个目录,并在该目录中查找CMakeLists.txt文件。传递给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()的文件的位置,而不是包含包含文件的目录。此外,与文件名总是为CMakeLists.txt的add_subdirectory()不同,当使用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()该文件。

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()

与手动编写if-endif代码相比,这更健壮,因为它在内部处理保护变量的名称。该命令还接受一个可选的关键字参数DIRECTORY或GLOBAL,以指定一个不同的范围,在这个范围内检查以前处理过的文件,但在大多数情况下不太可能需要这些关键字。如果没有指定任何参数,则假定变量的作用域,其效果与上面的if-endif代码完全相同。GLOBAL确保如果该文件在项目中的其他地方之前被处理过,则该命令将结束对该文件的处理(即忽略变量作用域)。DIRECTORY仅在当前目录范围内及以下检查以前的处理。
相关代码:https://gitee.com/jiangli01/cmake-learning
更多请关注微信公众号【Hope Hut】:
在这里插入图片描述

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

CMake(六):使用子目录 的相关文章

随机推荐

  • 5g DTU 无线数传终端应用

    DTU无线数传终端TD210全网通2G 3G 4G网络 xff0c 实现串口数据与IP数据的转换 xff0c DTU作为串口数据的无线终端设备 xff0c 可广泛应用于各行各业 DTU无线数传终端TD210应用 第一 xff0c 农业领域
  • 使用L298N电机驱动器和Arduino控制步进电机

    在本文中 xff0c 您将学习如何使用L298N电动机驱动器控制步进电动机 该驱动板通常用于控制直流电动机 xff0c 但它还是控制步进电动机的廉价替代品 xff01 它可以控制大多数步进电机 xff08 例如NEMA 17 xff09 的
  • STM32串口通讯(接收完成一整个数据帧再将数据发送出去)

    STM32串口通信可以分为查询 xff0c 中断 xff0c DMA三种方式进行通讯 xff0c 本文主要就中断的方式进行讲解 采用中断的方式进行通讯时 xff0c 可以使能接受非空中断 xff08 RXNE xff09 xff0c 当接收
  • 树的先序、中序、后序遍历

    遍历分分先序 中序 后序 先序 xff1a 先访问根结点 左结点 右结点 中序 xff1a 先访问左结点 根结点 右结点 后序 xff1a 先访问左结点 右结点 根结点 先序 xff1a ABC 中序 xff1a BAC 后序 xff1a
  • 调整Arduino STM32的串口缓存大小的方法

    通常Arduino中调整串口缓存大小的方法是修改HardwareSerial h中的常量 其实根本无需修改系统core中的定义值 xff0c 只需要在代码最上方添加以下常量定义 xff0c 抢在HardwareSerial h之前定义缓存大
  • C++入门学习(头文件)

    1 C 43 43 中的头文件 1 1 标准库中的头文件 C 43 43 标准库中的一切内容都被放在名字空间std中 xff08 名字空间中的内容对外是不可见的 xff09 xff0c 但是带来了一个新问题 xff0c 无数现有的C 43
  • 用for循环实现delay延时原理

    void Delay10ms unsigned int c 误差 0us unsigned char a b for c gt 0 c c可以不用初始化 xff0c 因为默认传的参数即为初始化 for b 61 38 b gt 0 b fo
  • 解决ROS中运行gazebo出现process has died的情况

    项目场景 xff1a gazebo 1 process has died pid 397 exit code 255 cmd opt ros melodic lib gazebo ros gzserver e ode worlds empt
  • 使用Ventoy制作U盘启动项

    最近在安装linux镜像的时候遇到了使用UltraISO软件制作U盘启动盘无法使用的情况 下面介绍另外一个软件把U盘制作成启动盘Ventoy xff1a 下载地址 xff1a Ventoy 使用方法 xff1a 1 下载好Ventoy xf
  • git 快速入手

    目录 一 xff1a 初次使用git及github 二 xff1a 将github上下载的代码上传到自己的github仓库里 三 xff1a 使用HTTP上传自己写的项目至github git常用指令汇总 使用需求 xff1a 初次接触gi
  • 线程、进程、并发、cpu、gpu的联系

    1 线程和进程的区别 进程 xff1a 一个在内存中运行的应用程序 每个进程都有自己独立的一块内存空间 xff0c 一个进程可以有多个线程 比如在Windows系统中 xff0c 一个运行的xx exe就是一个进程 线程 xff1a 进程中
  • ubuntu系统安装cuda、cudnn、pytorch和libtorch

    1 安装cuda和cudnn 本机安装的cuda版本11 0 2 cudnn版本 v8 0 5 cu11 0 Ubuntu20 04下CUDA cuDNN的详细安装与配置过程 xff08 图文 xff09 ubuntu20 04安装cuda
  • 深度学习语法篇

    一 基本常识 图像的分辨率的通道数 分辨率和通道数是两个不同的概念 分辨率指的是图像的像素数量 xff0c 它反映了图像的清晰度和细节程度 例如 xff0c 一个分辨率为64x64的图像意味着它有64个像素行和64个像素列 xff0c 总共
  • 第二讲:线性表示及坐标

    第二讲 xff1a 线性表示及坐标 一 线性表示 1 线性表示定义 xff1a 设 是线性空间V中的向量 xff0c 若存在V中一组向量 1 xff0c 2 xff0c xff0c n xff0c 及一组数x1 xff0c x2 xff0c
  • 快速理解掌握指针

    p gt next 61 q 像这种语句 xff0c 表示改变了p后面的连接关系 p 61 q gt next 这类语句 xff0c 没改变连接关系 xff0c 只是赋值而已 解读代码中指针所代表的节点之间的前后连接关系 只要输出该指针对应
  • 第三讲:子空间

    第三讲 xff1a 子空间 一 子空间定义 1 子空间 xff1a 设V是数域F上的线性空间 xff0c W是V的子集 xff0c 若对W中的任意元素 xff0c 及数K F xff0c 按V中的加法和数乘有 xff1a 1 xff09 4
  • Qt多线程之线程之间的传递数据

    hpp span class token macro property span class token directive keyword ifndef span MAINWINDOW H span span class token ma
  • 循环队列c代码实现

    循环队列的抽象数据类型 ADT 队列 xff08 Queue xff09 Data 同线性表 元素具有相同的类型 xff0c 相邻元素具有前驱和后继的关系 Operator span class token function InitQue
  • CMake(四):变量

    前面展示了如何定义基本目标和生成构建输出 就其本身而言 xff0c 这已经很有用了 xff0c 但CMake还附带了一大堆其他特性 xff0c 这些特性带来了极大的灵活性和便利性 本章涵盖了CMake最基本的部分之一 xff0c 即变量的使
  • CMake(六):使用子目录

    对于简单的项目 xff0c 将所有内容保存在一个目录中是可以的 xff0c 但是大多数实际项目倾向于将它们的文件分割到多个目录中 通常可以找到不同的文件类型或分组在各自的目录下的独立模块 xff0c 或者将属于逻辑功能组的文件放在项目目录层