MakeFile最全实践1

本文深入介绍了Makefile的基础知识,包括规则、目标、依赖关系和命令。通过实例展示了如何编写和理解Makefile,从简单的HelloWorld程序到构建和编译更复杂的项目。文章强调了依赖树的概念,解释了make如何根据文件的时间戳决定哪些目标需要重新编译。此外,还探讨了变量、自动变量、模式、函数以及override指令的使用,以提高Makefile的可读性和可维护性。最后,通过一个更复杂的项目例子,演示了如何利用这些工具进一步优化Makefile。

1.Makefile基础

Makefile 的好坏对于项⽬开发有些什么影响呢?

设计得好的 Makefile,当我们重新编译时,只需编译那些上次编译成功后修改过的⽂件,也就是说编译的是⼀个 delta,⽽不是整个项⽬。反之,如果⼀个不好的 Makefile 环境,可能对于每⼀次的编译先要clean,然后再重新编译整个项⽬。两种情况的差异是显然的,后者将耗费开发⼈员⼤量的时间⽤于编译,也就意味着低效率。对于⼩型项⽬,低效问题可能表现得并不明显,但对于规模相对⼤的项⽬,那就⾮常的明显了。开发⼈员可能⼀天做个⼗次编译(甚⾄更少)就没有时间⽤于编码和测试(调试)了。这就是为什么通常⼤型项⽬都会有⼀个专⻔维护 Makefile 的⼀个⼩团队,来⽀撑产品的开发。

最为重要的是掌握⼆个概念,⼀个是⽬标(target),另⼀个就是依赖(dependency)。⽬标就是指要⼲什么,或说运⾏ make 后⽣成什么,⽽依赖是告诉 make 如何去做以实现⽬标。⽬标依赖是通过规则(rule)来表达的。学习Makefile一定要学会使用目标和依赖关系来思考。

准备环境

需要⼀台 Linux 机器,或是在 Windows 上安装 Cgywin 来学习 Makefile。为了验证 make ⼯具在你的环境中是否被正确的安装了,你可以运⾏“make -v”命令进⾏验证。下图是我在我的 Linux中运⾏“make -v”命令的输出结果,如果在你的环境中能看到相类似的 make 版本信息,那么说明make 在你的环境中是可⽤的,接下来就可以开始学习如何设计 Makefile 了。

Makefile实战(1)(看完这3篇就足够了)

规则

写⼀个在命令终端上输出“Hello World”的简单 Makefile。采⽤⼀个⽂本编辑器编写⼀个下图所示的 Makefile ⽂件,⽂件的存放⽬录可以是任意的。

Makefile实战(1)(看完这3篇就足够了)

需要提醒你注意的是 echo 前⾯必须只有 TAB(即你键盘上的 TAB键),且⾄少有⼀个 TAB,⽽不能⽤空格代替,这是我们需要学习的第⼀个 Makefile 语法。对于很多初学者,最为容易犯的就是这种“低级”错误。这种错误往往在对 Makefile 进⾏调试时,还不⼤容易发现,因为,从⽂本编辑器中看来,TAB 与空格有时没有太明显的区别。

Makefile 中第⼀个很重要的概念就是⽬标(target), 上图所示的 Makefile 中的 all 就是我们的⽬标,⽬标放在‘:’的前⾯,其名字可以是由字⺟和下划线‘_’组成 。echo “Hello World”就是⽣成⽬标的命令,这些命令可以是任何你可以在你的环境中运⾏的命令以及 make 所定义的函数等等,后面再细谈。all⽬标在这⾥就是代表我们希望在终端上打印出“Hello World”,有时⽬标会是⼀个⽐较抽象的概念。all ⽬标的定义,其实是定义了如何⽣成 all ⽬标,这我们也称之为规则,即上图 Makefile 中定义了⼀个⽣成 all ⽬标的规则。

你可能很急于看到这个 Makefile 的运⾏结果是什么,下图列出了上面的makfile的三种不同的运⾏⽅式以及每种⽅式的运⾏结果。

  • 第⼀种⽅式是只要在 Makefile 所在的⽬录下运⾏ make 命令,于是在终端上会输出⼆⾏,第⼀⾏实际上是我们在 Makefile 中所写的命令,⽽第⼆⾏则是运⾏命令的结果,你看到我们的 Makefile 确实在终端上打印了“Hello World”,真是太棒了!
  • 第⼆种⽅式,则是运⾏“make all”命令,这告诉 make ⼯具,我要⽣成⽬标 all,其结果也不⽤多说了。
  • 第三种⽅式则是运⾏ make test,指示 make 为我们⽣成 test ⽬标。由于我们根本没有定义 test ⽬标,所以运⾏结果是可想⽽知的,make 的确报告了不能找到 test ⽬标。

