美文网首页Linux开发环境搭建
Makefile 学习精简(一)[待续]

Makefile 学习精简(一)[待续]

作者: 周筱鲁 | 来源:发表于2016-01-04 14:40 被阅读1180次

    来自陈浩的一片老文,但绝对营养。

    示例工程:3 个头文件*.h,和 8 个 C 文件*.c

    编译过程,源文件首先会生成中间目标文件,再由中间目标文件生成执行文件。在编译时,编译器只检测程序语法,和函数、变量是否被声明。如果函数未被声明,编译器会给出一个警告,但可以生成 Object File。而在链接程序时,链接器会在所有的 Object File 中找寻函数的实现,如果找不到,那到就会报链接错误码(Linker Error)。

    • Makefile规则:
    //target: 目标文件
    //prerequisites: 依赖文件
    //command: 执行命令
    target  : prerequisites    
    command
    

    示例,

    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
    
    • 使用变量
    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)
    
    • 自动推导(隐晦规则)
      只要 make 看到一个*.o 文件,它就会自动的把*.c文件加在依赖关系中,.PHONY伪目标文件。
    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)
    
    • 清空目标文件
      一般做法,
    clean:    
    rm edit $(objects)
    

    推荐做法,

    .PHONY : clean    
    clean :    
    -rm edit $(objects)
    

    .PHONY表示 clean 是一个“伪目标”,-rm 是某些文件出现问题,但不要管,继续做后面的事,clean的规则放在文件的最后”。

    细节

    Makefile 里主要包含了五个东西:显式规则、隐晦规则、变量定义、文件指示和注释。在 Makefile 中的命令,必须要以[Tab]键开始。

    1. 显式规则
      显式规则说明了,如何生成一个或多的的目标文件。这是由 Makefile 的书写者明显指出,要生成的文件,文件的依赖文件,生成的命令。
    2. 隐晦规则
      由于我们的 make 有自动推导的功能,所以隐晦的规则可以让我们比较粗糙地简略地书写 Makefile,这是由 make 所支持的。
    3. 变量的定义
      Makefile 中我们要定义一系列的变量,变量一般都是字符串,这个有点你 C 语言中的宏,当 Makefile 被执行时,其中的变量都会被扩展到相应的引用位置上。
    4. 文件指示
    • 在一个 Makefile 中引用另一个 Makefile,就像 C 语言中的include 一样;
    • 根据某些情况指定 Makefile 中的有效部分,就像 C 语言中的预编译#if 一样;
    • 定义一个多行的命令。
    1. 注释
      Makefile 中只有行注释,和 UNIX 的 Shell 脚本一样,其注释是用#字符。如果你要在你的 Makefile 中使用#字符,可以用反斜框进行转义,如:\#
    • Makefile 文件名
      默认的情况下,make 命令会在当前目录下按顺序为GNUmakefilemakefileMakefile的文件,推荐Makefilemake -f[--file]参数,指定特定的 Makefile

    • 引用Makefile
      include <filename>
      <filename>可以是当前操作系统 Shell 的文件模式(可以保含路径和通配符),在 include前面可以有一些空字符,不能是[Tab]键。include<filename>用一个或多个空格隔开。例,

    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安置在当前
    的位。如果文件都没有指定绝对路径或是相对路径的话,make 会在当前目录下首先寻找,如果当前目录下没有找到,那么,make 还会在下面的几个目录下找:

    1. 如果 make 执行时,有-I--include-dir参数,那么 make 就会在这个参数所指定的目录下去寻找。
    2. 如果目录<prefix>/include(一般是:/usr/local/bin/usr/include)存在的话,make 也会去找。如果有文件没有找到的话,make 会生成一条警告信息,继续载入其它的文件,一旦完成 Makefile 的读取,make 会再重试这些没有找到,或是不能读取的文件,如果还是不行,make 才会出现一条致命信息。在 include 前加一个减号-,如: -include <filename>其表示,无论 include 过程中出现什么错误,都不要报错继续执行。其它版本 make兼容的相关命令是 sinclude
    • 环境变量 MAKEFILES
      如果你的当前环境中定义了环境变量 MAKEFILES,那么,make 会把这个变量中的值做一个类似于 include 的动作。这个变量中的值是其它的 Makefile,用空格分隔。
      ** 建议不要使用这个环境变量。 **

    • make 的工作方式
      GNUmake 工作时的执行步骤入下:

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

    1-5 步为第一个阶段,6-7 为第二个阶段。第一个阶段中,如果定义的变量出现在依赖关系的规则中,那么仅当这条依赖被决定要使用了,变量才会在其内部展开。

    书写规则

    规则包含两个部分,一个是依赖关系,一个是生成目标的方法。
    Makefile 中,第一条规则中的目标将被确立为最终的目标。

    一、规则举例

    foo.o : foo.c defs.h # foo 模块    
    cc -c -g foo.c  
    

    foo.o 是我们的目标,foo.cdefs.h 是目标所依赖的源文件,而只有一个命令cc -c -g foo.c(以Tab 键开头)。

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

    二、规则的语法

    targets : prerequisites    
    command
    

    或是这样:

    targets : prerequisites ; command    
    command    
    

    targets 是文件名,以空格分开,可以使用通配符。一般来说,我们的目标基本上是一个文件,但也有可能是多个文件。command 是命令行,如果其不与target:prerequisites在一行,那么,必须以[Tab]开头,如果和 prerequisites 在一行,那么可以用;做为分隔。 prerequisites也就是目标所依赖的文件(或依赖目标)。如果其中的某个文件要比目标文件要新,那么, 目标就被认为是“过时的”,被认为是需要重生成的。

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

    三、在规则中使用通配符
    如果我们想定义一系列比较类似的文件,我们很自然地就想起使用通配符。make 支持三各通配符:*?[...]。这是和 Unix 的 B-Shell 是相同的。 波浪号(~)字符在文件名中也有比较特殊的用途。如果是“~/test,这就表示当前用户的$HOME 目录下的 test 目录。而~hchen/test则表示用户 hchen 的宿主目录下的 test 目录。而在 Windows 或是 MS-DOS 下,用户没有宿主目录,那么波浪号所指的目录则根据环境变量HOME而定。

    通配符代替了你一系列的文件,如*.c表示所以后缀为 c 的文件用转义字符\,如\*来表示真实的*字符。
    例:

    clean:    
    rm -f *.o
    
    print: *.c    
    lpr -p $?    
    touch print
    

    这个例子说明了通配符也可以在我们的规则中,目标 print 依赖于所有的.c文件。其中的$?是一个自动化变量。

    objects = *.o
    

    上面这个例子,表示了,通符同样可以用在变量中。并不是说*.o会展开,不!objects的值就是*.o。如果要让通配符在变量中展开,也就是让 objects 的值是所有.o的文件名的集合,那么,你可以这样:

    objects := $(wildcard *.o)    
    

    这种用法由关键字wildcard指出,关于 Makefile 的关键字。

    四、文件搜寻
    Makefile 文件中的变量VPATH,如果没有指明这个变量,make 只会在当前的目录中去找寻依赖文件和目标文件。如果定义了这个变量,那么,make就会在当当前目录找不到的情况下,到所指定的目录中去找寻文件了。

    VPATH = src:../headers
    

    上面的的定义指定两个目录,src../headersmake 会按照这个顺序进行搜索。目录由:分隔。
    另一个设置文件搜索路径的方法是使用 makevpath关键字,不是变量,这是一个 make 的关键字,可以指定不同的文件在不同的搜索目录中。这是一个很灵活的功能。它的使用方法有三种:

    • vpath <pattern> <directories>
      为符合模式<pattern>的文件指定搜索目录<directories>
    • vpath <pattern>
      清除符合模式<pattern>的文件的搜索目录。
    • vpath
      清除所有已被设置好了的文件搜索目录。

    vapth 使用方法中的<pattern>需要包含%字符匹配零或若干字符,例如 , %.h表示所有以 .h结 尾 的 文 件 。<pattern>指 定 了 要 搜 索 的 文 件 集 , 而 <directories>则指定了<pattern>的文件集的搜索的目录。例如: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目录。

    伪目标

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

    当然,为了避免和文件重名的这种情况,我们可以使用一个特殊的标记“.PHONY”来显
    示地指明一个目标是“伪目标”,向 make 说明,不管是否有这个文件,这个目标就是“伪
    目标”。
    .PHONY : clean
    只要有这个声明,不管是否有“clean”文件,要运行“clean”这个目标,只有“make
    clean”这样。于是整个过程可以这样写:
    第 20 页共 78 页 2005 年 10 月 14 日整理:祝冬华跟我一起写 Makefile 作者:陈皓
    .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”命令来达到清除不同种类文件的目的。
    第 21 页共 78 页 2005 年 10 月 14 日整理:祝冬华跟我一起写 Makefile 作者:陈皓
    六、多目标

    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,
    后面的为参数。关于函数,将在后面讲述。这里的这个函数是截取字符串的意思,“$@”表
    示目标的集合,就像一个数组,“$@”依次取出目标,并执于命令。
    七、静态模式
    静态模式可以更加容易地定义多目标的规则,可以让我们的规则变得更加的有弹性和灵

    。我们还是先来看一下语法:
    <targets ...>: <target-pattern>: <prereq-patterns ...>
    <commands>
    ....

    targets 定义了一系列的目标文件,可以有通配符。是目标的一个集合。
    target-parrtern 是指明了 targets 的模式,也就是的目标集模式。
    prereq-parrterns 是目标的依赖模式,它对 target-parrtern 形成的模式再进行一次依赖
    目标的定义。

    这样描述这三个东西,可能还是没有说清楚,还是举个例子来说明一下吧。如果我们的
    第 22 页共 78 页 2005 年 10 月 14 日整理:祝冬华跟我一起写 Makefile 作者:陈皓
    <target-parrtern>定义成“%.o”,意思是我们的<target>集合中都是以“.o”结尾的,而
    如果我们的<prereq-parrterns>定义成“%.c”,意思是对<target-parrtern>所形成的目标
    集进行二次定义,其计算方法是,取<target-parrtern>模式中的“%”(也就是去掉了[.o]
    这个结尾),并为其加上[.c]这个结尾,形成的新集合。

    所以,我们的“目标模式”或是“依赖模式”中都应该有“%”这个字符,如果你的文
    件名中有“%”那么你可以使用反斜杠“\”进行转义,来标明真实的“%”字符。
    看一个例子:
    objects = foo.o bar.o

    all: $(objects)
    $(objects): %.o: %.c
    $(CC) -c $(CFLAGS) {1}lt; -o $@
    上面的例子中,指明了我们的目标从$object 中获取,“%.o”表明要所有以“.o”结
    尾的目标,也就是“foo.o bar.o”,也就是变量$object 集合的模式,而依赖模式“%.c”
    则取模式“%.o”的“%”,也就是“foo bar”,并为其加下“.c”的后缀,于是,我们的
    依赖目标就是“foo.c bar.c”。而命令中的“{1}lt;”和“$@”则是自动化变量,“{1}lt;”表示
    所有的依赖目标集(也就是“foo.c bar.c”),“$@”表示目标集(也就是“foo.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) {1}lt; -o $@
    $(filter %.elc,$(files)): %.elc: %.el
    emacs -f batch-byte-compile {1}lt;
    $(filter %.o,$(files))表示调用 Makefile 的 filter 函数,过滤“$filter”集,只要其
    中模式为“%.o”的内容。其的它内容,我就不用多说了吧。这个例字展示了 Makefile 中更
    大的弹性。

    第 23 页共 78 页 2005 年 10 月 14 日整理:祝冬华跟我一起写 Makefile 作者:陈皓

    八、自动生成依赖性
    在 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 组织建议把编译器为每一个源
    第 24 页共 78 页 2005 年 10 月 14 日整理:祝冬华跟我一起写 Makefile 作者:陈皓
    文 件 的 自 动 生 成 的 依 赖 关 系 放 到 一 个 文 件 中 , 为 每 一 个 “name.c” 的 文 件 都 生 成 一 个
    “name.d”的 Makefile 文件,[.d]文件中就存放对应[.c]文件的依赖关系。于是,我们可
    以写出[.c]文件和[.d]文件的依赖关系,并让 make 自动更新或自成[.d]文件,并把其包含
    在我们的主 Makefile 中,这样,我们就可以自动化地生成每个文件的依赖关系了。
    这里,我们给出了一个模式规则来产生[.d]文件:

    %.d: %.c
    @set -e; rm -f $@; \
    $(CC) -M $(CPPFLAGS) {1}lt; > $@.$$; \
    sed 's,$∗.o[ :]*,\1.o $@ : ,g' < $@.$$ > $@; \
    rm -f $@.$$
    这个规则的意思是,所有的[.d]文件依赖于[.c]文件,“rm -f $@”的意思是删除所有
    的目标,也就是[.d]文件,第二行的意思是,为每个依赖文件“{1}lt;”,也就是[.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]文件中
    的目标会成为默认目标。

    第六部分 书写命令

    第 25 页共 78 页 2005 年 10 月 14 日整理:祝冬华跟我一起写 Makefile 作者:陈皓
    每条规则中的命令和操作系统 Shell 的命令行是一致的。make 会一按顺序一条一条的
    执行命令,每条命令的开头必须以[Tab]键开头,除非,命令是紧跟在依赖规则后面的分号
    后的。在命令行之间中的空格或是空行会被忽略,但是如果该空格或空行是以 Tab 键开头的,
    那么 make 会认为其是一个空命令。

    我 们 在 UNIX 下 可 能 会 使 用 不 同 的 Shell , 但 是 make 的 命 令 默 认 是 被
    “/bin/sh”——UNIX 的标准 Shell 解释执行的。除非你特别指定一个其它的 Shell。
    Makefile 中,“#”是注释符,很像 C/C++中的“//”,其后的本行字符都被注释。
    一、显示命令
    通常,make 会把其要执行的命令行在命令执行前输出到屏幕上。当我们用“@”字符在
    命令行前,那么,这个命令将不被 make 显示出来,最具代表性的例子是,我们用这个功能
    来像屏幕显示一些信息。如:
    @echo 正在编译 XXX 模块......
    当 make 执行时,会输出“正在编译 XXX 模块......”字串,但不会输出命令,如果没
    有“@”,那么,make 将输出:
    echo 正在编译 XXX 模块......
    正在编译 XXX 模块......
    如果 make 执行时,带入 make 参数“-n”或“--just-print”,那么其只是显示命令,
    但不会执行命令,这个功能很有利于我们调试我们的 Makefile,看看我们书写的命令是执
    行起来是什么样子的或是什么顺序的。

    而 make 参数“-s”或“--slient”则是全面禁止命令的显示。

    二、命令执行

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

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

    第 26 页共 78 页 2005 年 10 月 14 日整理:祝冬华跟我一起写 Makefile 作者:陈皓
    示例二:
    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”
    等后缀。

    三、命令出错

    每当命令运行完后,make 会检测每个命令的返回码,如果命令返回成功,那么 make 会
    执行下一条命令,当规则中所有的命令成功返回后,这个规则就算是成功完成了。如果一个
    规则中的某个命令出错了(命令退出码非零),那么 make 就会终止执行当前规则,这将有可
    能终止所有规则的执行。
    有些时候,命令的出错并不表示就是错误的。例如 mkdir 命令,我们一定需要建立一个
    目录,如果目录不存在,那么 mkdir 就成功执行,万事大吉,如果目录存在,那么就出错了。
    我们之所以使用 mkdir 的意思就是一定要有这样的一个目录,于是我们就不希望 mkdir 出错
    而终止规则的运行。

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

    还有一个全局的办法是,给 make 加上“-i”或是“--ignore-errors”参数,那么,
    Makefile 中所有命令都会忽略错误。而如果一个规则是以“.IGNORE”作为目标的,那么这
    个规则中的所有命令将会忽略错误。这些是不同级别的防止命令出错的方法,你可以根据你
    的不同喜欢设置。

    还有一个要提一下的 make 的参数的是“-k”或是“--keep-going”,这个参数的意思
    是,如果某规则中的命令出错了,那么就终目该规则的执行,但继续执行其它规则。
    第 27 页共 78 页 2005 年 10 月 14 日整理:祝冬华跟我一起写 Makefile 作者:陈皓
    四、嵌套执行 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
    第 28 页共 78 页 2005 年 10 月 14 日整理:祝冬华跟我一起写 Makefile 作者:陈皓

    其等价于:
    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”来执行,那么当进入该目录时,
    我们会看到:

    第 29 页共 78 页 2005 年 10 月 14 日整理:祝冬华跟我一起写 Makefile 作者:陈皓
    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”总是失效的。
    五、定义命令包

    如果 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 在执行命令包时,命令包中的每个命令会被依
    次独立执行。

    第七部分 使用变量

    在 Makefile 中的定义的变量,就像是 C/C++语言中的宏一样,他代表了一个文本字串,
    在 Makefile 中执行的时候其会自动原模原样地展开在所使用的地方。其与 C/C++所不同的
    是,你可以在 Makefile 中改变其值。在 Makefile 中,变量可以使用在“目标”,“依赖目
    标”,“命令”或是 Makefile 的其它部分中。

    变量的命名字可以包含字符、数字,下划线(可以是数字开头),但不应该含有“:”、
    第 30 页共 78 页 2005 年 10 月 14 日整理:祝冬华跟我一起写 Makefile 作者:陈皓
    “#”、“=”或是空字符(空格、回车等)。变量是大小写敏感的,“foo”、“Foo”和“FOO”
    是三个不同的变量名。传统的 Makefile 的变量名是全大写的命名方式,但我推荐使用大小
    写搭配的变量名,如:MakeFlags。这样可以避免和系统的变量冲突,而发生意外的事情。

    有一些变量是很奇怪字串,如“{1}lt;”、“$@”等,这些是自动化变量,我会在后面介绍。

    一、变量的基础

    变量在声明时需要给予初值,而在使用时,需要给在变量名前加上“$”符号,但最好
    用小括号“()”或是大括号“{}”把变量给包括起来。如果你要使用真实的“$”字符,
    那么你需要用“$”来表示。
    变量可以使用在许多地方,如规则中的“目标”、“依赖”、“命令”以及新的变量中。先
    看一个例子:
    objects = program.o foo.o utils.o
    program : $(objects)
    cc -o program $(objects)
    $(objects) : defs.h
    变量会在使用它的地方精确地展开,就像 C/C++中的宏一样,例如:
    foo = c
    prog.o : prog.$(foo)
    $(foo)$(foo) -$(foo) prog.$(foo)
    展开后得到:
    prog.o : prog.c
    cc -c prog.c
    当然,千万不要在你的 Makefile 中这样干,这里只是举个例子来表明 Makefile 中的变
    量在使用处展开的真实样子。可见其就是一个“替代”的原理。

    另外,给变量加上括号完全是为了更加安全地使用这个变量,在上面的例子中,如果你
    不想给变量加上括号,那也可以,但我还是强烈建议你给变量加上括号。
    第 31 页共 78 页 2005 年 10 月 14 日整理:祝冬华跟我一起写 Makefile 作者:陈皓
    二、变量中的变量

    在定义变量的值时,我们可以使用其它变量来构造变量的值,在 Makefile 中有两种方
    式来在用变量定义变量的值。
    先看第一种方式,也就是简单的使用“=”号,在“=”左侧是变量,右侧是变量的值,右侧
    变量的值可以定义在文件的任何一处,也就是说,右侧中的变量不一定非要是已定义好的值,
    其也可以使用后面定义的值。如:
    foo = $(bar)
    bar = $(ugh)
    ugh = Huh?
    all:
    echo $(foo)
    我们执行“make all”将会打出变量$(foo)的值是“Huh?”( $(foo)的值是$(bar),
    $(bar)的值是$(ugh),$(ugh)的值是“Huh?”)可见,变量是可以使用后面的变量来定义的。

    这个功能有好的地方,也有不好的地方,好的地方是,我们可以把变量的真实值推到后
    面来定义,如:
    CFLAGS = $(include_dirs) -O
    include_dirs = -Ifoo -Ibar
    当“CFLAGS”在命令中被展开时,会是“-Ifoo -Ibar -O”。但这种形式也有不好的地
    方,那就是递归定义,如:
    CFLAGS = $(CFLAGS) -O
    或:
    A = $(B)
    B = $(A)
    这会让 make 陷入无限的变量展开过程中去,当然,我们的 make 是有能力检测这样的定
    义,并会报错。还有就是如果在变量中使用函数,那么,这种方式会让我们的 make 运行时
    非常慢,更糟糕的是,他会使用得两个 make 的函数“wildcard”和“shell”发生不可预知
    的错误。因为你不会知道这两个函数会被调用多少次。

    为了避免上面的这种方法,我们可以使用 make 中的另一种用变量来定义变量的方法。
    这种方法使用的是“:=”操作符,如:
    第 32 页共 78 页 2005 年 10 月 14 日整理:祝冬华跟我一起写 Makefile 作者:陈皓

    x := foo
    y := $(x) bar
    x := later
    其等价于:
    y := foo bar
    x := later
    值得一提的是,这种方法,前面的变量不能使用后面的变量,只能使用前面已定义好了的变
    量。如果是这样:
    y := $(x) bar
    x := foo
    那么,y 的值是“bar”,而不是“foo bar”。
    上面都是一些比较简单的变量使用了,让我们来看一个复杂的例子,其中包括了 make
    的函数、条件表达式和一个系统变量“MAKELEVEL”的使用:
    ifeq (0,${MAKELEVEL})
    cur-dir := $(shell pwd)
    whoami := $(shell whoami)
    host-type := $(shell arch)
    MAKE := ${MAKE} host-type=${host-type} whoami=${whoami}
    endif

    关于条件表达式和函数,我们在后面再说,对于系统变量“MAKELEVEL”,其意思是,
    如果我们的 make 有一个嵌套执行的动作(参见前面的“嵌套使用 make”),那么,这个变
    量会记录了我们的当前 Makefile 的调用层数。

    下面再介绍两个定义变量时我们需要知道的,请先看一个例子,如果我们要定义一个变
    量,其值是一个空格,那么我们可以这样来:
    nullstring :=
    space := $(nullstring) # end of the line
    nullstring 是一个 Empty 变量,其中什么也没有,而我们的 space 的值是一个空格。因为

    操作符的右边是很难描述一个空格的,这里采用的技术很管用,先用一个 Empty 变量来
    标明变量的值开始了,而后面采用“#”注释符来表示变量定义的终止,这样,我们可以定
    义出其值是一个空格的变量。请注意这里关于“#”的使用,注释符“#”的这种特性值得我
    第 33 页共 78 页 2005 年 10 月 14 日整理:祝冬华跟我一起写 Makefile 作者:陈皓
    们注意,如果我们这样定义一个变量:

    dir := /foo/bar # directory to put the frobs in
    dir 这个变量的值是“/foo/bar”,后面还跟了 4 个空格,如果我们这样使用这样变量来指
    定别的目录——“$(dir)/file”那么就完蛋了。
    还有一个比较有用的操作符是“?=”,先看示例:
    FOO ?= bar
    其含义是,如果 FOO 没有被定义过,那么变量 FOO 的值就是“bar”,如果 FOO 先前被定义
    过,那么这条语将什么也不做,其等价于:
    ifeq ($(origin FOO), undefined)
    FOO = bar
    endif
    三、变量高级用法

    这里介绍两种变量的高级使用方法,第一种是变量值的替换。我们可以替换变量中的共有的
    部分,其格式是“$(var:a=b)”或是“${var:a=b}”,其意思是,把变量“var”中所有以
    “a”字串“结尾”的“a”替换成“b”字串。这里的“结尾”意思是“空格”或是“结束
    符”。

    还是看一个示例吧:
    foo := a.o b.o c.o
    bar := $(foo:.o=.c)
    这个示例中,我们先定义了一个“$(foo)”变量,而第二行的意思是把“$(foo)”中所
    有以“.o”字串“结尾”全部替换成“.c”,所以我们的“$(bar)”的值就是“a.c b.c
    c.c”。
    另外一种变量替换的技术是以“静态模式”(参见前面章节)定义的,如:
    foo := a.o b.o c.o
    bar := $(foo:%.o=%.c)
    这依赖于被替换字串中的有相同的模式,模式中必须包含一个“%”字符,这个例子同
    样让$(bar)变量的值为“a.c b.c c.c”。
    第二种高级用法是——“把变量的值再当成变量”。先看一个例子:
    第 34 页共 78 页 2005 年 10 月 14 日整理:祝冬华跟我一起写 Makefile 作者:陈皓

    x = y
    y = z
    a := $($(x))
    在这个例子中,$(x)的值是“y”,所以$($(x))就是$(y),于是$(a)的值就是“z”。(注意,
    是“x=y”,而不是“x=$(y)”)

    我们还可以使用更多的层次:
    x = y
    y = z
    z = u
    a := $($($(x)))

    这里的$(a)的值是“u”,相关的推导留给读者自己去做吧。
    让我们再复杂一点,使用上“在变量定义中使用变量”的第一个方式,来看一个例子:
    x = $(y)
    y = z
    z = Hello
    a := $($(x))

    这里的$($(x))被替换成了$($(y)),因为$(y)值是“z”,所以,最终结果是:a:=$(z),也
    就是“Hello”。
    再复杂一点,我们再加上函数:
    x = variable1
    variable2 := Hello
    y = $(subst 1,2,$(x))
    z = y
    a := $($($(z)))

    这 个 例 子 中 , “$($($(z)))” 扩 展 为 “$($(y))” , 而 其 再 次 被 扩 展 为 “$($(subst
    1,2,$(x)))”。$(x)的值是“variable1”,subst 函数把“variable1”中的所有“1”字
    串替换成“2”字串,于是,“variable1”变成“variable2”,再取其值,所以,最终,
    $(a)的值就是$(variable2)的值——“Hello”。(喔,好不容易)

    在这种方式中,或要可以使用多个变量来组成一个变量的名字,然后再取其值:
    first_second = Hello
    a = first
    第 35 页共 78 页 2005 年 10 月 14 日整理:祝冬华跟我一起写 Makefile 作者:陈皓
    b = second
    all = $($a_$b)

    这里的“$a_$b”组成了“first_second”,于是,$(all)的值就是“Hello”。
    再来看看结合第一种技术的例子:
    a_objects := a.o b.o c.o
    1_objects := 1.o 2.o 3.o
    sources := $($(a1)_objects:.o=.c)
    这个例子中,如果$(a1)的值是“a”的话,那么,$(sources)的值就是“a.c b.c c.c”;
    如果$(a1)的值是“1”,那么$(sources)的值是“1.c 2.c 3.c”。

    再来看一个这种技术和“函数”与“条件语句”一同使用的例子:
    ifdef do_sort
    func := sort
    else
    func := strip
    endif

    bar := a d b g q c
    foo := $($(func) $(bar))
    这个示例中,如果定义了“do_sort”,那么:foo := $(sort a d b g q c),于是$(foo)
    的值就是“a b c d g q”,而如果没有定义“do_sort”,那么:foo := $(sort a d bg q
    c),调用的就是 strip 函数。
    当然,“把变量的值再当成变量”这种技术,同样可以用在操作符的左边:
    dir = foo
    $(dir)_sources := $(wildcard $(dir)/*.c)
    define $(dir)_print
    lpr $($(dir)_sources)
    endef

    这个例子中定义了三个变量:“dir”,“foo_sources”和“foo_print”。
    第 36 页共 78 页 2005 年 10 月 14 日整理:祝冬华跟我一起写 Makefile 作者:陈皓
    四、追加变量值

    我们可以使用“+=”操作符给变量追加值,如:
    objects = main.o foo.o bar.o utils.o
    objects += another.o
    于是,我们的$(objects)值变成:“main.o foo.o bar.o utils.o another.o”(another.o
    被追加进去了)

    使用“+=”操作符,可以模拟为下面的这种例子:
    objects = main.o foo.o bar.o utils.o
    objects := $(objects) another.o
    所不同的是,用“+=”更为简洁。
    如果变量之前没有定义过,那么,“+=”会自动变成“=”,如果前面有变量定义,那么“+=”
    会继承于前次操作的赋值符。如果前一次的是“:=”,那么“+=”会以“:=”作为其赋值符,
    如:

    variable := value
    variable += more
    等价于:
    variable := value
    variable := $(variable) more
    但如果是这种情况:
    variable = value
    variable += more
    由于前次的赋值符是“=”,所以“+=”也会以“=”来做为赋值,那么岂不会发生变量的递
    补归定义,这是很不好的,所以 make 会自动为我们解决这个问题,我们不必担心这个问题。

    五、override 指示符

    如果有变量是通常make 的命令行参数设置的,那么Makefile 中对这个变量的赋值会被忽略。
    第 37 页共 78 页 2005 年 10 月 14 日整理:祝冬华跟我一起写 Makefile 作者:陈皓
    如果你想在 Makefile 中设置这类参数的值,那么,你可以使用“override”指示符。其语
    法是:

    override <variable> = <value>
    override <variable> := <value>
    当然,你还可以追加:
    override <variable> += <more text>
    对于多行的变量定义,我们用 define 指示符,在 define 指示符前,也同样可以使用 ovveride
    指示符,如:

    override define foo
    bar
    endef
    六、多行变量
    还有一种设置变量值的方法是使用 define 关键字。使用 define 关键字设置变量的值可
    以有换行,这有利于定义一系列的命令(前面我们讲过“命令包”的技术就是利用这个关键
    字)。

    define 指示符后面跟的是变量的名字,而重起一行定义变量的值,定义是以 endef 关
    键字结束。其工作方式和“=”操作符一样。变量的值可以包含函数、命令、文字,或是其
    它变量。因为命令需要以[Tab]键开头,所以如果你用 define 定义的命令变量中没有以[Tab]
    键开头,那么 make 就不会把其认为是命令。

    下面的这个示例展示了 define 的用法:
    define two-lines
    echo foo
    echo $(bar)
    endef

    相关文章

      网友评论

        本文标题:Makefile 学习精简(一)[待续]

        本文链接:https://www.haomeiwen.com/subject/osmpcttx.html