makefile的语法及写法

0 Makefile概述 

-------------------------------------------------------------------------------- 
什么是makefile?或许很多Winodws的程序员都不知道这个东西,因为那些Windows的IDE都为你做了这个工作,但我觉得要作一个好的和professional的程序员,makefile还是要懂。这就好像现在有这么多的HTML的编辑器,但如果你想成为一个专业人士,你还是要了解HTML的标识的含义。特别在Unix下的软件编译,你就不能不自己写makefile了,会不会写makefile,从一个侧面说明了一个人是否具备完成大型工程的能力。 
因为,makefile关系到了整个工程的编译规则。一个工程中的源文件不计数,其按类型、功能、模块分别放在若干个目录中,makefile定义了一系列的规则来指定,哪些文件需要先编译,哪些文件需要后编译,哪些文件需要重新编译,甚至于进行更复杂的功能操作,因为makefile就像一个Shell脚本一样,其中也可以执行操作系统的命令。 

makefile带来的好处就是——“自动化编译”,一旦写好,只需要一个make命令,整个工程完全自动编译,极大的提高了软件开发的效率。make是一个命令工具,是一个解释makefile中指令的命令工具,一般来说,大多数的IDE都有这个命令,比如:Delphi的make,Visual C++的nmake,Linux下GNU的make。可见,makefile都成为了一种在工程方面的编译方法。 

现在讲述如何写makefile的文章比较少,这是我想写这篇文章的原因。当然,不同产商的make各不相同,也有不同的语法,但其本质都是在“文件依赖性”上做文章,这里,我仅对GNU的make进行讲述,我的环境是RedHat Linux 8.0,make的版本是3.80。必竟,这个make是应用最为广泛的,也是用得最多的。而且其还是最遵循于IEEE 1003.2-1992 标准的(POSIX.2)。 

在这篇文档中,将以C/C++的源码作为我们基础,所以必然涉及一些关于C/C++的编译的知识,相关于这方面的内容,还请各位查看相关的编译器的文档。这里所默认的编译器是UNIX下的GCC和CC。 


0.1 关于程序的编译和链接 
在此,我想多说关于程序编译的一些规范和方法,一般来说,无论是C、C++、还是pas,首先要把源文件编译成中间代码文件,在Windows下也就是 .obj 文件,UNIX下是 .o 文件,即 Object File,这个动作叫做编译(compile)。然后再把大量的Object File合成执行文件,这个动作叫作链接(link)。 
编译时,编译器需要的是语法的正确,函数与变量的声明的正确。对于后者,通常是你需要告诉编译器头文件的所在位置(头文件中应该只是声明,而定义应该放在C/C++文件中),只要所有的语法正确,编译器就可以编译出中间目标文件。一般来说,每个源文件都应该对应于一个中间目标文件(O文件或是OBJ文件)。 

链接时,主要是链接函数和全局变量,所以,我们可以使用这些中间目标文件(O文件或是OBJ文件)来链接我们的应用程序。链接器并不管函数所在的源文件,只管函数的中间目标文件(Object File),在大多数时候,由于源文件太多,编译生成的中间目标文件太多,而在链接时需要明显地指出中间目标文件名,这对于编译很不方便,所以,我们要给中间目标文件打个包,在Windows下这种包叫“库文件”(Library File),也就是 .lib 文件,在UNIX下,是Archive File,也就是 .a 文件。 

总结一下,源文件首先会生成中间目标文件,再由中间目标文件生成执行文件。在编译时,编译器只检测程序语法,和函数、变量是否被声明。如果函数未被声明,编译器会给出一个警告,但可以生成Object File。而在链接程序时,链接器会在所有的Object File中找寻函数的实现,如果找不到,那到就会报链接错误码(Linker Error),在VC下,这种错误一般是:Link 2001错误,意思说是说,链接器未能找到函数的实现。你需要指定函数的Object File. 