Makefile实战(1)(看完这3篇就足够了)

现在,我们对前面的Makefile 做⼀点⼩⼩的改动,如下图 所示。其中的改动就是增加了 test规则⽤于构建 test ⽬标 —— 在终端上打印出“Just for test!”。

Makefile实战(1)(看完这3篇就足够了)

从⽬前这两个 Makefile 的运⾏结果中我们学到了什么呢?我想有如下⼏点:

(1)⼀个 Makefile 中可以定义多个⽬标。

(2)调⽤ make 命令时,我们得告诉它我们的⽬标是什么,即要它⼲什么。当没有指明具体的⽬标是什么时,那么 make 以 Makefile ⽂件中定义的第⼀个⽬标作为这次运⾏的⽬标。这“第⼀个”⽬标也称之为默认⽬标(和是不是all没有关系)。如下图所示,make构建的目标就是all这个目标。

(3)当 make 得到⽬标后,先找到定义⽬标的规则,然后运⾏规则中的命令来达到构建⽬标的⽬的。现在所示例的 Makefile 中,每⼀个规则中都只有⼀条命令,⽽实际的 Makefile,每⼀个规则可以包含很多条命令。

Makefile实战(1)(看完这3篇就足够了)

对于前⾯的示例我们看到当运⾏ make 时,在终端上还打印出了 Makefile ⽂件中的命令(也就是echo的命令)。有时,我们并不希望它这样,因为这样可能使得输出的信息看起来有些混乱。要使 make 不打印出命令,只要做⼀点⼩⼩的修改就⾏了,改过的 Makefile 如下图所示,就是在命令前加了⼀个‘@’。 这⼀符号告诉 make,在运⾏时不要将这⼀⾏命令显示出来。更改后相应的运⾏结果如下图 所示。

Makefile实战(1)(看完这3篇就足够了)

Makefile实战(1)(看完这3篇就足够了)

如果各目标之间有依赖关系,则可以将被依赖的目标放到目标的“:”之后。例如,下面的改动之⼀是在各命令前增加了⼀个‘@’,之⼆则是在 all ⽬标之后的‘:’后加上了 test ⽬标。运⾏ make 和make test 命令的结果如下图 所示。从输出结果中,你会发现当运⾏ make 时,test 目标好像也被构建了!

Makefile实战(1)(看完这3篇就足够了)

Makefile实战(1)(看完这3篇就足够了)

这⾥需要引⼊ Makefile 中依赖关系的概念,图 中 all ⽬标后⾯的 test 是告诉 make,all ⽬标依赖test ⽬标,这⼀依赖⽬标在 Makefile 中⼜被称之为先决条件

出现这种⽬标依赖关系时,make⼯具会按从左到右的先后顺序先构建规则中所依赖的每⼀个⽬标。如果希望构建 all ⽬标,那么make 会在构建它之前得先构建 test ⽬标,这就是为什么我们称之为先决条件的原因。采⽤UML 的类图表达了 all⽬标的依赖关系。

Makefile实战(1)(看完这3篇就足够了)

⾄此,我们已经认识了 Makefile 中的细胞 —— 规则,⼀个规则是由⽬标(targets)、先决条件(prerequisites)以及命令(commands)所组成。⽬标和先决条件之间表达的就是依赖关系(dependency),这种依赖关系指明在构建⽬标之前,必须保证先决条件先满⾜(或构建)。⽽先决条件可以是其它的⽬标,当先决条件是⽬标时,其必须先被构建出来。还有就是⼀个规则中⽬标可以有多个(下图中的all和test都是目标),当存在多个⽬标,且这⼀规则是 Makefile中的第⼀个规则时,如果我们运⾏ make 命令不带任何⽬标,那么规则中的第⼀个⽬标将被视为是缺省⽬标。下图是定义了两个目标的规则,运行结果如下图:

Makefile实战(1)(看完这3篇就足够了)

Makefile实战(1)(看完这3篇就足够了)

Makefile 说起来也很简单,因为其基本单元就是规则,不管多么复杂的 Makefile,都是⽤规则“码”出来的。当然,为了更⾼效的“码”出来,还得运⽤ Makefile 所提供的变量和函数等功能,这后⾯我们会慢慢的讲到。

