GNU Make学习总结(二)

Intro

通过上一篇的内容,已经可以写出比较简洁的Makefile了。这一篇主要是详细介绍了Makefile中变量、函数及命令的使用。

变量

在前面的Makefile中,其实已经用到了很多变量,这一章节对变量的不同形式及用法进行了总结。

首先,在Makefile中变量由一个前导的$加上一个字符或者圆括号括起来的字符串表示,名称区分大小写。习惯上用全部大写字母来表示常量,用全部小写字母来表示变量,单词之间用下划线隔开。

变量赋值

Make中的变量赋值有如下四种方式

1
2
3
4
CC = gcc
CC := gcc
CC ?= gcc
CC += gcc

第一种方式的赋值和我们平时在语言中用的赋值有所不同,它并不会直接将右边的内容赋值给变量,而是在使用到这个变量的时候再进行赋值,这导致如果赋给该变量的是一个变值的话,那在不同时刻使用该变量将得到不同的值。
第二种方式和我们平时使用的赋值相同,当运行到这一行的时候,make就会将右值的内容复制到左边。
第三种方式称作条件赋值,只当该变量不存在的时候才进行赋值,使用这个功能可以和环境变量有很好的交互。
第四种赋值方式也比较容易理解,和c++中的字符串操作一样,将右值附加到原变量的后面。

除了用变量来储存单行的值之外,Makefile中还可以用define来定义一个宏保存一组操作,方便在不同的地方使用宏调用这组操作,下面定义了一个简单的宏

1
2
3
4
define newdir
mkdir tmp
cd tmp
endef

何时扩展变量

由于Makefile中的变量并不是都是即时扩展的,知道何时扩展变量就变的十分重要,因为这决定了这个变量在使用时的值。

make会分为两个阶段来完成工作,第一阶段读进makefile以及include进来的makefile,其中定义的变量和规则都会被加进make的内部数据库,并建立依存图,第二阶段会根据依存图判断需要进行哪些更新操作。
下面是用来处理何时扩展变量的几条准则:

  1. 在变量左侧的部分,会在第一阶段立即扩展。(变量左侧也可以是一个$x形式的变量,例如x=y,$(x)=z就等于y=z)
  2. =和?=都在使用的时候,也就是第二阶段才扩展。
  3. :=的右边的值会在第一阶段立即扩展。
  4. +=左边如果是简单变量,右边就会立即扩展,否则会延到第二阶段扩展。(所谓简单变量就是指用:=赋值的变量,相对的,递归变量指用=赋值的变量)
  5. 宏定义的变量名会被立即扩展,宏的主体会被延后到使用时扩展。
  6. 对于每条规则,工作目标和必要条件都是立即扩展,命令总是延后扩展。
    可以总结成下表
    1
    2
    3
    4
    5
    6
    7
    8
    定义 		扩展a	扩展b
    a=b 立即 延后
    a?=b 立即 延后
    a:=b 立即 立即
    a+=b 立即 立即或延后
    define a 立即 延后
    b..
    endef

专属变量

有时候一个变量对于多数规则都适用,但在某条规则下需要对这个变量进行扩展或者改变,这时就可以用到条件专属变量。比如说我们在编译a.o时要DEFINE DEBUG,可以这样写

1
2
a.o: CFLAGS += -DDEBUG
a.o: a.h

这样的话在编译a.o时CFLAGS会在原来的CFLAGS后面加上-DDEBUG,编译完a.o后就还原了。
扩展到不同的赋值方式,共有以下四种格式,注意这些赋值操作都会延后到开始处理工作目标的时候进行,变量的值也只在处理该工作目标的时候有效。

1
2
3
4
target...: v = value
target...: v := value
target...: v += value
target...: v ?= value

include

在上一章生成自动依赖的时候用到了include指令,这里对include指令详细介绍。
在make读到include指令的时候,如果include文件存在,则会读取文件内容并继续执行下去,若不存在,会在汇报问题后继续读取剩下的makefile。读取完成后,make会从规则库中找出任何可用来更新引入文件的规则,如果找到了就执行更新操作,如果一个引入文件被规则更新,则make会清除内部数据库并且重新读进整个makefile,如果这之后include的文件仍然不存在,则报错终止执行。
使用-include或者sinclude指令来代替include可以让make忽略无法加载的引入文件。

