注:本系列内容大部分是基于GNU make的标准,其中cc命令默认调用Linux自带的c编译器程序

  1. makefile介绍
  2. makefile编写(1):规则
  3. makefile编写(2):变量
  4. makefile编写(3):条件执行和函数

makefile规则

makefile规则包含两个部分,

  • 目标依赖关系
  • 生成目标的方法(命令)

下面用一个简单的规则来说明这两种组成部分:

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

此例中foo.o是目标,foo.c和defs.h是目标所依赖的源文件,其中foo.c包含了defs.h头文件,只有一个命令cc -c -g foo.c。这个规则的目标依赖关系和生成方法分别是:

  • foo.o依赖于foo.c和defs.h,如果foo.c和defs.h的文件日期比foo.o文件日期要新或是foo.o不存在,那么目标依赖关系发生
    • 这就是为什么要在依赖文件中加入.h文件,虽然不加一样可以编译(因为.h已经在预处理时被包含进.c中了),但是此时.h中的修改将不会导致重新编译
  • 后面的cc命令指示如何利用foo.c和defs.h生成foo.o文件的生成方法

makefile规则的基本语法

规则的基本语法:

targets : prerequisites
[Tab键]command

或是这样:

targets : prerequisites ; command
[Tab键]command

简单分析一下:

  • targets是文件名,以空格分开,可以使用通配符;一般来说,目标基本上是一个文件,但也有可能是多个文件
  • command是命令
    • 如果其不与target:prerequisites在一行,那么必须以[Tab键]开头
    • 如果和prerequisites在一行,那么可以用分号做为分隔
  • prerequisites也就是目标所依赖的文件(或依赖目标)

makefile规则中的通配符和模式字符

makefile支持shell通配符:*、?、[list],(makefile支持shell命令,所以shell支持的通配符在makefile应该是同样适用的)。转义字符同样是有效的,比如文件名中有通配符*,那么可以用\*来表达*字符原本的意思,而不使用通配符*的语义。
注意通配符并不是可以用在任何地方,基本上只能用在makefile规则中,其它上下文中不应直接使用通配符(尤其是在变量定义中直接使用通配符是不会展开的)

  • 可以用在规则的目标依赖关系中,make在读取makefile时自动对其进行展开
  • 可以出现在规则的命令中(显然shell命令是支持通配符的),在命令执行时展开

除此外规则的目标依赖关系中还支持模式字符%,使用%定义的规则可以称为模式规则。带有模式字符%的目标被用来匹配一个目标文件名,可以匹配任何非空字符串。规则的依赖文件中同样可以使用%,依赖文件中%的取值情况由目标中的%来决定,如:

%.o : %.c
    cc -o $@ $<

这个模式规则的含义是编译make搜索到的所有.c文件。如果模式规则的依赖不包含%时代表的是所有与模式匹配的目标都依赖于指定的依赖文件。模式字符%的匹配和替换发生在规则中所有变量和函数引用展开之后,变量和函数的展开一般发生在make读取makefile时,而模式规则中的%的匹配和替换则发生在make执行规则时。

注意:模式字符%和通配符*的功能看似很像,实则完全不同

  • %可以表达一种特定的生成规则,它会同时影响规则中的目标和依赖
  • 而通配符只是文件名格式的匹配和扩展,实际上在规则层面上并没有任何影响

最终目标文件

在makefile中,规则编写的顺序是很重要的,因为makefile中只应该有一个最终目标(也就是.DEFAULT_GOAL的值),其它的目标都是被这个目标所依赖派生出来的,所以一定要让make知道你的最终目标是什么。一般来说定义在makefile中的目标可能会有很多,但是第一条规则中的目标将被确立为最终的目标,如果第一条规则中的目标有很多个,那么第一个目标(默认目标)会成为最终的目标,make最终完成的也就是这个目标。当然这是说的默认情况,是指直接使用make命令而没有显式指定目标文件名的情况,如果在make后加上目标名字就可以指定任何makefile中的目标。

  • 除了以-打头,或是包含了=的目标,因为有这些字符的目标会被解析成命令行参数或变量
  • 即使没有被明确写出来的目标也可以成为make的最终目标,也就是说只要make可以找到其隐含规则推导,那么这个隐含目标同样可以被指定成终极目标

更进一步来说,有一个make的环境变量叫MAKECMDGOALS,这个变量中会存放你所指定的终极目标的列表,如果在命令行上没有指定目标,那么这个变量是空值,比如下面的例子:

sources = foo.c bar.c
ifneq( $(MAKECMDGOALS),clean)
include $(sources:.c=.d)
endif

