makefile的语法和写法(2)

24 篇文章 0 订阅
5 篇文章 0 订阅

 3 Makefile书写规则 

-------------------------------------------------------------------------------- 
规则包含两个部分,一个是依赖关系,一个是生成目标的方法。 
在Makefile中,规则的顺序是很重要的,因为,Makefile中只应该有一个最终目标,其它的目标都是被这个目标所连带出来的,所以一定要让make知道你的最终目标是什么。一般来说,定义在Makefile中的目标可能会有很多,但是第一条规则中的目标将被确立为最终的目标。如果第一条规则中的目标有很多个,那么,第一个目标会成为最终的目标。make所完成的也就是这个目标。 

好了,还是让我们来看一看如何书写规则。 

3.1 规则举例 
foo.o : foo.c defs.h      # foo模块 
            cc -c -g foo.c 
看到这个例子,各位应该不是很陌生了,前面也已说过,foo.o是我们的目标,foo.c和defs.h是目标所依赖的源文件,而只有一个命令“cc -c -g foo.c”(以Tab键开头)。这个规则告诉我们两件事: 

文件的依赖关系,foo.o依赖于foo.c和defs.h的文件,如果foo.c和defs.h的文件日期要比foo.o文件日期要新,或是foo.o不存在,那么依赖关系发生。 
如果生成(或更新)foo.o文件。也就是那个cc命令,其说明了,如何生成foo.o这个文件。(当然foo.c文件include了defs.h文件) 

3.2 规则的语法 
      targets : prerequisites 
        command 
        ... 
或是这样: 
      targets : prerequisites ; command 
            command 
            ... 
targets是文件名,以空格分开,可以使用通配符。一般来说,我们的目标基本上是一个文件,但也有可能是多个文件。 
command是命令行,如果其不与“target:prerequisites”在一行,那么,必须以[Tab键]开头,如果和prerequisites在一行,那么可以用分号做为分隔。(见上) 

prerequisites也就是目标所依赖的文件(或依赖目标)。如果其中的某个文件要比目标文件要新,那么,目标就被认为是“过时的”,被认为是需要重生成的。这个在前面已经讲过了。 

如果命令太长,你可以使用反斜框(‘/’)作为换行符。make对一行上有多少个字符没有限制。规则告诉make两件事,文件的依赖关系和如何成成目标文件。 

一般来说,make会以UNIX的标准Shell,也就是/bin/sh来执行命令。 

3.3 在规则中使用通配符 
如果我们想定义一系列比较类似的文件,我们很自然地就想起使用通配符。make支持三各通配符:“*”,“?”和“[...]”。这是和Unix的B-Shell是相同的。 

波浪号(“~”)字符在文件名中也有比较特殊的用途。如果是“~/test”,这就表示当前用户的$HOME目录下的test目录。而“~hchen/test”则表示用户hchen的宿主目录下的test目录。(这些都是Unix下的小知识了,make也支持)而在Windows或是MS-DOS下,用户没有宿主目录,那么波浪号所指的目录则根据环境变量“HOME”而定。 