规则的功能就是指明 make 什么时候以及如何来为我们重新创建⽬标,在 Hello World 例⼦中,不论我们在什么时候运⾏ make 命令(带⽬标或是不带⽬标),其都会在终端上打印出信息来,和我们采⽤ make进⾏代码编译时的表现好象有些不同。当采⽤ Makefile 来编译程序时,如果两次编译之间没有任何代码的改动,理论上说来,我们是不希望看到 make 会有什么动作的,只需说“⽬标是最新的”,⽽我们的最终⽬标也是希望构建出⼀个“聪明的” Makefile 的。与 Hello World 相⽐不同的是,采⽤ Makefile 来进⾏代码编译时,Makefile 中所存在的先决条件都是具体的程序⽂件,后⾯我们会看到。

Makefile实战(1)(看完这3篇就足够了)

规则的语法:

Makefile实战(1)(看完这3篇就足够了)

对于上图中all 是⽬标,test 则是 all ⽬标的依赖⽬标,⽽@echo “HelloWorld”则是⽤于⽣成 all ⽬标的命令。make 处理⼀个规则的活动图如下图所示,当中的构建依赖⽬标(build dependent target(s))这⼀活动(注意是活动,⽽不是动作)就是重复下图 所示的同样的活动,你可以看作是对下图 活动图的递归调⽤。⽽运⾏命令构建⽬标(run command to build target)则是⼀个动作,是由命令所组成的动作。活动与动作的区别是,动作是只做⼀件事(但是可以有多个命令),⽽活动可以包括多个动作。

Makefile实战(1)(看完这3篇就足够了)

为了更加深刻的理解上图,拿之前的Makefile 为例来说⼀说make是如何处理 all ⽬标规则的。其处理活动图如下图所示,图中的左边是 all 规则的处理活动图,由于 all 规则有⼀个 test 依赖⽬标,所以其⾛的是[has dependency]分⽀,调⽤ build test taget活动,最后,运⾏all 规则的 echo 命令。图的右边则是构建 test ⽬标的活动图,由于 test ⽬标没有依赖关系,所以⾛的是[else]分⽀。

Makefile实战(1)(看完这3篇就足够了)

通过这⼀章节关于 Hello World 的⼏个例⼦,我们认识了 Makefile 中的规则。⽽规则中描述了⽬标是什么,先决条件是什么(即依赖关系),以及⽣成⽬标所需运⾏的命令是什么。对于规则需要特别注意的是,每⼀⾏命令之前必须⽤ TAB 键。当然,Hello World 的 Makefile 离我们现实⼯作中的 Makefile 还有很⼤的距离,其中的距离主要体现在功能性和可使⽤性上。为了让 Makefile 能更好的服务于我们的开发⼯作,我们还得学习 Makefile 中的其它的内容。⽆论如何,Hello World 是⼀个很好的开端!

原理

接下来我们试着将规则运⽤到程序编译当中去,下⾯我们假设有下图所示的⽤于创建 simple可执⾏⽂件的两个源程序⽂件,就假设我们是在做 simple 项⽬吧!现在,我们需要写⼀个⽤于创建simple 可执⾏程序的 Makefile 了,这个 Makefile 需要如何去写?还记得⽬标、依赖关系和命令吗?

此时的⽬录:Makefile/simple

Makefile实战(1)(看完这3篇就足够了)

Makefile实战(1)(看完这3篇就足够了)

写⼀个 Makefile ⽂件的第⼀步不是⼀个猛⼦扎进去试着写⼀个规则,⽽是先⽤⾯向依赖关系的⽅法想清楚,所要写的 Makefile 需要表达什么样的依赖关系,这⼀点⾮常的重要。通过不断的练习,我们最终能达到很⾃然的运⽤依赖关系去思考问题。到那时,你在写 Makefile 时,头脑会⾮常的清楚⾃⼰在写什么,以及后⾯要写什么。现在抛开 Makefile,我们先看⼀看 simple 程序的依赖关系是什么。

其中 simple 可执⾏⽂件显然是通过main.c 和 foo.c 最后编译,并连接⽣成的。通过这个依赖图,其实我们就可以写出⼀个 Makefile 来了。

Makefile实战(1)(看完这3篇就足够了)

上图是 simple 程序的依赖关系更为精确的表达,其中我们加⼊了⽬标⽂件。对于 simple 可执⾏程序来说,上图表示的就是它的“依赖树”。接下来需要做的是将其中的每⼀个依赖关系,即其中的每⼀个带箭头的虚线,⽤ Makefile 中的规则来表示。

Makefile实战(1)(看完这3篇就足够了)

