《跟我一起写makefile》学习笔记

86 阅读10分钟

makefile概述

makefile主要包含了五个东西:显式规则、隐式规则、变量定义、文件指示和注释

  1. 显式规则:如何生成一个或多的目标文件,这是由makefile的书写者明显指出,要生成的文件,文件的依赖文件,生成的命令。
  2. 隐式规则makefile拥有自动推导的功能,所以隐式的规则可以让我们比较粗糙地简略地书写makefile,这是由make所支持的。
  3. 变量定义:在makefile中定义一系列的变量,变量一般都是字符串,类似于C语言中的宏,当makefile被执行时,其中的变量都会扩展到相应的引用位置上。
  4. 文件指示:包含三个部分,一是在一个makefile中引用另一个makefile,这个类似于C语言中的宏;二是指根据某些情况指定makefile中的有效部分,就像C语言中的预编译#if一样;三是定义一个多行的命令。
  5. 注释makefile中只有行注释,和unixshell脚本一样,其注释是用“#”字符,这个类似于C/C++中的“//”一样;若要使用“#”,使用反斜杠对其进行转义。

makefile文件命名

在默认的情况下,make命令会在当前目录下按顺序查找文件名为GNUmakefilemakefileMakefile的文件,查找到后解释这个文件。在这些文件名中,通常使用Makefile这个文件名,但基本上来说,大多数make都支持makefileMakefile这两种默认文件名。当然,也可以使用其它自定义的文件名来编写makefile,在使用make命令时用-f--file来指定自定义文件名的makefile文件。

引用makefile

makefile文件中可以使用include关键字把其它makefile文件包含进来,类似于C语言的#include,被包含的文件会直接在包含位置进行展开,语法如下:

include <filename>

其中,filename可以是当前操作系统Shell的文件模式(可以包含路径和通配符)。在include关键字前可以有空字符(Space),但不能是制表符(Tab)。include使用举例如下:

bar = e.mk f.mk

include foo.make *.mk $(bar)
# 等价于
include foo.make a.mk b.mk c.mk e.mk f.mk

make命令执行时,会先查找include所指出的其它makefile,并把其内容在包含位置展开,类似于C/C++,如果没有指定绝对路径或是相对路径,make会在当前目录下查找;若当前目录下没有找到,那make还会在如下的几个目录下查找:

  1. make命令指定了-I--include-dir参数,则make就会在该参数指定的目录下寻找
  2. 若目录<prefix>/include(一般是/usr/local/bin/usr/include)存在,则make也会进行查找

若文件没有找到,则make会生成一条警告信息,但不会马上出现致命错误,它会继续载入其它文件,一旦完成makefile的读取,make会重试这些没有找到,或是不能读取的文件,若依然不行,make才会产生一条致命信息;如果想让make忽略无法读取的文件以便继续执行,可以在include前加一个减号“-”。例如:

-include <filename>

环境变量MAKEFILES

若当前环境中定义了环境变量MAKEFILES,那么make会把这个环境变量的值做一个类似include的动作。这个环境变量中的值是其它makefile,以空格分隔。但与include不同的是,从这个环境变量中引入的makefile的“目标”不会起作用,若该变量定义的文件发现错误,make也会忽略它。

建议不要使用这个环境变量,这个变量一旦定义,所有的makefile都会受其影响;若编写的makefile出现异常,可以首先查看当前环境中是否定义了这个变量。

make的工作方式

GNUmake执行时的步骤如下:

  1. 读入所有的makefile文件
  2. 读入被include的其它makefile文件
  3. 初始化文件中的变量
  4. 推导隐式规则,并分析所有规则
  5. 为所有的目标文件创建依赖关系链
  6. 根据依赖关系,决定哪些目标要重新生成
  7. 执行生成命令

在以上步骤中,1-5为第一个阶段,6-7为第二个阶段。在第一阶段中,若定义的变量被使用了,那make会把其展开在使用的位置,但是make并不会完全马上展开,make使用的是拖延战术,若变量出现在依赖关系的规则中,那仅当这条依赖被决定要使用时,才会在其内部展开。

makefile的书写规则

makefile的书写规则分为两个部分,一是依赖关系,二是生成目标的方法。在makefile中,规则的顺序的非常重要的,因为makefile只应该有一个最终目标,其它的目标都是这个目标所依赖的,所以要向make显式地指明你的最终目标。

一般来说,在makefile文件中编写的第一条规则中的目标将会被确立为最终目标,若第一条规则中的目标有很多个,那第一个目标会成为最终的目标。

规则举例

foo.o : foo.c defs.h	# foo 模块
	cc - c -g foo.c

在这个例子中,foo.o是我们的目标,foo.cdefs.h是目标所依赖的源文件,cc -c -g foo.c是所要执行的命令。这个例子中的规则指明了两件事:

  1. 文件的依赖关系foo.o依赖于foo.cdefs.h文件,若foo.cdefs.h的文件日期要比foo.o的文件要新,或是foo.o不存在,则依赖关系发生。
  2. 目标的生成指令:如果生成(或更新)foo.c文件。即使用cc命令编译foo.c文件,这个规则指明了如何生成foo.o这个文件