通配符代替了你一系列的文件,如“*.c”表示所以后缀为c的文件。一个需要我们注意的是,如果我们的文件名中有通配符,如:“*”,那么可以用转义字符“/”,如“/*”来表示真实的“*”字符,而不是任意长度的字符串。 

好吧,还是先来看几个例子吧: 

    clean: 
        rm -f *.o 
上面这个例子我不不多说了,这是操作系统Shell所支持的通配符。这是在命令中的通配符。 
    print: *.c 
        lpr -p $? 
        touch print 
上面这个例子说明了通配符也可以在我们的规则中,目标print依赖于所有的[.c]文件。其中的“$?”是一个自动化变量,我会在后面给你讲述。 
    objects = *.o 
上面这个例子,表示了,通符同样可以用在变量中。并不是说[*.o]会展开,不!objects的值就是“*.o”。Makefile中的变量其实就是C/C++中的宏。如果你要让通配符在变量中展开,也就是让objects的值是所有[.o]的文件名的集合,那么,你可以这样: 
    objects := $(wildcard *.o) 
这种用法由关键字“wildcard”指出,关于Makefile的关键字,我们将在后面讨论。 
3.4 文件搜寻 
在一些大的工程中,有大量的源文件,我们通常的做法是把这许多的源文件分类,并存放在不同的目录中。所以,当make需要去找寻文件的依赖关系时,你可以在文件前加上路径,但最好的方法是把一个路径告诉make,让make在自动去找。 

Makefile文件中的特殊变量“VPATH”就是完成这个功能的,如果没有指明这个变量,make只会在当前的目录中去找寻依赖文件和目标文件。如果定义了这个变量,那么,make就会在当当前目录找不到的情况下,到所指定的目录中去找寻文件了。 

    VPATH = src:../headers 
上面的的定义指定两个目录,“src”和“../headers”,make会按照这个顺序进行搜索。目录由“冒号”分隔。(当然,当前目录永远是最高优先搜索的地方) 
另一个设置文件搜索路径的方法是使用make的“vpath”关键字(注意,它是全小写的),这不是变量,这是一个make的关键字,这和上面提到的那个VPATH变量很类似,但是它更为灵活。它可以指定不同的文件在不同的搜索目录中。这是一个很灵活的功能。它的使用方法有三种: 

vpath < pattern> < directories> 
为符合模式 < pattern>的文件指定搜索目录 < directories>。 
vpath < pattern> 
清除符合模式 < pattern>的文件的搜索目录。 
vpath 
清除所有已被设置好了的文件搜索目录。 
vapth使用方法中的 < pattern>需要包含“%”字符。“%”的意思是匹配零或若干字符,例如,“%.h”表示所有以“.h”结尾的文件。 < pattern>指定了要搜索的文件集,而 < directories>则指定了的文件集的搜索的目录。例如: 

    vpath %.h ../headers 
该语句表示,要求make在“../headers”目录下搜索所有以“.h”结尾的文件。(如果某文件在当前目录没有找到的话) 
我们可以连续地使用vpath语句,以指定不同搜索策略。如果连续的vpath语句中出现了相同的 < pattern>,或是被重复了的 < pattern>,那么,make会按照vpath语句的先后顺序来执行搜索。如: 

    vpath %.c foo 
    vpath %  blish 
    vpath %.c bar 
其表示“.c”结尾的文件,先在“foo”目录,然后是“blish”,最后是“bar”目录。 
    vpath %.c foo:bar 
    vpath %  blish 
而上面的语句则表示“.c”结尾的文件,先在“foo”目录,然后是“bar”目录,最后才是“blish”目录。

 

3.5 伪目标 
最早先的一个例子中,我们提到过一个“clean”的目标,这是一个“伪目标”, 

    clean: 
            rm *.o temp 
正像我们前面例子中的“clean”一样,即然我们生成了许多文件编译文件,我们也应该提供一个清除它们的“目标”以备完整地重编译而用。 (以“make clean”来使用该目标) 
因为,我们并不生成“clean”这个文件。“伪目标”并不是一个文件,只是一个标签,由于“伪目标”不是文件,所以make无法生成它的依赖关系和决定它是否要执行。我们只有通过显示地指明这个“目标”才能让其生效。当然,“伪目标”的取名不能和文件名重名,不然其就失去了“伪目标”的意义了。 

当然,为了避免和文件重名的这种情况,我们可以使用一个特殊的标记“.PHONY”来显示地指明一个目标是“伪目标”,向make说明,不管是否有这个文件,这个目标就是“伪目标”。 

    .PHONY : clean 
只要有这个声明,不管是否有“clean”文件,要运行“clean”这个目标,只有“make clean”这样。于是整个过程可以这样写: 
    .PHONY: clean 
    clean: 
            rm *.o temp 
伪目标一般没有依赖的文件。但是,我们也可以为伪目标指定所依赖的文件。伪目标同样可以作为“默认目标”,只要将其放在第一个。一个示例就是,如果你的Makefile需要一口气生成若干个可执行文件,但你只想简单地敲一个make完事,并且,所有的目标文件都写在一个Makefile中,那么你可以使用“伪目标”这个特性: 
    all : prog1 prog2 prog3 
    .PHONY : all 

    prog1 : prog1.o utils.o 
            cc -o prog1 prog1.o utils.o 

    prog2 : prog2.o 
            cc -o prog2 prog2.o 

    prog3 : prog3.o sort.o utils.o 
            cc -o prog3 prog3.o sort.o utils.o 
我们知道,Makefile中的第一个目标会被作为其默认目标。我们声明了一个“all”的伪目标,其依赖于其它三个目标。由于伪目标的特性是,总是被执行的,所以其依赖的那三个目标就总是不如“all”这个目标新。所以,其它三个目标的规则总是会被决议。也就达到了我们一口气生成多个目标的目的。“.PHONY : all”声明了“all”这个目标为“伪目标”。 
随便提一句,从上面的例子我们可以看出,目标也可以成为依赖。所以,伪目标同样也可成为依赖。看下面的例子: 

    .PHONY: cleanall cleanobj cleandiff 

    cleanall : cleanobj cleandiff 
            rm program 

    cleanobj : 
            rm *.o 

    cleandiff : 
            rm *.diff 
“make clean”将清除所有要被清除的文件。“cleanobj”和“cleandiff”这两个伪目标有点像“子程序”的意思。我们可以输入“make cleanall”和“make cleanobj”和“make cleandiff”命令来达到清除不同种类文件的目的 

3.6 多目标 
Makefile的规则中的目标可以不止一个,其支持多目标,有可能我们的多个目标同时依赖于一个文件,并且其生成的命令大体类似。于是我们就能把其合并起来。当然,多个目标的生成规则的执行命令是同一个,这可能会可我们带来麻烦,不过好在我们的可以使用一个自动化变量“$@”(关于自动化变量,将在后面讲述),这个变量表示着目前规则中所有的目标的集合,这样说可能很抽象,还是看一个例子吧。 

    bigoutput littleoutput : text.g 
            generate text.g -$(subst output,,$@) > $@ 
    上述规则等价于: 

    bigoutput : text.g 
            generate text.g -big > bigoutput 
    littleoutput : text.g 
            generate text.g -little > littleoutput 
其中,-$(subst output,,$@)中的“$”表示执行一个Makefile的函数,函数名为subst,后面的为参数。关于函数,将在后面讲述。这里的这个函数是截取字符串的意思,“$@”表示目标的集合,就像一个数组,“$@”依次取出目标,并执于命令。 


3.7 静态模式 
静态模式可以更加容易地定义多目标的规则,可以让我们的规则变得更加的有弹性和灵活。我们还是先来看一下语法: 

<targets ...>: <target-pattern>: <prereq-patterns ...> 
    <commands> 
...targets定义了一系列的目标文件,可以有通配符。是目标的一个集合。 

target-parrtern是指明了targets的模式,也就是的目标集模式。 

prereq-parrterns是目标的依赖模式,它对target-parrtern形成的模式再进行一次依赖目标的定义。 

这样描述这三个东西,可能还是没有说清楚,还是举个例子来说明一下吧。如果我们的 <target-parrtern>定义成“%.o”,意思是我们的集合中都是以“.o”结尾的,而如果我们的 <prereq-parrterns>定义成“%.c”,意思是对 <target-parrtern>所形成的目标集进行二次定义,其计算方法是,取 <target-parrtern>模式中的“%”(也就是去掉了[.o]这个结尾),并为其加上[.c]这个结尾,形成的新集合。 

所以,我们的“目标模式”或是“依赖模式”中都应该有“%”这个字符,如果你的文件名中有“%”那么你可以使用反斜杠“/”进行转义,来标明真实的“%”字符。 

看一个例子: 

    objects = foo.o bar.o 

    all: $(objects) 

    $(objects): %.o: %.c 
            $(CC) -c $(CFLAGS) $ < -o $@ 

上面的例子中,指明了我们的目标从$object中获取,“%.o”表明要所有以“.o”结尾的目标,也就是“foo.o bar.o”,也就是变量$object集合的模式,而依赖模式“%.c”则取模式“%.o”的“%”,也就是“foo bar”,并为其加下“.c”的后缀,于是,我们的依赖目标就是“foo.c bar.c”。而命令中的“$ <”和“$@”则是自动化变量,“$ <”表示所有的依赖目标集(也就是“foo.c bar.c”),“$@”表示目标集(也褪恰癴oo.o bar.o”)。于是,上面的规则展开后等价于下面的规则: 
    foo.o : foo.c 
            $(CC) -c $(CFLAGS) foo.c -o foo.o 
    bar.o : bar.c 
            $(CC) -c $(CFLAGS) bar.c -o bar.o 
试想,如果我们的“%.o”有几百个,那种我们只要用这种很简单的“静态模式规则”就可以写完一堆规则,实在是太有效率了。“静态模式规则”的用法很灵活,如果用得好,那会一个很强大的功能。再看一个例子: 
    files = foo.elc bar.o lose.o 

    $(filter %.o,$(files)): %.o: %.c 
            $(CC) -c $(CFLAGS) $ < -o $@ 
    $(filter %.elc,$(files)): %.elc: %.el 
            emacs -f batch-byte-compile $ < 
$(filter %.o,$(files))表示调用Makefile的filter函数,过滤“$filter”集,只要其中模式为“%.o”的内容。其的它内容,我就不用多说了吧。这个例字展示了Makefile中更大的弹性。 

3.8 自动生成依赖性 
在Makefile中,我们的依赖关系可能会需要包含一系列的头文件,比如,如果我们的main.c中有一句“#include "defs.h"”,那么我们的依赖关系应该是: 

    main.o : main.c defs.h 
但是,如果是一个比较大型的工程,你必需清楚哪些C文件包含了哪些头文件,并且,你在加入或删除头文件时,也需要小心地修改Makefile,这是一个很没有维护性的工作。为了避免这种繁重而又容易出错的事情,我们可以使用C/C++编译的一个功能。大多数的C/C++编译器都支持一个“-M”的选项,即自动找寻源文件中包含的头文件,并生成一个依赖关系。例如,如果我们执行下面的命令: 
    cc -M main.c 
其输出是: 
    main.o : main.c defs.h 
于是由编译器自动生成的依赖关系,这样一来,你就不必再手动书写若干文件的依赖关系,而由编译器自动生成了。需要提醒一句的是,如果你使用GNU的C/C++编译器,你得用“-MM”参数,不然,“-M”参数会把一些标准库的头文件也包含进来。 
gcc -M main.c的输出是: 

    main.o: main.c defs.h /usr/include/stdio.h /usr/include/features.h / 
        /usr/include/sys/cdefs.h /usr/include/gnu/stubs.h / 
        /usr/lib/gcc-lib/i486-suse-linux/2.95.3/include/stddef.h / 
        /usr/include/bits/types.h /usr/include/bits/pthreadtypes.h / 
        /usr/include/bits/sched.h /usr/include/libio.h / 
        /usr/include/_G_config.h /usr/include/wchar.h / 
        /usr/include/bits/wchar.h /usr/include/gconv.h / 
        /usr/lib/gcc-lib/i486-suse-linux/2.95.3/include/stdarg.h / 
        /usr/include/bits/stdio_lim.h 

gcc -MM main.c的输出则是: 
    main.o: main.c defs.h 
那么,编译器的这个功能如何与我们的Makefile联系在一起呢。因为这样一来,我们的Makefile也要根据这些源文件重新生成,让Makefile自已依赖于源文件?这个功能并不现实,不过我们可以有其它手段来迂回地实现这一功能。GNU组织建议把编译器为每一个源文件的自动生成的依赖关系放到一个文件中,为每一个“name.c”的文件都生成一个“name.d”的Makefile文件,[.d]文件中就存放对应[.c]文件的依赖关系。 
于是,我们可以写出[.c]文件和[.d]文件的依赖关系,并让make自动更新或自成[.d]文件,并把其包含在我们的主Makefile中,这样,我们就可以自动化地生成每个文件的依赖关系了。 

这里,我们给出了一个模式规则来产生[.d]文件: 

    %.d: %.c 
            @set -e; rm -f $@; / 
            $(CC) -M $(CPPFLAGS) $ < > $@.$$$$; / 
            sed 's,/($*/)/.o[ :]*,/1.o $@ : ,g' < $@.$$$$ > $@; / 
            rm -f $@.$$$$ 