如果执行时输入的make命令不是make clean,那么makefile会自动包含foo.d和bar.d这两个记录依赖关系的makefile。基于上面的这个例子可以发现使用显式指定最终目标的方法通常可以更方便灵活地编译程序。

makefile规则中的目标依赖关系

目标文件和依赖文件的路径搜索

在一些工程中有大量的源文件,通常的做法是把这许多的源文件分类,并存放在不同的目录中。当make需要去找寻文件的目标依赖关系时,虽然可以在文件前加上路径,但最好是把路径告诉make,让make自动去找。makefile文件中的特殊变量VPATH(看到这种格式就该知道是make预定义的环境变量)就是完成这个功能的,如果当前环境没有定义这个变量,make只会在当前的目录中去找寻依赖文件和目标文件。如果定义了VPATH变量,那么make就会在当前目录找不到指定文件的情况下,到VPATH指定的目录中去寻找文件。
注意:

  • 当前目录永远是搜索优先权最高的地方
  • make搜索目标和依赖文件的路径与其命令执行的路径不是一个概念,后者是由Shell决定的

VPATH的定义中,使用空格或者冒号:将多个目录分开,搜索的顺序按照定义中的目录顺序进行,如:

VPATH = src:…/headers
VPATH = src:… headers

此例的定义指定两个目录,src和…/headers,make会按照这个顺序进行搜索。

另一个设置文件搜索路径的方法是使用make的vpath关键字,和上面提到的VPATH很类似。vpath可以指定不同模式的文件在不同的目录搜索,如果搜索条件中没有包含模式,那么搜索的文件就是具体的文件名称。它的使用方法主要有三种,假设<pattern>指定了要搜索文件集的模式(也就是带模式字符的文件名),而<directories>则指定了的文件集的搜索目录:

  • 为符合模式的文件指定搜索目录,多路径的用法和VPATH差不多,都是使用空格或者是冒号将文件名分隔开

vpath [<pattern1>:<pattern2>:…] <directories>

  • 清除符合模式的文件的搜索目录,vpath不加目标目录单独使用的意思是清除模式已被设置的文件搜索路径。

vpath [<pattern1>:<pattern2>:…]

  • 清除所有已被设置好了的文件搜索目录

vpath

可以连续地使用vpath语句以指定不同搜索策略。如果连续的vpath语句中指定类相同的模式,那么make会按照vpath语句的先后顺序来执行搜索。如:

vpath %.c foo:bar
vpath %.c blish

表示匹配到的.c结尾的文件,先在foo目录搜索,然后是bar目录,最后是blish目录。

使用VPATH还是vpath的搜索方法,主要是基于是否需要加入搜索条件。如果搜索路径下的文件较少,或者是搜索的文件不能使用模式符表示,可以考虑用VPATH,当然多数情况用vpath更好,因为可以过滤掉不符合条件的文件,搜索效率更高。

伪目标文件

伪目标文件并不是一个文件而只是一个标签。所谓的伪目标可以这样来理解,它并不会创建目标文件,定义它的目的只是单纯想去执行目标的命令。make无法生成伪目标文件的依赖关系,因此就没法通过默认目标的依赖关系链来执行,只有通过显式地指明这个“目标”才能让其生效。
伪目标文件的取名不能和已有文件名重名。为了避免和文件重名的这种情况,可以使用一个特殊的标记.PHONY来显示地指明一个目标是伪目标,以向make说明不管是否有这个文件,这个目标就是伪目标。最典型的一个例子就是make clean,由于考虑到项目可能要重编译,因此基本都会在makefile设定这个规则以便清楚之前生成的文件,如:

.PHONY : clean
clean:
    rm *.o temp

只要有.PHONY的声明,不管是否有clean文件,要运行clean的规则只有通过make clean命令。
伪目标一般没有依赖的文件,但是实际也可以为伪目标指定依赖文件。伪目标同样可以作为最终目标,只要将其放在第一个。一个典型应用就是,如果你的makefile需要一次生成若干个可执行文件,但是想简单地用一个make命令就完成所有最终目标文件的构建,并且都写在一个makefile中,那么可以使用伪目标all来实现,如:

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伪目标来作为最终目标,它的依赖文件就是要生成的可执行文件。这样的话只需要一个make命令,就会同时生成三个可执行文件,并且由于伪目标all每次都是直接被执行,因此就相当于永远最新,所以每次它所依赖的三个可执行文件都会重新生成。在上例中可以使用以下命令生成三个目标:

make
make all

也可以单独编译这三个中的任意一个源文件:

make prog1
make prog2
make prog3

伪目标文件不仅可以有依赖文件,也可以成为别的文件的依赖(所以除了不指定生成文件外,伪目标具有普通的目标相似的特性)。看下面的例子:

.PHONY: cleanall cleanobj cleandiff
cleanall : cleanobj cleandiff
    rm program
cleanobj :
    rm *.o
cleandiff :
    rm *.diff

cleanobj和cleandiff这两个伪目标有点像“子程序”的意思。可以通过输入make cleanall、make cleanobj和make cleandiff等命令来达到清除不同种类文件的目的。伪目标文件的另一种使用的场合是在make的并行和递归执行的过程中,此情况下一般会存在一个变量,定义为所有需要make的子目录。对多个目录进行make的实现,可以在一个规则的命令行中使用 shell循环来完成,如:

SUBDIRS=foo bar baz
subdirs:
    for dir in $(SUBDIRS);do $(MAKE) -C $$dir;done  

此例代码表达的意思是当前目录下存在三个子文件目录,每个子目录文件都有相对应的makefile文件,代码中实现的部分是用当前目录下的makefile控制其它子模块中的makefile的运行,但是这种实现方法存在以下几个问题:

  • 当子目录执行make出现错误时,make不会退出。就是说,在对某个目录执行make失败以后,会继续对其他的目录进行make。因此如果最终执行失败,很难根据错误提示定位出哪个目录的makefile有错误,这样给问题定位造成很大的困难。为了解决问题可以在命令部分加入错误检测,在命令执行的错误后主动退出。但是如果在执行make时使用了-k选项,此方式将失效。
  • 另外一个问题就是使用这种shell循环方式时,没有用到make对目录的并行处理功能,由于规则的命令是一条完整的shell命令,不能被并行处理

伪目标的命名

在Unix世界中,软件发布时,特别是GNU这种开源软件的发布时,其makefile都包含了编译、安装、打包等功能,因此可以参照一种约定俗成的规则(或者说规范)来命名makefile中的一些伪目标,这样就可以根据指定的不同的目标来完成不同的事。

伪目标名 功能
all 所有目标的目标,其功能一般是编译所有的目标
clean 删除所有被make创建的文件
install 安装已编译好的程序,一般就是把最终生成的可执行文件拷贝到指定的目录中去
print 列出改变过的源文件
tar 把源程序打包备份成一个tar文件
dist 创建一个压缩文件,一般是把tar文件压成Z文件,或是gz文件
TAGS 更新所有的目标,以备完整地重编译使用
check/test 这两个伪目标一般用来测试makefile的流程

当然一个项目的makefile中也不一定要编写这样的目标,只不过提供这些目标的功能可以显得自身项目更加正规、可维护,增强makefile的可读性,同时也相当于为用户使用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-pattern和prereq-patterns

首先再详细说明一下模式字符%的用法:
首先从目标模式target-pattern的目标名字中抽取一部分字符串(称为“茎”)。使用“茎”替代依赖模式(prereq-patterns)中的相应部分来产生对应目标的依赖文件。举个例子来说明,如果target-parrtern定义成%.o,则集合中都是以.o结尾的,如果prereq-patterns定义成%.c和.h(从patterns一词就可以看出依赖模式可以有多种),意思是对target-parrtern所形成的目标集进行二次定义,其推导方法是取target-pattern模式中的%(也就是去掉了[.o]这个扩展名),并为其加上[.c,.h]的扩展名,形成的新集合。“目标模式”或是“依赖模式”中都应有%这个字符(当然依赖模式里没有也是合法的),如果文件名中有%那么可以使用反斜杠\进行转义,来表示真实的%字符。看一个例子:

objects = foo.o bar.o
all: $(objects)
$(objects): %.o: %.c %.h
    $(CC) -c $(CFLAGS) $< -o $@

上例中指明目标从($object)中获取,%.o表示所有以.o结尾的目标也就是foo.o和bar.o,推导出的依赖文件就是foo.c和bar.c。命令中的$<和$@则是自动化变量,$<表示所有的依赖目标集。于是,上面的规则展开后等价于下面的规则:

foo.o : foo.c
    $(CC) -c $(CFLAGS) foo.c -o foo.o
bar.o : bar.c
    $(CC) -c $(CFLAGS) bar.c -o bar.o

如果模式目标很多,比如有几百个的话,那么只要用这种很简单的静态模式规则就可以写完很多规则,可以极大提升效率。静态模式规则的用法很灵活,可以实现很多强大的功能。

注意:在使用静态模式规则时,指定的目标必须和目标模式相匹配,否则在执行make时将会报错。在指定目标集方面静态规则与正常的多目标文件规则是一致的,这很好理解因为如果有目标没法跟模式匹配的话,就说明它很可能不适用于该规则,在规则构建时这个目标无法生成。换句话说模式字符%在静态规则里面仅使用了其推导依赖的能力,并不能用到它匹配筛选的能力。如果存在一个文件列表,其中只有一部分符合某种模式,这种情况下可以使用filter函数对这个文件列表进行分类,在分类之后对确定的某一类使用模式规则,例如:

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 $<

自动生成目标依赖关系

如果是一个大型工程则必需清楚哪些源文件包含了哪些头文件,并且在加入或删除头文件时,都需要小心地修改makefile,这是一个很不具有可维护性的工作。为了避免这种繁重而又容易出错的事,可以使用c/c编译的一个功能,大多数的c/c编译器都支持一个-M的选项,即自动搜索源文件中包含的头文件并生成一个目标依赖关系,需要注意的是,如果你使用GCC编译器需要用-MM参数,不然-M参数会将标准库的头文件也包含进来,例如:(这只是最简单的例子,甚至源文件都没有包含自己的头文件)

这是由编译器生成的目标依赖关系,这样一来就不必再手动编写若干文件的目标依赖关系,直接将其引入makefile中就可以了。不过GNU组织推荐的方法是把编译器为源文件自动生成的目标依赖关系专门放到一个文件中,也就是为每个[.c]文件生成一个[.d]的makefile文件,[.d]文件中就存放对应[.c]文件的目标依赖关系。然后在主makefile文件中写出[.c]文件和[.d]文件的目标依赖关系,让make自动更新[.d]文件并包含在主Makefile中,这样就可以达到自动化生成每个文件目标依赖关系的效果了,例:

%.d: %.c
    @set -e; rm -f $@; \
    $(CC) -M $(CPPFLAGS) $< > $@.; \
    sed 's,$∗\.o[ :]*,\1.o $@ : ,g' < $@.> $@; \
    rm -f $@.

上例这个规则的意思是:

  • 指定目标依赖关系:所有的[.d]文件依赖于[.c]文件
  • set -e表示执行这段脚本文件时有异常则退出,rm -f $@的意思是删除所有的目标文件
    • set -e前加上@表示shell在解释命令时不打印命名信息(通常,make会把其要执行的命令在执行前输出到屏幕上)
  • 使用编译程序的-M选项生成此规则中依赖文件(也就是[.c]文件)的目标依赖关系,重定向输出到临时文件$@.中
    • .表示在目标文件格式后面加上随机编号,比如filename.d.12345
  • sed命令表示根据命令提供的规则来处理文本文件,接下来借助sed命令将$@.中的内容替换后重定向到目标文件$@中
  • 最后删除所有临时文件

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

main.o : main.c defs.h

转成:

main.o main.d : main.c defs.h

此时[.d]文件会自动更新,然后自动生成,还可以在这个[.d]文件中加入目标依赖关系的同时,将包括生成的命令一并加入,让每个[.d]文件都包含一个完赖的规则。一旦完成这个工作,接下来就把这些自动生成的规则放进主makefile中,需要注意的是文件的包含次序,因为最先载入的[.d]文件中最终目标会成为默认目标,所以包含语句必须要放到主makefile目标的后面。下面是一个逻辑相对完整的makefile:

SRCS:=$(wildcard *.c)
OBJS=$(SRC:.c=.o)
all: dep main
.PHONY:all
dep:%d:%c
    @set -e;rm -f $@; \
    cc -MM $(CPPFLAGS) $< > $@.; \ 
    sed 's/$∗\.o[ :]*/ \1.o $@ : /g' < $@. > $@; \
    rm -f $@.
main:$(OBJS)
    cc -o main $^
include $(SRCS:.c=.d)
clean:
    rm -f *.o *.d main

包含.d文件的makefile是典型的有可能存在重建过程的makefile。make在读入所有makefile文件之后,首先将所读取的每个makefile作为一个目标,寻找更新它们的规则。如果存在一个更新某一个makefile文件明确规则或者隐含规则,就去更新对应的makefile文件,完成对所有的makefile文件的更新之后,make清除本次执行的状态重新读取一遍所有的makefile文件,然后再开始执行生成规则(因为所有makefile都被更新过一遍,一般到这里是不需要再重建一次的,除非是极个别情况)。在本例中:

  • .d文件被包含进主makefile,并且存在以它们作为目标的规则的情况
  • 如果.d文件还不存在,或者时间戳早于其依赖的.c和.h文件,说明.c在还没编译或在上次编译后更新过,所以此时需要创建或更新.d文件
  • .d文件更新后,重建主makefile(重新读取所有makefile文件)
  • 此时.d已经是最新,所以直接将其内容(.c文件的目标依赖关系)展开在include处

Makefile规则中的命令

命令的执行

makefile规则中的命令本身(当然指的是经过make处理后的形式)和Shell的命令是一致的。当目标依赖关系成立时(依赖时间戳新),make会顺序执行命令,每条命令的开头必须以[Tab]键开头,除非命令紧跟在目标依赖关系后的分号之后。在命令行之间的空字符会被忽略,但是如果该字符是以Tab键开头的,那么make会认为其是一个空命令。需要注意的是在makefile中只能在规则中执行Shell脚本,其他地方的命令都是无效的。
在不同系统的Shell本身也有所区别

  • 在类UNIX系统中make一般是使用环境变量SHELL中所定义的系统Shell来执行命令(除非显式地特别指定一个其它Shell),Shell程序一般存放在/bin目录下
    • 注意在GNU make中默认是/bin/sh,不像其他的绝大多数变量只可以直接从同名的系统环境变量那里获得,make的环境变量SHELL没有使用环境变量的定义
    • 因为系统环境变量SHELL指定的那个程序被用来作为用户和系统交互的接口程序,对于不存在直接交互过程的make显然不合适,因此在make中环境变量SHELL被定义为/bin/sh
  • Windows没有SHELL环境变量(Windows的cmd.exe支持的是DOS体系的命令),当然这个可以自定义,假设有这个变量的情况下
    • make首先在SHELL指定路径中找寻命令解释器,如果找不到会在当前盘符中的当前目录中寻找,如果再找不到其会在PATH环境变量中所定义的所有路径中寻找
    • 如果还没有找到,其会给定义的命令解释器名称加上诸如.exe、.com、.bat、.sh等后缀再找

另外,如果要让上一条命令的结果应用在下一条命令时,应该使用分号分隔这两条命令。比如第一条命令是cd命令,并且希望第二条命令在切换后的目录上运行:

exec:
    cd /home/hchen
    pwd

此时在命令行窗口中执行make exec时,pwd会打印出当前makefile所在目录,也就是cd命令的结果并没有影响到下一条命令。

exec:
    cd /home/hchen; pwd

在第二个例子中,cd命令就可以起到作用,pwd会打印出/home/hchen。因为在makefile中执行shell命令,每行都会创建一个子shell进程来执行,执行完后就默认exit后回到父进程,所以不同行的命令之间无法直接通信,当然也不存在依赖。这也是为什么很多makefile中有很多行的末尾都是“; \”,以此来保证代码是一行而不是多行,这样一个规则的命令可以在一个进程中执行。

命令回显

通常make工具会把其要执行的命令在执行前回显到命令行窗口中。当加一个@字符在命令前时,那么这个命令将不被make显示出来,最具代表性的例子是用这个功能来向屏幕显示一些信息。如:

@echo 正在编译XXX模块…

当make执行时,会输出“正在编译XXX模块…”的字串,但不会回显命令信息,如果没有@那么make将输出:

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

如果make执行时,带入make参数-n或--just-print,那么其只是回显命令但不会执行命令,这个功能有利于调试makefile,看看命令是执行起来是什么样子或是什么顺序的。而make参数-s或--slient则是全面禁止命令的回显。

错误处理

每当命令运行完后,make会检测每个命令的退出码,如果命令返回成功信号(类似于exit 0),那么make会执行下一条命令,当规则中所有的命令成功返回后,这个规则就算成功完成了。如果一个规则中的某个命令非正常地退出了(命令退出码非零),那么make就会终止执行当前规则,而这将很有可能终止所有规则的执行。

  • 除非使用make的-k或是--keep-going参数,这个参数的意思是如果某规则中的命令出错了,那么就终止该规则的执行但继续执行其它规则
  • 另外makefile中存在规则并发执行的方法,此时终止一个另一个还可以执行

但是有些时候,命令的非正常返回并不表示就是程序是错误的。例如mkdir命令一定需要建立一个目录,如果目录不存在那么mkdir成功执行,如果目录存在,那么就出错了。但是之所以使用mkdir的目的就是保证这样一个目录而已,此时并不希望因mkdir出错而终止规则的运行。为了达到这一目的可以在命令前加一个减号-(在Tab键之后),标记为忽略命令错误,如:

clean:
    -rm -f *.o

在命令前加-是在命令级别上忽略错误的方法,此外还存在不同级别的方法也可以忽略错误,可以根据不同情况具体设置:

  • 一个全局的办法是给make加上-i或--ignore-errors参数,那么makefile中所有命令都会忽略错误
  • 如果将目标声明为.IGNORE,那么这个目标所在的规则中所有命令将会忽略错误

命令中嵌套执行makefile

很多时候不同模块或是不同功能的源文件划分在不同的目录中,此时可以在每个目录中都编写一个该目录的makefile,这有利于让makefile更加地简洁,而不至于把所有东西全部堆砌在一个makefile中,这个方法对于模块编译和分段编译有着非常大的好处。例如,有一个子目录subdir,这个目录下有一个makefile文件来指明这个目录下文件的编译规则,那么主makefile可以这样编写:

subsystem:
    cd subdir && $(MAKE)  # &&第一条命令返回真后,第二条命令才能够被执行

其等价于:

subsystem:
    $(MAKE) -C subdir

定义$(MAKE)变量的意思是将make需要的一些参数定义好,这样比较利于维护。环境变量CURDIR代表make的当前工作目录,除非在makefile中对此变量进行显式的赋值操作将其改为其他目录。当使用make的选项-C时,make执行命令时就会切换到指定的目录中,CURDIR变量被重新赋值为指定目录。这两个例子的意思都是先进入subdir目录,然后执行make命令,这就是嵌套执行make,一般将这个正在执行的makefile叫做“总控makefile”。总控makefile的环境变量可以传递到下级makefile中,但是不会覆盖下层的makefile中所定义的变量,除非指定了-e参数。如果需要传递某些变量到下级makefile中,那么可以这样声明:

export<variable …>

如果不想让某些变量传递到下级makefile中,可以这样声明:

unexport<variable …>

如果需要传递所有的变量,那么只要一个export关键字就行了,后面什么都不用跟,表示传递所有的变量。需要注意的是有两个变量,一个是SHELL,一个是MAKEFLAGS,这两个变量不管是否export,其值总是传递到下层makefile中。特别是MAKEFLAGS变量,其中包含了make的参数信息,如果执行总控makefile时有make参数或是在上层makefile中定义了这个变量,那么MAKEFLAGS变量会将这些参数传递到下层makefile中。如果不想往下层传递MAKEFLAGS参数,那么可以这样:

subsystem:
    cd subdir && $(MAKE) MAKEFLAGS=#这种情况下如果还想重写MAKEFLAGS值需要用override

另外makefile中有几个特别的参数默认就不会往下传递,它们是-C、-f、-h、-o、-W。注意,如果定义了环境变量MAKEFLAGS,那么要确定其中的选项是所有makefile都会用到的,尤其是有-t、-n、-q等参数,否则很容易出现让人迷惑的情况。(-t、-n、-q的共同特点是不执行命令而是对makefile规则中的依赖关系进行调试,但是对于使用+声明或者使用$(MAKE)嵌套执行的命令是不起作用的,无论是否使用了这三个参数之一这些命令都得到执行)

-w或--print-directory参数会在make的过程中输出一些关于目前工作目录的信息。比如,如果下级目录与主控目录不是一个,下级makefile目录是/home/hetumessi,使用make -w执行主控makefile,那么当进入该目录时会在命令行窗口看到:

make: Entering directory /home/hetuemssi

而在执行完成下层makefile后离开目录时会看到:

make: Leaving directory /home/hetumessi

当使用-C参数指定make下层makefile时,-w会被自动打开,除非参数中有-s、--slient或是--no-print-directory,那么此时-w是失效的。

定义命令包

如果makefile中总是出现一些相同的命令序列(不过一般都是定义在一行的),那么可以为这些命令序列定义一个变量(多行变量)。定义这种命令序列的语法以define开始,以endef结束(定义多行变量也是以这种方式),如:

define run-yacc
    yacc $(firstword $^)
    mv y.tab.c $@
endef

foo.c : foo.y
    $(run-yacc)

这里run-yacc是这个命令包的名字,不能和makefile中其他变量重名。在define和endef中的两行就是命令序列。这个命令包中的第一个命令是运行Yacc程序,因为Yacc程序总是生成y.tab.c的文件,所以第二行的命令就修改一个这个文件的名字。使用这个命令包就像使用变量一样。make在执行命令包时,命令包中的每个命令会被依次顺序执行。

makefile隐含规则

在使用makefile时,有一些规则的使用频率非常高,比如编译c/c++的源程序为可重定位目标文件(Unix下是[.o]文件,Windows下是[.obj]文件),隐含规则所指的就是这样一些在makefile中的“隐含的”,早先约定了的,不需要再写出来的规则,就像把[.c]文件编译成[.o]文件这一规则根本不需要写出来,make会自动推导出这种规则,并生成需要的[.o]文件。在make执行时根据需要也可能使用多个隐含规则,比如make将从一个.y文件生成对应的.c文件,最后再生成最终的.o文件,也就是说只要目标文件名中除后缀以外其它部分相同,make都能够使用若干个隐含规则来最终产生这个目标文件(当然最原始的那个依赖文件必须存在)。makefile内嵌的隐含规则会使用一些系统变量,因此可以改变这些系统变量的值来自己定制隐含规则的运行时参数。此外,还可以通过模式规则或后缀规则的方式自定义隐含规则,不过用后缀规则来定义会有许多的限制,相比下使用模式规则会更加清晰明了,但后缀规则可以保证makefile的兼容性。
隐含规则提供了一个编译整个工程非常高效的手段,一个工程中毫无例外的会用到makefile的隐含规则。了解隐含规则可以了解一些makefile约定俗成了的东西,而不至于在运行makefile时才发现一些令人困惑的情况,当然,任何事物都是辩证的,隐含规则也会给makefile造成不小的麻烦,只有很好地了解隐含规则,才能更好地使用makefile。

