当前位置: 首页 > 工具软件 > makefile2dot > 使用案例 >

Makefile的使用摘记

竺焕
2023-12-01

前言

本文主要记录本人在学习嵌入式Linux过程中所接触到并学习到的一些GNU make的语法和用法(主要源于读uboot和kernel的Makefile以及查阅网络上的资料),因此内容可能没有《跟我一起写makefile》或官方文档那么系统。本文的内容会随着我学习的深入不断的增加完善。

1 Makefile的基本语法

1.1 目标、依赖、命令

Makefile的规则主要由目标、依赖、命令三部分组成,写法如下:

target: depdence
	command

三者之间关系密切,可以概括如下:
目标:我们期望构建的目标,比如一个可执行程序;
依赖:我们要构建的目标所依赖的“原料”,比如要编译得到一个可执行程序,就需要相应的源文件,这个源文件就是依赖;
命令:我们怎么通过“原料”得到目标呢,就是通过命令,命令把“原料”加工成目标(把源程序编译成可执行文件,编译使用的gcc命令就是这里所说的命令)。需要注意的是,命令不顶格写,需要空出一个Tab键
再看一个简单的例子体会一下:

test: test.o
	gcc -o test test.o

test.o: test.c
	gcc -c -o test.o test.c

对上例做个简单的说明:构建可执行程序test,需要test.o文件(编译过程中的一个中间文件),test.o文件又需要通过命令gcc -c -o test.o test.c从test.c得到。得到test.o文件后,再通过命令gcc -o test test.o即可得到可执行程序test。

1.2 make内建的变量

1.2.1 概述

有时候我们在Makefile中会看到一些变量,但始终找不到这些变量在哪里定义的,很可能这个变量就是make内建的变量。这些变量已经由make系统提前定义好了,各自有各自的含义,可以使用命令make -p查看。比如查看变量MAKE:

执行
# make -p | grep 'MAKE'
得到
...
MAKEFILE_LIST := 
MAKE_VERSION := 3.81
MAKE = $(MAKE_COMMAND)
MAKE_COMMAND := make
...

1.2.2 具体的内建变量及其含义

变量名含义
MAKEmake
MAKECMDGOALS执行make时输入的目标,比如输入make target,此时MAKECMDGOALS的值为target
CURDIR当前所在目录的绝对路径,其值与pwd命令的结果相同

1.3 make内建的函数

makefile提供了一些内建的函数,可以让我们更加灵活的编写Makefile的规则和命令。使用这些函数的语法通常如下(函数如果有多个参数,则参数之间使用逗号分隔):

$(<function> <arg1, arg2 ...>)
或使用花括号:${<function> <arg1, arg2 ...>}

1.3.1 if函数

使用注意与GNU make支持的条件语句ifeq等之间的区别):

$(if <cond>, <then>, <else>)

说明
else部分可以有也可以没有;
cond若返回非空字符串则执行then否则执行elsethenelse可以是嵌套的函数,可以是对一个或多个变量的引用,也可以仅仅是一个字符串;
cond若返回非空字符串则if函数的返回值为then,否则为else
举例

var1 := not empty
var2 := $(if $(var1), a string)

all:
        @echo $(var2)

执行make之后,打印出a string

1.3.2 subst函数

使用

$(subst <from>, <to>, <text>)

说明
① 把字符串text中的from替换成to;
② 返回替换后的字符串
举例

var1 := $(subst old, new, old string)

all:
        @echo $(var1)

执行make之后,打印出new string

1.3.3 patsubst函数

使用

$(patsubst <pattern>, <replacement>, <text>)

说明
text是一个或多个单词构成的字符串,单词之间使用空格、Tab、回车或换行分割;
② 使用pattern来匹配text中的每个单词,如果匹配上,则使用replacement来替换匹配上的一个或多个单词,最终返回匹配替换后的字符串,如果没有匹配上则原样返回
③ 通常结合通配符%一起使用。
举例

var1 := a.c b.c c.c

all:
        @echo $(patsubst %.c, %.o, $(var1))

执行make之后,打印出a.o b.o c.o

1.3.4 strip函数

使用

$(strip <string>)

说明
① 去掉字符串string中开头和结尾的空字符;
② 返回处理后的字符串。
举例

all:
        @echo $(strip     a b c      )

执行make之后,输出a b c

1.3.5 filter函数

使用

$(filter <patterns>, <text>)

