linux 下 C 编程和make的方法 (七、从项目组织化开发谈make操作模块)

说make不行了。至少我快受不了了。如同学会骑自行车的人,你让他故意摔跤,确实挺折磨人的。到现在,我们有两个目录,两个模块,分别如下: 

01 learn_make/
02 |-inc/
03 |-src/
04 |-obj/
05 |-bin/
06 |-Makefile
07 data_struct/
08 |-inc
09 |-src
10 |-obj
11 |-bin
12 |-Makefile
分别做测试,调整,恩,目前的make完全没有问题,但是,C语言强调的模块划设计,自然要非常注意模块间的耦合性。这不单单是在代码中要求如此,在工程开发中,也希望如此。我们把问题由简单说到复杂。 
1、最简单啦,一个人,一个C文件。爱咋搞咋搞。但有个问题,代码不是一天码成的,功能不是一批添加的,这就涉及到一个代码文本的版本管理问题。版本管理的本质原因在于两个: 
    a、代码有缺陷,且发现节点不可预测。 
    b、代码有切割,且切割方式不可预测。 
    任何可以预测的操作,和版本管理都没有任何关系。但通常遇到的问题都是不可预测的。 
        比如代码不可预测的缺陷,你前天码的内容,昨天没发现错误,今天发现了。你搞不清楚是昨天的错,还是今天的错。版本管理,可以让你很容易的获得前天的代码,从而独立测试。这方便的很。 
    而代码切割又什么意思呢?我们希望代码能够复用,但代码是为客户目标服务的,不是反过来,因此复用中,不同客户目标之间存在差异性。由此需要部分代码调 整,而这种调整甚至会影响到有些不用的代码,你对后者是改还是不改?例如,假设就是同一个客户,你要实现A,B,C三个功能。但是客户说,A我现在就 要,B,我要看下DEMO,C我不急。此时C调用了B,B调用了A。但A,B,C三个部分的设计目标就完全不一样,一个是要用的,一个是要演示的(就是忽 悠),一个是要计划的(有没有都无所谓)。那么时间是有限,你打算怎么办?肯定是,A严格保证,B暂时不能出去忽悠的,不展开,C先放放吧。此时就有问题 了。A,B,C都装着不妥啊。这里A,B,C是抽象的。甚至可能C和B就是同一个东西。我说了,B就是用来忽悠的。怎么办?拿掉啊,但是拿掉了,如果回头 B没有意义了,需要C,怎么办?或者客户只要A,版本控制就可以帮你随时提取对不同时刻处理操作后的代码文档。 
    简单总结,一个人,做一个C文件,工程开发的组织,就是文档在时间轴上的管理。 
2、一个人,一组C文件,构造一个模块。这个比上面复杂点了,因为如下两个原因: 
    模块的细节不一定可预测。 
    模块可能需要其他模块的依赖存在(复用精神嘛) 
    因此,这个时候,就需要make了。make就是用来对工程设计时,自动化的对文档,文档操作的批处理,和文档之间的依赖关系检测。可能你的一个小改动想 测试下结果,如果有一个文件,你折腾make做什么,但如果这个小改动需要导致很多C文件重新编译,例如.h文件,那就需要make了。编译器 的#include不会帮你检查文件更新的。而现在你只需要前面说的那点弱智版的make方式足以。 
3、一个人,一组模块,协同工作。例如上面的两个模块那样。又复杂了点。根本原因在于,弱智版的make无法适应模块与模块的高效依赖关联以及模块和模块的高度松耦合。这就会导致,模块单独处理和协同处理的差异性存在。举个例子。我们将上面的路径修正如下 
01 learn_make/
02 |-inc/
03 |-src/
04 |-obj/
05 |-bin/
06 |-Makefile
07 |data_struct/
08 ||-inc
09 ||-src
10 ||-obj
11 ||-bin
12 ||-Makefile