条件指令

条件指令也就是if-else指令,可以让make按条件选择执行哪一部分,有以下两种形式

1
2
3
4
5
6
7
8
9
if-condition
...
endif

if-condition
...
else
...
endif

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

1
2
3
4
ifdef variable-name
ifndef variable-name
ifeq test
ifneq test

注意的是variable-name不需要加前导$符,而test可以表示成”a” “b”或 (a,b)。

标准make变量

除了自动变量之外,make会为自己的状态和内置规则的定义提供变量。
为内置规则提供的变量通过make -p可以看到,我们可以通过直接修改这些参数来改变内置规则参数,比如CFLAGS修改编译C时的参数,CC修改C编译器等等。
而为表示Makefile自己状态提供的变量主要有以下几个。

1
2
3
4
5
MAKE_VERSION	GNU Make版本号
CURDIR 正在执行make进程的工作目录
MAKEFILE_LIST make所读进的各个makefile文件名称构成的列表,最后一个是自身文件名
MAKECMDGOALS make命令指定了哪些工作目标
.VARIABLES make从各个makefile文件读进的名称所构成的列表

比如,我们在clean的时候是不执行include指令的,可以通过以下几行来完成

1
2
3
ifneq "$(MAKECMDGOALS)" "clean"
-include ...
endif

函数

Make中包含了很多十分有用的内置函数,用户也可以根据自己的需求自定义函数,本章对这两部分分别介绍。

内置函数

列举内置函数就跟列举API一样无聊,不过没办法,内置函数大多是要掌握的,以下只做简单的介绍,具体怎么用试一下就记住了。
内置函数在语法上基本都是如同$(func-name arg1[, argn..])的形式,不同参数之间用逗号隔开,需要注意的是,除了第一个参数,逗号后的空格都会被保留下来,如果不小心多个空格会引起各种问题。另外,对于处理单词或文件名的函数,参数经常是一串空格隔开的单词,函数对每个单词进行匹配或处理。

杂项函数

这些函数一般是为其它的函数提供辅助作用的。
$(sort list)
对list参数排序并去重,此外,它还会删除前导及结尾的空格。虽然这个函数叫sort,但更多的时候是用它来去重。

$(shell command)
将command传递给subshell执行,并将标准输出值作为结果返回,其中换行符都会被替换为空格。

$(strip text)
去掉前导和后面空格,并将内部连续空格转化为单一空格。

$(origin variable)
返回变量的来源,也可以测试变量是否定义,返回值有以下几个:
undefined 未定义
default 来自make内置数据库
environment 来自环境变量
environment override 来自环境变量,而且使用了–environment-overrides指令
file 来自makefile
command line 来自命令行
override 来自override指令
automatic make所定义的自动变量
这里override变量是指在变量赋值前加override,使得该赋值比命令行赋值优先级高,而–environment-overrides选项是使默认环境变量比makefile中环境变量赋值优先级高。

$(warning text)
打印警告信息。

字符串函数

$(filter pattern ...,text)
$(filter-out pattern ...,text)
filter将text视为一系列空格隔开的单词,返回与pattern符合的单词,pattern中可以使用模式通配符。而filter-out找的是filter的补集。

$(findstring string...,text)
用处不是太大,在文本中找一个字符串,还不能使用通配符。。

$(subst search-string,replace-string,text)
$(patsubst search-string,replace-string,text)
$(variable:search-string=replace-string)
subst将text中出现search-string的地方全部替换成replace-string,而patsubst与subst的不同的是可以使用一个模式通配符%。第三个函数叫做替换引用,主要是用来替换文件后缀的,与subst不同的是,search-string一定出现在文件结尾。
以下尝试使用这三个函数替换文件后缀,以理解这几个函数之间的区别

