中山野鬼 linux 下 C 编程和make的方法 (四、开始make)

终于开始make了
    make 是个命令,先谈一下,为什么要make?
    首先的理由就是,你可以将前面很多gcc的命令行,汇总起来,并且一次确认,多个命令自动运行。我相信很多人说我在忽悠。这就是个“批处理”,就是加上复杂的变量替换,条件执行,也就是个脚本。没错,make首先就是个脚本分析执行的工作。
    但make有优势的地方在于依赖的检查。什么是依赖,初学者看GNU make之类的文档,别说英文版,就是中文“依赖”也够理解半天。这里重复说个生成.o文件的意义。意义就是不要对没有改动的过的C文件,进行再次编译。 这里隐含了“依赖”的概念。.o文件之所以不需要再编译,是依赖一个前提,C文件没有被改动过。但是C文件没有被改动过,不代表其#include的.h 文件就没有改动过啊。由此所谓“依赖”对于make而言,是一个操作存在的前提条件。当操作的前提条件没有刷新时,自己也就没有必要刷新。
    但是linux操作系统,只会孤立的判断每个文件是否被改动过了。你C文件里#include了一个h文件,OS没时间帮你慢慢分析,而make也不会帮你自动阅读理解C文件。这些都是gcc的事情。得,大家把责任都推给GCC。
    GCC也很无辜,“关我毛事啊!你告诉我要编译,我就编译,你不告诉我要编译,我为什么要编译。我的预编译系统只会识别#include 然后在对应的目录里找到文件,并打开,添加。这个文件有没有被修改过,第一我不知道,第二我不需要知道。如果你把执行文件删除了,我就是知道所有源文件没 有被改动过,你让我编译,难道我告诉你‘哈哈,经过我的这么这么分析,发现文件没有被修改过,所以我不会生成执行文件,如果执行文件被别人删除,请找相关 部门,寻求找回,我很智能的’”。
    现在我们讨论下来,文件的依赖性,看来没有任何工具可以主动跳出来承担责任,此时,make跑出来,很负责的说了一句“其实我能做,但需要你,设计者,愿 意描述依赖关系,那么我make可以根据你的依赖关系进行检查”。这就是make区别其他shell下的普通脚本工具的价值。

    因此,对于make几个基本概念,现在要有:
    1、我们用make主要是针对文件进行处理的。因为依赖关系的作用域(有效范围)是工程,工程内部有价值的依赖通常是多个文件之间的因果关系。
    2、make除了依次执行调用其他工具或命令,做所谓批处理的事情外,依赖关系的检查是他的一个重要特性。不然他跳出来主动担责任就是一种“欺骗”,准确说是“行政不作为”的表现。
    你把计划和依赖,描述在一个文件里,传递给make"。通常有两类做法。
1 $make -f filename
    filename是一个文件名。但不是潜规则要求的文件名。这样做通常用于临时性的make使用,正式情况,不需要如此折腾。潜规则有个好处,你被潜规则了,其他人也会被潜规则。这样降低你和其他人的交流成本。
    由此,另一个做法是将你的计划,写在名为 GNUmakefile ,makefile ,Makefile这几种文件名中任意一个。恩。make的潜规则文件名也有几种写法。但潜规则中的潜规则是,我们都用Makefile。如果老师考你 make的潜规则,都可以识别哪几个文件名,他们的依次顺序时,你可以毫不犹豫的向他竖中指。他如果问哪学的,你可以说是野鬼教的,因为他没有说是那个版 本的make。不过竖归竖,为了防止意外,你还是要知道,GNU make是按照GNUmakefile ,makefile ,Makefile依次进行查找的,同时尽可能的确保你的目录下只有Makefile一个文件。
    需要特别说明的是,GNUmakefile人如其名,只有GNU make可以识别,make也有不同的版本和来源和C的编译器一样,只不过linux上用GNU make的还是比较多。但仍然建议使用 Makefile这个文件名。
    有了潜规则,你就不需要如此写