这个规则的意思是,所有的[.d]文件依赖于[.c]文件,“rm -f $@”的意思是删除所有的目标,也就是[.d]文件,第二行的意思是,为每个依赖文件“$ <”,也就是[.c]文件生成依赖文件,“$@”表示模式“%.d”文件,如果有一个C文件是name.c,那么“%”就是“name”,“$$$$”意为一个随机编号,第二行生成的文件有可能是“name.d.12345”,第三行使用sed命令做了一个替换,关于sed命令的用法请参看相关的使用文档。第四行就是删除临时文件。 

总而言之,这个模式要做的事就是在编译器生成的依赖关系中加入[.d]文件的依赖,即把依赖关系: 

    main.o : main.c defs.h 
转成: 
    main.o main.d : main.c defs.h 
于是,我们的[.d]文件也会自动更新了,并会自动生成了,当然,你还可以在这个[.d]文件中加入的不只是依赖关系,包括生成的命令也可一并加入,让每个[.d]文件都包含一个完赖的规则。一旦我们完成这个工作,接下来,我们就要把这些自动生成的规则放进我们的主Makefile中。我们可以使用Makefile的“include”命令,来引入别的Makefile文件(前面讲过),例如: 
    sources = foo.c bar.c 

    include $(sources:.c=.d) 
