《跟我一起写makefile》笔记

Posted by wsxq2 on 2018-04-25
TAGS:  makeMakefile

本文最后一次编辑时间:2019-12-26 10:23:21 +0800

本文是笔者根据跟我一起写Makefile一文学习 Makefile 的笔记。其目录和原文具有高度相似性,以便查阅

快速参考:

目录:

概述

make是一个命令工具,是一个解释makefile中指令的命令工具,一般来说,大多数的IDE都有这个命令,比如:Delphi 的make,Visual C++ 的nmake,Linux 下 GNU 的make

  • 本文讲述对象:GNU make
  • 环境:redhat Linux 8.0 make 3.80
  • 默认编译器:unix gcc/cc

关于程序的编译和链接

在此,我想多说关于程序编译的一些规范和方法。一般来说,无论是c还是c++,首先要把源文件编译成中间代码文件,在windows下也就是.obj文件,unix下是.o文件,即objectfile,这个动作叫做编译(compile)。然后再把大量的objectfile合成执行文件,这个动作叫作链接(link)。

编译时,编译器需要的是语法的正确,函数与变量的声明的正确。对于后者,通常是你需要告诉编译器头文件的所在位置(头文件中应该只是声明,而定义应该放在c/c++文件中),只要所有的语法正确,编译器就vim可以编译出中间目标文件。一般来说,每个源文件都应该对应于一个中间目标文件(.o文件或.obj文件)。

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

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

Makefile 介绍

Makefile 规则

1
2
3
4
target ... : prerequisites ...
    command
    ...
    ...

makefile 核心规则: prerequisites中如果有一个以上的文件比target文件要新的话,command所定义的命令就会被执行

需要注意的是:其中command前的缩进只能是\t(制表符),而不能是多个连续的空格

一个示例

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

make自动推导

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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) #'-'忽略某些文件出现的问题

Makefile 里有什么?

  1. 显式规则。显式规则说明了如何生成一个或多个目标文件。这是由makefile的书写者明显指出要生成的 文件、文件的依赖文件和生成的命令。
  2. 隐晦规则。由于我们的make有自动推导的功能,所以隐晦的规则可以让我们比较简略地书写 makefile,这是由make所支持的。
  3. 变量的定义。在makefile中我们要定义一系列的变量,变量一般都是字符串,这个有点像你c语言中的 宏,当makefile被执行时,其中的变量都会被扩展到相应的引用位置上。
  4. 文件指示。其包括了三个部分,一个是在一个makefile中引用另一个makefile,就像 C 语言中的#include一样;另一个是指根据某些情况指定makefile中的有效部分,就像 C 语言中的预编译#if一 样;还有就是定义一个多行的命令。
  5. 注释。makefile中只有行注释,和unixshell脚本一样,其注释是用 # 字符,这个就像C/C++中的 // 一样。如果你要在你的makefile中使用 # 字符,可以用反斜杠进行 转义,如: \#

Makefile 的文件名

  • 查找顺序: GNUmakefile, makefile, Makefile
  • 使用别的文件名: make -f make.linux

引用其它的 Makefile

1
-include foo.make *.mk $(BAR) #无论 include 过程中出现什么错误,都不要报错,而是继续执行

寻找目录:

  1. 如果make执行时,有-i--include-dir参数,那么make就会在这个参数所指定的目录下去寻找。
  2. 如果目录<prefix>/include(一般是:/usr/local/include/usr/include)存在的话,make也会去找。

make的工作方式

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

书写规则

1
2
3
targets : prerequisites ; command
    command
    ...

在规则中使用通配符

*, ?, ~

  1. ~root/test
  2. OBJECTS := $(wildcard *.o)
  3. OBJECTS = *.o
1
2
3
print: *.c
    lpr -p $?
  touch print

另一个例子:

  1. 列出一确定文件夹中的所有 .c 文件。
    1
    
    OBJECTS := $(wildcard *.c)
    
  2. 列出(1)中所有文件对应的 .o 文件,在(3)中我们可以看到它是由make自动编译出的:
    1
    
    $(patsubst %.c,%.o,$(wildcard *.c))
    
  3. 由(1)(2)两步,可写出编译并链接所有.c.o文件
    1
    2
    3
    
    OBJECTS := $(patsubst %.c,%.o,$(wildcard *.c))
    foo : $(OBJECTS)
        cc -o foo $(OBJECTS)
    

文件搜寻

VPATH = src:../headers

上面的定义指定两个目录,src../headersmake会按照这个顺序进行搜索。目录由:分隔 。(当然,当前目录永远是最高优先搜索的地方)

另一个设置文件搜索路径的方法是使用makevpath关键字:

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

vapth使用方法中的<pattern>需要包含%符。%的意思是匹配零或若干字符。例如:

vpath %.c foo:bar
vpath %   blish

伪目标

为了避免clean和文件clean重名, 故使用.PHONY来显式地指明一个目标是“伪目标”,即向make说明,不管是否有这个文件,这个目标就是“伪目标”。

1
2
3
.PHONY : clean
clean :
    rm *.o temp

生成若干目标:

1
2
3
4
5
6
7
8
9
10
11
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

目标也可以成为依赖。所以,伪目标同样也可成为依赖:

1
2
3
4
5
6
7
8
9
10
.PHONY : cleanall cleanobj cleandiff

cleanall : cleanobj cleandiff
    rm program

cleanobj :
    rm *.o

cleandiff :
    rm *.diff

多目标

1
2
3
4
5
6
7
8
9
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

静态模式

1
2
3
<targets ...> : <target-pattern> : <prereq-patterns ...> #prereq为依赖
    <commands>
    ...
1
2
3
4
5
6
7
8
9
10
11
12
13
OBJECTS = foo.o bar.o

all: $(OBJECTS)

$(OBJECTS): %.o: %.c
    $(CC) -c $(CFLAGS) $< -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
1
2
3
4
5
6
7
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 函数,过滤`$(FILES)`集,只要其中模式为`%.o`的内容

自动生成依赖性

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

1
main.o : main.c defs.h

大多数的 C/C++ 编译器都支持一个-M的选项,即自动找寻源文件中包含的头文件,并生成一个依赖关系。例如:

1
2
$ cc -M main.c
main.o : main.c defs.h

如果使用是 GNU 的 C/C++ 编译器,你得用-MM参数,不然,-M参数会把一些标准库的头文件也包含进来。

