三、Makefile文件的语法
3.1 注释
井号(#)在Makefile中表示注释。
# 这是注释 result.txt: source.txt # 这是注释 cp source.txt result.txt # 这也是注释
3.2 回声(echoing)
正常情况下,make会打印每条命令,然后再执行,这就叫做回声(echoing)。
test: # 这是测试
执行上面的规则,会得到下面的结果。
$ make test # 这是测试
在命令的前面加上@,就可以关闭回声。
test: @# 这是测试
现在再执行make test,就不会有任何输出。
由于在构建过程中,需要了解当前在执行哪条命令,所以通常只在注释和纯显示的echo命令前面加上@。
test: @# 这是测试 @echo TODO
3.3 通配符
通配符(wildcard)用来指定一组符合条件的文件名。Makefile 的通配符与 Bash 一致,主要有星号(*)、问号(?)和 […] 等。
比如:
● *.o :表示所有后缀名为o的文件。
● ?:表示任意单个字符。
例如,abc?.txt匹配到的文件名是abca.txt、abcb.txt等。
● [...]:表示括号中任意一个字符,可以使用短划线(-)表示范围。
例如,abc[de].txt匹配到的文件名可以是abc.txt或者abcde.txt,但不包括abcce.txt。
而abc[a-d].txt则匹配到abc.txt、abcb.txt、abcc.txt和abcd.txt。
clean: rm -f *.o
3.4 模式匹配
Make命令允许对文件名,进行类似正则运算的匹配,主要用到的匹配符是%。比如,假定当前目录下有 f1.c 和f2.c两个源码文件,需要将它们编译为对应的对象文件。
%.o: %.c
这是一个Makefile文件中的一个典型规则,它表示:
对于所有依赖目标名以“.o”结尾的目标,在当前目录或者其它指定搜索路径下,查找与之同名但后缀名为“.c”的源文件,并执行相应的命令,生成这个目标。
换句话说,这个规则用于描述如何将一个C语言源文件编译成一个目标文件。在这个规则中,“%.o”和“%.c”是通配符形式,它们将会自动匹配当前目录下相同前缀名称的.o和.c文件。例如,foo.o 依赖于 foo.c,那么这条规则就会被用来编译foo.c生成foo.o。这个规则可以被多个目标文件依赖并使用到,方便批量编译源代码。
等同于下面的写法。
f1.o: f1.c f2.o: f2.c
使用匹配符%,可以将大量同类型的文件,只用一条规则就完成构建。
在Makefile中,%是一个通配符,表示任意匹配0个或多个字符的模式。它用于将规则(依赖关系)应用于一组相关的目标文件(例如,所有以“.o”结尾的目标文件),而不需要一个一个地列举它们。
具体来说,%通配符常常和特定的后缀名一起使用,如:
%.o: %.c
表示所有以.c结尾的源文件都可以被编译成对应的.o目标文件。在实际使用时,%会被自动替换成被匹配的目标名称的通配符部分,从而实现批量处理和编译。
这种技巧可以使 Makefile 更灵活和可维护,减少重复工作,提高代码重用率。
3.5 变量和赋值符
Makefile 允许使用等号自定义变量。
txt = Hello World test: @echo $(txt)
上面代码中,变量 txt 等于 Hello World。调用时,变量需要放在 $( ) 之中。
当需要在 Makefile 中引用变量的值时,通常使用美元符号和括号或者花括号。
1.美元符号和括号:适用于大部分情况。可以用于引用任何已定义的变量,以及未定义的变量(在这种情况下将返回空值)。
test: @echo "Hello, $(NAME)!"
在这个规则中,$(NAME) 可以引用已经定义的变量 NAME 的值,也可以引用未定义的变量(在这种情况下将会输出空字符串)。
2.花括号:主要用于区分变量名和其他字符。花括号内的变量名称必须已经定义,否则会引发语法错误。
test: @echo "My name is ${FIRST_NAME}${LAST_NAME}"
在这个规则中,${FIRST_NAME} 和 ${LAST_NAME} 都必须是已经定义的变量名称。花括号可以使这两个变量的名称与紧随其后的}符号区分开来,从而避免了歧义和错误。
总之,美元符号和括号或者花括号都是用于引用 Makefile 中的变量,但在具体使用时,可能需要根据场景的不同做出选择。
如果需要区分变量和其他字符,建议使用花括号;如果不需要区分,则使用美元符号和括号即可。
调用Shell变量,需要在美元符号前,再加一个美元符号,这是因为Make命令会对美元符号转义,等于$是一个普通字符。
test: @echo $$HOME
有时,变量的值可能指向另一个变量。
v1 = $(v2)
上面代码中,变量 v1 的值是另一个变量 v2。这时会产生一个问题,v1 的值到底在定义时扩展(静态扩展),还是在运行时扩展(动态扩展)?如果 v2 的值是动态的,这两种扩展方式的结果可能会差异很大。
为了解决类似问题,Makefile一共提供了四个赋值运算符 (=、:=、?=、+=),它们的区别请看StackOverflow。
VARIABLE = value # 在执行时扩展,允许递归扩展。 VARIABLE := value # 在定义时扩展。 VARIABLE ?= value # 只有在该变量为空时才设置值。 VARIABLE += value # 将值追加到变量的尾端。
3.6 内置变量(Implicit Variables)
Make命令提供一系列内置变量,比如,$(CC) 指向当前使用的编译器,$(MAKE) 指向当前使用的Make工具。这主要是为了跨平台的兼容性,详细的内置变量清单见手册。
output: $(CC) -o output input.c
3.7 自动变量(Automatic Variables)
Make命令还提供一些自动变量,它们的值与当前规则有关。主要有以下几个。
1)$@
$@:指当前目标,就是Make命令当前构建的那个目标。
比如,make foo的 $@ 就指代foo。
a.txt b.txt: touch $@
等同于下面的写法。
a.txt: touch a.txt b.txt: touch b.txt
2)$<
$<:指代第一个前置条件。比如,规则为 t: p1 p2,那么$< 就指代p1。
a.txt: b.txt c.txt cp $< $@
等同于下面的写法。
a.txt: b.txt c.txt cp b.txt a.txt
3)$?
$? 指代比目标更新的所有前置条件,之间以空格分隔。比如,规则为 t: p1 p2,其中 p2 的时间戳比 t 新,$?就指代p2。
4)$^
$^ 指代所有前置条件,之间以空格分隔。比如,规则为 t: p1 p2,那么 $^ 就指代 p1 p2 。
5)$*
$* 指代匹配符 % 匹配的部分, 比如% 匹配 f1.txt 中的f1 ,$* 就表示 f1。
6)$(@D) 和 $(@F)
$(@D) 和 $(@F) 分别指向 $@ 的目录名和文件名。比如,$@是 src/input.c,那么$(@D) 的值为 src ,$(@F) 的值为 input.c。
7)$(<D) 和 $(<F)
$(<D) 和 $(<F) 分别指向$< 的目录名和文件名。
所有的自动变量清单,请看手册。下面是自动变量的一个例子。
dest/%.txt: src/%.txt @[ -d dest ] || mkdir dest cp $< $@
dest/%.txt表示目标文件的路径格式,它由两部分组成:
● dest/表示目标文件所在的目录,%.txt表示匹配任意.txt文件。因此,对于src/foo.txt文件,它的目标文件就是dest/foo.txt。
● src/%.txt表示依赖文件的路径格式,它匹配与目标文件同名的.txt文件,并且它们都在src/目录下。
规则的主体是命令行,它以一个Tab键开头。
● -d dest表示要测试的路径,其中-d选项用于测试一个路径是否是一个目录。如果dest/目录存在,则这个命令返回0;否则,返回非0值。
● ||是一个逻辑或运算符,当左侧命令返回非0值时,就会执行右侧的命令。
mkdir dest表示创建一个dest/目录。
● 然后,它使用cp命令将依赖文件$<(第一个前置条件)复制到$@(目标文件)中。$< 指代前置文件(src/%.txt),$@指代目标文件(dest/%.txt)。
举例二
● $(<D)表示路径部分,表示$<所在的目录名。比如,如果$<为src/foo.txt,那么$(<D)就表src/。
● $(<F)表示文件名部分,表示$<中最后一个斜杠后面的字符串,也就是文件名部分。比如,如果$<为src/foo.txt,那么$(<F)就表示foo.txt。
这些变量通常用于构建过程中的命令行中,以方便地引用目标文件和依赖项的路径和名称。例如,下面是一个示例规则:
obj/%.o: src/%.c $(CC) -c $< -o $@ -I$(<D)
这是一个Makefile规则,用于将src/目录下的.c文件编译成.o目标文件,并将它们放到obj/目录中。具体来说,它的语法如下:
● obj/%.o表示目标文件的路径格式,它由两部分组成:obj/表示目标文件所在的目录,%.o表示匹配任意以.o结尾的文件。因此,对于src/foo.c文件,它的目标文件就是obj/foo.o。
● src/%.c表示依赖文件的路径格式,它匹配与目标文件同名的.c文件,并且它们都在src/目录下。
● 规则的主体是命令行,它以一个Tab键开头。在这个例子中,它首先使用$(CC)变量指定了gcc编译器,并使用-c选项告诉编译器只编译不链接。
● 然后,它使用$<变量引用第一个依赖文件(也就是源文件)的路径和名称。
● -o表示指定输出文件名字(不然就是默认a.out) 。
● $@表示目标文件的路径和名称。
● -I$(<D)选项表示包含源代码所在的目录。
-I是一个编译器选项,用于指定头文件搜索路径。
具体来说,当我们编写一个C或C++程序时,通常需要包含一些标准库或自定义库的头文件。这些头文件通常位于某个特定的目录下,例如/usr/include或/usr/local/include等。为了让编译器正确地查找这些头文件,我们需要使用-I选项将它们添加到搜索路径中。
例如,假设我们有一个foo.c文件,它需要包含位于include/目录下的头文件bar.h,那么可以使用如下的编译命令:
gcc -c foo.c -o foo.o -Iinclude
在这个示例中,-Iinclude选项会将include/目录添加到头文件搜索路径中,使得编译器能够正确地查找并包含bar.h。
所以!!!!
当我们运行make命令时,它会检查所有的规则,并尝试构建那些需要更新的目标文件。在这个示例中,如果src/foo.c文件被修改,那么它的目标文件obj/foo.o就会被重新生成。
反之,如果目标文件已经是最新的,那么这个规则就不会被执行。通过使用Makefile,我们可以自动化地管理这些依赖关系,并且只需要更新必要的文件,从而提高效率并减少错误。
3.8 特殊变量
1).PHONY变量
有一个在 Makefile 文件中常见的特殊变量,那就是 .PHONY。
.PHONY 是一个特殊的目标(target),用于声明一组“伪目标”,这些伪目标不代表真正的文件或者动作,而是代表需要运行的操作或任务名称。
例如:
.PHONY: all clean all: gcc -o main main.c clean: rm -f main
在这个例子中,我们使用 .PHONY 关键字来定义 all 和 clean 两个目标。在执行 make 命令时,可以通过指定这些目标来运行对应的操作,例如 make all 或 make clean。
需要注意的是,定义了伪目标后,并不会检查对应的文件是否存在或是否需要更新。相反,Makefile 将直接执行对应的命令,而不管是否已经有对应的文件存在。因此,定义伪目标需要通过其他方式确保其正确执行,例如在规则中加入必要的依赖项或者命令验证。
总之,.PHONY 关键字是用于定义伪目标的常用技巧,可以让 Makefile 文件更加清晰和易于维护。
2)VPATH变量
VPATH 是 Makefile 中的一个变量,用于指定寻找文件的路径。它是为了通过简单地设置查找路径来实现模块化设计。
当 Makefile 通过 include 包含其他 Makefile 或者要求在不同目录中搜索依赖项时,就需要使用 VPATH 变量来定义一个“搜索路径列表”。
如果一个依赖文件位于搜索路径内,则Make 将跳过当前目录,并进入下一搜索路径。如果依赖文件在所有搜索路径中都找不到,则会产生一条错误信息。
下面是一个示例:
VPATH = src:../headers a.out: main.o sum.o gcc -o a.out main.o sum.o main.o: main.c gcc -c main.c sum.o: sum.c gcc -c sum.c
这里,VPATH 定义了两个搜索路径,src 和 ../headers。
并使用冒号(: )来分隔搜索路径,例如 VPATH = dir1:dir2:dir3。冒号后面紧跟一个空格是为了避免与 Windows 路径中的驱动器符号(如 C:)发生歧义。
但在一些 Make 实现中,例如 GNU Make,也可以不加空格,像这样: VPATH=src:…/headers。
所以,如果你没在文件名前面指定它应该在哪个目录中被找到的话,当 Make 需要查找 main.c 和 sum.c 文件时,它将首先在 src 目录中查找,如果没有找到,在 ../headers 目录中查找。如果依然没有找到,将出现错误。
注意!!
你可以在每个文件名前面指定它应该在哪个目录中被找到。
怎么指定?
那么我来解释一下。
在 VPATH 中定义搜索路径时,Makefile 会从指定的目录中查找依赖文件。如果在 Makefile 中使用相对路径来引用文件,就需要在文件名前面指定它应该在哪个路径中被找到。
在依赖项列表(例如下面的目标:main.o 和 sum.o)中,需要在文件名前面添加路径,以指示它们位于 VPATH 定义的哪个路径下:
main.o: src/main.c gcc -c src/main.c sum.o: ../headers/sum.c gcc -c ../headers/sum.c
在这个示例 Makefile 中,src/main.c 是相对路径,指相对于当前 Makefile 所在的目录中的 src 目录。同样地,…/headers/sum.c 也是相对路径,指相对于当前 Makefile 所在的目录中的 …/headers 目录。
相对路径根据指定的文件或目录与当前工作目录之间的相对位置来计算路径,而不考虑(或“忽略”)任何特定的根目录或绝对路径信息。因此,如果当前 Makefile 所在的目录为 /path/to/Makefile,则src/main.c和 ../headers/sum.c 最终解析为路径为/path/to/src/main.c和 /path/headers/sum.c(“…/” 表示返回上级目录)。
● 两个 “…” 表示返回上一级目录。
在这段代码中,`sum.o 是一个目标文件,用于存储编译后的 sum.c 文件生成的二进制代码。而 -c 选项则表示只编译但不链接,也就是只生成目标文件而不生成可执行文件。
在 gcc 命令行中指定 “…/headers/sum.c” 作为源文件路径时,“…/” 表示返回到上一级目录,然后再进入 “headers/” 目录,最终找到名为 sum.c 的源文件。
因此,这段代码通过 “…/” 表示返回上一级目录,来定位 sum.c 文件所在的 headers 目录,并对其进行编译操作。该目录结构通常用于将源代码和头文件分开存放,便于代码管理和维护。
3)include变量
首先给出几个定义:
由Makefile文件中的所有规则组成的集合称为U1;
由file.dep文件中的所有规则组成的集合称为U2;
由集合U1和集合U2的并集称为集合U。
● 第一步:Makefile中的include命令先将文件file.dep包含进当前Makefile文件(第一次包含),这样Makefile文件中就有了file.dep文件的内容。
● 第二步:然后在集合U(特别注意,这里是集合U)中检查是否有以file.dep为目标的规则。如果U中没有以file.dep为目标的规则,或者虽然有以file.dep为目标的规则,但根据依赖关系(即便在规则中的命令执行后也)不能使file.dep文件发生更新,则Makefile文件最终包含的就是file.dep文件的当前内容,include命令执行结束。
● 第三步:如果集合U中有以file.dep为目标的规则,并且该规则使得file.dep文件发生了更新,则include命令会将更新后的file.dep文件再次包含进当前Makefile文件(再次包含),跳转到第一步往下继续执行。
3.9 判断和循环
这些是Makefile中的条件语句:
● ifdef 检查变量名是否被定义过(即确定它是否存在)。如果是,则执行指定的命令。使用 ifndef 可以检查变量不存在的情况。
ifdef VAR <commands> endif
● ifeq 和 ifneq 检查两个字符串是否相等或者不相等,如果相等则运行指定的命令。可以检查变量的值。
ifeq (string1, string2) <commands> endif ifneq (string1, string2) <commands> endif
其中字符串可以是变量或者常量。
注意,在 Makefile 中使用上述条件语句时,应该使用 Tab 而不是四个空格来缩进任何指定的命令。
举个栗子!!
Makefile使用 Bash 语法,完成判断和循环。
ifeq ($(CC),gcc) libs=$(libs_for_gcc) else libs=$(normal_libs) endif
上面代码判断当前编译器是否 gcc ,然后指定不同的库文件。
LIST = one two three all: for i in $(LIST); do \ echo $$i; \ done # 等同于 all: for i in one two three; do \ echo $i; \ done
上面代码的运行结果。
one
two
three
3.10 函数
Makefile 还可以使用函数,格式如下。
$(function arguments) # 或者 ${function arguments}
Makefile提供了许多内置函数,可供调用。下面是几个常用的内置函数。
(1)shell 函数
shell 函数用来执行 shell 命令
srcfiles := $(shell echo src/{00..99}.txt)
(2)wildcard 函数
wildcard 函数用来在 Makefile 中,替换 Bash 的通配符。
srcfiles := $(wildcard src/*.txt)
(3)subst 函数
subst 函数用来文本替换,格式如下。
$(subst from,to,text)
下面的例子将字符串"feet on the street"替换成"fEEt on the strEEt"。
$(subst ee,EE,feet on the street)
下面是一个稍微复杂的例子。
comma:= , empty:= # space变量用两个空变量作为标识符,当中是一个空格 space:= $(empty) $(empty) foo:= a b c bar:= $(subst $(space),$(comma),$(foo)) # bar is now `a,b,c'.
(4)patsubst函数
patsubst 函数用于模式匹配的替换,格式如下。
$(patsubst pattern,replacement,text)
下面的例子将文件名"x.c.c bar.c",替换成"x.c.o bar.o"。
$(patsubst %.c,%.o,x.c.c bar.c)
(5)替换后缀名
替换后缀名函数的写法是:变量名 + 冒号 + 后缀名替换规则。它实际上patsubst函数的一种简写形式。
min: $(OUTPUT:.js=.min.js)
上面代码的意思是,将变量OUTPUT中的后缀名 .js 全部替换成 .min.js 。