上述语句中的“$(sources:.c=.d)”中的“.c=.d”的意思是做一个替换,把变量$(sources)所有[.c]的字串都替换成[.d],关于这个“替换”的内容,在后面我会有更为详细的讲述。当然,你得注意次序,因为include是按次来载入文件,最先载入的[.d]文件中的目标会成为默认目标

 

4 Makefile 书写命令 

-------------------------------------------------------------------------------- 

每条规则中的命令和操作系统Shell的命令行是一致的。make会一按顺序一条一条的执行命令,每条命令的开头必须以[Tab]键开头,除非,命令是紧跟在依赖规则后面的分号后的。在命令行之间中的空格或是空行会被忽略,但是如果该空格或空行是以Tab键开头的,那么make会认为其是一个空命令。 

我们在UNIX下可能会使用不同的Shell,但是make的命令默认是被“/bin/sh”——UNIX的标准Shell解释执行的。除非你特别指定一个其它的Shell。Makefile中,“#”是注释符,很像C/C++中的“//”,其后的本行字符都被注释。 


4.1 显示命令 
通常,make会把其要执行的命令行在命令执行前输出到屏幕上。当我们用“@”字符在命令行前,那么,这个命令将不被make显示出来,最具代表性的例子是,我们用这个功能来像屏幕显示一些信息。如: 

    @echo 正在编译XXX模块...... 