有了“依赖树”,写 Makefile 就会相对的轻松了。下图是上图所对应的 Makefile,⽽下面第二张图则是依赖关系与规则的映射图。在这个 Makefile 中,我还增加了⼀个 clean ⽬标⽤于删除所⽣成的⽂件,包括⽬标⽂件和 simple 可执⾏程序,这在现实的项⽬中很是常⻅。

Makefile实战(1)(看完这3篇就足够了)

Makefile实战(1)(看完这3篇就足够了)

 下图给出了 simple 程序的编译、执⾏以及清除的运⾏结果。就这么简单?当然!不过需要指出的是,这种⽅法与真正的⼤型项⽬所需要的还相差很远,慢慢来,我们已经迈出了很重要的⼀步!

Makefile实战(1)(看完这3篇就足够了)

如果我们在不改变代码的清况下再编译会出现什么现象呢?下图给出了结果,注意到了第⼆次编译并没有构建⽬标⽂件的动作吗?但为什么有构建simple可执⾏程序的动作呢?为了明⽩为什么,我们需要了解make 是如何决定哪些⽬标(这⾥是⽂件)是需要重新编译的。为什么 make会知道我们并没有改变main.c 和 foo.c 呢?答案很简单,通过⽂件的时间戳当 make 在运⾏⼀个规则时,我们前⾯已经提到了⽬标和先决条件之间的依赖关系,make 在检查⼀个规则时,采⽤的⽅法是:如果先决条件中相关的⽂件的时间戳⼤于⽬标的时间戳,即先决条件中的⽂件⽐⽬标更新,则知道有变化,那么需要运⾏规则当中的命令重新构建⽬标。这条规则会运⽤到所有与我们在 make时指定的⽬标的依赖树中的每⼀个规则。⽐如,对于 simple 项⽬,其依赖树中包括三个规则(之前的第3个图),make 会检查所有三个规则当中的⽬标(⽂件)与先决条件(⽂件)之间的时间先后关系,从⽽来决定是否要重新创建规则中的⽬标。

Makefile实战(1)(看完这3篇就足够了)

知道了 make 是如何⼯作以后,我们不难想明⽩,为什么前⾯进⾏第⼆次 make 时,还会重新构建simple 可执⾏⽂件,因为 simple ⽂件不存在,将 Makefile 做⼀点⼩⼩的改动,如下图所示。其最后的运⾏结果则如下面第2张图所示。为什么还是和以前⼀样呢?哦,因为 Makefile 中的第⼀条规则中的⽬标是 all,⽽ all ⽂件在我们的编译过程中并不⽣成,即 make 在第⼆次编译时找不到,所以⼜重新编译了⼀遍。

Makefile实战(1)(看完这3篇就足够了)

Makefile实战(1)(看完这3篇就足够了)

再⼀次更改后的 Makefile 如下图所示(把目标all改成simple),⽽下面第2张图是其最终的运⾏结果,它的确是发现了不需要进⾏第⼆次的编译。这正是我们所希望的!

Makefile实战(1)(看完这3篇就足够了)

Makefile实战(1)(看完这3篇就足够了)

下⾯我们来验证⼀下如果对 foo.c 进⾏改动,是不是 make 能正确的发现并从新构建所需。对于make ⼯具,⼀个⽂件是否改动不是看⽂件⼤⼩,⽽是其时间戳。在我的环境中只需⽤ touch 命令来改变⽂件的时间戳就⾏了,这相当于模拟了对⽂件进⾏了⼀次编辑,⽽不需真正对其进⾏编辑。下图列出了所有相关的命令操作,从最终的结果来看,make 发现了 foo.c 需要重新被编译,⽽这,最终也导致了 simple 需要重新被编译。

Makefile实战(1)(看完这3篇就足够了)

⾄此,你完全明⽩了什么是⽬标的依赖关系以及 make 选择哪些⽬标需要重新编译的⼯作原理。掌握如果在头脑中勾画(当然初学时,可以⽤纸画⼀画)出我们想让 make 做的事的“依赖树”是编写 Makefile 最为重要和关键的⼀步。后⾯我们需要做的是让 Makefile 更加的简单但却更加的强⼤。

假⽬标

在前⾯的 sample 项⽬中,现在假设在程序所在的⽬录下⾯有⼀个 clean ⽂件,这个⽂件也可以通过touch 命令来创建。创建以后,运⾏ make clean 命令,你会发现 make 总是提示 clean ⽂件是最新的,⽽不是按我们所期望的那样进⾏⽂件删除操作,如下图所示。从原理上我们还是可以理解的,这是因为 make 将 clean 当作⽂件,且在当前⽬录找到了这个⽂件,加上 clean ⽬标没有任何先决条件,所以,当我们要求 make 为我们构建 clean ⽬标时,它就会认为 clean 是最新的。

