makefile ifneq多个判断条件_Makefile实战(1)(看完这3篇就足够了)

1.Makefile基础

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

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

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

准备环境

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

4f90e9bc93ee5c0e930a14f86d5a0e93.png

规则

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

f5c09a72b0259c1c8c42c433173bd8e4.png

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

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

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

第⼀种⽅式是只要在 Makefile 所在的⽬录下运⾏ make 命令,于是在终端上会输出⼆⾏,第⼀⾏实际上是我们在 Makefile 中所写的命令,⽽第⼆⾏则是运⾏命令的结果,你看到我们的 Makefile 确实在终端上打印了“Hello World”,真是太棒了!

第⼆种⽅式,则是运⾏“make all”命令,这告诉 make ⼯具,我要⽣成⽬标 all,其结果也不⽤多说了。

第三种⽅式则是运⾏ make test,指示 make 为我们⽣成 test ⽬标。由于我们根本没有定义 test ⽬标,所以运⾏结果是可想⽽知的,make 的确报告了不能找到 test ⽬标。

787ebb87fd06c1df4d432b1ac2c4862d.png

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

466ba47f43f5caf790d1c03965fb9f77.png

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

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

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

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

56e32923149a093afd628d8c9fcb579a.png

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

390a4059856a191baef2a44cfb3e259f.png
486c11ed3452cfc0aa48bdc70973956c.png

改动之⼀是在各命令前增加了⼀个‘@’,之⼆则是在 all ⽬标之后的‘:’后加上了 test ⽬标。运⾏ make 和make test 命令的结果如下图 所示。从输出结果中,你会发现当运⾏ make 时,test 目标好像也被构建了!

8c8a640c498dbc1685081c5f706f2db8.png
f1cce30c0ebd922a5d2d0efc410e9af4.png

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

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

2a0bb99427a9cafbd9e410a2ea4cc196.png

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

fad45eaec179a7df44f62bc8a915289d.png
f933e31719f21978f36c9341da1208ec.png

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

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

907fa3055e451d3c81a57f9f6fb9d793.png

规则的语法:

80a4e44ae3b8fd8814352f94ab19f62d.png

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

7a18442ccb4720de9e40f8c5d9a09b18.png

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

82c781338c2e062031af4a40b0cf7694.png

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

原理

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

此时的⽬录:Makefile/simple

16966539867c1e2930d9fef7ff8ec77c.png
733095653e3fccb62e1f9ef5ea5fdc94.png

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

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

c942714b1d378225fff74a3206807dae.png

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

aed4287ca94c43b8859dbf887335bc06.png

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

fee3e6c8a1cf3d4b26c79b46086792fc.png
bebb29d4f59e5e5864abe4b9bc506a6b.png

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

96c171afacf2d69f3f6dca8355c81a63.png

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

ef6fc8cb378ca0d8e5d9f1e1d7cb7341.png

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

ab8a755fdcd2f427d48af54334cdf435.png
82a06dea801b277761538ff5f6589d88.png

再⼀次更改后的 Makefile 如图 1.26 所示,⽽图 1.27 是其最终的运⾏结果,它的确是发现了不需要进⾏第⼆次的编译。这正是我们所希望的!

84d04c694f357ff252e62e265d5030d2.png
e565d52763d3467271d13326705a01f2.png

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

71258fcdb190411bff3a6e7466521883.png

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

假⽬标

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

a5091b68e26ef5c2d1f56c10fe2b53bb.png

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

5cc8e3994aa3f32c3237eefa76b3be6a.png
37bfd2f2b1606dbe978d7ca0380cd4d5.png

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

变量

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

3a4a71c4ba13ae34c073d449a3b466e1.png

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

⾃动变量

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

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

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

(3)$

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

7984ce0e181774e452ebef8b5d2ebccb.png
0a296766c99496afb8c9806f50b8c84e.png

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

2e7d740c3d637d467264cd124a2e1eae.png

特殊变量

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

Makefile

.PHONY: all

all:

@echo "MAKE = $(MAKE)"

执⾏

$make

MAKE = make

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

4c2adc37fe8708bd0c04292599c4937f.png
054e7fefda16d705181fd124772df2cf.png
991b295e802aa332e451e1e1832c0d04.png

注意:

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

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