当make执行时,会输出“正在编译XXX模块......”字串,但不会输出命令,如果没有“@”,那么,make将输出: 
    echo 正在编译XXX模块...... 
    正在编译XXX模块...... 
如果make执行时,带入make参数“-n”或“--just-print”,那么其只是显示命令,但不会执行命令,这个功能很有利于我们调试我们的Makefile,看看我们书写的命令是执行起来是什么样子的或是什么顺序的。 
而make参数“-s”或“--slient”则是全面禁止命令的显示。 


4.2 命令执行 
当依赖目标新于目标时,也就是当规则的目标需要被更新时,make会一条一条的执行其后的命令。需要注意的是,如果你要让上一条命令的结果应用在下一条命令时,你应该使用分号分隔这两条命令。比如你的第一条命令是cd命令,你希望第二条命令得在cd之后的基础上运行,那么你就不能把这两条命令写在两行上,而应该把这两条命令写在一行上,用分号分隔。如: 

    示例一: 
        exec: 
                cd /home/hchen 
                pwd 

    示例二: 
        exec: 
                cd /home/hchen; pwd 
当我们执行“make exec”时,第一个例子中的cd没有作用,pwd会打印出当前的Makefile目录,而第二个例子中,cd就起作用了,pwd会打印出“/home/hchen”。 

