Makefile入门

来源:互联网 发布:linux 查看进程内存 编辑:程序博客网 时间:2024/05/21 17:59

本文是学习了陈皓老师的《跟我一起写Makefile》后结合自己的例子整理的文章,原文在此跟我一起写Makefile,感谢耗子老师

0.1 概念及基础准备

makefile定义了一系列的规则来制定,哪些文件需要先编译,哪些需要后编译,哪些文件需要重新编译,甚至基于更复杂的功能操作。总之,makefile是实现自动化编译

  • 基本知识:
    1. 程序的编译:将源文件编译成中间代码文件,如将.c文件编译成.o文件;编译时,会检查语法的正确、函数与变量的声明正确
    2. 程序的链接:将大量的中间代码文件合成可执行程序的过程;链接时,主要是链接函数和全局变量,会在所有的obj文件中寻找函数的实现

本文示例:
目录结构

|--- include|    |--- bar.h|    |--- foo.h|--- Makefile|--- src     |--- bar.c     |--- foo.c     |--- main.c

bar.h

#ifndef __BAR_H#define __BAR_Hextern void bar(void);#endif

foo.h

#ifndef __FOO_H#define __FOO_Hextern void foo(void);#endif

bar.c

#include <stdio.h>void bar(void){    printf("Bar Func\r\n");}

foo.c

#include <stdio.h>void foo(void){    printf("Foo Func\r\n");}

main.c

#include <stdio.h>#include "bar.h"#include "foo.h"int main(void){    printf("Foo Func\r\n");    bar();    foo();    return 0;}

1 makefile介绍

1.1 makefile的规则

<target...>:<prerequisites ...>[tab]<command>    ...    ...

target也就是一个目标文件,可以是Object File,也可以是执行文件。还可以是一个标签(Label),对于标签这种特性,在后续的“伪目标”章节中会有叙述。

prerequisites就是前置条件,要生成那个target所需要依赖的文件或是目标。

command也就是make需要执行的命令,一定要以一个tab键开头。(任意的Shell命令)

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

1.2 示例

如果要编译基础工作中的工程,生成一个prog程序,我们的Makefile应该是下面的这个样子的。

实现的目标:
1. 如果这个工程没有编译过,那么我们的所有C文件都要编译并被链接。
2. 如果这个工程的某几个C文件被修改,那么我们只编译被修改的C文件,并链接目标程序。
3. 如果这个工程的头文件被改变了,那么我们需要编译引用了这几个头文件的C文件,并链接目标程序。

#Makefileprog : main.o bar.o foo.o     gcc -o prog main.o bar.o foo.omain.o : src/main.c include/bar.h include/foo.h    gcc -I include -c src/main.cbar.o : src/bar.c    gcc -c src/bar.cfoo.o : src/foo.c    gcc -c src/foo.clean :     rm prog main.o bar.o foo.o 

1.3 make的工作流程

  1. make会在当前目录下找名字叫“Makefile”或“makefile”的文件。
  2. 如果找到,它会找文件中的第一个目标文件(target),在上面的例子中,他会找到“prog”这个文件,并把这个文件作为最终的目标文件。
  3. 如果prog文件不存在,或是prog所依赖的后面的 .o 文件的文件修改时间要比prog这个文件新,那么,他就会执行后面所定义的命令来生成prog这个文件。
  4. 如果prog所依赖的.o文件也存在,那么make会在当前文件中找目标为.o文件的依赖性,如果找到则再根据那一个规则生成.o文件。(这有点像一个堆栈的过程)
  5. 当然,你的C文件和H文件是存在的啦,于是make会生成 .o 文件,然后再用 .o 文件声明make的终极任务,也就是执行文件prog了。

上面的例子中,clean是一个操作命令,称之为标签,后面并没有依赖文件,所以clean是一个伪目标,所以不会执行,需要显式调用make clean,才会执行clean下面的命令

1.4 makefile中使用变量

在上面的例子中,有很多重复的文件名,例如main.o bar.o foo.o,如果新增一个文件的话,就要把整个文件中需要添加的地方都要改一遍,工作量大,也很容易出错,这时可以使用变量,类似于c语言中的宏,只需要定义一次,后面需要的地方用变量名就可以了,修改的时候只在变量初始化的位置更改就可以。