就是将data_struct放在learn_make的目录下,由此data_struct就可以看作learn_make下的子模块。但是这样操作, 一个直接问题是,你得make两次,而且得针对不同的目录里的Makefile。还有个头疼的问题,data_struct生成的 libds_tree.a需要被learn_make里面的源码调用,在链接时,前面已经讲过,你要么copy到当前的-L指定的目录,要么增加一个新的 -L的路径。也就是说,无论如何,上面这种做法,都要至少对一个 Makefile进行大范围修改。
修改不怕,但是记得上面说的版本管理的例子吗?如果今天你需要增加这个模块,明天出去忽悠时,暂时不要这个模块,你的Makefile肯定要改来改去。聪 明的小朋友会说,Makefile作为版本管理的对象之一,解决这个问题。恩我承认,确实如此,Makefile和inc, src的代码文本一样,也需要管理。但别忘了,这样只能解决问题本身,不能高效的解决问题。 
怎么办?当然有办法?什么办法?得用点make的高级货。高级货非常神奇的帮助你高效,严格的完成工程组织管理。当然你是不是会问,这样我们就可以不用在 模块堆叠(模块组合)中修改Makefile吗?哈,当然还得改Makefile?(如同乞丐中的霸主,。。。他还是乞丐)(别吐),此改非彼改。下面就 说说,如何针对某块堆叠利用make点高级货做些最少的改动。 
说make的高级货前(其实没什么高级,只是不弱智了),把工程组织延伸说完。上面我都谈的是一个人。既然工程开发的组织,就包括开发对象,开发本身和开 发者,三个方面的组织。而上面只看了开发对象的组织。开发本身的组织,这个很难抽象的谈,涉及到公司定位,已有资源,客户需求等各种情况。 
而项目开发者本身的组织,其实是个角色分工的问题。展开讨论偏题了。我们就基于都是码农的群体讨论。如果真是农民就好了,你耕你的,我耕我的。但是码农毕 竟最终要整合。此时就存在个耦合度的问题。否则,出了问题,就踢皮球了,谁都不认可是自己的错,这种情况确实有,大家都是对的,但是合起来不行。这种耦合 这就是太松了,松的没法管理了。但你今天的代码写了,我必须明天立刻测试,或者咱两都要上传代码,看谁先做完,做的慢的,嘿嘿merge是你的事情,这事 也太紧了。 
比较理想的做法,就是各做各的,只有接口存在相互配合,通过大家的约束来确定,而内部不需要相互关联,由此,至少每个人都能切个模块出来,或者基于某个版本各自独立有分支,最终再合并,但在合并前,应以能完成阶段目标为准。不过通常不是这么理想,因为有以下几个问题: 
1、不是每个码农都是同样的技术面,可能确实存在,A做完了,B才能基于A的基础处理。 
2、存在模块与模块(或类似库文件)的依赖。A,B都依赖C,甚至有时A,B相互依赖。 
有人会说了,“野鬼,你大爷的,水平不行就是不行,回家重新学管理去,没对策别找理由!”问题是,确实有类似“先有鸡,还是先有蛋”的混沌系统需要分组开 发。将各种情况都列出来,只是希望新手知道,工程组织化,没有终极答案。灵活的应用工具和手段做的更好,才是有效的目标。 
闲扯了这么多工程组织的问题,同时也想对新手说一下以下几个认识上的建议: 
1、你的任何代码工作。尽可能都落在一个模块中,这个模块从文件组织形式上看,要类似learn_make,或data_struct的目录结构。而不要一个目录下堆出一群C和H文件。 
2、不要把Makefile当源代码文本那样改来改去,他是用于组织你的模块以及帮助你参与到整个项目组织中的一个接口工具。除非你永远只是一个人,在一个C文件里做开发,否则掌握好版本管理工具和make,才能有机会真正的协同或组织开发(哪怕只有你一个人) 
由于上面的建议,我们便不能一直使用弱智版的Makefile,毕竟弱智版的内容不方便我们实现上面的目的。 

回到正题,一个有效的Makefile需要能做到以下几点,已作为我们make的现阶段学习的目标: 
1、便于模块的堆叠。保证依赖和松耦合。 
2、除了模块与模块堆叠的组合信息外,其他内容不动,且组合信息的改动越少越好。 