make一般是使用环境变量SHELL中所定义的系统Shell来执行命令,默认情况下使用UNIX的标准Shell——/bin/sh来执行命令。但在MS-DOS下有点特殊,因为MS-DOS下没有SHELL环境变量,当然你也可以指定。如果你指定了UNIX风格的目录形式,首先,make会在SHELL所指定的路径中找寻命令解释器,如果找不到,其会在当前盘符中的当前目录中寻找,如果再找不到,其会在PATH环境变量中所定义的所有路径中寻找。MS-DOS中,如果你定义的命令解释器没有找到,其会给你的命令解释器加上诸如“.exe”、“.com”、“.bat”、“.sh”等后缀。 


4.3 命令出错 
每当命令运行完后,make会检测每个命令的返回码,如果命令返回成功,那么make会执行下一条命令,当规则中所有的命令成功返回后,这个规则就算是成功完成了。如果一个规则中的某个命令出错了(命令退出码非零),那么make就会终止执行当前规则,这将有可能终止所有规则的执行。 

有些时候,命令的出错并不表示就是错误的。例如mkdir命令,我们一定需要建立一个目录,如果目录不存在,那么mkdir就成功执行,万事大吉,如果目录存在,那么就出错了。我们之所以使用mkdir的意思就是一定要有这样的一个目录,于是我们就不希望mkdir出错而终止规则的运行。 

为了做到这一点,忽略命令的出错,我们可以在Makefile的命令行前加一个减号“-”(在Tab键之后),标记为不管命令出不出错都认为是成功的。如: 

  clean: 
            -rm -f *.o 
还有一个全局的办法是,给make加上“-i”或是“--ignore-errors”参数,那么,Makefile中所有命令都会忽略错误。而如果一个规则是以“.IGNORE”作为目标的,那么这个规则中的所有命令将会忽略错误。这些是不同级别的防止命令出错的方法,你可以根据你的不同喜欢设置。 
还有一个要提一下的make的参数的是“-k”或是“--keep-going”,这个参数的意思是,如果某规则中的命令出错了,那么就终目该规则的执行,但继续执行其它规则。 

4.4 嵌套执行make 
在一些大的工程中,我们会把我们不同模块或是不同功能的源文件放在不同的目录中,我们可以在每个目录中都书写一个该目录的Makefile,这有利于让我们的Makefile变得更加地简洁,而不至于把所有的东西全部写在一个Makefile中,这样会很难维护我们的Makefile,这个技术对于我们模块编译和分段编译有着非常大的好处。 

例如,我们有一个子目录叫subdir,这个目录下有个Makefile文件,来指明了这个目录下文件的编译规则。那么我们总控的Makefile可以这样书写: 

    subsystem: 
            cd subdir && $(MAKE) 

