# Makefile 介绍

# Makefile 的规则

<target> : <prerequisites>
[tab]  <commands>
    ...
1
2
3
  • target 目标,可以是执行文件,也可以是一个标签。
  • prerequisites 生成 target 所依赖的文件或 target
  • commandtarget 要执行的命令。

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

# Makefile 里有什么?

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 中使用 # 字符,可以用反斜杠进行转义,如:\# 。 最后,还值得一提的是,在 Makefile 中的命令,必须要以 Tab 键开始。

# 引用其他 Makefile

include <filename>

filename 可以是当前操作系统 Shell 的文件模式(可以包含路径和通配符)。

make 命令开始时,会找寻 include 所指出的其它 Makefile,并把其内容安置在当前的位置。

-I 选项指定查找目录。

# 环境变量

如果你的当前环境中定义了环境变量 MAKEFILES ,那么,make 会把这个变量中的值做一个类似于 include 的动作。

建议不要使用。

# Make 的工作方式

GNU 的 make 工作时的执行步骤如下:

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

# 书写规则

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

一般来说,定义在 Makefile中的第一条规则中的目标将被确立为最终的目标。

# 规则语法

<target> : <prerequisites>
[tab]  <commands>
    ...
1
2
3
  • target 目标,可以是执行文件,也可以是一个标签。
  • prerequisites 生成 target 所依赖的文件或 target
  • commandtarget 要执行的命令。

如果命令太长,你可以使用反斜杠(\ )作为换行符。

# 通配符

make 支持三个通配符:*?~

~ 表示用户的 $HOME 目录。

如果要将通配符用于变量中,需要这样写:

objects := $(wildcast *.o)

列出一确定文件夹中的所有 .c 文件:

objects := $(wildcard *.c)

列出文件对应的 .o 文件:

$(patsubst %.c %.o, $(wildcard, *.c))

objects := $(patsubst %.c,%.o, $(wildcard *.c))
foo: $(objects)
    cc -o foo $(objects)
1
2
3

# 文件搜寻

当 make 需要去找寻文件的依赖关系时,你可以在文件前加上路径,但最好的方法是把一个路径告诉 make,让 make 在自动去找。

Makefile 文件中的特殊变量 VPATH 就是完成这个功能的,如果没有指明这个变量,make 只会在当前的目录中去找寻依赖文件和目标文件。

VPATH = src:../headers

上面的定义指定两个目录,“src”和“../headers”,make 会按照这个顺序进行搜索。目录由“冒号”分隔。

vpath 关键字

vpath <pattern> <directories>

为符合模式<pattern> 的文件指定搜索目录 <directories>

vpath <pattern>

清除符合模式<pattern> 的文件的搜索目录。

vpath 清除所有设置好的文件搜索目录。

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

vpath %.h ../headers

该语句表示,要求 make 在“../headers”目录下搜索所有以 .h 结尾的文件。

# 伪目标

“伪目标”并不是一个文件,只是一个标签,需要显式指明才能让其生效。为了避免和文件重名,需要使用特殊标记:

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

可以利用伪目标一次性生成多个目标:

.PHONY : all
all : prog1 prog2 prog3

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
11

伪目标也可以是依赖:

.PHONY: cleanall cleanobj cleandiff

cleanall: cleanobj cleandiff
    rm program

cleanobj:
    rm *.o

cleandiff:
    rm *.diff
1
2
3
4
5
6
7
8
9
10

# 多目标

Makefile 的规则中的目标可以不止一个,其支持多目标,有可能我们的多个目标同时依赖于一个文件,并且其生成的命令大体类似。于是我们就能把其合并起来。

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

等价于:

bigoutoutput: text.g
    generate text.g -big > bigoutput

littleoutput: text.g
    generate text.g -little > littleoutput
1
2
3
4
5

# 静态模式

静态目标可以更容易地定义多目标的规则。

<targets ...> : <target-pattern> : <prereq-patterns ...>
    <commands>
    ...
1
2
3

targets 定义了一系列的目标文件,可以有通配符。是目标的一个集合。

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

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

例子:

CC = gcc
CFLAGS = -Wall -Werror -g -O0

OBJS := foo.o bar.o

all: $(OBJS)

$(OBJS): %.o : %.c
    $(CC) $(CFLAGS) -o $@ -c $<
1
2
3
4
5
6
7
8
9

%.o 指明我们的目标从 OBJS 中获取所有以 .o 结尾的目标,%.c 则取 % 匹配的内容,加上 .c 的后缀。