说明
text是一个或多个单词构成的字符串,单词之间使用空格、Tab、回车或换行分割;
patterns是一个或多个模式,模式之间通常用空格分隔,用于匹配text中的每个单词;
③ 返回所有匹配上的单词(匹配不上的就不会出现在返回值里);
④ 与filter函数功能正好相反的是filter-out函数,后者返回所有没有匹配上的单词。filter-out函数后面不再专门介绍。
举例

patterns := %.c %.s
text := a.c b.c c.s others

all:
        @echo $(filter $(patterns), $(text))

执行make之后,输出a.c b.c c.s

1.3.6 dir函数

用法

$(dir <names>)

说明
names是一个或多个单词构成的字符串,单词之间一般以空格隔开,这些单词通常是一个路径(当然也可以是一个普通的字符串,但是好比水果刀也可以杀鸡,只是我们一般不这么用);
② 取出names中每一个路径(单词)的目录部分,这里所谓的目录部分是路径中最后一个反斜杠之前的部分,包括最后一个反斜杠。如果路径中没有反斜杠,那么将得到./;
③ 返回names中所有路径(单词)的目录部分;
④ 与dir函数功能相反的是notdir函数, 后者返回路径的非目录部分,即最后一个反斜杠后面的部分,如果路径(单词)里不含有反斜杠,那么就返回这个路径(单词)本身。notdir函数后面不再专门介绍。
举例

all:
        @echo $(dir $(CURDIR), /usr/local, testdir/test, shuiguodao)

执行make之后,输出/root/ /usr/ testdir/ ./。

1.3.7 wildcard函数

用法

$(wildcard <patterns>)

说明
patterns是一个或多个模式,模式之间通常用空格分隔,模式中可以带有路径,用于匹配模式中所带路径下的所有文件的文件名(不包括隐藏文件),若模式不带有路径则默认是当前路径;
② 返回所有匹配到的文件名(以空格隔开,且如果模式带有路径,那么返回的文件名中也会包括这个路径,即此时返回的是pathname),如果没有匹配到则返回空。
举例