我们的前提,将data_struct目录带子目录的CP到learn_make目录下如下执行命令 
learn_make:cp ../data_struct ./. -r 
以上假设,你的data_struct 和learn_make在同一个目录下。 
我们现在的目标是,learn_make下的源代码能够使用data_struct下的两个东西,头文件,和库文件。有两种做法 
1、我们在Makefile里将对应需要的内容copy到learn_make/inc 和learn_make/bin 里,或者新建目录,叫lib和include,专门存放其他模块的lib和头文件。 
2、我们在Makefile里将对应需要的内容所在的路径,在Makefile里描述出来。 
两种方法都不错,我们看下改动方法,对比下怎么改动最小。不过衍生说一下我的观点。 
第一种做法是在learn_make下独立一个include目录,这个目录区别于inc目录,用于存放其他库的头函数。这种做法,只有在没有源码,仅提 供了库时有利。你连源码都没有,谈何模块的目录结构(如/src ,/inc),既然已经是个库了就不存在该库自动生成的make问题。但对于有源码时,这种做法就不妥当。我的理由如下: 
  一旦你将子模块的头文件,CP到include的目录里独立存在时,不谈和子模块的inc的版本对齐问题。你的当前模块的目录里,就已经隐含了子模块的属性,此时子模块和你的当前模块无法做到很好的松耦合。 
    “野鬼,这个皮筋已经是底线了,你抽取做弹弓,我真的提裤子上街了,已经不能再松了” 
    这我不认同。皮筋是封在裤子里的,没皮筋你不会用皮带啊。皮带抽走,再换一条,多大事。这才叫松耦合。 
    此时还有另一个问题。模块的依次依赖。如下: 
    A ---> B 表示A模块需要B模块的头文件以及B模块编译后形成的最终库文件。但B的编译过程又需要C。更复杂的是A也需要C,当然相互循环嵌套的情况是不存在的。依 赖如果循环(特指,A的任意步骤都需要B先成立,同样B需要C,C又需要A),连make都没有办法搞。那么C里的头文件你打算CP一份到A,还是B,还 是A,B里的include都CP一份? 
    因此我的态度和做法,就是,模块的头文件和生成文件,仍然放在模块里,除非是外来无源码的库和头文件,独立放置在一个包装的模块内,而这个模块被其他模块 引用,如果他同时服务于多个模块时。放心,这是路径和make一个级别的事情,和C代码没关系,不影响效率,并不是让你自己写套代码,把外面的库包个新函 数。但影响管理。 

    费了这些口水,我们就先谈一下,make内如何调用其他make.因为,learn_make的编译,既然data_struct是其组成模块,则必然需要先处理掉。由此引出一个make的使用方法,如下: 
data_struct: 
    cd data_struct && $(MAKE) 
这个用前面的知识可以理解一半。这是个data_struct的目标规则,没有依赖。也就是立刻执行自己下面的命令。命令是什么呢?要解释下 
    cd data_struct是执行到data_struct目录下,就是个shell下的命令,如同我们前面在Makefile里写gcc命令一样。至 于&&,你要醒醒脑袋,这是shell下的一个规则,表示,前面的正确执行后,则会处理后面的,这里不讨论为啥这么做了,错了,总不至于 继续错吧。 
    谈一下$() 和 MAKE(大写哦) 
    $(X),意思是使用一个变量。例如如下 
X=NOT_FUCK 
     @echo  $(X) ;make里,@在一个命令行开头就不会回显这个命令本身,此时只是打印了$(X)的情况,而不会打出echo $(X)命令本身。 
    则此时屏幕会出现NOT_FUCK。就这么简单。不然屏幕会只出现 X,如果你“echo X"的方式。 
    变量的赋值,不需要$()如 X=NOT_FUCK 
    使用变量时则需要$(X)。 
    谈一下MAKE变量。通常你对别人的Makefile上下求索MAKE的定义是不会有答案的。因为他是系统隐含的一个变量。非常明确一点,你完全可以不使 用这个变量,这是你的权利。但我们可以看一下使用$(MAKE)相对直接写 cd data_struct && make有什么不同(我说了,优势劣势得放具体环境下,此处只讲特点)。 
    $(MAKE)实际是执行的当前处理Makefile的那个make。极端的举例。假设搜索文件的优先级最高是在当前目录下,你在data_struct 目录下,就有一个叫做make的执行文件,用于处理Makefile的,但这个make和处理learn_make的make(且不谈他存在在哪反正不是 data_struct里的那个),版本不一致。由此导致对Makefile的处理方式不同,怎么办?这种问题的解决很简单(将不同make执行文件改统 一了),惨的是这种问题的发现很难。也即$(MAKE)保证此处的make和本Makefile执行的make完全一个的程序,而你直接使用make就无 法保证。 
    另一个比较明显的区别在于,make的参数传递。 
    你可以尝试修改一下规则。哪修改呢。我们调整一下clean:的规则目标如下 