好,言归正传,GNU的make有许多的内容,闲言少叙,还是让我们开始吧。

 

1 Makefile 介绍 

-------------------------------------------------------------------------------- 
make命令执行时,需要一个 Makefile 文件,以告诉make命令需要怎么样的去编译和链接程序。 
首先,我们用一个示例来说明Makefile的书写规则。以便给大家一个感兴认识。这个示例来源于GNU的make使用手册,在这个示例中,我们的工程有8个C文件,和3个头文件,我们要写一个Makefile来告诉make命令如何编译和链接这几个文件。我们的规则是: 

如果这个工程没有编译过,那么我们的所有C文件都要编译并被链接。 
如果这个工程的某几个C文件被修改,那么我们只编译被修改的C文件,并链接目标程序。 
如果这个工程的头文件被改变了,那么我们需要编译引用了这几个头文件的C文件,并链接目标程序。 
只要我们的Makefile写得够好,所有的这一切,我们只用一个make命令就可以完成,make命令会自动智能地根据当前的文件修改的情况来确定哪些文件需要重编译,从而自己编译所需要的文件和链接目标程序。 

1.1 Makefile的规则 
在讲述这个Makefile之前,还是让我们先来粗略地看一看Makefile的规则。 
target ... : prerequisites ... 
  command 
  ... 
  ... 
target也就是一个目标文件,可以是Object File,也可以是执行文件。还可以是一个标签(Label),对于标签这种特性,在后续的“伪目标”章节中会有叙述。 
prerequisites就是,要生成那个target所需要的文件或是目标。 

command也就是make需要执行的命令。(任意的Shell命令) 

这是一个文件的依赖关系,也就是说,target这一个或多个的目标文件依赖于prerequisites中的文件,其生成规则定义在command中。说白一点就是说,prerequisites中如果有一个以上的文件比target文件要新的话,command所定义的命令就会被执行。这就是Makefile的规则。也就是Makefile中最核心的内容。 

说到底,Makefile的东西就是这样一点,好像我的这篇文档也该结束了。呵呵。还不尽然,这是Makefile的主线和核心,但要写好一个Makefile还不够,我会以后面一点一点地结合我的工作经验给你慢慢到来。内容还多着呢。:) 

1.2 一个示例 
正如前面所说的,如果一个工程有3个头文件,和8个C文件,我们为了完成前面所述的那三个规则,我们的Makefile应该是下面的这个样子的。 
    edit : main.o kbd.o command.o display.o / 
          insert.o search.o files.o utils.o 
            cc -o edit main.o kbd.o command.o display.o / 
                      insert.o search.o files.o utils.o 

    main.o : main.c defs.h 
            cc -c main.c 
    kbd.o : kbd.c defs.h command.h 
            cc -c kbd.c 
    command.o : command.c defs.h command.h 
            cc -c command.c 
    display.o : display.c defs.h buffer.h 
            cc -c display.c 
    insert.o : insert.c defs.h buffer.h 
            cc -c insert.c 
    search.o : search.c defs.h buffer.h 
            cc -c search.c 
    files.o : files.c defs.h buffer.h command.h 
            cc -c files.c 
    utils.o : utils.c defs.h 
            cc -c utils.c 
    clean : 
            rm edit main.o kbd.o command.o display.o / 
              insert.o search.o files.o utils.o 
反斜杠(/)是换行符的意思。这样比较便于Makefile的易读。我们可以把这个内容保存在文件为“Makefile”或“makefile”的文件中,然后在该目录下直接输入命令“make”就可以生成执行文件edit。如果要删除执行文件和所有的中间目标文件,那么,只要简单地执行一下“make clean”就可以了。 
在这个makefile中,目标文件(target)包含:执行文件edit和中间目标文件(*.o),依赖文件(prerequisites)就是冒号后面的那些 .c 文件和 .h文件。每一个 .o 文件都有一组依赖文件,而这些 .o 文件又是执行文件 edit 的依赖文件。依赖关系的实质上就是说明了目标文件是由哪些文件生成的,换言之,目标文件是哪些文件更新的。 

