Make--(2)变量与宏

作者 by adtxl / 2021-02-04 / 暂无评论 / 442 个足迹

一个变量名称几乎可以由任何字符组成,包括大部分的标点符号。即使是空格也可以使用,但请不要这样做。事实上,只有:、#和=等字符不允许使用在变量名称中。

请注意,变量名称是区分大小写的。

建议使用的命名习惯:
当变量用来表示用户在命令行上或环境中所自定义的常数时,习惯上全部以大写来命名,单词之间用下划线隔开。
至于只在makefile中出现的内部变量,则全部用小写来编写其名称,单词之间以下划线符号隔开。
内含用户自定义函数的变量以及宏都会以小写来编写其名称,单词之间以破折号(-)隔开。

1.自动变量

自动变量 含义
$@ 工作目标的文件名
$% 档案文件成员(archive member)结构中的文件名元素。
$< 第一个必要条件的文件名
$? 时间戳在工作目标(的时间戳)之后的所有必要条件,并以空格隔开这些必要条件
$^ 所有必要条件的文件名,并以空格隔开这些文件名。这份列表已删掉重复的文件名,因为对大多数的应用而言,比如编译、复制等,并不会用到重复的文件名
$+ 如同$^,代表所有必要条件的文件名,并以空格隔开这些文件名。不过,$+包含重复的文件名。
$* 工作目标的主文件名。一个文件名称是由两部分组成:主文件名(stem)和扩展名(suffix)

2. 变量的类型

一般来说,以变量来代表外部程序是一个不错的注意,这让makefile的用户较容易针对他们特有的环境来改写makefile。
变量可用来保存简单的常数,也可用来存放自定义的命令序列。例如下面的设定可用来汇报尚未使用的磁盘空间:

DF = df
AWK =awk
free-space := $(DF) . | $(AWK) 'NR == 2 {print $$4}'

make的变量有两种类型:经简单扩展的变量(simply expanded variable)以及经递归扩展的变量(recursively expanded variable)。

  • 经简单扩展的变量
    使用:=赋值运算符来定义一个经简单扩展的变量,如

    MAKE_DEPEND := $(CC) -M

    一旦make从makefile中读进该变量的定义语句,赋值运算符的右边部分会立刻被扩展。赋值运算符的右边部分只要出现make变量的引用就会被扩展,而扩展后所产生的的文本则会被存储成该变量的值。上面的变量被扩展之后就变成下面这样

    gcc -M

    然而,如果上面的CC变量尚未定义,则变量被扩展后将变成

    <space>-M

    未被定义的变量将被扩展为空值

  • 经递归扩展的变量
    直接使用=来定义,如

    MAKE_DEPEND = $(CC) -M
    ...
    # 稍后
    CC = gcc

    make在读取变量时只会将=号右边的值赋值给变量,不会做任何的展开的动作,展开的动作会被延迟到该变量被使用的时候才进行。
    这样,当MAKE_DEPEND被使用的时候,即使CC并未定义,MAKE_DEPEND在脚本的值也会被扩展成gcc -M

2.1 其他的赋值类型

  • 条件赋值?=
    此运算只会在变量的值尚不存在的状况下进行变量要求赋值的动作。

  • 附加运算符+=
    此运算符会将文本附加到变量里。当递归变量被使用时,赋值运算符右边部分的值会在"不影响变量中原有值的状况下"被附加到变量里。

3. 宏

变量适合用来存储单行形式的值,可是对于多行形式的值,例如命令脚本,如果我们想在不同的地方执行它,该怎么办?
在GNU make中,我们可以通过define指令以创建“封装命令序列”(canned sequence)的方式来解决此问题,在这里简称为宏。
例如,

define build_target_with_subfeature_country
    @echo build_target_with_subfeature_country......
    $(call build_target,$(filter $(TARGETS),$(subst _, ,$(1))),$(filter $(SUBFEATURE),$(subst _, ,$(1))),$(filter $(COUNTRY),$(subst _, ,$(1))))
endef

define指令后面跟着变量名称以及一个换行符号。变量的主体包含了所有命令序列(每一行命令都必须前置一个Tab符号)直到ended关键字出现为止,endef关键字必须自成一行

在echo命令前置了一个@字符。当执行命令脚本时,前置@字符的命令不会被make输出。因此,当我们运行echo命令本身,只会输出该命令的输出。如果在宏内部使用@前缀,这个前缀字符只会影响使用到它的命令行。然而,如果将这个前缀字符用在宏引用上,则整个宏主体都会被隐藏起来。

4. 何时扩展变量

当make运行时。它会以两个阶段来完成它的工作。第一个阶段,make会读进makefile以及被引入的任何其他makefile。这个时候,其中所定义的变量和规则会被加载进make的内部数据库,而且依存图也会被建立起来。
第二个阶段,make会分析依存图并且判断需要更新的工作目标,然后执行脚本以完成所需要的更新动作。