规则的语法

targets : prerequisites
	command
	...
# 或
targets : prerequisites ; command
	command
	...

其中,targets是文件名,以空格分隔,可以使用通配符;一般来说目标基本上是一个文件,也可能是多个。command是命令行,若单独一行则需以制表符缩进,也可以和prerequisites在一行,但需以分号作为分隔。prerequisites是目标所依赖的文件(或依赖目标),若其中某个文件要比目标文件要新,那目标就被认为是“过时的”,需要重新生成该目标。

若一行命令过于长时,可以使用反斜杠(\)作为换行符。

在规则中使用通配符

make支持三个通配符:“*”,“?”,“[...]”。除此之外,“~”在文件名中也有特殊用途,例如~/test表示当前用户的HOME目录下的test目录。通配符代替了一系列的文件,例如*.c表示所有后缀为.c的文件,若文件名中存在通配符,可以使用反斜杠(\)对其进行转义。

文件搜寻

VPATH变量

make需要去查找文件的依赖关系时,可以在文件前加上路径,但一个最好的办法是把一个路径告诉make,然后让make自动去查找。makefile文件中的特殊变量VPATH就是为了完成这个功能的,若没有指定这个变量,make只会在当前目录下去查找依赖文件和目标文件。若定义了这个变量,make就会在当前目录下找不到的情况下,到该变量指定的目录下去查找文件。

VPATH = src:../headers

在上面的例子中所指定的两个目录,make会按从左到右的顺序进行搜索,目录以冒号分隔。

当前目录是make优先搜索的路径

vpath关键字

另一个设置文件搜索路径的方法是使用makevpath关键字,这是一个make的关键字,和VPATH变量使用类似,但它更为灵活,它可以指定不同的文件在不同的搜索目录中,使用方法有三种:

# 1
vpath <pattern> <directories>	# 为符合模式<pattern>的文件指定搜索目录<directories>

# 2
vpath <pattern>	# 清除符合模式<pattern>的文件的搜索目录

# 3
vpath # 清除所有已被设置好了的文件搜索目录

其中,<pattern>指定了要搜索的文件集,使用时需要包含“%”字符,“%”的意思是匹配零或若干字符,例如,“%.h”表示所有以“.h”结尾的文件;<directories>则指定了<pattern>的文件集要搜索的目录,例如:

vpath %.h ../headers

此例要求make../headers目录下搜索所有以.h结尾的文件(make优先查找某文件的当前目录,若没有则查找此例中指定的目录)。还可以连续地使用vpath语句,用来指定不同的搜索策略;若连续的vpath语句中出现了相同的<pattern>,或是被重复了的<pattern>,那make会按照vpath语句的先后顺序来执行搜索,例如:

vpath %.c foo
vpath % blish
vpath %.c bar

其表示.c结尾的文件,先搜索foo目录,然后搜索blish目录,最后搜索bar目录;而下面的例子表示.c结尾的文件,先搜索foo目录,然后搜索bar目录,最后搜索blish目录:

vpath %.c foo:bar
vpath % blish

伪目标

makefile中可以使用.PHONY来显式地指明一个目标是伪目标,向make说明,不管是否存在这个文件,这个目标就是伪目标

.PHONY : clean

只要有这个声明,不管clean文件是否存在,要执行clean这个目标,必须显式地使用下面的命令:

make clean

伪目标一般没有依赖文件,也可以为伪目标指定所依赖的文件,伪目标也可以作为“默认目标”,只要将其放在第一个;若想要使用一个简单的make命令生成若干个可执行文件,可以利用“伪目标”这个特性:

all :prog1 prog2 prog3
.PHONY : all

prog1 : prog1.o utils.o
	cc -o prog1 prog1.o utils.o
prog2 : prog2.o
	cc -o prog2 prog2.o
prog3 : prog3.o sort.o utils.o
	cc -o prog3 prog3.o sort.o utils.o

在上面的例子中声明了一个“all”的伪目标,其依赖于其它三个目标,由于伪目标的特性是总是被执行的,所以其依赖的那三个目标就总是不如“all”这个目标新,所以其它三个目标的规则总是会被决议,也就达到了一次性生成多个目标的目的;从上面例子中可以看出目标也可以成为依赖,伪目标也可以成为依赖,比如下面的例子:

.PHONY : cleanall cleanobj cleandiff

cleanall : cleanobj cleandiff
	rm program
cleanobj :
	rm *.o
cleandiff :
	rm *.diff

可以通过输入make cleanallmake cleanobj以及make cleandiff命令来达到清除不同种类文件的目的。

多目标

makefile的规则中的目标可以不止一个,其支持多目标,有可能多个目标同时依赖于一个文件,且生成的命令类似,可以把多个生成规则合并起来,使用一个自动化变量$@来执行这个操作,这个变量表示这目前规则中所有的目标的集合:

bigoutput littleoutput : text.g
	generate text.g -$(subst output,,$@) > $@