在定义好依赖关系后,后续的那一行定义了如何生成目标文件的操作系统命令,一定要以一个Tab键作为开头。记住,make并不管命令是怎么工作的,他只管执行所定义的命令。make会比较targets文件和prerequisites文件的修改日期,如果prerequisites文件的日期要比targets文件的日期要新,或者target不存在的话,那么,make就会执行后续定义的命令。 

这里要说明一点的是,clean不是一个文件,它只不过是一个动作名字,有点像C语言中的lable一样,其冒号后什么也没有,那么,make就不会自动去找文件的依赖性,也就不会自动执行其后所定义的命令。要执行其后的命令,就要在make命令后明显得指出这个lable的名字。这样的方法非常有用,我们可以在一个makefile中定义不用的编译或是和编译无关的命令,比如程序的打包,程序的备份,等等。

 

1.3 make是如何工作的 
在默认的方式下,也就是我们只输入make命令。那么, 

make会在当前目录下找名字叫“Makefile”或“makefile”的文件。 
如果找到,它会找文件中的第一个目标文件(target),在上面的例子中,他会找到“edit”这个文件,并把这个文件作为最终的目标文件。 
如果edit文件不存在,或是edit所依赖的后面的 .o 文件的文件修改时间要比edit这个文件新,那么,他就会执行后面所定义的命令来生成edit这个文件。 
如果edit所依赖的.o文件也存在,那么make会在当前文件中找目标为.o文件的依赖性,如果找到则再根据那一个规则生成.o文件。(这有点像一个堆栈的过程) 
当然,你的C文件和H文件是存在的啦,于是make会生成 .o 文件,然后再用 .o 文件生命make的终极任务,也就是执行文件edit了。 
这就是整个make的依赖性,make会一层又一层地去找文件的依赖关系,直到最终编译出第一个目标文件。在找寻的过程中,如果出现错误,比如最后被依赖的文件找不到,那么make就会直接退出,并报错,而对于所定义的命令的错误,或是编译不成功,make根本不理。make只管文件的依赖性,即,如果在我找了依赖关系之后,冒号后面的文件还是不在,那么对不起,我就不工作啦。 

通过上述分析,我们知道,像clean这种,没有被第一个目标文件直接或间接关联,那么它后面所定义的命令将不会被自动执行,不过,我们可以显示要make执行。即命令——“make clean”,以此来清除所有的目标文件,以便重编译。 

于是在我们编程中,如果这个工程已被编译过了,当我们修改了其中一个源文件,比如file.c,那么根据我们的依赖性,我们的目标file.o会被重编译(也就是在这个依性关系后面所定义的命令),于是file.o的文件也是最新的啦,于是file.o的文件修改时间要比edit要新,所以edit也会被重新链接了(详见edit目标文件后定义的命令)。 

而如果我们改变了“command.h”,那么,kdb.o、command.o和files.o都会被重编译,并且,edit会被重链接。 

1.4 makefile中使用变量 
在上面的例子中,先让我们看看edit的规则: 
      edit : main.o kbd.o command.o display.o / 
                  insert.o search.o files.o utils.o 
            cc -o edit main.o kbd.o command.o display.o / 
                      insert.o search.o files.o utils.o 
我们可以看到[.o]文件的字符串被重复了两次,如果我们的工程需要加入一个新的[.o]文件,那么我们需要在两个地方加(应该是三个地方,还有一个地方在clean中)。当然,我们的makefile并不复杂,所以在两个地方加也不累,但如果makefile变得复杂,那么我们就有可能会忘掉一个需要加入的地方,而导致编译失败。所以,为了makefile的易维护,在makefile中我们可以使用变量。makefile的变量也就是一个字符串,理解成C语言中的宏可能会更好。 
比如,我们声明一个变量,叫objects, OBJECTS, objs, OBJS, obj, 或是 OBJ,反正不管什么啦,只要能够表示obj文件就行了。我们在makefile一开始就这样定义: 

    objects = main.o kbd.o command.o display.o / 
              insert.o search.o files.o utils.o 