objects = main.o bar.o foo.o

于是上面例子中的makefile可以简化为:

#Makefileobjects = main.o bar.o foo.oprog : $(objects)     gcc -o prog $(objects)main.o : src/main.c include/bar.h include/foo.h    gcc -I include -c src/main.cbar.o : src/bar.c    gcc -c src/bar.cfoo.o : src/foo.c    gcc -c src/foo.clean :     rm prog $(objects)

使用$()形式引用变量

1.5 自动推导

只要make看到一个[.o]文件,它就会自动的把[.c]文件加在依赖关系中,如果make找到一个whatever.o,那么whatever.c,就会是whatever.o的依赖文件。并且 gcc -c whatever.c 也会被推导出来,于是,我们的makefile再也不用写得这么复杂。我们的是新的makefile又出炉了。

#Makefileobjects = main.o bar.o foo.oVPATH = src:includeprog : $(objects)     gcc -o prog $(objects)main.o : bar.h foo.h    gcc -I include -c src/main.cbar.o :    gcc -c src/bar.cfoo.o :    gcc -c src/foo.c.PHONY clean : clean    rm $(objects)

上面用到了VPATH变量,类似IDE中的文件搜索目录,后面会有解释,可以先不管,这里只要注意我们不需要写明src/main.c了,因为可以从main.o自动推导出来

上面使用了.PHONY,如果文件夹中正好有个文件叫clean,因为Make发现clean文件已经存在,就认为没有必要重新构建了,makefile中的clean不会执行,为了避免这种情况,可以使用内置目标名.PHONY,这样clean就被声明为伪目标,make就不会去检查是否存在一个叫clean的文件

makefile详解

2.1 makefile里有什么?

  1. 显式规则。

    显式规则说明了,如何生成一个或多的的目标文件。这是由Makefile的书写者明显指出,要生成的文件,文件的依赖文件,生成的命令。

  2. 隐晦规则。

    由于我们的make有自动推导的功能,所以隐晦的规则可以让我们比较粗糙地简略地书写Makefile,这是由make所支持的。

  3. 变量的定义。在Makefile中我们要定义一系列的变量,变量一般都是字符串,这个有点你C语言中的宏,当Makefile被执行时,其中的变量都会被扩展到相应的引用位置上。
  4. 文件指示。

    其包括了三个部分,一个是在一个Makefile中引用另一个Makefile,就像C语言中的include一样;另一个是指根据某些情况指定Makefile中的有效部分,就像C语言中的预编译#if一样;还有就是定义一个多行的命令。有关这一部分的内容,我会在后续的部分中讲述。

  5. 注释。

    Makefile中只有行注释,和UNIX的Shell脚本一样,其注释是用“#”字符,这个就像C\/C++中的“//”一样。如果你要在你的Makefile中使用“#”字符,可以用反斜框进行转义,如:“#”

2.2 makefile的文件名

默认情况下,make命令会在当前目录下按顺序寻找文件名为“GNUmakefile”、“makefile”、“Makefile”的文件,最好使用Makefile这个名字,因为是大写,比较醒目,GNUmakefile只被GNU的make识别,不通用。

2.3 引用其它的makefile

makefile中的可以使用include关键字把别的makefile文件包含进来,include的语法为:

include <filename>  #filename可以包含路径和通配符

make命令开始时,会把找寻include所指出的其它Makefile,并把其内容安置在当前的位置。就好像C\/C++的#include指令一样。如果文件都没有指定绝对路径或是相对路径的话,make会在当前目录下首先寻找,如果当前目录下没有找到,那么,make还会在下面的几个目录下找:
1. 如果make执行时,有“-I”或“–include-dir”参数,那么make就会在这个参数所指定的目录下去寻找。还记得前面我们的命令中

gcc -I include -c src/main.c 

中有-I,是告诉gcc如果main.c中的头文件在当前目录找不到的话就到include文件夹中寻找,make使用-I参数和这个功能一致。
2. 如果目录/include(一般是:/usr/local/bin或/usr/include)存在的话,make也会去找。