# 等价于
bigoutput : text.g
	generate text.g -big > bigoutput
littleoutput : text.g
	generate text.g -little > littleoutput

其中,-$(subst output,,$@)中的“$”表示执行一个makefile函数,函数名为subst,后面的是参数;$@表示目标的集合,依次取出目标,并执行命令。

静态模式

静态模式可以更加容易地定义多目标的规则,让规则变得更加的灵活和弹性:

<targets...> : <target-pattern> : <prereq-patterns...>
	<command>
	...

其中,targets定义了一系列的目标文件,可以包含通配符,是目标的一个集合;target-pattern是指明了targets的模式,也就是目标集模式;prereq-patterns是目标的依赖模式,对target-pattern形成的模式再进行一次依赖目标的定义。例如下面这个例子:

objects = foo.o bar.o
all: $(objects)
$(objects) : %.o : %.c
	$(cc) -c $(CFLAGS) $< -o $@

这个例子中,目标是从$objects中获取,%.o表示目标是以.o结尾的,即foo.obar.o,也就是变量$objects集合的模式,而依赖模式%.c则取模式%.o%,并为其加上.c后缀,即foo.cbar.c,也就是依赖目标;而$<$@则是自动化变量,$<表示所有的依赖目标集(即foo.cbar.c),$@表示目标集(即foo.obar.o)。上面的例子等价于下面的规则:

foo.o : foo.c
	$(cc) -c $(CFLAGS) foo.c -o foo.o
bar.o : bar.c
	$(cc) -c $(CFLAGS) bar.c -o bar.o

自动生成依赖性

C/C++编译器可以通过指定-M参数自动生成依赖关系(在GNUC/C++编译器下需要使用-MM参数,不然会将一些标准库的头文件包含进来)。在makefile中也可以一些迂回的手段来实现这一功能,可以通过为每个.c文件生成一个.d文件,.d文件用来保存.c文件的依赖关系。例如下面的规则:

%.d: %.c 
 @set -e; rm -f $@; \ 
 $(CC) -M $(CPPFLAGS) $< > $@.$$$$; \ 
 sed 's,\($*\)\.o[ :]*,\1.o $@ : ,g' < $@.$$$$ > $@; \ 
 rm -f $@.$$$$

在这个规则中,所有的.d依赖于.c文件,rm -f $@用来删除所有的目标文件,即.d文件,之后就是通过编译器来生成.c文件的依赖文件,$$$$表示一个随机编号,此处的生成的依赖文件并非最终的依赖文件,所以通过sed将上一步生成的带随机编号的依赖文件中的内容进行一个替换后写入到最终的依赖文件中,即$@所指代的目标中,最后将带随机编号的临时依赖文件删除。

总之,这个规则主要是将依赖关系:

main.o : main.c defs.h

转化成

main.o main.d : main.c defs.h

执行了这个规则后,就要将这些规则放入到主要的makefile文件,通过使用之前的include命令来引入这些规则:

sources = foo.c bar.c
include $(sources:.c=.d)

$(sources:.c=.d)这个会将.c字串替换成.d,下面的include命令最终就变成了include foo.d bar.d

在makefile书写命令

显式命令

make会将其要执行的命令行(command)在命令执行前输出到屏幕上,若不想显示命令,可以在命令行前使用@字符。比如下面这个例子:

@echo 正在编译XXX模块...

make执行时,只会输出echo命令之后的文本;若没有添加@字符,则会输出两行内容:

echo 正在编译 XXX 模块...
正在编译 XXX 模块...

也可以使用make的参数-s--slient来全面禁止命令的显示;若只是想要显示命令,可以使用make的参数-n--just-print,这个功能可以帮助调试makefile,理解和知晓书写的命令是如何执行的,是以何顺序执行的。

命令执行

make执行规则的命令时,会一条一条的依次执行,若想要上一条命令的结果应用于下一条命令时,不能将两条命令写在两行上,要将命令写在一行,以分号(;)分隔。比如:

# 1
exec :
	cd /home/eric
	pwd
# 2
exec :
	cd /home/eric; pwd

执行#1时,cd命令对pwd命令不生效,只会打印当前makefile文件所在的目录;执行#2时,cd命令生效,pwd正确打印。

命令出错

每当命令运行完后,make会检测每个命令的返回码,若命令返回成功,则make会执行下一条命令,所有命令都成功返回后,这个规则就算成功完成了。若某个命令出错,则make会终止执行,这将导致所有规则的执行终止。有些时候,命令的出错并不代表就是错误的,例如mkdir命令用来创建一个目录,若目录存在,则命令报错,但使用这个命令的本意其实是保证一定要有这个目录。

为了做到这一点,可以在命令行前加上一个减号(-)来标记这个命令就算出错也是成功的,例如:

clean:
	-rm -f *.o

还有一种全局的做法是指定make的参数,使用-i--ignore-errors参数,指定后所有命令都将会忽略错误;也可以对一个规则进行指定,通过指定一个规则是以.IGNORE作为目标的,这个规则中的所有命令将会忽略错误;这些做法影响的范围不一样,可根据实际需要进行设置。