于是,我们就可以很方便地在我们的makefile中以“$(objects)”的方式来使用这个变量了,于是我们的改良版makefile就变成下面这个样子: 
    objects = main.o kbd.o command.o display.o / 
              insert.o search.o files.o utils.o 

    edit : $(objects) 
            cc -o edit $(objects) 
    main.o : main.c defs.h 
            cc -c main.c 
    kbd.o : kbd.c defs.h command.h 
            cc -c kbd.c 
    command.o : command.c defs.h command.h 
            cc -c command.c 
    display.o : display.c defs.h buffer.h 
            cc -c display.c 
    insert.o : insert.c defs.h buffer.h 
            cc -c insert.c 
    search.o : search.c defs.h buffer.h 
            cc -c search.c 
    files.o : files.c defs.h buffer.h command.h 
            cc -c files.c 
    utils.o : utils.c defs.h 
            cc -c utils.c 
    clean : 
            rm edit $(objects) 
于是如果有新的 .o 文件加入,我们只需简单地修改一下 objects 变量就可以了。 

关于变量更多的话题,我会在后续给你一一道来。 

1.5 让make自动推导 
GNU的make很强大,它可以自动推导文件以及文件依赖关系后面的命令,于是我们就没必要去在每一个[.o]文件后都写上类似的命令,因为,我们的make会自动识别,并自己推导命令。 
只要make看到一个[.o]文件,它就会自动的把[.c]文件加在依赖关系中,如果make找到一个whatever.o,那么whatever.c,就会是whatever.o的依赖文件。并且 cc -c whatever.c 也会被推导出来,于是,我们的makefile再也不用写得这么复杂。我们的是新的makefile又出炉了。 

    objects = main.o kbd.o command.o display.o / 
              insert.o search.o files.o utils.o 

    edit : $(objects) 
            cc -o edit $(objects) 

    main.o : defs.h 
    kbd.o : defs.h command.h 
    command.o : defs.h command.h 
    display.o : defs.h buffer.h 
    insert.o : defs.h buffer.h 
    search.o : defs.h buffer.h 
    files.o : defs.h buffer.h command.h 
    utils.o : defs.h 

    .PHONY : clean 
    clean : 
            rm edit $(objects) 
这种方法,也就是make的“隐晦规则”。上面文件内容中,“.PHONY”表示,clean是个伪目标文件。 
关于更为详细的“隐晦规则”和“伪目标文件”,我会在后续给你一一道来。 

1.6 另类风格的makefile 
即然我们的make可以自动推导命令,那么我看到那堆[.o]和[.h]的依赖就有点不爽,那么多的重复的[.h],能不能把其收拢起来,好吧,没有问题,这个对于make来说很容易,谁叫它提供了自动推导命令和文件的功能呢?来看看最新风格的makefile吧。 
    objects = main.o kbd.o command.o display.o / 
              insert.o search.o files.o utils.o 

    edit : $(objects) 
            cc -o edit $(objects) 

    $(objects) : defs.h 
    kbd.o command.o files.o : command.h 
    display.o insert.o search.o files.o : buffer.h 

    .PHONY : clean 
    clean : 
            rm edit $(objects) 
这种风格,让我们的makefile变得很简单,但我们的文件依赖关系就显得有点凌乱了。鱼和熊掌不可兼得。还看你的喜好了。我是不喜欢这种风格的,一是文件的依赖关系看不清楚,二是如果文件一多,要加入几个新的.o文件,那就理不清楚了。 
1.7 清空目标文件的规则 
每个Makefile中都应该写一个清空目标文件(.o和执行文件)的规则,这不仅便于重编译,也很利于保持文件的清洁。这是一个“修养”(呵呵,还记得我的《编程修养》吗)。一般的风格都是: 
        clean: 
            rm edit $(objects) 