1
2
3
4
5
6
7
8
9
.PHONY: test
sc = a.c b.c c.c.c
to1 = $(subst .c,.o,$(sc));
to2 = $(patsubst %.c,%.o,$(sc));
to3 = $(sc:.c=.o);
test:
@echo $(to1)
@echo $(to2)
@echo $(to3)

输出结果如下,可以发现subst函数对于文件名中也出现.c的单词会更改文件名,另外两个函数的效果相同,都能替换文件后缀。

1
2
3
a.o b.o c.o.o
a.o b.o c.c.o
a.o b.o c.c.o

$(words text)
$(words n,text)
$(firstword text)
$(wordlist start,end,text)
这几个函数中,text都是用空格隔开的单词列表。第一个函数返回单词的个数,第二个函数返回第n个单词,第三个函数返回第一个单词,第四个返回从start到end的单词。

文件名函数

Makefile中很多时候都是在对文件名进行处理,这些函数经常会用一个变量包含一组文件名,中间用空格隔开,下面的函数中变量后加…都表示这个变量是一个由空格隔开的字符串。

$(wildcard pattern...)
$(dir list...)
$(notdir name...)
第一个函数比较常用,可以使用通配符来匹配一组文件,这些文件名之间用空格隔开作为函数返回值,比如$(wildcard *.cpp)可以获得当前目录下所有的cpp文件。第二个函数返回list中每个文件的目录部分,而第三个函数会返回文件名部分。

$(suffix name...)
$(basename name...)
$(addsuffix suffix,name...)
$(addprefix prefix,name...)
$(join prefix-list,suffix-list)
suffix函数返回name列表中的所有后缀,basename返回不带后缀的部分,而addsuffix和addprefix顾名思义,就是为name列表中所有单词添加后缀或前缀。而join就是将prefix-list和suffix-list中的单词按顺序组合,可用来重建被dir和notdir分解的列表。

