自动化工程的构建 makefile自动化工程1、简单演示2、makefile内容的理解3、自动化工程编译时的推导过程4、清除工程4.1、前两个细节4.2、.PHONY4.2.1、“总是被执行”与最佳实践4.2.2、code.c什么时候需要重新编译4、makefile的最佳实践5、makefile的实用语法5.1、特殊变量5.2、自定义变量5.3、禁止回显5.4、进一步优化在学习自动化构建工程之前我们每要想编一个译文件就得不厌其烦地编写一个gcc指令。自己编写指令比较麻烦还不是主要的问题主要的问题是gcc指令写错造成的问题假设你有一个编写好代码的文件code.c你用code.c文件拷贝出一份文件code1.c当你想要编译code1.c你应该执行的代码是gcc code1.c -o code1但是你将code1.c与code1的位置搞混写成了gcc code1 -o code1.c后果就是不仅Bash报错code1.c文件也丢失了。为了解决这些问题我们就需要学习make / makefile以构建自动化工程。首先我们要清楚make指令Makefile / makefile文件 —— 描述的是如何编译当前工程我们先来见一见自动化工程构建的过程。1、简单演示这里有一个我们剩下的code.c文件里面的内容也是正常的。创建文件makefilevim打开makefile写下指令code:code.c gcc code.c -o code然后在外面执行make。就会发现code.c文件被编译成了code接着我们再在makefile中添加指令.PHONY:clean clean:rm-rfcode然后在外面执行make clean。就会发现code被清除了我们就完成了一个简单的自动化工程即对code.c文件进行自动化编译和清除。创建自动化工程中我们创建了一个文件makefile。现在我们试着理解makefile文件里到底写了些什么东西。2、makefile内容的理解依赖关系和依赖方法对于指令code:code.c gcc code.c-ocode我们称code:code.c依赖关系gcc code.c -o code依赖方法依赖关系与依赖方法我们可以简单举两个例子你考入了大学花钱上了大学此时你与大学就建立了依赖关系。你想找工作于是你好好学大学里的知识然后大学老师会帮你找工作学习知识与让老师帮你找工作以达成你找工作的目的就是依赖方法。父亲与儿子的父子关系是依赖关系。月底儿子打电话给父亲叫父亲给生活费以达成自己需要得到生活费的目的这就是依赖方法。对应的makefile里记录的就是依赖关系与依赖方法的集合。目标文件与依赖文件列表code:code.c# 冒号左边的是目标文件# 冒号右边的是依赖文件列表注意此处的目标文件要与源文件翻译中出现的目标文件(.o文件)区分开来。当然我们观察与清除有关的指令.PHONY:clean clean:rm-rfcode依赖文件列表是可以为空的。make指令的默认行为我们这样做将当前makefile的code.c编译指令与程序清除指令的顺序颠倒然后自行gcc编译code.c生成code我们执行makecode就被我们删掉了所以默认情况下make执行的是makefile中第一个依赖关系的依赖方法。其实默认情况下make执行后生成的也只是从上到下第一个目标文件。如果我们需要指定生成哪个目标文件只需在make后添加目标文件的名称。对于mkaefile文件内容有了基本的认识我们再来理解make / makefile到底是怎么形成可执行文件的。3、自动化工程编译时的推导过程我们知道.c源文件编译的整个过程是预处理成.i文件编译成.s文件汇编成.o文件.o文件再进行链接形成可执行程序对于一个给定的文件源文件code.c我们在makefile中反着设计code.c的编译过程code:code.o gcc code.o-ocode code.o:code.s gcc-ccode.s-ocode.o code.s:code.i gcc-Scode.i-ocode.s code.i:code.c gcc-Ecode.c-ocode.i我们惊讶地发现code被编译出来了而且能够执行。看上去make进行了逆向的推导那么make是怎么做到逆向的推导的事实上make命令内部维护了一个栈结构。make命令会在当前目录下寻找makefile文件并解析make对makefile的解析是自上而下的。当前makefile内容code:code.o gcc code.o-ocode code.o:code.s gcc-ccode.s-ocode.o code.s:code.i gcc-Scode.i-ocode.s code.i:code.c gcc-Ecode.c-ocode.i对于code:code.omake会在当前目录下寻找依赖文件code.o。而当前目录下没有code.o依赖方法也就执行不了。此时make就会将依赖方法gcc code.o -o code入栈make继续向下找到code.o:code.smake依旧没能在当前目录下找到code.s依赖方法也执行不了。此时make也将依赖方法gcc -c code.s -o code.o入栈make一直向下如果没有在当前目录下找到依赖文件就将依赖方法入栈。直到make能够找到依赖文件make就开始循环执行栈顶依赖方法出栈…直到栈为空。以上过程就是make编译时的逆向推导过程。这里有两个细节实际上我们不会做code.c - code.i - code.s - code.o - code这么麻烦的操作我们顶多将code.c直接翻译为code.o再进行链接。编写makefile代码(脚本)的时候只能用tab不能用空格代替。4、清除工程工程被创建即文件被统一编译好后工程就可以执行而工程不被需要的时候我们就应该清除工程。清除工程就是清除一些临时文件。准备一个C语言文件code.c建立code.c的makefile文件其中清除临时文件的代码为.PHONY:clean clean:rm-rfcoderm-rfcode.c其中有三个细节我们将分别进行讲解。4.1、前两个细节依赖文件列表可以为空这个我们之前已经提到过。依赖方法的种类与数量如上面代码所示依赖方法可以有多个只需缩进并列在依赖关系下即可。其实依赖方法可以是Linux的任意指令。比如我们添加一个创建文件的指令.PHONY:clean clean:rm-rfcoderm-rfcode.ctouchtest1.txt4.2、.PHONY.PHONY:clean clean:rm-rfcoderm-rfcode.c我们可以认为.PHONY是一个修饰词。.PHONY修饰clean就是告诉makeclean是一个伪目标。.PHONY修饰的目标文件目标文件对应的依赖方法总是被执行。总是被执行是什么意思4.2.1、“总是被执行”与最佳实践对于一个完整的makefile文件code:code.o gcc-ocode code.o code.o:code.c gcc-ccode.c-ocode.o .PHONY:clean clean:rm-rfcoderm-rfcode.o当code.c还未被编译或者code.c内容被修改的时候我们执行第一次make可以成功而接下来执行make就不成功了这就是不总是被执行。而我们执行make clean可以发现make clean每次都成功这就是总是被执行。我们建议.PHONY修饰clean而不要修饰创建目标文件等其它依赖方法。因为我们可以反复清理工程以保证临时文件被彻底消除而已经编译好的文件就没必要再进行编译只有文件还未被编译或者文件内容被修改才需要重新编译。这个最佳实践本质上是为了进行有效编译从而提高效率。那么问题来了make怎么知道code.c需要被重新编译4.2.2、code.c什么时候需要重新编译我们先来搞清楚文件的三个时间。执行指令stat code.c我们主要关注这三个时间Accesscode.c文件上一次被访问的时间Modifycode.c文件上一次内容被修改的时间或者code.c被创建的时间Changecode.c文件上一次属性被修改的时间我们修改文件属性文件的Change就会改变。比如我们修改文件code.c使之对other不具有读权限接着使用stat查看code.c的Change果然被改变了并且code.c的Access与Modify没有被改变。我们修改文件的内容呢code.c的三个时间都被改变了。我们可以做这样的解释修改文件内容需要打开文件Access被改变修改文件内容那么Modify一定会被改变修改文件内容文件的大小被改变Change被改变。访问文件内容Access有时会改变有时不会改变。文件被访问是最频繁的事理论上Access应该是三个时间中改动最频繁的那个而文件Access的每一次改变都必须将结果写入磁盘以保存Access的频繁改动就会加重系统的IO负担。所以Access并不是每次访问都更新而是有一个更新的策略不同的系统有不同的更新策略。如果我们touch已有的文件文件的三个时间也会改变有了上面的知识的铺垫我们就可以解释code.c什么时候需要重新编译。正常情况下code.c文件完成编译生成code可执行文件code的Modify肯定晚于code.c当code.c还未被编译或者code可执行文件被删除了code.c需要再次编译此时code都没有更别谈code的时间了。当code.c第一次编译后code.c的内容被修改。此时code.c的Modify被修改code.c的modify就晚于code的modify由此我们就能弄清楚code.c什么时候需要重新编译当可执行文件code存在且code的Modify晚于code.ccode.c就不需要重新编译当可执行文件code不存在或者code存在但code的Modify早于code.ccode.c就需要重新编译我们也可以顺势推导出.PHONY的作用就是忽略以上对于Modify时间的对比使得make执行后直接生成目标文件。4、makefile的最佳实践我们建议多个源文件分别编译成.o文件所有的.o文件再与第三方库链接形成可执行文件.execode.exe:code.o gcc-ocode.exe code.o code.o:code.c gcc-ccode.c-ocode.o .PHONY:clean clean:rm-rfcode.exe code.o5、makefile的实用语法我们可以先给当前makefile备份因为makefile待会儿是要被修改的备份以防不时之需。(makefile-backup)我们在最佳实践中写的makefile依赖关系与依赖方法有很多重复使用的名字太冗余了当前makefile只能针对code.c这一个源文件进行自动化编译应用范围太狭窄了。所以我们来学习一些makefile的语法使我们写的makefile更加简洁更加通用。首先对于code.o:code.c gcc-ccode.c-ocode.o可以直接省略简写成code.o:code.c gcc-ccode.c# -o code.o可省略5.1、特殊变量我们用表示目标文件用^表示所有依赖文件用$提取。那么code.exe:code.o gcc-ocode.exe code.o就可以改成code.exe:code.o gcc-o$$^其中与^就是特殊变量。5.2、自定义变量makefile里可以做到一种类似自定义变量的行为Bincode.exeObjcode.oSrccode.c注意以上写法不要带空格。所以我们就可以对一些重复使用的文件名替换成变量名Bincode.exeObjcode.oSrccode.c$(Bin):$(Obj)gcc-o$$^$(Obj):$(Src)gcc-ccode.c .PHONY:clean clean:rm-rf$(Bin)$(Obj)(copy内容到makefile上的时候记得检查缩进可以观察代码高亮的颜色)其中我们需要用$()提取变量的内容。我们也可以认为类似Bincode.exe的语句就好像C语言里的宏其中Bin就是宏名code.exe就是宏值。我们要想改变文件的文件名就可以直接修改宏值。5.3、禁止回显为了方便调试代码我们可能会在编译和链接的时候添加一些提示语句Bincode.exeObjcode.oSrccode.c$(Bin):$(Obj)echo我要开始链接了...gcc-o$$^$(Obj):$(Src)echo我要开始编译了...gcc-ccode.c .PHONY:clean clean:rm-rf$(Bin)$(Obj)我们这样设计好提示语句后执行make结果却有点冗余echo语句都打印上来了。有没有办法不要回显 —— 依赖方法前加上Bincode.exeObjcode.oSrccode.c$(Bin):$(Obj)echo我要开始链接了...gcc-o$$^$(Obj):$(Src)echo我要开始编译了...gcc-ccode.c .PHONY:clean clean:rm-rf$(Bin)$(Obj)实际上依赖方法语句本身默认会回显在屏幕上。我们在每一个依赖方法后加都可以禁止其回显。5.4、进一步优化我们可以建立一个debug目标以简单演示工程调试的过程我们之后会用到.PHONY:debug debug: echoBin:$(Bin)echoObj:$(Obj)echoSrc:$(Src)由此我们可以发现$()不仅可以在“外面提取宏值还可以在”里面提取宏值。工具的变量化工具也可以变量化Bincode.exeObjcode.oSrccode.cEchoecho# 工具变量化CCgcc$(Bin):$(Obj)$(Echo)我要开始链接了...# $()提取$(CC)-o$$^$(Obj):$(Src)$(Echo)我要开始编译了...$(CC)-ccode.c .PHONY:clean clean:rm-rf$(Bin)$(Obj)选项的变量化选项也可以变量化Bincode.exeObjcode.oSrccode.cEchoechoCCgccFlags-c-WallLD_Flags-o$(Bin):$(Obj)$(Echo)我要开始链接了...$(CC)$(LD_Flags)$$^$(Obj):$(Src)$(Echo)我要开始编译了...$(CC)$(Flags)code.c .PHONY:clean clean:rm-rf$(Bin)$(Obj)当然选项和工具可以一起打包被变量化RMrm-rf.PHONY:clean clean:$(RM)$(Bin)$(Obj)特殊变量与%的使用对于$(Obj):$(Src)# Obj: code.o Src: code.c$(CC)$(Flags)code.c我们也可以写成%.o:%.c$(CC)$(Flags)$对于之前的^我们可以理解为依赖文件列表中所有依赖文件一起经过依赖方法形成目标文件。而对于我们可以这样理解依赖文件列表中每一个依赖文件都单独经过依赖方法形成各自对应的目标文件。至于%.o:%.c我们可以类比通配符*即%.c代表了所有的.c文件%.o代表了所有将要形成的.o文件。如何证明每一个依赖文件都单独经过依赖方法证明这时我们就需要多文件。如下图当我们添加好了code1.c ~ code1000.c这么1000个文件设置源文件变量Src我们难道要将code.c ~ code1000.c这么1001个文件一个一个输进去吗太麻烦了。有两种做法Src$(shell ls *.c)Src$(wildcard *.c)。我们推荐这种方法。所有的源文件都要分别翻译成.o文件那么我们也要一个一个输入也不是的。我们可以执行Obj(Src:.c.o)执行make debug我们就能观察到多个.c文件与多个.o文件都有了。至此我们就完成了一个当前目录下多文件编译的makefile工程。Bincode.exeObj$(Src:.c.o)#Src$(shell ls *.c)Src$(wildcard *.c)EchoechoCCgccFlags-c-WallLD_Flags-oRMrm-f$(Bin):$(Obj)$(Echo)我要开始链接了...$(Obj)---$(Bin)$(CC)$(LD_Flags)$$^ %.o:%.c $(Echo)我要开始编译了... $ ---$$(CC)$(Flags)$.PHONY:clean clean:$(RM)$(Bin)$(Obj).PHONY:debug debug: echoBin:$(Bin)echoObj:$(Obj)echoSrc:$(Src)我们执行make也确实会看到每一个.c文件都是分别翻译为.o文件的。编译有时间成本我们写好上面的makefile执行make的时候发现多文件.c编译成.o的过程花了不少时间。所以随着文件越多越大编译的时间是越长的。所以多个.c文件分开翻译成.o文件并且要是修改了某几个文件make对修改了的文件重新编译而对没修改的文件不编译再重新生成可执行文件是能提高工程效率的。(你可以试试只修改两个文件再重新make就会发现编译非常快了)