0. 前言
一直想写一篇关于VPP构建系统的博客,但VPP的构建系统里包含着一个顶级目录下的Makefile,一个在build-root下由数据驱动的Makefile,以及一系列子目录下的Makefile。理解VPP的构建系统就要求对Makefile掌握熟练。本文就是自己在使用Makefile过程中的相关知识点的回顾和总结。
1. Makefile简介
1.1 Makefile的规则
1 | target ... : prerequisites ... |
target可以是一个目标文件,也可以是一个可执行文件,还可以是一个标签(伪目标)。
prerequisites是生成target所依赖的文件。
command是生成target所要执行的命令。
需要注意的是,Makefile中的命令,也就是上述的command,必须是以Tab键开始。
以上描述的三者相互构成依赖关系,一句话描述的话就是:prerequisites中如果有文件比target要更新的话,command所定义的命令就会被执行。
上述描述的就是Makefile的规则,也是Makefile的核心内容。
1.2 make命令自动推导
GNU make命令很强大,可以自动推导文件以及文件依赖后面的命令。只要make看到一个.o文件,它就会自动地把.c文件添加到依赖关系中。例如,如果make找到一个example.o,那么example.c就会是example.o的依赖文件,并且cc -c example.c也会被自动推导出来。这种方法,就是make的隐式推导规则。
1.3 Makefile文件包含的内容
Makefile文件里主要包含了五个部分的内容:显示规则、隐式规则、变量定义、文件指示和注释。
- 显示规则说明了如何生成一个或多个目标文件。程序员负责编写要生成的文件、文件的依赖以及生成的命令。
- 隐晦规则使用的是
make自动推导的过程,不需要程序员像显示规则一样指定整个依赖关系。 Makefile里的变量可以理解为C语言中的宏。当Makefile被执行时,其中的变量会被扩展到相应的引用位置上。- 文件指示。
Makefile可以引用另一个Makefile文件,类似于C中的include。Makefile中可以通过#if等条件语句来指定有效部分。同时,Makefile中还可以定义一个多行的命令。 - 注释。
Makefile使用#字符作为注释符。
1.4 引入其它的Makefile
在Makefile中使用include关键字可以把别的Makefile包含进来,语法如下:
1 | include <filename> |
make命令执行时,会搜索include所包含的所有的Makefile,并将其内容放置在当前位置。如果文件没有声明绝对路径或者相对路径的话,make会在当前目录下首先搜索。如果当前目录没有对应的文件,make继续在以下目录中搜索:
- 若
make执行时,添加了-I或者--include-dir参数,那么make回去此参数指定的目录搜索。 - 若
<prefix>/include包括/usr/local/bin或/usr/include存在的话,make也会在相应目录中搜索。
若文件没有被查找到,make会提示警告信息,但不会立即出错。如果想让make不理会无法读取的文件,继续执行接下来的命令,可在include前加一个-号。
1 | -include <filename> |
1.5 make工作流程
GNUmake工作流程如下:
- 读入所有的
Makefile文件。 - 读入被
include的其它Makefile文件。 - 初始化文件中的变量。
- 推导隐式规则,并分析所有规则。
- 为所有的目标文件创建依赖关系链。
- 根据依赖关系,决定哪些目标要重新生成。
- 执行生成命令。
2. Makefile命令
Makefile中每条规则中的命令和操作系统Shell的命令行是一致的。make会按照顺序一条一条地执行命令,每条命令的开头必须是以Tab键开头的。
2.1 命令显示
一般情况下,make会把其要执行的命令行在命令执行前输出到屏幕上。当我们在命令行前添加@字符时,这个命令将不被显示出来。
1 | # Makefile1 |
make执行时带入参数-n或者--just-print,只会显示命令,不会执行命令,可帮助调试Makefile,了解对应Makefile执行的具体步骤和内容。
make执行时带入参数-s或者--silent或者--quiet,则在执行时不会显示执行的命令。
2.2 命令执行
当在执行Makefile中的命令去更新目标文件时,执行命令的过程中,shell会为每一行命令创建一个子进程,在子进程中执行对应的命令。这就意味着一些如cd等shell命令的作用域只在当前行,不会影响到接下来命令的执行结果。如果想要cd的结果作用于接下来的命令,则需把两条命令写在同一行,中间用;隔开。
1 | # Makefile1 |
2.3 并行执行
GNUmake可一次执行多条命令。通常情况下,make一次只执行一条命令,下一条命令需等待当前命令结束再执行。但是我们可以通过设置-j或者--jobs选项来显示地通知make并行执行命令,并行执行命令的数量即是-j或--jobs后接的数字。
2.4 错误处理
make会检查每行命令执行过后的退出码。如果其中一条命令的退出码有问题,make则不会继续执行那条命令对应接下来的命令,甚至可能直接退出执行。
当然有时候一些命令执行错误也无伤大雅,比如用mkdir创建一个已经存在的目录,make执行就会报错。解决这种问题可以在命令前添加-,如下例所示。即使rm无法删除文件,make也会继续执行。
1 | clean: |
另一种方法是在make选项中添加-i或--ignore-errors,显示地说明忽略make执行过程中所有的错误。
2.5 嵌套使用
我们可以在Makefile中把make当作命令使用。一般对于构建大型软件系统来说,我们会对不同的子项目编写单独的Makefile。举个例子,如果我们有个子目录subdir,对应subdir有自己的Makefile,我们可以在顶级Makefile里做如下编写:
1 | # Method 1 |
当我们嵌套使用make时,最好添加.PHONY对其目标声明为伪目标。
2.6 定义命令包
如果Makefile中出现一组相同的命令序列,我们则可以将这些相同的命令序列组合定义成一个变量。语法如下所示。echo_var即是这个命令包的名字,命令包还可以接受参数。我们可以直接把echo_var看作一个变量,用$(echo_var)。也可以把echo_var看作一个函数,用call调用echo_var。下述例子t1和t2的执行结果相同,都是输出one。
1 | define echo_var |
3. Makefile变量
Makefile中定义的变量,类似于C/C++语言中的宏,本质上就是文本字符串,可以在Makefile执行过程中自动展开。变量在声明时需要赋初值,在使用变量时,需要给变量名前加$,而且最好使用()和{}将变量包起来。
3.1 变量赋值
在Makefile中,赋值方式有=、:=、?=和+=四种。
= 递归展开变量
用=或者define关键词都可以定义此类型变量。如果变量的定义援引了其它的变量,则引用会一直展开下去,直到找到被引用变量的最新定义,并以此作为变量的返回值。
1 | var = I love |
上述例子运行结果是:
1 | I love ubuntu |
=的赋值时没有先后顺序的,使用时要小心递归的出现,如A = $(A)这样的写法就会导致变量递归定义。
:=简单扩展变量
:=用此方式定义的变量,会在变量定义的位置,按照被引用的变量的当前值做一次展开。被引用的变量的值后续发生变化不会影响当前定义变量的值。
1 | var := I love |
上述例子运行结果是:
1 | I love centos |
但如果上述中的res := $(var) $(distro)改为res = $(var) $(distro),运行的结果则为I love ubuntu。可以体会到两种赋值方式的差别。
?=条件变量
?=只在变量未被赋值的情况下起作用。如果变量没有初始化,变量的定义即为默认值。
1 | ARCH = arm |
上述例子运行结果是:
1 | arm |
+=变量添加新的文本
当变量未被定义,则+=的作用和=是一致的,定义一个递归展开的变量。但如果变量之前已被定义,+=只是做字符的添加工作。如果一开始使用:=定义变量,则+=就是对变量添加新的文本。但如果一开始使用:定义变量,+=则不会立即进行变量展开,直到找到最新的变量定义。第一个例子中的var的最终值是I love ubuntu说明了这种用法。
3.2 自动化变量
Makefile的自动变量包括目标文件、依赖文件等,本质上是对一类变量的简写。
首先来看三个常用的自动化变量的含义:
$@代表目标文件名$<代表规则的第一个依赖文件$^代表规则的所有的依赖文件,以空格分离
举一个例子来说明三个自动化变量的使用。创建一个项目,包含如下所示的文件:
1 | /* func1.h */ |
在这个示例项目中,main函数调用func1和func2,两个函数分别在func1.h和func2.h里声明。我们编写项目的makefile如下所示:
1 | CC = gcc |
生成目标main,则需要main.o、func1.o和func2.o。所以$^指的是所有的依赖条件。而%.o:%.c的写法,则是采用了Makefile的静态模式,$<表示的是第一个依赖条件,所以其等价于如下所示的写法:
1 | main.o: main.c |
还有一些出现频率不高的自动化变量:
$?所有比目标文件新的依赖文件的集合
1 | libxyz: x.o y.o z.o |
如果x.o、y.o或者z.o发生了更新,$?的含义则是将更新过后的目标文件插入到库libxyz中。
$+和$^类似,表示所有依赖文件的集合(重复的依赖文件不剔除),主要用于链接过程的使用$*表示在模式匹配和静态模式规则中,代表目标模式中%的部分。比如hello.c,当匹配模式为%.c时,$*表示的是hello
4. Makefile条件判断
Makefile里有两种条件判断的使用方法: ifeq和ifdef。接下来我们就用例子加以说明。
4.1 ifeq
ifeq 和ifneq用于比较Makefile中变量是否相等。
1 | DEBUG = true |
在上述例子中,变量DEBUG的值与true比较,如果相等,则CFLAGS被设置成-g,为调试选项;如果不等,则CFLAGS被设置成-O2,为优化选项。
4.2 ifdef
ifdef和ifndef用于判断变量是否被定义。
1 | ifdef VERBOSE |
上述例子中,检查VERBOSE是否被定义(非空),如果已被定义,则PRINT设为echo,makefile运行时打印出Building my_program...。若未定义,则PRINT设为@echo,只展示编译的命令。
5. Makefile函数
Makefile中函数的基本调用语法如下:
1 | (function arguments) |
或者是:
1 | {function arguments} |
arguments为函数的参数,参数间以逗号分隔,函数名和参数之间以空格分隔。接下来我们来具体看看Makefile里的函数。
5.1 字符串处理函数
subst字符串替换函数
1 | $(subst <from>,<to>,<text>) |
把字符串text中的from全部替换成to。
示例:
1 | $(subst ee,EE,feet on the street) |
上述返回结果为fEEt on the strEEt。
patsubst模式字符串替换函数
1 | $(patsubst <pattern>,<replacement>,<text>) |
查找text中的单词(单词以空格、Tab、回车或换行分隔)是否符合模式pattern,如果匹配的话,则用replacement替换。
示例:
1 | $(patsubst %.c,%.o,x.c.c y.c) |
上述返回结果为x.c.o y.o。
strip去空格函数
1 | $(strip <string>) |
去除string字符串中开头和结尾的空字符。
findstring查找字符串函数
1 | $(findstring <find>,<in>) |
在字符串in中查找find字符串,如果找到,则返回find。否则返回空字符串。
示例:
1 | $(findstring a,a b c) |
第一个函数返回结果a,第二个函数返回空字符串。
filter过滤函数
1 | $(filter <pattern>,<text>) |
以pattern模式过滤text字符串中的单词,保留符合模式pattern的单词。匹配的模式可以有多个。
示例:
1 | sources := foo.c bar.c baz.s ugh.h |
上述$(filter %.c %.s, $(sources))返回的值是foo.c bar.c baz.s。
filter-out反过滤函数
1 | $(filter-out <pattern>,<text>) |
同filter函数的含义相反,以pattern模式过滤text字符串中的单词,去除符合模式pattern的单词。
word取单词函数
1 | $(word <n>,<text>) |
取字符串text中的第n的单词(从1开始索引)。
示例:
1 | $(word 2, foo bar baz) |
上述返回值是bar。
wordlist取单词串函数
1 | $(wordlist <start>,<end>,<text>) |
从字符串text中取start到end的字符串。
示例:
1 | $(wordlist 2,3,foo bar baz) |
上述返回值是bar baz。
words单词个数统计函数
1 | $(words <text>) |
统计字符串中text的单词个数。
示例:
1 | $(word $(words, foo bar baz), foo bar baz) |
上述返回值是baz。
5.2 文件名操作函数
dir取目录函数
1 | $(dir <names...>) |
从文件名names取出目录部分。目录部分指最后一个反斜杠之前的部分。若无反斜杠,则返回./。
示例:
1 | $(dir src/foo.c main) |
上述返回值是src/ ./。
notdir取文件函数
1 | $(notdir <names...>) |
从文件名names取出非目录部分。
示例:
1 | $(notdir src/foo.c main) |
上述返回值是foo.c main。
suffix取后缀函数
1 | $(suffix <names...>) |
从文件名names取出各个文件名的后缀。返回文件名names的后缀。若无后缀,则返回空字符串。
示例:
1 | $(suffix src/foo.c src-1.0/bar.c hacks) |
上述返回值.c .c。
basename取前缀函数
1 | $(basename <names...>) |
suffix函数的对立函数,从文件names取出每个文件名的前缀。
示例:
1 | $(basename src/foo.c src-1.0/bar.c hacks) |
上述返回值是src/foo src-1.0/bar hacks。
addsuffix加后缀函数
1 | $(addsuffix <suffix>,<names...>) |
把suffix添加到names的每个单词后。
示例:
1 | $(addsuffix .c, foo bar) |
上述返回值是foo.c bar.c。
addprefix加前缀函数
1 | $(addprefix <prefix>,<names...>) |
把prefix添加到names的每个单词之前。
示例:
1 | $(addprefix src, foo.c bar.c) |
上述返回值是src/foo.c src/bar.c。
5.3 foreach函数
1 | $(foreach <var>,<list>,<text>) |
foreach函数的含义是,把参数list中的单词逐一取出放到参数var所指定的变量中,然后再执行text所包含的表达式。每一次执行text都会返回一个字符串,最终的返回值就是每次字符串的组合。
示例:
1 | names := a b c d |
上述返回值是a.o b.o c.o d.o。
需要注意的一点是,foreach函数中的var参数是一个临时的局部变量,作用域只在foreach函数中。
5.4 if函数
1 | $(if <condition>,<then-part>) |
或者
1 | $(if <condition>,<then-part>,<else-part>) |
使用含义直接明了,不再赘述。
5.5 call函数
1 | $(call <expression>,<parm1>,<parm2>,...,<parmN>) |
call函数执行时,expression参数中的变量,$(1)、$(2)等,会被parm1、parm2等取代。expression的返回值就是call的返回值。
call函数使用中需要注意的一点是,传递参数时不要有多余的空格。
5.6 shell函数
1 | $(shell <command>) |
shell命令能够把执行操作系统命令后的输出作为函数返回。
5.7 origin函数
1 | $(origin <variable>) |
orign函数用作判断变量的来源。这里不作细究,需要时查文档。
5.8 eval函数
eval函数执行时会对参数进行两次展开。第一次展开过程是由函数本身完成的,第二次是函数展开过后的结果会被认为是Makefile的内容由make执行。
总结
了解Makefile的基本语法规则和工作流程,熟悉makefile的自动变量,静态模式等技巧,会使用基本的条件判断语句和Makefile中相应的函数,我觉得对Makefile的掌握也就可以算是入了门。