1 $make Makefile

  因为当你在当前目录下存在Makefile时,make会自动查找,这就是在“默默的享受被潜规则”

    由于你对工程希望做很多事情,典型的几个事情

    build :

    只编译那些修改过的,或新的文件

    rebuild all :

    不管有没有修改过,我就是要全部重来,有过工程经验的程序员,我相信大多数都吃过一个苦,因为疏忽,导致build并没有实实在在的把最新的代码给编译到 最终文件中去,于是总问“为什么还是不行!!”,所以没有养成正确良好的make习惯的新手,rebuild还是很必要的,特别是一个工程调用了另一个工 程生成的库,而当前工程并不会检查另一个工程生成的库的变动性的时候。

    其实我个人的理解,之所以存在rebuild all是因为你的Makefile没有写好,理论上rebuild all是没有存在的意义的,这引申出不建议使用IDE的话题。我经常被VC6.0搞晕。特别是有些外部库VC6.0无法自动检测依赖性关系,使得我必须 rebuild all

    clean all:
    至少有两个作用。第一,rebuild all可以拆解成两个动作。把所有obj 和最终文件都删除,再build。第二,你想转移你的有效代码,比如打包COPY到另一个机器时,一个大目录下整体打包压缩,更有效。但是obj和执行文 件还有很多其他中间文件,都很大,而且对于代码文本的转移没有必要。因此clean all .此时,你的目录下,只有干净的源码和一些说明文件,当然也包括Makefile,此时岂不是清爽的很?
    为了有效区分不同的操作,make应可以如下处理。
    $make clean //那么我们就调用clean的操作
    $make build //正常的带依赖性检测的build
    $make rebuild //我们就调用rebuild的操作。
    由此,我们引出了几个关于makefile的设计目标。
    1、我们要能在makefile里面描述出依赖关系。

    2、我们要能在makefile里面区分不同的操作。当然一个操作,是由多个执行步骤组成的。
    3、可以make ,make build ,make rebuild,make clean

   
    由此我们现在写第一个Makefile。其实很简单,就是将前面的gcc命令汇总起来,放在Makefile里。包括rm的命令。

如下: 