依赖目标就变为 foo.c bar.c

上面例子等价于

CC = gcc
CFLAGS = -Wall -Werror -g -O0

foo.o : foo.c
    $(CC) -o $@ $(FLAGS) -c $<

bar.o : bar.c
    $(CC) -o $@ $(FLAGS) -c $<
1
2
3
4
5
6
7
8

例子二:

files = foo.elc bar.o lose.o

$(filter %.o, $(files)) : %.o : %.c
    $(CC) -c $(CFLAGS) $< -o $@

$(filter %.o, $(files)) : %.o : %.c
    emacs -f batch-byte-compile $<
1
2
3
4
5
6
7

$(filter %.o $(files)) 表明调用 makefile 的 filter 函数,过滤 $(files) 集。只要其中模式为 %.o 的内容。

# 自动生成依赖性

太复杂,跳过,以后用到再学。

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

这个规则的意思是,所有的 .d 文件依赖于 .c 文件,rm -f $@ 的意思是删除所有的目标,也就是 .d 文件,第二行的意思是,为每个依赖文件 $< ,也就是 .c 文件生成依赖文件,$@ 表示模式 %.d 文件, 如果有一个 C 文件是 name.c,那么 % 就是 name ,$$$$ 意为一个随机编号,第二行生成的文件有可能 是“name.d.12345”,第三行使用 sed 命令做了一个替换

这个模式要做的事就是在编译器生成的依赖关系中加入 .d 文件的依赖,即把依赖关系

main.o : main.c defs.h

转成

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

# 书写命令

@ 屏蔽回显。

-n 参数,只打印,不执行。

-s silent 只执行不打印。

# 命令执行

如果需要上一条的命令应用在下一条命令时,写在同一行,并用 ; 分隔。

exec:
    cd /home/hchen; pwd
1
2

# 命令出错

命令执行完后,make 会检测每个命令的返回码。

为了忽略命令的出错,可在命令行前加上一个 - 号。

clean:
    -rm -f *.o
1
2

# 嵌套执行 Make

举例:

subsystem:
    $(MAKE) -C subdir
1
2

表示进入 subdir 目录,执行 make 命令。

传递变量给下级的 Makefile

export <variable ...>

variable = value
export variable
1
2

传递所有变量,只要一个 export 就行。

有两个变量,一个是 SHELL ,一个是 MAKEFLAGS ,这两个变量不管你是否 export,其总是要传递到下层 Makefile 中

如果我们在执行 make 时,有参数,那么 MAKEFLAGS 变量会将这些参数传递到下层 Makefile,这是系统级的环境变量。

但是 make 命令中的有几个参数并不往下传递,它们是 -C , -f , -h, -o 和 -W

如果不想往下传参数,可以这样写:

subsystem:
    cd substr && $(MAKE) MAKEFLAGS=
1
2

# 定义命令包

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

foo.c : foo.y
    $(run-yacc)
1
2
3
4
5
6
7

# 使用变量

类似于 C 中的宏。

自定义变量建议不要全大写,首字母大写,避免和系统变量冲突。

# 变量基础

使用变量需要用 $() 包裹。

Objects = program.o foo.o utils.o

program : $(Objests)
    cc -o program $(Objects)

$(Objects) : defs.h
1
2
3
4
5
6

例如:

foo = c

prog.o : prog.$(foo)
    $(foo)$(foo) -$(foo) prog.$(foo)
1
2
3
4

# 变量中的变量

延时变量表示:使用该变量的时候,才展开该变量,并确定该变量的值

立即变量表示:定义的时候就已经确定了该变量的值

= 递归赋值。

:= 赋值。

碰到变量需要展开,如果是=,则先往后面查找,如果是:=,则先往前面查找

?= 如果没有被赋值才赋值

# 变量的高级用法

变量值的替换

$(var:a=b) 把变量 "var" 中所有以 'a' 字符串结尾的 'a' 替换为 'b' 。

foo := a.o b.o c.o
bar := $(foo:.o=.c)
1
2

把变量的值当作变量:

$($(x))

静态模式替换:

foo := a.o b.o c.o
bar := $(foo: %.o=%.c)
1
2

# 追加变量值

+=:= 作为赋值符。

# 函数

函数调用:

$(<function> <arguments>) 参与用逗号分隔。

# 字符串处理函数

subst

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

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

patsubst

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

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

# 文件名操作函数

dir

$(dir <names...>)

$(dir src/foo.c hacks) 返回值是 src/ ./

notdir

$(not dir src/foo.c hacks) 返回值是 foo.c hacks

