我们在 Windows 下开发,或者初学 Linux 的时候,可能几乎没有接触过 makefile ,甚至都不知道 makefile 是什么,这是因为在 Windows 下各种各样的集成开发工具,已经内置做好了 makefile 的工作,而初学 Linux 时编译一两个源文件似乎也用不到 makefile ,只要 gcc 一下就好了。而实际上,在 Linux 下的大型项目开发中,必须要用到 makefile ,会写 makefile 是Linux/Unix 程序员的必备技能之一,而且即便是 WIndows 程序员也应该掌握 makefile ,makefile 的编写能力一定程度上反映了一个程序员处理大型工程的能力。接下来,本文将带你走进 makefile 的世界,一起来探索 makefile 的语法规则以及工作原理,并通过实战演练来编写我们自己的 makefile 文件。
我们在学习 Linux 的时候,一般都是直接通过 gcc 对源文件进行编译的,我们可以通过指定 gcc 的参数来指定生成什么样的文件、使用哪个库、在哪个路径搜索等等。但是想象一下,加入现在我们的项目工程中包含上百个源文件,并且不同的源文件包含了不同的库(动态库、静态库、标准库等),又甚至不同的非标准库存放在不同的目录,或者有的源文件要用到多线程。这样的话,如果像初学时一样,在 shell 下使用 gcc 命令变异的话,要使用无数的参数去指定不同路径,不同的链接库等等。这么繁琐复杂的命令显然是不太合理的,并且每次编译都要来这么一遍大大消耗了时间成本。那么这就是使用 makefile 的第一个原因,通过 makefile 可以制定好相应的编译与连接规则,先编译哪个文件后编译哪个文件、哪个需要编译哪个不需要编译、如何链接、如何生成、要生成什么文件等等全部都在 makefile 文件中提前制定好,在编译的时候只需要使用 make 工具,执行 make 命令就可以了。 另外,使用 makefile 的第二个原因是,我们在项目开发中难免会对源码进行修修改改,如果每次修改都要重新编译所有的源文件,那么将浪费大量的时间,我们可以在 makefile 中制定规则,只去编译被修改的源文件,其他文件不需要重新编译,就像我们使用 MDK 集成开发工具的时候,编译选项分为全部编译和只编译修改的文件。总之,有了 makefile 大型项目的编译效率将大大提高。
make 是一个命令工具,它负责解释 makefile 中的指令,而 makefile 文件负责向 make 提供如何去执行的规则。在 makefile 文件中描述了整个工程所有文件的编译顺序、编译规则等。makefile 有自己的书写格式、关键字、函数,就像任何一门编程语言有自己的语法一样。而且在 makefile 中可以使用 shell 的命令来完成某些工作,也就是说 makefile 中可以使用 shell 命令,比如说,编译完成后删除所有的中间文件,可以使用 rm -f *.o 这样的 shell 命令。makefile 在绝大多数的集成开发环境中也都在使用的,只不过我们看不到而已,可以说,makefile 几乎已经成为一种工程编译的基本方法。
首先,makefile 中有三要素:目标(要生成什么)、依赖(用什么去生成)、和命令(如何去生成),这三个要素组成一个规则。实际上,三要素中必不可少的是目标,依赖和命令都可以没有,但是命令必须要有,这一点在后面的实战编写 makefile 的时候会有体现。 下面举个最简单的 makefile 的例子,直接在 shell 下输入 vim makefile ,然后输入 目标 : 依赖 ,换行 Tab+shell命令 即可,注意,命令前一定要加 Tab 键,然后再加一条 shell 命令。这是因为,makefile 中所有以 Tab 开始的行,make 都会交给 shell 去处理,所以在命令的前面一定要以 Tab 键开头。
在这里,目标是可执行文件 exe ,也就是要生成的文件,依赖是所有的 .c 文件,也就是用这些 .c 文件去生成可执行文件 exe ,生成所使用的命令是下面的 gcc 命令。 当我们执行 make 的时候,会显示 make 执行的命令
对于makefile 的命名,可以是 Makefile 也可以是 makefile 或者 GNUmakefile,三种方法都可以,但也只能是这三种。这样在 shell 中执行 make 命令就会直接使用这个 makefile 文件。当然,如果你取了其他名字也是可以的,不过要在 make 命令的时候显示指出文件
make -f makefile01
在执行 make 命令时,首先,make 会先去比较目标文件和依赖文件的修改日期,如果依赖文件的日期要比目标文件的日期新,或者目标文件不存在,那么 make 就会执行后面的命令。假如说在目标的后面没有依赖,比如我们经常用伪目标 clean 去清除中间文件,当 make 发现先冒号后面没有依赖的时候,它默认是不会执行后面的命令的,除非在 make 后面显示的指出这个目标的名字,这也就是我们经常使用的 make clean 命令。 总结来说,makefile 的工作原理可以理解为它是根据依赖去递推的。 ① 执行 make 时,首先 make 工具会在当前目录查找名为 makefile 或 Makefile 的文件,如果我们在 make 命令后面指定了文件名,make 就在当前目录查找制定好的文件名。 ② 如果找到了 makefile 文件,那么会先查找文件中的第一个目标,如果目标的依赖存在,并且依赖文件的更新时间比目标文件的更新时间新,那么就执行后面的命令重新生成目标文件;如果目标文件不存在,则生成目标文件;如果依赖不存在,那么不执行后面的命令。 ③ 如果上一个目标文件的依赖存在,那么 make 会递推查找依赖文件的依赖,然后重复上面的操作。 所以说,makefile 是根据依赖一层一层递推的,不停的去递推寻找依赖。make 只负责在 makefile 中递推寻找依赖,并根据依赖执行命令,而不关心编译是否成功,只要最终的依赖可以找到,就能执行成功,如果最终的依赖没找到,那么 make 就会直接退出。而对于伪目标的执行,可以直接在 make 后面指定目标,这样即使目标后面没有依赖,也会执行命令。而正是 make 的这种依赖递推查找特性,以及根据更新时间决定是否生成的特性,我们可以把依赖分解,这样就能做到某单个源文件修改可以只编译这一个源文件,而不必所有源文件都重新编译。 我们可以验证一下这种根据时间去决定是否生成的特性,我们首先 make 一下,然后再次 make ,会看到提示“目标文件是最新的”
这时候我们修改随便一个文件的更新时间,就可以再次编译(touch 命令可以修改文件的最后访问时间)
① 显示规则 要生成的目标文件,依赖文件,生成目标文件要使用的命令。这些内容说明了要生成什么,用什么去生成,如何生成,其实就是包含了三要素。显示规则有 makefile 编写者显示写出。 ② 隐含规则 也就是依赖 makefile 中的自动推导功能,来自动推导出我们模糊表达的语句,比如根据文件后缀推导文件名等等。 makefile 的自动推导功能是非常强大的,比如说我们定义变量 Src = 1.c 2.c 3.c ,假如说在生成 2.o 目标的时候,我们直接在 gcc 命令中使用变量 Src,它也可以推导出源文件为 2.c。
Src = 1.c 2.c 3.c
obj:$(Src)
gcc $(Src) -o obj
③ 注释 与 shell 一样,使用 # 作为注释符。也就是说,如果某一行的第一个非空字符是 # 那么就认为这一行是注释行。另外,在 makefile 中,反斜杠 \ 表示换行,所以如果注释行末尾有 \ 则代表下一行也是注释。如果想要使用 # 符号,可以使用转义 \ # ,类似于c语言中的转义符。 ④ 变量定义 我们可以定义变量,比如说我们可以把所有的 .c 文件定义为一个 Src 变量,这样在编译命令中就可以用 Src 变量来代替所有的 .c 文件。 ⑤ 指令 make 在读取 makefile 文件时执行某些特殊操作的指令,包括三个部分:一是在一个 makefile 中引用另一个 makefile,类似于c语言中的 #include ;二是指根据条件指定 makefile 中的有效部分,类似于c语言中的条件编译 #if ;三是定义一个多行的命令,通过 define 和 endef 关键字实现,类似于c语言中的 #define。
最基本的就是按照三要素去写:目标、依赖和命令。 目标 : 依赖 Tab 命令 目标和命令都可以使用通配符、变量、函数去代替,命令是一条 shell 命令。目标可以是伪目标,依赖和命令可以没有,但是目标必须要有。
① 通配符 字符匹配首先想到的就是通配符,因为 makefile 中使用的是 shell 中的命令,所以 shell 中的通配符在 makefile 中也适用。我们使用的通配符主要有两个:
比如说,依赖是所有的 .c 文件,就可以用通配符来表示 *.c,但是如果我们在定义变量的时候要使用通配符的话,要注意一点,如果我们直接把 *.c 等号给变量的话,这个变量会默认去匹配文件名为 *.c 的文件
Src = *.c Src变量表示 *.c 文件
要想使变量 Src 表示所有源文件,也就是让 * 作为通配符而不是文件名,需要借助一个函数 wildcard ,该函数就是表示通配符的意思,具体使用将在后面的函数章节介绍。
Src=$(wildcard *.c) Src变量表示 *.c 文件
还有一个通配符 [ ] 并不常用,在中括号中可以指定匹配的字符。比如说,[a-z] 表示匹配 a 到 z 中任何一个字符。 ② 模式匹配字符 % 第二种用于字符匹配的是 % , % 字符作用类似于通配符 * ,它和 * 的区别是,模式匹配字符可以对目标文件与依赖文件进行匹配。比如说我们在写 makefile 的时候,经常会写这样的一条规则
%.o:%.c
这里的 % 代表的是一个文件名,也就是一个字符串。首先,所有的 .o 文件会组成一个列表,然后挨个被拿出来,% 表示当前拿出来的 %.o 文件的文件名,然后根据文件名 % 来寻找和 .o 文件同名的 %.c 文件,并把取出的 %.o 文件和寻找到的 %.c 文件用于执行后面的命令。这是 makefile 中自动匹配的一种规则。
默认情况下,make 会在 makefile 文件所在目录进行搜索规则中所用到的文件,如果我们把所有的文件都和 makefile 文件放在同一个目录下,那肯定是没有问题的,但是世家开发中,我们用到的源文件、头文件、库文件可能会根据用途和种类分别位于不同的目录下,所以这就需要有文件搜索的功能。makefile 中文件搜搜主要有两种方法,一个是环境变量 VPATH 一个是关键字 vpath 。 ① VPATH 环境变量 环境变量的用法如下
VPATH:=/mkdir1/:/mkdir2/
当使用环境变量指定上面的路径后,make 会现在当前目录搜索,然后去目录 /mkdir1/ 搜索,然后再去 /mkdir2/ 搜索,搜索的顺序是先当前目录,然后按照变量赋值中的顺序去搜索。这里的 := 是变量赋值的一种方式,表示在定义时立即展开应用的变量。另外,不同的目录之间要用 : 或者空格隔开。 附:变量赋值的几种方式(后面详细介绍)
② vpath 关键字 在上面的环境变量中,VPATH 是搜索指定路径的所有文件, vpath 关键字的搜索方式是选择性搜索,使用方法如下:
vpath 1.c /mkdir/ 在 /mkdir/ 路径下搜索 1.c
vpath 1.c 清除 1.c 的搜索路径
vpath 清除已设置好的所有搜索路径
makefile 中的变量类似于c语言中的宏定义,在执行的时候会用变量后面的值去替换变量所在的位置。变量的赋值就是在变量后面写上值文本字符串,在使用时直接用后面的文本字符串去替换变量本身。变量的赋值方式有四种,下面将通过一个例子来介绍四种赋值方式的区别
可以看下效果,他的逻辑就是从前往后,和我们在c语言中用的普通 = 赋值是一样的。
可以通过 @ 来屏蔽执行过程( @可以不显示命令,不输出在终端)
执行后发现,B 中的 A 被展开为 AAA ,而不是 aaa 。
在打印结果中,A 还是第一次赋值时的值
可以看到,A 和 B 的值都会受影响
变量在使用的时候要在前面加一个 $ 符号,并使用 () 或 {} 把变量括起来,实际上变量的使用就是一个替换的原理,用括号括起来是为了使用的安全性。
我们可以对变量的值进行替换,主要有如下两种方法
① 自动化变量 自动化变量是指 makefile 根据模式规则自动推导的变量,这类变量只能在命令中使用。实际上,自动化变量属于“规则型变量”,这种变量的值依赖于规则的目标和依赖目标的定义。下面是常用的自动化变量列表
自动化变量 | 说明 |
---|---|
$@ | 代表目标文件,在模式规则中, $@ 就是目标中模式定义的相匹配的目标文件集合 |
$< | 第一个依赖文件,如果依赖是以模式 % 定义的,那么 $< 代表符合模式的一系列的文件集,在生成目标时,一个个的取出来去执行命令 |
$^ | 所有依赖文件(无重复文件),用空格分隔并且会自动去重 |
$? | 比目标更新的依赖文件(集合),也就是发生变化的依赖文件的集合 |
其他自动化变量列表
自动化变量 | 说明 |
---|---|
$% | 当目标文件是一个静态库文件时起作用,代表静态库的一个成员名,比如目标是 1.a 那么 $% 表示 1.o, $@ 表示 1.a |
$+ | 类似“$^”,但是它保留了依赖文件中重复出现的文件(主要用在程序链接时库的交叉引用场合),也就是说他也代表所有依赖文件,但是不会去除重复文件 |
$* | 在模式规则和静态模式规则中,代表茎,茎是目标模式中 % 所代表的部分 |
$(@D) | 表示文件的目录部分(不以斜杠结尾),如果 $@ 表示的是 dir/1.c 那么 $(@D) 表示的值就是目录 dir |
$(@F) | 表示的是文件除目录外的部分即文件名,如果 $@ 表示的是 dir/1.c,那么 $@F 表示的是 1.c |
$(*D) $(*F) | 分别代表茎中的目录部分和文件名部分 |
$(%D) $(%F) | 当目标是静态库文件时,分别表示库文件成员中的目录部分和文件名部分 |
$(<D) $(<F) | 分别表示第一个依赖文件的目录部分和文件名部分 |
$(^D) $(^F) | 分别表示所有依赖文件的目录部分和文件部分(无重复文件) |
$(+D) $(+F) | 分别表示所有的依赖文件的目录部分和文件部分(保留了依赖文件中重复出现的文件) |
$(?D) $(?F) | 分别表示更新的依赖文件的目录部分和文件名部分 |
② 模式变量 模式变量(Pattern-specific Variable),它可以实现给定一种模式,可以把变量定义在符合这种模式的所有目标上。模式变量中至少包含一个模式匹配字符 % 。
makefile 也支持函数,可以通过函数来控制变量,函数的使用和变量类似,需要 () 或 {} 来标识,如果函数有参数的话直接在函数后面列出参数, 参数之间用 , 隔开,比如
$(patsubst <pattern>,<replacement>,<text>)
$(subst <from>,<to>,<text>)
$(strip <string>)
$(findstring <find>,<in>)
$(filter <pattern>,<text>)
$(filter-out <pattern>,<text>)
$(sort <list>)
$(word <n>,<text>)
$(words <text>)
$(firstword <text>)
$(dir <names>)
$(notdir <names>)
$(suffix <names>)
$(basename <names>)
$(addsuffix <suffix>,<names>)
$(addprefix <prefix>,<names>)
$(join <list1>,<list2>)
$(wildcard PATTERN)
$(foreach <var>,<list>,<text>)
$(if <condition>,<then-part>)
$(if <condition>,<then-part>,<else-part>)
$(call <expression>,<parm1>,<parm2>,<parm3>,...)
$(origin <variable>)
返回值 | 含义 |
---|---|
undefined | 如果 variable 从来没有定义过,函数将返回这个值。 |
default | 如果 variable 是一个默认的定义,比如说 CC 这个变量 |
environment | 如果 variable 是一个环境变量并且当Makefile被执行的时候, -e 参数没有被打开。 |
file | 如果 variable 这个变量被定义在 makefile 中,将会返回这个值。 |
command line | 如果 variable 这个变量是被命令执行的,将会被返回。 |
override | 如果 variable 是被 override 指示符重新定义的。 |
automatic | 如果 variable 是一个命令运行中的自动化变量。 |
ifdef bletch
ifeq "$(origin bletch)" "environment"
bletch = barf,gag,etc
endif
endif
③ shell shell 函数以 Linux 的 shell 命令为函数参数,并把执行 shell 命令后的输出作为函数返回值。 ④ make 控制函数
$(error <text ...>) 产生一个致命的错误,<text ...>是错误信息。
$(warning <text ...>) 这个函数很像error函数,只是它并不会让make退出,只是输出一段警告信息,而make继续执行。
有时候,我们并非真正的想要生成一个目标,而是想让 makefile 执行这个目标后面的命令,这时候我们可以通过伪目标来实现。最常见的使用伪目标的例子就是 make clean ,我们执行 make clean 可以把所有的目标文件删除
.PHONY:clean all
clean:
-@rm -f *.o
-@rm exe
在这条规则中,clean 是一个伪目标,它没有依赖,我们也不需要去生成这个目标。在 clean 后面的命令并不是创建目标 clean 的命令,而是一条删除命令,用于删除所有的目标文件。我们在 shell 命令行执行
make clean
就可以执行上面的删除语句,删除所有 .o 文件和终极目标 exe 可执行文件。这里的 .PHONY 是声明伪目标的意思。假如说我们不声明伪目标
clean:
-@rm -f *.o
-@rm exe
直接通过上面的规则,也就是说 clean 是一个真正的目标,如果当前目录下存在一个名为 clean 的文件,当我们在 shell 中执行命令 make clean,由于这个规则没有依赖文件,所以目标 clean 被认为是最新的,所以不再去执行规则所定义的命令,也就是说 rm 命令将不会被执行。那么我们的目的也就无法达到了。声明为伪目标就可以解决这个问题,其实就是把 clean 作为特殊目标 .PHONY 的依赖,这样就保证了不管当前是否有 clean 同名文件,伪目标后面的命令都可以执行,并且 make 不会去推导构建伪目标的隐含规则,这也提高了编译效率。 伪目标还有两个用途,就是递归调用 makefile 和实现多文件编辑,这里不再详细介绍。下面列出 makefile 常用的一些伪目标以及他们的含义。
下面列出的这些伪目标都是 GNU 的一些定义,我们在定义实现下面功能的伪目标时,应尽量使用下面列出的伪目标名称。在大型工程中,这些伪目标是非常有用的,并且它们类似于一种约定俗成的东西,用起来会更加统一。实际上,通过 make 指定伪目标为最终目标,在 make 中是非常常见的,比如 make clean,这在后面会有详细介绍。
伪目标 | 含义 |
---|---|
all | 这个伪目标是所有目标的目标,它的功能一般是编译所有的目标。 |
clean | 这个伪目标功能是删除所有被 make 创建的文件。 |
install | 这个伪目标功能是安装已编译好的程序,其实就是把目标执行文件拷贝到指定的目标中去。 |
这个伪目标的功能是例出改变过的源文件。 | |
tar | 这个伪目标功能是把源程序打包备份,也就是一个 tar 文件。 |
dist | 这个伪目标功能是创建一个压缩文件,一般是把 tar 文件压成 gz 文件。 |
TAGS | 这个伪目标功能是更新所有的目标,以备完整地重新编译使用。 |
check 和 test | 这两个伪目标一般用来测试 makefile 的流程。 |
隐含规则可以理解为一种惯例,它是一种早就约定好的规则,不需要再显示的写出来,makefile 会自动进行推导这种规则。隐含规则会使用系统变量,我们可以通过系统变量来改变隐含规则运行时的参数,比如系统变量 CFLAGS 可以控制编译器的参数等。另外,我们还可以通过模式规则来写自己的隐含规则或使用后缀规则来保证兼容性。说白了,隐含规则就是我们只写出目标(目标是必不可少的),由 make 自动推导生成目标的依赖和命令,当然,如果我们手动去显示写了命令,make 会执行我们显示写的命令。举例说明:
我们在 makefile 中只写了可执行文件(最终目标)以及依赖 .o 文件,以及通过 .o 文件生成可执行文件的命令,我们没有写 .o 文件的生成规则,并且文件夹中没有 .o 文件
这时,我们执行一下 make
可以看到,执行 make 之后,编译器自动执行了一条命令
cc -c -o test.o test.c
这条命令在 makefile 中,我们并没有显示写出。这就是 makefile 中的隐含规则,他会自动推导出生成 .o 文件的命令。但是有一点要注意,隐含条件只能省略中间目标文件重建的命令和规则,但是最终目标的命令和规则不能省略。 实际上,make 中存在一个隐含规则库,这个隐含规则库中的每一条隐含规则都有相应的优先级顺序,优先级也就会越高(顺序在前的优先级高),也就会被优先使用。这里有一个预先设置的后缀列表
.out、.a、.in、.o、.c、.cc、.C、.p、.f、.F、.r、.y、.l、.s、.S、.mod、.sym、.def、.h、.info、.dvi、.tex、.texinfo、.texi、.txinfo、.w、.ch、.web、.sh、.elc、.el
在有些时候,我们使用隐含规则可能会出现问题,比如我们在 makefile 中写一条规则
test.o:test.cc
这里 .cc 是C++源文件,这条规则本意是使用 test.cc 生成 test.o 文件,但是我们没有显示指定命令。这时候,makefile 就会根据隐含规则去推导命令,在推导过程中,如果当前文件夹中同时存在 test.c 和 test.cc 文件,那么根据隐含规则的优先级,会默认选择 tes.c 去生成 test.o 文件(根据后缀列表可以看到优先级顺序),这就和我们本来希望的操作完全不符了,因为目标文件 test.o 已经生成了,所以不会再去继续推导了。 当然,我们可以使用 make 选项 -r 或 -n-builtin-rules 来取消所有的预设的隐含规则,但是即便使用参数,有些隐含规则也会生效。因为大部分隐含规则都是使用了后缀规则来定义的,所以,只要隐含规则中有后缀列表,那么隐含规则就会生效。
隐含规则按照执行顺序排列如下:
实际上,我们完全可以使用模式规则来重载内建的隐含规则或者取消内建隐含规则,甚至是创建一个我们自己的隐含规则。模式规则将在后面介绍。 另一种创建隐含规则的方式是后缀规则,其中,双后缀规则定义了一对后缀,目标文件的后缀和依赖文件的后缀,比如 .c.o 相当于 %o : %c ;单后缀规则只定义一个后缀,也就是依赖文件的后缀,比如 .c 相当于 % : %.c 。注意,这些后缀必须是 make 认识的后缀。 这里要注意一个库文件的隐含规则,库文件 .a 就是 .o 文件通过 ar 打包出来的文件,关于链接库内容可见我的 Linux 专栏动态库与静态库文章。当 make 搜索一个目标的隐含规则时,有一个特殊的特性是,如果这个目标是 a(m) 形式的,make 会把目标变成 (m) 。比如,如果我们的成员是 %.o 的模式定义,并且如果我们使用 make 111.a(222.o) 的形式调用 makefile 时,隐含规则会去找 222.o 的规则,如果没有定义 222.o 的规则,那么内建隐含规则生效,make 会去找 222.c 文件来生成 222.o,如果找到了 222.c,make 会先生成 222.o,然后用生成的 .o 去用到规则中,最后删除 .o 文件。
包含隐含规则的命令中所使用的变量都是预定义好的,这种变量称为隐含变量。我们可以通过命令行参数传值或者是修改系统环境变量的方式对隐含变量赋值或重定义,也可以通过 make 的 -R 或 --no– builtin-variables 参数来取消你自定义变量对隐含规则的作用。。在上面我们介绍的 makefile 的隐含规则中,都可以看到隐含变量的身影。下面介绍隐含变量。
隐含变量 | 变量代表的含义 |
---|---|
AR | 函数库打包程序,可用于创建静态库 .a 文件(我的 Linux 专栏动静态库文章中已详细介绍 ar 命令),默认命令是 ar |
AS | 汇编语言编译程序,默认命令是 as |
CC | C语言编译程序,默认命令是 cc |
CXX | C++语言编译程序,默认命令是 g++ |
CO | 从 RCS文件中扩展文件程序,默认命令是 co |
CPP | C程序的预处理器(输出是标准输出设备),默认命令是 $(CC) –E |
FC | Fortran 和 Ratfor 的编译器和预处理程序,默认命令是 f77 |
GET | 从SCCS文件中扩展文件的程序,默认命令是 get |
LEX | Lex 方法分析器(针对于C或Ratfor),默认命令是 lex |
PC | Pascal 语言编译程序,默认命令是 pc |
YACC | Yacc 文法分析器(针对于C程序),默认命令是 yacc |
YACCR | Yacc 文法分析器(针对于Ratfor程序),默认命令是 yacc –r |
MAKEINFO | 转换Texinfo 源文件(.texi)到 Info 文件程序,默认命令是 makeinfo |
TEX | 从 TeX 源文件创建 TeX DVI 文件的程序,默认命令是 tex |
TEXI2DVI | 从 Texinfo 源文件创建军 TeX DVI 文件的程序,默认命令是 texi2dvi |
WEAVE | 转换 Web 到 TeX 的程序,默认命令是 weave |
CWEAVE | 转换 C Web 到 TeX 的程序,默认命令是 cweave |
TANGLE | 转换 Web 到 Pascal 语言的程序,默认命令是 tangle |
CTANGLE | 转换 C Web 到 C,默认命令是 ctangle |
RM | 删除文件命令,默认命令是 rm –f |
这些隐含变量将作为上面代表命令的隐含变量的参数,并且变量的默认值为空,也就是说,如果不指定的话,相当于没有加命令参数。
隐含变量 | 变量代表的含义 |
---|---|
ARFLAGS | 函数库打包程序 AR 命令的参数,默认值是 rv |
ASFLAGS | 汇编语言编译器参数 |
CFLAGS | C 语言编译器参数 |
CXXFLAGS | C++ 语言编译器参数 |
COFLAGS | RCS 命令参数 |
CPPFLAGS | C 预处理器参数( C 和 Fortran 编译器也会用到) |
FFLAGS | Fortran 语言编译器参数 |
GFLAGS | SCCS get 程序参数 |
LDFLAGS | 链接器参数(比如 ld ) |
LFLAGS | Lex 文法分析器参数 |
PFLAGS | Pascal 语言编译器参数 |
RFLAGS | Ratfor 程序的 Fortran 编译器参数 |
YFLAGS | Yacc文法分析器参数 |
关于隐含变量的使用,比如说拿第一条隐含规则来举例:编译C程序的隐含规则的命令是 (CC) –c (CFLAGS) (CPPFLAGS),根据我们上面介绍的隐含变量表,make 默认使用的编译命令是 cc (CC 变量的默认值是 cc),如果我们把变量 (CC) 重定义成 g++,在使用 make 编译的时候就会使用 g++ 编译程序。另外,我们知道,代表命令参数的隐含变量默认值为空,也就是不加参数,如果我们把变量 (CFLAGS) 重定义成 -g ,那么,在 make 编译的时候就会加上 -g 选项参数来生成调试信息。这样,隐含规则中的命令经过我们的修改,就变成了 g++ –c -g (CPPFLAGS),使用 make 命令的时候,就会按这条命令来执行。
有时候一个目标的生成可能会有多条隐含规则发生作用,比如一个 .o 文件的生成,如果当前目录中只有一个 .y 文件,没有 .c 文件,那么首先会先由 Yacc C 程序的隐含规则由 .y 生成 .c 然后再由 C 程序编译隐含规则将 .c 生成 .o 。多个隐含规则链式执行,所以把这些隐含规则称为隐含规则链。在这里,因为最开始没有 .c 文件,而 .o 文件的生成依赖 .c 文件,所以 make 会推导生成 .c 文件的规则,这时就通过当前已有的 .y 文件和 Yacc C 隐含规则来生成 .c 文件,因此, .c 文件也被称为中间目标。当然,这是在当前目录中没有 .c 文件的前提下,才会触发 Yacc C 隐含规则,如果当前目录中有 .c 文件,那么会直接使用 .c 文件生成目标 .o 文件,即使存在 .y 文件,也不会执行Yacc C 隐含规则,也就是说,只要生成了目标文件,make 就不会继续推导了。并且,生成目标文件后,中间目标文件会被自动删除。
模式规则也就是说在规则中存在模式匹配字符 % ,并且是一定要存在 % ,该字符用于对文件名进行匹配。模式匹配字符 % 我们在 makefile 的字符匹配章节已经介绍过了。通过模式规则可以指定多个目标和依赖,make 根据文件名去匹配哪个目标文件对应哪个依赖文件,比如 %.o:%.c ,make 会自动去根据文件名匹配 1.c 生成 1.o,2.c 生成 2.o并推导出这样的规则。 我们举个例子:
%.o:%.c
$(CC) -c $(CFLAGS) $(CPPFLAGS) $< -o $@
这就是模式规则最常用的一个例子,我们通过自动化变量章节已经知道 @ 代表目标文件集, < 代表依赖文件集,通过这条命令,make 会把所有的 .c 文件挨个执行下面的命令来生成对应的 .o 文件,至于如何对应的,就是通过模式匹配字符 % ,根据相同的文件名把 .c 文件生成同名的 .o 文件。
模式匹配字符 % 看可以代表任意个字符,在一个目标模式中必须要包含 % ,它可以代表文件后缀或者文件名。在模式中,把 % 所匹配的的内容叫做 茎 。比如说,我们在上面的例子中,%.o:%.c,假如当前目录中有 111.c 222.c 333.c ,那么 111 222 333 这些被 % 所代表的内容就是茎,并且当目标和依赖都包含 % 时,依赖的茎会传递给目标,作为目标的茎。比如上面命令生成的所有 .o 目标文件,都会用相应的 .c 文件的名称作为自己的名称,比如 111.c 生成 111.o,222.c 生成 222.o,333.c 生成 333.o,这就是茎的传递。 这里有一点要注意的是,如果模式中包含目录 / ,那么在模式匹配的时候会先去除目录,等模式匹配完成后再加上目录。比如现在有一个依赖的模式是 src/a%c.c,该模式的目标也含有一个模式 d%f.o,实际上就是这样的
d%f.o:src/a%c.c
假如说,依赖文件的模式 src/a%c.c 被匹配为 src/abc.c 文件,那么这个模式的茎应该是 src/b,当把这个茎传递给目标的时候,b 匹配目标中的 % ,也就是 dbf.o ,在加上目录,最终的目标文件应该是 src/dbf.o。
关键字 | 功能 |
---|---|
ifeq | 判断参数是否相等,相等为 true,不相等为 false。 |
ifneq | 判断参数是否相等,不相等为 true,相等为 false。 |
ifdef | 判断是否有值,有值为 true,没有值为 false。 |
ifndef | 判断是否有值,没有值为 true,有值为 false。 |
这四个关键字都要搭配 else 和 endif 来使用,并且以 endif 介绍。 例1:如果变量 CC 值为 gcc 使用 gcc 编译,否则使用 g++ 编译。
ifeq($(CC), gcc)
gcc src.c -o exe
else
g++ src.c -o exe
endif
例2:如果没有定义变量 Src 则定义该变量
ifndef Src
Src=1.c 2.c
endif
以上四个条件控制关键字只对 makefile 命令起作用,对于 shell 的命令不起作用。
make 在执行命令的时候会把命令打印到标准输入输出设备,如果命令前加一个 @ 则不会打印。
可以看到,如果不加 @ 的话,执行 make 时会把要执行的命令打印出来,然后再执行这个命令。有时候还会在 @ 前面加一个 - ,表示即便出错也不报错,继续执行,这个在删除命令中更常见,因为如果重复删除某个文件,shell 命令会报错,可以通过在命令前加 - 或者 rm -f 两种方法解决。
-@echo rm *.c #如果 .c 不存在不会报错
@echo rm -f *.c #强制删除,.c 不存在也不会报错
另外我们还可以通过 make 的参数来设置。 ① -n 和 –just-print 参数 执行时只打印所要执行的命令,不执行命令。make 会打印出所有要执行的命令,其中包括了使用的 @ 字符开始的命令。通过这个选项就可以按执行顺序打印出 makefile 中所需要执行的所有命令来达到查看待执行命令的目的。
可以看到,加了这两个参数后,make 只会打印 makefile 中待执行的 shell 命令(包括 @ 开头的),但是不会执行命令。 ② -s 和 –silent 参数 关闭 make 执行时所有的执行命令的回显,相当于所有的命令行都使用 @ 开头,在命令非常多的时候,要比 @ 更方便。
可以看到,加了这两个参数后,make 只会执行 makefile 中的 shell 命令,但是不会回显命令,即便不加 @ 也不会回显。 make 的更多参数可以通过 man 去查看。
通过 define 和 endef 可以定义一个命令包,来把一连串的命令打包在一起,在执行的时候,会把命令包中的命令一块执行。命令包的使用方法和变量一样,也需要 $ 符来说明。 define 的语法如下:
define name #name是命令包的名字,通过name来调用命令包
cmd1 #多行命令
cmd2
cmd...
endef
和 c/c++ 引入头文件一样,makefile 也可以引入其他文件,通过 include 关键字来实现,其语法如下
include <file> #file 是 shell 中所支持的文件
-include <file> #忽略文件不存在或无法创建等错误提示
make 命令在执行的时候,如果遇到 include 关键字,会在当前文件中暂停,转去 include 所引入的文件去读取。 include 关键字所在的行首可以含有(任意个)空格,这些空格在读取的时候会被自动忽略,但是,绝对不能使用 Tab 开始,因为 Tab 开始的都会被作为 shell 命令,会把 include 当作 shell 命令来处理。如果包含多个文件,要使用空格分隔开。使用 include 引入的 makefile 文件中,如果存在函数或者变量的引用,它们会在包含的 makefile 中展开。 make 在搜索引入文件时,假如使用 include 包含文件的时候使用相对路径或者当前目录下没有这个文件,make 会根据文件名首先在我们通过 make -I 或 make --include-dir 指定的路径中寻找文件(如果我们显示指定了这个参数的话),然后再去 usr/gnu/include 、 usr/local/include 和 usr/include 这几个目录中寻找。在寻搜索过程中,如果找到了那么就会停止搜索,如果没找到的话会按照上面的顺序依次寻找。如果都没有找到,make 将会提示一个文件没有找到的警告(如果不想看到这个警告,可以在 include 前面加一个 - ),但是不会退出,而是继续执行 makefile 的后续内容。当整个 makefile 扫描完毕后,make 会尝试通过规则来创建被 include 引入但搜索失败的的那个文件。如果创建失败,文件将会保存退出。
我们每写一个命令就会换行,重启一行加 Tab 键写第二个命令。这种不在同一行的命令是互不影响的,也就是上一个命令的结果不会影响下一个命令。如果我们希望上一条命令的结果应用在下一条命令时,可以使用分号 ; 分隔这两条命令 ,并把这两个命令写在同一行。
这样的效果有点像 shell 中 -exec、-ok 和 xargs 这样的选项所起到的效果,又有点像 C++ 中的链式编程,我就姑且叫它链式命令吧,哈哈哈。
目标是规则中要生成的目标,在一个 makefile 中,至少要有一个最终目标。但是,目标是多种多样的,甚至有一些目标是不需要实际生成,比如前面说过的伪目标。下面介绍各种类型的目标。 ① 特殊目标
名称 | 功能 |
---|---|
.PHONY: | 这个目标的所有依赖被作为伪目标。伪目标是这样一个目标:当使用 make 命令行指定此目标时,这个目标所在的规则定义的命令、无论目标文件是否存在都会被无条件执行。 |
.SUFFIXES: | 这个目标的所有依赖指出了一系列在后缀规则中需要检查的后缀名。 |
.DEFAULT: | Makefile 中,这个特殊目标所在规则定义的命令,被用在重建那些没有具体规则的目标,就是说一个文件作为某个规则的依赖,却不是另外一个规则的目标时,make 程序无法找到重建此文件的规则,这种情况就执行 “.DEFAULT” 所指定的命令。 |
.PRECIOUS: | 这个特殊目标所在的依赖文件在 make 的过程中会被特殊处理:当命令执行的过程中断时,make 不会删除它们。而且如果目标的依赖文件是中间过程文件,同样这些文件不会被删除。 |
.INTERMEDIATE: | 这个特殊目标的依赖文件在 make 执行时被作为中间文件对待。没有任何依赖文件的这个目标没有意义。 |
.SECONDARY: | 这个特殊目标的依赖文件被作为中过程的文件对待。但是这些文件不会被删除。这个目标没有任何依赖文件的含义是:将所有的文件视为中间文件。 |
.IGNORE: | 这个目标的依赖文件忽略创建这个文件所执行命令的错误,给此目标指定命令是没有意义的。当此目标没有依赖文件时,将忽略所有命令执行的错误。 |
.DELETE_ON_ERROR: | 如果在 Makefile 中存在特殊的目标 “.DELETE_ON_ERROR” ,make 在执行过程中,荣国规则的命令执行错误,将删除已经被修改的目标文件。 |
.LOW_RESOLUTION_TIME: | 这个目标的依赖文件被 make 认为是低分辨率时间戳文件,给这个目标指定命令是没有意义的。通常的目标都是高分辨率时间戳。 |
.SILENT: | 出现在此目标 “.SILENT” 的依赖文件列表中的文件,make 在创建这些文件时,不打印出此文件所执行的命令。同样,给目标 “SILENT” 指定命令行是没有意义的。 |
.EXPORT_ALL_VARIABLES: | 此目标应该作为一个简单的没有依赖的目标,它的功能是将之后的所有变量传递给子 make 进程。 |
.NOTPARALLEL: | Makefile 中如果出现这个特殊目标,则所有的命令按照串行的方式执行,即使是存在 make 的命令行参数 “-j” 。但在递归调用的子make进程中,命令行可以并行执行。此目标不应该有依赖文件,所有出现的依赖文件将会被忽略。 |
② 强制目标 如果一个规则没有依赖和命令,仅有目标,那么在执行这条规则的时候,规则中的目标会被认为是每次更新的。也就是说,每当执行这条规则的时候都会认为这个目标被更新过,那么以这个目标作为依赖的那条规则中的命令,就总是会被执行。我们常用的用法是
clean:FORCE
@rm -f *.o $(BIN)
FORCE:
这样,每次执行时, rm 命令都会被执行,相当于把 clean 定义为伪目标的效果。但是,伪目标需要 make 命令指定作为最终目标,比如 make clean 这样来指定 clean 为最终目标才可以执行。 我们做个测试,首先不加强制目标测试一下,会生成中间文件
我们在加强制目标删除中间文件
③ 空目标文件 类似于伪目标,与伪目标的区别是,空目标文件可以是一个具体的文件,但是文件的内容我们不关心,一般设置为空文件。在执行时,与伪目标一样需要 make 显示指定为最终目标。 空目标文件一般用来记录上一次执行这条规则的时间,一般本规则的实现是通过 shell 的 touch 命令实现的。在规则的命令部分,当所有命令执行完毕后,使用 touch 命令作为最后一条命令来更新目标文件的时间戳,以此实现记录命令执行时间的功能。执行 make 命令时需要指定此目标作为最终目标,如果当前目录不存在这个文件(空目标文件), touch 命令会在第一次执行时创建一个文件。
Log:1.c
lpr -p $? # $?列出比目标文件(print)更新的所有依赖文件,并由lpr命令提交给打印机
touch Log
一般来说,一个空目标文件应该存在一个或者多个依赖文件,将这个空目标作为最终目标,当它所依赖的文件比它更新时,此目标所在的规则的命令行将被执行。也就是说,如果空目标文件的依赖文件被改变之后,空目标文件所在的规则中定义的命令会被执行。 ④ 多规则目标 如果将一个文件作为多个规则的目标,那么以这个文件为目标的规则的所有依赖文件都将会被合并为该目标文件的依赖文件列表,当这个依赖文件列表中的任何一个依赖文件比目标文件更新时,make 都会重建这个目标。虽然该目标是多个规则的目标,但是重建目标的命令只能出现在一个规则中。即使多个规则都含有重建该目标的命令,make 也只会使用最后一个规则中所定义的命令来重建,并且会提示一个错误信息。如果我们需要对相同的目标使用不同规则中定义的命令,就要使用双冒号规则来实现。
每当 makefile 中的一条命令运行完毕,make 都会去检查命令的返回码,如果命令返回代表成功的返回码,那么 make 会继续去执行下一条命令。当一个规则中的所有命令都运行完毕且都返回成功,那么这个规则就运行成功了。如果一个规则中的某个命令出错了即命令退出码非零,那么 make 就会终止执行当前规则,并且有可能会终止所有规则的执行。 make 命令执行后有三个退出码:
返回值 | 含义 |
---|---|
0 | 执行成功 |
1 | 运行时出错 |
2 | 使用make的 -q 选项,使得一些目标不需要更新 |
但实际上,命令执行完毕没有返回正确(返回码非0)并不说明就一定是错误的。比如我们执行 mkdir 建立一个目录,如果目录不存在,那么执行成功返回0,如果目录存在,那么就出错并停止执行。但是,目录存在并不代表有错误,我们的目的是达到的,只要结果是有了这个目录,我们的目的就达到了,也就是说,实际上这也是正确的,我们不希望 make 停止执行。解决方法是,在命令前加一个 - (Tab 键与命令之间),这样不管命令是否出错,是否返回0,都认为运行成功。另一个方法是,给 make 加上 -i 或 --ignore-errors 参数,代表忽略命令执行中的错误。这里还有一个参数 -k 或是 --keep-going ,这个参数代表,如果某规则中的命令出错了,那么就停止该规则的执行,但继续执行其它规则。 我们在 makefile 中的特殊目标中介绍过一个特殊目标 .IGNORE ,如果一个规则是以 .IGNORE 为目标,那么这个规则中的所有命令将会忽略错误。
在 makefile 中有两个函数 error 和 warning 可以控制 make 运行过程中的出错处理。当 make 执行过程中检测到某些错误时为用户提示消息,并且可以控制 make 执行过程是否继续。 ① 致命错误并停止 error
$(error TEXT...)
② 错误提示并继续 warning
$(warning TEXT...)
① 致命错误,通过 - 可以忽略错误,继续执行。 make 执行过程的致命错误都带有一个前缀字符串 ***
② 无法为重建目标 XXX 找到合适的规则,包括明确规则和隐含规则。
No rule to make target XXX.
No rule to make target XXX, needed by xxx.
更多的错误不再列出,make 会打印出出错位置以及错误原因,一般根据这些信息去修改即可。
GNU make 搜索默认的 makefile 的规则是在当前目录下依次寻找 GNUmakefile 、 makefile 和 Makefile ,并且是按照顺序寻找这三个文件,一旦找到,就开始读取这个文件并执行,并且不会再继续搜索。 我们可以测试一下,在当前目录创建GNUmakefile 、 makefile 和 Makefile 文件,并执行 make 且不指定文件,那么 make 的默认执行的文件按优先级排序为 GNUmakefile > makefile > Makefile。
当然,我们也可以给 make 命令指定一个 makefile 文件的名字。这个功能是通过 make 的参数 -f 或 --file 或 – makefile实现的。如果在 make 的命令行一次使用多个 -f 参数,那么,所有指定的 makefile 文件将会被连在一起传递给 make 执行。 这里需要区分一下:make target 是指定 target 为最终目标(如果规则会生成这个目标的话就生成 target),而 make -f target 是指执行 target 这个 makefile 文件。指定目标和指定文件是不同的,本节所讲为通过 -f 指定文件,指定目标将在后面讲解。
默认情况下,make 的最终目标是 makefile 文件中的第一个目标,其他目标都是为了生成这个最终目标而设置的中间目标。在默认情况下执行 make 命令生成的就是 makefile 中的最终目标,当然,我们也可以在执行 make 命令的时候,显示指定 make 的最终目标,直接在 make 后面接目标名(makefile 文件中存在的目标)即可。但是, - 开头或者包含 = 的目标不能被指定为 make 的最终目标,因为包含这两个字符的目标会被解析为命令行参数或是变量。即使是没有被我们明确写出来的目标也可以指定为为 make 的终极目标,只要是 make 可以找到一个隐含规则推导出该规则,那么这个隐含目标同样可以被指定成终极目标。比如下面的例子,我们在 makefile 中并没有写以 1.o 为目标的规则,甚至都没有 1.o 这个目标,但是却可以通过 make 来指定 1.o 为最终目标,这是因为 make 根据隐含规则可以推导出 1.o 的规则,并生成 1.o 这个目标。
在 make 的环境变量中有一个 MAKECMDGOALS 变量,这个环境变量中会存放我们所指定的终极目标列表,如果在命令行没有指定目标,那么这个变量就是空值。通过这个环境变量,我们可以结合条件逻辑控制来根据条件去执行命令。比如下面的例子,只要我们输入的命令不是 make clean (环境变量 MAKECMDGOALS 的值不是 clean),那么 makefile 会自动包含 1.d 和 2.d 这两个 makefile。
sources = 1.c 2.c
ifneq ( $(MAKECMDGOALS), clean)
include $(sources:.c=.d)
endif
make 命令同样可以指定最终目标为伪目标,在 makefile 发布时,常用的用于实现编译、安装、打包等功能的伪目标已经在前面 makefile 的伪目标章节列出,这些伪目标在大型工程中非常有用。
参数 | <-n> 、 <–just-print> 、 <–dry-run> 、 <–recon> |
---|---|
作用 | 不执行命令,仅打印命令,不管目标是否更新,只是把规则和规则下的命令打印出来,但不执行。这些参数通常用于调试 makefile 来查看规则中要执行的命令。 |
参数 | <-t>、 <–touch> |
---|---|
作用 | 把目标文件的时间更新,但不更改目标文件。也就是说,make 并不是真正的编译目标,只是把目标变成已编译过的状态。 |
参数 | <-q> 、 <–question> |
---|---|
作用 | 搜索目标。如果目标存在,无输出且不会执行编译;如果目标不存在,打印出错信息。 |
参数 | <-W> 、<–what-if> 、<–assume-new> 、<–new-file> |
---|---|
作用 | 后面都要加一个文件名,一般是源文件或依赖文件,Make 会根据规则推导来运行依赖于这个文件的命令,通常和 -n 参数一同使用,来查看这个依赖文件所发生的规则命令。 |
参数 | <-B> 、<–always-make> |
---|---|
作用 | 重新编译,即认为所有目标都要更新 |
参数 | <-f> 、<–file> 、<–makefile> |
---|---|
作用 | 后面加一个文件名,指定需要运行的 makefile 文件。 |
参数 | <-I> 、<–include-dir> |
---|---|
作用 | 后面跟一个目录,指定一个 makefile 文件的搜索路径,可以使用多个 -I 参数来指定多个目录。 |
这里只是例举了几个常用的参数,更多 make 的参数,可以通过下面的命令来查看,通过 help 帮助参数,可以查看到 make 命令的所有参数。
make -h
make --help
有时候我们会对整个工程的文件进行功能划分,划分好的每个模块都有自己的 makefile 编译规则。这时,就需要用到 makefile 的嵌套执行,也就是在一个 makefile 文件中包含另一个 makefile 文件,当 make 命令执行外层 makefile 的时候会转去执行它内部包含的内层 makefile 。 首先我们建立两个目录,每个目录下都包含一个 makefile 文件
两个 makefile 文件的内容如下,在 dir2 目录下的 makefile 调用了 dir1 目录下的 makefile 文件。
我们进入到 dir2 目录并执行 make 命令
我们看到,当执行到调用嵌套 makefile 文件的语句时,会提示进入被嵌套的 makefile 文件所在的目录,并执行被嵌套的 makefile 文件,执行完毕会提示离开被嵌套的 makefile 文件所在的目录,并且继续执行外层 makefile 。 这里有一条命令
#进入 ../dir1/ 目录,并执行 make 命令
cd ../dir1/ && $(MAKE)
这句话表示,先通过 cd 命令切换到目标目录,然后在目标目录下执行 make 命令,也就是执行目标目录下的 makefile 文件。 其实,这句话也可以被替代为
$(MAKE) -C ../dir1/
在 make 中,有一个环境变量 CURDIR ,此变量代表 make 的工作目录。当使用 make 的选项 -C 时,就会进入指定的目录中去执行 make 命令,然后此变量就会被重新赋值。
一般我们把最外层的那个 makefile 文件叫做总控 makefile 。
指定变量是否传递给下一级(内嵌的)makefile 文件,使用下面两个关键字
export val #将 val 传递给下级 makefile
unexport val #不将 val 传递给下级 makefile
如果我们仅用一个单一的关键字 export 而不指定变量名,则表示所有变量都传递给下一级 makefile。但是有两个特殊变量 SHELL 和 MAKEFLAGS,这两个变量不管是否使用关键字 export 都会传递给下一级 makefile 文件。其中MAKEFLAGS 变量中包含了 make 命令的参数信息。如果上层 makefile 文件中定义了 MAKEFLAGS 变量,或者说在执行 make 命令的时候使用了 make 的参数,那么这些参数将会被 MAKEFLAGS 变量传递到下一层 makefile 文件,并作为 make 的参数传递。make 命令中有几个参数选项不传递,它们是 -C 、 -f 、 -o 、 -h 和 -W 选项。如果不想传递 make 的参数,可以显示的把 MAKEFLAGS 定义为空,让它传递一个空参数给下层 makefile 。 下面举例说明 export 的用法,在这个例子中,dir1/ 目录中的 makefile 使用了一个未定义的变量 Vul,这个变量来自于它的上层 makefile (包含调用它的 makefile 文件),在 dir2/ 目录下的 makefile 嵌套了dir1/ 目录中的 makefile ,并且声明了一个 export 的变量 Vul ,那么这个 Vul 变量将传递给被他嵌套的所有其他 makefile 文件。
上面讲了很多理论知识,这一章就来实战写一个 mkefile 文件。首先准备几个文件,头文件 my_print.h 中声明了一些函数接口,这些函数接口在 my_print.c 和 print_hello.c 中实现,并在 main.c 中调用。
① 初级:使用变量、函数、模式规则
SrcFiles = $(wildcard *.c) #wildcard函数,匹配所有 .c 文件
ObjFiles = $(patsubst %.c, %.o, $(SrcFiles)) #内用替换函数及模式匹配
HeadDirs = ../header #头文件路径
#最终目标是可执行文件 exe
exe:$(ObjFiles)
gcc $(SrcFiles) -o exe -I $(HeadDirs)
#使用模式匹配来生成中间目标
%.o:%.c
gcc -c $< -o $@ -I $(HeadDirs)
clean:
-@rm -f *.o
-@rm -f exe
执行 make 命令来测试一下,这里可以明显的看到,根据模式匹配规则,每个 .c 都对应生成的 .o ,执行 make clean 可以删除目标文件。
② 中级:使用伪目标 all 来构建多个可执行文件,我们在使用的时候,只要把我们必须要执行的命令所在的规则中的目标作为 all 的依赖即可,这样就保证了这些命令一定会被执行。
1 .PHONY:all clean #伪目标,并不会真正生成相应文件
2 SrcFiles = $(wildcard *.c) #wildcard函数,匹配所有 .c 文件
3 ObjFiles = $(patsubst %.c, %.o, $(SrcFiles)) #内用替换函数及模式匹配
4 HeadDirs = ../header #头文件路径
5
6 #最终目标是伪目标 all,在这条规则中,all 是目标,所以 make 需要去构建它的依赖,也就是 exe1 exe2 ,这就保证了 exe1 exe2 对应规则中的命令一定会执行
7 all:exe1 exe2
8
9 exe1:$(ObjFiles)
10 gcc $(SrcFiles) -o exe1 -I $(HeadDirs)
11
12 exe2:$(ObjFiles)
13 gcc $(SrcFiles) -o exe2 -I $(HeadDirs)
14
15 #使用模式匹配来生成中间目标
16 %.o:%.c
17 gcc -c $< -o $@ -I $(HeadDirs)
18
19 clean:
20 -@rm -f *.o
21 -@rm -f exe1 exe2
测试一下
需要特别强调的是。这里的伪目标 all 作为最终目标,把所有要生成的多个可执行文件作为 all 的依赖。这样,在构建最终目标 all 的时候,就需要去构建它的依赖,也就是所有的可执行文件,这样就保证了,所有可执行文件所在规则中的构建命令一定会执行,并生成这些可执行文件,而 all 作为一个伪目标,并不会真正生成。假如你不这么做,去掉伪目标 all ,直接去写 exe1 和 exe2 的规则,你会发现,它们俩只会生成一个,哪个在前面就生成哪一个,这是因为一个文件中,最终目标只有一个,make 的规则推导是以生成最终目标为目的的。
③ 高级:借助隐含规则,这里借助隐含规则自动推导 .c -> .o 的规则,不用再显示写出中间目标 .o 的规则。
1 .PHONY:all clean
2 CC = gcc
3 CFLAGS = -Wall -g
4 BIN = exe
5 HeadDirs = ../header
6 SrcFiles = main.c print_hello.c my_print.c
7
8 all:$(BIN)
9
10 $(BIN):$(SrcFiles)
11 $(CC) $(CFLAGS) $(SrcFiles) -o $(BIN) -I $(HeadDirs)
12
13 clean:
14 -@rm -f *.o $(BIN)
测试一下
虽然说,在实际编写 makefile 的时候,有很多模板可以参考,并且大部分也都是使用的基本的规则命令。但是了解 makefile 的语法、变量、函数、规则等等更深层次的知识也是非常有必要的。 最后,作为 Linux 程序员或爱好者所必备的基本技能,shell 命令、VIM 编辑器、GCC 编译器、GDB 调试器、makefile 都已经讲解完毕,具体内容请查看本人 Linux 系列专栏中的文章,打好这些基本功是成为 Linux 开发高手的必备技能。