1 haha: #需要顶头写,表示一个操作的开始,用此来直接区分不同操作的描述范围#本身是个注释符号
2     gcc -Iinc -c src/learn_make.c -o obj/learn_make.o
3 #需要TAB一下,不然会当成其他的含义,比如一个操作本身
4     gcc obj/learn_make.o -o bin/learn_make
5 heihei:
6     rm obj/*

这里要做几个解释

    操作:只是我的口头语,官方的说法叫“规则的目标”,我只是希望新手理解实际就是一个操作,而且操作里面可以有很多命令 依次组成依次执行,而且make一次只会对一个操作进行操作,除非其他操作和这个操作有依赖关系。但操作通常都是有操作的结果(输出的文件),所以叫规则 的目标
    顶头写:除了描述“规则的目标”外,还有很多其他顶头写的事情,因此,不单单要顶头写,你还需要加上个‘:’,这样,make就可以知道,顶头写,同时存在一个‘:’则是一个操作的开始,也就是“规则的目标”。

    haha:我是在想不出什么名词,能说明“规则的目标”有什么特殊的命名方式。用haha ,heihei 是希望大家理解,规则的目标的名字,并没有什么特殊约束,你爱怎么写怎么写,但存在一些潜规则和make的规矩会让你吃苦头,只是我在边上 “haha,heihei,写这个例子时,我确实在haha,heihei"。比如通常,heihei应该用 clean来实现。同时,你执行如下命令

1 $make
2 $make haha
3 $make heihei



 你会发现,make没有后面的参数时,执行了haha。不是因为haha的单词更少。而是因为haha是第一个规则。因此,通常你需要将最常用的,当然 未 必是build操作,放在第一个。这样可以简化你的操作。直接make就可以。但这个最常用的,与你和你的小组第一直觉希望make做什么有关系。通常程 序员之间会说“这样这样后,make一下”而不会解释make what。
    你问“make what?"
    同事说:“对就是make what!"
    "make what what 啊?”
    “就是make what 啊,你what 什么 what 啊?”
    这说明两个问题,第一,make的第一个规则,尽可能是你们小组的共识常用工作,第二,命名很重要。你和你的同伙"what"来“what"去,最多相互 怀疑智商问题。但如果你起名叫“love",然后和你的女同事说,记得tar -xvf后,make love一下。当心她告你性骚扰。

    现在回到haha上。继续make haha

1 $make haha
2 $make haha


 执行两边,没错不是笔误。你会发现,make有啥用?和普通的批处理一样,一样什么都又运行了一遍。这不是make的错。只是我挖了个坑,你不摔 一下,可 能不理解依赖怎么实现的。make要求,对Makefile里的依赖,在规则 ":"后面说明依赖关系。如下Makefile的清单。  

1 haha: src/learn_make.c  inc/learn_make.h
2     gcc -Iinc -c src/learn_make.c -o obj/learn_make.o
3     gcc obj/learn_make.o -o bin/learn_make
4 heihei:
5     rm obj/*

现在继续执行

1 $make haha

哈哈,我又哈哈了。,你会发现,虽然存在依赖关系,而且我的src/learn_make.c  inc/learn_make.h没有改动,怎么还是所有的都在执行啊。这不是我们的目标。此时我需要再告诉你,make 对依赖的检查是会判断目标是否存在的,即便依赖的文件没有更新,但目标(操作的输出结果,通常就是文件)不存在,仍然会继续执行。试想,你在 learn_make/下,实际的文件只有Makefile,没有haha这个文件,make自然会努力的继续工作。

    因此,如果你希望make的一个操作,是针对文件的最终形成,而且希望make能自动帮你检测文件是否有必要改动,则你需要明确规则的目标就是文件的名 称。否则,他如何判断目标呢?修改成这样,我们就比较爽了吗?(需要注明,这是弱智版的说法,希望对新手能从原理上搞清楚make的工作机制)

1 learn_make: src/learn_make.c  inc/learn_make.h
2     gcc -Iinc -c src/learn_make.c -o obj/learn_make.o
3     gcc obj/learn_make.o -o bin/learn_make
4 heihei:
5     rm obj/*


    你可以继续执行发现仍然没有改变。因为你忘了你的Makefile存哪了。你希望判断的文件是否更新又存在哪里。所以你得改成这样 

1 bin/learn_make: src/learn_make.c  inc/learn_make.h
2     gcc -Iinc -c src/learn_make.c -o obj/learn_make.o
3     gcc obj/learn_make.o -o bin/learn_make
4 heihei:
5    rm obj/*

你可以再试试。我相信,绝大多数熟练使用make的朋友会说我“弱智”。实际的工程级的Makefile也很少这么书写。但我希望这样的“弱智”Makefile能让新手明确 规则的目标,通常是对文件的操作。 同时,根据上面一个例子,haha: 开头的,你可以知道,make的操作,首先判断依赖的内容src/learn_make.c inc/learn_make.h是否被更新,虽然依赖即便没有被更新,但当找不到目标时,仍然会努力尝试创建目标。这个目标就是haha。可惜haha 并不是你最终的目标,learn_make也不是,因为存在路径问题,所以你必须修改为bin/learn_make

    貌似现在,虽然写法弱智,但是已经可以开始爽make了。你可以很HIGH的告诉不会make对人,我可以针对src/learn_make.c  inc/learn_make.h是否修改,让make动态的决定,是否进行后续操作。但这里有个小BUG。你尝试如下命令 

1 $rm obj/* //将learn_make.o删除
2 $make

  此时,仍然提示,目标没有被更新,而拒绝编译。有人会说,“怕什么,c文件如果改动,能更新就可以了。中间文件,可有可无。”但是这种依赖关系不明确描述时,会产生一些莫名其妙的错误。

    例如,你存在两个C文件,当只有一个文件出现修改时,则另一个文件不会进行编译,这是你希望的目标。否则重新编译浪费时间啊。因为你还年轻,没有经历过编 译一个需要小时这个级别的系统。但当你将obj全部删除时,那些没有更新的C文件无法自动生成obj。此时连接成可执行程序时,就会出错。有人会说,我有 足够的大脑知道只有两个文件,并且不会删除所有.o文件。但是当你的工程足够大时,没有严格规范的Makefile的依赖关系的描述,任何一个依赖关系的 遗漏,都会导致上述rebuild(需要rebuild啊,build不能自动全部更新)的问题发生。

    easy,一个目前所学到的知识告诉你,在:后面依次加上需要依赖的文件或目标就可以了。你可能会如下修改。

1 bin/learn_make: src/learn_make.c  inc/learn_make.h obj/learn_make.o
2     gcc -Iinc -c src/learn_make.c -o obj/learn_make.o
3     gcc obj/learn_make.o -o bin/learn_make
4 heihei:
5     rm obj/*


      你run把,我可以用已经告诉你的知识说明你的错误。
    :后面,确实是依赖,问题是,你的obj/learn_make.o是由src/learn_make.c生成的。你直接说明bin /learn_make需要依赖obj/learn_make.o是否更新,这是没错的。但是如果obj/learn_make.o不存在时,你并没有告 诉make怎么做。你可能会说,gcc -Iinc -c src/learn_make.c -o obj/learn_make.o 这不是摆明了写出来了吗?但你需要注意,这里的位置,是基于make认为bin/learn_make需要更新后,才出现的。而在确认是否需要更新对依赖 文件进行检查时,make就已经对obj/learn_make.o在哪的问题开始冒汗了。

    正确的做法如下,虽然我的举例都很“弱智”但这里重点强调的是Makefile中依赖关系。

 

1 bin/learn_make:  obj/learn_make.o
2     gcc obj/learn_make.o -o bin/learn_make
3 obj/learn_make.o: src/learn_make.c  inc/learn_make.h
4     gcc -Iinc -c src/learn_make.c -o obj/learn_make.o
5 heihei:
6     rm obj/*

此时,你再执行,或者执行后,删除obj/*,再执行,此时会发

1 $make

现,现在的逻辑严谨了。而且你会发现一个规律。通常Makefile的内容,得反过来读。 因为首先要处理的事情,通常在后面,而不是在前面,而书写Makefile的逻辑通常是,先说果,再描述因。另外说一句,此时你实际上存在了两个目标可操 作   

1 $make bin/learn_make
2 $make obj/learn_make.o

尝试调换Makefile里两个规则的顺序,再分别执行

 

1 $make

  你会对Makefile的规则有加深的认识。我就不再废话了。

不过作为build命令我们仍然存在问题。
    难道我们make的目标,都要添加相对路径吗,  我们如何通过build这个目标实现 bin/learn_make这个输出呢?

   同时我们不希望 make build会导致因为没有生成build文件,而反复做同样的动作,如上面的错误?

   在设计 build这个操作时,我先谈下clean,来解决第一个问题。为说明问题,我们继续挖坑。在learn_work/下创建一个名为clean的文件。

1 $:>clean //创建一个空文件,叫clean
2 $ls

我们修改Makefile的代码如下: 

1 bin/learn_make:   obj/learn_make.o
2    gcc obj/learn_make.o -o bin/learn_make
3 obj/learn_make.o:    src/learn_make.c  inc/learn_make.h
4    gcc -Iinc -c src/learn_make.c -o obj/learn_make.o
5 clean:
6     rm obj/*

执行

1 $make clean



没错,make很HIGH的告诉你,clean是最新的。因为Makefile里的操作都是默认"假设操作的结果会产生个文件。"当当前目录下,文件存在 时,就会有这种“文件没有更改,而拒绝执行的情况”。现在我们在删除clean之前不妨考虑一下,难道每次你make clean前都需要ls一下,当前目录是否有clean文件吗?make提供了“伪目标”帮助你省掉每次都有ls一下这么繁琐的事情。如下修改 Makefile 
1 bin/learn_make:   obj/learn_make.o
2     gcc obj/learn_make.o -o bin/learn_make
3 obj/learn_make.o:    src/learn_make.c  inc/learn_make.h
4     gcc -Iinc -c src/learn_make.c -o obj/learn_make.o
5 clean:
6 .PHONY:clean
7     rm obj/*

执行 

1 $make clean

呵呵,是不是发现错误了。这个错误是因为.PHONY顶头写了,先不谈他什么意义,.PHONY需要顶头写,不顶头写,会被认为是一个操作的组成部 分。但 顶头写,又直接将clean的操作给截断了。因为我们说过。make认为一个规则目标的执行,也就是个操作的所有执行部分,是需要【TAB】做前缀,当发 现一个顶头写的行时,则认为命令的描述截止。此时make自然认为 rm obj/* 不是clean的命令组成部分。

    正确做法,你可以在clean申明之前,或整体操作命令之后描述,例如

1 bin/learn_make:   obj/learn_make.o
2     gcc obj/learn_make.o -o bin/learn_make
3 obj/learn_make.o:    src/learn_make.c  inc/learn_make.h
4    gcc -Iinc -c src/learn_make.c -o obj/learn_make.o
5 .PHONY:clean
6 clean:
7     rm obj/*

此时运行,则会出现两种提示。正确运行或错误提示:“obj/下没有文件可删除”。现在可以简单的说一下.PHONY的作用。就是强制说明,后面的 clean是不存在输出文件的。所以clean此时变成了“伪目标”也就是“伪操作”,“伪”只是说明他不会生成一个文件,因此,不会去考虑当前路径下, 是否要判断同名文件存在与否或是是否存在依赖,而始终是埋头苦干。此时你可以删除clean这个文件了。当你确认.PHONY作用之后。

1 $rm clean

可能有小朋友就琢磨了,如果是个伪目标,可以回避检查文件的方式,那么我把bin/learn_make改成build,这样make build实际就可以处理bin/learn_make了。

    如下

1 .PHONY:build
2 build:   obj/learn_make.o
3     gcc obj/learn_make.o -o bin/learn_make
4 obj/learn_make.o:    src/learn_make.c  inc/learn_make.h
5     gcc -Iinc -c src/learn_make.c -o obj/learn_make.o
6 .PHONY:clean
7 clean:
8     rm obj/*
1 $make build
    一切正常,如果之前你make clean了。再执行
    $make build

    一切不正常,因为,又跑了一次。(我非常建议新学者,将所有错误的方式都操作一边)。因为这样做,虽然回避了build需要检测当前目录下build的问 题,但仍然解决不了bin/learn_make是否存在,是否需要依赖性检测的问题。所以正确做法应该如下

1 .PHONY:build
2 build:  bin/learn_make
3 bin/learn_make: obj/learn_make.o
4     gcc obj/learn_make.o -o bin/learn_make
5 obj/learn_make.o:    src/learn_make.c  inc/learn_make.h
6    gcc -Iinc -c src/learn_make.c -o obj/learn_make.o
7 .PHONY:clean
8 clean:
9     rm obj/*



    你执行两次make build 

1 $make build
2 $make build

没问题了吧。这是因为,我们先用了.PHONY,让build成为一个不需要检测文件伪目标。但同时,我们将 build的实际输出文件作为一个依赖,让build需要每次都进行检查是否需要更新。同时不要被前面的伪目标clean误导,认为伪目标一定埋头苦干, 那是基于clean没有依赖,而build存在依赖,当依赖发现不需要更新,build也就不会埋头苦干了。

    增补完整的包括rebuild的内容如下 

01 .PHONY:build
02 build:  bin/learn_make
03 bin/learn_make: obj/learn_make.o
04     gcc obj/learn_make.o -o bin/learn_make
05 obj/learn_make.o:    src/learn_make.c  inc/learn_make.h
06     gcc -Iinc -c src/learn_make.c -o obj/learn_make.o
07 rebuild: build clean
08 .PHONY:clean
09 clean:
10     rm obj/*

大家执行一下

1 $make rebuild
    哈,会出现两种可能的错。一种是只有rm obj/*被执行,另一种,先gcc了东西,随后rm obj/*。不是我故意喜欢挖坑让你们跳,因为我的职业经历发现,寻找边界错误临界点更容易理解系统运行原理,这些错误都是我在琢磨 make的工作原理中自己挖自己跳的坑。上面这个错误,可以让你明确两个道理。
    1,make对依赖的检测,是从左到右,有顺序的。你与其记住这句话,不如记住上面这个失败的例子。
    2、make呼呼的向下run不代表正确。你的Makefile的逻辑关联很重要。
    再次,分析一下两种错的原因。
    只执行了rm obj/*,这是因为bin/learn_make经过检测,没有需要更新的。所以make直接无视,飘了过去。
    当由于src/ inc/ 下面的源码被改动了。或者obj/learn_make.o 或则bin/learn_make的 o被删除了。make认为需要重新更新bin/learn_make,因此执行了gcc部分。
    当然无论哪种错,最终都会执行clean ,你的 obj/下 o文件没有了。

    因此,正确的做法如下:

01 .PHONY:build
02 build:  bin/learn_make
03 bin/learn_make: obj/learn_make.o
04     gcc obj/learn_make.o -o bin/learn_make
05 obj/learn_make.o:    src/learn_make.c  inc/learn_make.h
06    gcc -Iinc -c src/learn_make.c -o obj/learn_make.o
07 rebuild: clean build
08 .PHONY:clean
09 clean:
10     rm obj/*

     此处希望大家理解一个概念。看上去rebuild是在调用了clean 和build的函数。没错你这个理解是可以。但你需要带着更新检测的思维,这是make强大之处。真是因为存在更新检测,你就要注意观测clean ,和build是否为“伪目标”,这对理解别人的Makefile和自己书写Makefile都是有好处的。是否存在伪目标的定义,对于这个依赖关系的推 导会有不同的结果。

    回到最近的这个错误,clean build的顺序写反的错误。甚至可能有些新手发现并没有什么结果性的错误。因为我只要make rebuild两边,始终能得到最新的bin/learn_make。因为当目前的所有依赖文件都没有更新时,第一边不会GCC,但会rm,由此导致第二 次因为obj/*.o都不存在,所有再次全部重编。
    这里隐含了一个事实,你clean存在一个漏洞,只删除了o。没有删除bin下的learn_make。导致莫名其妙的新手会认为,make 弱智,需要两边make rebuild才能真正实现最新版本的执行文件。或者说,这个Makefile(如果它很大,你没有信心去理解它)有问题,你需要执行两边才能确保严格正 确(其实更本就是不正确),甚至有人会去开骂make或GCC甚至是linux的版本问题,当然也可能会考虑键盘是否老化了等各种奇怪的责任者。其实是 Makefile自身的错误+错误,导致两次也得到了最终结果。这样的错误类型,在很多粗制滥造的应用软件里很常见,而用户只能自发的想对策。因此你需要 修改Makefile 如下 
01 .PHONY:build
02 build:  bin/learn_make
03 bin/learn_make: obj/learn_make.o
04     gcc obj/learn_make.o -o bin/learn_make
05 obj/learn_make.o:    src/learn_make.c  inc/learn_make.h
06     gcc -Iinc -c src/learn_make.c -o obj/learn_make.o
07 rebuild: clean build
08 .PHONY:clean
09 clean:
10     rm obj/*
11     rm bin/*

此时你开始执行如下命令

1 $make  //build一下,确保obj/learn_make.o bin/learn_make都存在。
2 $make //哦。make 不执行了。好爽
3 $make clean //清除所有的生成文件
4 $make //哦,又爽了,因为又开始工作了。
5 $make rebuild //哦,爽完再爽,rebuild也行了。

没错。一切都让你爽了。但是你尝试如下命令 

1 $rm obj/* //没错,只删掉obj/*下的东西。
2 $make rebuild //我们先clean,再build

一定错了。make异常退出了。当你准备骂make不智能时,其实make已经准备好了方案。就是如下修改 

01 .PHONY:build
02 build:  bin/learn_make
03 bin/learn_make: obj/learn_make.o
04     gcc obj/learn_make.o -o bin/learn_make
05 obj/learn_make.o:    src/learn_make.c  inc/learn_make.h
06     gcc -Iinc -c src/learn_make.c -o obj/learn_make.o
07 rebuild: clean build
08 .PHONY:clean
09 clean:
10     -rm obj/*
11     -rm bin/*

此时再试一下

1 $make rebuild

虽然仍然提示rm obj/*存在错误。但是会继续做完其他的事情。够人性化吧。由此,我们的一个完整简单的弱智版本Makefile处理完毕了。

    不过之所以最后才说 ‘-’这个因为工具没有问题,但操作不当就会有问题。一定要预防一些存在依赖关系的操作被你加上‘-’,这时你很多事情都会白做。通常只有你确定,当该操 作即便出错,对你的整体make工作没有任何影响时,你才能使用 ‘-’。make的价值,不在于普通脚本的选择性批处理命令,而在于每个动作的相互依赖关系的检测(通常是通过文件的依赖关系),你强迫忽视某个动作是否 错误,实际上在等同修改你的依赖关系(如果这个动作确实存在依赖关系的情况下)。

你可能感兴趣的:(中山野鬼 linux 下 C 编程和make的方法 (四、开始make))