Makefile实战(1)(看完这3篇就足够了)

那对于这种情况,在现实中也难免存在所定义的⽬标与所存在的⽂件是同名的,采⽤ Makefile如何处理这种情况呢?Makefile 中的假⽬标(phony target)可以解决这个问题。假⽬标可以采⽤.PHONY 关键字来定义,需要注意的是其必须是⼤写字⺟。下图是将 clean 变为假⽬标后的Makefile,更改后运⽤make clean 命令的结果如下面第二张图所示。

Makefile实战(1)(看完这3篇就足够了)

Makefile实战(1)(看完这3篇就足够了)

正如你所看到的,采⽤.PHONY 关键字声明⼀个⽬标后,make 并不会将其当作⼀个⽂件来处理,⽽只是当作⼀个概念上的⽬标。对于假⽬标,我们可以想像的是由于并不与⽂件关联,所以每⼀次 make 这个假⽬标时,其所在的规则中的命令都会被执⾏

变量

Makefile 中也有变量的概念,我们可以在 Makefile 中通过使⽤变量来使得它更简洁、更具可维护性。下⾯,我们来看⼀看如何通过使⽤变量来提⾼ simple 项⽬ Makefile 的可维护性,下图是运⽤变量的第⼀个 Makefile。

Makefile实战(1)(看完这3篇就足够了)

从上图可以看出,⼀个变量的定义很简单,就是⼀个名字(变量名)后⾯跟上⼀个等号,然后在等号的后⾯放这个变量所期望的值。对于变量的引⽤,则需要采⽤$(变量名)或者${变量名}这种模式。在这个Makefile 中,我们引⼊了 CC 和 RM 两个变量,⼀个⽤于保存编译器名,⽽另⼀个⽤于指示删除⽂件的命令是什么。还有就是引⼊了 EXE 和 OBJS 两个变量,⼀个⽤于存放可执⾏⽂件名,可另⼀个则⽤于放置所有的⽬标⽂件名。采⽤变量的话,当我们需要更改编译器时,只需更改变量赋值的地⽅,非常⽅便,如果不采⽤变量,那我们得更改每⼀个使⽤编译器的地⽅,很是麻烦。显然,变量的引⼊增加了 Makefile的可维护性。你可能会问,既然定义了⼀个 CC 变量,那是不是要将-o 或是-c 命令参数也定义成为⼀个变量呢?好主意!的确,如果我们更改了⼀个编译器,那么很有可能其使⽤参数也得跟着改变。现在,我们不急着这么去做,为什么?因为后⾯我们还会对 Makefile 进⾏简化,到时再改变也来得及,现在我们只是将注意⼒放在变量的使⽤上。

⾃动变量

为了简化目标或者依赖的名字,在文件中多次重复出现时,需要简化这类操作。包括如下几点:

(1)$@⽤于表示⼀个规则中的⽬标。当我们的⼀个规则中有多个⽬标时,$@所指的是其中任何造成命令被运⾏的⽬标。

(2)$^则表示的是规则中的所有先决条件。

(3)$<表示的是规则中的第⼀个先决条件。

下图是⽤于测试上⾯三个⾃动变量的值的Makefile,其运⾏结果从下面的第二张图中可以找到。需要注意的是,在 Makefile 中‘$’具有特殊的意思,因此,如果想采⽤ echo 输出‘$’,则必需⽤两个连着的‘$’。还有就是,$@对于 Shell 也有特殊的意思,我们需要在“$$@”之前再加⼀个脱字符‘\’。(想输出$^则只需在前面加1个$,而不再加转义字符\)如果你还有困惑,你可以通过改⼀改 Makefile 来验证它。第二张图的最后⼀⾏是⼀个只有⽬标的规则(3个目标),如果去除它会出现什么问题呢?试试看!

Makefile实战(1)(看完这3篇就足够了)

Makefile实战(1)(看完这3篇就足够了)

采⽤⾃动变量后 simple 项⽬的 Makefile 可以被重写为如下图所示,⽤了⾃动变量以后这个Makefile看起来有点怪怪的,有些什么‘^’、‘@’,等等。这就对了,你所看到的 Makefile 看起来不都很奇怪吗?我们要的就是这个“味”!

Makefile实战(1)(看完这3篇就足够了)

