入门

规则

一个简单的makefile由具有以下形状的rule组成:

target … : prerequisites …
    recipe
    …
    …
  • target: 目标文件名或者所要执行的动作,例如clean
  • prerequisites: 目标所依赖的文件
  • recipe: 就是由prerequisites产生target所要执行的步骤.(注意:recipe中需用tab作为开头,否则需要用 .RECIPEPREFIX 设定)

一个makefile文件也可能包括除了rule之外的文本

一个简单的makefile

下面是一个例子,目标edit程序依赖于8个对象,由八个C程序和三个头文件组成,并满足以下规则:

  • 所有C文件都引用了defs.h
  • 只有那些定义编辑命令的文件引用command.h
  • 仅更改编辑器缓冲区的低级文件引用buffer.h
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)

自动推导

进一步简化

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
# 伪目标,make时不会执行clean
.PHONY : clean
clean :
        rm edit $(objects)

另一种风格:

objects = main.o kbd.o command.o display.o \
          insert.o search.o files.o utils.o

edit : $(objects)
        cc -o edit $(objects)

$(objects) : defs.h
kbd.o command.o files.o : command.h
display.o insert.o search.o files.o : buffer.h

进阶

通配符

下面这个例子说明,可以在规则的命令中使用通配符.

.PHONY:clean
clean:
    rm -rf *.o test

但不可以通过引用变量的方式来使用,例如下面这种方式是错误的.

OBJ=*.c
test:$(OBJ)
    gcc -o $@ $^

要在引用变量中达到同样的效果,请用wildcard函数.

同样和通配符类似的一个字符%,也是匹配任意个字符,例如:

test:test.o test1.o
    gcc -o $@ $^
%.o:%.c
    gcc -o $@ $^

变量的赋值

  • 简单赋值 ( := )
    编程语言中常规理解的赋值方式,只对当前语句的变量有效。
  • 递归赋值 ( = )
    赋值语句可能影响多个变量,所有目标变量相关的其他变量都受影响。
  • 条件赋值 ( ?= )
    如果变量未定义,则使用符号中的值定义变量。如果该变量已经赋值,则该赋值语句无效。
  • 追加赋值 ( += ) 原变量用空格隔开的方式追加一个新值。

简单赋值

x:=foo
y:=$(x)b
x:=new
test:
      @echo "y=>$(y)"
      @echo "x=>$(x)"

在 shell 命令行执行make test我们会看到:

y=>foob
x=>new

递归赋值

x=foo
y=$(x)b
x=new
test:
      @echo "y=>$(y)"
      @echo "x=>$(x)"

在 shell 命令行执行make test我们会看到:

y=>newb
x=>new

条件赋值

x:=foo
y:=$(x)b
x?=new
test:
      @echo "y=>$(y)"
      @echo "x=>$(x)"

在 shell 命令行执行make test我们会看到:

y=>foob
x=>foo

追加赋值

x:=foo
y:=$(x)b
x+=$(y)
test:
      @echo "y=>$(y)"
      @echo "x=>$(x)"

在 shell 命令行执行make test我们会看到:

y=>foob
x=>foo foob

上面的的结果告诉我们变量不同的赋值条件,所产生的不同的结果,我们使用的时候应该根据具体的情况选择相应的赋值规则。

变量使用的范围很广,它可以出现在规则的模式中,也可以出现在规则的命令中或者是作为
Makefile 函数的参数来使用。总之,变量的使用在我们的 Makefile
编写中还是非常广泛的,可以说我们的 Makefile 中必不可少的东西。

其实变量在我们的 Makefile
中还是有很多种类的,它们的意义是不相同的。比如我们的环境变量,自动变量,模式指定变量等。

自动变量

自动化变量我们可以理解为由 Makefile自动产生的变量。在模式规则中,规则的目标和依赖的文件名代表了一类的文件。规则的命令是对所有这一类文件的描述。我们在Makefile中描述规则时,依赖文件和目标文件是变动的,显然我们在命令中不能出现具体的文件名称,否则模式规则将失去意义。

那么模式规则命令中该如何表示文件呢?就需要使用"自动化变量",自动化变量的取值根据执行的规则来决定,取决于执行规则的目标文件和依赖文件。下面是对所有的自动化变量进行的说明:

  • $@: 表示规则的目标文件名。如果目标是一个文档文件(Linux 中,一般成 .a 文件为文档文件,也成为静态的库文件),
    那么它代表这个文档的文件名。在多目标模式规则中,它代表的是触发规则被执行的文件名。
  • $%: 当目标文件是一个静态库文件时,代表静态库的一个成员名。
  • $<: 规则的第一个依赖的文件名。如果是一个目标文件使用隐含的规则来重建,则它代表由隐含规则加入的第一个依赖文件。
  • $?: 所有比目标文件更新的依赖文件列表,空格分隔。如果目标文件时静态库文件,代表的是库文件(.o 文件)。
  • $^: 代表的是所有依赖文件列表,使用空格分隔。如果目标是静态库文件,它所代表的只能是所有的库成员(.o 文件)名。
    一个文件可重复的出现在目标的依赖中,变量“$”只记录它的第一次引用的情况。就是说变量“$”会去掉重复的依赖文件。
  • $+: 类似“$^”,但是它保留了依赖文件中重复出现的文件。主要用在程序链接时库的交叉引用场合。
  • $*: 在模式规则和静态模式规则中,代表“茎”。“茎”是目标模式中“%”所代表的部分(当文件名中存在目录时,
    “茎”也包含目录部分)