若指定了make-k--keep-going参数,则makefile文件的某个规则中的命令出错了,就会终止该规则的运行,继续执行其它规则。

嵌套执行make

在一个大型工程中会把不同模块或是不同功能的源文件放在不同的目录中,可以在每个目录中编写一个makefile,这个有利于makefile变得更加简洁,不至于将所有的规则都放在一个makefile中,导致makefile文件难以维护。

比如,有个子目录subdir,目录下有个makefile文件,里面编写了这个目录下文件的编译规则。那么核心的makefile文件可以按下面这样编写:

subsystem :
	cd subdir && $(MAKE)
# 等价于
subsystem :
	$(MAKE) -C subdir

定义$(MAKE)宏变量是因为make可能需要附带一些参数,后面更好维护;这个例子所要执行的操作都是进入subdir目录,然后执行make命令。核心makefile文件中的变量也可以传递到下级makefile中,需要显式地声明,但是不会覆盖下层的makefile中所定义的变量,除非指定-e参数。

若要传递变量到下级makefile中,可以使用下面的声明:

export <variable...>

若不想传递变量,使用以下声明:

unexport <variable...>

若想要传递所有的变量,则只需要一个export即可。

变量SHELLMAKEFLAGS不受export限制,总是传递到下层makefile中,尤其是MAKEFLAGS,这是一个系统级的环境变量,保存了make参数或是上层makefile为这个变量指定的参数。

make命令的某些参数不会往下传递,它们是-C-f-h-o-W参数;若不想往下层传递参数,可以按下面这样编写:

subsytem:
	cd subdir && $(MAKE) MAKEFLAGS=

若定义了MAKEFLAGS环境变量,必须保证其中的参数是其它makefile都需要使用的,不然会有异常情况发生,这些参数可能会导致异常情况发生:-t-n-q参数。

在“嵌套执行”中比较有用的参数是-w--print-directory,它们会在make执行过程中输出目前工作的目录,在进入和退出目录时都会打印提示信息。当使用-C参数来指定make下层makefile时,-w是自动开启的;当指定了-s(--slient)或--no-print-directory-w总是失效的。

定义命令包

makefile中出现一些相同的命令序列,则可以为这些相同的命令序列定义一个变量:

define run-test
test $(firstword $^)
mv test.c $@
endef

其中,run-tset是命令包的名字,不能和makefile中定义的变量重名;在defineendef中间的两行就是命令序列,第一条命令是运行test程序,生成tset.c文件,然后将这个test.c文件重命名为$@所指代的文件名;可以通过下面的方式来使用上面的命令包:

foo.c : foo.h
	$(run-test)

$(run-test)调用上面定义的run-test命令包,调用命令包就像使用变量一样,命令包run-test中的$^就是foo.h$@就是foo.cmake在执行命令包时,命令包中的每个命令会被依次独立执行。

在makefile中使用变量

makefile中定义的变量,和C/C++语言中的宏一样,仅代表一段文本,在makefile执行时其会在引用的位置展开,但也有所不同,makefile中定义的变量可以改变其值。

makefile中,变量可以使用在“目标”,“依赖目标”,“命令”或是makefile的其它部分中。

变量命名要求:

  1. 命名可以包含字符,数字,下划线(可以是数字开头),但是不应该含有:#=或是空字符;
  2. 变量是大小写敏感的

变量基础

变量在声明时需要给予初值,在使用时需要给变量名前加上$符号,除此之外,建议用小括号()或花括号{}将变量包括起来。

若需要使用$字符,需要使用$$对其转义。

变量可以使用在很多位置,例如规则中的“目标”,“依赖”、“命令”以及新的变量中。下面是一个示例:

objects = program.o foo.o utils.o
program : $(objects)
	cc -o program $(objects)
	
$(objects) : defs.h

变量中的变量

在定义变量的值是,我们可以使用其它变量来构造变量的值,在makefile中有两种方式:

  1. 使用=号,左侧为变量,右侧为,右侧的变量不一定非要是已定义的值,也可以是后定义的值。比如:
foo = $(bar)
bar = $(ugh)
ugh = Huh?

all :
	echo $(foo)

执行make all将会在屏幕上输出变量$(foo)的值,即Huh?

这种方式既有优点,也有缺点。优点就是右侧值可以后定义,缺点是可能会导致递归定义:

CFLAGS = $(include_dirs) -O
include_dirs = -Ifoo -Ibar
CFLAGS = $(CFLAGS) -O
# 或
A = $(B)
B = $(A)

这个例子将会导致make陷入到无限的变量展开过程中,但是make可以检测到这些错误的定义,并且报错;若是在变量中使用函数,还会让make运行缓慢。

  1. 第二种方式就是使用:=操作符,:=不能使用后定义的变量,只能使用已定义的变量:
# 1
x := foo
y := $(x) bar
x := later
# 等价于
y := foo bar
x := later

# 2
y := $(x) bar
x := foo
# 等价于
y := bar
x := foo