fdd4cc37ba2b3d94519f6f6f84b449f7.png
8ca4e658221a5cfa62bfef21dd33a0a8.png
43273f2affebe75f80edf4dd0e99ca4b.png

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

bd6a1c34eeae46085574a2dddb85ca69.png
936449cb3cd6fa1c9678d3b48f35d553.png

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

ff387f81c04c21e25d936974eaf0b73c.png
e636a3389131896d03f957997ca3e9f9.png

从图 1.45 可以明显的看出 make 是如何处理递归扩展变量和简单扩展变量的。最后,Makefile中还存在⼀种条件赋值符“?=”,图 1.46 和图 1.47 分别是运⽤条件赋值的 Makefile 和运⾏结果。

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

bd2e063aaba9ba890877835aff76e09d.png
b0d8b9ce971603666c04c100ddaded43.png
ba55a618bc85ae57f776012d314d7b59.png

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

5066e0972dacbeeabbba6f841779f1bf.png
c8e5f183b0b9b3ffe80640bae7d4b71a.png

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

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

(2)可以在运⾏ make 时,在 make 命令⾏上定义⼀个或多个变量。对于图 1.46 所示的Makefile,如果采⽤ make bar=x 运⾏ Makefile,则得到的结果将完全不同,如图 1.50 所示。从结果可以看出,在make 命令⾏中定义的变量及其值同样在 Makefile 中是可⻅的。其实,我们可以通过在 make 命令⾏中定义变量的⽅式从⽽覆盖 Makefile 中所定义的变量的值,图 1.51 示例了对图 1.46 中的 Makefile中的变量值进⾏覆盖操作的结果。

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

f1487a414bd03d840f8695ee5fad21a0.png
a845a4df165fb98fae5f95f6026946c2.png
72b62fed0991e2040c4ac5917b17825f.png
86891410f4140f40e8cc624c59bd58f4.png
aa1740955307ec02958398b0795258f3.png

接下来,我们看⼀看在 Makefile 还可以如何对变量赋值,图 1.53 示例了采⽤“+=”操作符对变量进⾏斌值的⽅法,⽽其输出结果与图 1.49是完全⼀样的。此外,图 1.54中的 Makefile与图 1.53中的是等价的。

e85cfdf77ea0de1f6a7241995036e5da.png
71b788b6562fd402088092b4ca2317e1.png

⾼级变量引⽤功能

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

139e04179f302b9d45f7e976c5394913.png
a427495be7db1a419e7dc397573a3db7.png
e5cf5a3be0eaf6827d6e16c6c0489de7.png

override 指令

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

c136384a1eceb073d9c7acb31d4a8c6a.png
a500f3818b0f6aa0631f9c04909d61cb.png

模式

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

c363759cbf52df345a4c3c8a897cbebc.png
d8f4c70173751bd300d40598f64fc4bd.png

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

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

函数

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

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

cf485715a2e5a0d5d6c989df8b18de15.png

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

68b5bd6496559588d3d2112438d37176.png
037c1e2ac62522b22eb94fa6f1cf1a53.png
8bba0a567daa1f3bcfa02cde3dafe950.png

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

addprefix 函数

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

$(addprefix prefix, names...)

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

a549da277849ead997ebad1baaa821ec.png
b6eaa7200bab9921721c4d751dfb2ed9.png

filter

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

$(filter pattern..., text)

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

657567700d54ded7188a7d08d3a07444.png
b1231c06ab4f60f5aa92e4c2852533e6.png

filter-out

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

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

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

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

c0ed1fe92f78ce035ba5668ac5493d12.png
b2057ebd8e5fc656d2b6312aa5f4d021.png

patsubst 函数

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

$(patsubst pattern, replacement, text)

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

dc8a5fd3e817a6ccddca498967abd698.png
4665734b86741791fe152d69de5f0a73.png

strip

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

$(strip string)

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

4a5965a0ed90da6d0f1ad14390914204.png
a7f7d477983ec74eb4c7faf315970b88.png

wildcard 函数

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

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

$(wildcard pattern)

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

89e99d758648105cc9edbc3322b7eec1.png
2d0bc909b8f17627bd9e0c92223eb502.png

⼩结

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

本篇讲了基础部分,在接下来的文章会继续讲解,复杂目录以及在工程项目中的Makefile。欢迎关注,转发,收藏。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值