当make在处理递归变量或define指令的时候,会将变量里的每一行或宏的主体存储起来,包括换行符号,但不会予以扩展。宏定义里的最后一个换行符号并不会被存储成宏的一部分;否则,宏被扩展时make会读进一个额外的换行符号。

当宏被扩展时,make会立即扫描被扩展的文本中是否存在宏或变量的引用,如果存在就予以扩展,如此递归进行下去。如果宏是在命令脚本的语境中被扩展的,则宏主体的每一行都会被插入一个前导的跳格符(Tab)。

下面是用来处理“makefile中的元素何时被扩展”的准则:

  • 对于变量赋值,make会在第一阶段读进该行时,立即扩展赋值运算符左边的部分。
  • =?=的右边部分会被延后到它们被使用的时候扩展,并且在第二阶段进行。
  • :=的右边部分会被立即扩展
  • 如果+=的左边部分原本就被定义成一个简单变量,+=的右边部分就会被立即扩展,否则,它的求值动作会被延后。
  • 对于宏定义(使用define指令),宏的变量名称会被立即扩展,宏的主体会被延后到被使用的时候扩展
  • 对于规则,工作目标和必要条件总是会被立即扩展,然而命令总是会被延后扩展
定义 何时扩展a 何时扩展b
a = b 立即 延后
a ?= b 立即 延后
a := b 立即 立即
a += b 立即 延后或立即
define a
b
...
endef
立即 延后

一个通则是总是先定义变量和宏,然后再使用它们。尤其是,在工作目标或必要条件中使用变量时,就需要在使用变量之前予以定义。

5. 工作目标与模式的专属变量

在makefile运行期间,变量通常只有一个值。对需要经过两个处理阶段的makefile来说是这样没错。第一个阶段,make读进makefile之后,会对变量进行赋值和扩展的动作并建立依存图。第二个阶段,make会分析以及遍历依存图。所以,等到make指令命令脚本的时候,所有的变量都已经处理完毕了。但是如果我们想为特定的规则或模式重新定义变量,该怎么办?

例如,现在我们想要编译一个需要额外命令行选项-DUSE_NEW_MALLOC=1的文件,但是其他的编译项目并不需要这个额外的命令行选项:

gui.o: gui.h
    $(COMPILE.c) -DUSE_NEW_MALLOC=1 $(OUTPUT_OPTION) $<

当规则有改动,或者有许多这样的文件需要处理,就会做很多重复的动作。

为了解决此类问题,make提供了工作目标的专属变量。这些变量的定义会附加在工作目标之上,且只有该工作目标以及相应的任何必要条件被处理的时候,它们才会起作用。通过使用专属变量,可以把前面的例子改写为

gui.o: CPPFLAGS += -DUSE_NEW_MALLOC=1
gui.o: gui.h
    $(COMPILE.c) $(OUTPUT_OPTION) $<

工作目标的专属变量的语法如下所示:

target...: variable = value
target...: variable := value
target...: variable += value
target...: variable ?= value

这类变量的赋值动作会延后到开始处理工作目标的时候进行。所以赋值运算符右边部分的值,可由另一个工作目标的专属变量来设定。同样地,此变量只有在必要条件的处理期间,才会发生作用。

6. 变量来自何处

  • 文件
    在文件中创建,或者通过include指令引入

  • 命令行
    直接在make命令行上定义或重新定义变量:

    $ make CFLAGS=-g CPPFLAGS='-DBSD -DDEBUG'

    在命令行上,每个变量赋值运算符的右边部分必须是一个单独的shell参数。如果变量的值(或变量本身)包含空格,则必须为参数加上括号或是规避空格。
    命令行上变量的赋值结果将会覆盖掉环境变量以及makefile文件中的赋值结果。还可以使用:==赋值运算符将命令行参数设定成简单或递归变量。此外,如果使用override指令,你还可以要求make采用makefile的赋值结果,而不要采用命令行的赋值结果。

  • 环境
    make启动时,所有来自环境的变量都会被自动定义成make的变量。
    这些环境变量的优先级很低,所以makefile文件或命令行参数的赋值结果将会覆盖掉环境变量的值。
    不过,可以使用--environment-overrides-e命令行选项,让环境变量覆盖掉相应的makefile变量。
    当make被递归调用时,有若干来自上层的make变量会通过环境传递给下层的make。默认情况下,只有原先就来自环境的变量会被导出到下层的环境之中。不过,你只要使用export指令就可以让任何变量被导出到环境之中:
    要求将所有变量全部导出,可以这么做:

    export

    请注意,即使这些变量的名称包含了无效的shell变量字符,make也会进行导出的动作。
    使用unexport可以避免环境变量被导出到子进程
    条件赋值运算符与环境变量的交互良好。假如你已经在makefile中定义了输出目录,但是你希望用户能轻易地改写,使用条件赋值运算符将会是最佳解决方案:

    # 假设输出目录为$(PROJECT_DIR)/out
    OUTPUT_DIR ?= $(PROJECT_DIR)/out

    使用下面的较冗长的方式同样可以实现同样的效果

    ifndef OUTPUT_DIR
      # 假设输出目录为$(PROJECT_DIR)/out
      OUTPUT_DIR = $(PROJECT_DIR)/out
    endif

    其中的差别在于,如果变量的值已经设定,那么即使是空值,条件赋值运算符也会跳过赋值的动作,而运算符ifdef和ifndef只会测试“非空值”。因此,我们会使用条件运算符而不会使用ifdef来对OUTPUT_DIR赋值。
    不建议过多的使用环境变量

  • 自动创建
    最后,make会在执行一个规则的命令脚本之前立刻创建自动变量。