更为稳健的做法是: 
        .PHONY : clean 
        clean : 
                -rm edit $(objects) 
前面说过,.PHONY意思表示clean是一个“伪目标”,。而在rm命令前面加了一个小减号的意思就是,也许某些文件出现问题,但不要管,继续做后面的事。当然,clean的规则不要放在文件的开头,不然,这就会变成make的默认目标,相信谁也不愿意这样。不成文的规矩是——“clean从来都是放在文件的最后”。 

上面就是一个makefile的概貌,也是makefile的基础,下面还有很多makefile的相关细节,准备好了吗?准备好了就来。

 


2 Makefile 总述 
2.1 Makefile里有什么?
 
Makefile里主要包含了五个东西:显式规则、隐晦规则、变量定义、文件指示和注释。 

显式规则。显式规则说明了,如何生成一个或多的的目标文件。这是由Makefile的书写者明显指出,要生成的文件,文件的依赖文件,生成的命令。 
隐晦规则。由于我们的make有自动推导的功能,所以隐晦的规则可以让我们比较粗糙地简略地书写Makefile,这是由make所支持的。 
变量的定义。在Makefile中我们要定义一系列的变量,变量一般都是字符串,这个有点你C语言中的宏,当Makefile被执行时,其中的变量都会被扩展到相应的引用位置上。 
文件指示。其包括了三个部分,一个是在一个Makefile中引用另一个Makefile,就像C语言中的include一样;另一个是指根据某些情况指定Makefile中的有效部分,就像C语言中的预编译#if一样;还有就是定义一个多行的命令。有关这一部分的内容,我会在后续的部分中讲述。 
注释。Makefile中只有行注释,和UNIX的Shell脚本一样,其注释是用“#”字符,这个就像C/C++中的“//”一样。如果你要在你的Makefile中使用“#”字符,可以用反斜框进行转义,如:“/#”。 
最后,还值得一提的是,在Makefile中的命令,必须要以[Tab]键开始。 

2.2Makefile的文件名 
默认的情况下,make命令会在当前目录下按顺序找寻文件名为“GNUmakefile”、“makefile”、“Makefile”的文件,找到了解释这个文件。在这三个文件名中,最好使用“Makefile”这个文件名,因为,这个文件名第一个字符为大写,这样有一种显目的感觉。最好不要用“GNUmakefile”,这个文件是GNU的make识别的。有另外一些make只对全小写的“makefile”文件名敏感,但是基本上来说,大多数的make都支持“makefile”和“Makefile”这两种默认文件名。 
当然,你可以使用别的文件名来书写Makefile,比如:“Make.Linux”,“Make.Solaris”,“Make.AIX”等,如果要指定特定的Makefile,你可以使用make的“-f”和“--file”参数,如:make -f Make.Linux或make --file Make.AIX。 

2.3 引用其它的Makefile 
在Makefile使用include关键字可以把别的Makefile包含进来,这很像C语言的#include,被包含的文件会原模原样的放在当前文件的包含位置。include的语法是: 
include <filename>filename可以是当前操作系统Shell的文件模式(可以保含路径和通配符) 
在include前面可以有一些空字符,但是绝不能是[Tab]键开始。include和可以用一个或多个空格隔开。举个例子,你有这样几个Makefile:a.mk、b.mk、c.mk,还有一个文件叫foo.make,以及一个变量$(bar),其包含了e.mk和f.mk,那么,下面的语句: 

    include foo.make *.mk $(bar) 
等价于: 
    include foo.make a.mk b.mk c.mk e.mk f.mk 
make命令开始时,会把找寻include所指出的其它Makefile,并把其内容安置在当前的位置。就好像C/C++的#include指令一样。如果文件都没有指定绝对路径或是相对路径的话,make会在当前目录下首先寻找,如果当前目录下没有找到,那么,make还会在下面的几个目录下找: 

