Linux就这个范儿 第8章 我是Makefile
P287
Makefile的作用就是——自动化编译,一旦写好,只需要一个make命令(解析Makefile,执行Makefile中描述的操作),整个工程就能完成自动编译,
无论这个工程拥有多少个源代码文件。Makefile定义了一系列的规则,来指定哪些文件需要先编译,哪些文件需要后编译,
哪些文件需要重新编译,甚至于进行更复杂的功能操作,因为Makefile就像一个shell脚本一样,其中也可以执行操作系统的命令。
最典型的就是在微软的Visual Studio中,当你需要别人编写的dll作为你项目的一部分时,如果这个dll采用多线程库,而你的工程却采用了单线程库,
构建这个工程的时候一定会失败。如果你不清楚这些,永远解决不了这个问题。
即便你不具备编写Makefile的技能,那也需要掌握make这个工具的使用方法。因为还有很大一部分你需要的、或者感兴趣的软件是采用源代码方式发行的。
即便rpm、deb等软件打包方案在Linux世界已经大行其道,但是如果你要使用这些软件的最新版本,基本上脱离不了使用make这个工具通过源代码来创建它。
即便那些采用Python、Perl等解释语言开发的软件,绝大多数的情况也是利用Makefile做为安装脚本的。
当然,更进一步说,你有了一个很了不起的创意,并且准备分享给全世界,那么在Linux上你就更需要掌握make乃至Makefile的编写技巧。
因为大多数人是这么做的,我想你还不足以强大到可以改变这一现实的程度。但是如果你一定要说不,那我也没有办法,那你最好远离Linux,因为Linux正在远离你。
不过对于大多数Java程序员来说,Ant和Maven或许是他们更为常用或乐于使用的工具。
但这并不表明Makefile在遇到Java时就会显得有些ED,因为Makefile对Java也提供了良好的支持。
但是为何Ant和Maven在这个行当中走到了领先的位置,还是在于它们相对Makefile的学习周期更为简短,而且Maven能够比较容易地解决一些jar包依赖问题。
但是这里有一个始终让我疑惑的地方,就是当我跟好多Java高手们谈起XML时,他们会将它说得一无是处,就好像是所有问题的根源,但是Ant和Maven又是完全的XML格式而他们又对它爱不释手。
也许就像矛盾冲突是戏剧的精髓一样,人生也是一场戏吧。不过Ant和Maven的确都是非常强大的工具。只是在我看来我更喜欢Makefile,因为它更具灵活性和描述能力,能够用很简单的方式完成更为复杂的任务。
所以即便你是Java程序员,我也希望你能够掌握Makefile,它或许能够更加开阔你的视野。不过很多时候Ant或Maven已经足够用了,
只是我要多说一句的是,纵使IDE可以对它们提供良好的支持,但是作为初学者最好远离IDE。因为IDE是高手的挡泥板,却是初学者的遮阳伞(久不晒太阳会得病的)!
GNU make及其基本用法
实际上Makefile并不是这个工程管理工具的名称,但却是一种约定俗成的叫法。
它是一类工程管理工具的工程描述文件的默认名称,它只是相当于Ant的build.xml和Maven的pom.xml文件。
而且更让初学者头痛的是,这个文件名它不是唯一的。可以是Makefile,可以是makefile(注意Linux的文件名是区分大小写的),甚至是GNUmakefile。
或许你已经注意到了,我刚才说Makefile是一类工程管理工具的工程描述文件,并不是说一个,是不是丈二和尚了?其实我在学习Makefile的时候也遇到了同样的困惑。
后来才慢慢了解到,Makefile是make工程管理工具的工程描述文件,make是类UNIX世界的一个传统工程管理工具,是它们文化的一部分,可以称作Makefile文化。
那么既然是文化,就得允许百花齐放、百家争鸣。
传统UNIX的主要分支,比如Solaris、HP-UNIX、AIX等采用一套Makefile体系;BSD系列如:FreeBSD、OpenBSD、NetBSD等采用一套Makefile体系;
Linux采用另一套Makefile体系。而且即便属于同一体系,在某些方面都会显得大相径庭。
在Linux中采用的是GNU make这一体系,属于GNU计划中的一部分,可以说是所有体系中功能最为丰富、兼容性最好的一个Makefile文化的传承品。
Linux内核的编译是有两个强依赖的,其中一个就是GNU make,另外一个是gcc。
其实大家会奇怪,Linux既然有这么好的通用性和可移植性,为什么会有两个强依赖呢?
其实这就更充分说明GNU make和gcc是足够的NB,它们本身就具备了极强的通用性和可移植性。它们几乎已经被移植到已知乃至未知的所有平台上了,所以作为Linux即便对他们有强依赖也根本不会影响到它的通用性和可移植性。
GNU make已经是当前事实上的“标准”了,就好像它说的是标准普通话,其他make说的都是方言。本章所介绍的内容也是适用于其他make的,毕竟普通话与方言的区别仅在发音上,具体语素和语法习惯还是一致的。
在Linux就是执行make命令来完成工作的(在其他系统中就不一定是这样了)。
make就是Linux系统下的一个工具,在任意目录中执行make都可以(/usr/bin/make),只是效果不同罢了。
make会在执行路径中搜索Makefile文件。前面已经说过,Makefile并不是唯一的选择,还可以是makefile和GNUmakefile。
如果一个路径中同时存在它们三者,make会选择一个。不过不要惊慌,这是有规矩的,要是没有规矩不就乱性了吗?
make的选择顺序是GNUmakefile、makefile和Makefile。
只要选中了一个就不再理会其他的了。这个时候make还是很专一的。
当然你也可以让make不那么专一。方法就是用“—f”选项给make指定一个工作文件。
这个时候任何文件名都可以,只要内容符合Makefile的书写规范就行。“—f”选项有两种变形,分别是“--file=FILE”和“--makefile=FILE”,功能完全相同,随你的喜好决定。
这是一个非常常用的命令选项,尤其是在跨平台项目中,会针对不同的平台提供不同的Makefile文件。
另外一个常用的命令选项是“-d”,用于调试Makefile。在手工编写一个Makfile文件时非常有用。不过不要指望类似于调试C或Java程序那样可以单步跟踪。
它的作用仅是将Makefile的执行过程一一打印出来,让你观察在哪个地方出现了意想不到的错误。变形选项是“-debug[=FLAGS]”,功能会更加丰富一些。
对于只是想通过Makefile来作为阅读代码入口的人来说,make提供了一个“—t”选项,它能够帮你输出为生成一个可执行文件而需要的所有目标文件。
这样你就排除了那些不相干的东西,使得这一严峻的问题变得简单了许多。这个过程并不执行真正的工程创建过程,所以这不会影响到代码的原始性,只是帮你做了很好的梳理,而且过程既简单又快捷。
变形选项是“-touch”,功能完全相同。
对于make这个命令提供的选项还有很多,这里不一一列出,有兴趣的话可以使用“—h”或“-help”选项查看其他的命令以及说明。
如果感觉通过这个命令得到的内容有点过于单薄,那么就可以使用man命令查看它的在线帮助手册,这个“男人”真的很有用!
其实make的用法还远不止是命令选项这些,只是我都留在接下来的内容中了。因为我永远相信:若想真正掌握一个工具是必须去实际使用的。
-f选项:指定makefile
-t选项:输出为生成一个可执行文件而需要的所有目标文件
-d选项:调试makefile
8.2 基本概念
首先让我们了解一下Makefile的基本概念。
8.2.1 第一个Makefile例子
话说有一个功成身退的程序员,闲来无趣准备练习书法消磨时间并期望能有所建树。于是斥巨资购买了上等文房四宝。一日,饭后突然兴起。一番研墨、铺纸、焚香,颇有王、颜之概。待气定神闲,面色凝重地写下一行字:hello world。
好吧,这就是悲催的程序猿,只要学习一些新东西,总是会想到“hello world”。而且有些人居然号称会使用数十门编程语言,那咱们得向这等高手请教一番吧。结果他只告诉了你每门语言如何写“hello world”。我真的不想找骂,所以我给大家展示的第一个例子,跟“helloworld”无关。具体参见代码1所示。
代码1:
all:TinyEdit
TinyEdit: main.o line.o buffer.o tools.o chunk.o document.o cursor.o
cc main.o line.o tools.o buffer.o -o TinyEdit
myless:myless.o line.o buffer.o tools.o chunk.o
ccmyless.o line.o tools.o buffer.o -omyless -lcurses
myless.o:myless.c line.h buf fer.h tools.h tedef.h
cc -c -o myless.o myless.c
document.o:document.c line.h buf fer.h cursor.h tools.h tedef.h
cc -c -o document.o document.c
cursor.o:cursor.c line.h tools.h tedef.h
cc -C -o cursor.o cursor.C
line.o:line.c line.h buf fer.h tools.h tedef.h
cc -c -o line.o line,c
tools.o:tools.c tools.h tedef.h
cc -c -o tool.o tools.c
chunk.o: chunk.c line.h buffer.h tedef.h
cc -c -o chunk.o chunk.c
buf fer.o:buf fer.c buf fer.h tools.h
cc-c -o buffer.o buf fer.c
main.o:main.c line.h buf fer.h tedef.h
cc -c -o maln.o maln.c
这是我曾经写过的一个行文本编辑器的Makefile文件,虽然看似内容很多,实际上是很好理解的,也基本包含了这一部分我所要讲述的内容。
这个Makefile可以生成两个可执行文件:TinyEdit和myless。
TinyEdit就是一个简单的行文本编辑器:myless是一个简单的文本阅读器。
根据前面我们掌握的知识,当我执行make命令后,就会生成TmyEdit。那我要生成myless怎么办呢?要执行make myless命令。
哈,聪明的你是不是会根据这个联想一下,我要是执行make love这样的命令,是不是就生成love了呢?答案让你失望了,make会报错的。因为这个Makefile中没有love,所以生成不了。
这也给我们传达了一个道理,没有love的make love实际上是一种错误,是产生不了love的。
在这里make后面的参数不是命令选项,而是用于标明make要处理并最终生成的目标。
到这里我猜你们会有一个疑问。就是在生成TinyEdit的时候,并没有指定目标啊?嗯,这的确是个好问题,我们继续下面的内容来找到答案。
8.2.2 目标、条件和命令
对于一个Makefile来说,最主要的就是目标、条件和命令这三大要素。
条件+命令=目标
目标:是make要产生的东西或要做的事情
条件:是用于产生目标所需要的文件
命令:是由条件转化为目标的方法。
由这三大要素构成了Makefile的基本规则。
就像代码1中的最后一段:
maln.o:main.c line.h buffer.h tedef.h
cc -c -o main.o maln.c
maln.o就是目标,mam.c、line.h、buffer.h、tedef.h就是条件,而下面的“cc-c mam.c”就是命令。这个命令可以将main.c文件编译成mam.o文件。
现在就可以更好地理解为什么对这个Makefile文件执行makelove会出错了。因为make后面的参数是要生成的目标。而这个Makefile是没有Iove这个目标的。
所以make会抱怨找不到love这个目标。如果我们给定了一个存在的目标,比如执行make main.o命令,就会生成mam.o文件。而且我们还可以一次性指明生成多个目标,
如:make main.o document.o就可以生成mam.o和document.o文件。
但是这解释不了生成TinyEdit的方式。我们可以看第一行:
aII:TinyEdit
这里all是目标,TinyEdit是条件。all是Makefile的默认目标,也就是说不给make任何目标,就生成all这个目标。可是为什么没有生成all,却把条件生成了呢?
首先,并没有指定生成all的命令;其次,还需要引入一个概念——依赖。这是依赖所产生的效果。
8.2.3 依赖
什么是依赖呢?依赖就是一个目标的条件是另外一个目标。这就好比你期望取一个你心爱的女孩为妻,但是那个女孩嫁给你的条件是她妈妈能够看得上你;
如果要她妈妈能够看得上你,你就必须有车、有房、有存款;如果你想有车、有房、有存款,你就得在像阿里巴巴这样待遇丰厚又有前途的公司工作;
如果你想在阿里巴巴工作,就最起码是一个好人,有知识、有文化、有一颗上进的心……
在前面我说的生成TinyEdit的例子中,不但生成了TinyEdit,还生成了其他的“.o”文件。因为生成TinyEdit需要它们,于是Makefile就要先将它们作为其首要的目标去生成。
如果找不到就抱怨失败,找到了就安心继续工作,直到生成最终目标-all-为止。因为没有生成all的命令,所以也就没有all这个文件被生成。
由此可见,依赖是make进行连续工作的动力之源。如果你想用Makefile玩点花样,是完全可以利用依赖来控制它的工作流程的。
8.2.4 工作方式
我们平常会用一个很武断的方法来判断一个人是聪明还是愚蠢,那就是看这个人在做事情的时候是否会走捷径。其实我们中国人总是喜欢走捷径的,比如考试打小抄、办事走后门,乃至“汉芯”的造假等,
走捷径走到了极端还不如做一个“蠢货”活得踏实。但并不是说走捷径就不好。如果懂得删繁去冗、大道至简,这样得到的捷径才是天才所为。
make就是这样工作的。
一个规模宏大的项目,比如Linux内核,其源代码文件的数量数十万计,make要生成的目标几乎也要按数十万计,编译一次Linux内核大概需要几十分钟甚至数小时(视机器的速度而定)。
这对于我们最终用户还不算什么,顶多花些时间,编译一次就够用了。但是作为内核的开发者来说,每做一些修改都需要这么久的编译过程才能检验修改结果,那简直就是一场灾难,估计直到世界末日我们也用不到Linux了。
当然,make就这点水平也没人会用它了。make解决这个问题的方法就是哪个文件进行了修改就只编译它,其他没有改动过的文件不予理会。这显然可以节省大把的时间了,这就是make的聪明所在。
它是怎么办到的呢?
答案是看文件的时间戳,也就是文件最后被修改的时间(任何操作系统都会给它所管理的文件赋予这个属性的)。make会依次把所有目标以及它们所对应的依赖进行时间戳的对比。
当发现某一个目标比它的某些依赖的时间戳小的时候,就重新生成它。因为目标最后被修改的时间早于条件被最后修改的时间,可以认为产生这个目标的条件发生了改变,因此就应该重新生成。
按照前述的依赖关系,就会产生一系列动作,使得生成的最终目标被更新。
所以make的工作方式归纳起来就是:
1.当目标不具备时,根据条件来生成目标;
2.当目标具备但条件发生改变时,重新生成目标;
3.当目标的条件不具备时,就按照1的方法创造条件。
8.2.5 基本语法
好了,我想现在你基本上已经具备了编写一个能够正常工作的Makefile的基本知识了。那么我现在就介绍一下基本的语法结构,这样你就可以动手了。
Makefile可以认为是一种解释型语言,一般设计解释型语言都是采用“自上而下”的解释逻辑,Makefile也不例外,总是喜欢更新最上层的目标(不给make指定目标的时候),这个最上层的目标通常是all。
下层目标一般都是为上层目标提供条件的,当然也有做其他事情的。目标并不一定是文件,任何名称都可以,但是条件一定是具体的文件。
Makefile的基本语法是这样的:
目标1目标2目标3……:条件1条件2条件3……
命令1
命令2
命令3
……
冒号的左边至少要有一个目标,而冒号的右边可以有0个或任意多个条件。如果没有给目标指定条件,
就只有在工作路径下目标所代表的文件不存在时才会执行相应的命令去生成目标,因为没有给定条件就无法比对时间戳。
但是这种情况下一般也不会在命令中生成目标所对应的文件,因此这个目标的命令一直都能被执行。比如常用的clean目标一直都会执行清理工作。
这里需要注意的一个问题是,每个命令必须以制表符“Tab”开头,空格不行。不要问为什么,这是王八的屁股——龟腚(规定)。
如果你一定要用空格(这样可以保证代码的整洁)也不是不行,但是不能出现在开头,在“Tab”之后有多少空格都没关系。但是一定是“Tab”开头的。
更要注意这个“Tab”只能出现在命令开头。在非命令行上不慎输入了Tab的话,它之后的内容多数情况下会被当做命令来解释,如果这段内容是有意义的命令,
说不定就会给你造成很大损失,比如源代码被改写了。所以这个问题一定要切记。
有人会问,既然Makefile可以被认为是一种解释型语言,那么它有注释吗?答案是:这个可以有!所有以井号“#”开头的内容都被视作注释,make不对这些内容做解释。
不过切记“Tab”的问题。就是只要在“Tab”后,就会被作为命令解释,包括“捍”以后的内容。你看,“Tab”多麻烦!我们是没有办法反抗的,所以就享受它吧(貌似有点痛苦)。
Makefile是可以支持折行的,方法是使用“\”。它可以出现在条件或命令行的末尾,表明接下来的一行是本行的延续。
使用折行是一个很好的习惯,因为从人体工程学角度讲,纵向滚动文字比横向滚动文字给人的感觉更加舒服。如果你要怀疑这个论调的话,你可以自己亲自试验一下。
8.3 认识规则
经过前面的讲解,大家应该已经对规则有了一些感性的认识了。接下来我们还要从理性上认识一下规则。因为就我们“第一个例子”来说,已经显得足够麻烦和死板,
如果make就这点水平,肯定活不到现在,毕竟这是一个物竞天择的世界。我们越深入,我们的“第一个例子”就应该越简单、越灵活。
而且当你阅读完本章后,你会发现,规则贯穿于整个Makefile,是它工作的灵魂。
8.3.1 标准规则
这就是规则,它是公开说明的,必须要遵守,一旦违背了规则,就得不到预期的效果。
我们前面所讲述的目标、条件和命令就构成了Makefile规则的三要素,然后是依照工作方式和依赖关系生成我们要得到结果。
像我们在“第一个例子”中那样明确写明的,被称为标准规则,也叫显示规则。在Makefile申与显示规则相对应的还有一种规则,叫隐式规则。
8.3.2 隐式规则
如果有一天哪个漂亮女明星突然爆红,很多人都会发出同样的一个感叹:又一个被潜规则的!的确,在这个物欲横流、竞争激烈的世界,不由得人们不为那些一夜飞升的艺人们发出如此的感叹。
潜规则似乎已经成为娱乐圈的常态。其实Makefile也具有潜规则,只是我们叫它隐式规则。这两种“潜规则”相同的是都不会摆出来让你看到但是它确实存在,而且都是为了达到目的所采用的一种捷径。
而不同的是前者多少都会有make love,后者大多只是make file。
现在我将“第一个例子”修改成代码2这样。
代码2:
all:TinyEdit
TinyEdit:main.o line.o buffer.o tools.o chunk.o document.o cursor.o
cc main.o line.o tools.o buf fer.o chunk.o document.o cursor.o-o TinyEdit myless: myless.o line.o buf fer.o tools.o chunk.o
cc myless.o line.o tools.o buf fer.o chunk.o -o myless -lcurses
myless.o:myless.c line.h buf fer.h tools.h tedef.h
document.o:document.c line*h buffer.h cursor.h tools.h tedef.h
cursor.o:cursor.c line.h tools.h tedef.h
line.o:line.c line.h buf fer.h tools.h tedef.h
tools.o:tools.c tools.h tedef.h
chunk.o:chunk.c line.h buf fer.h tedef.h
buf fer.o:buffer.c buffer.h tools.h
maln.o:main.c line.h buffer.h tedef.h
显然这简单多了。你可能会很奇怪,很多命令都被删掉了,能工作吗?答案是能,而且还等同于“第一个例子”,甚至输出结果都一样。
难道make知道从条件生成目标的命令?对,make知道,这就是Makefile的“潜规则”。
最新的GNU make具有90多个隐式规则。这些隐式规则可以应用于C、C++、Obj ective-C、Pascal、Lisp等。大多数情况下隐式规则都能满足你的需要。
即便你用到当前未被支持的语言,如Java,也可以自己添加这种隐式规则。
正因为可以自行添加隐式规则,完全可以认为隐式规则是一种内置规则。
make也提供了一个命令行选项-p(或-print-data-base)让我们查看它所支持的全部隐式规则。这个输出少说也要有1000行。
所以查看它的办法一般推荐两种:一种就是使用“>>”IO重定向将它输出到文件中,如:
make -p >> data_base. txt
然后使用一种你比较熟悉的文本编辑器查看data base.txt的内容;
另外一种方法是使用管道结合grep命令来查看自己想看的内容,如: make -p l grep -e ”\.c\.o”
这个命令会输出“.c.o:”这样的内容,这代表支持从“.c”文件到“,。”文件转换的隐式规则。
如果什么都没有输出,那就代表不支持“.c”文件到“.o”文件转换的隐式规则。
8.3.3 变量
前面介绍的隐式规则可以有效地减少Makefile的长度,但是还缺乏一种灵活性。比如需要给生成的程序加入调试信息,以便在发现bug的时候可以联机调试。
这个显然很容易,
方法就是在每条命令中加入“—g”选项。只是这是一个馊主意。
作为“第一个例子”中所涉及的源代码文件数量不多的情况或许还可以忍受,要是Linux内核这样的工程你该做何感想呢?
那么有什么方法使得只修改一个地方就能搞定这一切呢,确实有一个方法可以解决现在这个问题,那就是——变量!
估计你现在对宏是有比较深刻认识的,就是将宏的名称替换为宏的内容。
其实在Makefile中的变量跟宏的特性有些类似,比如我们定义了一个变量CC,在Makefile中可以采用这样的语法:
cc:=gcc -g
这就定义了一个变量,变量的名称是CC,变量的值是“gcc-g”。当make在解析Makefile遇到$(CC)或${CC}时,就会将它替换为“gcc-g”。
Makefile中的变量值可以是任意字符串(注意“Tab”的问题),我们把“$O”或“${}”称为变量展开语句。那么我们就可以修改我们的“第一个例子”为代码3:
代码3:
cc := gcc -g
all:TinyEdit
TinyEdit:main.o line.o buf fer.o tools.o chunk.o document.o cursor.o
$ (CC) main.o line.o tools.o buf fer.o -o TinyEdit
myless: myless.o line.o buf fer.o tools.o chunk.o
$ (CC) myless.o line.o tools.o buffer.o -o myless -lcurses
myless.o:myless.c line.h buffer.h tools.h tedef.h
$ (cc) 一c 一omyless.o myless.c
doclunent.o:document.c line.h buf fer.h cursor.h tools.h tedef.h
$ (cc) 一c -o document.o document.c
cursor.o:cursor.c line.h tools.h tedef.h
$ (cc) 一c -o cursor.o cursor.c
line.o:line.c line.h buf fer.h tools.h tedef.h
$ (cc) 一c -o line.o line.c
tools.o:tools.c tools.h tedef.h
$ (cc) -c -o tool.o tools.c
chunk.o:chunk.c line.h buf fer.h tedef.h
$( cc) -c -o chunk.o chunk.c
buf fer.o:buffer.c buf fer.h tools.h
$ (cc) 一c -o buf fer.o buffer.c
maln.o:main.c line.h buf fer.h tedef.h
$(cc) 一c -o main.o main.c
啊哈,这样如果想给编译命令添加参数就很容易了。只要修改该变量的值就可以了。有了变量,聪明的你一定想到了一个更好的主意,
就是可以多定义几个变量,分别代表编译命令、编译参数、连接命令和连接参数等。我猜的不错吧?因为我也很聪明。那就看看我改的代码4吧。
代码4:
cc := gcc
LD := gcc
CFLAGS := -g -W -std=c99 -c
LDFLAGS := -lcurses
all:TinyEdit
TinyEdit: main.o line.o buf fer.o tools.o chunk.o docLunent.o cursor.o
$ (LD) $(LDFLAGS) main.o line.O tools.o buf fer.O -o TinyEdit
myless: myless.o line.o buf fer.o tools.o chunk.o
$ (LD) $(LDFLAGS) myless.o line.O tools.。 buffer.O -o myless
myless.o:myless.c line.h buffer.h tools.h tedef.h
$ (cc) $(CFLAGS) -o myless.o myless.c
document.o:document.c line.h buf fer.h cursor.h tools.h tedef.h
$( cc) $(CFLAGS) -o document.o document.c
cursor.o:cursor.c line.h tools.h tedef.h
$ (cc) $(CFLAGS) -o cursor.o cursor.c
line.o:line.c line.h buf fer.h tools.h tedef.h
$ (cc) $(CFLAGS) -o line. line.c
tools.o:tools.c tools.h tedef.h
$ (cc) $(CFLAGS) -o tool.o tools.c
chunk.o:chunk.c line.h buffer.h tedef.h
$ (cc) $(CFLAGS) -o chunk.o chunk.c
buffer.o:buffer.c buf fer.h tools.h
$ (cc) $(CFLAGS) -o buffer.o buf fer.c
main.o:main.c line.h buffer.h tedef.h
$ (cc) $(CFLAGS) -o main.o main.c
这个灵活性就更强了。只要修改变量的值,就可以改变编译器、改变连接器、改变连接选项、改变编译选项。
不过看到这里我猜你会有疑问了,灵活性有了,复杂性可又来了。难道鱼与熊掌不能兼得吗?这个真可以兼得的,且听我慢慢道来……
8.3.4 自动变量
Makefile中有一种特殊的变量,不用定义而且还会随着上下文的不同而发生改变,我们将这种变量称之为自动变量。
它们可以自动取用一条规则中目标和条件中的元素,这样就可以节省一些用于输入目标文件名和条件文件名的力气。
不过现在不要武断地认为它们就这么点能耐,其实它们非常有用,随后我会一一介绍。
表8-1 最为常用的6个自动变量
变量名 |
作 用 |
$@ |
目标的文件名 |
$< |
第一个条件的文件名 |
$? |
时间戳在目标之后的所有条件,并以空格隔开这些条件 |
$ˆ |
所有条件的文件名,并以空格隔开,且排除了重复的条件 |
$+ |
与$ˆ类似,只是没有排除重复的条件 |
$* |
目标的主文件名,不包含扩展名 |
需要注意的是,与普通变量不同的是,要展开变量的值需要使用“$O”或“${}”,自动变量则不需要这样。
make只会在目标与条件能够进行匹配的时候,才会设置自动变量,所以自动变量只能应用在命令中。
那么现在我再利用自动变量来简化一下我们的Makefile,看代码5。经过这样修改后还会得到一个好处就是可以避免一些错误。
回顾代码4我们就会发现,在生成TinyEdit这个目标的条件中有一个cursor.o这个条件,但是我在命令中并没有包含它,所以我最后生成的TinyEdit根本就不是我想要的结果。
当使用自动变量之后,这个问题不存在了,我只要写好条件就行了。
代码5:
cc := gcc
LD := gcc
CFLAGS := -g -W -std=c99 -c
LDFLAGS := -lcurses
all:TinyEdit
TinyEdit: main.o line.o buf fer.o tools.o chunk.o document.o cursor.o
$ (LD) $(LDFLAGS) $^ -o $@
myless: myless.o line.o buf fer.o tools.o chunk.o
$ (LD) $(LDFLAGS) $^ -o $@
myless . o : myless . c line .h buffer .h tools .h tedef .h
document . o : document . c line . h buf fer .h cursor .h tools .h tedef .
cursor. o : cursor . c line . h tools .h tedef .h
line . o : line . c line .h buf fer .h tools .h tedef .h
tools .o : tools .c tools .h tedef .h
chunk . o : chunk . c line h buffer .h tedef .h
buf fer . o : buf fer . c buf fer . h tools h
main . o : main . c line .h buf fer .h tedef .h
8.3.5 模式规则
一直到现在,我所给出的所有代码,看起来只有在讲解隐式规则的那段是最为简练的。还能再简练一些吗?嗯,当然可以。好吧,该拿出杀手锏了。
我们先看代码6。
代码6:
cc := gcc
LD := gcc
CFLAGS := -g -W -std=c99 一c
LDFLAGS := -lcurses
all:TinyEdit
TinyEdit:main.o line.o buf fer.o tools.o chunk.o document.o cursor.o
$ (LD) $(LDFLAGS) $^ -o $@
myless: myless.o line.o buffer.o tools.o chunk.o
$ (LD) $(LDFLAGS) $^ -o $@
%.o:%.c
$(CC) $(CFLiqLGS)一o $@ $<
在匹配文件名的时候,除“%”以外的任意字符都会精确匹配。一个模式可以包含一个前缀或一个后缀,或者这两者同时存在,乃至同时不存在。
模式规则在工作的时候首先得找到与模式规则中的目标相匹配的目标。在我们这个例子中就是TinyEdit和myless目标的那些条件(别忘了依赖)。
当找到了这个匹配,就会将“%”所匹配到的部分——我们称之为词干——替换掉模式规则中条件模式的“%”而成为一个条件。
这样就构成了一个显式模式,从而按照显示模式的工作方式完成目标的生成。
我们介绍完模式规则后,大家一定会小激动一下。因为这个实在太帅了,原来可以这么简单啊。但你一定会认为在代码6中遗漏了什么。
因为在将目标文件连接为可执行文件的时候我依然使用了显式规则,所以你应该会想到,在这里也应该可以使用模式规则。但是这里除了使用自动变量能够使得这个地方好看一点之外,并没有什么好的办法。
因为这里有两个最主要的问题:第一,生成TinyEdit与myless的条件是不同的,必须严格区别;第二,模式规则的条件中的“%”要被目标中的“%”所匹配到的词干所替换。
因此,模式规则中的目标与条件为一对一关系,无法处理这种一对多关系。所以,没办法!
另外再说一句,我们之前介绍的隐式规则就是模式规则的一个实例。而且实际上就是因为make内置了很多模式规则,才使得我们有这么多的隐式规则能够使用。
因此,如果遇到了一个make所不支持的隐式规则,我们就可以通过模式规则手工创建它,比如Java。既然已经内置了,所以在代码6中完全可以将最后的那个模式规则删除,所得到的结果也会是相同的。
而且更为让你欣喜的是,甚至我们之前定义的变量都被使用了,所以并没有影响你所定义的编译器和编译条件。
这是因为CC、LD、CFLAGS、LDFLAGS等变量也是make的内置变量,并且在它的内置模式中也会使用。可以通过“make-p”命令来查看拥有哪些内置变量。方法可以参照隐式规则中所介绍的那样。
有了模式规则,是不是Makefle已经足够简单了?
尽管代码6已经是最精简的Makefile了,但是我有点不推荐,因为作为C语言的头文件无法成为条件,毕竟头文件修改了也需要重新编译的(头文件也是依赖)。
我比较推荐代码2这样的写法,尽管有些麻烦,但是可以解决这个问题。当然,既使用这种简单的模式规则又能照顾好依赖关系的方法也是有的,我们留待后面介绍。
好了,接下来的内容我们将使得这个Makefile更为强大。
8.3.6 假目标
唐骏的学历、上海交大的芯片、达芬奇的家具、江苏的狂犬疫苗、健力宝的金罐儿……哎,这年头假货实在是太多了。
难道Makefile也无法忍受这尘世的喧嚣开始造假了?对,它也会造假,因为但凡造假的背后都有一些显而易见的目的,造假者都能获得意想不到的好处。
所以,Makefile也是会有假目标的。
其实这个假目标可以给Makefile提供十分强大的功能。比如对源代码进行清理、将生成的最终目标安装到合适的地方、进行反安装、对生成的目标进行测试、给使用者提供帮助信息,等等。
那么什么是假目标呢?
在前面的讨论中所涉及的所有目标和条件都会进行文件的创建或更新的动作,尽管这是最为常见的用法。但是之前也说过,条件一定会是文件,但是目标就未必。
因为有些时候将目标作为标签来代表一系列命令也是非常有用的。一般我们把不生成目标文件的规则中的目标就称作假目标。因为没有文件生成,所以假目标的命令就一定会被执行。
不过有的时候虽然定义了一个不会生成目标文件的规则,但很难保证项目中不会有与这个目标重名的文件存在。一旦有这种现象发生,我们的假目标就不会工作了。
所以需要进行一下特殊的处理。这个特殊的处理方式就是让这个假目标成为一个特殊目标-.PHONY-的条件。这样无论项目中是否有重名的文件,这个目标所对应的命令都会被执行。
所以我们的“第一个例子”就被修改成代码7所示的那样了。
代码7:
cc :二 gcc
LD := gcc
CFLAGS :二 -g -W -std=c99 一c
LDFLAGS := -lcurses
all:TinyEdit
. PHONY all install uninstall clean
install:TinyEdit myless
cp TinyEdit /usr/bin/
cp myless /usr/bin/myless
unins tall:
rm -f /usr/bin/TinyEdit
rm -f /usr/bin/myless
TinyEdit: main.o line.o buf fer.o tools.o chunk.o document.o cursor.o
$ (LD) $(LDFLAGS) $^ -o $@
myless: myless.o line.o buffer.o tools.o chunk.o
$ (LD) $(LDFLAGS) $ˆ -o $@
clean:
rm-f k.
rm -f TinyEdit myless
到现在为止,这个Makefile已经非常酷了。不是吗?它不但可以生成TinyEdit和myless,还能够安装或反安装它们,还能对源代码进行清理。
不过我们还不能自满,因为到目前为止,我们所使用的代码都还在同一个目录,这显然不是我们管理工程的好方法,尤其是使用Java的同学,每一个包就要有一个目录。
所以,得让我们的Makefile支持这种多目录的项目管理方案。
8.3.7 路径搜索
我现在要将我的代码重新组织一下,建立src和include子目录,并将所有“.c”文件移动到src子目录中,所有“.h”文件移动到include子目录中,Makefile保持不动。
最简单的办法就是修改规则中的条件,增加子目录。但是这显然很麻烦,而且容易弄错。所以Makefile引入了VPATH和vpath。
VPATH相当于一个变量,变量的值是需要make搜索的子目录, 目录之间用空格分割。经过重新组织代码,Makefile如代码8这样。
代码8:
CC := gcc
LD := gcc
CFLAGS := -g -W -std=c99 一c -Iinclude
LDFLAGS := -lcurses
VPATH := src include
all:TinyEdit
TinyEdit:main.o line.o buffer.o tools.o chunk.o document.o cursor.o
$ (LD)$ (LDFLAGS) $人 一O $@
myless: myless.o line.o buffer.o tools.o chunk.o
$ (LD) $(LDFLAGS) $人 -o $@
myless.o:myless.c line.h buf fer.h tools.h tedef.h
document.o:doclunent.c line.h buffer.h cursor.h tools.h tedef.h
cursor.o:cursor.c line.h tools.h tedef.h
line.o:line.c line.h buf fer.h tools.h tedef.h
tools.o:tools.c tools.h tedef.h
chunk.o:chunk.c line.h buf fer.h tedef.h
buf fer.o:buffer.c buf fer.h tools.h
main.o:main.c line.h buf fer.h tedef.h
虽然VPATH可以解决路径所有的问题,但是有一个限制,就是make会为任何它所需要的文件搜索由VPATH所指定的所有路径,
如果在多个路径中找到同名文件,则会采用第一个被找到的。这种问题在“山”文件中经常出现,会很麻烦,或许找到的完全不是你想要的。为此就需要使用vpath了。
vpath要比VPATH更加精准,因为它采用模式来指定在某个具体路径中搜索哪些类型的
文件。语法如下:
vpath %.c src
vpath %.h include
这就指明了在src目录下只搜索“.c”文件,在include路径下只搜索“.h”文件。
8.4 高级特性
你现在已经具备了使用Makefile来管理一些规模较小的工程的能力了。不过这个时候如果要想读懂其他大型开源软件的Makefile还是有一些困难的。
接下来我将开始给大家讲述Makefile的一些高级特性。make的官方文档在描述这部分内容的时候,并没有将它们纳入所谓的高级特性之中。
我这么划分是因为我认为拥有这些特性的一份文档,它就是一个“高级语言”的源代码文件了,高级特性因此而得名。好,那我们看看Makefile到底还能干些什么事情呢?
8.4.1 文件包含 模块化
不管是现代化的面向对象的,还是很多人早己不屑的面向过程的高级语言,模块化都是它们必须具备的素质。
毕竟将一堆臭袜子放在一个筐里,当你想再次使用的时候,想找到一对儿相同的还是有困难的,至少你要多花一些时间去忍受那令人作呕的气味。
所以将不同的功能划分到不同的文件来实现是一个良好的编程习惯。这其中Java就提供了“import”,C提供了“#include”来支持这种写作习惯。
我们的Makefile也不例外,只是它还要与规则扯上关系。
Makefile使用“include”指令来包含其他文件。功能有二:一是类似于C语言那样引入一个头文件,实现代码复用;二是用来自动产生依赖关系的信息。
include指令的格式如下:
include FILEl FILE2 FILE3 …
这个指令可以引入任意多个文件,且支持shell的通配符和变量。
include指令的特性与我们熟知的高级语言相同的地方是将引入的内容与所在的
Makefile做融合处理,不过也就这么一点点了。刚才说过,它要与规则扯上关系,是怎么扯上的呢?
当make遇到include指令,就会读取它指定的文件:一旦遇到一个不存在的,不要紧,顶多抱怨一句,继续来。直到将所有文件读完,这个时候就要收拾刚才找不到的文件了。
怎么收拾的呢?就是将这些文件作为规则的目标,尝试着去生成它。生成一个读进来一个。但凡有一个没搞定,make就马上报错罢工。
等到都读完了,就开始常规工作了。这个时候一旦根据某些规则将incTude所指定的某个文件更新了,也会导致这个这个文件被重新读入。
因此我们可以将整个include指令看作一个特殊规则,只是这个规则缺少中间的“:”,include就是目标,它所指定的文件都是条件,
这条规则的命令就是逐一读入每个条件所对应的文件并与其所在的Makefile进行融合。
理论是需要实践来检验的,我提供了两个例子。代码9或许是一个比较蛋疼的例子,不过它能够描述上述的内容,它也是从“第一个例子”修改而来的。
代码10就更是蛋疼了,这是一个死循环,提供这个例子是为了充分展示Makefile的能力(最新版的make在循环一段时间后会停止)。
代码9:
common.mk文件
cc = gcc
LD = gcc
CFLAGS = -g -w
LDFLAGS= -lcurses
define .mk文件
CFLAGS += -std=c99
Makefile文件
include common. mk def ine. mk
all:TinyEdit
TinyEdit: main.o line.o buf fer.o tools.o chunk.o document.o cursor.o
$ (LD) $(LDFLAGS) $^ 一o $@
myless: myless.o line.o buf fer.o tools.o chunk.o
$ (LD) $(LDFLAGS) $^ -o $@
代码10:
.PHONY:dummy.mk
include dummy.mk
dummy.mk:
touch $@
还有一点是必须要讲的,就是include指令怎么找文件。如果给定的是绝对路径,那就很简单了,直接读取就是了。如果给定的相对路径,最优先的自然是当前工作路径。
如果找不到,就会按照执行make命令时给定的“-I”或“-include-dir”选项所指明的路径中去搜索。
如果还找不到,make也不会气馁,它会在自己当初被编译的路径中去查找(这个路径是什么,鬼才知道),可见它有多么的执着!
即便到了这份田地它还是不肯罢休,还要查看是否有可用的规则去创建这个文件。只有这些希望都破灭之后才会报错停工。
但是如果你依然不希望它去休息一下的话,在include指令之前加入“-”,或者使用“sinclude”指令,它就:什么也不说,主人知道我,一颗博大的心啊,愿天下都快乐。
8.4.2 命令
我猜想你到现在多少对命令有了一些了解,但是一定还有很多疑问并没有得到解答。比如我说过,命令实际上就是shell命令。
但是Linux的shell有很多种,能叫上名字的就有如:Bash、Korn shell、C shell等几种。那么make执行的是那个shell的命令呢?
是不是执行make命令时所采用的shell呢(比如你采用C shell作为默认shell)?
答案很给力,就是“/bin/sh”这个东西,别无它选。这个东西实际上就是bash,也是Linux世界中应用最为广泛的shell,而且任何Linux发行版都会提供。
make之所以选择它也正是因为如此。如果你坚持要让make使用其他shcll是不是没办法呢?不是,有办法!不过我不想告诉你,因为这会降低你所写的Makefile的兼容性。
所以这种费力不讨好的事情就留给别人吧!
那既然命令是bash脚本,是不是所有的bash脚本语句都适用呢?不是,Makefile的命令有一个限制,就是只能单行处理bash脚本。
注意,这并不是说一个命令只能是一行,而是每一行必须是一个完整的bash脚本。比如for语句:
for d in a b c
do
echo $d/*
done > list.txt
这条语句是将a、b、c三个目录中的内容输出到list.txt文件中。这是一个正确的bash脚本。但是你要在Makefile的命令中使用它,就会报错。
你应该将它们写在一行。形如这样:
for d in a b c; do echo $$d/*; done > list.txt
这是合法的bash脚本格式,同时也是合法的Makefile命令。需要注意的是对变量“d”的引用上,采用的是$$d,这样写是为了区别于Makefile的变量。
而且注意我的用词。我说这是合法的bash脚本格式,但是如果原封不动的写在bash脚本文件中,得到的结果就完全不同了。
因为“$$”是bash的一个内置变量(当前shell进程的pid)。当然,如果你觉得这个缺乏可读性,还是可
以写多行的,但必须是这样:
for d in a b c; \
do \
echo $$d/*; \
done > list,txt
这就是耍了点小把戏,因为在Makefile中“\”代表折行。Makefile的命令除了这个限制,还是支持多行的。比如可以这样写:
@echo "Get files list,”
@for d in a b c; \
do \
echo $$d/★; \
done > list.txt
这样这个命令就可先提示“Get files list.”,然后生成list.txt这个文件。
另外,命令不是随便写的。它只能出现在规则中和用于变量的赋值(还有一个特殊的地方后面会介绍)。其他地方都认为是非法的。
所以我们必须是这样写:
list,txt:
@echo "Get files list."
@for d in a b c; \
do \
echo $$d/*; \
done > S@
或者
FILE- LIST := 、for d in a b c;do echo $$d/*; done、
在实际使用中,这种用于获取文件列表的操作很多时候都是赋给变量的。
如果你足够细心的话,就会发现我在后面给出的例子中,每个命令前面都冠以”@”符号,尤其是在规则中的命令。这就是命令的修饰符。
Makefile的命令修饰符有三个,分别是“@”、“—”和“+”。它们都是什么含义呢?
“@”要求不要输出命令。如果make遇到了没有冠以这个修饰符的命令时,会将命令本身输出出来。就像我们前面看到的那些例子中看到的一大堆编译指令。
使用这个命令修饰符的好处是,可以让Makefile的输出内容非常容易阅读。
“—”要求命令忽略错误。这个修饰符我们在介绍include指令的时候就说过了。
“+”要求只显示命令,而不去执行。这个看起来用处不大,不过在编写递归式的Makefile时会用到。
8.4.3 深入变量
前面我们已经介绍过变量了,而且也说过变量与C语言的宏在行为表现上颇为类似。不要以为这些都是细枝末节,其实只有真正掌握了这些“细枝末节”才能够明白Makefile的某些行为方式。就变量本身而言,也要分为两种。前面我们看到的,使用“:=”赋值运算符来定义的变量被称之为“经简单扩展的变量”,另外一种叫“经递归扩展的变量”。怎么样,名字,唬人吧。当我们进一步讨论它们的行为时,还有更唬人的呢。
首先我们看看经过简单扩展的变量,它的行为是这样的:只要执行赋值语句,变量的值就是赋值运算符右面的内容了。如果赋值运算符右面的内容含有变量,则这个变量的值也会一同被赋值。比如:
MAKE- DEPEND := $(cC) 一M
变量MAKE DEPEND的值可能是“gcc -M”。如果变量CC之前没有被定义,此变量的值就会是“<space> -M”。注意这个<space>,它代表一些空格,不过有多少个空格是不确定的,完全取决于你赋值语句的写法。详细情况是这样的,一个变量如果是空的(也就是没定义),那么make就用前导空格来代替。
经递归扩展的变量会有什么行为呢?它的行为有些迟钝,它就是将赋值运算符右面的内容完整地、不做任何处理地赋给变量。这个时候如果赋值运算符的右面不包含变量,它的行为与经过简单扩展的变量没有任何区别。但是有变量的时候就不同了,它不会取这个变量的值,直到被使用的时候才会这么做。这也是“经递归扩展”的含义所在。这也是一种“缓释评估”的方法。“缓释评估”非常之有用,在我们设计程序的时候也应该尽量采用,可以极大地提高程序的性能。而且我们非常熟悉的工厂模式也是一种“缓释评估”方案。经递归扩展的变量是这样定义的:
MAKE_DEPEND = $(CC) -M
与经简单扩展的变量就是一个“:”的差别,很多人会把它们弄混,所以要切记。经过这么定义之后,只要稍后再定义CC这个变量,比如:
cc = gcc
那么使用MAKE- DEPEND的时候,它的值就是“gcc -M”了。不过混乱才真正开始,因为接下来你再改变变量CC的值,如:
CC = cc
这之后你再使用MAKE- DEPEND变量,它的值就是“cc -M”。像这个例子在Linux中还不算什么大问题,因为gcc和cc实际上是相同的东西,所以行为都是一样的。
现在让我们看另外一个例子,你定义了一个这样的变量:
start = $(shell date)
这是要执行shell命令data来获取当前时间(这里的“shell”是make函数,后面会介绍)。你的麻烦就出来了。这个时候start的值并不是时间,只有使用它的时候才会执行shell命令来获得时间。每使用一次start,它的值就会变一次。这根本就不是你想要的结果。这个地方适用于经简单扩展的变量。
另外还要多说一句,就是这里为什么没有用前面提到的“一”这个方法来说明。因为无论是采用经简单扩展的变量还是经递归扩展的变量在使用“一”方法通过shell命令来给变量赋值时,都不会执行对应的shell脚本,只有在使用对应变量的时候才会去具体执行并获得输出值。这个时候都可以认为是“经递归扩展的变量”。
其实经递归扩展的变量很多时候是非常有用的,当然,它烦人的时候也是真烦人。所以一定要注意它与经简单扩展的变量在定义上的细微差别。
变量的类型讨论完了,我们还得关心一下变量的赋值问题。前面所讲的内容算是两种赋值的方式,而Makefile还有另外两种赋值方式,真是要多乱有多乱啊。没办法,这部分内容太重要了,所以我们还得耐心继续学。
首先要介绍的是“?=”这个东东,名字挺邪乎的,叫“附带条件的变量赋值运算符”。又是一个唬人的东西。首先你就要问,附带什么条件啊?其实也就一个条件,就是它左边的变量之前没有被定义过。定义过就跳过,没定义就赋值。这玩意就干这点事儿。所以很多东西别看它咋呼得很邪乎,其实没什么了不起的。
那么接下来的就很有用了,是“十=”这个东东。学过C或Java的很熟悉吧。不过Makefile中的变量不支持数值运算,所以它的作用是用于扩展变量内容的,即保持变量原有的内容不变,在其后面再追加新的值。你可能马上会想到,这个东西可以用前面的赋值运算符模拟。没错,对于经简单扩展的变量这一点问题都没有。就是下面这样的形式:
A: := $(J\) B
但是遇到经递归扩展的变量呢?看看下面的形式:
i,L= $(A) B
这个有什么问题呢?当使用A的时候,会遇到A引用了A本身,这会无限循环下去的。所以需要由“+=”出面来解决这个问题。
好了,现在终于吧变量的赋值也都搞清楚了,但是不是就这样结束了呢?嘿嘿,耐心点吧,这才刚刚开始。
其实之前我们所说的变量都是全局变量。一个编程语言怎么可能只有全局变量而没有局部变量呢?所以Makefile也必须有(我一直认为Makefile是一门编程语言)。不过这又要与规则扯上关系。什么关系呢?看名字就知道了,叫目标专属变量。不过这并不是我们印象中的局部变量。它是怎么工作的呢?可以先看一下“代码11”。
代码11:
cc := gcc
LD := gcc
CFLAGS := -g -W -std=c99 一c
LDFLAGS :=
all:TinyEdit
TinyEdit:main.o line.o buffer.o tools.o chunk.o document.o cursor.o
$ (LD) $(LDFLAGS) $^ 一o $@
myless: LDFLAGS += -lcurses
myless: myless.o line.o buf fer.o tools.o chunk.o
$ (LD) $(LDFLAGS) $^ 一o $@
这个还是从“第一个例子”修改过来的。要生成myless这个可执行文件,必须依赖curses库,之前改过的一个例子是将引用这个库的连接参数放入LDFLAGS变量中。但是这个变量TinyEdit也需要使用,而它却不需要curses库。通过目标专属变量就能够搞定这个问题。
目标专属变量的值,在处理对应目标的规则时会进行改动,当离开这个规则之后,变量值会被恢复,因此不会影响到其他的目标,这也是目标专属变量名称的由来。对目标专属变量的赋值可以是我们之前所介绍的所有赋值方式。
除了我们这些自定义的变量和make内建的一些变量之外,make还提供了几个非常有用的专有变量,见表8-2。
表8-2 make的专有变量
变量名 |
作 用 |
MAKE_VERSION |
GNU makc的当前版本号 |
CURDIR |
makc的当前工作路径 |
MAKEFILE_LIST |
make所读入的所有Makcfile文件的列表,包括include的。很常用 |
MAKECMDGOALS |
在makc命令中给定的要生成的目标 |
VACRIABLES |
所有已定义的变世名列表,不包含目标专有变量。这是一个只读变量 |
此外,变是的定义并不仅限于Makefile文件,它还有很多来源,见表8-3。
表8-3 make变量的来源
来源 |
说 明 |
文件 |
也就是定义在Makcfile中的变量 |
命令行 |
执行make命令时可以指定,如: make CFLAGS=-g 要注意shell的一些规范,以免shell将变量解析为命令选项 |
环境 |
在递归式的Makefile中,上层所定义的变量会通过环境传递给下层的Makefile。稍后我们会详细讨论递归式的Makefle |
自动创建 |
就如我们前面所讲述的自动变量,由make自己创建的 |
有关变量的内容看起来算是告一段落了,不过Makefile中的变量还远没有这么简单,我们接下来会遇到更为特殊的变量形式。
8.4.4 宏与函数
前面在介绍命令的时候说过可让命令的标准输出作为变量的值。不过有些时候我们对命令的输出并不感兴趣,感兴趣的是可以节省重复写这些命令的时间和命令的执行结果。显然通过经递归扩展的变量就能做到,并且也有过简单的一个例子(不会现在就忘了start=$( shell date)吧?)。虽然作为单条命令采用这种方法问题不大,但是要是多条命令呢?显然可读性会是一个问题。
所以Makefile引入了宏这个概念。宏这个概念与经递归扩展的变量基本上是一致的,只是写法不同。而且在GNU make的官方文档中,也是把它们放在一起说明。也正是因为这样,我才在介绍变量的时候提到它与C语言的宏颇为类似。
我们看一下定义宏的语法:
define MACRO- NAME
endef
只要将你的命令写在“……”那里就行了,不过要注意命令的写法。
如何使用定义好的宏呢?跟变量一样,使用“$O”或“${}”。这回明白为什么将宏跟变量往一块扯了吧!
聪明的你一定马上就会想到,如果让宏可以带有参数不就更灵活了吗?嗯,这个可以有。带有参数的宏叫函数,至少make的官方文档是这么定义的。定义方法与宏的定义方法完全一致。要引用参数需要使用“$1、$2、$3……$n”这样的自动变量,“$”后面的数字代表第几个参数。不过要调用这个自定义的函数跟引用普通宏的方式就有差别了,语法是这样的:
$ (call MACRO_NAME [, argl, ……, argn])
这样说看来有点不好理解,所以我们修改一下我们之前的一个例子:
def ine get_files_list
@echo "Get files list”
@for b in $ $ $; \
do \
echo %%b/★; \
done > S@
endef
list.txt:
$(call get_files_list.a,b,c)
其实在Makefile中已经内置了很多非常有用的函数。不过调用方法与我们自定义的函数不一样,语法是这样的:
$( function argl[, argn])
明显比我们自定义的要简单,Makefile有些欺负人。不过真实的情况是,Makefile本不打算支持自定函数的,只是需求太多了,也就得支持了。它并没有采用与内置函数相一致的语法取而代之的是一个巧妙的方法,就是提供了一个名为“call”的内置函数,用这个函数使用一些技巧来扩展宏,使得它们支持“$1”,“$2”这样的自动变量来模拟调用用户自定义函数的功能。而且call函数不只是对宏起作用,对变量同样有效。如果你能让你的变量支持“$l”,“$2”这些东西,也可以用call函数来扩展它。
Makefile内置的函数非常多,功能也非常强大,我简单列一下它们都能干付么:
可以用来处理字符串,但凡你能想到的都有;
可以对列表进行排序或调用系统命令;
可以对文件名进行处理;
控制Makefile的工作流程;
提供eval和value这样强悍的函数。
最后要说明一点,无论是宏还是函数,都是有所谓的“返回值”的。这个返回值就是宏或函数中命令的标准输出。取得函数的返回值很容易,不需要做什么特殊的动作,只要将函
数调用语句放在赋值运算符的右侧就行。宏就需要使用“一”这个东东了,大家还记得它吗?
8.4.5 条件指令
虽说Makefile的内置函数能够提供一定的流程控制,但是它并不是万能。因为不管是命令、宏还是函数,它们的使用是有严格限制的,就是要么在规则中,
要么在变量赋值中(其实这并不是十分严谨,有一些函数可以“乱写”,只要不涉及命令就问题不大)。如果要对规则进行舍取该肿么办捏?
办法就是采用条件指令,同时也为宏和函数乃至命令提供了另外一个能够使用的地方。
Makefile的条件指令与c的预编译指令很像,它们是:ifdef、ifndef、ifeq. ifneq,结尾统一使用endif,并且支持else。
ifdef和ifndef用于判断某个变量是否被定义。ifeq和ifneq用于两个值是否相等。ifeq和ifneq用得非常多,它要进行比对的两个值可以来自变量、宏或函数,
如果你不嫌麻烦,来自命令也可以(还记得“…’吗(反引号)?另外命令也只能出现在这里)。
条件指令的语法如下:
if -condition
条件为真的所有内容
endif
或:
if -condition
条件为真的所有内容
else
条件为假的所有内容
endif
8.5 Makefile实战
我个人认为,如果能把上述的内容全部掌握并融汇贯通,再结合一些实践,基本上可以使用Makefile应对任意规模项目的管理和创建工作。
8.5.1 自动产生依赖
依赖关系始终是一个特别烦人的事情,尤其是C语言的头文件。因为只要修改了头文件,那么所有引用的“.c”文件都应该被重新编译,否则就得不到预期的结果。
要手动维护这些依赖关系既麻烦又容易出错,如果能够自动维护生成,那就真是轻松加愉快了。make和gcc通过“狼狈为奸”成就了这种可能,下面我就介绍一下如何自动产生依赖关系。
gcc提供了一个-M和-MM的命令选项。前者可以输出一个“.c”文件所引入的所有头文件,后者则排除掉了系统提供的头文件,只保留用户自定义的头文件。
我们一般只关心后者,因为系统提供的头文件谁都不会去更改。它的输出差不多是这样:
main.o: main.c buffer.h line.h
大家很熟悉吧,这不就是Makefile的目标和条件吗?可见gcc与make的关系不一般了吧。那么有了这个功能,我们看看一个实际的例子,见代码12。
代码12:
cc := gcc
LD := gcc
CFLAGS := -g -W -std=c99 一c
LDFLAGS := -lcurses
SOURCES := $(wildcard *.c)
all:TinyEdit
TinyEdit: main. line.o buf fer.o tools.o chunk.o document.o cursor.o
$ (LD) $(LDFLAGS) $人 -o $@
myless: LDFLAGS += -lcurses
myless: myless.o line.o buf fer.o tools.o chunk.o
$ (LD) $(LDFLAGS) $^ -o $@
-include $(subst .c,.d,$(SOURCES))
%.d:%.c
$(cc) 一MM $(cFlj9LGs) $<> $@.$$$$;\
sed ,s,/($★/)/.o[ :]★,/.。 $@ : ,g, < $@.$$$$ > $@;\
rm-rf $@.$$$$;
这段代码改编自我们的“第一个例子”,这个也算是终极精简版本了。这里需要注意的是这一段:
%.d:%.c
$(cc) 一M4$(cFLJ\Gs) $<> $@.$$$$f\
sed ,s,/($*/)/.o[ :)*,/1.o $@ : ,g, < $@.$$$$ > $@:\
rm-rf$@.$$$$;
现在你们理解这段代码应该不是太困难的事情,就是一个模式规则。将“.c”文件转化为“.d”文件。至于它的命令,第一行也很好理解。就是使用我们前面介绍的gcc -MM命令选项生成“.c”文件与它的头文件的依赖关系。并且通过IO重定向输出到一个临时文件。第三行也容易理解,就是删除这个临时文件。至于第二行就很难理解了。
然后我们要注意的是用到的两个函数。第一个函数wildcard,是将参数中的通配符与当前路径中的文件进行匹配,将所有匹配到的文件返回,接收这个返回结果的就是变量SOURCES。
在这个例子中,SOURCES保存了所有的“.c”文件的文件名。第二个函数subst是字符串替换,将参数1的内容替换成参数2的内容,替换目标是参数3,并将结果返回。
那么这个例子中include就相当于要引入所有的“.d”文件。根据之前讲过的规则,如果include
找不到文件,就会去生成它,所以就会执行最后的那个模式规则来生成所有“.d”文件。然
后再重新引入这些.d文件,因此之前所产生的依赖关系就被引入到这个Makefile中了。
我猜大家一定会有一个疑问,就是为什么确定依赖关系的规则中有一个“.d”做目标。这个其实是为了保证当有文件被修改的时候,不会遗漏依赖关系变化。
比如你在一个“.c”的头文件中又引入了新的头文件,是不是这个依赖关系就发生变化了呢,而我们又没有定义过“.h”到“.d”的模式规则(而且也不现实)。
上面产生依赖文件的代码实际上源自make的官方文档,设计得非常巧妙。而且新版本的gcc又配合这个设计提供了一个新的“-MT”选项。
可以直接在依赖关系中加入“.d”文件。代码13采用了这种方法。
代码13:
cc := gcc
LD := gcc
CFLAGS := -g -W -std=c99 一c
LDFLAGS := -lcurses
SOURCES := $(wildcard *.c)
all:TinyEdit
TinyEdit: main.o line.o buffer.o tools.o chunk.o document.o cursor.o
$ (LD) $(LDFLAGS) -o $@
myless: LDFLAGS += -lcurses
myless: myless. line.o buf fer.o tools.o chunk.o
$ (LD) $(LDFLAGS) $ˆ -o $@
-include $(subst .c,.d,$ (SOURCES))
%.d:%.c
$(cc) 一MM -MT ”$< $@” $(cFLJ\Gs) $< > $e
这个更简单也更容易懂了吧?gcc与make真是“狼狈为奸”啊!其实在Linux世界中,这种“狼狈为奸”的行为是数不胜数的,不过最终的受益者就是我们啦。
上面给定的代码是Makefile的官方文档给的例子,但是我个人认为要生成.d作为附加的依赖文件有点不爽,
所以我自己发明了一段同样可以自动处理依赖的代码,并且不用生成.d文件。见代码14。
代码14:
cc := gcc
LD := gcc
CFLAGS := -g -W -std=c99
LDFLAGS :=
TinyEdit_OBJS :=main.o line.o buffer.otools.o chunk.o document.o cursor.o
myles S_OBJS :=myless.o line.o buf fer.o tools.o chunk.o
all:TinyEdit
.PHONY:all
all_SRCS := $(wildcard *.c)
$( foreach src,$(all_SRCS), $(eval $(shell $(CC) $(CFLAGS) -MM $(src))))
$ (subst .c,.o.$ (all_SRCS)):
$ (cc) $(CFLAGS) 一c -o $e $<
TinyEdit:$(TinyEdi t_OBJS)
$ (LD) $(LDFLAGS) $人 一O $@
myless: LDFLAGS += -lcurses
myless:$(myless_OBJS)
$ (LD) $(LDFLAGS) $人 -o $@
代码稍微复杂了一点,但是不难理解。主要是用到的几个内置函数需要解释一下。
$(wildcard*.c)函数,用于扩展“*.C”这个通配符,匹配当前目录下的所有.c文件,即变量$(aII_SRCS)的值是当前目录下所有.c文件名,之间采用空格分割。
$(foreach src,$(aII_SRCS),$(……)),实现foreach循环。逐个取出$(aII_SRCS)中的文件名,并放入临时变量$(src)中,并调用最后给定的函数。
(eval$(……)),一个非常特别的函数。用于给定的宏,同时让make解析这段宏所展开的内容。这个函数是实现本例的关键。
$(sheU……),执行后面的shell脚本。在例子中实际上实行的就是gcc -MM这个命令。
$(subst.c,.o,$(aIl_SRCS》,返回一个值,这个值就是$(aII_SRCS)的值中将.c替换为.o。即返回所有.c文件对应的.o文件。
那么在本例中就是将所有.o文件作为了一个目标。
那么生成.o文件的条件呢?在前面调用foreach函数时指定了。Makefile中允许分开指定目标的条件的。
8.5.2 递归式的Makefile
什么是递归我想没有必要多解释,就一句话:Makefile里再调用make去执行另一个Makefile。为什么要递归呢?
这背后的动机也很简单:处理单一目录的Makefile可以非常精简,目录越多越复杂。
因此,使用Makefile来管理大型工程时,给每个目录准备一个Makefile,可以很好地降低复杂度。
不过一定要跟我抬杠,说你可以使用脚本来完成,那我也有后招。
就是如果在较高的目录层次中存在依赖关系,还是Makefile容易搞定。
首先看一下在Makefile中如何调用make去执行另一个Makefile。常用的语法是这样的:
cd subdir && $(MAKE)
或
$ (MAKE) -C subdir
这两条命令的作用是等价的,也应该很好理解,就是进入一个子目录,然后执行make。
这里需要注意的地方有两个:一是被执行的Makefile在执行完毕后不需要类似“cd.,”这样的命令,
因为每个命令都有一个独立的shell环境;二是永远要使用$(MAKE)这个变量来执行make,这可以保证在一个系统中安装了多个版本的make时使用的make就是你想要的。
其次,进行调用的Makefile和被调用的Makefile之间你一定不希望完全没有任何联系,那么有什么联系呢?
有两个:一是变量传递,二是命令行参数传递。干嘛要传变量呢?最简
单的解释就是顶层Makefile中定义的变量可能在底层Makefile中也要用,比如编译参数。
但是如果什么变量都传递也会带来不小的麻烦,所以也就有了下面这些规则:
1.在命令行上赋值的变量默认传递,并且仍以命令行方式传递,这可以保证在任意递归深度时确保被传递下去;
2.Makefile中定义的变量默认不传递;
3.用export命令可以明确传递某一个变量,这个export跟bash的export不是一回事,不过用法却一样(如export VAR);
4.用unexport命令可以明令不传递某个变量;
5.低层Makefile对上层Makefile传递的变量进行的更动不会传递回上层;
6.如果对一个变量有多次赋值,则它们的优先级顺序为:命令行赋值优先级最高;
Makefile自身的赋值次之;顶层Makefile传递过来的最低,上层Makefile传递过来
的命令行除外(这也解释了a中为什么可以保证传递下去的原因)。
命令行参数为啥要传递呢?用脊髓思考一下吧,就是为了保证make整体行为的一致性。
不能因为增加了递归Makefile就无视我们给定的命令参数不是?
最后我们还是给一个例子吧,毕竟我总是不习惯光说不练的。我们拿代码8路径搜索的例子为例,
所有的.c文件都在src目录下,所有的.h文件都在include目录下。
在代码的根目录和src目录下的Makefile分别见代码15和代码16。
代码15:
cc := gcc
CFLAGS := -g -w -std=c99
LDFLAGS :=
TO P_DIR := $(CURDIR)
export TOP_DIR CC CFLAGS LDFLAGS
all : TinyEdit
. PHONY : all clean
TinyEdit :
$ (MAKE) -C src $ (TOP_DIR) /$@
myless :
$ (MAKE) -C src $ (TOP_DIR) /$@
clean :
rm -f src/* .o
rm -f TinyEdit myless
代码 16:
CFLAGS += -I$ (TOP_DIR) /include
$ ( TOP_DIR) /TinyEdit : main . o line . o buf fer . o tools . o chunk . o document . o
cursor . o
$ (cc) $ (LDFLAGS) $^ -o $@
$ ( TOP_DIR) /myless : LDFLAGS += -lcurses
$ (TOP_DIR) /myless : myless . o line.o buf fer.o tools . o chunk . o
$ (cc) $ (LDFLAGS) $^ -o $@
8.5.3 自动产生Makefile
“人类为什么会进步?”
答:“因为人很懒!”
没错!因为人都懒得去做算术题才发明计算机;因为程序员懒得处理指针才发明了Java;因为有人懒得写Makefile才有了这个话题。
其实就整个Linux世界而言,使用工具来生成Makefile也是一种常态,所以我也不得不给大家介绍。
但到底是不是真正懒得写Makefile才去自动生成,等我讲完接下来的内容后,我们一起来评判。
首先我们应该牢记,在当前,如果使用源代码来安装Linux软件,基本上都是下面的三步走:
./conf igure
make
make install
其实第二步和第三部我们应该很了解了,但是第一步是做什么的呢?第一步就是用来自动生成Makefile的。
其实大多数Linux上以源代码发行的软件都没有Makefile,但是却带有一个configure文件。
这是一个使用Bash脚本写成的程序,不过这东西绝大多数时候真“不是人写”的,因为它也是自动生成的,作用就是生成Makefile文件。
因为configure是自动生成的,所以对于所有的软件(使用configure的软件)都是有共性的,
共性就是都会有基本相同的命令行选项可用。最常用的命令行选项见表8-4。
表8-4 configure常用命令行选项
选项名 |
描述 |
-h, --help |
用于显示帮助信息,如有哪些选项可用和选项的作用 |
--prcfix=PREFIX |
指定默认的安装路径,默认值是/usr/local |
--exec-prefix=EPREFIX |
指定默认的可执行程序安装路径,默认值是PREFIX |
--bindir=EPREFDIR |
可执行程序安装路径,默认的是EPREFIX/bin |
--libcdir=DIR |
库文件安装路径,默认的是EPREFIX/lib |
--sysconfdir=DIR |
配置文件安装路径,默认是PREFIX/etc |
--includedir=DIR |
需要安装的头文件路径,默认是PREFIX/include |
--mandir=DIR |
帮助文件安装路径,默认是PREFIX/man |
--disable=FEATURE |
是否关闭某个特性。等同于-enable-FEATURE=no |
--cnable-FEATURE[=ARG]MAKE_VERSION |
是否开启某个特性,ARG是yes或no |
表8-4中的大多数命令行选项可以影响到最终生成的Makefile的install目标,从而决定软件的安装位置。
configure还可以决定生成软件时使用什么样的连接器、编译器乃至连接或编译器选项。
这是通过在命令行中给定的环境变量实现的。常用的环境变量见表8-5。
表8-5 configure常用的环境变量
表8-5 configure常用的环境变量
变量名 |
描 述 |
CC |
C编译器 |
CFLAGS |
C编译器选项 |
LDFLAGS |
连接器选项 |
CPPFLAGS |
C++编译器选项 |
上面的变量名都很熟悉吧,就是Makefile的一些内置变量,能够起到什么作用现在也应该很明白的。
configure很有用是吧?那么它是怎么生成的呢?它又是怎么生成Makefile的呢?这里就要用到Autotools工具了。
Autotools
autoscan
autoconf
autoheader
automake
我们实际执行一个例子来看看整个过程,然后我们再解释。我们还是与TinyEdit的代码为例,依然是代码8路径搜索的例子。
1. 在源代码根目录建立一个Makefile.am文件,内容如下:
AUTOMAKE_OPTIONS=foreign
SUBDIRS = include src
在src子目录下建立Makefile.am文件,内容如下:
INCLUDES = -I$ (top_srcdir) /include
bin_PROGRAMS=TinyEdit myless
TinyEdit_SOURCES = main.c line.c buf fer.c \
tools.c chunk. cdocument.c\
cursor.C
myless_SOURCES =myless.c line. cbuf fer.c \
tools.c chunk.c
myless_LDADD = -lcurses
clean-am:
-rm -f*.O
-rm -f TinyEdit myless
在include子目录下建立Makefile.am文件,内容如下:
include_HEADERS = line. hbuf fer. hcursor.h 、
tools.h tedef.h document.h \
chunk.h
2. 在源代码的根目录下执行autoscan命令。之后查看目录内容应该类似下面的结果:
Makefile. am autoscan. log configure. scan include src
使用你喜欢的编辑器,如vim打开configure.scan文件(我比较喜欢Emacs),它的
内容应该如下:
# 一。一Autoconf -★一
# Process this file with autoconf to produce a configure script.
AC_PREREQ([2.68])
AC_INIT([ FULL- PACKAGE -NAME], [VERSION], [BUG-REPORT -ADDRESS])
AC_CONFIG_SRCDIR([include/buff er.h])
AC_CONFIG_HEADERS([config.h])
# Checks for programs.
AC_PROG- CC
# Checks for libraries .
# FIXME : Replace 'main ' with a function in ' -lcurses ' :
AC_CHECK_LIB ( [ curses ] , [main] )
# Checks for header files .
AC_CHECK_HEADERS ( [ fcntl . h locale . h memory . h stdlib . h string . h unistd . h] )
# Checks for typedefs, structures, and compiler characteristics.
AC_HEADER_STDBOOL
AC_C_INLINE
AC_TYPE_INT3 2_T
AC_TYPE_SIZE_T
AC_TYPE_SSIZE_T
# Checks for library func七ions. '
AC_FUNC_MALLOC
AC_FUNC_REALLOC
AC_CHECK_FUNCS ( [memmove setlocale] )
AC_CONFIG_FILES ( [Makefile
include/Makefile
src/Makefile] )
AC_OUT PUT
# _ k_ Autoconf -:Jc-
# Process this file with autoconf to produce a configure script.
AC_PREREQ ( [ . ] )
AC_INIT ( TinyEdit , . , j agen . zhao@gmail . com)
AC_CONFIG_SRCDIR ( [ include/bu f f er . h] )
AC_CONFIG_HEADERS ( [ config . h] )
AM_INIT_AUTOMAKE ( TinyEdit , . )
CFLAGS=-std=c9
# Checks for programs .
AC_PROG_CC
# Checks for libraries.
# FIXME: Replace 'main' with a function in '-lcurses' :
AC_CHECK_LIB ( [ curses l , [main] )
# Checks for header files.
AC_CHECK_HEADERS ( [ f cntl . h locale . h memory . h stdlib . h string .h unistd . h ] )
# Checks for typedefs, structures, and compiler characteristics.
AC_HEADER_STDBOOL
AC-C一INLINE
AC_TYPE_INT3 2_T
AC- TYPE_SIZE-T
AC- TYPE_SSIZE-T
# Checks for library functions.
AC- FUNC- MALLOC
AC- FUNC- REALLOC
AC_CHECK_FUNCS([ memmove setlocale])
AC_CONFIG_FILES([ Makefile
include/Makefile
src/Makefile])
AC- OUTPUT
将configure.scan更名为configure.ac。
3. 在源代码根目录执行aclocal命令,之后查看目录应该有如下结果:
Makefile. am autom4 te. cache conf igure. ac src
aclocal. m4 autoscan. log include
4. 在源代码根目录执行autoconf命令,之后查看目录应该有如下结果:
Makef ile. am autom4 te. cache configure include
aclocal. m4 autoscan. log configure. ac src
5, 在源代码根目录执行autoheader命令,之后查看目录应该有如下结果:
Makef ile. am autom4 te. cache config.h.in conf igure. ac src
aclocal. m4 autoscan. log configure include
6. 在源代码根目录执行automake -add-missing命令,之后查看目录应该有如下结果:
Makefile. am autom4 te. cache configure include src
Makefile.in autoscan. log configure. ac install-sh
aclocal .m4 config.h.in depcomp missing
在六步之后我们就得到了我们所有想要的东西,然后执行:
./configure
make
make install
在/usr/local/bin目录就可以找到了TinyEdit和myless这两个可执行文件,在/usr/local/include目录可以找到我们定义的各种头文件。
现在我稍微解释一下这个例子。首先我们定义的各个Makefile.am文件是automake命令要处理的文件;之后我使用autoscan自动生成了一个模版,也就是configure.scan文件,
修改这个模版以便适合于我们的项目,并且指定它要与automake命令配合,同时也增加了一个编译选项;
然后使用aclocal命令根据configure.ac文件中的内容生成一个autoconf命令要调用的脚本aclocal.m4文件及一个缓存目录autom4te.cache;
接着就是调用autoconf命令来生成configure文件了;接下来使用autoheader命令来生成config.h.in文件,这个文件automake命令要使用;
最后执行automake命令,它处理所有Makefile.am和config.h.in文件生成对应的Makefile.in文件;至此我们的工作完成。
那么configure如何生成Makefile文件呢?就是根据Makefile.in文件来生成的,同时还要根据config.h.in文件最后生成config.h文件。
这个文件可以作为全局配置文件使用,在跨平台开发的时候非常有用。在我们这里的例子里没什么实际作用。
另外我们在调用automake命令时使用了-add-missing选项,这是让它自动添加一些必要的命令脚本。当最终生成configure文件之后,这一系列文件就不需要Autotools工具了。
也就是说,要创建这些代码的环境中可以不用安装Autotools工具,这种自包含的特性使得它在使用起来非常方便。
另外需要注意的是,在我们这个例子中利用-add-missing选项所添加的脚本是采用符号连接方式引入这个项目的,在实际项目发布前,应该将这些符号连接的内容复制到项目中,随代码一同发布。
经过这一系列步骤你们觉得这是“懒人”干的活吗?显然这不是!那为什么Linux上的软件要使用Autotools工具来生成Makefile,而不是直接手工编写呢?
至少在我们现在接触到的所有例子中,自己手工编写的Makefile最方便。实际情况是这样的。由于Linux系统太过*,导致各发行版之间会有较大的差别。
不同发行版所提供的基础程序库也会不尽相同,而且即便是相同的基础程序库也会有版本上的差异。如果要手工编写能够适用于所有Linux发行版的Makefile,那不但是技巧活,也是体力活了。
所以就需要有一套工具能够识别这种差异,自动根据当前的系统情况结合用户当前的软硬件环境来生成合适的Makefile文件(编译安装的configure步骤).即便发现当系统环境无法满足软件的创建要求时,也应该尽早地报告问题,
不至于一个大项目编译了几个小时之后才报告无法继续。Autotools提供了这种能力,因此它被广泛应用了。而且还不仅限于在Linux系统上。
Autotools所生成的最终configure文件和Makefile.in文件几乎可以涵盖目前所有的类UNIX系统,生成正确的Makefile或报告错误。
那么说到这里,我需要大家明白我并不直接介绍Autotools工具,而是直接讲述Makefile的原因了,因为它很少在实际应用中直接使用。
因为我们大多都是在编写我们公司自己使用的系统。
作为一家知名的互联网企业,不可能允许它的服务器系统运行那么多不同种类的Linux发行版本,因此我们要完成工作是根本用不到Autotools工具的。
当然,如果你想为开源世界贡献自己的力量,还是要掌握它的。
8.6 结束语
很佩服你能够坚持到最后。说实在的,学习Makefile是一个很痛苦的过程,至少我是这么过来的。曾经放弃过很多次,但是由于对计算机技术的热爱与对技术的无尽追求使我坚持了下来。但回过头来想想,Makefile的确是一个伟大的工具。它的灵活性与通用性是其他同类工具所无法比拟的,也正因为如此,它才拥有了这么顽强的生命力,帮助人类完成了数以万计的软件工程。这也从另外一个侧面推动了Linux社区的发展。想想如果没有Makefile这样强大的工具来管理那些软件工程,我们现在将会是怎样的一个世界呢?
Autotools工具合集(为了弄出一个configure文件 autoscan-》autoconf-》autoheader-》automake)
autoscan
autoconf
autoheader
automake
lnmp一键安装包
for packages in make cmake gcc gcc-c++ gcc-g77 flex bison file libtool libtool-libs autoconf kernel-devel patch wget libjpeg libjpeg-devel libpng libpng-devel libpng10 libpng10-devel gd gd-devel libxml2 libxml2-devel zlib zlib-devel glib2 glib2-devel unzip tar bzip2 bzip2-devel libevent libevent-devel ncurses ncurses-devel curl curl-devel e2fsprogs e2fsprogs-devel krb5 krb5-devel libidn libidn-devel openssl openssl-devel vim-minimal gettext gettext-devel ncurses-devel gmp-devel pspell-devel unzip libcap diffutils net-tools libc-client-devel psmisc libXpm-devel git-core c-ares-devel;
do yum -y install $packages; done Install_Autoconf()
{
Echo_Blue "[+] Installing ${Autoconf_Ver}"
Tar_Cd ${Autoconf_Ver}.tar.gz ${Autoconf_Ver}
./configure --prefix=/usr/local/autoconf-2.13
make && make install
} Check_Autoconf()
{
if [[ -s /usr/local/autoconf-2.13/bin/autoconf && -s /usr/local/autoconf-2.13/bin/autoheader ]]; then
Echo_Green "Autconf 2.13...ok"
export PHP_AUTOCONF=/usr/local/autoconf-2.13/bin/autoconf
export PHP_AUTOHEADER=/usr/local/autoconf-2.13/bin/autoheader
else
Install_Autoconf
fi
} Autoconf_Ver='autoconf-2.13' if [[ -s /usr/local/autoconf-2.13/bin/autoconf && -s /usr/local/autoconf-2.13/bin/autoheader ]]; then
Echo_Green "Autconf 2.13...ok"
else
Install_Autoconf
fi
Linux就这个范儿 第17章 窈窕淑女君子好逑
P657
在Linux系统上编写C程序,除了掌握C语言的一般规律,还建议你学会使用GNU autoconf和automake工具。automake工具
检查C文件,判断它们如何互相依赖,并生成一个Makefile以使文件能按照正确的顺序进行编译。autoconf则是编译好帮手,它允许软件安装过程自动配置
,处理大量的系统相关问题并且增加了程序的可移植性。
在Linux分发包中还有一整套有效的GNU 工具支持和补充C语言
P662
用C写界面,需要使用如下一连串工具:vim+gcc+gdb+make+automake+autoconf
C/C++ 中的 gdb 也是一个类似的命令行 debugger,只是用来调试 C/C++ 而已,使用的模式跟Python的pdb/ipdb相似,具体可参考 用GDB调试程序。
make all
http://blog.csdn.net/qing101hua/article/details/53228432
make,仅编译;
make install,编译并安装(比如安装到/usr/bin目录下,然后可以直接使用。因为/usr/bin只有管理员才能向里面添加文件,所以通常要加sudo)
这个要看你的Makefile的,约定俗成的而已
一般"潜规则"
make等同于make all,编译用的,具体编译了那些文件要看你的Makefile
make install就是把编译出来的二进制文件,库,配置文件等等放到相应目录下
make clean清除编译结果
具体的东西都在Makefile里面,只不过大部分应用程序的Makefile都是由configure脚本自动生成的,所以Makefile内容都差不多
f
Linux就这个范儿 第8章 我是Makefile的更多相关文章
-
Linux就这个范儿 第16章 谁都可以从头再来--从头开始编译一套Linux系统 nsswitch.conf配置文件
Linux就这个范儿 第16章 谁都可以从头再来--从头开始编译一套Linux系统 nsswitch.conf配置文件 朋友们,今天我对你们说,在此时此刻,我们虽然遭受种种困难和挫折,我仍然有一个梦 ...
-
Linux就这个范儿 第18章 这里也是鼓乐笙箫 Linux读写内存数据的三种方式
Linux就这个范儿 第18章 这里也是鼓乐笙箫 Linux读写内存数据的三种方式 P703 Linux读写内存数据的三种方式 1.read ,write方式会在用户空间和内核空间不断拷贝数据, ...
-
Linux就这个范儿 第19章 团结就是力量 LSB是Linux标准化基地(Linux Standards Base)的简称
Linux就这个范儿 第19章 团结就是力量 LSB是Linux标准化基地(Linux Standards Base)的简称 这个图片好可爱,它是LSB组织的图标.你肯定会问:“图标这么设计一定有说 ...
-
Linux就这个范儿 第15章 七种武器 linux 同步IO: sync、fsync与fdatasync Linux中的内存大页面huge page/large page David Cutler Linux读写内存数据的三种方式
Linux就这个范儿 第15章 七种武器 linux 同步IO: sync.fsync与fdatasync Linux中的内存大页面huge page/large page David Cut ...
-
Linux就这个范儿 第14章 身在江湖
Linux就这个范儿 第14章 身在江湖 “有人的地方就有江湖”,如今的计算机世界就像一个“江湖”.且不说冠希哥有多么无奈,把微博当QQ的局长有多么失败,就说如此平凡的你我什么时候就成了任人摆布的羔羊 ...
-
Linux就这个范儿 第13章 打通任督二脉
Linux就这个范儿 第13章 打通任督二脉 0111010110……你有没有想过,数据从看得见或看不见的线缆上飞来飞去,是怎么实现的呢?数据传输业务的未来又在哪里?在前面两章中我们学习了Linux网 ...
-
Linux就这个范儿 第12章 一个网络一个世界
Linux就这个范儿 第12章 一个网络一个世界 与Linux有缘相识还得从一项开发任务说起.十八年前,我在Nucleus OS上开发无线网桥AP,需要加入STP生成树协议(SpanningTree ...
-
Linux就这个范儿 第11章 独霸网络的蜘蛛神功
Linux就这个范儿 第11章 独霸网络的蜘蛛神功 第11章 应用层 (Application):网络服务与最终用户的一个接口.协议有:HTTP FTP TFTP SMTP SNMP DNS表示层 ...
-
Linux就这个范儿 第10章 生死与共的兄弟
Linux就这个范儿 第10章 生死与共的兄弟 就说Linux系统的开机.必须经过加载BIOS.读取MBR.Boot Loader.加载内核.启动init进程并确定运行等级.执行初始化脚本.启动内核模 ...
随机推荐
-
1001. A+B Format (20)
原题连接:https://www.patest.cn/contests/pat-a-practise/1001 题目如下: Calculate a + b and output the sum in ...
-
理解HTTP协议
在互联网时代HTTP协议的重要性无需多言,对于技术岗位的同学们来说理解掌握HTTP协议是必须的.本篇博客就从HTTP协议的演进.特性.重要知识点和工作中常见问题的总结等方面进行简单的介绍.理解掌握了这 ...
-
一个网站完整详细的SEO优化方案
根据自己的个人经验完成了这篇文章,希望对SEOer有点帮助,高手直接跳过,请勿喷水... 一个完整的SEO优化方案主要由四个小组组成: 一.前端/页编人员 二.内容编辑人员 三.推广人员 四.数据分析 ...
-
Ansible简明使用手册
Ansible使用简明手册 1.简介 ansible是新出现的自动化运维工具,基于Python开发,集合了众多运维工具(puppet.cfengine.chef.func.fabric ...
-
JavaWeb学习总结(五十一)——邮件的发送与接收原理
一. 邮件开发涉及到的一些基本概念 1.1.邮件服务器和电子邮箱 要在Internet上提供电子邮件功能,必须有专门的电子邮件服务器.例如现在Internet很多提供邮件服务的厂商:sina.sohu ...
-
Python基础7- 流程控制之循环
循环: 把一段代码重复性的执行N次,直到满足某个条件为止. 为了在合适的时候,停止重复执行,需要让程序出现满足停止循环的条件.Python中有三种循环(实质只有两种): while循环 for循环 嵌 ...
-
js的function
1.funciton的js如果直接写是不会执行的 例如 function TableInit() { //var treeNode = $('#otherTree').omTree('getSelec ...
-
JS对象的创建与使用
本文内容: 1.介绍对象的两种类型: 2.创建对象并添加成员: 3.访问对象属性: 4.利用for循环枚举对象的属性类型: 5.利用关键字delete删除对象成 ...
-
(转载)MatLab绘图
转载自:http://www.cnblogs.com/hxsyl/archive/2012/10/10/2718380.html 转载自:http://www.cnblogs.com/jeromebl ...
-
JSP中嵌入java代码的标签方式(转)
(1)声明变量或方法 : <%! 声明; %> :慎重使用,因为此方法定义的是全局变量 (2)java片段(scriptlet): <% java代码; %> (3)表达式 ...