2.4 make的工作方式

  1. 读入所有的Makefile
  2. 读入被include的其他Makefile
  3. 初始化文件中变量
  4. 推导隐晦规则,并分析所有规则
  5. 为所有目标文件创建依赖关系链
  6. 根据依赖关系,决定哪些目标文件要重新生成
  7. 执行生成命令

1-5为第一阶段,建立依赖链;6-7为第二阶段,执行命令生成目标

Makefile的书写规则

规则包含两个部分,一个是依赖关系,一个是生成目标的方法。

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

示例:

foo.o : src/foo.c    gcc -I include -c src/foo.c
  1. 文件的依赖关系,foo.o依赖于foo.c文件,如果foo.c文件日期要比foo.o文件日期要新,或是foo.o不存在,那么依赖关系发生。
  2. 如何生成(或更新)foo.o文件。也就是下面的gcc命令,其说明了,如何生成foo.o这个文件。

3.1 在规则中使用通配符

make中支持三个通配符:”*”, “?” 和 “[…]”
1. “.o 代表所有后缀为.o的文件
注意,在变量中使用”*”的话,例如 obj = *.o,并不是把文件夹中所有.o的文件名赋给obj,这个是代表obj是“*.o”
2. “?”
3. “[…]”

3.2 文件搜寻

在一个工程中,由于很多源文件分布在不同的目录中,我们希望把路径告诉make,让它自动搜寻

Makefile中VPATH特殊变量可以完成这个功能,如果没有指明这个变量,make会在当前的目录中去寻找依赖文件和目标文件;如果定义了这个变量,会现在当前目录找,找不到就到VPATH路径中寻找。

例如

VPATH = src:include

路径之间用冒号隔开 注意是冒号

Makefile代码

# Makefile CC = gccCFLAGS = -I includeobjects = main.o bar.o foo.oVPATH = src:includeprog : $(objects)     $(CC) -o prog $(objects)main.o : bar.h foo.h    $(CC) $(CFLAGS) -c src/main.cbar.o :    $(CC) -c src/bar.cfoo.o :    $(CC) -c src/foo.c.PHONY clean : clean    rm prog $(objects)

VAPTH只对make起作用,以便make找到目标和依赖,不会对编译起任何作用。举例来说:

main.o:bar.h foo.h    $(CC) -c src/bar.c

执行这个命令会提示找不到头文件

main.o:include/bar.h include/foo.h    $(CC) -c src/bar.c

可以

main.o:include/bar.h include/foo.h    $(CC) $(CFLAGS) -c src/main.c

使用-I参数简化后。

3.3 伪目标

之前clean谈到过伪目标,伪目标并不是一个文件,只是一个标签,由于“伪目标”不是文件,所以make无法生成它的依赖关系和决定它是否要执行。我们只有通过显示地指明这个“目标”才能让其生效。当然,“伪目标”的取名不能和文件名重名,不然其就失去了“伪目标”的意义了。

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

伪目标一般没有依赖的文件。但是,我们也可以为伪目标指定所依赖的文件。伪目标同样可以作为“默认目标”,只要将其放在第一个。一个示例就是,如果你的Makefile需要一口气生成若干个行文件,但你只想简单地敲一个make完事,并且,所有的目标文件都写在一个Makefile中,那么你可以使用“伪目标”这个特性,例如一次生成三个.o文件:

# Makefile CC = gccCFLAGS = -I includeobjects = main.o bar.o foo.oVPATH = src:includeall:main.o bar.o foo.o.PHONY:allmain.o : bar.h foo.h    $(CC) $(CFLAGS) -c src/main.cbar.o :    $(CC) -c src/bar.cfoo.o :    $(CC) -c src/foo.c.PHONY clean : clean    rm prog $(objects)

如果不适用为目标all,那么只会生成第一个目标main.o

我们知道,Makefile中的第一个目标会被作为其默认目标。我们声明了一个“all”的伪目标,其依赖于其它三个目标。由于伪目标的特性是,总是被执行的,所以其依赖的那三个目标就总是不如“all”这个目标新。所以,其它三个目标的规则总是会被决议。也就达到了我们一口气生成多个目标的目的。“.PHONY : all”声明了“all”这个目标为“伪目标”。