1
2
$ gcc -MM main.c
main.o : main.c defs.h

那么,编译器的这个功能如何与我们的Makefile联系在一起呢?GNU组织建议把编译器为每一个源文件的自动生成的依赖关系放到一个文件中,为每一个name.c的文件都生成一个name.d的 Makefile 文件,.d文件中就存放对应.c文件的依赖关系:

1
2
3
4
5
6
7
8
%.d: %.c
    @set -e; rm -f $@; \
    $(CC) -M $(CPPFLAGS) $< > $@.$$$$; \
    sed 's,\($*\)\.o[ :]*,\1.o $@ : ,g' < $@.$$$$ > $@; \
    rm -f $@.$$$$

sources = foo.c bar.c
include $(sources:.c=.d)

使用变量

  • 可以包含: 字符(如$<, $@, $^等)、数字、下划线(可以是数字开头,如$1
  • 不能含有: :#= 或是空字符(空格、回车等)
  • 大小写敏感: 是
  • 命名惯例:传统的 Makefile 的变量名是全大写的命名方式,但原文作者推荐使用大小写搭配的变量名,如: MakeFlags。他认为这样可以避免和系统的变量冲突,而发生意外的事情

变量的基础

变量在声明时需要给予初值,而在使用时,需要给在变量名前加上$符号,但最好用小括号()或是大括号{}把变量给包括起来(给变量加上括号完全是为了更加安全地使用这个变量)。如果你要使用真实的$字符,那么你需要用$$来表示。变量会在使用它的地方精确地展开,就像 C/C++ 中的宏一样,但是不推荐这么做

变量中的变量

  1. =, 可以把变量的真实值推到后面来定义, 但是其递归定义会让make陷入无限的变量展开过程中去
    1
    2
    3
    4
    5
    6
    7
    
    foo = $(bar)
    bar = $(ugh)
    ugh = huh?
    all:
        echo $(foo)
       
    CFLAGS = $(CFLAGS) -o
    
  2. :=, 不能使用未定义的变量
    1
    2
    3
    4
    5
    
    y := $(x) bar
    x := foo
    #上述语句等价于:
    y := bar
    x := foo
    

    复杂一点的例子:

    1
    2
    3
    4
    5
    6
    
    ifeq (0,${makelevel})
    cur-dir   := $(shell pwd)
    whoami    := $(shell whoami)
    host-type := $(shell arch)
    make := ${make} host-type=${host-type} whoami=${whoami}
    endif
    

    定义值为空格的变量space:

    1
    2
    
    nullstring :=
    space := $(nullstring) # end of the line
    

    因为在操作符的右边是很难描述一个空格的,这里采用的技术很管用,先用一个nullstring变量来标明变量的值开始了,而后面采用#注释符来表示变量定义的终止,这样,我们可以定义出其值是一个空格的变量。请注意这里关于#的使用,注释符#的这种特性值得我们注意,如果我们这样定义一个变量:

    1
    
    dir := /foo/bar    # directory to put the frobs in
    

    dir这个变量的值是/foo/bar ,即其后面跟了4个空格,如果我们这样使用这样变量来指定别的目录——$(dir)/file,那么就完蛋了。

  3. ?=, foo ?= bar, 其含义是,如果foo没有被定义过,那么变量foo的值就是bar,如果foo先前被定义过,那么这条语将什么也不做

变量高级用法

  1. 变量值的替换
    1. 使用$(var:a=b):
      1
      2
      
        foo := a.o b.o c.o
        bar := $(foo:.o=.c)
      
    2. 使用静态模式:
      1
      2
      
       foo := a.o b.o c.o
       bar := $(foo:%.o=%.c)
      
  2. 把变量的值再当成变量
    1
    2
    3
    4
    5
    
    x = variable1
    variable2 := hello
    y = $(subst 1,2,$(x))
    z = y
    a := $($($(z))) #a=hello
    

    可以使用多个变量来组成一个变量的名字,然后再取其值:

    1
    2
    3
    4
    
    first_second = hello
    a = first
    b = second
    all = $($a_$b)
    

    一个”变量值的替换”和“函数”与“条件语句”一同使用的例子:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    ifdef do_sort
        func := sort
    else
        func := strip
    endif
       
    bar := a d b g q c
       
    foo := $($(func) $(bar))
    

    “把变量的值再当成变量”这种技术,同样可以用在操作符的左边:

    1
    2
    3
    4
    5
    
    DIR = foo
    $(DIR)_sources := $(wildcard $(DIR)/*.c)
    define $(DIR)_print
    lpr $($(DIR)_sources)
    endef
    

追加变量值

+=: variable += more

如果变量之前没有定义过,那么,+=会自动变成=,如果前面有变量定义,那么+=会继承于前次操作的赋值符。如果前一次的是:=,那么+=会以:=作为其赋值符,如:

1
2
variable := value
variable += more

override 指示符

用于覆盖make的命令行参数设置的参数的值, 如:

1
2
override <variable>; = <value>;
override <variable>; := <value>;

当然,你还可以追加:

1
override <variable>; += <more text>;

define指示符前,也同样可以使用override指示符, 如:

1
2
3
override define foo
bar
endef

多行变量

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

1
2
3
4
define two-lines
echo foo
echo $(bar)
endef

环境变量

make运行时的系统环境变量可以在make开始运行时被载入到Makefile文件中,但是如果Makefile中已定义了这个变量,或是这个变量由make命令行带入,那么系统的环境变量的值将被覆盖。(如果make指定 了-e参数,那么,系统环境变量将覆盖Makefile中定义的变量)

因此,如果我们在环境变量中设置了CFLAGS环境变量,那么我们就可以在所有的Makefile中使用这个变量了。这对于我们使用统一的编译参数有比较大的好处。如果 Makefile 中定义了CFLAGS,那么则会使用Makefile中的这个变量,如果没有定义则使用系统环境变量的值,一个共性和个性的统一,很像“全局 变量”和“局部变量”的特性。

make嵌套调用时(参见前面的“嵌套调用”章节),上层 Makefile 中定义的变量会以系统环境变量的方式传递到下层的 makefile 中。当然,默认情况下,只有通过命令行设置的变量会被传递。而定义在文件中的变量,如果要向下层 Makefile 传递,则需要使用exprot关键字来声明。(参见前面章节)

当然,我并不推荐把许多的变量都定义在系统环境中,这样,在我们执行不用的 Makefile 时,拥有的是同一套系统变量,这可能会带来更多的麻烦。

目标变量

为某个目标设置的局部变量,这种变量被称为“Target-specific Variable”(即目标变量),它可以和“全局变量”同名,因为它的作用范围只在这条规则以及连带规则中,所以其值也只在作用范围内有效。而不会影响规则链以外的全局变量的值。

其语法是:

1
2
<target ...> : <variable-assignment>;
<target ...> : overide <variable-assignment>

如:

1
2
3
4
5
6
7
8
9
10
11
12
prog : cflags = -g
prog : prog.o foo.o bar.o
    $(CC) $(CFLAGS) prog.o foo.o bar.o

prog.o : prog.c
    $(CC) $(CFLAGS) prog.c

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

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

模式变量

给定一种“模式”,可以把变量定义在符合这种模式的所有目标上。如:

1
%.o : cflags = -o

同样,模式变量的语法和“目标变量”一样:

1
2
<pattern ...>; : <variable-assignment>;
<pattern ...>; : override <variable-assignment>;

使用条件判断

示例

1
2
3
4
5
6
7
8
9
10
11
libs_for_gcc = -lgnu
normal_libs =

ifeq ($(CC),gcc)
    libs=$(libs_for_gcc)
else
    libs=$(normal_libs)
endif

foo: $(objects)
    $(CC) -o foo $(objects) $(libs)

语法

1
2
3
4
5
<conditional-directive>
<text-if-true>
else
<text-if-false>
endif
  1. ifeq:
    1
    2
    3
    4
    5
    6
    7
    8
    
    ifeq (<arg1>, <arg2>)
    ifeq '<arg1>' '<arg2>'
    ifeq "<arg1>" "<arg2>"
    ifeq "<arg1>" '<arg2>'
    ifeq '<arg1>' "<arg2>"
    ifeq ($(strip $(foo)),)
    <text-if-empty>
    endif
    
  2. ifneq:
    1
    2
    3
    4
    5
    
    ifneq (<arg1>, <arg2>)
    ifneq '<arg1>' '<arg2>'
    ifneq "<arg1>" "<arg2>"
    ifneq "<arg1>" '<arg2>'
    ifneq '<arg1>' "<arg2>"
    
  3. ifdef:
    1
    
    ifdef <variable-name>
    

    如果变量 的值非空,那到表达式为真。否则,表达式为假。

    例如:

    1
    2
    3
    4
    5
    6
    7
    8
    
    bar =
    foo = $(bar)
    ifdef foo
        frobozz = yes
    else
        frobozz = no
    endif
    #frobozz=yes
    
    1
    2
    3
    4
    5
    6
    7
    
    foo =
    ifdef foo
        frobozz = yes
    else
        frobozz = no
    endif
    #frobozz=no
    
  4. ifndef:
    1
    
    ifndef <variable-name>
    

注意: make是在读取 Makefile 时就计算条件表达式的值,并根据条件表达式的值来选择语句,所以,你最好不要把自动化变量(如$@等)放入条件表达式中,因为自动化变量是在运行时才有的。

使用函数

函数的调用语法

1
2
$(<function> <arguments>) #或
${<function> <arguments>}

这里,<function>就是函数名,make支持的函数不多。<arguments>为函数的参数,参数间以逗号,分隔,而函数名和参数之间以 (空格)分隔。函数调用以$开头,以圆括号或花括号把函数名和参数括起。感觉很像一个变量,是不是?函数中的参数可以使用变量,为了风格的统一,函数和变量的括号最好一样,如使用$(subst a,b,$(x))这样的形式,而不是$(subst a,b, ${x})的形式。因为统一会更清楚,也会减少一些不必要的麻烦。如:

1
2
3
4
5
comma:= ,
empty:=
space:= $(empty) $(empty)
foo:= a b c
bar:= $(subst $(space),$(comma),$(foo))

函数参考

字符串处理函数

subst

$(subst <from>,<to>,<text>)

  • 名称:字符串替换函数
  • 功能:把字串 中的 字符串替换成
  • 返回:函数返回被替换过后的字符串。
  • 示例:

    1
    
      $(subst ee,EE,feet on the street)
    

    把 feet on the street 中的 ee 替换成 EE ,返回结果是 fEEt on the strEEt 。

patsubst

$(patsubst <pattern>,<replacement>,<text>)

  • 名称:模式字符串替换函数。
  • 功能:查找 中的单词(单词以“空格”、“Tab”或“回车”“换行”分隔)是否符合模式 ,如果匹配的话,则以 替换。这里, 可以 包括通配符 % ,表示任意长度的字串。如果 中也包含 % ,那么, 中的这个 % 将是 中的那个 % 所代表的字串。 (可以用 \ 来转义,以 \% 来表示真实含义的 % 字符)
  • 返回:函数返回被替换过后的字符串。
  • 示例:

    1
    
      $(patsubst %.c,%.o,x.c.c bar.c)
    

    把字串 x.c.c bar.c 符合模式 %.c 的单词替换成 %.o ,返回结果是 x.c.o bar.o

  • 备注:这和我们前面“变量章节”说过的相关知识有点相似。如 $(var:=;) 相当于 $(patsubst ,,$(VAR)) ,而 $(var: =) 则相当于 $(patsubst %,%,$(VAR)) 。 例如有:

    1
    
    OBJECTS = foo.o bar.o baz.o,
    

    那么, $(OBJECTS:.o=.c) 和 $(patsubst %.o,%.c,$(OBJECTS)) 是一样的。

strip

$(strip <string>)

  • 名称:去空格函数。
  • 功能:去掉 字串中开头和结尾的空字符。
  • 返回:返回被去掉空格的字符串值。
  • 示例:

    1
    
      $(strip a b c )
    

    把字串 a b c 去到开头和结尾的空格,结果是a b c 。

findstring

$(findstring <find>,<in>)

  • 名称:查找字符串函数
  • 功能:在字串 中查找 字串。
  • 返回:如果找到,那么返回 ,否则返回空字符串。
  • 示例:

    1
    2
    
      $(findstring a,a b c)
      $(findstring a,b c)
    

    第一个函数返回 a 字符串,第二个返回空字符串 filter

$(filter <pattern...>,<text>)

  • 名称:过滤函数
  • 功能:以 模式过滤 字符串中的单词,保留符合模式 的单词。可以有多个模式。
  • 返回:返回符合模式 的字串。
  • 示例:

    1
    2
    3
    
      SOURCES := foo.c bar.c baz.s ugh.h
      foo: $(SOURCES)
          cc $(filter %.c %.s,$(SOURCES)) -o foo
    

    $(filter %.c %.s,$(SOURCES)) 返回的值是 foo.c bar.c baz.s 。

filter-out

$(filter-out <pattern...>,<text>)

  • 名称:反过滤函数
  • 功能:以 模式过滤 字符串中的单词,去除符合模式 的单词。可以有多个模式。
  • 返回:返回不符合模式 的字串。
  • 示例:

    1
    2
    
      OBJECTS=main1.o foo.o main2.o bar.o
      MAINS=main1.o main2.o
    

    $(filter-out $(MAINS),$(OBJECTS)) 返回值是 foo.o bar.o 。

sort

$(sort <list>)

  • 名称:排序函数
  • 功能:给字符串 中的单词排序(升序)。
  • 返回:返回排序后的字符串。
  • 示例: $(sort foo bar lose) 返回 bar foo lose 。
  • 备注: sort 函数会去掉 中相同的单词。

word

$(word <n>,<text>)

  • 名称:取单词函数
  • 功能:取字符串 中第 个单词。(从一开始)
  • 返回:返回字符串 中第 个单词。如果 中的 单词数要大,那么返回空字符串。
  • 示例: $(word 2, foo bar baz) 返回值是 bar 。

wordlist

$(wordlist <ss>,<e>,<text>)

  • 名称:取单词串函数
  • 功能:从字符串 中取从 开始到 的单词串。 是一个数字。
  • 返回:返回字符串 中从 的单词字串。如果 中的单词数要大,那么返回空字符串。如果 大于 的单词数, 那么返回从 开始,到 结束的单词串。
  • 示例: $(wordlist 2, 3, foo bar baz) 返回值是 bar baz 。

words

$(words <text>)

  • 名称:单词个数统计函数
  • 功能:统计 中字符串中的单词个数。
  • 返回:返回 中的单词数。
  • 示例: $(words, foo bar baz) 返回值是 3 。
  • 备注:如果我们要取 中最后的一个单词,我们可以这样: $(word $(words ),) 。

firstword

$(firstword <text>)

  • 名称:首单词函数——firstword。
  • 功能:取字符串 中的第一个单词。
  • 返回:返回字符串 的第一个单词。
  • 示例: $(firstword foo bar) 返回值是 foo。
  • 备注:这个函数可以用 word 函数来实现: $(word 1,) 。

以上,是所有的字符串操作函数,如果搭配混合使用,可以完成比较复杂的功能。这里,举一个现实中 应用的例子。我们知道,make使用 VPATH 变量来指定“依赖文件”的搜索路径。于是,我们可以 利用这个搜索路径来指定编译器对头文件的搜索路径参数 CFLAGS ,如:

1
override CFLAGS += $(patsubst %,-I%,$(subst :, ,$(VPATH)))

如果我们的 $(VPATH) 值是 src:../headers ,那么 $(patsubst %,-I%,$(subst :, ,$(VPATH))) 将返回 -Isrc -I../headers , 这正是cc或gcc搜索头文件路径的参数。

文件名操作函数

下面我们要介绍的函数主要是处理文件名的。每个函数的参数字符串都会被当做一个或是一系列的文件名 来对待。

dir

$(dir <names...>)

  • 名称:取目录函数——dir。
  • 功能:从文件名序列 中取出目录部分。目录部分是指最后一个反斜杠( / )之前 的部分。如果没有反斜杠,那么返回 ./ 。
  • 返回:返回文件名序列 的目录部分。
  • 示例: $(dir src/foo.c hacks) 返回值是 src/ ./ 。

notdir

$(notdir <names...>)

  • 名称:取文件函数——notdir。
  • 功能:从文件名序列 中取出非目录部分。非目录部分是指最後一个反斜杠( / ) 之后的部分。
  • 返回:返回文件名序列 的非目录部分。
  • 示例: $(notdir src/foo.c hacks) 返回值是 foo.c hacks 。

suffix

$(suffix <names...>)

  • 名称:取後缀函数——suffix。
  • 功能:从文件名序列 中取出各个文件名的后缀。
  • 返回:返回文件名序列 的后缀序列,如果文件没有后缀,则返回空字串。
  • 示例: $(suffix src/foo.c src-1.0/bar.c hacks) 返回值是 .c .c。

basename

$(basename <names...>)

  • 名称:取前缀函数——basename。
  • 功能:从文件名序列 中取出各个文件名的前缀部分。
  • 返回:返回文件名序列 的前缀序列,如果文件没有前缀,则返回空字串。
  • 示例: $(basename src/foo.c src-1.0/bar.c hacks) 返回值是 src/foo src-1.0/bar hacks 。

addsuffix

$(addsuffix <suffix>,<names...>)

  • 名称:加后缀函数——addsuffix。
  • 功能:把后缀 加到 中的每个单词后面。
  • 返回:返回加过后缀的文件名序列。
  • 示例: $(addsuffix .c,foo bar) 返回值是 foo.c bar.c 。

addprefix

$(addprefix <prefix>,<names...>)

  • 名称:加前缀函数——addprefix。
  • 功能:把前缀 加到 中的每个单词后面。
  • 返回:返回加过前缀的文件名序列。
  • 示例: $(addprefix src/,foo bar) 返回值是 src/foo src/bar 。

join

$(join <list1>,<list2>)

  • 名称:连接函数——join。
  • 功能:把 中的单词对应地加到 的单词后面。如果 的 单词个数要比 的多,那么, 中的多出来的单词将保持原样。如果 的单词个数要比 多,那么, 多出来的单词将被复制到 中。
  • 返回:返回连接过后的字符串。
  • 示例: $(join aaa bbb , 111 222 333) 返回值是 aaa111 bbb222 333 。

foreach 函数

foreach函数和别的函数非常的不一样。因为这个函数是用来做循环用的,Makefile中的foreach函数 几乎是仿照于Unix标准Shell(/bin/sh)中的for语句,或是C-Shell(/bin/csh)中的foreach语句 而构建的。它的语法是:

1
$(foreach <var>,<list>,<text>)

这个函数的意思是,把参数 中的单词逐一取出放到参数 所指定的变量中, 然后再执行 所包含的表达式。每一次 会返回一个字符串,循环过程中, 的所返回的每个字符串会以空格分隔,最后当整个循环结束时, 所返回的 每个字符串所组成的整个字符串(以空格分隔)将会是foreach函数的返回值。

所以, 最好是一个变量名, 可以是一个表达式,而 中一般会 使用 这个参数来依次枚举 中的单词。举个例子:

1
2
names := a b c d
files := $(foreach n,$(names),$(n).o)

上面的例子中, $(name) 中的单词会被挨个取出,并存到变量 n 中, $(n).o 每次 根据 $(n) 计算出一个值,这些值以空格分隔,最后作为foreach函数的返回,所以, $(files) 的值是 a.o b.o c.o d.o 。

注意,foreach中的 参数是一个临时的局部变量,foreach函数执行完后,参数 的变量将不在作用,其作用域只在foreach函数当中。

if 函数

if函数很像GNU的make所支持的条件语句——ifeq(参见前面所述的章节),if函数的语法是:

1
$(if <condition>,<then-part>)

或是

1
$(if <condition>,<then-part>,<else-part>)

可见,if函数可以包含“else”部分,或是不含。即if函数的参数可以是两个,也可以是三个。 参数是if的表达式,如果其返回的为非空字符串,那么这个表达式就相当于返回真, 于是, 会被计算,否则 会被计算。

而if函数的返回值是,如果 为真(非空字符串),那个 会是整个函数的返回值,如果 为假(空字符串),那么 会是 整个函数的返回值,此时如果 没有被定义,那么,整个函数返回空字串。

所以, 只会有一个被计算。

call函数

call函数是唯一一个可以用来创建新的参数化的函数。你可以写一个非常复杂的表达式,这个表达式中, 你可以定义许多参数,然后你可以call函数来向这个表达式传递参数。其语法是:

1
$(call <expression>,<parm1>,<parm2>,...,<parmn>)

当make执行这个函数时, 参数中的变量,如 $(1) 、 $(2) 等,会 被参数 依次取代。而 的 返回值就是 call 函数的返回值。例如:

1
2
reverse =  $(1) $(2)
foo = $(call reverse,a,b)

那么, foo 的值就是 a b 。当然,参数的次序是可以自定义的,不一定是顺序的,如:

1
2
reverse =  $(2) $(1)
foo = $(call reverse,a,b)

此时的 foo 的值就是 b a 。

需要注意:在向 call 函数传递参数时要尤其注意空格的使用。call 函数在处理参数时,第2个及其之后的 参数中的空格会被保留,因而可能造成一些奇怪的效果。因而在向call函数提供参数时,最安全的做法是 去除所有多余的空格。

origin函数

origin函数不像其它的函数,他并不操作变量的值,他只是告诉你你的这个变量是哪里来的?其语法是:

1
$(origin <variable>)

注意, 是变量的名字,不应该是引用。所以你最好不要在 中使用 $ 字符。Origin函数会以其返回值来告诉你这个变量的“出生情况”,下面,是origin函数的返回值:

  • undefined 如果 从来没有定义过,origin函数返回这个值 undefined
  • default 如果 是一个默认的定义,比如“CC”这个变量,这种变量我们将在后面讲述。
  • environment 如果 是一个环境变量,并且当Makefile被执行时, -e 参数没有被打开。
  • file 如果 这个变量被定义在Makefile中。
  • command line 如果 这个变量是被命令行定义的。
  • override 如果 是被override指示符重新定义的。
  • automatic > 如果 是一个命令运行中的自动化变量。关于自动化变量将在后面讲述。

这些信息对于我们编写Makefile是非常有用的,例如,假设我们有一个Makefile其包了一个定义文件 Make.def,在 Make.def中定义了一个变量“bletch”,而我们的环境中也有一个环境变量“bletch”, 此时,我们想判断一下,如果变量来源于环境,那么我们就把之重定义了,如果来源于Make.def或是命令行 等非环境的,那么我们就不重新定义它。于是,在我们的Makefile中,我们可以这样写:

1
2
3
4
5
ifdef bletch
    ifeq "$(origin bletch)" "environment"
        bletch = barf, gag, etc.
    endif
endif

当然,你也许会说,使用 override 关键字不就可以重新定义环境中的变量了吗?为什么需要使用这样 的步骤?是的,我们用 override 是可以达到这样的效果,可是 override 过于粗暴,它同时 会把从命令行定义的变量也覆盖了,而我们只想重新定义环境传来的,而不想重新定义命令行传来的。 shell函数

shell函数也不像其它的函数。顾名思义,它的参数应该就是操作系统Shell的命令。它和反引号“`”是 相同的功能。这就是说,shell函数把执行操作系统命令后的输出作为函数返回。于是,我们可以用操作 系统命令以及字符串处理命令awk,sed等等命令来生成一个变量,如:

1
2
contents := $(shell cat foo)
files := $(shell echo *.c)

注意,这个函数会新生成一个Shell程序来执行命令,所以你要注意其运行性能,如果你的Makefile中 有一些比较复杂的规则,并大量使用了这个函数,那么对于你的系统性能是有害的。特别是Makefile的 隐晦的规则可能会让你的shell函数执行的次数比你想像的多得多。

控制make的函数

make提供了一些函数来控制make的运行。通常,你需要检测一些运行Makefile时的运行时信息,并且 根据这些信息来决定,你是让make继续执行,还是停止。

1
$(error <text ...>)

产生一个致命的错误, <text …> 是错误信息。注意,error函数不会在一被使用就会产生错误 信息,所以如果你把其定义在某个变量中,并在后续的脚本中使用这个变量,那么也是可以的。例如:

示例一:

1
2
3
ifdef ERROR_001
    $(error error is $(ERROR_001))
endif

示例二:

1
2
3
4
5
ERR = $(error found an error!)

.PHONY: err

err: $(ERR)

示例一会在变量ERROR_001定义了后执行时产生error调用,而示例二则在目录err被执行时才发生error调用。

1
$(warning <text ...>)

这个函数很像error函数,只是它并不会让make退出,只是输出一段警告信息,而make继续执行。

make的运行

一般来说,最简单的就是直接在命令行下输入make命令,make命令会找当前目录的 Makefile 来执行,一切都是自动的。但也有时你也许只想让make重编译某些文件,而不是整个工程,而又有的时候你有几套编译规则,你想在不同的时候使用不同的编译规则,等等。本章节就是讲述如何使用make命令的。

make的退出码

  • 0 表示成功执行。
  • 1 如果make运行时出现任何错误,其返回1
  • 2 如果你使用了make-q选项,并且make使得一些目标不需要更新,那么返回2

指定 Makefile

使用make-f或是--file参数(--makefile参数也行):

1
make –f hchen.mk

指定目标

make命令后直接跟目标的名字:

1
make main.o

使用make的环境变量makecmdgoals:

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

Makefile 中常见的伪目标:

  • all:这个伪目标是所有目标的目标,其功能一般是编译所有的目标。
  • clean:这个伪目标功能是删除所有被make创建的文件。
  • install:这个伪目标功能是安装已编译好的程序,其实就是把目标执行文件拷贝到指定的目标中去。
  • print:这个伪目标的功能是例出改变过的源文件。
  • tar:这个伪目标功能是把源程序打包备份。也就是一个tar文件。
  • dist:这个伪目标功能是创建一个压缩文件,一般是把tar文件压成z文件。或是gz文件。
  • tags:这个伪目标功能是更新所有的目标,以备完整地重编译使用。
  • checktest:这两个伪目标一般用来测试 Makefile 的流程。

检查规则

  • -n, --just-print, --dry-run, --recon 不执行参数,这些参数只是打印命令,不管目标是否更新,把规则和连带规则下的命令打印出来,但不执行,这些参数对于我们调试 Makefile 很有用处。
  • -t, --touch 这个参数的意思就是把目标文件的时间更新,但不更改目标文件。也就是说,make假装编译目标,但不是真正的编译目标,只是把目标变成已编译过的状态。
  • -q, --question 这个参数的行为是找目标的意思,也就是说,如果目标存在,那么其什么也不会输出,当然也不会执行编译,如果目标不存在,其会打印出一条出错信息。
  • -w <file>, --what-if=<file>, --assume-new=<file>, --new-file=<file> 这个参数需要指定一个文件。一般是是源文件(或依赖文件),make会根据规则推导来运行依赖于这个文件的命令,一般来说,可以和-n参数一同使用,来查看这个依赖文件所发生的规则命令。

另外一个很有意思的用法是结合-p-v来输出 Makefile 被执行时的信息(这个将在后面讲述)。

make的参数

  • -b, -m
    这两个参数的作用是忽略和其它版本make的兼容性。
  • -b, --always-make
    认为所有的目标都需要更新(重编译)。
  • -c <dir>, --directory=<dir>
    指定读取makefile的目录。如果有多个“-c”参数,make的解释是后面的路径以前面的作为相对路径 ,并以最后的目录作为被指定目录。如:“make -c ~hchen/test -c prog”等价于 “make -c ~hchen/test/prog”。
  • -debug[=<options>]

    输出make的调试信息。它有几种不同的级别可供选择,如果没有参数,那就是输出最简单的调试信息。 下面是的取值:

    • a: 也就是all,输出所有的调试信息。(会非常的多)
    • b: 也就是basic,只输出简单的调试信息。即输出不需要重编译的目标。
    • v: 也就是verbose,在b选项的级别之上。输出的信息包括哪个makefile被解析,不需要被重编 译的依赖文件(或是依赖目标)等。
    • i: 也就是implicit,输出所以的隐含规则。
    • j: 也就是jobs,输出执行规则中命令的详细信息,如命令的pid、返回码等。
    • m: 也就是makefile,输出make读取makefile,更新makefile,执行makefile的信息。
  • -d
    相当于“–debug=a”。
  • -e, --environment-overrides
    指明环境变量的值覆盖makefile中定义的变量的值。
  • -f=<file>, --file=<file>, --makefile=<file>
    指定需要执行的makefile。
  • -h, --help
    显示帮助信息。
  • -i , --ignore-errors
    在执行时忽略所有的错误。
  • -i <dir>, --include-dir=<dir>
    指定一个被包含makefile的搜索目标。可以使用多个“-i”参数来指定多个目录。
  • -j [<jobsnum>], --jobs[=<jobsnum>]
    指同时运行命令的个数。如果没有这个参数,make运行命令时能运行多少就运行多少。如果有一个以上的“-j”参数,那么仅最后一个“-j”才是有效的。(注意这个参数在ms-dos中是无用的)
  • -k, --keep-going
    出错也不停止运行。如果生成一个目标失败了,那么依赖于其上的目标就不会被执行了。
  • -l <load>, --load-average[=<load>], -max-load[=<load>]
    指定make运行命令的负载。
  • -n, --just-print, --dry-run, --recon
    仅输出执行过程中的命令序列,但并不执行。
  • -o <file>, --old-file=<file>, --assume-old=<file>
    不重新生成的指定的,即使这个目标的依赖文件新于它。
  • -p, --print-data-base
    输出makefile中的所有数据,包括所有的规则和变量。这个参数会让一个简单的makefile都会输出 一堆信息。如果你只是想输出信息而不想执行makefile,你可以使用“make -qp”命令。如果你想查 看执行makefile前的预设变量和规则,你可以使用 “make –p –f /dev/null”。这个参数输出的 信息会包含着你的makefile文件的文件名和行号,所以,用这个参数来调试你的 makefile会是很有 用的,特别是当你的环境变量很复杂的时候。
  • -q, --question
    不运行命令,也不输出。仅仅是检查所指定的目标是否需要更新。如果是0则说明要更新,如果是2则说 明有错误发生。
  • -r, --no-builtin-rules
    禁止make使用任何隐含规则。
  • -r, --no-builtin-variabes
    禁止make使用任何作用于变量上的隐含规则。
  • -s, --silent, --quiet
    在命令运行时不输出命令的输出。
  • -s, --no-keep-going, --stop
    取消“-k”选项的作用。因为有些时候,make的选项是从环境变量“makeflags”中继承下来的。所以你 可以在命令行中使用这个参数来让环境变量中的“-k”选项失效。
  • -t, --touch
    相当于unix的touch命令,只是把目标的修改日期变成最新的,也就是阻止生成目标的命令运行。
  • -v, --version
    输出make程序的版本、版权等关于make的信息。
  • -w, --print-directory
    输出运行makefile之前和之后的信息。这个参数对于跟踪嵌套式调用make时很有用。
  • --no-print-directory
    禁止“-w”选项。
  • -w <file>, --what-if=<file>, --new-file=<file>, --assume-file=<file>
    假定目标;需要更新,如果和“-n”选项使用,那么这个参数会输出该目标更新时的运行动作。 如果没有“-n”那么就像运行unix的“touch”命令一样,使得;的修改时间为当前时间。
  • --warn-undefined-variables
    只要make发现有未定义的变量,那么就输出警告信息。

隐含规则

使用隐含规则

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

隐含规则

  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

  3. 编译pascal程序的隐含规则。

    <n>.o 的目标的依赖目标会自动推导为<n>.p,并且其生成命令是$(PC) -c $(PFLAGS)

  4. 编译fortran/ratfor程序的隐含规则。

    <n>.o 的目标的依赖目标会自动推导为<n>.r.f 或`.f`,并且其生成命令是: .f $(FC) -c $(FFLAGS) .f $(FC) -c $(FFLAGS) $(CPPFLAGS) .f $(FC) -c $(FFLAGS) $(RFLAGS)

  5. 预处理fortran/ratfor程序的隐含规则。

    <n>.f 的目标的依赖目标会自动推导为<n>.r<n>.f。这个规则只是转换ratfor或有预处理的fortran程序到一个标准的fortran程序。其使用的命令是:

    1
    2
    
     .f $(FC) -f $(CPPFLAGS) $(FFLAGS)
     .r $(FC) -f $(FFLAGS) $(RFLAGS)
    
  6. 编译modula-2程序的隐含规则。

    <n>.sym 的目标的依赖目标会自动推导为<n>.def,并且其生成命令是:$(M2C) $(M2FLAGS) $(DEFFLAGS)<n>.o的目标的依赖目标会自动推导为<n>.mod, 并且其生成命令是:$(M2C) $(M2FLAGS) $(MODFLAGS)

  7. 汇编和汇编预处理的隐含规则。

    <n>.o 的目标的依赖目标会自动推导为<n>.s,默认使用编译品as,并且其生成命令是:$ (as) $(ASFLAGS)<n>.s的目标的依赖目标会自动推导为<n>.s, 默认使用c预编译器cpp,并且其生成命令是:$(AS) $(ASFLAGS)

  8. 链接object文件的隐含规则。

    <n>目标依赖于<n>.o,通过运行c的编译器来运行链接程序生成(一般是ld),其生成命令是:$(CC) $(LDFLAGS) <n>.o $(LOADLIBES) $(LDLIBS)。这个规则对于只有一个源文件的工程有效,同时也对多个object文件(由不同的源文件生成)的也有效。例如如下规则:

    1
    
    x : y.o z.o
    

    并且x.cy.cz.c都存在时,隐含规则将执行如下命令:

    1
    2
    3
    4
    5
    6
    7
    
    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
    

    如果没有一个源文件(如上例中的x.c)和你的目标名字(如上例中的x)相关联,那么,你最好写出自己的生成规则,不然,隐含规则会报错的。

  9. yacc c程序时的隐含规则。

    <n>.c 的依赖文件被自动推导为n.y(yacc生成的文件),其生成命令是:$(YACC) $(YFALGS)。 (“yacc”是一个语法分析器,关于其细节请查看相关资料)

  10. lex c程序时的隐含规则。

    <n>.c 的依赖文件被自动推导为n.l(lex生成的文件),其生成命令是:$(LEX) $(LFALGS)。 (关于“lex”的细节请查看相关资料)

  11. lex ratfor程序时的隐含规则。

    <n>.r 的依赖文件被自动推导为n.l(lex生成的文件),其生成命令是:$(LEX) $(LFALGS)

  12. c程序、yacc文件或lex文件创建lint库的隐含规则。

  13. <n>.lnlint生成的文件)的依赖文件被自动推导为n.c,其生成命令是:$(LINT) $(LINTFALGS) $(CPPFLAGS) -i。对于<n>.y<n>.l也是同样的规则。

隐含规则使用的变量

  1. 关于命令的变量。
    • AR : 函数库打包程序。默认命令是ar
    • AS : 汇编语言编译程序。默认命令是as
    • CC : c语言编译程序。默认命令是cc
    • CXX : c++语言编译程序。默认命令是g++
    • CO : 从 rcs文件中扩展文件程序。默认命令是co
    • CPP : c程序的预处理器(输出是标准输出设备)。默认命令是$(CC) -e
    • FC : fortran 和 ratfor 的编译器和预处理程序。默认命令是f77
    • GET : 从sccs文件中扩展文件的程序。默认命令是get
    • LEX : lex方法分析器程序(针对于c或ratfor)。默认命令是lex
    • PC : pascal语言编译程序。默认命令是pc
    • YACC : yacc文法分析器(针对于c程序)。默认命令是yacc
    • YACCR : yacc文法分析器(针对于ratfor程序)。默认命令是yacc -r
    • MAKEINFO : 转换texinfo源文件(.texi)到info文件程序。默认命令是makeinfo
    • TEX : 从tex源文件创建tex dvi文件的程序。默认命令是tex
    • TEXI2DVI : 从texinfo源文件创建军tex dvi 文件的程序。默认命令是texi2dvi
    • WEAVE : 转换web到tex的程序。默认命令是weave
    • CWEAVE : 转换c web 到 tex的程序。默认命令是cweave
    • TANGLE : 转换web到pascal语言的程序。默认命令是tangle
    • CTANGLE : 转换c web 到 c。默认命令是ctangle
    • RM : 删除文件命令。默认命令是rm -f
  2. 关于命令参数的变量
    • ARFLAGS : 函数库打包程序ar命令的参数。默认值是rv
    • ASFLAGS : 汇编语言编译器参数。(当明显地调用 .s 或 .s 文件时)
    • CFLAGS : c语言编译器参数。
    • CXXFLAGS : c++语言编译器参数。
    • COFLAGS : rcs命令参数。
    • CPPFLAGS : c预处理器参数。( c 和 fortran 编译器也会用到)。
    • FFLAGS : fortran语言编译器参数。
    • GFLAGS : sccs “get”程序参数。
    • LDFLAGS : 链接器参数。(如: ld )
    • LFLAGS : lex文法分析器参数。
    • PFLAGS : pascal语言编译器参数。
    • RFLAGS : ratfor 程序的fortran 编译器参数。
    • YFLAGS : yacc文法分析器参数。

定义模式规则

%.o : %.c ; <command ......>;

示例1:

1
2
%.o : %.c
    $(CC) -c $(CFLAGS) $(CPPFLAGS) $< -o $@

示例2:

1
2
%.tab.c %.tab.h: %.y
    bison -d $<

自动化变量

  • $@: 表示规则中的目标文件集。在模式规则中,如果有多个目标,那么, $@ 就是匹配于 目标中模式定义的集合。
  • $%: 仅当目标是函数库文件中,表示规则中的目标成员名。例如,如果一个目标是 foo.a(bar.o) , 那么, $% 就是 bar.o , $@ 就是 foo.a 。如果目标不是函数库文件 (unix下是 .a ,windows下是 .lib ),那么,其值为空。
  • $<: 依赖目标中的第一个目标名字。如果依赖目标是以模式(即 % )定义的,那么 $< 将是符合模式的一系列的文件集。注意,其是一个一个取出来的。
  • $?: 所有比目标新的依赖目标的集合。以空格分隔。
  • $^: 所有的依赖目标的集合。以空格分隔。如果在依赖目标中有多个重复的,那个这个变量会去除 重复的依赖目标,只保留一份。
  • $+: 这个变量很像 $^ ,也是所有依赖目标的集合。只是它不去除重复的依赖目标。
  • $*: 这个变量表示目标模式中 % 及其之前的部分。如果目标是 dir/a.foo.b ,并且 目标的模式是 a.%.b ,那么, $* 的值就是 dir/a.foo 。这个变量对于构造有关联的 文件名是比较有较。如果目标中没有模式的定义,那么 $* 也就不能被推导出,但是,如果目标文件的 后缀是make所识别的,那么 $* 就是除了后缀的那一部分。例如:如果目标是 foo.c ,因为 .c 是make所能识别的后缀名,所以, $* 的值就是 foo 。这个特性是gnu make的, 很有可能不兼容于其它版本的make,所以,你应该尽量避免使用 $* ,除非是在隐含规则或是静态 模式中。如果目标中的后缀是make所不能识别的,那么 $* 就是空值。

下面是对于上面的七个变量分别加上 d 或是 f 的含义:

  • $(@d): 表示 $@ 的目录部分(不以斜杠作为结尾),如果 $@ 值是 dir/foo.o ,那么 $(@d) 就是 dir ,而如果 $@ 中没有包含斜杠的话,其值就是 . (当前目录)。
  • $(@f): 表示 $@ 的文件部分,如果 $@ 值是 dir/foo.o ,那么 $(@f) 就是 foo.o , $(@f) 相当于函数 $(notdir $@) 。
  • $(*d), $(*f): 和上面所述的同理,也是取文件的目录部分和文件部分。对于上面的那个例子, $(d) 返回 dir , 而 $(f) 返回 foo
  • $(%d), $(%f): 分别表示了函数包文件成员的目录部分和文件部分。这对于形同 archive(member) 形式的目标中的 member 中包含了不同的目录很有用。
  • $(<d), $(<f): 分别表示依赖文件的目录部分和文件部分。
  • $(^d), $(^f): 分别表示所有依赖文件的目录部分和文件部分。(无相同的)
  • $(+d), $(+f): 分别表示所有依赖文件的目录部分和文件部分。(可以有相同的)
  • $(?d), $(?f): 分别表示被更新的依赖文件的目录部分和文件部分。

模式的匹配

有一个模式e%t,文件src/eat匹配于该模式,于是src/a就是其“茎”,如果这个模式定义在依赖目标中,而被依赖于这个模式的目标中又有个模式c%r,那么,目标就是src/car。(“茎”被传递)

重载内建隐含规则

重载内建的隐含规则:

1
2
%.o : %.c
    $(CC) -c $(CPPFLAGS) $(CFLAGS) -d$(DATE)

取消内建的隐含规则:

1
%.o : %.s

老式风格的“后缀规则”

  • 单后缀规则只定义一个后缀,也就是源文件的后缀。如.c相当于% : %.c
  • 双后缀规则定义了一对后缀:目标文件的后缀和依赖目标(源文件)的后缀。如.c.o相当于%o : %c

如:

1
2
.c.o:
    $(CC) -c $(CFLAGS) $(CPPFLAGS) -o $@ $<

后缀规则不允许任何的依赖文件,如果有依赖文件的话,那就不是后缀规则,那些后缀统统被认为是文件名,如:

1
2
.c.o: foo.h
    $(CC) -c $(CFLAGS) $(CPPFLAGS) -o $@ $<

后缀规则中,如果没有命令,那是毫无意义的。因为他也不会移去内建的隐含规则。
而要让make知道一些特定的后缀,我们可以使用伪目标 .suffixes 来定义或是删除,如:

1
.suffixes: .hack .win #把后缀 .hack 和 .win 加入后缀列表中的末尾。
1
2
.suffixes:              # 删除默认的后缀
.suffixes: .c .o .h   # 定义自己的后缀

使用make更新函数库文件

1
archive(member)

一般来说,这种用法基本上就是为了 ar 命令来服务的。如:

1
2
foolib(hack.o) : hack.o
    ar cr foolib hack.o

函数库成员的隐含规则

make foo.a(bar.o)

1
2
3
4
5
6
7
foo.a(bar.o):
#等价于:
foo.a(bar.o):
  ar r foo.a bar.o
bar.o:
  cc -c bar.c -o bar.o
  rm -f bar.o

函数库文件的后缀规则

1
2
3
4
5
6
7
8
9
.c.a:
  $(CC) $(CFLAGS) $(CPPFLAGS) -c $< -o $*.o
  $(AR) r $@ $*.o
  $(RM) $*.o>
#等价于:
(%.o) : %.c
    $(CC) $(CFLAGS) $(CPPFLAGS) -c $< -o $*.o
    $(AR) r $@ $*.o
    $(RM) $*.o

伪目标

  • .INTERMEDIATE: 强制声明目标是中介目标
  • .SECONDARY: 阻止make自动删除中间目标, 只能挨个指定
  • .PRECIOUS: 把你的目标,以模式的方式来指定(如:%.o)成伪目标的依赖目标,以保存被隐含规则所生成的中间文件。
  • .SUFFIXES: 定义或是删除特定的后缀
  • .PHONY :显式地指明一个目标是“伪目标”