概述
博客内容包含linux下make命令的使用与makefile的书写规则等,希望通过本文档使读者对make命令makefile文件有进一步了解,由于鄙人经验学识有限文档中会有描述不准确以及理解偏差,欢迎读者指正。fythons@sina.com
从一只猫说起hello kitty
linux系统中的make命令与makefile文件
make与makefile
在linux系统中make是一个非常重要的编译命令,不管是自己进行项目开发还是安装应用软件,我们都经常要用到make或makeinstall。利用make工具,我们可以将大型的开发项目分解成为多个更易于管理的模块,一个工程中的源文件不计数,其按类型、功能、模块分别放在若干个目录中,makefile定义了一系列的规则来指定,哪些文件需要先编译,哪些文件需要后编译,哪些文件需要重新编译,甚至于进行更复杂的功能操作,因为makefile就像一个Shell脚本一样,其中也可以执行操作系统的命令。makefile 带来的好处就是“自动化编译”,一旦写好,只需要一个make命令,整个工程完全自动编译,极大的提高了软件开发的效率。
Make的工作原理
当make 命令被执行时,它会扫描当前目录下Makefile或makefile文件找到目标以及其依赖。如果这些依赖自身也是目标,继续为这些依赖扫描Makefile 建立其依赖关系,然后编译它们。一旦主依赖编译之后,然后就编译主目标,假设你对某个源文件进行了修改,你再次执行make 命令,它将只编译与该源文件相关的目标文件,因此,编译完最终的可执行文件节省了大量的时间。
Make命令的参数
-f:指定“makefile”文件;
-i:忽略命令执行返回的出错信息;
-s:沉默模式,在执行之前不输出相应的命令行信息;
-r:禁止使用build-in规则;
-n:非执行模式,输出所有执行命令,但并不执行;
-t:更新目标文件;
-q:make操作将根据目标文件是否已经更新返回"0"或非"0"的状态信息;
-p:输出所有宏定义和目标文件描述;
-d:Debug模式,输出有关文件和检测时间的详细信息。
-C dir:在读取makefile 之前改变到指定的目录dir;
-I dir:当包含其他makefile文件时,利用该选项指定搜索目录;
-h:help文挡,显示所有的make选项;
-w:在处理makefile之前和之后,都显示工作目录。
make命令隐藏了什么
linux 编译hello_kitty 只需要简单的make hello_kitty
上述过程可分解为四部分,预处理(Propressing),编译(Compilation),汇编(Assembly),链接(Linking).
如下图所示:
预编译
预编译器cpp 将hello_kitty.c与stdio.h编译成.i 文件,c++的源代码文件扩展名为cpp或cxx,头文件扩展名为hpp,而与编译后的文件为.ii。预编译过程相当于执行
gcc -E hello_kitty.c -o hello_kitty.i
预编译过程主要处理以#开头的预处理指令,#include #define等,处理过程如下:
1 将所有#define删除,并展开所有宏定义
2 处理所有条件预编译指令#if #ifdef #elif #else #endif等
3 处理#include 预编译指令,将包含的文件插入到预编译指令的位置(递归进行,所包含的文件可能包含其他文件)
4 删除所有注释// /* */
5 添加行号和文件名标识,如:#2 hello_kitty.c 2 ,用于编译时编译器产生调试信息和编译时产生的错误和警告时能显示行号。
6 保留说有的#pragma编译器指令
经过预编译的.i文件,不包含任何宏定义,并且所包含的文件也被插入进来。
编译
编译过程就是将预编译产生的.i文件经过一系列的词法分析,语法分析,语意分析,优化后产生汇编代码文件,.s (详情可查阅《编译原理》或《编译系统透视》)相当于
gcc -S hello_kitty.i -o hello_kitty.s
高版本的GCC将预编译和编译合为一步,后台调用ccl来完成,预编译和编译,我们可以用
ccl hello_kitty.c或
gcc -S hello_kitty.c -o hello_kittu.s
都可以得到hello_kitty.s文件
gcc只是GCC编译器后台程序的包装,他会根据不同的参数来掉用后台程序
如ccl cclplus jcl 等(深入学习可参考《深入分析gcc》或《自制编译器》)
汇编
汇编器将汇编代码转换成成机器指令,每一条汇编语句对应一条或几条机器指令,根据汇编指令和机器指令一一翻译的过程,汇编过程我们可以用
as hello_kitty.s -o hello_kitty.o或
gcc -c hello_kitty.s -o hello_kitty.o来完成。
或者使用gcc 命令从c源文件直接生成目标文件
gcc -c hello_kitty.c -o hello_kitty.o
链接
将库文件与目标文件链接成可执行文件的过程。
make命令的运行
make最简单的用法就是直接在命令行下输入make命令,make命令会找当前目录的makefile来执行,一切都是自动的。或者make targetfile ,但也有时你也许只想让make重编译某些文件,而不是整个工程,而又有的时候你有几套编译规则,想在不同的时候使用不同的编译规则,等等,本章节就是讲述如何使用make命令的使用。
make的退出码
make命令执行后有三个退出码:
0 表示成功执行。
1 如果make运行时出现任何错误,其返回1。
2 如果你使用了make的“-q”选项,并且make使得一些目标不需要更新,那么返回2。
指定makefile
GNU make找寻默认的Makefile的规则是在当前目录下依次找三个文件“GNUmakefile”、“makefile”和“Makefile”。其按顺序找这三个文件,一旦找到,就开始读取这个文件并执行。
当前,我们也可以给make命令指定一个特殊名字的Makefile。要达到这个功能,我们要使用make的-f 或是--file 参数(--makefile 参数也行)。例如,我们有个makefile的名字是“hchen.mk”,那么,我们可以这样来让make来执行这个文件:make -f hchen.mk
指定目标
make的最终目标是makefile中的第一个目标,而其它目标一般是由这个目标连带出来的。这是make的默认行为。当然,你的makefile中的第一个目标是由许多个目标组成,你可以指示make,让其完成你所指定的目标。要达到这一目的很简单,需在make命令后直接跟目标的名字就可以完成(如前面提到的“make hello_kitty”形式)任何在makefile中的目标都可以被指定成终极目标,甚至没有被我们明确写出来的目标也可以成为make的终极目标,也就是说,只要make可以找到其隐含规则推导规则,那么这个隐含目标同样可以被指定成终极目标。
有一个make的环境变量叫MAKECMDGOALS,这个变量中会存放你所指定的终极目标的列表,如果在命令行上,你没有指定目标,那么,这个变量是空值。这个变量可以让你使用在一些比较特殊的情形下。比如下面的例子:
sources = foo.c bar.c
ifneq ( $(MAKECMDGOALS),clean)
include $(sources:.c=.d)
endif
基于上面的这个例子,只要我们输入的命令不是“make clean”,那么makefile会自动包含“foo.d”和“bar.d”这两个makefile。
使用指定终极目标的方法可以很方便地让我们编译我们的程序,例如下面这个例子:
.PHONY: all
all: pro1 pro2 pro3 pro4
从这个例子中,我们可以看到,这个makefile中有四个需要编译的程序——“pro1”,“pro2”,
“pro3”和“pro4”,我们可以使用“make all”命令来编译所有的目标(如果把all置成第一个目标,那么只需执行“make”),我们也可以使用“make pro2”来单独编译目标“pro2”
即然make可以指定所有makefile中的目标,那么也包括“伪目标”,我们可以根据这种性质来让我们的makefile根据指定的不同的目标来完成不同的事。在Unix世界中,软件发布时,特别是GNU这种开源软件的发布时,其makefile都包含了编译、安装、打包等功能。
我们可以参照这种规则来书写我们的makefile中的目标。
all:这个伪目标是所有目标的目标,其功能一般是编译所有的目标。
clean:这个伪目标功能是删除所有被make创建的文件。
install:这个伪目标功能是安装已编译好的程序,其实就是把目标执行文件拷贝到指定的目标中去。
print:这个伪目标的功能是例出改变过的源文件。
tar:这个伪目标功能是把源程序打包备份。也就是一个tar文件。
dist:这个伪目标功能是创建一个压缩文件,一般是把tar文件压成Z文件。或是gz文件。
TAGS:这个伪目标功能是更新所有的目标,以备完整地重编译使用。
check和test:这两个伪目标一般用来测试makefile的流程。
如果你要书写这种功能,最好使用这种名字命名你的目标,这样规范一些,规范的好处就是——不用解释,大家都明白。
检查规则
有时候,我们不想让我们的makefile中的规则执行起来,我们只想检查一下我们的命令,或是执行的序列。于是我们可以使用make命令的下述参数:
-n, --just-print, --dry-run, --recon 不执行参数,这些参数只是打印命令,不管目标是否更新,把规则和连带规则下的命令打印出来,但不执行,这些参数对于我们调
试makefile很有用处。
-t, --touch 这个参数的意思就是把目标文件的时间更新,但不更改目标文件。也就是说,make假装编译目标,但不是真正的编译目标,只是把目标变成已编译过的状态。
-q, --question 这个参数的行为是找目标的意思,也就是说,如果目标存在,那么其什么也不会输出,当然也不会执行编译,如果目标不存在,其会打印出一条出错信息。
-W <file>, --what-if=<file>, --assume-new=<file>, --new-file=<file>
这个参数需要指定一个文件。一般是是源文件(或依赖文件),Make会根据规则推导来运行依赖于这个文件的命令,一般来说,可以和“-n”参数一同使用,来查看这个依赖文件所发生的规则命令。
另外一个很有意思的用法是结合-p 和-v 来输出makefile被执行时的信息。
make命令的参数
下面列举了所有GNU make 3.80版的参数定义。其它版本和产商的make大同小异,不过其它产
商的make的具体参数还是请参考各自的产品文档。
-b, -m 这两个参数的作用是忽略和其它版本make的兼容性。
-B, --always-make 认为所有的目标都需要更新(重编译)。
-C <dir>, --directory=<dir> 指定读取makefile的目录。如果有多个“-C”参数,make的解释是后面的路径以前面的作为相对路径,并以最后的目录作为被指定目录。
如:“make -C ~/fany/test/prog”。
-debug[=<options>] 输出make的调试信息。它有几种不同的级别可供选择,如果没有参数,那就是输出最简单的调试信息。下面是<options>的取值:
a: 也就是all,输出所有的调试信息。(会非常的多)
b: 也就是basic,只输出简单的调试信息。即输出不需要重编译的目标。
v: 也就是verbose,在b选项的级别之上。输出的信息包括哪个makefile被解析,不需要被重编译的依赖文件(或是依赖目标)等。
i: 也就是implicit,输出所以的隐含规则。
j: 也就是jobs,输出执行规则中命令的详细信息,如命令的PID、返回码等。
m: 也就是makefile,输出make读取makefile,更新makefile,执行makefile的信息。
-d 相当于“–debug=a”。
-e, --environment-overrides 指明环境变量的值覆盖makefile中定义的变量的值。
-f=<file>, --file=<file>, --makefile=<file> 指定需要执行的makefile。
-h, --help 显示帮助信息。
-i , --ignore-errors 在执行时忽略所有的错误。
-I <dir>, --include-dir=<dir> 指定一个被包含makefile的搜索目标。可以使用多个“-I”参数来指定多个目录。
-j [<jobsnum>], --jobs[=<jobsnum>] 指同时运行命令的个数。如果没有这个参数,make运行命令时能运行多少就运行多少。如果有一个以上的“-j”参数,那么仅最后一个“-j”才是
有效的。
-k, --keep-going 出错也不停止运行。如果生成一个目标失败了,那么依赖于其上的目标就不会被执行了。
-l <load>, --load-average[=<load>], -max-load[=<load>] 指定make运行命令的负载。
-n, --just-print, --dry-run, --recon 仅输出执行过程中的命令序列,但并不执行。
-o <file>, --old-file=<file>, --assume-old=<file> 不重新生成的指定的<file>,即使这个目标的依赖文件新于它。
-p, --print-data-base 输出makefile中的所有数据,包括所有的规则和变量。这个参数会让一个简单的makefile都会输出一堆信息。如果你只是想输出信息而不想执行makefile,
你可以使用“make -qp”命令。如果你想查看执行makefile前的预设变量和规则,你可以使用“make –p –f /dev/null”。这个参数输出的信息会包含着你的makefile文件的文件名和行号,所以,用这个参数来调试你的makefile会是很有用的,特别是当你的环境变量很复杂的时候。
-q, --question 不运行命令,也不输出。仅仅是检查所指定的目标是否需要更新。如果是0则说明要更新,如果是2则说明有错误发生。
-r, --no-builtin-rules 禁止make使用任何隐含规则。
-R, --no-builtin-variabes 禁止make使用任何作用于变量上的隐含规则。
-s, --silent, --quiet 在命令运行时不输出命令的输出。
-S, --no-keep-going, --stop 取消“-k”选项的作用。因为有些时候,make的选项是从环境变量“MAKEFLAGS”中继承下来的。所以你可以在命令行中使用这个参数来让环境变量中的“-k”选项失效。
-t, --touch 相当于UNIX的touch命令,只是把目标的修改日期变成最新的,也就是阻止生成目标的命令运行。
-v, --version 输出make程序的版本、版权等关于make的信息。
-w, --print-directory 输出运行makefile之前和之后的信息。这个参数对于跟踪嵌套式调用make时很有用。
--no-print-directory 禁止“-w”选项。
-W <file>, --what-if=<file>, --new-file=<file>, --assume-file=<file> 假定目标<file>;需要更新,如果和“-n”选项使用,那么这个参数会输出该目标更新时的运行动作。如果没有“-n”那么就像运行UNIX的“touch”命令一样,使得<file>;的修改时间为当前时间。
--warn-undefined-variables 只要make发现有未定义的变量,那么就输出警告信息。
make的隐含规则
“隐含规则"也就是一种惯例,make会按照这种“惯例”来运行,哪怕我们的Makefile中没有书写这样的规则。例如,把.c文件编译成.o文件这一规则,你根本就不用写出来,make会自动推导出这种规则,并生成我们需要的.o 文件。
“隐含规则”会使用一些我们系统变量,我们可以改变这些系统变量的值来定制隐含规则的运行时的参数。如系统变量CFLAGS 可以控制编译时的编译器参数。我们还可以通过“模式规则”的方式写下自己的隐含规则。用“后缀规则”来定义隐含规则会有许多的限制。使用“模式规则”会更回得智能和清楚,但“后缀规则”可以用来保证我们Makefile的兼容性。我们了解了“隐含规则”,可以让其为我们更好的服务,也会让我们知道一些“约定俗成”了的东西,而不至于使得我们在运行Makefile时出现一些我们觉得莫名其妙的东西。有时候“隐含规则”也会给我们造成不小的麻烦。只有了解了它,我们才能更好地使用它。
使用隐含规则
如果要使用隐含规则生成你需要的目标,你所需要做的就是不要写出这个目标的规则。那么,make会试图去自动推导产生这个目标的规则和命令,如果make可以自动推导生成这个目标的规则和命令,那么这个行为就是隐含规则的自动推导。当然,隐含规则是make事先约定好的一些东西。
例如,我们有下面的一个Makefile:
hello_kitty : hello.o kitty.o
cc -o hello_kitty hello.o kitty.o $(CFLAGS) $(LDFLAGS)
我们可以注意到,这个Makefile中并没有写下如何生成hello.o和kitty.o这两目标的规则和命令。因为make的“隐含规则”功能会自动为我们自动去推导这两个目标的依赖目标和生成命令。make会在自己的“隐含规则”库中寻找可以用的规则,如果找到,那么就会使用。如果找不到,那么就会报错。在上面的那个例子中,make调用的隐含规则是,把.o 的目标的依赖文件设置成.c ,并使用C的编译命令cc -c $(CFLAGS) hello.c 来生成hello.o 的目标。也就是说,我们完全没有必要写下下面的两条规则:
hello.o : hello.c
cc -c hello.c $(CFLAGS)
kitty.o : kitty.c
cc -c kitty.c $(CFLAGS)
因为,这已经是“约定”好了的事了,make和我们约定好了用C编译器cc 生成.o 文件的规则,这就是隐含规则。当然,如果我们为.o 文件书写了自己的规则,那么make就不会自动推导并调用隐含规则,它会按照我们写好的规则地执行。还有,在make的“隐含规则库”中,每一条隐含规则都在库中有其顺序,越靠前的则是越被经常使用的,所以,这会导致我们有些时候即使我们显示地指定了目标依赖,make也不会管。
如下面这条规则:
hello.o : hello.p
依赖文件hello.p (Pascal程序的源文件)有可能变得没有意义。如果目录下存在了hello.c 文件,那么我们的隐含规则一样会生效,并会通过hello.c 调用C的编译器生成hello.o 文件。因为,在隐含规则中,Pascal的规则出现在C的规则之后,所以,make找到可以生成hello.o的C的规则就不再寻找下一条规则了。如果你确实不希望任何隐含规则推导,那么,你就不要只写出“依赖规则”,而不写命令。
隐含规则列表
这里我们将讲述所有预先设置(也就是make内建)的隐含规则,如果我们不明确地写下规则,那么,make就会在这些规则中寻找所需要规则和命令。当然,我们也可以使用make的参数-r或--no-builtin-rules选项来取消所有的预设置的隐含规则。当然,即使是我们指定了-r参数,某些隐含规则还是会生效,因为有许多的隐含规则都是使用了“后缀规则”来定义的,所以,只要隐含规则中有“后缀列表”(也就一系统定义在目标.SUFFIXES的依赖目标),那么隐含规则就会生效。默认的后缀列表是:.out, .a, .ln, .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。常用的隐含规则如下:
1. 编译C程序的隐含规则。
<n>.o 的目标的依赖目标会自动推导为<n>.c ,并且其生成命令是
$(CC) -c $(CPPFLAGS) $(CFLAGS)
2. 编译C++程序的隐含规则。
<n>.o 的目标的依赖目标会自动推导为<n>.cc 或是<n>.C ,并且其生成命令是
$(CXX) -c $(CPPFLAGS) $(CFLAGS) 。
3. 编译Pascal程序的隐含规则。
<n>.o 的目标的依赖目标会自动推导为<n>.p ,并且其生成命令是
$(PC) -c $(PFLAGS) 。
隐含规则使用的变量
在隐含规则中的命令中,基本上都是使用了一些预先设置的变量。你可以在你的makefile中改变这些变量的值,或是在make的命令行中传入这些值,或是在你的环境变量中设置这些值,无论怎么样,只要设置了这些特定的变量,那么其就会对隐含规则起作用。当然,你也可以利用make的-R或--no-builtin-variables 参数来取消你所定义的变量对隐含规则的作用。
例如,第一条隐含规则——编译C程序的隐含规则的命令是$(CC) -c $(CFLAGS) $(CPPFLAGS)Make默认的编译命令是cc,如果你把变量$(CC)重定义成gcc,把变量$(CFLAGS)重定义成-g,
那么,隐含规则中的命令全部会以gcc -c -g $(CPPFLAGS)的样子来执行了。
我们可以把隐含规则中使用的变量分成两种:一种是命令相关的,如CC;一种是参数相的关,如CFLAGS。下面是所有隐含规则中会用到的变量:
•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 : 汇编语言编译器参数。(当明显地调用.s 或.S 文件时)
•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文法分析器参数
定义模式规则
你可以使用模式规则来定义一个隐含规则。一个模式规则就好像一个一般的规则,只是在规则中,目标的定义需要有% 字符。% 的意思是表示一个或多个任意字符。在依赖目标中同样可以使用% ,只是依赖目标中的% 的取值,取决于其目标。有一点需要注意的是,% 的展开发生在变量和函数的展开之后,变量和函数的展开发生在make载入Makefile时,而模式规则中的% 则发生在运行时。
模式规则介绍
模式规则中,至少在规则的目标定义中要包含%,否则,就是一般的规则。目标中的% 定义表示对文件名的匹配,%表示长度任意的非空字符串。
例如:%.c 表示以.c 结尾的文件名(文件名的长度至少为3),而s.%.c 则表示以s.开头,.c 结尾的文件名(文件名的长度至少为5)。
如果%定义在目标中,那么,目标中的%的值决定了依赖目标中的%的值,也就是说,目标中的模式的%决定了依赖目标中%的样子。
例如有一个模式规则如下:
%.o : %.c ; <command ......>;
其含义是,指出了怎么从所有的.c 文件生成相应的.o 文件的规则。如果要生成的目标是a.ob.o ,那么%c 就是a.c b.c 。
一旦依赖目标中的% 模式被确定,那么,make会被要求去匹配当前目录下所有的文件名,一旦找到,make就会规则下的命令,所以,在模式规则中,目标可能会是多个的,如果有模式匹配出多个目标,make就会产生所有的模式目标,此时,make关心的是依赖的文件名和生成目标的命令这两件事。
模式规则示例
下面这个例子表示了,把所有的.c 文件都编译成.o 文件.
%.o : %.c
$(CC) -c $(CFLAGS) $(CPPFLAGS) $< -o $@
其中,$@ 表示所有的目标的挨个值,$< 表示了所有依赖目标的挨个值。这些奇怪的变量我们叫“自动化变量”,后面会详细讲述。
自动化变量
在上述的模式规则中,目标和依赖文件都是一系列的文件,那么我们如何书写一个命令来完成从不同的依赖文件生成相应的目标?因为在每一次的对模式规则的解析时,都会是不同的目标和依赖文件。自动化变量就是完成这个功能的。在前面,我们已经对自动化变量有所提涉,相信你看到这里已对它有一个感性认识了。所谓自动化变量,就是这种变量会把模式中所定义的一系列的文件自动地挨个取出,直至所有的符合模式的文件都取完了。这种自动化变量只应出现在规则的命令中。
下面是所有的自动化变量及其说明:
•$@ : 表示规则中的目标文件集。在模式规则中,如果有多个目标,那么,$@ 就是匹配于目标中模式定义的集合。
•$% : 仅当目标是函数库文件中,表示规则中的目标成员名。例如,如果一个目标是foo.a(bar.o),那么,$%就是bar.o,$@就是foo.a。如果目标不是函数库文件那么,其值为空。
•$< : 依赖目标中的第一个目标名字。如果依赖目标是以模式(即%)定义的,那么$<将是符合模式的一系列的文件集。注意,其是一个一个取出来的。
•$? : 所有比目标新的依赖目标的集合。以空格分隔。
•$^ : 所有的依赖目标的集合。以空格分隔。如果在依赖目标中有多个重复的,那个这个变量会去除重复的依赖目标,只保留一份。
•$+ : 这个变量很像$^ ,也是所有依赖目标的集合。只是它不去除重复的依赖目标。
•$* : 这个变量表示目标模式中% 及其之前的部分。如果目标是dir/a.foo.b,并且目标的模式是a.%.b,那么,$*的值就是dir/a.foo。
这个变量对于构造有关联的文件名是比较有较。如果目标中没有模式的定义,那么$*也就不能被推导出,但是,如果目标文件的后缀是make所识别的,那么$*就是除了后缀的那一部分。例如:如果目标是foo.c,因为.c是make所能识别的后缀名,所以,$*的值就是foo。这个特性是GNUmake的,很有可能不兼容于其它版本的make,所以,你应该尽量避免使用$*,除非是在隐含规则或是静态模式中。如果目标中的后缀是make所不能识别的,那么$*就是空值。
当你希望只对更新过的依赖文件进行操作时,$?在显式规则中很有用,例如,假设有一个函数库文件叫lib,其由其它几个object文件更新。那么把object文件打包的比较有效率的Makefile规则是:
lib : foo.o bar.o lose.o win.o
ar r lib $?
在上述所列出来的自动量变量中。四个变量($@ 、$< 、$% 、$* )在扩展时只会有一个文件,而另三个的值是一个文件列表。这七个自动化变量还可以取得文件的目录名或是在当前目录下的符合模式的文件名,只需要搭配上D 或F 字样。这是GNUmake中老版本的特性,在新版本中,我们使用函数dir或notdir就可以做到了。D的含义就是Directory,就是目录,F的含义就是File,就是文件。下面是对于上面的七个变量分别加上D 或是F 的含义:
$(@D) 表示$@ 的目录部分(不以斜杠作为结尾),如果$@ 值是dir/foo.o ,那么$(@D)就是dir,而如果$@中没有包含斜杠的话,其值就是.(当前目录)。
$(@F) 表示$@ 的文件部分,如果$@ 值是dir/foo.o ,那么$(@F) 就是foo.o ,$(@F)相当于函数$(notdir $@) 。
$(*D), $(*F) 和上面所述的同理,也是取文件的目录部分和文件部分。对于上面的那个例子,$(*D) 返回dir ,而$(*F) 返回foo
$(%D), $(%F) 分别表示了函数包文件成员的目录部分和文件部分。这对于形同archive(member)形式的目标中的member中包含了不同的目录很有用。
$(<D), $(<F) 分别表示依赖文件的目录部分和文件部分。
$(^D), $(^F) 分别表示所有依赖文件的目录部分和文件部分。(无相同的)
$(+D), $(+F) 分别表示所有依赖文件的目录部分和文件部分。(可以有相同的)
$(?D), $(?F) 分别表示被更新的依赖文件的目录部分和文件部分。
最后想提醒一下的是,对于$< ,为了避免产生不必要的麻烦,我们最好给$后面的那个特定字符都加上圆括号,比如,$(<) 就要比$< 要好一些。
还得要注意的是,这些变量只使用在规则的命令中,而且一般都是“显式规则”和“静态模式规则”其在隐含规则中并没有意义。
模式的匹配
一般来说,一个目标的模式有一个有前缀或是后缀的% ,或是没有前后缀,直接就是一个% 。
因为% 代表一个或多个字符,所以在定义好了的模式中,我们把%所匹配的内容叫做“茎”,例如%.c所匹配的文件“test.c”中“test”就是“茎”。因为在目标和依赖目标中同时有%时,依赖目标的“茎”会传给目标,当做目标中的“茎”。当一个模式匹配包含有斜杠(实际也不经常包含)的文件时,那么在进行模式匹配时,目录部分会首先被移开,然后进行匹配,成功后,再把目录加回去。在进行“茎”的传递时,我们需要知道这个步骤。例如有一个模式e%t ,文件src/eat 匹配于该模式,于是src/a 就是其“茎”,如果这个模式定义在依赖目标中,而被依赖于这个模式的目标中又有个模式c%r,那么,目标就是src/car 。(“茎”被传递)
重载内建隐含规则
你可以重载内建的隐含规则(或是定义一个全新的),例如你可以重新构造和内建隐含规则不同的命令,如:
%.o : %.c
$(CC) -c $(CPPFLAGS) $(CFLAGS) -D$(date)
你可以取消内建的隐含规则,只要不在后面写命令就行。如:
%.o : %.s
同样,你也可以重新定义一个全新的隐含规则,其在隐含规则中的位置取决于你在哪里写下这个规则。
隐含规则搜索算法
比如我们有一个目标叫T。下面是搜索目标T的规则的算法。
请注意,在下面,我们没有提到后缀规则,原因是,所有的后缀规则在Makefile被载入内存时,会被转换成模式规则。如果目标是archive(member)的函数库文件模式,那么这个算法会被运行两次,第一次是找目标T,如果没有找到的话,那么进入第二次,第二次会把member 当作T来搜索。
1. 把T的目录部分分离出来。叫D,而剩余部分叫N。(如:如果T是src/foo.o ,那么,D就是src/ ,N就是foo.o )
2. 创建所有匹配于T或是N的模式规则列表
3. 如果在模式规则列表中有匹配所有文件的模式,如% ,那么从列表中移除其它的模式。
4. 移除列表中没有命令的规则。
5. 对于第一个在列表中的模式规则:
(a) 推导其“茎”S,S应该是T或是N匹配于模式中% 非空的部分。
(b) 计算依赖文件。把依赖文件中的% 都替换成“茎”S。如果目标模式中没有包含斜框字符,而把D加在第一个依赖文件的开头。
(c) 测试是否所有的依赖文件都存在或是理当存在。(如果有一个文件被定义成另外一个规则的目标文件,或者是一个显式规则的依赖文件,那么这个文件就叫“理当存在”)
(d) 如果所有的依赖文件存在或是理当存在,或是就没有依赖文件。那么这条规则将被采用,退出该算法。
6. 如果经过第5步,没有模式规则被找到,那么就做更进一步的搜索。对于存在于列表中的第一个模式规则:
(a) 如果规则是终止规则,那就忽略它,继续下一条模式规则。
(b) 计算依赖文件。(同第5步)
(c) 测试所有的依赖文件是否存在或是理当存在。
(d) 对于不存在的依赖文件,递归调用这个算法查找他是否可以被隐含规则找到。
(e) 如果所有的依赖文件存在或是理当存在,或是就根本没有依赖文件。那么这条规则被采用,退出该算法。
(f) 如果没有隐含规则可以使用,查看.DEFAULT 规则,如果有,采用,把.DEFAULT的命令给T使用。一旦规则被找到,就会执行其相当的命令,而此时,我们的自动化变量的值才会生成。
————————————————
版权声明:本文为CSDN博主「y_fan」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_22182835/article/details/89467386