特殊变量

在 Makefile 中有⼏个特殊变量,我们可能经常需要⽤到。第⼀个就是 MAKE 变量,它表示的是make 命令名是什么。当我们需要在 Makefile 中调⽤另⼀个 Makefile 时需要⽤到这个变量,采⽤这种⽅式,有利于写⼀个容易移植的 Makefile。图 1.36 是对 MAKE 变量进⾏测试的 Makefile,⽽图1.37 则是其测试结果。

Makefile

.PHONY: all
all:
    @echo "MAKE = $(MAKE)"

执⾏

$make

MAKE = make

第⼆个特殊变量则是 MAKECMDGOALS,它表示的是当前⽤户所输⼊的 make ⽬标是什么。下图是⽤于对其进⾏测试的 Makefile

Makefile实战(1)(看完这3篇就足够了)

Makefile实战(1)(看完这3篇就足够了)

Makefile实战(1)(看完这3篇就足够了)

注意:

从测试结果看来,MAKECMDGOALS 指的是⽤户输⼊的⽬标,当我们只运⾏ make 命令时,虽然根据Makefile 的语法,第⼀个⽬标将成为缺省⽬标,即 all ⽬标,但 MAKECMDGOALS 仍然是空,⽽不是all。

前面的图示例了使⽤等号进⾏变量定义和赋值,对于这种只⽤⼀个“=”符号定义的变量,我们称之为递归扩展变量(recursively expanded variable)。现在我们需要了解⼀些细节,先看⼀看下图所示的Makefile 及其运⾏结果(如下面第二张图所示)。

Makefile实战(1)(看完这3篇就足够了)

Makefile实战(1)(看完这3篇就足够了)

Makefile实战(1)(看完这3篇就足够了)

从结果来看,递归扩展变量的引⽤是递归的。这种递归性有利也有弊。对于利,如下图所示的Makefile,最后 CFLAGS 将会被展开为“-Ifoo -Ibar -O”。但这也存在弊,那就是我们不能CFLAGS变量再采⽤赋值操作。也就是说下面第二张图中的 CFLAGS 会出现⼀个死循环。

Makefile实战(1)(看完这3篇就足够了)

Makefile实战(1)(看完这3篇就足够了)

除了递归扩展变量还有⼀种变量称之为简单扩展变量(simply expanded variables),是⽤“:=”操作符来定义的。对于这种变量,make 只对其进⾏⼀次扫描和替换,请看下图所示的 Makefile及之后所对应的运⾏结果。

Makefile实战(1)(看完这3篇就足够了)

Makefile实战(1)(看完这3篇就足够了)

从上图可以明显的看出 make 是如何处理递归扩展变量和简单扩展变量的($(y)中$(x)是用递归扩展得到的,也就是只保留最后一个x的值进行置换,也就是$(y)为later b;而$(yy)采用了简单扩展,所以只要找到这个yy之前的最后一个$(xx)进行置换即可,也就是最后面的later并没有生效,而是前面的foo生效了)。最后,Makefile中还存在⼀种条件赋值符“?=”,下面两张图分别是运⽤条件赋值的 Makefile 和运⾏结果。

从运⾏结果来看,条件赋值的意思是当变量以前没有定义时,就定义它并且将左边的值赋值给它,如果已经定义了那么就不再改变其值。条件赋值类似于提供了给变量赋缺省值的功能

Makefile实战(1)(看完这3篇就足够了)

Makefile实战(1)(看完这3篇就足够了)

Makefile实战(1)(看完这3篇就足够了)

对于前⾯所说的变量类别,是针对⼀个赋值操作⽽⾔的,下图示例了对于同⼀样变量采用不同的斌值操作的 Makefile,后面的运⾏结果可以验证我们可以对同一个变量采⽤不同的斌值操作。

Makefile实战(1)(看完这3篇就足够了)

Makefile实战(1)(看完这3篇就足够了)

变量可以从哪来呢?从前⾯的示例可以看出,在 Makefile 中我们可以对变量进⾏定义。此外,还有其它的地⽅让 Makefile 获得变量及其值。如:

(1)对于前⾯所说到的⾃动变量,其值是在每⼀个规则中根据规则的上下⽂⾃动获得变量值的。

(2)可以在运⾏ make 时,在 make 命令⾏上定义⼀个或多个变量。对于下面第一张图所示的Makefile,可以采⽤ make bar=x 运⾏ Makefile。从结果可以看出,在make 命令⾏中定义的变量及其值同样在 Makefile 中是可⻅的。其实,我们可以通过在 make 命令⾏中定义变量的⽅式从⽽覆盖 Makefile 中所定义的变量的值。