下面举几个例子来看一下文件名函数的使用方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#当前目录下以.c和.h文件组成的变量
sources := $(wildcard *.c *.h)
#判断主目录下是否存在.emacs文件
dot-eamcs-exists := $(wildcard ~/.emacs)
#显示包含C文件的子目录(使用find查找,sort去重)
source-dirs := $(sort $(dir $(shell find . -name '*.c')))
#返回$JAVAFILE变量(a.java的形式之间用空格隔开组成)中的JAVA类名
class-name := $(notdir $(subst .java,,$(JAVAFILE)))
#测试$files中是否所有单词具有相同的后缀(判断去重后是否只有一种后缀)
same-suffix = $(filter 1,$(words $(sort $(suffix $files))))
#从Java文件名转换成class名(包含包名,a/b/c.java=>a.b.c)
ftc-name := $(subst /,.,$(basename $(JAVAFILE)))
#计算PATH环境变量中所有程序的数量。最后三行都是处理特殊情况的,处理完后使用空格替代冒号作为分隔符并去重,再在每个目录后加/*并作为wildcard的参数,最后用words统计wildcard匹配到的程序数目。
program-nums = $(words \
$(wildcard \
$(addsuffix /*, \
$(sort \
$(subst :, , \
$(subst ::,:.:, \
$(patsubst :%,.:%, \
$(patsubst %:,%:.,$(PATH)))))))))

流程控制

$(error text)
输出错误信息,并在这之后结束make程序。

$(if condition,then-part,else-part)
与前面提到的条件指令不同,这里的condition可以是一个函数或者表达式,then-part和else-part也可以是宏或者函数,会根据condition返回的结果是否为空决定执行哪一部分,这里的为空指的是不包含任何字符(包括空格)。
下面这个例子用来判断make是否在3.81版本下执行

1
$(if $(filter $(MAKE_VERSION),3.81),,$(error version not 3.8.1))

$(foreach variable,list,body)
对于list中的每个变量variable,执行body部分的内容。每次body部分的内容会被以空格为分隔符累计起来,最后作为返回值。

自定义函数

其实make中定义函数的方式和定义变量的方式几乎一样,有单行定义和宏定义两种方式,单行定义一般用来替代一系列内置命令组合,而对于复杂一点的过程,就要用宏来定义。
make中函数的调用方法如下
$(call func-name[, param1...])
而在函数体中,则是用$0来表示func-name,$1~$n来表示传递的参数,这点和shell的传值是一样的。
下面通过一个实例来展示怎样自定义函数,这个makefile示例如何去写一个简单的调试追踪函数,样例在命令中调用了函数b,而b又调用了a。另外一个file-num则是一个单行定义的函数,返回src下指定格式的文件数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
debug_trace = 1
echo-args = $(foreach a,1 2 3,'$($a)'))
debug-enter = $(if $(debug_trace),$(warning Entering $0($(echo-args))))
debug-leave = $(if $(debug_trace),$(warning Leaving $0))
file-num = $(words $(wildcard src/*.$1))
define a
$(debug-enter)
@echo $1 $2 $3
@echo $(call file-num,c)
$(debug-leave)
endef
define b
$(debug-enter)
$(call a,$1,$2,hello)
$(debug-leave)
endef

.PHONY: test
test:
$(call b,123,$(CC))

make输出如下,通过输出可以看到这两个函数的运行过程。

1
2
3
4
5
6
Makefile:21: Entering b('123' 'cc' ''))
Makefile:21: Entering a('123' 'cc' 'hello'))
Makefile:21: Leaving a
Makefile:21: Leaving b
123 cc hello
3

接下来介绍eval函数,eval函数的用途是将文本直接放入make解析器,首先make会扫描eval参数中是否有变量需要进行替换,如果有的话先替换变量,接下来make会再解析文本并进行求值操作。
eval函数的理解有些困难,下面通过一个实例来说明。

1
2
3
4
5
6
7
8
9
10
11
12
.PHONY: test
test:
@echo $(obj)

src = tt_a.c tt_b.c tt_c.c
define func
head = $(patsubst %.c,%.h,$1)
obj = $(head:.h=.o)
$(obj):$(head)
endef

$(call func, $(src))

这个makefile的功能比较容易看懂,就是传进去.c文件,分别替换后缀成.h和.o,再建立依赖关系。但是悲剧的是,这个makefile是会报错的,原因是make不允许在顶层将一个宏扩展成多行(只有在命令的地方可以)。解决这个问题需要用到eval函数,将函数调用那一行改成

1
$(eval $(call func,$(src)))

这次可以通过编译了,但是仍然悲剧的是,打印出的obj是空的,通过make –print-data-base也看不到我们定义的规则。这是因为在eval第一遍读取宏的时候,会对变量进行替换,这些替换只依赖于在调用这个宏之前就已经有的变量以及函数传递的变量,而在调用宏之前,head是空的,从而使$(obj)和$(head)都被替换为空。解决的方法是使用$$来表示变量,这样eval在第一遍读取的时候只会将$$变成$,接下来eval会进行第二遍读取并执行整个函数,再处理之前的变量。最后,经过改动的makefile如下

1
2
3
4
5
6
7
8
9
10
11
12
.PHONY: test
test:
@echo $(obj)

src = tt_a.c tt_b.c tt_c.c
define func
head = $(patsubst %.c,%.h,$1)
obj = $$(head:.h=.o)
$$(obj):$$(head)
endef

$(eval $(call func,$(src)))

make和make --print-data-base | grep tt_ 结果如下,可以看到obj变量内容正确,也正确生成了依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# make
tt_a.o tt_b.o tt_c.o
# make --print-data-base|grep tt_
tt_a.o tt_b.o tt_c.o
head = tt_a.h tt_b.h tt_c.h
src = tt_a.c tt_b.c tt_c.c
tt_a.o: tt_a.h tt_b.h tt_c.h
tt_c.h:
tt_b.o: tt_a.h tt_b.h tt_c.h
tt_a.h:
tt_c.o: tt_a.h tt_b.h tt_c.h
tt_b.h:
```


## **命令**
命令是每条规则的三种组成元素之一,它的实质就是一个单行的shell命令,对于大多数命令,make会将它传给subshell去执行,对于某些不会影响make程序行为的shell命令,make会避免去fork/exec,直接在make中执行。

make默认使用的是/bin/sh,用户也可以通过修改SHELL变量来更改使用的shell。为了编写具有可移植性的makefile,可以使用/usr/bin/bash,bash是GNU/Linux采用的标准shell,可以在大多数系统上运行。

在makefile中所有以TAB开头的文本行都被认为是命令,但是像注释以及条件处理命令即使以TAB开头,也会被make识别出来并正确处理。而空行则会被Makefile直接忽略掉。

### **长命令**
需要注意的是,make是以行为单位将命令输送到shell的,如果一个命令超过一行,需要进行处理,下面举一个简单的例子
```makefile
.PHONY: test
test:
cd src
ls

该makefile试图进入src目录并显示src下的文件列表(当然ls src是OK的,这里只是为了举例),但运行后会发现ls显示的还只是make所在目录下的文件,这是因为两行命令被传给了两个不同的subshell,从而变的不具有关联性。如果想要两条命令在一个subshell中执行,我们可以使用反斜杠符将命令连成一行并在命令间加上分隔符。下面两种写法都可以实现,但实际上是有所区别的,在后面的错误处理中会说这个问题。

1
2
3
4
5
6
7
8
9
10
# way 1
.PHONY: test
test:
cd src && \
ls
# way 2
.PHONY: test
test:
cd src; \
ls

命令修饰符

@
Makefile默认会输出命令本身,而在命令前加上@可以禁止make的这种行为。加上@的好处是使make输出较容易阅读,坏处是使命令调试变的困难。建议使用一个含@的变量,并用在命令上,通过修改这个变量就可以决定程序的行为

1
2
3
QUIET = @
test:
@(QUIET) shell script..

-
指示make忽视改行发生的错误,一般当make遇到一个命令返回错误时,会停止make脚本的执行。但如果命令前加上了破折号,make就会忽略错误并继续执行。

+
要求make执行命令,即使是–just-print或-n命令来执行make,在编写递归makefile时会用到这个功能。

错误处理

make每执行一条命令就会返回一个状态码,值为零代表命令执行成功,如果某一行命令返回非零时,make就会停止执行。这种情况下可以用破折号前缀或者–keep going命令让make继续执行下去,但是并不建议这么做,除非能保证这一行命令的错误无伤大雅。

在之前长命令的例子中,分别使用&&;作为连接符,当src存在时执行结果是没有区别的,但如果这个目录不存在呢,我们将src改成srcs(不存在的目录)后看一下输出

1
2
3
4
5
6
7
8
9
10
#使用&&做连接符
cd srcs && \
ls
/bin/sh: 1: cd: can't cd to srcs
make: *** [test] Error 2
#使用;作连接符
cd srcs; \
ls
/bin/sh: 1: cd: can't cd to srcs
include Makefile src

可以看到,如果&&作连接符,其中某一条命令的错误会导致整个make的停止,而使用分号作连接符则不会因为中间某条命令的错误而使make停止。这里建议使用&&作连接符,在执行每一条命令之前都保证前一条命令执行成功。

另一种避免错误的方式就是使用程序内置的错误处理机制,比如使用rm -f来替代rm,这样在删除不存在的文件时就不会报错,与此相似的还有mkdir -p等。

文章目录
  1. 1. Intro
  2. 2. 变量
    1. 2.1. 变量赋值
    2. 2.2. 何时扩展变量
    3. 2.3. 专属变量
    4. 2.4. include
    5. 2.5. 条件指令
    6. 2.6. 标准make变量
  3. 3. 函数
    1. 3.1. 内置函数
      1. 3.1.1. 杂项函数
      2. 3.1.2. 字符串函数
      3. 3.1.3. 文件名函数
      4. 3.1.4. 流程控制
    2. 3.2. 自定义函数
    3. 3.3. 命令修饰符
    4. 3.4. 错误处理