如果make执行时,有“-I”或“--include-dir”参数,那么make就会在这个参数所指定的目录下去寻找。 
如果目录/include(一般是:/usr/local/bin或/usr/include)存在的话,make也会去找。 
如果有文件没有找到的话,make会生成一条警告信息,但不会马上出现致命错误。它会继续载入其它的文件,一旦完成makefile的读取,make会再重试这些没有找到,或是不能读取的文件,如果还是不行,make才会出现一条致命信息。如果你想让make不理那些无法读取的文件,而继续执行,你可以在include前加一个减号“-”。如: 

-include <filename>其表示,无论include过程中出现什么错误,都不要报错继续执行。和其它版本make兼容的相关命令是sinclude,其作用和这一个是一样的。 
2.4 环境变量 MAKEFILES 
如果你的当前环境中定义了环境变量MAKEFILES,那么,make会把这个变量中的值做一个类似于include的动作。这个变量中的值是其它的Makefile,用空格分隔。只是,它和include不同的是,从这个环境变中引入的Makefile的“目标”不会起作用,如果环境变量中定义的文件发现错误,make也会不理。 
但是在这里我还是建议不要使用这个环境变量,因为只要这个变量一被定义,那么当你使用make时,所有的Makefile都会受到它的影响,这绝不是你想看到的。在这里提这个事,只是为了告诉大家,也许有时候你的Makefile出现了怪事,那么你可以看看当前环境中有没有定义这个变量。 

2.5 make的工作方式 
GNU的make工作时的执行步骤入下:(想来其它的make也是类似) 

读入所有的Makefile。 
读入被include的其它Makefile。 
初始化文件中的变量。 
推导隐晦规则,并分析所有规则。 
为所有的目标文件创建依赖关系链。 
根据依赖关系,决定哪些目标要重新生成。 
执行生成命令。 
1-5步为第一个阶段,6-7为第二个阶段。第一个阶段中,如果定义的变量被使用了,那么,make会把其展开在使用的位置。但make并不会完全马上展开,make使用的是拖延战术,如果变量出现在依赖关系的规则中,那么仅当这条依赖被决定要使用了,变量才会在其内部展开。 