从上面的例子中,我们发现目标可以变成依赖项,同样的,伪目标也可以作为依赖项,例如清除不同后缀的文件:

.PHONY:cleanall cleanobj cleandiffcleanall:cleanobj cleandiff    rm prograncleanobj:    rm *.ocleandiff:    rm *.diff

输入make cleanall可以清除全部文件,输入make cleanobj可以清除.o后缀文件,输入make cleandiff可以清除.diff后缀文件,使用起来比较方便。

3.4 多目标

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

bigoutput littleoutput : text.g    generate text.g -$(subst output,,$@) > $@

上述规则等价于:

bigoutput : text.g    generate text.g -big > bigoutputlittleoutput : text.g    generate text.g -little > littleoutput

其中,-(substoutput,,@)中的“Makefilesubst@”表示目标的集合,就像一个数组,“$@”依次取出目标,并执于命令。

3.5 静态模式

静态模式语法:

<targets...>:<target-pattern>:<prereq-pattern>    <commands>

targets定了一系列目标文件,是目标的一个集合

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

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

示例:

# Makefile CC = gccCFLAGS = -I includeobjects = main.o bar.o foo.oVPATH = src:includeall:prog1 prog2prog1 prog2 : $(objects)     $(CC) -o $@ $(objects)$(objects):%.o:%.c    $(CC) $(CFLAGS) -c $<main.o : bar.h foo.h.PHONY clean : clean    rm -f prog1 prog2 $(objects)

上面的例子中,指明了我们的目标从objectobject集合的模式,而依赖模式“%.c”则取模式“%.o”的“%”,也就是“main foo bar”,并为其加下“.c”的后缀,于是,我们的依赖目标就是“main.c foo.c bar.c”。而命令中的“<@”则是自动化变量,“<main.cfoo.cbar.c@”表示目标集(也就是prog1 prog2”)。

如果我们的“%.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 $<

相当于:

bar.o:bar.c    $(CC) -c $(CFLAGS) bar.c -o bar.olose.o:lose.c    $(CC) -c $(CFLAGS) lose.c -o lose.ofoo.elc:foo.el    emacs -f batch-byte-compile foo.el

(filter(files))表示调用Makefile的filter函数,过滤“$filter”集,只要其中模式为“%.o”的内容。其的它内容,我就不用多说了吧。这个例字展示了Makefile中更大的弹性。

3.6 自动生成依赖性

为了演示这个功能,我们把测试文件要做的复杂一些:

|--- include|    |--- bar.h|    |--- foo.h|    |--- utils.h|    |--- defs.h|--- Makefile|--- src     |--- bar.c     |--- foo.c     |--- utils.c     |--- main.c

defs.h

#ifndef __DEFS_H#define __DEFS_H#include <stdio.h>typedef signed   char                   rt_int8_t;      /**<  8bit integer type */typedef signed   short                  rt_int16_t;     /**< 16bit integer type */typedef signed   long                   rt_int32_t;     /**< 32bit integer type */typedef unsigned char                   rt_uint8_t;     /**<  8bit unsigned integer type */typedef unsigned short                  rt_uint16_t;    /**< 16bit unsigned integer type */typedef unsigned long                   rt_uint32_t;    /**< 32bit unsigned integer type */typedef int                             rt_bool_t;      /**< boolean type */#endif

utils.h

#ifndef __UTILS_H#define __UTILS_H#include "defs.h"extern rt_int32_t sum(rt_int32_t a, rt_int32_t b);extern rt_int32_t add(rt_int32_t a, rt_int32_t b);#endif

bar.h

#ifndef __BAR_H#define __BAR_H#include "defs.h"extern rt_int32_t bar(rt_int32_t a, rt_int32_t b);#endif

foo.h

#ifndef __FOO_H#define __FOO_H#include "defs.h"extern rt_int32_t foo(rt_int32_t a, rt_int32_t b);#endif

utils.c

#include <stdio.h>#include "utils.h"rt_int32_t add(rt_int32_t a, rt_int32_t b){    return a+b;}rt_int32_t sum(rt_int32_t a, rt_int32_t b){    return a+b;}