7. 条件指令

条件指令的基本语法如下所示:

if-condition
    text if the condition is true
endif

或:

if-condition
    text if the condition is true
else
    text if the condition is false
endif

其中,if-condition可以是以下之一:

ifdef variable-name
ifndef variable-name
ifeq test
ifneq test

进行ifdef/ifndef的测试时,不应该以$()括住variable-name。最后,test可以表示成下面这样

    "a" "b"
    或
    (a,b)

其中,单引号或双引号可以交替使用(但是引号必须成对出现)。

条件处理指令可用在宏定义和命令脚本中,还可以放在makefile的顶层:

libGui.a: $(gui_objects)
        $(AR) $(ARFLAGS) $@ $<
    ifdef RANLIB
        $ (RANLIB) $@
    endif

我喜欢缩排我的条件指令,但是草率的缩排动作可能会导致错误。在前面的例子中,条件指令被缩排了四个空格,而且其所括住的命令具有一个前导的tab符号。如果其所括住的命令并非以一个tab符开头,make将不会把它视为命令;如果条件指令具有一个前导的tab符,make会误以为"条件指令"就是"命令"而将之传递给subshell。

ifeq和ifneq条件指令可用来测试其参数是否相等。条件指令里空格的处理有些微妙。举例来说,如果参数采用小括号的形式,那么逗号之后的空格将会被忽略,除此之外所有其他的空格都是有意义的:

ifeq (a, a)
  # These are equal
endif

ifeq ( b, b )
  # So are these
endif

我比较喜欢使用等效的引号形式:

ifeq "a" "a"
  # These are equal
endif

ifeq 'b' 'b'
  # So are these
endif

即使如此,还是经常会发生"变量扩展后包含了非预期的空格符号"的状况。这可能引发一些问题,因为进行匹配时会将所有字符纳入考虑。为了创建更稳定的makefile,我们会使用strip函数。

ifeq "$(strip $(OPTIONS))" "d"
  COMPILATION_FLAGS += -DDEBUG
endif

8. include指令

用法

include xxx.mk

引入文件与依存关系
当make看到include指令时,会事先对通配符以及变量引用进行扩展的动作,然后试着读进include的文件。如果这个文件存在,则整个处理过程会继续下去;然而,如果这个文件不存在,则make会汇报问题并且继续读取其余的makefile。

当所有的读取动作皆已完成之后,make会从规则数据库中找出任何可用来更新引入文件的规则。如果找到了一个相符的规则,make就会按照正常的步骤来更新工作目标。如果任何一个引入文件被规则更新,make接着会清楚它的内部数据库并且重新读进整个makefile。如果完成读取、更新和重新读取的过程之后,仍有include指令因为文件不存在而执行失败,那么make就会显示错误状态并终止执行。

如果想让make忽略无法加载的引入文件,可以为include指令前置一个破折号

-include xxx.mk 

9. 标准的make变量

除了自动变量,make还会为“自己的状态以及内置规则的定义”提供变量,以便对外提供相关信息:

  • MAKE_VERSION

GNU make的版本号

  • CURDIR

正在执行make进程的当前工作目录。
此变量的值将会是shell变量PWD的值,除非make在运行时用到了--directory(或-C)选项。--directory选项会使得make在搜索任何makefile之前变更到不同的目录。这个选项的完整形式为--directory=directory-name-C directory-name。这样,CURDIR将会包含--include-dir的目录参数。

在makefile文件中,所有路径都应该被设定成相对于makefile所在的目录。需要使用绝对路径时可以通过CURDIR进行访问。

  • MAKEFILE_LIST

make所读进的各个makefile文件的名称所构成的列表,包括默认的makefile以及命令行或include指令所指定的makefile。
在每个makefile被读进make之前,其文件名会被附加到MAKEFILE_LIST变量里。所以,任何一个总是可以查看此列表的最后一项来判断自己的文件名。

  • MAKECMDGOALS

对当前运行的make而言,make运行时命令行上指定了哪些工作目标。此变量并不包含命令行选项或变量的赋值。

  • .VARIABLES

到目前为止,make从各个makefile文件所读进的变量的名称所构成的列表,不含工作目标的专属变量。此变量仅供读取,对它所进行的任何赋值动作都会被忽略掉。

独特见解