作者:胡彦2013-5-21本文档可能有更新,更新版本请留心一目的:编撰一个实际可用的makefile,能手动编译当前目录下所有.c源文件,但是任何.c、.h或依赖的源文件被更改后,能手动重编这些改动了的源文件,未改动的不编译。二要达到这个目的,用到的技术有:1-使用wildcard函数来获得当前目录下所有.c文件的列表。2-make的多目标规则。3-make的模式规则。4-用gcc-MM命令得到一个.c文件include了什么文件。5-用sed命令对gcc-MM命令的结果作更改。6-用include命令包含依赖描述文件.d。三打算知识(一)多目标对makefile里下边2行,可看出多目标特点,执行makebigoutput或makelittleoutput可见到结果:bigoutputlittleoutput:defs.hpub.h@echo$@$(substoutput,OUTPUT,$@)$^#$@指这个规则里所有目标的集合,$^指这个规则里所有依赖的集合。该行是把目标(bigoutput或littleoutput)里所有子串output替换成小写的OUTPUT(二)蕴涵规则对makefile里下边4行,可看出make的蕴涵规则,执行foo可见到结果:第3、4行表示由.c得到.o,第1、2行表示由.o得到可执行文件。
假如把第3、4行注释的话,疗效一样。即不写.o来自.c的规则,它会手动执行gcc-c-ofoo.ofoo.c这条命令,由.c编译出.o(其中-c表示只编译不链接),之后手动执行gcc-ofoofoo.o链接为可执行文件。foo:foo.ogcc-ofoofoo.o;./foofoo.o:foo.c#注释该行看疗效gcc-cfoo.c-ofoo.o#注释该行看疗效(三)定义模式规则下边定义了一个模式规则,即怎样由.c文件生成.d文件的规则。foobar:foo.dbar.d@echocompletegeneratefoo.dandbar.d%.d:%.c#make会对当前目录下每位.c文件linux include 头文件 路径,依次做一次上面的命令,因而由每位.c文件生成对应.d文件。@echofrom$<to$@g++-MM$$@假设当前目录下有2个.c文件:foo.c和bar.c(文件内容随便)。验证方式有2种,都可:1-运行makefoo.d(或makebar.d),表示想要生成foo.d这个目标。按照规则%.d:%.c,这时%匹配foo,这样%.c等于foo.c,即foo.d这个目标依赖于foo.c。
此时会手动执行该规则里的命令gcc-MMfoo.c>foo.d,来世成foo.d这个目标。2-运行makefoobar,由于foobar依赖于foo.d和bar.d这2个文件,即会一次性生成这2个文件。四下边阐述怎样手动生成依赖性,进而实现本例的makefile。(一)本例使用了makefile的模式规则,目的是对当前目录下每位.c文件,生成其对应的.d文件,比如由main.c生成的.d文件内容为:main.o:main.ccommand.h这儿指示了main.o目标依赖于哪几个源文件,我们只要把这一行的内容,通过make的include指令包含到makefile文件里,即可在其任意一个依赖文件被更改后,重新编译目标main.o。下边解读怎样生成这个.d文件。(二)gcc/g++编译器有一个-MM选项,可以对某个.c/.cpp文件,剖析其依赖的源文件,比如假设main.c的内容为:#include//标准头文件(以形式包含的),被-MM选项忽视,被-M选项搜集#include"stdlib.h"//标准头文件(以""形式包含的),被-MM选项忽视,被-M选项搜集#include"command.h"intmain(){printf("#####HelloMakefile#####n");return0;}则执行gcc-MMmain.c后,屏幕输出:main.o:main.ccommand.h执行gcc-Mmain.c后,屏幕输出:main.o:main.c/usr/include/stdio.h/usr/include/features.h/usr/include/bits/predefs.h/usr/include/sys/cdefs.h/usr/include/bits/wordsize.h/usr/include/gnu/stubs.h/usr/include/gnu/stubs-64.h/usr/lib/gcc/x86_64-linux-gnu/4.4.3/include/stddef.h/usr/include/bits/types.h/usr/include/bits/typesizes.h/usr/include/libio.h/usr/include/_G_config.h/usr/include/wchar.h/usr/lib/gcc/x86_64-linux-gnu/4.4.3/include/stdarg.h/usr/include/bits/stdio_lim.h/usr/include/bits/sys_errlist.h/usr/include/stdlib.h/usr/include/sys/types.h/usr/include/time.h/usr/include/endian.h/usr/include/bits/endian.h/usr/include/bits/byteswap.h/usr/include/sys/select.h/usr/include/bits/select.h/usr/include/bits/sigset.h/usr/include/bits/time.h/usr/include/sys/sysmacros.h/usr/include/bits/pthreadtypes.h/usr/include/alloca.hcommand.h(三)可见,只要把这种行转到makefile里,能够手动定义main.c的依赖是什么文件了,做法是把命令的输出重定向到.d文件里:gcc-MMmain.c>main.d,再把这个.d文件include到makefile里。
怎么include当前目录每位.c生成的.d文件:sources:=$(wildcard*.c)#使用$(wildcard*.cpp)来获取工作目录下的所有.c文件的列表。dependence=$(sources:.c=.d)#这儿,dependence是所有.d文件的列表.即把串sources串里的.c换成.d。include$(dependence)#include前面可以跟若干个文件名,用空格分开,支持键值,比如includefoo.make*.mk。这儿是把所有.d文件一次性全部include进来。注意该句要置于终极目标all的规则以后,否则.d文件里的规则会被误当成终极规则了。(四)如今main.ccommand.h这几个文件,任何一个改了就会重编main.o。并且这儿还有一个问题,假若更改了command.h,在command.h中加入#include"pub.h",这时:1-再make,因为command.h改了,这时会重编main.o,但是会使用新加的pub.h,看上去是正常的。2-这时打开main.d查看,发觉main.d中未加入pub.h,由于按照模式规则%.d:%.c中的定义,只有依赖的.c文件变了,就会重新生成.d,而刚刚改的是command.h,不会重新生成main.d、及在main.d中加入对pub.h的依赖关系,这会造成问题。
3-更改新加的pub.h的内容,再make,果然问题出现了,make报告uptodate,没有像期望那样重编译main.o。现今问题在于,main.d里的某个.h文件改了,没有重新生成main.d。进一步说,main.d里给出的每位依赖文件,任何一个改了,都要重新生成这个main.d。所以main.d也要作为一个目标来世成,它的依赖应当是main.d里的每位依赖文件,也就是说make里要有这样的定义:main.d:main.ccommand.h这时我们发觉,main.d与main.o的依赖是完全相同的RED HAT LINUX 9.0,可以借助make的多目标规则,把main.d与main.o这两个目标的定义合并为一句:main.omain.d:main.ccommand.h如今,main.o:main.ccommand.h这一句我们早已有了,怎么进一步得到main.omain.d:main.ccommand.h呢?(五)解决方式是行内字符串替换,对main.o,取出其中的子串main,加上.d后缀得到main.d,再插入到main.o旁边。能实现这些替换功能的命令是sed。实现的时侯,先用gcc-MM命令生成临时文件main.d.temp,再用sed命令从该临时文件中读出内容(用输出到最终文件main.d。
命令可以如此写:g++-MMmain.c>main.d.tempsed's,(main).o[:]*,1.omain.d:,g'main.d其中:sed's,(main).o[:]*,1.omain.d:,g',是sed命令。main.d,把行内替换结果输出到最终文件main.d。(六)这条sed命令的结构是s/match/replace/g。有时为了清晰,可以把每位/写成冒号,即这儿的格式s,match,replace,g。该命令表示把源串内的match都替换成replace,s指示match可以是正则表达式。g表示把每行内所有match都替换,假如除去g,则只有每行的第1处match被替换(实际上不须要g,由于一个.d文件中,只会在开头有一个main.o:)。这儿match是正则式(main).o[:]*,它分成3段:第1段是(main),在sed命令里把main用(和)括上去,使接出来的replace中可以用1引用main。第2段是.o,表示匹配main.o,(这儿不知何意,除去也是可以的)。
第3段是正则式[:]*,表示若干个空格或逗号,(虽然一个.d里只会有一个逗号,假如这儿写成[]*:,即匹配若干个空格后跟一个逗号,也是可以的)。总体来说match拿来匹配'main.o:'这样的串。这儿的replace是1.omain.d:,其中1会被替换为上面第1个(和)括起的内容,即main,这样replace值为main.omain.d:这样该sed命令就实现了把main.o:替换为main.omain.d:的目的。这两行实现了把临时文件main.d.temp的内容main.o:main.ccommand.h改为main.omain.d:main.ccommand.h,并存入main.d文件的功能。(七)进一步更改,采用手动化变量。促使当前目录下有多个.c文件时,make会依次对每位.c文件执行这段规则,生成对应的.d:gcc-MM$$@.temp;sed's,($*).o[:]*,1.o$@:,g'$@;(八)如今来看里面2行的执行流程:第一次make,假设这时从来没有make过,所有.d文件不存在,这时键入make:1-include所有.d文件的命令无效果。
2-首次编译所有.c文件。每位.c文件中若#include了其它头文件,会由编译器手动读取。因为此次是完整编译,不存在哪些依赖文件改了不会重编的问题。3-对每位.c文件,会依照依赖规则%.d:%.c,生成其对应的.d文件,比如main.c生成的main.d文件为:main.omain.d:main.ccommand.h第二次make,假设改了command.h、在command.h中加入#include"pub.h",这时再make:1-include所有.d文件,比如include了main.d后,得到依赖规则:main.omain.d:main.ccommand.h注意所有include命令是首先执行的,make会先把所有include进来,再生成依赖规则关系。2-此时,按照依赖规则,因为command.h的文件戳改了,要重新生成main.o和main.d文件。3-先调用gcc-cmain.c-omain.o生成main.o,再调用gcc-MMmain.c>main.d重新生成main.d。此时main.d的依赖文件里降低了pub.h:main.omain.d:main.ccommand.hpub.h4-对其它依赖文件没改的.c(由其.d文件得到),不会重新编译.o和生成其.d。
5-最后会执行gcc$(objects)-omain生成最终可执行文件。第三次make,假设改了pub.h,再make。因为第二遍中,已把pub.h加入了main.d的依赖,此时会重编main.c,重新生成main.o和main.d。这样便实现了当前目录下任一源文件改了,手动编译涉及它的.c。(九)进一步更改,得到目前你们普遍使用的版本:set-e;rm-f$@;$(CC)-MM$(CPPFLAGS)$$@.$$$$;sed's,($*).o[:]*,1.o$@:,g'$@;rm-f$@.$$$$第一行,set-e表示,假如某个命令的返回参数非0,这么整个程序立即退出。rm-f拿来删掉上一次make时生成的.d文件,由于现今要重新生成这个.d,老的可以删掉了(不删也可以)。第二行:上面临时文件是用固定的.d.temp作为后缀,为了避免重名覆盖掉有用的文件,这儿把temp换成一个随机数,该数可用$$得到,$$的值是当前进程号。因为$是makefile特殊符号,一个$要用$$来通配符,所以2个$要写成$$$$(你可以在makefile里用echo$$$$来显示进程号的值)。
第三行:sed命令的输入也改成该临时文件.$$。每位shell命令的进程号一般是不同的,为了每次调用$$时得到的进程号相同,必须把这4行置于一条命令中,这儿用分号把它们联接成一条命令(在书写时为了易读,用拆成了多行),这样每次.$$便是同一个文件了。你可以在makefile里用下边命令来比较:echo$$$$echo$$$$;echo$$$$第四行:当make完后,每位临时文件.d.$$,早已不须要了,删掉之。但每位.d文件要在下一次make时被include进来,要保留。(十)综合上面的剖析,得到我们的makefile文件:#使用$(wildcard*.c)来获取工作目录下的所有.c文件的列表sources:=$(wildcard*.c)objects:=$(sources:.c=.o)#这儿,dependence是所有.d文件的列表.即把串sources串里的.c换成.ddependence:=$(sources:.c=.d)#所用的编译工具CC=gcc#当$(objects)列表里所有文件都生成后,便可调用这儿的$(CC)$^-o$@命令生成最终目标all了#把all定义成第1个规则,促使可以把makeall命令简写成makeall:$(objects)$(CC)$^-o$@#这段是make的模式规则linux源代码分析,指示怎样由.c文件生成.o,即对每位.c文件,调用gcc-cXX.c-oXX.o命令生成对应的.o文件。
#倘若不写这段也可以,由于make的蕴涵规则可以起到同样的疗效%.o:%.c$(CC)-c$<-o$@include$(dependence)#注意该句要置于终极目标all的规则以后,否则.d文件里的规则会被误当成终极规则了%.d:%.cset-e;rm-f$@;$(CC)-MM$(CPPFLAGS)$$@.$$$$;sed's,($*).o[:]*,1.o$@:,g'$@;rm-f$@.$$$$.PHONY:clean#之所以把clean定义成伪目标,是由于这个目标并不对应实际的文件clean:rm-fall$(objects)$(dependence)#消除所有临时文件:所有.o和.d。.$$已在每次使用后立刻删掉。-f参数表示被删文件不存在时不报错(十一)里面这个makefile早已能正常工作了(编译C程序),但假如要用它编译C++linux include 头文件 路径,变量CC值要改成g++,每位.c都要改成.cpp,有点冗长。如今我们继续建立它,使其同时支持C和C++,并支持两者的混和编译。#一个实用的makefile,能手动编译当前目录下所有.c/.cpp源文件,支持两者混和编译#而且当某个.c/.cpp、.h或依赖的源文件被更改后,仅重编涉及到的源文件,未涉及的不编译#解读文档:#author:胡彦2013-5-21#----------------------------------------------------------#编译工具用g++,以同时支持C和C++程序,以及两者的混和编译CC=g++#使用$(winldcard*.c)来获取工作目录下的所有.c文件的列表#sources:=main.cppcommand.c#变量sources得到当前目录下待编译的.c/.cpp文件的列表,两次调用winldcard、结果连在一起即可sources:=$(wildcard*.c)$(wildcard*.cpp)#变量objects得到待生成的.o文件的列表,把sources中每位文件的扩充名换成.o即可。
这儿两次调用patsubst函数,第1次把sources中所有.cpp换成.o,第2次把第1次结果里所有.c换成.oobjects:=$(patsubst%.c,%.o,$(patsubst%.cpp,%.o,$(sources)))#变量dependence得到待生成的.d文件的列表,把objects中每位扩充名.o换成.d即可。也可写成$(patsubst%.o,%.d,$(objects))dependence:=$(objects:.o=.d)#----------------------------------------------------------#当$(objects)列表里所有文件都生成后,便可调用这儿的$(CC)$^-o$@命令生成最终目标all了#把all定义成第1个规则,促使可以把makeall命令简写成makeall:$(objects)$(CC)$(CPPFLAGS)$^-o$@@./$@#编译后立刻执行#这段使用make的模式规则,指示怎样由.c文件生成.o,即对每位.c文件,调用gcc-cXX.c-oXX.o命令生成对应的.o文件#若果不写这段也可以,由于make的蕴涵规则可以起到同样的疗效%.o:%.c$(CC)$(CPPFLAGS)-c$<-o$@#同上,指示怎样由.cpp生成.o,可省略%.o:%.cpp$(CC)$(CPPFLAGS)-c$<-o$@#----------------------------------------------------------include$(dependence)#注意该句要置于终极目标all的规则以后,否则.d文件里的规则会被误当成终极规则了#由于这4行命令要多次凋用,定义成命令包以简化书写definegen_depset-e;rm-f$@;$(CC)-MM$(CPPFLAGS)$$@.$$$$;sed's,($*).o[:]*,1.o$@:,g'$@;rm-f$@.$$$$endef#指示怎样由.c生成其依赖规则文件.d#这段使用make的模式规则,指示对每位.c文件,怎样生成其依赖规则文件.d,调用里面的命令包即可%.d:%.c$(gen_dep)#同上,指示对每位.cpp,怎样生成其依赖规则文件.d%.d:%.cpp$(gen_dep)#----------------------------------------------------------#消除所有临时文件(所有.o和.d)。
之所以把clean定义成伪目标,是由于这个目标并不对应实际的文件.PHONY:cleanclean:#.$$已在每次使用后立刻删掉。-f参数表示被删文件不存在时不报错rm-fall$(objects)$(dependence)echo:#调试时显示一些变量的值@echosources=$(sources)@echoobjects=$(objects)@echodependence=$(dependence)@echoCPPFLAGS=$(CPPFLAGS)#提醒:当混和编译.c/.cpp时,为了才能在C++程序里调用C函数,必须把每一个要调用的C函数,其申明都包括在extern"C"{}块上面,这样C++链接时才会成功链接它们。五makefile学习感受:刚学过C语言的读者,可能会认为makefile有点难,由于makefile不像C语言那样,一招一式都这么清晰明了。在makefile里四处是“潜规则”,都是一些委婉的东西,要弄明白只有认清楚这种“潜规则”。基本的规则无非是“一个依赖改了,去更新什么目标”。正由于委婉动作较多,写成一个makefile才不须要这么多篇幅,虽然项目代码才是主体。
只要晓得makefile的框架,往它的套路里填就行了。较好的学习资料是《跟我一起写Makefile.pdf》这篇文档(下载包里早已附送了),比较详尽,适宜初学者。我们学习的目的是,能否编撰一个像本文这样的makefile,以满足简单项目的基本需求,这要求理解上面makefile几个关键点:1-多目标2-蕴涵规则3-定义模式规则4-手动生成依赖性可惜的是,这篇文档其实比较全面,却没有以一个完整的反例为引导,对几处要点没有突出指明,尤其是“定义模式规则”在最后不醒目的位置(第十一部份第五点),致使看了“自动生成依赖性”一节后还比较模糊。所以,看了《跟我一起写Makefile.pdf》后,再结合本文针对性的讲解,会有更实际的收获。另一个学习资料是《GNUmakev3.80英文指南v1.5.pdf》,这个指南更详尽,但较沉闷,不适宜完整学习,一般是遇见问题再去查阅。其它文章和代码请留心我的blog:[END]