bar.c

#include <stdio.h>#include "utils.h"rt_int32_t bar(rt_int32_t a, rt_int32_t b){    rt_int32_t c;    c = sum(a, b);    return c;}

foo.c

#include <stdio.h>#include "utils.h"rt_int32_t foo(rt_int32_t a, rt_int32_t b){    rt_int32_t c;    c = add(a, b);    return c;}

main.c

#include <stdio.h>#include "bar.h"#include "foo.h"int main(void){    printf("Bar: %d, Foo: %d \r\n", bar(1,2), foo(2,3));    return 0;}

依赖关系如下:

main.o –> main.c foo.h bar.h defs.h

bar.o –> bar.c utils.h defs.h

foo.o –> foo.c utils.h defs.h

utils.o –> utils.c utils.h defs.h

可以看到,相对之前的例子,我们的依赖关系中包含了更多的头文件,如果是一个比较大型的工程,你必需清楚哪些C文件包含了哪些头文件,并且,你在加入或删除头文件时,也需要小心地修改Makefile,这是一个很没有维护性的工作。我们希望有一种方式,makefile可以每次运行时自动生成并能在make时自动更新依赖关系。

在gcc命令中-MM可以用来查看文件的依赖文件

gcc -MM src/make

会输出:

main.o:src/main.c include/foo.h include/bar.h include/defs.h

那么,编译器的这个功能如何与我们的Makefile联系在一起呢?因为这样一来,我们的Makefile也要根据这些源文件重新生成,让Makefile自已依赖于源文件?这个功能并不现实,不过我们可以有其它手段来迂回地实现这一功能。GNU组织建议把编译器为每一个源文件的自动生成的依赖关系放到一个文件中,为每一个“name.c”的文件都生成一个“name.d”的Makefile文件,[.d]文件中就存放对应[.c]文件的依赖关系。

回顾下make的工作流程
1. 读入所有的Makefile
2. 读入被include的其他Makefile
3. 初始化文件中变量
4. 推导隐晦规则,并分析所有规则
5. 为所有目标文件创建依赖关系链
6. 根据依赖关系,决定哪些目标文件要重新生成
7. 执行生成命令

从上面make的执行过程中可看出,要动态生成依赖关系,只能利用第2步读入其它Makefile的机制。这样我们用include把上面提到的[.d]文件包含进来实现每次make时动态更新依赖关系。

示例模板:

CC      = gcc CFLAGS  = -Wall -OINCLUDEFLAGS = LDFLAGS = OBJS    = seq.oTARGETS = test_seq .PHONY:all all : $(TARGETS)test_seq:test_seq.o $(OBJS)    $(CC) -o $@ $^ $(LDFLAGS)%.o:%.c    $(CC) -o $@ -c $< $(CFLAGS) $(INCLUDEFLAGS)%.d:%.c    @set -e; rm -f $@; $(CC) -MM $< $(INCLUDEFLAGS) > $@.$$$$; \    sed 's,\($*\)\.o[ :]*,\1.o $@ : ,g' < $@.$$$$ > $@; \    rm -f $@.$$$$-include $(OBJS:.o=.d).PHONY:clean clean:    rm -f $(TARGETS) *.o *.d *.d.*

3.6.1 如何动态生成依赖关系?

include关键字是用于读入其它Makefile文件。当该文件不存在时,make会寻找是否有生成它的规则,如果有,则执行其生成命令,然后再尝试读入。在include前加减号”-“可以上make忽略其产生的错误,并不输出任何错误信息。

也就是说,我们需要提供生成规则文件([.d]文件)的规则。例如,我们可以这样动态生成头文件依赖关系:

seq.d : seq.c    @echo “seq.o seq.d : seq.c seq.h" > $@-include seq.d

当make执行时,Makefile中的内容将是这样子(指内存上的数据):

seq.d : seq.c    @echo “seq.o seq.d : seq.c seq.h" > $@seq.o seq.d : seq.c seq.h

特别注意的是,由于对seq.c和seq.h的修改需要更新seq.d的内容(因为依赖关系可能已变化),因此seq.d也要在依赖关系的目标列表中。