# /root/目录下有两个目录文件:work winshare
# 当前目录下有a.c b.c两个.c文件
all:
        @echo $(wildcard /root/* *.c)

执行make之后,输出/root/winshare /root/work a.c b.c。

1.3.8 addprefix

用法

$(addprefix <prefix>, <names>)

说明
names是一个或多个单词构成的字符串,单词之间一般以空格隔开;
prefix是一个字符串,它作为前缀被添加到names中的每一个单词的前面,并将添加过前缀的字符串返回;
③ 如果names为空,那么即使prefix不为空最后也会返回空;
④ 与addprefix函数功能相反的是addsuffix函数, 后者是添加后缀,即添加到names中的每一个单词的后面。addsuffix函数后面不再专门介绍。
举例

all:
        @echo $(addprefix /, )
        @echo $(addprefix /, root usr)

执行make后,会打印出:


/root /usr

1.3.9 foreach函数

用法

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

说明
var是foreach的临时变量;
var_list是要遍历的变量列表;
③ 遍历变量列表时,会把变量逐个取出,先放到var中,然后再执行text所包含的表达式,该表达式中通常包含对var的处理。每次循环,text都会返回一个字符串,将每次返回的字符串以空格分隔,最后作为foreach的返回值。
举例

all: 
        @echo $(foreach file, a b c d, $(file).c)

执行make后,打印出:

a.c b.c c.c d.c

1.3.10 origin函数

用法

$(origin <var>)

说明
var是变量名,注意不是变量的引用;
② origin的返回值指示了变量的来源:

返回值含义
undefined未定义的变量
default内建变量
environment环境变量(export导出的变量),且Makefile被执行时,-e参数没有被打开
file变量被定义在Makefile中
command line变量是在执行make时从命令行定义的
override变量被override指示符重新定义
automatic自动化变量

举例
用法比较简单,不进行示例。

1.3.11 shell函数

用法

$(shell <cmd>)

说明
cmd是系统的shell命令;
② 函数可以获得shell命令cmd的执行结果
③ 在Makefile中还可以使用反引号来获取shell命令的执行结果,如`pwd`;
举例
用法比较简单,不进行示例。

1.3.12 info、warning、error函数

用法

$(warning/error/info <text>)

说明
① text表示要输出的信息,text可以为空;
② text字符串不需要加双引号(Makefile中的字符串是不需要双引号的);
③ info是仅输出text,warning是给出警告并输出text,error是报错(会终止make的执行)并输出text。
举例
用法比较简单,不进行示例。

1.3.13 eval函数

用法

$(eval <text>)

说明
① eval能够实现将text的内容作为Makefile的一部分被make解析和执行。当然前提是text能够被make解释执行,否则make会报错;
② eval本身是没有返回值的,它完成的工作是将text的值交给make。
举例

v1 := $(eval target: all1 all2)

all1:
        @echo $@
        @echo v1 = $(v1)

all2:
        @echo $@

执行make或者make target,会打印出:

all1
v1 =
all2

1.3.14 call函数

用法

$(call <var>,<parm1>,<parm2>,<parm3>...)

说明
var可以是一个自定义变量,比如一个多行变量;
param是通过call传递的参数,在var中,$1表示parm1,$2表示parm2,依此类推;
③ 参数之间用逗号隔开,但不同于其他函数的是,逗号后面如果添加了空格的话,空格也会被当成参数的一部分。
举例
call函数一般连同自定义变量一起使用,形式上像“自定义函数”,但实际上Makefile是没有真正意义上的自定义函数的。后面介绍自定义变量的使用时会举例子。

1.3.15 basename函数

用法

$(basename <names>)

说明
① names是一个或多个单词构成的字符串,单词之间一般以空格隔开;
② basename会取names中每个字符串的前缀,即最后一个小数点之前的内容;
③ 如果names中字符串没有后缀则原样返回,没有前缀则返回为空。
举例
简单不例。

1.4 自动化变量

Makefile预定义了一系列自动化变量,这些变量各自有各自的含义,不同于一般的变量,这些自动化变量由特殊的符号构成,且其值通常在执行时才能确定

变量含义
$@目标名称,如果是多目标的情况,那么$@只表示匹配上的那个目标的名称
$(@D)$@的目录部分,即最后一个反斜杠前面的部分,如果目标名称是dir1/dir2/target,那么该变量的值就是dir1/dir2
$(@F)$@的文件部分,即最后一个反斜杠后面的部分,如果目标名称是dir1/dir2/target,那么该变量的值就是target
$*不包含扩展名的目标名称(即把目标名称xxx.yyy中的.yyy去掉),如果目标名称不是xxx.yyy的格式,那么其值为空
$<第一个依赖的名称
$^所有依赖的名称,以空格分开,不含重复项
$+所有依赖的名称,以空格分开,可能包含重复项
$?所有比目标新的依赖的名称,不含重复项
$%目标若不是函数库文件(.a/.lib),其值为空;否则,表示函数库文件中的成员名:如一个目标是foo.a(bar.o),那么其值为bar.o,此时$@是foo.a;如果目标只是函数库文件,没有用括号给出成员名,那么其值还是空

1.5 自定义变量

1.5.1 怎么自定义变量

Makefile允许自定义变量。Makefile的变量没有类型,直接在赋值的时候定义,并且可以赋空值,但是不能只写一个光秃秃的变量,必须要有赋值运算符。比如我们可以这样定义一个值为空的变量var

# 赋值运算符两端可加空格也可不加(shell是不允许加空格的)
var :=

通过测试发现,make在执行命令之前会先处理所有的变量的定义赋值操作。对于同一个变量前后多次赋值,则后面的赋值到底有何影响由具体的赋值运算符来决定,下面我们介绍几种Makefile中的赋值运算符的含义:

赋值运算符含义
=后面的赋值会覆盖前面的赋值,且在被引用时才会展开所赋的值
:=后面的赋值会覆盖前面的赋值,且在赋值时就直接展开了
?=前面以定义(赋值),则不发生作用,否则进行定义赋值
+=前面已定义(赋值),则起到接续赋值的作用,否则相当于=

定义完成后,如果要引用变量,可以使用$符号。要注意的是,引用变量时,若变量名超过1个字符,那么需要加上括号,shell中引用变量是不允许加括号的。

1.5.2 自定义变量的一些特殊用法

① 替换变量值的部分内容($(var:xxx=yyy))
当我们需要对变量的值里面的部分内容做替换的时,比如把test.c中的.c换成.o,可以这么做:

var := test.c
var1:= aaabbb

all: 
        @echo $(var:c=o)    # var必须以c结尾
        @echo $(var:%.c=%.o)
        @echo $(var1:bbb=c) # var1必须以bbb结尾

执行make后输出:

test.o
test.o
aaac

值得一提的是,我们得到替换后的结果的同时不会影响变量原本的值。其实在Makefile中,只要不对原变量重新赋值,一般来说我们施加于变量的各种操作都不会影响变量原本的值。
② 用变量实现“自定义函数”
Makefile支持使用define和endef来定义多行变量,借助多行变量和call函数,我们可以实现类似自定义函数的用法,还是通过举例来说明:

define test
echo "$1"
echo "$2"
endef

all:
        @$(call test,arg1,     arg2)

执行make之后,打印出:

arg1
     arg2

当然,并不是只有多行变量才能配合call使用,我们看一个uboot/kernel的Makefile文件(Kbuild.include)中的例子:

if_changed = $(if $(strip $(any-prereq) $(arg-check)),                       \
	@set -e;                                                             \
	$(echo-cmd) $(cmd_$(1));                                             \
	printf '%s\n' 'cmd_$@ := $(make-cmd)' > $(dot-target).cmd)

我们也可以call变量if_changed值得注意的是, 该变量使用了=赋值,结合=的语义不难明白,这里应该用=而不能使用:=。因为,我们希望在call这个变量并向它传参的时候,这个变量的赋值部分才发挥作用(执行参数指定的命令),这一定意义上说,也是一种“引用时展开”。如果一定义就直接展开,if_changed的值也就固定了,传参也就没有意义。如果上述解释还比较抽象的话,那么下面这个简单的例子可能能够帮助理解:

var := "$1" "$2"
var1 := $(call var,arg1,     arg2)

all:
        @echo $(var1)

执行make后,输出为空。而执行下面这段Makefile:

var = "$1" "$2"
var1 := $(call var,arg1,     arg2)

all:
        @echo $(var1)

输出为:

arg1      arg2

1.6 执行Makefile

1.6.1 如何执行

我们在Makefile所在的目录下执行make,make程序就会读入我们的Makefile文件并执行其中的命令。但需要注意的是,Makefile文件的命名不是随意的,make程序会按GNUmakefilemakefileMakefile的顺序,在当前目录下搜索。如果我们非要自行命名一个Makefile也是可以的,我们可以通过make的-f选项来指定要执行的Makefile。

1.6.2 执行时指定的选项

make的选项总结

2 一些零碎的知识点

2.1 伪目标(.PHONY)

2.1.1 语法介绍

Makefile的目标通常是一个文件,比如目标是xxx.o文件,它需要依赖相应的xxx.c生成。而伪目标并不是一个文件,只是一个标签,比如Makefile中的clean,clean不是文件。当我们执行make clean的时候,只是想执行这个标签下面的命令而已。我们可以使用.PHONY来显示的指明一个目标是伪目标,具体是这么使用:

.PHONY: target
target:
	command

当然也可以将.PHONY放在文件的末尾,当要显示的指明多个目标时,用空格隔开:

target1:
	command1

target2:
	command2

.PHONY: target1 target2

一般情况下,目标和依赖都是文件时,这个目标下的命令会不会执行取决于依赖文件的最后修改时间晚于还是早于目标文件的最后修改时间。伪目标也可以有依赖,但由于伪目标不是文件,所以无法根据修改时间判定它是否要执行。通常伪目标下的命令总是被执行。

2.1.2 举例说明

即便我们并不使用.PHONY来指明伪目标,一个伪目标似乎也可以工作的很好,即当我们执行make target是,伪目标target下的命令总是被执行,使不使用.PHONY似乎都一样。那什么场景下使用.PHONY和不使用.PHONY不一样呢

如上文所述,伪目标不是文件。那么试想这样一种情况,伪目标名为target,当Makefile所在目录下有一个文件也叫target,那此时会发生什么呢?此时,Makefile中的target会被当成文件,如果这个target没有依赖或没有文件依赖,那么就没有依赖文件与target文件做比较,即make系统看来,target总是最新的,此时target下的command始终不会得到运行,并且还会提示:

make: `target' is up to date.

此时的Makefile如下:

target:
        echo "phony target"

这时候,.PHONY的作用就显现出来了。我们使用.PHONY来显示地指明一个目标是伪目标,向make说明,不管是否有同名的文件,总之这个目标就是伪目标。即当我们将上述Makefile修改成下面这个样子:

target:
        echo "phony target"
 
.PHONY: target

再执行make target,就可以执行target下面的command了,本例中是打印phony target。

2.1.3 常见用法

如果我们需要将一些目标显示的指示为伪目标,通常可以使用一个PHONY变量,将需要显示指示的伪目标添加到这个变量中,最后在文件的末尾使用.PHONY来声明伪目标:

PHONY :=
...
PHONY += target1
...
PHONY += target2
...
.PHONY: $(PHONY)

2.2 自动合并依赖项

当同一个目标多次出现在Makefile中时,Makefile会自动合并以来项,并依次构建依赖项,如下例所示:

all: dependency1

dependency1:
        @echo "dependency1"

all: dependency2

dependency2:
        @echo "dependency2"

执行make all会打印出:

dependency1
dependency2

值得一提的是,当同一个目标多次出现,部分有依赖,部分没有依赖时,会先构建依赖:

all:
        @echo "dependency1"

all: dependency2

dependency2:
        @echo "dependency2"

执行make all会打印出:

dependency2
dependency1

而当一个目标多次出现,但都没有依赖时,后出现的目标会覆盖先出现的,即只执行最后出现的那个目标下的命令,同时给出警告:

all:
        @echo "1"
all:
        @echo "2"

执行make all会打印出:

Makefile:5: warning: overriding commands for target `all'
Makefile:2: warning: ignoring old commands for target `all'
2

2.3 多目标

在Makefile中,一个目标当然可以有多个依赖,上文已经介绍,即便同一个目标多次出现也无妨,make会自动合并该目标的依赖项。那么,目标能不能同时有多个呢?答案是肯定的。GNU make支持多目标的用法:

target := target1 target2 target3

all: $(target)

$(target):
        @echo $(@)

执行make后,会打印出:

target1
target2
target3

可能初学的同学会把$(target)看成一个整体,认为@echo $(@)只执行一次。这是不对的,上例中,引用target的结果是target1 target2 target3,作为目标all的依赖,target1~3如果不存在于当前文件中的话,会逐个被拿去匹配以它们为目标的规则,然后按照写好的规则被生产出来。

2.4 target pattern

Makefile的通配符%可以简化我们的Makefile文件编写,常见的用法是用来自动匹配目标和依赖,如下:

%.o : %.c
	[cmd]

这里的目标%.o通常会匹配上某个目标的依赖,也就是说通配的源头往往是某个依赖。那目标能够成为通配的源头吗?答案是可以的,我们可以这样使用通配符:

$(host-csingle): $(obj)/%: $(src)/%.c FORCE

不妨设host-csingle的值为scripts/basic/fixdepobj的值为scripts/basicsrc的值为./scripts/basic/fixdep,则上面的目标和依赖为:

scripts/basic/fixdep: scripts/basic/%: ./scripts/basic/%.c FORCE

此时scripts/basic/%会尝试匹配目标scripts/basic/fixdep,本例中当然会匹配上(如果匹配不上会有target xxx doesn’t match the target pattern这样的提示),此时通配符%表示的是fixdep,进而我们可以得到目标./scripts/basic/fixdep.c。因此上面的目标和依赖实际上等价于:

scripts/basic/fixdep: ./scripts/basic/fixdep.c FORCE

如果我们想要将目标文件与源文件的命名保持一致的话,那么采用这种做法是有益处的,通过变量和通配符让make帮助我们去自动匹配,能够使Makefile的规则保持稳定,即便我们要编译其他源文件,只需要修改相应变量的值即可,其他部分不用动。

2.5 Makefile中的$$

Makefile中的$$

2.6 对Makefile中只见变量的引用不见其定义的说明

有时我们会在Makefile文件中发现一些被引用的变量无法在当前文件中找到定义(只有引用不见定义),这种情况一般有以下四种可能:

  • 变量定义在其他Makefile中,并被export导出
  • 变量定义在其他Makefile中,且定义所在的Makefile被当前Makefile包含(include)
  • 变量是make的内建变量
  • 变量本就没有定义,使用者可以在执行make的时候传入变量,若没有传入,则引用的就是没有定义的变量,结果将为空

2.7 include、-include以及sinclude

Makefile可以通过include命令将其它符合Makefile语法的文件包含进来,就类似于头文件包含,这样方便将稳定的部分抽取出来。make开始执行时,会将include的文件在当前位置展开,倘若include的文件没有找到,make会报错。如果不想因为include的文件不存在而导致make报错,那么可以使用-includesinclude,这两者的功能相同,但后者的兼容性更好。

3 编写一个通用的Makefile

loading…

参考文献

[1] 《跟我一起写makefile》
[2] uboot/linux kernel中的Makefile文件

 类似资料: