IDE 不是程序员的唯一选择(三)

有了前面的介绍,相信好学的同学们对 make 已经有了一定的了解。

记住,编写 Makefile 也是构建整个软件的一部分,其重要性并不亚于编写 .c 或 .h 文件。当你用 IDE 的时候,是由 IDE 来生成相当于 Makefile 的文件。但是这个生成的过程并不是完全自动的,它是由你的鼠标点击、拖拽(把 .c 文件加入项目)、和填写一些表单、以及勾选编译选项完成的。

如果你的整个项目中其它源文件都是从键盘输入来编写的,那么 Makefile 也由手工编写就理所当然了。

如果你在用 C++ ,在用 C++ 中的 STL ,那么是不是应该搞清楚 STL 到底做了什么,怎么做到的,这些问题呢?这是用 C/C++ 语言编程的程序员的一个基本态度。即使不去研究透彻,至少也应该了解一些大致的原理吧。那么对于 Make 来说也是这样。我们应弄清楚 Make 到底如何在工作,我们在 Makefile 里写的每一行代码是什么含义。为什么可以帮我们完成那些工作。这个连载选择 Make 来展开,也正是因为 Make 的工作原理非常简单,方便我们学习。

越是简单的东西,越可以在其上做出各种奇妙的东西。Make 也是这样。但一开始就给出别人做好的完善的库,简单的用一下,会让我们迷失其真谛。相信我,最终,编写 Makefile 可以非常简单,善用工具、不必写任何多余的东西,甚至比在 IDE 里拖入几个源文件更简洁。但一开始,还是从繁琐开始,这些繁琐都是帮助你去理解,等你理解了自然能找到方法简化这些繁琐的工作。

云风绝非使用 Make 的高手,从某种意义上来说,也是一个入门者。写这个系列的时候,也需要去查阅文档确认是否写错。平时工作的时候,Makefile 文件通常也需要多次调试,才能正确的完成工作。也正是如此,才能体会到:怎样去理解和学习,容易跨过最初的门槛。


上一篇开篇,我们讲到了,如何用命令行分开编译 .c 文件,并把它们链接起来。这样做,可以使每次修改都可以让机器做最少的编译任务。对于小工程,这样做的意义不大。但是大工程,可能就能帮我们节约不少时间了。记住这一点,永远没有通用的最优方案。因为实施方案本身也是有成本的。我们只需要找到最直接最简单的方法就可以了。比如项目一开始,可以写一个最简单的 Makefile 文件,随着项目规模的扩大再逐步完善。

分开编译再链接这件事,人做起来都比较繁琐,把它教给机器去做,当然也会更繁琐。所以我没有在上一篇中详解。好学的同学应该会自己弄了,今天,我也来写写自己的方案。但在此之前,我们先梳理一下对 Make 的理解。

Make 是一个工作于非常简单的模式下的工具。它的内部有一张表,记录了目标文件之间的依赖关系。Makefile 就是用来描述这张依赖关系表的。对于依赖关系表的描述,用了一种非常简单的语法:

目标 : 依赖

这表示,"目标" 的构建依赖于 "依赖" 先构建出来。这里,"目标" 和 "依赖" 都是文件系统中的文件,而"依赖"本身也可以是一个"目标"。如果 "依赖" 的文件时间新于 "目标" 的文件时间,表示 "目标" 需要重新构建。如果 "目标" 文件不存在,也会触发这种构建过程。

一个目标可以有多个依赖,可以在 : 后以空格分开写上多个,比如:

目标 : 依赖1 依赖2 依赖3

这在我们前一篇中已经多次见过了。其实还有另一个规则,我们可以写:

目标 : 依赖1

目标 : 依赖2

这样分两行写,即,每次写 "目标 : 依赖" 都在依赖关系的依赖关系表中添加了一项。(关于同名的依赖添加的问题,以后我们在讨论)

举个例子:

a : b c

a : b
a : c

其实是等价的。

每个目标的构建方法并不是由 Make 内置功能完成的,Make 只是简单调用写在 "目标" 定义的下一行的若干命令行脚本而已。而每一行命令行脚本必须以 Tab 键开头。注意,如果你把目标的依赖关系分成若干行实现,只可以有一个地方定义构建脚本。

举例:

all : a
all : b

a :
    echo $@

b :
    echo $@

这样一个 Makefile 用 Make 运行后,会显示:

echo a
a
echo b
b

为什么呢?因为 Make 会找 Makefile 中定义的第一个目标,做为它这次的终极目标。在这里是 all 。all 依赖于 a 和 b 两个目标。由于你的工作目录下没有 a 和 b 两个文件,所以触发了 a 以及 b 的构建。而 a 的构建指令是 echo $@ ,$@ 指代了当前目标 "a" ,结果就执行了 echo a 。同样,b 文件的不存在,导致了 b 的构建,执行了 echo b 。

需要强调的是,echo 并不是 Make 的内置功能。echo 是 Windows 命令行指令(在 *nix 系统上,称为 shell 指令)。Make 只管忠实的执行那些相关的以 Tab 开始行内描述的命令行指令。目标的构建成功也不以目标文件是否正确生成为依据。而是以命令执行的结果是否为 0 。记得学 C 语言的时候,老师教你,main 函数得到正确结果时,应该 return 0 吧。这个 main 的 return 0 就是返回给系统用的。Make 通过检查这个返回值来觉得命令行指令是否被正确的执行。如果收到非 0 的返回值,整个 Make 的过程会被中断。当然,echo 这样的指令,一般都会返回 0 的。

注意,一个目标是否被正确构建,只取决于构建它的命令行指令是否正确的返回 0 ,而不取决于文件是否被创建出来或是被更新到最新的时间。在一次构建中,每个目标最多只会被构建一次。这是由于 Make 只对依赖关系表做一次简单的拓扑排序,然后就开始工作了。

同样,我们还可以让多个目标依赖同样的东西:

a b : c d

就等价于

a : c d
b : c d

现在我们可以看到,在 Makefile 里写 : 定义,其实就是在填写一张依赖关系表。每次的一个 : 都向表里追加一些项目。Make 工作的时候,先读完整个文件,把完整的依赖关系表建立好,再根据命令行指定的目标开始工作,如果在命令行不指定目标,默认就是 Makefile 里写的第一个目标了。

这样我们就好理解,除了 Makefile 里的第一个目标定义之外,所有的依赖关系描述都是无所谓书写的先后次序的。Make 只管向依赖关系表里添加表项,不会删除。原则上也不保证按依赖表中的次序先后来构建,如果你需要一个目标一定先于另一个目标构建,就需要显式的描述出依赖关系。


让我们回到早先的例子:从 foo.c 和 bar.c 构建出 foobar.exe 。并且要求 foo.c 和 bar.c 里若只修改了一个文件,就只编译新修改的那一个。

foobar.exe : foo.obj bar.obj
    link /out:$@ $^

foo.obj : foo.c
    cl /c $<

bar.obj : bar.c
    cl /c $<

先别急着编译,我们来看看如何调试 Makefile 文件。使用 gmake -n 看看,是不是显示了:

cl /c foo.c
cl /c bar.c
link /out:foobar.exe foo.obj bar.obj

如果显示的是,