上面都是一些比较简单的变量使用,下面有一个复杂的例子:

ifeq (0,${MAKELEVEL}) 
cur-dir := $(shell pwd) 
whoami := $(shell whoami) 
host-type := $(shell arch) 
MAKE := ${MAKE} host-type=${host-type} whoami=${whoami} 
endif

其中,MAKELEVEL变量会记录当前makefile的调用层数。

变量定义还有两个要点需要了解:

  1. 若要定义一个值为空格的变量,可以通过下面的方式实现:
nullstring := 
space := $(nullstring) # 行尾

nullstring是一个Empty变量,而space变量的值是一个空格。这里是用一个Empty变量标识变量值开始,然后使用#指示变量定义结束,这样就可以定义出一个值为空格的变量,这个用法比较特殊,在定义路径变量时最好要注意这个特性。

  1. 还有一个比较有用的操作符是?=,下面是一个使用的示例:
foo ?= bar
# 等价于
ifeq ($(origin foo), undefined)
	foo = bar
endif

?=含义是:若foo没有定义过,那变量foo的值是bar,若foo之前被定义过,那么这条语句将什么都不做。

变量的高级用法

  1. 变量值的替换:替换变量中的共有部分,其格式是$(var:a=b)或是${var:a=b},其意思是把变量var中所有以a字符串“结尾”(指的是空格或是结束符)的a替换成b字符串,比如:
foo := a.o b.o c.o
bar := $(foo:.o=.c)

在上面的例子中,$(bar)的值就是a.c b.c c.c

除了上面的这种替换方式,还有另外一种替换,是以“静态模式”定义的,比如:

foo := a.o b.o c.o
bar := $(foo:%.o=%.c)

这依赖于被替换字符串中有相同的模式,这个例子和前面的例子结果相同。

  1. 把变量的值再当成变量,比如:
x = y
y = z
a := $($(x))

在这个例子中,$(x)的值是y,所以$($(x))就是$(y),那么$(a)的值就是z

注意:x=y而不是x=$(y)

下面是一个更复杂的例子:

x = $(y)
y = z
z = Hello
a := $($(x))

这个例子的$($(x))被替换成了$($(y))$(y)的值是z,所以最终结果是a:=$(z),即Hello。下面是一些更复杂的用法:

# 1
x = variable1 
variable2 := Hello 
y = $(subst 1,2,$(x)) 
z = y 
a := $($($(z)))
# $($($(z))) --> $($(y)) --> $($(subst 1,2,$(x))) --> $(variable2) --> Hello

# 2
first_second = Hello 
a = first 
b = second 
all = $($a_$b)
# $($a_$b) --> $(first_second) --> Hello

# 3
a_objects := a.o b.o c.o 
1_objects := 1.o 2.o 3.o 
sources := $($(a1)_objects:.o=.c)
# a1 := a --> sources := a.c b.c c.c
# a1 := 1 --> sources := 1.o 2.o 3.o

# 4
ifdef do_sort
func := sort
else
func := strip
endif
bar := a d b g q c
foo := $($(func) $(bar))
# 已定义do_sort --> foo := $(sort a d b g q c)
# 未定义do_sort --> foo := $(strip a d b g q c)