其等价于: 

    subsystem: 
            $(MAKE) -C subdir 
定义$(MAKE)宏变量的意思是,也许我们的make需要一些参数,所以定义成一个变量比较利于维护。这两个例子的意思都是先进入“subdir”目录,然后执行make命令。 

我们把这个Makefile叫做“总控Makefile”,总控Makefile的变量可以传递到下级的Makefile中(如果你显示的声明),但是不会覆盖下层的Makefile中所定义的变量,除非指定了“-e”参数。 

如果你要传递变量到下级Makefile中,那么你可以使用这样的声明: 
export <variable ...> 
如果你不想让某些变量传递到下级Makefile中,那么你可以这样声明: 
unexport <variable ...> 
如: 
    
    示例一: 

        export variable = value 

        其等价于: 

        variable = value 
        export variable 

        其等价于: 

        export variable := value 

        其等价于: 

        variable := value 
        export variable 

    示例二: 

        export variable += value 

        其等价于: 

        variable += value 
        export variable 
    
如果你要传递所有的变量,那么,只要一个export就行了。后面什么也不用跟,表示传递所有的变量。 

需要注意的是,有两个变量,一个是SHELL,一个是MAKEFLAGS,这两个变量不管你是否export,其总是要传递到下层Makefile中,特别是MAKEFILES变量,其中包含了make的参数信息,如果我们执行“总控Makefile”时有make参数或是在上层Makefile中定义了这个变量,那么MAKEFILES变量将会是这些参数,并会传递到下层Makefile中,这是一个系统级的环境变量。 

但是make命令中的有几个参数并不往下传递,它们是“-C”,“-f”,“-h”“-o”和“-W”(有关Makefile参数的细节将在后面说明),如果你不想往下层传递参数,那么,你可以这样来: 
    
    subsystem: 
            cd subdir && $(MAKE) MAKEFLAGS= 
    
如果你定义了环境变量MAKEFLAGS,那么你得确信其中的选项是大家都会用到的,如果其中有“-t”,“-n”,和“-q”参数,那么将会有让你意想不到的结果,或许会让你异常地恐慌。 

还有一个在“嵌套执行”中比较有用的参数,“-w”或是“--print-directory”会在make的过程中输出一些信息,让你看到目前的工作目录。比如,如果我们的下级make目录是“/home/hchen/gnu/make”,如果我们使用“make -w”来执行,那么当进入该目录时,我们会看到: 
    
    make: Entering directory `/home/hchen/gnu/make'. 
    
而在完成下层make后离开目录时,我们会看到: 
    
    make: Leaving directory `/home/hchen/gnu/make' 

当你使用“-C”参数来指定make下层Makefile时,“-w”会被自动打开的。如果参数中有“-s”(“--slient”)或是“--no-print-directory”,那么,“-w”总是失效的。 
---++++4.5 定义命令包 

如果Makefile中出现一些相同命令序列,那么我们可以为这些相同的命令序列定义一个变量。定义这种命令序列的语法以“define”开始,以“endef”结束,如: 
    define run-yacc 
    yacc $(firstword $^) 
    mv y.tab.c $@ 
    endef 

这里,“run-yacc”是这个命令包的名字,其不要和Makefile中的变量重名。在“define”和“endef”中的两行就是命令序列。这个命令包中的第一个命令是运行Yacc程序,因为Yacc程序总是生成“y.tab.c”的文件,所以第二行的命令就是把这个文件改改名字。还是把这个命令包放到一个示例中来看看吧。 
    foo.c : foo.y 
            $(run-yacc) 

我们可以看见,要使用这个命令包,我们就好像使用变量一样。在这个命令包的使用中,命令包“run-yacc”中的“$^”就是“foo.y”,“$@”就是“foo.c”(有关这种以“$”开头的特殊变量,我们会在后面介绍),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、付费专栏及课程。

余额充值