(3)变量还可以来⾃于 Shell 环境,下面第5张图采⽤ Shell 中的 export 命令定义了⼀个 bar变量后Makefile 的运⾏结果。

Makefile实战(1)(看完这3篇就足够了)

Makefile实战(1)(看完这3篇就足够了)

Makefile实战(1)(看完这3篇就足够了)

Makefile实战(1)(看完这3篇就足够了)

Makefile实战(1)(看完这3篇就足够了)

接下来,我们看⼀看在 Makefile 还可以如何对变量赋值, 下图采⽤“+=”操作符对变量进⾏斌值的⽅法,⽽其输出结果与上述第4张图是完全⼀样的。此外,下面两个Makefile是等价的。

Makefile实战(1)(看完这3篇就足够了)

Makefile实战(1)(看完这3篇就足够了)

⾼级变量引⽤功能

下图(图1.55)的 Makefile 示例了变量引⽤的⼀种⾼级功能,即在赋值的同时完成后缀替换操作。从图 1.56 的结果来看,bar 变量中的⽂件名从.o 后缀都变成了.c。这种功能也可以采⽤后⾯我们将要说的 patsubst函数来实现,与函数相⽐,这种功能更加的简洁。当然,patsubst 功能更强,⽽不只是⽤于替换后缀。

Makefile实战(1)(看完这3篇就足够了)

Makefile实战(1)(看完这3篇就足够了)

Makefile实战(1)(看完这3篇就足够了)

override 指令

采⽤在 make 命令⾏上定义变量的⽅式,使得 Makefile 中定义的变量覆盖掉,从⽽不起作⽤。可能,在设计 Makefile 时,我们并不希望⽤户将我们在 Makefile 中定义的某个变量覆盖掉,那就得⽤ override 指令了。图 1.57 和图 1.58 分别是使⽤了 override 指令的Makefile 及其运⾏结果。你可以对⽐前⾯的图 1.48 和图 1.51 来理解 override 指令的作⽤。

Makefile实战(1)(看完这3篇就足够了)

Makefile实战(1)(看完这3篇就足够了)

模式

对于前⾯的 Makefile,其中存在多个规则⽤于构建⽬标⽂件。⽐如,main.o 和 foo.o 都是采⽤不同的规则进⾏描述的。我相信你也会觉得,如果对于每⼀个⽬标⽂件都得写⼀个不同的规则来描述,那会是⼀种“体⼒活”,太繁了!对于⼀个⼤型项⽬,就更不⽤说了。Makefile 中的模式就是⽤来解决我们的这种烦恼的,先看图 1.59 所示的运⽤了模式的 simple 项⽬的 Makefile。

Makefile实战(1)(看完这3篇就足够了)

Makefile实战(1)(看完这3篇就足够了)

与 simple 项⽬前⼀版本的 Makefile 相⽐,最为直观的改变就是从⼆条构建⽬标⽂件的规则变成了⼀条。模式类似于我们在 Windows 操作系统中所使⽤的通配符,当然是⽤“%”⽽不是“*”。采⽤了模式以后,不论有多少个源⽂件要编译,我们都是应⽤同⼀个模式规则的,很显然,这⼤⼤的简化了我们的⼯作。

使⽤了模式规则以后,你同样可以⽤这个 Makefile 来编译或是清除 simple 项⽬,这与前⼀版本在功能上是完全⼀样的。

函数

采⽤函数如何来简化 simple 项⽬的 Makefile。对于simple 项⽬的 Makefile,尽管我们使⽤了模式规则,但还有⼀件⽐较恼⼈的事,我们得在这个Makefile中指明每⼀个需要被编译的源程序。对于⼀个源程序⽂件⽐较多的项⽬,如果每增加或是删除⼀个⽂件都得更新 Makefile,其⼯作量也不可⼩视!

采⽤了 wildcard 和 patsubst 两个函数后 simple 项⽬的 Makefile。你可以先⽤它来编译⼀下 simple 项⽬以验证其功能性。

Makefile实战(1)(看完这3篇就足够了)

模拟增加⼀个源⽂件的情形,看⼀看如果我们增加⼀个⽂件,在 Makefile 不做任何更改的情况下其是否仍能正常的⼯作。增加⽂件的⽅式仍然是采⽤ touch 命令,通过 touch 命令⽣成⼀个内容是空的 bar.c 源⽂件,然后再运⾏ make 和 make clean,其结果示于图 1.61。