# 5
dir = foo 
$(dir)_sources := $(wildcard $(dir)/*.c) 
define $(dir)_print 
lpr $($(dir)_sources) 
endef
# 这个例子定义了三个变量:dir, foo_sources和foo_print

追加变量值

使用+=操作符给变量追加值,比如:

objects := main.o foo.o bar.o utils.o
objects += another.o

最终,$(objects)的值为main.o foo.o bar.o utils.o another.o

若变量之前没有定义过,则+=会自动变成=;若有定义,则+=会继承前一次的赋值符,若前一次是:=,则+=会以:=作为赋值符:

variable := value
variable += more
# 等价于
variable := value
variable := $(variable) more

但要注意下面的这种情况:

variable = value
variable += more

+=的前一次的操作符是=,故+=会以=作为赋值,则会发生变量的递归定义,这显示是不合适的。(但是make会处理这个问题,不必担心这个问题)

override指示符

若有变量是在make的命令行参数设置的,则makefile中对这个变量的赋值会被忽略。若想在makefile中设置这类参数的值,可以使用override指示符,语法如下:

override <variable> = <value>
override <variable> := <value>
# 也可以追加
override <variable> += <value>

对于多行变量定义,可以使用define指示符,在define指示符前也可以使用override指示符:

override define foo
bar
endef

多行变量

可以通过define指示符来设置变量值,使用这种方式设置变量的值可以有换行。define指示符后面跟的是变量的名称,而重起一行定义变量的值,最后以endef指示符结束。

这种方式的工作方式和=操作符一样;变量的值可以包含函数、命令、文字、或是其它变量;需要注意的是命令需要以制表符开头,不然make不会将其认为是命令。

下面是一个define指示符的用法:

define two-lines 
echo foo 
echo $(bar) 
endef

环境变量

make运行时的系统环境变量可以在make开始运行时被载入到makefile文件中,若makefile中定义了这个变量,或是这个变量由make命令行带入,则系统的环境变量的值将被覆盖。

make指定了-e参数,则系统环境变量将覆盖makefile中定义的变量。

目标变量

前面所定义的变量都是“全局变量”,在整个makefile文件中,都可以访问这些变量。(自动化变量除外,如$<,属于“规则型变量”,这种变量依赖于规则的目标和依赖目标的定义)。除此之外,还可以为某个目标设置局部变量,这种变量称为“Target-specific Variable”,它可以和“全局变量”同名,作用范围只在这条规则以及连带规则中,所以其值也只在作用范围内有效,不会影响规则链以外的值。语法如下:

<target...> : <variable-assignment>
<target...> : override <variable-assignment>

其中,<variable-assignment>可以是前面涉及到的各种赋值表达式,例如=:=+=?=。下面是一个例子:

prog : CFLAGS = -g 
prog : prog.o foo.o bar.o 
$(CC) $(CFLAGS) prog.o foo.o bar.o 
prog.o : prog.c 
$(CC) $(CFLAGS) prog.c 
foo.o : foo.c 
$(CC) $(CFLAGS) foo.c 
bar.o : bar.c 
$(CC) $(CFLAGS) bar.c

在这个例子中,不管全局的$(CFLAGS)的值是什么,在prog目标以及其依赖的所有规则中,$(CFLAGS)的值都是-g

模式变量

make的模式一般是至少含有一个“%”的,所以可以以下面的方式来给所有以.o结尾的目标定义目标变量:

%.o : CFLAGS = -O

模式变量的语法和目标变量一样。

使用条件判断

示例

通过使用条件判断可以让make根据运行时的不同情况选择不同的条件分支,条件表达式可以是比较变量的值,或是变量和常量的值。下面是一个例子:

libs_for_gcc = -lgnu 
normal_libs = 
foo: $(objects) 
ifeq ($(CC),gcc) 
$(CC) -o foo $(objects) $(libs_for_gcc) # 当$(CC)的值为gcc时
else 
$(CC) -o foo $(objects) $(normal_libs) # 当$(CC)的值不为gcc时
endif

在这个例子中,目标foo可以根据变量$(CC)值来选择不同的函数库来编译程序。ifeq表示条件语句的开始,并指定一个条件表达式,包含两个参数,以逗号分隔,表达式用小括号括起;else表示条件表达式为假的情况;endif表示一个条件语句的结束。这个例子还可以写得简洁一些:

libs_for_gcc = -lgnu 
normal_libs = 
ifeq ($(CC),gcc) 
libs=$(libs_for_gcc) 
else 
libs=$(normal_libs) 
endif 
foo: $(objects) 
$(CC) -o foo $(objects) $(libs)

语法

条件表达式的语法如下:

<conditional-directive> 
<text-if-true> 
endif
# 以及
<conditional-directive> 
<text-if-true> 
else 
<text-if-false> 
endif

其中,<conditional-directive> 表示条件关键字,有四种:

# ifeq 比较参数arg1和参数arg2是否相同(参数可以使用make的函数)
ifeq (<arg1>, <arg2>) 
ifeq '<arg1>' '<arg2>' 
ifeq "<arg1>" "<arg2>" 
ifeq "<arg1>" '<arg2>' 
ifeq '<arg1>' "<arg2>"

# ifneq 比较参数arg1和参数arg2是否不相同,不同为真
ifneq (<arg1>, <arg2>) 
ifneq '<arg1>' '<arg2>' 
ifneq "<arg1>" "<arg2>" 
ifneq "<arg1>" '<arg2>' 
ifneq '<arg1>' "<arg2>"

# ifdef 若<variable-name>的值非空,则为真
ifdef <variable-name>

# ifndef 若<variable-name>的值为空,则为真
ifndef <variable-name>

make是在读取makefile时就计算条件表达式的值,并根据条件表达式的值来选择语句,因此,最好不要把自动化变量(如$@等)放入条件表达式中,因为自动化变量是在运行时才有的。

为了避免混乱,make不允许把整个条件语句分成两部分放在不同的文件中。

使用函数

makefile中可以使用函数来处理变量,在函数调用后的返回值可以当做变量来使用。

函数的调用语法

makefile中函数的调用很像变量的使用,也是用$来标识的:

$(<function> <arguments>)
# 或
${<function> <arguments>}

其中,<function>是函数名称,<arguments>是函数的参数,参数以逗号分隔,而函数名称和参数之间是以“空格”分隔。下面是一个例子:

comma:= , 
empty:= 
space:= $(empty) $(empty) 
foo:= a b c 
bar:= $(subst $(space),$(comma),$(foo))
# $(subst $(space),$(comma),$(foo)) --> $(subst  , , ,a b c) --> a,b,c

这个例子中bar的值为a,b,c

字符串处理函数

字符串替换函数

# 字符串替换函数 -- subst
# 将字符串<text>中的<from>字符串替换成<to>字符串
# 函数返回被替换过后的字符串
$(subst <from>,<to>,<text>)

模式字符串替换函数

# 模式字符串替换函数 -- patsubst
# 查找<text>中符合模式<pattern>的单词(以空格、制表符或是回车以及换行分隔),用<replacement>替换
# 函数返回被替换过后的字符串
$(patsubst <pattern>,<replacement>,<text>)

删除首尾空格函数

# 删除首尾空格函数 -- strip
# 去除<string>字符串中开头和结尾的空字符
# 函数返回被去除空格的字符串
$(strip <string>)

查找字符串函数

# 查找字符串函数 -- findstring
# 在字符串<in>中查找<find>字符串
# 若找到,返回<find>;未找到,返回空字符串
$(findstring <find>,<in>)

过滤函数

# 过滤函数 -- filter
# 以<pattern>模式过滤<text>字符串中的单词,保留符合模式<pattern>的单词
# 返回符合模式<pattern>的字符串
$(filter <pattern...>,<text>)

反过滤函数

# 反过滤函数 -- filter-out
# 以<patten>模式过滤<text>字符串中的单词,去除符合模式<pattern>的单词
# 返回不符合模式<pattern>的字符串
$(filter-out <pattern...>,<text>)

排序函数

# 排序函数 -- sort
# 给字符串<list>中的单词排序(升序),sort函数会去掉<list>中相同的单词
# 返回排序后的字符串
$(sort <list>)

取单词函数

# 取单词函数 -- word
# 取字符串<text>中第<index>个单词(从1开始)
# 返回字符串<text>中的第<index>个单词,若<index>大于<text>的单词数,返回空字符串
$(word <index>,<text>)

取单词串函数

# 取单词串函数 -- wordlist
# 从字符串<text>中取从<start>开始到<end>的单词串
# 返回字符串<text>中从<start>到<end>的单词字串。若<start>大于<text>中的单词数,则返回空字符串;若<end>大于<text>的单词数,则返回从<start>到<text>结束的单词串
$(wordlist <start>,<end>,<text>)

单词个数统计函数

# 单词个数统计函数 -- words
# 统计<text>中字符串中的单词个数
# 返回<text>中的单词数
$(words <text>)

首单词函数

# 首单词函数 -- firstword
# 取字符串<text>中的第一个单词
# 返回字符串<text>的第一个单词
$(firstword <text>)

文件名操作函数

取目录函数

# 取目录函数 -- dir
# 从文件名序列<names>中取出目录部分,目录部分是指最后一个反斜杠(“/”)之前的部分。如果没有反斜杠,那么返回“./”
# 返回文件名序列<names>的目录部分
$(dir <names...>)

取文件函数

# 取文件函数——notdir
# 从文件名序列<names>中取出非目录部分,非目录部分是指最后一个反斜杠(“/”)之后的部分
# 返回文件名序列<names>的非目录部分
$(notdir <names...>)

取后缀函数

# 取后缀函数——suffix
# 从文件名序列<names>中取出各个文件名的后缀
# 返回文件名序列<names>的后缀序列,如果文件没有后缀,则返回空字串
$(suffix <names...>)

取前缀函数

# 取前缀函数——basename
# 从文件名序列<names>中取出各个文件名的前缀部分
# 返回文件名序列<names>的前缀序列,如果文件没有前缀,则返回空字串
$(basename <names...>)

加后缀函数

# 加后缀函数——addsuffix
# 把后缀<suffix>加到<names>中的每个单词后面
# 返回加过后缀的文件名序列
$(addsuffix <suffix>,<names...>)

加前缀函数

# 加前缀函数——addprefix
# 把前缀<prefix>加到<names>中的每个单词后面
# 返回加过前缀的文件名序列
$(addprefix <prefix>,<names...>)

连接函数

# 连接函数——join
# 把<list2>中的单词对应地加到<list1>的单词后面。如果<list1>的单词个数要比<list2>的多,那么,<list1>中的多出来的单词将保持原样。如果<list2>的单词个数要比<list1>多,那么,<list2>多出来的单词将被复制到<list2>中
# 返回连接过后的字符串
$(join <list1>,<list2>)

foreach函数

foreach函数的语法是:

$(foreach <var>,<list>,<text>)

这个函数的含义是将参数<list>中的单词依次取出到参数<var>所指定的变量中,再对<var>执行<text>所包含的表达式,每执行一次<text>都会返回一个字符串,每个字符串以空格分隔,<text>所返回的每个字符串所组成的整个字符串就是foreach函数的返回值。下面是一个示例:

names := a b c d
files := $(foreach n, $(names), $(n).o)
# $(files)的值是“a.o b.o c.o d.o”

注意:foreach中的<var>参数是一个临时的局部变量,只作用于foreach函数中。

if函数

if函数的语法是:

$(if <condition>,<then-part>)
# 或
$(if <condition>,<then-part>,<else-part>)

其中,<condition>是条件表达式,条件为真就执行<then-aprt>, 为假就执行<else-part>

call函数

call函数是唯一一个可以用来创建新的参数化的函数,可以通过call函数向一个非常复杂的表达式传递参数。语法如下:

$(call <expression>,<param1>,<param2>...)

make执行这个函数时,<expression>参数中的变量,如$(1)$(2)等,会被参数<param1><param2>依次取代,而<expression>的返回值就是call函数的返回值。例如:

reverse = $(1) $(2) 
foo = $(call reverse,a,b)
# $(foo)的值是“a b”
# 或
reverse = $(2) $(1) 
foo = $(call reverse,a,b)
# $(foo)的值是“b a”

origin函数

origin函数指示变量的来源,语法如下:

$(origin <variable>)

注意:<variable>是变量的名称,不应该是引用。最好不要在<variable>中使用$字符。

下面列举了origin函数的返回值:

  • "undefined":表示<variable>未定义过;
  • "default":表示<variable>是一个默认的定义;
  • "environment":表示<variable>是一个环境变量,并且当makefile执行时,”-e“参数没有被打开;
  • "file":表示<variable>被定义在makefile中;
  • "command line":表示<variable>是被命令行定义的;
  • "override":表示<variable>是被override指示符重新定义的;
  • "automatic":表示<variable>是一个命令运行中的自动化变量。

shell函数

shell函数把执行操作系统命令后的输出作为函数返回,它的参数是操作系统shell的命令。下面是一些例子:

contents := $(shell cat foo)

files := $(shell echo *.c)

注意:shell函数会生成一个Shell程序来执行命令,要注意其运行性能。

控制make的函数

error函数会产生一个致命的错误,语法如下:

$(error <text...>)

其中,<text>是错误信息。示例如下:

# 1
ifdef ERROR_001 
$(error error is $(ERROR_001)) 
endif

# 2
ERR = $(error found an error!) 
 .PHONY: err 
 err: ; $(ERR)

#1会在变量ERROR_001定义后执行时产生error调用,#2在目标err被执行时才发生error调用。

warning函数是输出一段警告信息,但是不会让make退出,而是make继续执行,语法如下:

$(warning <text>...)

make的运行

make的退出码

make命令执行后有三个退出码:

  • 0 - 表示成功执行
  • 1 - 表示make运行时出现任何错误
  • 2 - 表示使用了make-q选项,并且make使得一些目标不需要更新

指定makefile

可以为make命令指定一个特殊名字的makefile,使用make-f或是--file参数来指定。下面是一个例子:

make -f test.make

若不止一次地使用了-f参数,则所有指定的makefile将会被连在一起传递给make执行。

指定目标

make的最终目标是makefile中的第一个目标,其它目标是其所依赖的。在实际使用时,也可以为make指定目标,比如之前的make clean就是一个例子。

任何在makefile中的目标都可以被指定成终极目标,除了以-开头或是包含了=的目标,因为这些会被解析成命令行参数或是变量。

隐含目标也可以成为(被指定为)终极目标。

make有一个环境变量是MAKECMDGOALS,这个变量存放在命令行上指定的终极目标的列表,若没有指定目标,则这个变量为空,下面是一个例子:

sources = foo.c bar.c 
ifneq ( $(MAKECMDGOALS),clean) 
include $(sources:.c=.d) 
endif
# 若输入的命令不是make clean,则会包含foo.d和bar.d这两个makefile

make也可以指定“伪目标”

下面是一个可参考开源软件发布时makefile目标使用说明的编写规则:

  • "all":编译所有的目标
  • "clean":删除所有被make创建的目标
  • "install":安装已编译的程序
  • "print":列出改变过的源文件
  • "tar":把源程序打包备份
  • "dist":创建一个压缩文件
  • "TAGS":更新所有的目标
  • "check"和"test":用来测试makefile的流程

检查规则

  1. 检查命令或是执行的序列,可以使用make的下列参数:
-n
--just-print
--dry-run
--recon
  1. 把目标文件的时间更新,但不更改目标文件,就是make直接把目标变成已编译的状态,可以使用下面的参数:
-t
--touch
  1. 查找一个目标可以使用下面的参数:
-q
--question
# 找到则无输出,未找到打印出错信息
  1. 使用下面的参数指定一个文件(一般是源文件或依赖文件),make会根据规则推导来运行依赖于这个文件的命令:
-W <file>
--what-if=<file>
--assume-new=<file>
--new-file=<file>

隐含规则

自动化变量

下面是自动化变量及其说明:

  • $@:表示规则中的目标文件集,在模式规则中,若有多个目标,则$@就是匹配于目标中模式定义的集合;
  • $%:仅当目标是函数库文件时,表示规则中的目标成员名;若不是函数库文件(.a(Unix)或.lib(Windows)),则其值为空;
  • $<:依赖目标中的第一个目标名字,若依赖目标时以模式定义的,则$<将是符合模式的一系列的文件集,其是依次取出的;
  • $?:所有比目标新的依赖目标的集合,以空格分隔;
  • $^:所有的依赖目标的集合,以空格分隔;若是有多个重复的依赖目标,则会去重,仅保留一份;
  • $+:所有的依赖目标的集合,不去重
  • $*:表示目标模式中“%”及其之前的部分