gmake: `foobar.exe' is up to date.

那么就先 del foo.obj bar.obj foobar.exe 然后再看。

-n 参数可以让我们观察到底会执行些什么命令行指令,而不真的去执行它们。这对调试复杂的 Makefile 非常有用。

如果没有问题,就可以 gmake 构建出 foobar.exe 了。

如果你烦透了每次做一次实验都要手工清理一下 obj 和 exe 文件,那么可以把 del 指令也写在 Makefile 中。在上面的 Makefile 最后加上两行:

clean :
    -del foo.obj bar.obj foobar.exe

当然,你也可以偷懒写成 del *.obj *.exe

同学们会注意到,del 前有个减号。这个 - 是告诉 Make ,忽略掉后面这行指令的返回值,不要因为返回值非 0 而中断。什么时候 del 会返回非 0 值呢?自然是要删除的文件不存在啦。

现在你就可以通过 gmake clean 来清除目标文件了。或者用 gmake clean foobar.exe 来先清理再编译,这相当于我们在 IDE 中使用的 rebuild 指令。Make 的命令行可以跟多个目标,它会顺着构建这些目标。对于举一反三的同学,应该已经给 Makefile 加了一个 rebuild 目标了:

rebuild: clean foobar.exe

暂时先这么写着,以后我们会介绍更好的,更偷懒的写法。


最后给出一个云风当年初学 Make 时,犯过的一个错误。属于对 Make 理解不当造成的。

假设 foo.c 里 include 了一个 foo.h 文件,依赖关系应该怎么写?

在 Makefile 的最后,加上一行

foo.c : foo.h

这样行吗?

你可以试试,修改一下 foo.h ,然后 gmake 一下,看看有没有重新编译 foo.o 。为什么不能工作呢?留给同学们思考了。

TrackBack

如果你想引用这篇文章,请复制下面的链接发送引用通告(GBK)
http://blog.codingnow.com/mt/mt-tb.cgi/410

Comments

12楼的有一句说得不错,感觉是在考古,但是好像我们一直也没有出现历史的断痕,也还没有抛弃经典呢.
我估摸着,下一篇可能要介绍flex、yacc了吧.

@david

你应该确认一下 link 是不是和 CL 配套的那个 link 。

或者,CL 本身也是可以做链接器用的。

@天堂的隔壁,

如果实现需要弄清楚 make 的工作流程,可以用 -d 来看了 :)

这引入一个新问题,如果我make -n的话,可以正常工作。即make -n的时候虽然没有真正执行命令,但它*假设*该命令该改变目标的时间,调试和运行的情况不一致,不知道算不算make的bug呢,呵呵

all-->foobar.out-->foo.c-->foo.h
所以要生成all,就必须先运行“@echo make foo.c”,*但是*,运行它并不意味着也要运行其上一层依赖的命令,即“gcc foo.c bar.c -o foobar.out”,是否要运行其上一层命令还是取决于foobar.out和foo.c的时间,但“@echo make foo.c”并不会改变foo.c的生成时间,也可以说foo.c并不是依赖foo.h生成,而依赖-生成才是make的基本原则

刚刚又仔细分析了下,看来云风犯的错误,果然不一般-_-!

这里的理解并不在makefile系统的基本原则:依赖关系,而在于每一层依赖之间的关系。

不好意思,有超过公司网络一次能提交的上限了,分贴之

学习过程中遇到障碍,解决不了,请云风指点。我的VC可以工作,已安装Mingw。cl foo.c bar.c是正常的(从执行信息看到,生成两个.obj后,执行/out:foo.exe foo.obj bar.obj)。问题是,使用/c分别编译后再使用link foo.obj bar.obj出错,"link:cannot create link 'bar.obj' to 'foo.obj':File exists".
而link /out:foobar.exe foo.obj bar.obj的出错信息为"link: extra operand 'bar.obj'",看来它只认前两个参数了。是不是很奇怪?

makefile写作:
all : foobar.out
@echo make all

foobar.out : foo.c bar.c
gcc foo.c bar.c -o foobar.out

foo.c : foo.h
@echo make foo.c

实在有些不幸,,我的第一感觉也是应该不能这样写(因为没见过),但是:
[root@localhost make]# ls -l
total 16
-rwxrw-rw- 1 900428 900428 0 Sep 25 11:24 bar.c
-rwxr-xr-x 1 root root 4630 Sep 25 11:25 foobar.out
-rwxrw-rw- 1 900428 900428 36 Sep 25 11:24 foo.c
-rwxrw-rw- 1 900428 900428 0 Sep 25 11:25 foo.h
-rwxrw-rw- 1 900428 900428 134 Sep 25 11:24 makefile
[root@localhost make]# make
make foo.c
make all

@sjinny:
奇怪……foo.h更新了,时间戳会比foo.c更新,这时foo.c会过时,应该会导致依赖于foo.c的foo.obj也过时……
---------------
最后一行写上
foo.c: foo.h等于没用啊,foo.c是目标名字,不是foo.c文件,没有任何实际意义的,所以foo.h 没有东西进行比较,自然就不执行了。
要想执行,把": foo.h"加到原来的 foo.obj 或者 foo.exe目标都行,因为那两个目标都有生成的文件可以比较。不知道这样理解对不对?

对于最后一题:
foo.obj : foo.c
应该改为:
foo.obj : foo.c foo.h

后面的还没细读,8点多读到这儿的时候我就卡这儿了~~~但是以现在改的情况来看,是根据”依赖“文件来生成”目标“文件了?我这么理解应该没问题吧?但是好像和”"目标" 的构建依赖于 "依赖" 先构建出来。“矛盾了,我这么理解:”"目标" 的构建所依赖的 "依赖" 先构建出来。“,怎么样?

写的时候有 bug ,下午写的时候太着急,写着写着就写错了 -_- 。刚才我改了一下,还加了一段文字。

哦,现在看起来好像和之前的意思反了,现在应该是根据”依赖“来生成”目标“吧?之前在时间那里你好像用的是”老于“这个词,我忘了”依赖“和”目标“你有没有换先后顺序,应该是这里让我误解了~,我要再读读,琢磨琢磨~~

试了一下,原来 BSD Make 只兼容了 $@, 没有兼容 $^ 这些。

不过 ${.ALLSRC}, ${.TARGET} 似乎更讨人喜欢。

嗯,“依赖目标”这个词是有歧义。我想着那个东西也是个目标,又是前一个所依赖的,就直接连起来造词了。

写成“依赖的目标”又太累赘了,所以现在改为“依赖”。不过依赖这个词似乎做名词不妥当。凑合着看吧。

正说着,macro来了

@Cloud
这种考古学问题最适合云风乐:)
随便Google下得http://www.wgdd.de/?p=28
ps. 看来还没有人注意到目标 : 依赖目标嘛

"目标 : 依赖目标

这表示,"目标" 的构建依赖于 "依赖目标" 先构建出来.",读到这里我大概分析了一下,有这么个意思:目标早于“依赖目标“,但是我不知道 “构建依赖于“依赖目标””是什么意思。请指点一下!

那个循环替换我觉得是 gmake 扩展,还没查文档确认。

话说我以前不知道,都是写 $(foreach ... 的。下面讲怎么自动做 .h 的依赖关系,这玩意少不了。

$@ 这些是最老的 make 里就有的,绝对通用。所以就先拿出来说了。

.PHONY 在 bsd make 里也有的。似乎很多版本的 make 都支持了。gmake 的文档上说是它先扩展的。

按照云风留下的尾题,目前Makefile文件会是这个样子:
all : foobar.exe
echo Hello World

foobar.exe : foo.c bar.c
cl /Fefoobar foo.c bar.c

foo.c : foo.h

-----------
现在,我们修改了foo.h,但执行gmake时,它只管判断all与其依赖文件foobar.exe的修改时间,发现all不存在,在执行all下的echo命令前,它会递归把foorbar.exe作为目标文件,检查foorbar.exe与foo.c bar.c的修改时间,结果gmake发现foorbar.exe比其两个依赖文件都新,所以递归返回.只执行了all下的命令:
echo Hello World

@Cloud
那个东西是扩展的么?既然有$这些正则来的东西,$()也应该有的吧
肯定是GNUMake扩展的是.PHONY之流

我猜是,因为make没有被告知怎么修改foo.c,当foo.c依赖的foo.h改动之后。

@Zhe

慢慢来,吓跑了同学们可不好。

话说, $(SRC:.c=.o) 是 Gmake 扩展吧。估计要到下下次才会出现了。


话说上次不是讲了半天正则的$么,怎么不搞搞$*.c $(SRC:.c=.o)……

btw 目标 : 依赖目标

没写要运行的命令吧

奇怪……foo.h更新了,时间戳会比foo.c更新,这时foo.c会过时,应该会导致依赖于foo.c的foo.obj也过时……

因为foo.c已经存在,make根本不会去理会foo.c:foo.h

::...
免责声明:
当前网页内容, 由 大妈 ZoomQuiet 使用工具: ScrapBook :: Firefox Extension 人工从互联网中收集并分享;
内容版权归原作者所有;
本人对内容的有效性/合法性不承担任何强制性责任.
若有不妥, 欢迎评注提醒:

或是邮件反馈可也:
askdama[AT]googlegroups.com


订阅 substack 体验古早写作:


点击注册~> 获得 100$ 体验券: DigitalOcean Referral Badge

关注公众号, 持续获得相关各种嗯哼:
zoomquiet


自怼圈/年度番新

DU22.4
关于 ~ DebugUself with DAMA ;-)
粤ICP备18025058号-1
公安备案号: 44049002000656 ...::