下面我们就自动化变量的使用举几个例子。

实例1:

test:test.o test1.o test2.o
         gcc -o $@ $^
test.o:test.c test.h
         gcc -o $@ $<
test1.o:test1.c test1.h
         gcc -o $@ $<
test2.o:test2.c test2.h
         gcc -o $@ $<

这个规则模式中用到了 “$@” 、"$<" 和 “$^”
这三个自动化变量,我们可以对比之前写的 Makefile 中的命令,我们可以发现"$@“代表的是目标文件test,”$^“代表的是依赖的文件,”$<"代表的是依赖文件中的第一个。我们在执行make 的时候,make
会自动识别命令中的自动化变量,并自动实现自动化变量中的值的替换,这个类似与我们在编译C语言文件的时候的预处理的作用。

实例2:

lib:test.o test1.o test2.o
    ar r $?

假如我们要做一个库文件,库文件的制作依赖于这三个文件。当我们修改了其中的某个依赖文件,在命令行执行
make 命令,我们的库文件 “lib” 就会自动更新。"$?" 表示修改的文件。

在 GNU make 中我们在这些变量中加入字符 “D” 或者 “F”
就形成了一系列变种的自动化变量,这些自动化变量可以对文件的名称进行操作。

下面是一些详细的描述:

  • $(@D): 表示文件的目录部分(不包括斜杠)。如果 “$@” 表示的是 “dir/foo.o” 那么 “$(@D)” 表示的值就是 “dir”。如果 “$@” 不存在斜杠(文件在当前目录下),其值就是 “.”。
  • $(@F): 表示的是文件除目录外的部分(实际的文件名)。如果 “$@” 表示的是 “dir/foo.o”,那么 “$@F” 表示的值为 “dir”。
  • $(*D),$(*F): 分别代表 “茎” 中的目录部分和文件名部分
  • $(%D) 和 $(%F): 当以 “archive(member)” 形式静态库为目标时,分别表示库文件成员 “member” 名中的目录部分和文件名部分。踏进对这种新型时的目标有效。
  • $(<D)和$(<F): 表示第一个依赖文件的目录部分和文件名部分。
  • $(^D)和$(^F): 分别表示所有依赖文件的目录部分和文件部分。
  • $(+D)和$(+F): 分别表示所有依赖文件的目录部分和文件部分(去重)。
  • $(?D)和$(?F): 分别表示更新的依赖文件的目录部分和文件名部分。

目标文件搜索

介绍

我们都知道一个工程文件中的源文件有很多,并且存放的位置可能不相同(工程中的文件会被放到不同的目录下),所以按照我们之前的方式去编写Makefile 会有问题,我们再去创建编译规则的时候会缺少依赖的文件。

我们之前列举的例子,源文件基本上都是存放与 Makefile相同的目录下,只要依赖的文件存在,并且依赖规则没有问题,在命令行执行make,整个工程就会按照对应的规则去编译,就会重建我们的目标文件。那如果我们需要的文件是存在于不同的路径下,在编译的时候要去怎么办呢(不改变工程的结构)?这就用到了Makefile 提供的目录搜索文件的功能。

我们常见的搜索的方法的主要有两种:一般搜索VPATH和选择搜索vpath。这两个不仅是大小写的区别,本质上也是不同的。

VPATH 和 vpath 本质上的区别:

  • VPATH 是变量,更具体的说是环境变量,Makefile
    中的特殊变量,搜索时需要指定文件的路径;
  • vpath
    是关键字,按照模式搜索,也可以说成是选择搜索。搜索的时候不仅需要加上文件的路径,还需要加上相应限制的条件。

我们先来了解一下 VPATH,VAPTH是变量,我们在使用的时候,要把他当作变量来使用。具体使用方法我们可以看一下。

我们在 Makefile 中可以这样写:

VPATH := src

我们这可以这样理解,把 src 的值赋值给变量 VPATH,所以我们在执行 make的时候会,从 src 目录下找我们需要的文件。

当存在多个路径的时候我们可以这样写:

VPATH := src car

或者是

VPATH := src:car

要用空格或者是冒号隔开,这样的话我们就会在这两个路径下搜索文件。搜索的顺序是我们定义的顺序,那上面的例子来说,我们应该先搜索src 目录底下的文件,再搜索 car 目录下的文件。

注意:无论你定义了多少路径,make执行的时候会先搜索当前路径下的文件,当前目录下没有我们要找的文件,才去VPATH 的路径中去寻找。如果当前目录下有我们要使用的文件,那么 make就会使用我们当前目录下的文件。

实例:

VPATH=src car
test:test.o
    gcc -o $@ $^

假设我们的 test.c 文件没有在当前的目录而在我们的子目录 “src” 或者是"car" 下,我们的程序执行是没有问题的,但是生成的 test
的文件没有在子目录中而在当前的目录下,我们也可以指定在子目录中生成。

我们了解完 VPATH,再来了解一下关键字搜索
vpath,我们称这样的方法为选择性搜索。要说具体和 VPATH
的区别的话,我们可以这样理解:VPATH 是搜索路径下所有的文件,而 vpath更像是添加了限制条件,会过滤出一部分再去寻找。

具体用法:

  1. vpath PATTERN DIRECTORIES
  2. vpath PATTERN
  3. vpath

( PATTERN:我们可以理解为我们要寻找的条件,DIRECTORIES:我们要寻找的路径)

首先是用法一我们可以这样来用:

vpath test.c src

我们可以这样理解,在 src 路径下,去搜索文件test.c。多路径的我们可以这样写:

vpath test.c src car

或者是

vpath test.c src : car

多路径的用法其实和 VPATH差不多,都是使用空格或者是冒号分隔开,搜索路径的顺序是先 src目录,然后是 car 目录。

其次是用法二,我们可以这样写:

vpath test.c

用法二的意思是清除符合文件 test.c 的搜索目录。\

最后是用法三,用法很简单:

vpath

vpath 单独使的意思是清除所有已被设置的文件搜索路径。

另外我们在去使用 vpath的时候,搜索的条件可以包含模式字符"%",这个符号的作用是匹配一个或者是多个字符,例如,"%.c"表示搜索路径下所有的.c 结尾的文件。如果我们的搜索条件中没有包含"%",那么我们搜索的条件就是我们使用具体的文件名称。

使用什么样的方法,主要是根据编译器的效率问题。使用 VPATH
的时候当是前路径下的文件较少,或者是需要的文件不能使用 “%”
表示的,这样写我们更方便。如果我们去找的某个路径的文件特别的多或者是可以使用通配符表示的时候,就不建议使用VPATH 这种方法,为什么呢?因为 VPATH在去搜索文件的时没有限制条件,所以它回去检索这个目录下的所有文件,每一个文件都会进行对比,搜索和我们目录名相同的文件,不仅速度会很慢,而且效率会很低。我们在这种情况下就可以使用
vpath的方法,它包含搜索条件的限制,搜索的时候只会从我们规定的条件中搜索目标,帮我们过滤掉不符合条件的文件,我们在查找的时候会比较的快。

使用案例

我们了解了一下路径搜索的使用方式,我们再来看一下具体的使用方法。

为了体验实例的效果的更加明显,我们按照源代码树的布局来放置文件。我们把源代码放置在src目录下,包含的文件文件是:list1.c、list2.c、main.c文件,我们把头文件包含在 include 的目录下,包含文件 list1.h、list2.h文件。Makefile 放在这两个目录文件的上一级目录。

我们按照之前的方式来编写 Makefile 文件:

main:main.o list1.o list2.o
    gcc -o $@ $<
main.o:main.c
    gcc -o $@ $^
list1.o:list1.c list1.h
    gcc -o $@ $<
list2.o:list2.c list2.h
    gcc -o $@ $<

我们编译执行的 make 时候会发现命令行提示我们:

make:*** No rule to make target ‘main.c’,need by ‘main.o’. stop.

出现错误并且编译停止了,为什么会出现错误呢?我们来看一下出现错误的原因,再去重建最终目标文件main 的时候我们需要 main.o 文件,但是我们再去重建目标main.o文件的时候,发现没有找到指定的 main.c 文件,这是错误的根本原因。

这个时候我们就应该添加上路径搜索,我们知道路径搜索的方法有两个:VPATH 和vpath。我们先来使用一下
VPATH,使用方式很简单,我们只需要在上述的文件开头加上这样一句话:

VPATH=src include

再去执行 make 就不会出现错误。所以 Makefile 中的最终写法是这样的:

VPATH=src include
main:main.o list1.o list2.o
gcc -o $@ $<
main.o:main.c
gcc -o $@ $^
list1.o:list1.c list1.h
gcc -o $@ $<
list2.o:list2.c list2.h
gcc -o $@ $<

我们使用 vpath
的话同样可以解决这样的问题,只需要把上述代码中的VPATH 所在行的代码改写成:

vpath %.c src
vpath %.h include

这样我们就可以用 vpath 实现功能,代码的最终展示为:\

vpath %.c src
vpath %.h include
main:main.o list1.o list2.o
gcc -o $@ $<
main.o:main.c
gcc -o $@ $^
list1.o:list1.c list1.h
gcc -o $@ $<
list2.o:list2.c list2.h
gcc -o $@ $<

函数

  1. wildcard 查找当前目录下所有.c文件,返回值给src
src=$(wildcard ./*.c)
  1. patsubst 替换所有.c文件为.o文件
obj=$(patsubst ./%.c, ./%.o, $(src))

TODO 隐含规则


我很好奇