3.6.2 自动生成头文件依赖

大多数c\/c++编译器提供了-M选项,可自动寻找源文件依赖的头文件,并生成依赖规则。对于gcc,需要使用-MM选项,否则它会把系统依赖的头文件也包含进来。例如执行下面一个命令:

gcc -MM seq.c

将输出:

seq.o : seq.c seq.h

但我们需要结果是seq.d也要包含在目标列表中,所以还需要对它进行文本处理。因此,上面的例子可改为:

seq.d : seq.c    @set -e; \    gcc -MM $< > $@.$$$$; \    sed 's,\($*\)\.o[ :]*,\1.o $@ : ,g' < $@.$$$$ > $@; \    rm -f $@.$$$$-include seq.d

3.6.3 生成规则中的执行命令解释

第一个命令@set -e。@关键字告诉make不输出该行命令;set -e的作用是,当后面的命令的返回值非0时,立即退出。

那么为什么要把几个命令写在”同一行”(是对于make来说,因为\的作用就是连接行),并用分号隔开每个命令。因为在Makefile这样做才能使上一个命令作用于下一个命令。这里是想要set -e作用于后面的命令。

第二个命令gcc -MM <>@.

, 作用是根据源文件生成依赖关系,并保存到临时文件中。内建变量<seq.c)
$"
",由于makefile中所有的使,需要用
;
是shell的特殊变量,它的值为当前进程号;使用进程号为后缀的名称创建临时文件,是shell编程常用做法,这样可保证文件唯一性。

第三个命令作用是将目标文件加入依赖关系的目录列表中,并保存到目标文件。关于正则表达式部分就不说了,唯一要注意的是内建变量\**的值为第一个依赖文件去掉后缀的名称(这里即是seq)。

第四个命令是将该临时文件删除。

如果把内建变量都替换成其值后,实际内容是这样子:

seq.d : seq.c    @set -e; \    gcc -MM seq.c > seq.d.$$$$; \    sed 's,\(seq\)\.o[ :]*,\1.o seq.d : ,g' < seq.d.$$$$ > seq.d; \    rm -f seq.d.$$$$-include seq.d

3.6.4 Makefile的模式匹配

最后,再把Makefile的模式匹配应用上,就完成自动生成头文件依赖功能了:

%.d : %.c    @set -e; \    gcc -MM $@ > $@.$$$$; \    sed 's,\($*\)\.o[ :]*,\1.o $@ : ,g' < $@.$$$$ > $@; \    rm -f $@.$$$$-include seq.d

修改我们自己的Makefile文件:

# MakefileVPATH = src:includeCC = gccCFLAGS = -I includeOBJ := main.o bar.o foo.o utils.oprog:$(OBJ)    $(CC) $(OBJ) -o prog%.o:%.c    $(CC) $(CFLAGS) -o $@ -c $<%.d:%.c    @set -e; rm -f $@;\    $(CC) $(CFLAGS) -MM $< > $@.$$$$;\    sed 's,\($*\)\.o[ :]*,\1.o $@ : ,g' < $@.$$$$ > $@; \    rm -f $@.$$$$-include $(OBJ:.o=.d) .PHONY:cleanclean:    -rm -f prog $(OBJ) *.d *.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模块......

如果不加@,那么会输出:

echo 正在编译XXX模块......正在编译XXX模块......

加上@后,命令那行就不会输出啦

特殊地,如果想只显示命令但不执行,那么需要在执行make时带上参数
参数“-n”或“–just-print”,那么其只是显示命令,但不会执行命令,这个功能很有利于我们调试我们的Makefile,看看我们书写的命令是执行起来是什么样子的或是什么顺序的。而make参数“-s”或“–slient”则是全面禁止命令的显示,但是echo中的打印会显示。

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键之后),标记为不管命令出不出错都认为是成功的。如:

-include $(OBJ:.o=.d) 

还有一个全局的办法是,给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 = valueexport variable

其等价于:

export variable := value

其等价于:

variable := valueexport variable

示例二:

export variable += value

其等价于:

variable += valueexport 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-yaccyacc $(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
原创粉丝点击