Makefile实战(1)(看完这3篇就足够了)

Makefile实战(1)(看完这3篇就足够了)

Makefile实战(1)(看完这3篇就足够了)

这⾥并不找算将 Makefile 中所有能⽤的函数都列出,⽽只是列出⼏个这篇⽂章中我们会⽤到的。当然,图 1.60 中的 wildcard 和 patsubst 函数⼀定列在其中。建议你看⼀看《GUN make》以了解 Makefile中到底有些什么函数,这样的话,当我们在碰到具体的问题时就会想到它们。

addprefix 函数

addprefix 函数是⽤来在给字符串中的每个⼦串前加上⼀个前缀,其形式是:

$(addprefix prefix, names...)

图 1.62 示例了它的⽤法,addprefix 函数的最终⾏为可以从图 1.63 中看出。

Makefile实战(1)(看完这3篇就足够了)

Makefile实战(1)(看完这3篇就足够了)

filter

filter 函数⽤于从⼀个字符串中,根据模式得到满⾜模式的字符串,其形式是:

$(filter pattern..., text)

图 1.64 示例了它的⽤法,图 1.65 则是其运⾏结果。从结果来看,经过 filter 函数的调⽤以后,source变量中只存在.c ⽂件和.s ⽂件了,⽽.h ⽂件则则被过滤掉了。

Makefile实战(1)(看完这3篇就足够了)

Makefile实战(1)(看完这3篇就足够了)

filter-out

filter-out 函数⽤于从⼀个字符串中根据模式滤除⼀部分字符串,其形式是:

$(filter-out pattern..., text)

图 1.66 示例了它的⽤法,图 1.67 则是其运⾏结果。从结果来看,filter-out 函数将 main1.o 和

main2.o。从 objects 变量中给滤除了。filter 与 filter-out 是互补的。

Makefile实战(1)(看完这3篇就足够了)

Makefile实战(1)(看完这3篇就足够了)

patsubst 函数

patsubst 函数是⽤来进⾏字符串替换的,其形式是:

$(patsubst pattern, replacement, text)

图 1.68 示例了它的⽤法,从图中可以看出 mixed 变量中包括了.c ⽂件也包括了.o ⽂件,采⽤patsubst函数进⾏字符串替换时,我们希望将所有的.c ⽂件都替换成.o ⽂件。图 1.69 是最后的运⾏结果。这⾥示例的功能与我们在 1.5.5 节中所讲的变量的⾼级引⽤功能是⼀样的。当然,由于patsubst 函数可以使⽤模式,所以其也可以⽤于替换前缀等等,功能更加的强。

Makefile实战(1)(看完这3篇就足够了)

Makefile实战(1)(看完这3篇就足够了)

strip

strip 函数⽤于去除变量中的多余的空格,其形式是:

$(strip string)

图 1.70 示例了它的⽤法,图 1.71 则是其运⾏结果。从结果来看,strip 函数将 foo.c 和 bar.c 之间的多余的空格给去除了。

Makefile实战(1)(看完这3篇就足够了)

Makefile实战(1)(看完这3篇就足够了)

wildcard 函数

wildcard 是通配符函数,通过它可以得到我们所需的⽂件,这个函数如果我们在 Windows 或是Linux 命

令⾏中的“*”。其形式是:

$(wildcard pattern)

图 1.72 示例了如何从当前 Makefile 所在的⽬录下通过 wildcard 函数得到所有的 C 程序源⽂件。图1.73则显示了最后的运⾏结果。

Makefile实战(1)(看完这3篇就足够了)

Makefile实战(1)(看完这3篇就足够了)

⼩结

正如 simple 这个程序的名字那样,毕竟这是⼀个简单的项⽬,现实情况中的项⽬却更加的复杂。⽐如通常会将⽬标⽂件放⼊⼀个 objs 的⼦⽬录中,⽽可执⾏⽂件放⼊⼀个 exes 的⼦⽬录,⽽不是直接将这些⽂件都放在与源程序相同的⼀个⽬录中。还有,我们的 simple项⽬只有两个源程序⽂件,即使加上后来⽣成的 bar.c 空⽂件,也就只有四个。为了简单,我们除了包含了 stdio.h 头⽂件外,其它的头⽂件⼀概没有⽤。实际上,如果将头⽂件等复杂的因素合在⼀起,你会发现 simple 项⽬的Makefile 还有很多地⽅需要提⾼。接下来,我们将通过做⼀个复杂项⽬的Makefile 来学习更多的知识。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值