当然,这个工作方式你不一定要清楚,但是知道这个方式你也会对make更为熟悉。有了这个基础,后续部分也就容易看懂了

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
作者:胡彦 2013-5-21 本文档可能有更新,更新版本请留意http://blog.csdn.net/huyansoft/article/details/8924624 一 目的:编写一个实际可用的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行,可看出多目标特征,执行make bigoutput或make littleoutput可看到结果: bigoutput littleoutput: defs.h pub.h @echo $@ $(subst output,OUTPUT,$@) $^ # $@指这个规则里所有目标的集合,$^指这个规则里所有依赖的集合。该行是把目标(bigoutput或littleoutput)里所有子串output替换成大写的OUTPUT (二)隐含规则 对makefile里下面4行,可看出make的隐含规则,执行foo可看到结果: 第3、4行表示由.c得到.o,第1、2行表示由.o得到可执行文件。 如果把第3、4行注释的话,效果一样。 即不写.o来自.c的规则,它会自动执行gcc -c -o foo.o foo.c这条命令,由.c编译出.o(其-c表示只编译不链接),然后自动执行gcc -o foo foo.o链接为可执行文件。 foo:foo.o gcc -o foo foo.o; ./foo foo.o:foo.c #注释该行看效果 gcc -c foo.c -o foo.o #注释该行看效果 (三)定义模式规则 下面定义了一个模式规则,即如何由.c文件生成.d文件的规则。 foobar: foo.d bar.d @echo complete generate foo.d and bar.d %.d: %.c #make会对当前目录下每个.c文件,依次做一次里面的命令,从而由每个.c文件生成对应.d文件。 @echo from $< to $@ g++ -MM $ $@ 假定当前目录下有2个.c文件:foo.c和bar.c(文件内容随意)。 验证方法有2种,都可: 1-运行make foo.d(或make bar.d),表示想要生成foo.d这个目标。 根据规则%.d: %.c,这时%匹配foo,这样%.c等于foo.c,即foo.d这个目标依赖于foo.c。 此时会自动执行该规则里的命令gcc -MM foo.c > foo.d,来生成foo.d这个目标。 2-运行make foobar,因为foobar依赖于foo.d和bar.d这2个文件,即会一次性生成这2个文件。 四 下面详述如何自动生成依赖性,从而实现本例的makefile。 (一) 本例使用了makefile的模式规则,目的是对当前目录下每个.c文件,生成其对应的.d文件,例如由main.c生成的.d文件内容为: main.o : main.c command.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" int main() { printf("##### Hello Makefile #####\n"); return 0; } 则执行gcc -MM main.c后,屏幕输出: main.o: main.c command.h 执行gcc -M main.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.h command.h (三) 可见,只要把这些行挪到makefile里,就能自动定义main.c的依赖是哪些文件了,做法是把命令的输出重定向到.d文件里:gcc -MM main.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后面可以跟若干个文件名,用空格分开,支持通配符,例如include foo.make *.mk。这里是把所有.d文件一次性全部include进来。注意该句要放在终极目标all的规则之后,否则.d文件里的规则会被误当作终极规则了。 (四) 现在main.c command.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报告up to date,没有像期望那样重编译main.o。 现在问题在于,main.d里的某个.h文件改了,没有重新生成main.d。进一步说,main.d里给出的每个依赖文件,任何一个改了,都要重新生成这个main.d。 所以main.d也要作为一个目标来生成,它的依赖应该是main.d里的每个依赖文件,也就是说make里要有这样的定义: main.d: main.c command.h 这时我们发现,main.d与main.o的依赖是完全相同的,可以利用make的多目标规则,把main.d与main.o这两个目标的定义合并为一句: main.o main.d: main.c command.h 现在,main.o: main.c command.h这一句我们已经有了,如何进一步得到main.o main.d: main.c command.h呢? (五) 解决方法是行内字符串替换,对main.o,取出其的子串main,加上.d后缀得到main.d,再插入到main.o后面。能实现这种替换功能的命令是sed。 实现的时候,先用gcc -MM命令生成临时文件main.d.temp,再用sed命令从该临时文件读出内容(用输出到最终文件main.d。 命令可以这么写: g++ -MM main.c > main.d.temp sed 's,\(main\)\.o[ :]*,\1.o main.d : ,g' main.d 其: sed 's,\(main\)\.o[ :]*,\1.o main.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.o main.d :,其\1会被替换为前面第1个\(和\)括起的内容,即main,这样replace值为main.o main.d : 这样该sed命令就实现了把main.o :替换为main.o main.d :的目的。 这两行实现了把临时文件main.d.temp的内容main.o : main.c command.h改为main.o main.d : main.c command.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.o main.d: main.c command.h 第二次make,假定改了command.h、在command.h加入#include "pub.h",这时再make: 1-include所有.d文件,例如include了main.d后,得到依赖规则: main.o main.d: main.c command.h 注意所有include命令是首先执行的,make会先把所有include进来,再生成依赖规则关系。 2-此时,根据依赖规则,由于command.h的文件戳改了,要重新生成main.o和main.d文件。 3-先调用gcc -c main.c -o main.o生成main.o, 再调用gcc -MM main.c > main.d重新生成main.d。 此时main.d的依赖文件里增加了pub.h: main.o main.d: main.c command.h pub.h 4-对其它依赖文件没改的.c(由其.d文件得到),不会重新编译.o和生成其.d。 5-最后会执行gcc $(objects) -o main生成最终可执行文件。 第三次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换成.d dependence:=$(sources:.c=.d) #所用的编译工具 CC=gcc #当$(objects)列表里所有文件都生成后,便可调用这里的 $(CC) $^ -o $@ 命令生成最终目标all了 #把all定义成第1个规则,使得可以把make all命令简写成make all: $(objects) $(CC) $^ -o $@ #这段是make的模式规则,指示如何由.c文件生成.o,即对每个.c文件,调用gcc -c XX.c -o XX.o命令生成对应的.o文件。 #如果不写这段也可以,因为make的隐含规则可以起到同样的效果 %.o: %.c $(CC) -c $< -o $@ include $(dependence) #注意该句要放在终极目标all的规则之后,否则.d文件里的规则会被误当作终极规则了 %.d: %.c set -e; rm -f $@; \ $(CC) -MM $(CPPFLAGS) $ $@.$$$$; \ sed 's,\($*\)\.o[ :]*,\1.o $@ : ,g' $@; \ rm -f $@.$$$$ .PHONY: clean #之所以把clean定义成伪目标,是因为这个目标并不对应实际的文件 clean: rm -f all $(objects) $(dependence) #清除所有临时文件:所有.o和.d。.$$已在每次使用后立即删除。-f参数表示被删文件不存在时不报错 (十一) 上面这个makefile已经能正常工作了(编译C程序),但如果要用它编译C++,变量CC值要改成g++,每个.c都要改成.cpp,有点繁琐。 现在我们继续完善它,使其同时支持C和C++,并支持二者的混合编译。 #一个实用的makefile,能自动编译当前目录下所有.c/.cpp源文件,支持二者混合编译 #并且当某个.c/.cpp、.h或依赖的源文件被修改后,仅重编涉及到的源文件,未涉及的不编译 #详解文档:http://blog.csdn.net/huyansoft/article/details/8924624 #author:胡彦 2013-5-21 #---------------------------------------------------------- #编译工具用g++,以同时支持C和C++程序,以及二者的混合编译 CC=g++ #使用$(winldcard *.c)来获取工作目录下的所有.c文件的列表 #sources:=main.cpp command.c #变量sources得到当前目录下待编译的.c/.cpp文件的列表,两次调用winldcard、结果连在一起即可 sources:=$(wildcard *.c) $(wildcard *.cpp) #变量objects得到待生成的.o文件的列表,把sources每个文件的扩展名换成.o即可。这里两次调用patsubst函数,第1次把sources所有.cpp换成.o,第2次把第1次结果里所有.c换成.o objects:=$(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个规则,使得可以把make all命令简写成make all: $(objects) $(CC) $(CPPFLAGS) $^ -o $@ @./$@ #编译后立即执行 #这段使用make的模式规则,指示如何由.c文件生成.o,即对每个.c文件,调用gcc -c XX.c -o XX.o命令生成对应的.o文件 #如果不写这段也可以,因为make的隐含规则可以起到同样的效果 %.o: %.c $(CC) $(CPPFLAGS) -c $< -o $@ #同上,指示如何由.cpp生成.o,可省略 %.o: %.cpp $(CC) $(CPPFLAGS) -c $< -o $@ #---------------------------------------------------------- include $(dependence) #注意该句要放在终极目标all的规则之后,否则.d文件里的规则会被误当作终极规则了 #因为这4行命令要多次凋用,定义成命令包以简化书写 define gen_dep set -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: clean clean: #.$$已在每次使用后立即删除。-f参数表示被删文件不存在时不报错 rm -f all $(objects) $(dependence) echo: #调试时显示一些变量的值 @echo sources=$(sources) @echo objects=$(objects) @echo dependence=$(dependence) @echo CPPFLAGS=$(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》后,再结合本文针对性的讲解,会有更实际的收获。 另一个学习资料是《GNU make v3.80文手册v1.5.pdf》,这个手册更详细,但较枯燥,不适合完整学习,通常是遇到问题再去查阅。 其它文章和代码请留意我的blog: http://blog.csdn.net/huyansoft [END]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值