makefile隐含规则的使用

如果要使用隐含规则生成需要的目标,需要做的就是不写出这个目标的规则,此时make会试图去自动根据已存在(或者可以被创建)的源文件类型推导产生这个目标的规则和命令,如果make可以自动推导并启动生成这个目标的规则和命令,那么这个行为就是隐含规则的自动推导,例如下面的一个makefile:

foo : foo.o bar.o
    cc –o foo foo.o bar.o $(CFLAGS) $(LDFLAGS)

可以注意到这个makefile中并没有写下如何生成foo.o和bar.o这两个目标文件的规则和命令,因为make的隐含规则功能会自动去推导这两个目标文件的依赖目标和生成命令,在隐含规则库中寻找可以用的规则,如果找到则使用该规则,找不到则报错。在上例中,make调用的隐含规则是,把[.o]的目标的依赖文件置为[.c],并使用c的编译命令来生成[.o]的目标,也就是说会自动生成下面两条规则:

foo.o:foo.c
    cc -c foo.c $(CFLAGS)
bar.o:bar.c
    cc -c bar.c $(CFLAGS)

make执行过程中找到的隐含规则提供了此目标的基本依赖关系,并确定目标的依赖文件(通常是源文件,不包含对应的头文件依赖)和生成或重建目标文件所需要使用的命令。隐含规则所提供的只是一个最基本的依赖文件(通常它们之间的对应关系为:EXENAME.o对应EXENAME.c、EXENAME对应于EXENAME.o),当需要增加目标文件的依赖文件时,需要额外指定一个没有命令的规则。每一个内嵌的隐含规则中都存在一个目标模式和依赖模式,而且同一个目标模式可以对应多个依赖模式,例如一个.o文件的目标可以由c编译器编译对应的.c源文件得到、Pascal编译器编译.p的源文件得到等等,make会根据不同的源文件来使用不同的编译器,对于foo.c就是用c编译,对于foo.p就使用Pascal编译器编译。make会自动根据已存在或可被创建的源文件类型来启动相应的隐含规则,可被创建文件是指这个文件在makefile中作为目标文件或依赖文件被明确的提及,或者可以根据已存在文件使用其它隐含规则来创建,当一个隐含规则的目标是另外一个隐含规则的依赖时,称它们是一个隐含规则链。通常make会对那些没有命令的规则、双冒号规则寻找一个隐含规则来执行,以及作为一个规则的依赖文件,在没有一个规则明确描述它的依赖关系的情况下,make会将其作为一个目标并为它搜索一个隐含规则试图生成或重建它。注意即使给目标文件指定明确的依赖文件也并不会影响隐含规则的搜索规则,比如:

foo.o: foo.p

这个规则指定了foo的依赖文件是foo.p,但是如果在工作目录下存在同名.c源文件foo.c,执行make的结果就不是用pc编译foo.p来生成foo,而是用cc编译foo.c来生成目标文件,这是因为在隐含规则列表中对.c文件的隐含规则处于.p文件隐含规则之前。这种情况当需要给目标指定明确的生成或重建规则时,规则描述中就不能省略命令,这个规则必须提供明确的生成或重建命令来说明目标需要的动作,比如为了能够在存在foo.c的情况下编译foo.p,就需要显式指定规则:

foo.o:foo.p
    pc -o $@ $<

这一点在多语言实现的工程编译中需要特别注意,否则编译行为可能与期望不符。

空命令规则

当不想让make为一个没有命令的规则中的目标文件搜索隐含规则时,此时需要使用空命令来实现,也就是定义一个不需要任何执行的命令,规则中只有目标文件(或者也可以存在依赖文件)而没有命令,定义方式为:

target: ; 或者
terget:
[Tab键]

需要注意的是独立命令行的空命令必须以[Tab]字符开始,一般在定义空命令时,建议不使用命令行的方式,因为看起来空命令行和空行在感觉上没有区别。空命令行可以防止make在执行时试图为生成或重建这个目标去查找隐含命令,包括了使用隐含规则中的命令和.DEFAULT指定的命令。对于空命令规则,最好也不要给它指定依赖文件,避免特殊情况下产生错误的情况。