wildcard

$(patsubst %.c,%.o,$(wildcard *.c)),首先使用“wildcard”函数获取工作目录下的.c文件列表;之后将列表中所有文件名的后缀.c替换为.o。

# Make 的运行

-f 指定 make。

# 常见伪目标:

  • all:所有目标。
  • clean:清理。
  • install;安装编译好的程序,其实就是把目标执行文件拷贝到指定的目标中去。
  • print:列出所有改变过的源文件。
  • tar:把源代码打包备份成一个 tar 文件。
  • dist:创建一个压缩文件,一般是xz文件。
  • TAGS:更新所有的目标,以备完整编译使用。
  • check 和 test,测试用。

# 选项

  • -n打印不执行。
  • -B 重编译所有目标。
  • -C 指定读取 makefile 的目录。如果有多个“-C”参数,make 的解释是后面的路径以前面的作为相对路径,并以最后的目录作为被指定目录。如:“make -C ~hchen/test -C prog”等价于“make -C hchen/test/prog”。

# 隐含规则

例如,把 .c 文件编译成 .o 文件这一规则,你根本就不用写出来,make 会自动推导出这种规则,并生成我们需要的 .o 文件。

make 调用的隐含规则是,把 .o 的目标的依赖文件置成 .c ,并使用 C 的编译命令 cc –c $(CFLAGS) foo.c 来生成 foo.o 的目标。

隐含规则一览:

  1. 编译 C 程序的隐含规则。<n>.o 的目标的依赖目标会自动推导为 <n>.c ,并且其生成命令是 $(CC) –c $(CPPFLAGS) $(CFLAGS)
  2. 编译 C++ 程序的隐含规则。<n>.o 的目标的依赖目标会自动推导为 <n>.cc 或是 <n>.C ,并且其生成命令是 $(CXX) –c $(CPPFLAGS) $(CXXFLAGS) 。(建议使用 .cc 作为 C++ 源文件的后缀,而不是 .C )

# 命令变量

  • 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

# 命令参数的变量

  • 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 文法分析器参数。

# 自动化变量

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

打包:

lib : foo.o bar.o lose.o win.o
    ar r lib $?
1
2

四个变量($@$<$%$* )在扩展时只会有一个文件,另外三个是文件列表。

  • $(@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)分别表示被更新的依赖文件的目录部分和文件部分。

最后想提醒一下的是,对于 $< ,为了避免产生不必要的麻烦,我们最好给 $ 后面的那个特定字符 都加上圆括号,比如,$(<) 就要比 $< 要好一些。

# 参考

陈皓-跟我一起写 Makefile (opens new window)

自动生成依赖关系

foreach 函数。

filter 函数

filter-out 函数。

ifneq

通过命令自动生成对头文件的依赖,将生成的依赖自动包含进 Makefile 中, 头文件改动后,自动确认需要重新编译的文件。

gcc -M des 获取 des 的完整依赖关系

gcc -MM des 获取目标des 的部分依赖关系(-E 仅对依赖关系做初步解析)

gcc -MM 命令可以自动生成源文件对头文件的依赖关系,且剔除掉库里面的头文件

makefile 中的 include 关键字

类似 C 中的 include,将其他文件的内容原封不动的搬入当前文件

include 是按次序来载入文件,最先载入的 .d 文件中的目标会成为默认目标。

GNU Make手册建议使用正则表达式显式创建依赖文件

GNU make 官方手册建议写法:

all: main

main: main.o stack.o maze.o
	gcc $^ -o $@

clean:
	-rm main *.o

.PHONY: clean

sources = main.c stack.c maze.c

-include $(sources:.c=.d)

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

clean:
    rm -rf $(TARGET) *.o *.d *.d.*
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

自动处理头文件的依赖关系 (opens new window) Makefile-自动生成依赖-示例分析 (opens new window)

objs := Hello.o Fun.o
Hello : $(objs)
gcc -o $@ $^
#判断是否存在依赖文件
dep_files := $(foreach f, $(objs), .$(f).d)
dep_files := $(wildcard $(dep_files))
#包含依赖文件
ifneq ($(dep_files),)
include $(dep_files)
endif
%.o:%.c
#编译.c文件,并且把依赖的文件写入.$@.d
    gcc -Wp,-MD,.$@.d -c -o $@ $<
clean:
    rm -rf *.o Hello
#增加伪目标
.PHONY : clean
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 自动以最大线程编译

sudo make -j $(nproc)

最后更新: 2023/9/26 23:29:00