clean: 
    cd data_struct && $(MAKE) clean 
    -rm obj/* 
    -rm bin/* 
    这里的意思是先进入data_struct进行make,但是我们给入clean这个规则目标。此时在data_struct下的Makefile执行 clean的目标。由此完成两个模块的clean动作。当然我们在两个Makefile的clean都加上如下的话 
clean: 
     @echo  $(MAKEFLAGS) 
    ... 
    显示一下MAKEFLAGS这个变量的内容,这是个和MAKE一样是系统默认的变量,用于存放make的参数。我们这样调用。 
    make clean -j4 ,-jN表示在可以并发下,make用几个同步线程(也可能是进程,这个不纠结,看系统怎么折腾)。我们主要先看看,-j4这个参数是否能传递给 data_struct。答案很明确,我就不列输出信息了。使用$(MAKE)可以简单的就把当前参数给传递下去了。至少你可以调用 make clean -j1,此时data_struct中输出的 MAKEFLAGS的内容会和 -j4时不一样。 
    有人开始骂我变态了。恩,上面这个例子确实蛮变态的,make clean需要多处理做什么(此处省去线程或进程字样,因为要看系统嘛)?但我觉得这个举例比较容易说清楚,什么是参数,什么是规则目标。对于新手,千万别把自己写的规则目标当参数来认为。 
    因此,总结一下,你希望另一个Makefile想自动被本Makefile的解析执行中,被执行,而同时两种执行是公平的,则老实点,用$(MAKE) 吧,除非你特意需要两个make过程,make本身版本不一样,或参数不一样。对于这种很特殊的目的,那就自己独立处理吧。我对新手的劝告一向的态度是, 我不是你老师,我可没有学分可以分你,所以你大可不必要听我的。而且只有在特定情况下,我说的方法才有意义。一切根据你自己的情况考虑选择的决策。 
    说到现在我们解决了一个问题,如何模块堆叠时,重复利用各个模块中已经有的 Makefile。并根据规则依次调用。那么有新的情况。如果模块A,需要模块B,模块C,B,C完全老死不相往来,那么肯定我们需要都在A中折腾,由此 提出个问题,如何调用多个子模块的Makefile。简单的方法如下: 
1 clean:
2     cd B && $(MAKE) clean
3     cd C && $(MAKE) clean
4     -rm obj/*
5     -rm bin/*



这事算做完了,但是是否达到我们目标了?最少的改动。我们其实也可以如下操作(至于是否必上面方便,看你自己判断了)。 
1 SUB_MODULE=B C
2 clean:$(SUB_MODULE)
3  
4 $(SUB_MODULE):
5     $(MAKE) -C $@ clean


    这里我一一解释。 
    首先是SUB_MODULE,这是个变量没错。但是make里的变量,实际上是个变量集合。他有两个内容,B和C。 
    在clean的依赖描述中$(SUB_MODULE)实际相当于把变量进行了展开,等于 B C,但此处用变量的方式描述就有下面的好处了。 
    $(SUB_MODULE):则表示了一个变量集合的规则。而目标是这个变量里每个元素。当然这里的规则方法(就是下面的命令)是统一的。不统一,又何必 放在一个变量集合里呢。变量实际是个集合。里面可以有很多元素。每个元素都是个独立的值,而变量整体的值是这些元素的组合。 
    余下说一下$(MAKE) -C $@ ,clean不废话了。是make 的目标。 -C(大写)意思是,进入后续的目录,这种方式,可以在进入目录并执行make结束后,自动返回,如同 cd $(@) && $(MAKE) clean。有人说,这两个是一样的,包括英文的资料,大体也可以这么理解。但还是有些不一样。这放在后面说。说下$@。$@就是针对当前的目标。 
    例如: 
1 bin/test:obj/xx.o
2       gcc xx.o -o bin/test
    你完全可以如下写 
1 gcc xx.o -o $@

    甚至原则上,你应该针对每个规则目标。如果这个规则目标是你期望其命令的输出结果,则你一定要用$@。因为输出结果差异会导致的依赖性检测的结果差异。你没有任何理由去将目标在命令行中重新敲一边。如果这样操作 
1 bin/test:obj/xx.o
2    gcc xx.o -o test

    这个笔误,会令你郁闷,为什么bin/test总被执行。原理性原因前面说了,另一个主观原因就是你不肯用$@ 
    由此,我们根据已经获取的新知识,重复一下 
    变量,$@,变量的依赖,如clean:$(SUB_MODULE), 
    这里些一下(不太弱智版的)data_struct的Makefile如下 
01 MODULE_NAME = bin/libds_tree.a
02 MODULE_OBJ = obj/libds_tree.o
03 MODULE_SRC = src/ds_tree.c
04 MODULE_INC = inc/ds_tree.h
05 RM = -rm -f
06  
07 all:$(MODULE_NAME)
08  
09 $(MODULE_NAME):$(MODULE_OBJ)
10     ar -r $@ $(MODULE_OBJ)
11 $(MODULE_OBJ):$(MODULE_SRC) $(MODULE_INC)
12     gcc -Iinc -c $(MODULE_SRC) -o $@
13 .PNONY:all clean
14  
15 clean:
16     $(RM) $(MODULE_NAME)    
17     $(RM) $(MODULE_OBJ)

这里我们声明了几个变量,前缀就不说了。分别是NAME ,OBJ ,SRC ,INC,RM。着重说一下 MODULE_INC,和 gcc命令中的-Iinc。 
这是两个事情,MOUDLE_INC是为了让 $(MODULE_OBJ)进行依赖性更新检测。-Iinc是为了对gcc提供头文件可寻找的路径,因此不要混为一谈。但此处肯定有问题了。为啥?后面谈。先说下$(RM) $(MODULE_NAME)。 
此时的删除已经是精确到文件了,很安全而且即有 前缀 ‘-’ 防止执行错误中断make,也有-f,强制删除。针对OBJ,和BIN应该如此。不过针对诸如 input,output目录(作用前面已经说过),最好还是全删除特别是output,因为output下的文件,是代码运行时生成的,这些运行时的文 件名,往往是动态设置的,如果将这些信息增加到Makefile里,角色就不清楚了。 
Makefile是提供给make用于工程代码文本,在编译开发过程中使用的。最后一步工作是安装程序和清除程序。而在程序实际使用中,make就毛责任都没有了。因此,任何试图对于程序动态运行的信息,写入Makefile的内容,都会导致make的角色混乱。 
    回到Makefile的分析上来,上面的方式我们尝试增加一个头文件,用前面给的create_module的脚本执行如下 
1 $./create_module.sh data_struct -h ds_tree1
    没什么意义,就是在data_struct/inc/下多了一个至少编译器认为没有错的头文件(注意create_module.sh和data_struct在一个目录下)。假设ds_tree.c需要引用他,则在ds_tree.c 的代码里增加 
    #include "ds_tree1.h" 
    此时,必然需要将ds_tree1.h增加到Makefile里,确保文件间的依赖关系。如下 
    MODULE_INC = inc/ds_tree.h inc/ds_tree1.h 
    此时make ,会有新的动作出现。我们打开inc/ds_tree1.h,增加一个空行,存储,保证时间戳改变,再make ,至少生成 .o文件的动作存在了。因为ds_tree1.h改变了。 
    堪称完美啦。 
    “放屁”我只能这么说。这事回头说。继续实验。 
    我们现在在data_struct下再增加一组C文件,即同名的.c 和.h如下 
1 $./create_module.sh data_struct -f ds_tree2
    此时在Makefile显然要修改 INC OBJ和SRC 如下 
MODULE_NAME = bin/libds_tree.a 
MODULE_OBJ = obj/libds_tree.o obj/libds_tree2.o 
MODULE_SRC = src/ds_tree.c src/ds_tree2.c 
MODULE_INC = inc/ds_tree.h inc/ds_tree1.h inc/ds_tree2.h 
    保存,运行,嘿嘿,错了没有? 
    原因在于 
    gcc -c a.c b.c -o x.o,此时的错误是因为编译器对C文件的编译,是一对一的,而当你多个文件要编译的话,gcc需要如下操作。 
    gcc -c a.c b.c,此时会产生同名的,a.o. b.o。但问题出来,你是需要指定路径的,obj/a.o obj/b.o。 
    因此我们希望make对Makefile在 
    gcc -Iinc -c $(MODULE_SRC) -o $@ 
    执行时,能如 
    $(MAKE) -C $@ clean 
    这样多条展开。其实make确实多条展开了。不过展成这样了。 
    gcc -Iinc src/ds_tree.c src/ds_tree2.c -o obj/libds_tree.o 
    gcc -Iinc src/ds_tree.c src/ds_tree2.c -o obj/libds_tree2.o 
    是不是你要气的吐血?这样的问题出现是因为,变量作为目标,可以依次展开,但是依赖,无法对应依次展开,如果可以,则如下问题如何解决? 
    任何一个变量集合里面的目标都需要依赖里变量集合的全部内容。 
    解决这个问题有很多种做法,我个人推荐如下操作。 
01 MODULE_NAME = bin/libds_tree.a
02 MODULE_OBJ = obj/libds_tree.o obj/libds_tree2.o
03 MODULE_SRC = src/ds_tree.c src/ds_tree2.c
04 MODULE_INC = inc/ds_tree.h inc/ds_tree1.h inc/ds_tree2.h
05  
06 RM = -rm -f
07  
08 obj/libds_tree.o:src/ds_tree.c inc/ds_tree.h inc/ds_tree1.h
09 obj/libds_tree2.o:src/ds_tree2.c inc/ds_tree2.h
10  
11 all:$(MODULE_NAME)
12  
13 $(MODULE_NAME):$(MODULE_OBJ)
14 #@echo $(MODULE_OBJ)
15     ar -r $@ $(MODULE_OBJ)
16  
17 $(MODULE_OBJ):
18 #@echo $@
19 #@echo $^
20     gcc -Iinc -c $< -o $@
21  
22 .PNONY:all clean
23  
24 clean:
25     $(RM) $(MODULE_NAME)    
26     $(RM) $(MODULE_OBJ)

    解释一下 $(MODULE_OBJ),此时,其包含的每个元素,都有个目标和依赖。你可以尝试把$(MODULE_OBJ):后我注释的部分打开,将当前目标和当 前依赖给显示出来。同时你可以打开$(MODULE_NAME)后的注视,看一下$(MODULE_OBJ)都有什么? 
    $<是取依赖里面的第一个内容。如果你把 
    obj/libds_tree2.o:src/ds_tree2.c inc/ds_tree2.h 
写成 
    obj/libds_tree2.o:src/ds_tree2.h inc/ds_tree2.c 
    看看有啥好玩的,嘿嘿。 
    上述这种做法,常用在,目标不一,目标的依赖不一,但是通过依赖生成目标的规则统一的情况下使用。 
    到目前位置,我们可以把前面一个问题,一个放屁的事情拿来说了。重复问题和放屁如下: 
    1、“MOUDLE_INC是为了让 $(MODULE_OBJ)进行依赖性更新检测。-Iinc是为了对gcc提供头文件可寻找的路径,因此不要混为一谈。但此处肯定有问题了” 
    2、“MODULE_INC = inc/ds_tree.h inc/ds_tree1.h 
    此时make ,会有新的动作出现。。。。因为ds_tree1.h改变了。堪称完美。放屁,我只能这么说。” 
    放屁的事情,和问题其实是一个讨论话题。因此就谈问题。gcc准确说cc,cc与gcc的区别我们另谈。新手你全当一个东西(其实不是),有个-M的参 数。注意是gcc的,不是make的,其作用是根据#include的情况,C代码里面的内容,自动查找#include并将规则显示出来。当这个放在 Makefile里执行时,显示出来的依赖关系,对Makefile的执行是有影响的。你完全可以如下设计Makefile 
01 MODULE_NAME = bin/libds_tree.a
02 MODULE_OBJ = obj/libds_tree.o obj/libds_tree2.o
03 MODULE_SRC = src/ds_tree.c src/ds_tree2.c
04 MODULE_INC = inc/ds_tree.h inc/ds_tree1.h inc/ds_tree2.h
05  
06 RM = -rm -f
07  
08  
09  
10 all:$(MODULE_NAME)
11  
12 obj/libds_tree.o:src/ds_tree.c
13 obj/libds_tree2.o:src/ds_tree2.c
14  
15 $(MODULE_NAME):$(MODULE_OBJ)
16     ar -r $@ $(MODULE_OBJ)
17  
18 $(MODULE_OBJ):
19     gcc -Iinc -MM $<
20     gcc -Iinc -c $< -o $@
21  
22 .PNONY:all clean
23  
24 clean:
25     $(RM) $(MODULE_NAME)    
26     $(RM) $(MODULE_OBJ)

    注意对于.o的依赖,我删除了.h。同时,在.o的规则命令中,多了一个gcc -Iinc -MM $<。此时会自动增加依赖。你尝试修改ds_tree1.h,虽然这个Makefile没有依赖描述,但是由于这个命令,会自动添加,所以仍然会继 续执行后面的内容。-MM是不想吓着小朋友,相对-M而言,不会列出所有的包括库头文件的依赖。 
    但这种做法,我不建议使用。先修正一个“MOUDLE_INC是为了让 $(MODULE_OBJ)进行依赖性更新检测”的描述,这是针对本章节最早的案例。而后期的案例,已经不存在$(MODULE_INC)作为整体的依赖 描述作用了。那么此时$(MODULE_INC)有什么用? 
    我的项目组织经验和个人的失败教训导致我认为: 
    Makefile,除非你是用于发行,另谈,应该具备模块描述功能。有人说在README中解释。我觉得这和代码后面,或者代码前面加上函数说明的文字性 描述一样,四个字“形而上学”,也即,形式主义。Makefile本身就是针对模块或者项目进行依赖性确认的。模块内部的关联信息本身就要在 Makefile里描述,为什么不把模块的架构(涉及文件与文件的关联信息都放在Makefile里呢?)至少如下方式 
MODULE_NAME = bin/libds_tree.a 
MODULE_OBJ = obj/libds_tree.o obj/libds_tree2.o 
MODULE_SRC = src/ds_tree.c src/ds_tree2.c 
MODULE_INC = inc/ds_tree.h inc/ds_tree1.h inc/ds_tree2.h 

obj/libds_tree.o:src/ds_tree.c inc/ds_tree.h inc/ds_tree1.h 
obj/libds_tree2.o:src/ds_tree2.c inc/ds_tree2.h 
告诉我们几个内容。 
1、模块名,模块输出文件名 
2、模块目标文件,和源文件,头文件组成 
3、通过模块目标文件的依赖关系,明确该模块每个文件之间的依赖关系。 
一个项目组,你的工作给别人,别人看这个Makefile的前半部分,就可以对模块内部关联有所了解,这不很好吗?为什么你还要写个额外的文档来说明模块内部的文件关联?其实往往这个看的人,就是日后的你,书写者就是曾经的你。 
同时在开发中,还存在一些情况,有时在调试时,需要临时拿掉某个C文件和头文件。通常用#if 0,或#ifdef TEST等等 的方式实现。如果这样处理,gcc -MM是不会把#ifdef不成立的部分作为分析对象的。例如 
#if 0 
#include "ds_tree1.h" 
#endif 
但这个只是临时的,难道也需要gcc如临大敌的重新规划规则?同时也可能导致你的同伙误解。 
    “咦,你的XX文件好像没有依赖意义啊,我MM没看出来” 
    “确实MM看不出来,因为我临时关了” 
    “那可以删了?” 
    “暂时不用它,过两天我DEBUG完补上去” 
    。。版本管理的server上少了个这个文件。 
    过了N多个两天,经理来了。 
    “那个C文件呢?” 
    “你问他,关我毛事!” 
    “我给忘了DEBUG了,所以暂时拿掉了。。他MM看不到就删了,我看server上这个文件删掉了,我给忘了” 
    说这个例子是想说。模块在开发中确实存在调整,但是很多情况下是临时的不用某个C文件并入模块。这种操作,并不会影响模块的原先规划。而Makefile 上面的做法,就是明确出模块的规划目标。当临时的文件的调整,没有必要修改Makefile的模块信息部分。不要认为,暂时不用的,Makefile里写 着就浪费。如果基于认可这个思想,gcc 的M 或MM就无法帮你完成这个工作,因为它不会显式的写出模块(规划中)的关联信息。 
    同时放屁的问题也解决了。MODULE_INC主要的目的是列表模块头文件信息,并作为一个集合,供项目管理中,代码文本管理中,其他的检测使用。强调的是当前模块,不基于标准库文件外,整体需要的头文件总集合。 

    需要补充说一下, 
    obj/libds_tree.o:src/ds_tree.c 
obj/libds_tree2.o:src/ds_tree2.c 
    放在最上面不是个好注意。这是个依赖规则。只不过没有命令而已。如果放在最上面,你只是执行make,就会觉得机器速度超级快了。 

    现在我们的data_struct的Makefile阶段性的就这样了。至少看上去有点模样了。虽然还是存在问题,例如: 
    文件名总有前缀路径。 
    但得关注一下另一个事情,就是模块与模块之间依赖的问题。前面说的,已经可以make一个Makefile时,调用别的Makefile执行了。但目前还 没解决模块依赖更新。说这个,先说说前面暂时没展开的 cd data_struct && $(MAKE) clean于$(MAKE) -C data_struct clean的区别。 
    我们先列一下learn_make clean的部分 
MODULE_SUB = data_struct tt 
    
clean: 
    $(foreach dir,$(MODULE_SUB),cd $(dir) && $(MAKE) clean;) 
    $(RM) $(MODULE_OBJ) 
    $(RM) $(MODULE_BIN) 
    你可以运行一下。会出问题。假设你learn_make目录下,确实有data_struct 和tt两个目录。且不谈tt下面有什么。出错的情况是 
    在data_struct目录下,尝试进入tt,但没有tt这个目录,废话,tt在learn_make下。就此停一下,解释下foreach,学点新东西。 
    foreach是make的内部函数。相当于循环。$(foreach ...)是表示函数,一个循环操作的函数调用。 
    规则如下: 
    $(foreach var,list,text) 
    var就是我上面的dir,这个变量算是临时性变量和C语言的 
    for (int i = 0 ; i < 10 ;i++)类似。 
    list是元素的集合,此处也是个变量。foreach依次将$(MODULE_SUB)里面的元素给予var,并展开放在text中。因此,上面的效果就如下 
    cd data_struct && $(MAKE) clean; cd tt && $(MAKE) clean; 
    而如果上面的函数修改如下方式 
    $(foreach dir,$(MODULE_SUB),$(MAKE) -C $(dir) clean;) 
    则展开位 
    make -C data_struct clean;make -C tt clean; 
    有些资料上说,make -C dir 和 cd dir && make 是相同的作用。但很显然,此时并不相同。通常正确的做法,不要使用我的举例,如果你想cd && $(MAKE),而是应该 for ... done的方式。让cd dir && $(MAKE) 作为一个整体。 
    但可以非常明确的说,cd dir && $(MAKE),特别是在自动化展开时,并不是均能保证整体性,也即操作的原子性。当然cd dir && ..有好处啊。如果cd dir不行,后面的也就不会执行,而 $(MAKE) -C dir,dir如果不存在时,此时的make操作,会形成错误。但没关系,此 make 非彼make.使用$(MAKE),在 Makefile里调用make,只要make本身执行文件存在,就可以,至于这个新make是否正确执行,和当前正在运行的make没有关系,因此不会 导致当前make的执行中断,只会说有个make错误了。 
    这里解释一下,上面的举例很变态,本身用foreach去操作make调用make就不妥当。但这里只是希望新手注意,细小的差别存在和没有差别是两回事。另外如果make 调用make最好是 -C的方式操作。 
    回到依赖性上。如何解决? 
    这里明确一下,依赖性必须基于模块与模块存在共识,也即,当父模块需要子模块的一个目标时,则父模块应当和子模块有相同的目标。简单说,父模块是make all时,子模块也是make all ,父模块需要make clean是,子模块也是make clean。也即大家都做相同的目标的事情,而相同目标的事情的名称大家是达成共识的。不能说,父模块的 make love是为了生成目标文件 love,但子模块缺没有love 这个目标。这比较霸道,凭什么你能我不能?由此我将当前阶段learn_make的Makefile贴出来如下,为了统一data_struct,我将 build改成了all: 

01 MODULE_NAME = bin/learn_make
02 MODULE_OBJ = obj/learn_make.o
03 MODULE_SRC = src/learn_make.c
04 MODULE_INC = inc/learn_make.h
05 MODULE_SUB = data_struct
06 RM = -rm -f
07 all: $(MODULE_SUB) $(MODULE_NAME)
08  
09 obj/learn_make.o :src/learn_make.c inc/learn_make.h
10  
11  
12 $(MODULE_NAME): $(MODULE_OBJ)
13     gcc obj/learn_make.o -o bin/learn_make
14 $(MODULE_OBJ):
15     gcc -Iinc -c $< -o $@  
16  
17 rebuild: clean all
18  
19  
20 .PHONY:rebuild clean all $(MODULE_SUB)
21  
22  
23 clean:$(MODULE_SUB)
24     $(RM) obj/*
25     $(RM) bin/*
26     
27 $(MODULE_SUB):
28     $(MAKE) -C $@ $(MAKECMDGOALS)


    这里有几个要说明的: 
    1、all 此处依赖$(MODULE_SUB) $(MODULE_NAME),但有前后顺序,目前我们的learn_make没有真正需要data_struct的libds_tree.a文件。但任何子模块应该先在父模块处理前处理掉。 
    2、clean: 依赖了子模块$(MODULE_SUB),和 all一样,由于$(MODULE_SUB)是伪目标所以会始终尝试执行,补充一下,各种伪目标可以一行处理如我上面的方式。如果没有这个依赖,你的 make clean无法对子模块进行处理。你当然可以写两个目标,而防止子模块都被clean,或许没有这个必要,以后也会如此扩展如下 

1 clean:
2     $(RM) obj/*
3     $(RM) bin/*
4 clean_all:clean $(MODULE_SUB)  
5 $(MODULE_SUB):
6     $(MAKE) -C $@ $(MAKECMDGOALS)

    但有个问题我们放以后说。 
    3、$(MAKECMDGOALS),这是个好东西啊。这和MAKE一样是个默认的变量,这个里面是什么值?就是你在命令行里的 make all的 all,make clean的 clean。将它传递给对子模块处理的make,这样大家就统一了。当然只有make时,大家仍然是表示默认方式。不过此时内容可就不一样 了。$(MAKECMDGOALS)表示啥都没有,父模块和子模块都开始执行各自自己第一默认的目标规则。 
    关于2的问题,其实在3里面说明了。不知道你是否能理解。如果子模块没有clean_all怎么办?呵呵,统一,才能更有效的协同工作嘛。 

    至此,我们总结一下,模块和模块,为了开发的高效,应当降低耦合度。make虽然是对Makefile的内容进行处理。但不代表模块和模块合并时,需要改 动很多。如果每个模块的Makefile的书写风格统一,那么模块与模块的交错关联,可以针对对应Makefile 的 MODULE_SUB的变量来进行调整,仅此展开其他改动,例如子模块的头文件和库文件的引用问题。而这种改动即是必须的,同时也能有效反应出当前模块和 其子模块的关系。当然子模块与谁又有父子关系,肯定通过这个Makefile看不出来,不过如果能看出来,就又有矛盾了。耦合度太高了嘛。 

    怎么样,现在的Makefile不太弱智了,当然还有问题,不过至少解决掉了我们模块与模块的松散耦合的组织化开发的问题。你已经从多个C文件的组织开发,进步到多个模块了。

 

 

共有  1条补充说明
中山野鬼:上面有个错误,当Makefile内调用 $(MAKE) -C dir $(MAKECMDGOALS) 如果 dir没有该目录。或dir目录下没有Makefile时,仍然会出错。这个错误是上述这条语句在运行时的错误,属于本Makefile的错误。所以上述需要加上 -也即 - $(MAKE) -C dir $(MAKECMDGOALS)

 


转自:http://my.oschina.net/luckystar/blog/67084

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值