常见的makefile内嵌隐含规则

以下是常用的一些隐含规则(对于不常见的隐含规则这里没有描述),除非在makefile有显式定义、或者使用命令行-r或-R参数取消使用隐含规则,否则这些隐含规则将始终有效:

  1. 编译c程序
    • N.o自动由N.c生成,执行命令为$(CC) -c $(CPPFLAGS) $(CFLAGS)
  2. 编译c++程序
    • N.o自动由N.cc或者N.C生成,执行命令为$(CXX) -c $(CPPFLAGS) $(CFLAGS),建议使用.cc作为c++源文件的后缀,而不是.C作为后缀
    • 个人测试.cpp也是可以的,所以如果项目不是纯面向UNIX系统的话应该也没必要用.cc后缀
  3. 编译Pascal程序
    • N.o自动由N.p创建,执行命令为$(PC) -c $(PFLAGS)
  4. 编译和预处理Fortran/Ratfor程序
    • N.o自动由N.r、N.F或N.f生成,根据源文件后缀执行对应的命令:
      • .f后缀:$(FC) –c $(FFLAGS)
      • .F后缀:$(FC) –c $(FFLAGS) $(CPPFLAGS)
      • .r后缀:$(FC) –c $(FFLAGS) $(RFLAGS)
    • N.f自动由N.r或N.F生成,此规则只是转换一个Ratfor或没有预处理的Fortran程序到一个标准Fortran程序,根据源文件后缀执行对应的命令:
      • .F后缀:$(FC) –F $(CPPFLAGS) $(FFLAGS)
      • .r后缀:$(FC) –F $(FFLAGS) $(RFLAGS)
  5. 编译Modula-2程序
    • N.sym自动由N.def生成,执行的命令为$(M2C) $(M2FLAGS) $(DEFFLAGS)
    • N.o自动由N.mod生成,执行的命令为$(M2C) $(M2FLAGS) $(MODFLAGS)
  6. 汇编和需要预处理的汇编程序
    • N.s是不支持预处理的汇编源文件(gcc的特性,如果想在汇编文件里面使用宏的话不能用.s后缀),N.S是需要预处理的汇编源文件
    • N.o自动由N.s生成,执行命令为$(AS) $(ASFLAGS)
      • 测试结果是.s直接调用as汇编器,.S是调用gcc
    • N.s由N.S通过c预编译器cpp生成,执行命令为$(CPP) $(CPPFLAGS)
  7. 链接单一的可重定位目标文件
    • N自动由N.o生成,执行命令为$(CC) $(LDFLAGS) N.o $(LOADLIBES) $(LDLIBS)
    • 此规则仅适用于由一个同名源文件直接产生可执行文件的情况,当需要有多个源文件共同来链接成一个可执行文件时,需要在makefile中增加隐含规则的依赖文件
      • 例:规则x : y.o z.o,当x.c、y.c和z.c都存在时,执行如下命令

cc -c x.c -o x.o
cc -c y.c -o y.o
cc -c z.c -o z.o
cc x.o y.o z.o -o x
rm -f x.o
rm -f y.o
rm -f z.o

  1. Yacc C程序
    • N.c自动由N.y生成,执行的命令为$(YACC) $(YFALGS)
  2. Lex C程序
    • N.c自动由N.l生成,执行的命令为$(LEX) $(LFALGS)

在隐含规则中,命令是使用一个变量展开得到的,这些变量被展开之后就是对应的命令(包括了命令行选项),例如变量COMPILE.c的值定义为cc -c(更进一步说应该是$(CC) -c),如果makefile中存在CFLAGS的定义,选项的值会存在于这个变量中,make会根据默认的约定,使用COMPILE.x来编译一个.x的文件,类似地使用LINK.x来连接.x文件,使用PREPROCESS.x对.x文件进行预处理。

对于输出文件,每个隐含规则在创建一个文件时都使用变量OUTPUT_OPTION,make根据执行命令中是否包含-o选项决定它的值,如果没有的话它的值为-o $@,否则为空。OUTPUT_OPTION的另一个用法是将生成或重建的目标文件移回到当前目录,这是因为在编译一个多目录项目时,如果makefile中使用了VPATH或vpath指定不同的搜索目录时,编译后的.o文件或者其它文件会出现在搜索目录中,可能与预期文件结构不符,因此一般来说在规则的命令中建议明确使用-o选项指定输出文件路径,然而有些系统的编译器不接受-o选项,那么解决这个问题的方式就是将OUTPUT_OPTION的值赋为;mv $*.o $@(在编译命令后面直接多加一条移动文件到当前目录的命令),这样就可以将编译完成的.o目标文件移动到当前工作目录(对于其他后缀的文件同理)。