makefile
了解两个概念一个是⽬标(target),另⼀个就是依赖(dependency)。⽬标就是指要⼲什么,或说运⾏ make 后⽣成什么,⽽依赖是告诉 make 如何去做以实现⽬标。在 Makefile 中,⽬标和依赖是通过规则(rule)来表达的。
目标
首次编写makefile
上面Makefile 中的 all 就是我们 的⽬标,⽬标放在‘:’的前⾯,其名字可以是由字⺟和下划线‘_’组成 。echo “Hello World”就是⽣成⽬标的命令,这些命令可以是任何你可以在你的环境中运⾏的命令以及 make 所定义的函数等等。all ⽬标的定义,其实是定义了如何⽣成 all ⽬标,这我们也称之为规则.
⼀个 Makefile 中可以定义多个⽬标。调⽤ make 命令时,我们得告诉它我们的⽬标是什么,即要它⼲什么。当没有指明具体的⽬标是什么 时,那么 make 以 Makefile ⽂件中定义的第⼀个⽬标作为这次运⾏的⽬标。这“第⼀个”⽬标也称之 为默认⽬标(和是不是all没有关系)。当 make 得到⽬标后,先找到定义⽬标的规则,然后运⾏规则中的命令来达到构建⽬标的⽬的。
makefile中取消多余的命令行显示
在上面的指令中,多了很多的echo "......"的内容,这部分不是我们所期望的,如果要去掉,需要对上面的makefile进行一个改动,也就是在命令前加上一个@,这个符号就是告诉make,在运行的时候这一行命令不显示出来。
对makefile进行如下的改动,在all的后面加上test
此时test也被构建了。
依赖
如上面的makefile,all ⽬标后⾯的 test 是告诉 make,all ⽬标依赖 test ⽬标,这⼀依赖⽬标在 Makefile 中⼜被称之为先决条件。出现这种⽬标依赖关系时,make⼯具会按 从左到右的先后顺序先构建规则中所依赖的每⼀个⽬标。如果希望构建 all ⽬标,那么make 会在构建它之 前得先构建 test ⽬标,这就是为什么我们称之为先决条件的原因。
规则
⼀个规则是由⽬标(targets)、先决条件(prerequisites)以及命令(commands)所组成的。
需要指出的是,⽬标和先决条件之间表达的就是依赖关系(dependency),这种依赖关系指明在构建⽬标之前,必须保证先决条件先满⾜(或构建)。⽽先决条件可以是其它的⽬标,当先决条件是⽬标时,其必须先被构建出来。还有就是⼀个规则中⽬标可以有多个,当存在多个⽬标,且这⼀规则是 Makefile 中的第⼀个规则时,如果我们运⾏ make 命令不带任何⽬标,那么规则中的第⼀个⽬标将被视为是缺省⽬标。
规则的功能就是指明 make 什么时候以及如何来为我们重新创建⽬标,在 Hello World 例⼦中,不论我们 在什么时候运⾏ make 命令(带⽬标或是不带⽬标),其都会在终端上打印出信息来,和我们采⽤ make 进⾏代码编译时的表现好象有些不同。当采⽤ Makefile 来编译程序时,如果两次编译之间没有任何代码 的改动,理论上说来,我们是不希望看到 make 会有什么动作的,只需说“⽬标是最新的”,⽽我们的最终 ⽬标也是希望构建出⼀个“聪明的” Makefile 的。与 Hello World 相⽐不同的是,采⽤ Makefile 来进⾏ 代码编译时,Makefile 中所存在的先决条件都是具体的程序⽂件,后⾯我们会看到。
规则语法:
如上, all 是⽬标,test 是 all ⽬标的依赖⽬标,⽽@echo “Hello World”则是⽤于⽣成 all ⽬标的命令。
makefile的原理
foo.c
main.c
main.c
makefile
三段代码生成的依赖树
编译
上面的展示了测试结果,注意到了第⼆次编译并没有构建⽬标⽂件的动作吗?但为什么有构建simple可执⾏程序的动作呢?为了明⽩为什么,我们需要了解 make 是如何决定哪些⽬标(这⾥是⽂件)是需要重新编译的。为什么 make会知道我们并没有改变 main.c 和 foo.c 呢?答案很简单,通过⽂件的时间戳!当 make 在运⾏⼀个规则时,我们前⾯已经提到 了⽬标和先决条件之间的依赖关系,make 在检查⼀个规则时,采⽤的⽅法是:如果先决条件中相关的⽂件的时间戳⼤于⽬标的时间戳,即先决条件中的⽂件⽐⽬标更新,则知道有变化,那么需要运⾏规则当中 的命令重新构建⽬标。这条规则会运⽤到所有与我们在 make时指定的⽬标的依赖树中的每⼀个规则。⽐如,对于 simple 项⽬,其依赖树中包括三个规则,make 会检查所有三个规则当中的⽬标(⽂件)与先决条件(⽂件)之间的时间先后关系,从⽽来决定是否要重新创建规则中的⽬标。(什么是时间戳时间戳是使用数字签名技术产生的数据,签名的对象包括了原始文件信息、签名参数、签名时间等信息。时间戳系统用来产生和管理时间戳,对签名对象进行数字签名产生时间戳,以证明原始文件在签名时间之前已经存在。时间戳(timestamp),通常是一个字符序列,唯一地标识某一刻的时间。)
第二次构建的时候为什么simple会被重新构建?
是因为simple文件不存在,我们在这次构建的目标是all,而all在我们编译的过成中并不生成,所以第二次make的时候找不到,所以又重新编译了一遍。
修改makefile
第二次编译时不需要重新生成
一个文件是否改变不是看这个文件的大小是否改变,而是看这个文件的时间戳是否发生了变化。可以直接使用touch指令对文件的时间戳进行修改。
这时候就会进行重新编译
假目标
如果我们的创建了一个clean文件之后,继续去运行make clean,这时候不是按照我们前面运行的make clean进行清理文件。
为什么出现上面的原因?
因为这个时候make 将clean单程是一个文件,并且在当前的目录下找到了这个文件,再加上clean目标没有任何的先决条件,这时候进行make clean时,系统会认为clean是最新的
如何解决上面的问题?使用假目标,假目标最从常用清净就是避免所定义的目标和的已经存在文件是从重名的情况,假⽬标可以采⽤.PHONY 关键字来定义,需要注意的是其必须是⼤写字⺟。使用假目标修改makefile
采⽤.PHONY 关键字声明⼀个⽬标后,make 并不会将其当作⼀个⽂件来处理,⽽只是当作⼀个概念上的⽬标。对于假⽬标,我们可以想像的是由于并不与⽂件关联,所以每⼀次 make 这个假⽬标时,其所在的规则中的命令都会被执⾏。
变量
变量的使用可以提高makefile的可维护性。⼀个变量的定义很简单,就是⼀个名字(变量名)后⾯跟上⼀个等号,然后在等号的后⾯放这个变量所期望的值。对于变量的引⽤,则需要采⽤$(变量名)或者${变量名}这种模式。在这个 Makefile 中,我们引⼊了 CC 和 RM 两个变量,⼀个⽤于保存编译器名,⽽另⼀个⽤于指示删除⽂件的命令是什么。还有就是引⼊了 EXE 和 OBJS 两个变量,⼀个⽤于存放可执⾏⽂件名,可另⼀个则⽤于放置所有的⽬标⽂件名。采⽤变量的话,当我们需要更改编译器时,只需更改变量赋值的地⽅,⾮常⽅便,如果不采⽤变量,那我们得更改每⼀个使⽤编译器的地⽅,很是麻烦。
自动变量
对于每⼀个规则,⽬标和先决条件的名字会在规则的命令中多次出现,每⼀次出现都是⼀种麻烦,更为麻烦的是,如果改变了⽬标或是依赖的名,那得在命令中全部跟着改。有没有简化这种更改的⽅法呢?这我们需要⽤到 Makefile 中的⾃动变量,最常用包括:
$@⽤于表示⼀个规则中的⽬标。当我们的⼀个规则中有多个⽬标时,$@所指的是其中任何造成命令被运⾏的⽬标。
$^则表示的是规则中的所有先择条件。
$<表示的是规则中的第⼀个先决条件。
在 Makefile 中‘$’具有特殊的意思,因此,如果想采⽤ echo 输出‘$’,则必需⽤两个连着的‘$’。还有就是,$@对于 Shell 也有特殊的意思,我们需要在“$$@”之前再加⼀个脱字符‘\’。
特殊变量
MAKE变量
它表示的是make 命令名是什么。当我们需要在 Makefile 中调⽤另⼀个 Makefile 时需要⽤到这个变量,采⽤这种⽅式,有利于写⼀个容易移植的 Makefile。
MAKECMDGOALS变量
它表示的是当前⽤户所输⼊的 make ⽬标是什么。
MAKECMDGOALS 指的是⽤户输⼊的⽬标,当我们只运⾏ make 命令时,虽然根据Makefile 的语法,第⼀个⽬标将成为缺省⽬标,即 all ⽬标,但 MAKECMDGOALS 仍然是空,⽽不是all,这⼀点我们需要注意。
递归扩展变量
示例了使⽤等号进⾏变量定义和赋值,对于这种只⽤⼀个“=”符号定义的变量,我们称之为递归扩展变量(recursively expanded variable)。
除了递归扩展变量还有⼀种变量称之为简单扩展变量(simply expanded variables),是⽤“:=”操作符来定义的。对于这种变量,make 只对其进⾏⼀次扫描和替换。
另外还有一种条件赋值符“?=”,条件赋值的意思是当变量以前没有定义时,就定义它并且将左边的值赋值给它,如果已经定义了那么就不再改变其值。条件赋值类似于提供了给变量赋缺省值的功能。
此外,还有"+="操作符,对变量进⾏赋值的⽅法
override指令
在设计 Makefile 时,我们并不希望⽤户将我们在 Makefile 中定义的某个变量覆盖掉,那就得⽤ override 指令了。
模式
如果对于每⼀个⽬标⽂件都得写⼀个不同的规则来描述,那会是⼀种“体⼒活”,太繁了!对于⼀个⼤型项⽬,就更不⽤说了。Makefile 中的模式就是⽤来解决我们的这种烦恼的。
与 simple 项⽬前⼀版本的 Makefile 相⽐,最为直观的改变就是从⼆条构建⽬标⽂件的规则变成了⼀条。模式类似于我们在 Windows 操作系统中所使⽤的通配符,当然是⽤“%”⽽不是“*”。采⽤了模式以后,不论有多少个源⽂件要编译,我们都是应⽤同⼀个模式规则的,很显然,这⼤⼤的简化了我们的⼯作。使⽤了模式规则以后,你同样可以⽤这个 Makefile 来编译或是清除 simple 项⽬,这与前⼀版本在功能上是完全⼀样的。
函数
函数是 Makefile 中的另⼀个利器,现在我们看⼀看采⽤函数如何来简化 simple 项⽬的 Makefile。对于simple 项⽬的 Makefile,尽管我们使⽤了模式规则,但还有⼀件⽐较恼⼈的事,我们得在这个Makefile中指明每⼀个需要被编译的源程序。对于⼀个源程序⽂件⽐较多的项⽬,如果每增加或是删除⼀个⽂件都得更新 Makefile,其⼯作量也不可⼩视!
采⽤了 wildcard 和 patsubst 两个函数后 simple 项⽬的 Makefile。可以先⽤它来编译⼀下 simple 项⽬代码以验证其功能性。需要注意的是函数的语法形式很是特别,对于我们来说只要记住其形式就⾏了。
现在,我们来模拟增加⼀个源⽂件的情形,看⼀看如果我们增加⼀个⽂件,在 Makefile 不做任何更改的情况下其是否仍能正常的⼯作。增加⽂件的⽅式仍然是采⽤ touch 命令,通过 touch 命令⽣成⼀个内容是空的 bar.c 源⽂件,然后再运⾏ make 和 make clean。
addprefix函数
addprefix 函数是⽤来在给字符串中的每个⼦串前加上⼀个前缀,其形式是:$(addprefix prefix, names...)
filter函数
filter 函数⽤于从⼀个字符串中,根据模式得到满⾜模式的字符串,其形式是:$(filter pattern..., text)
结果来看,经过 filter 函数的调⽤以后,source变量中只存在.c ⽂件和.s ⽂件了,⽽.h⽂件则则被过滤掉了。
filter-out函数
filter-out 函数⽤于从⼀个字符串中根据模式滤除⼀部分字符串,其形式是:$(filter-out pattern..., text)
patsubst函数
patsubst 函数是⽤来进⾏字符串替换的,其形式是:$(patsubst pattern, replacement, text)
上述代码中 mixed 变量中包括了.c ⽂件也包括了.o ⽂件,采⽤patsubst 函数进⾏字符串替换时,我们希望将所有的.c ⽂件都替换成.o ⽂件。上图是最后的运⾏结果。
strip
strip 函数⽤于去除变量中的多余的空格,其形式是:$(strip string)
wildcard函数
wildcard 是通配符函数,通过它可以得到我们所需的⽂件,这个函数类似我们在 Windows 或Linux 命
令⾏中的“*”。其形式是:$(wildcard pattern)
makefile拔高
创建目录
毫⽆疑问,我们在编译项⽬之前希望⽤于存放⽂件的⽬录先准备好,当然,我们可以在编译之前通过⼿动来创建所需的⽬录,但这⾥我们希望采⽤⾃动的⽅式。makefile的依赖树的样子是这样的。
这个依赖图从概念上说来是对的,但从 Makefile 的实现上存在⼀些问题。我们说 all 是⼀个⽬标,如果 all 直接依赖 objs 和 exes ⽬录的话,那应该如何创建⽬录呢?首先写一个makefile【注意代码的最后一行不能换行,表示一个依赖】
改进依赖关系图
改进上面的makefile
在这个 Makefile 中,需要注意的是 OBJS 变量即是⼀个依赖⽬标也是⼀个⽬录,在不同的场合其意思是不同的。⽐如,第⼀次 make 时,由于 objs 和 exes ⽬录都不存在,所以 all ⽬标将它们视作是⼀个先决条件或者说是依赖⽬标,接着 Makefile 先根据⽬录构建规则构建 objs 和 exes ⽬标,即Makefile 中的第⼆条规则就被派上了⽤场。构建⽬录时,第⼆条规则中的命令被执⾏,即真正的创建了 objs 和 exes ⽬录。当我们第⼆次进⾏ make 时,此时,make 仍以 objs 和 exes 为⽬标,但从⽬录构建规则中发现,这两个⽬标并没有依赖关系,⽽且能从当前⽬录中找到 objs 和 exes ⽬录,即认为 objs 和 exes ⽬标都是最新的,所以不⽤再运⾏⽬录构建规则中的命令来创建⽬录。
更新后代码的依赖树关系
接下来也得为 Makefile 创建⼀个 clean ⽬标,专⻔⽤来删除所⽣成的⽬标⽂件和可执⾏⽂件。加 clean 规则还是相当简单,需要再增加了两个变量,⼀个是RM,另⼀个则是 RMFLAGS。
增加头文件
将文件放进目录
为了将⽬标⽂件或是可执⾏程序分别放⼊所创建的 objs 和 exes ⽬录中,我们需要⽤到 Makefile中的⼀个函数 —— addprefix。对上面的makefile进行修改。
最⼤的变化除了增加了对于 addprefix 函数的运⽤为每⼀个⽬标⽂件加上“objs/”前缀外,还有⼀个很⼤的变化是,我们需要在构建⽬标⽂件的模式规则中的⽬标前也加上“objs/”前缀,即增加“$(DIR_OBJS)/”前缀。之所以要加上,是因为规则的命令中的-o 选项需要以它作为⽬标⽂件的最终⽣成位置,还有就是因为 OBJS 也加上了前缀,⽽要使得 Makefile 中的⽬标创建规则被运⽤,也需要采⽤相类似的格式,即前⾯有“objs/”。此外,由于改动后的 Makefile 会将所有的⽬标⽂件放⼊ objs ⽬录当中,⽽我们的 clean 规则中的命令包含将 objs ⽬录删除的操作,所以我们可以去除命令中对 OBJS 中⽂件的删除。这导致的改动就是 Makefile 中的最后⼀⾏中删除了$(OBJS)。同样的方法将 test 放入到 exes 文件夹中。