make学习三:书写规则
系列文章目录
Make学习一:make初探
Make学习二:makefile组成要素
文章目录
- 系列文章目录
- 前言
- 默认目标
- 规则语法
- order-only prerequisites
- 文件名中的通配符
- 伪目标 Phony Targets
- 没有 Prerequisites 和 recipe
- 内建特殊目标名
- 一个目标多条规则或多个目标共享 prerequisites
- 自动生成依赖
- 总结
前言
前文 Make学习二:makefile组成要素 中提到,Makefile 组成中的重要成员就是显示规则。本文重点在编写规则上。
显示规则的格式如下:
target ... : prerequisites ...recipe...
引用前面 Make学习一:make初探 中的话,简单来说,一条规则就像一道菜。如果说 target
是我们的西红柿炒鸡蛋,那 prerequisites
就是西红柿炒鸡蛋的依赖,或者说输入,简单点就是西红柿和鸡蛋。而 recipe
就是西红柿炒鸡蛋的菜谱,也就是要做成这道菜,需要什么步骤,每一步要怎么做。 实际上,recipe
确实是菜谱的意思,prerequisites
意为先决条件。
默认目标
每一条规则在 Makefile 中的 书写顺序一般不影响功能,但会决定默认目标(default goal),即如果你运行 make 没有指定目标,默认会构建的那个目标。
默认目标的规则是:取第一个 Makefile 里第一条规则的第一个 target。
因此,通常会把整个程序编译的规则写在 Makefile
最前面,这个规则的目标经常叫做 all
。类似,make 的约定。
如下示例,直接运行 make 输出 Hello, world!:
all: hellohello:@echo "Hello, world!"
规则语法
一条规则的基本写法是:
target ... : prerequisites ...recipe...
targets 一般是文件名,可以用空格分隔多个目标。也可以用 通配符(wildcards)。
配方(recipe)行必须以 Tab 开头(这一点非常重要!)。 也可以通过设置 .RECIPEPREFIX 变量来改成别的符号
一条规则可以有:
- 多个依赖(prerequisites);
- 多个目标(targets);
- 也可以没有依赖(比如 .PHONY 规则);
- 或者没有 recipe(表示目标是伪目标或不需要命令)。
多个依赖示例:
edit: main.o kbd.o command.o display.ogcc -o edit main.o kbd.o command.o display.o
多个目标示例:
clean temp:rm -f *.o temp
这里 clean 和 temp 是 两个目标,共享同一个配方。
长行可以用 \ 续行,但 recipe 不需要续行,必须每行一个 Tab。
规则的作用:如果目标不存在或目标的修改时间早于任何一个依赖,则按照 recipe 去更新它们
order-only prerequisites
有时你会想要某个依赖在目标之前被构建,但不希望因为这个依赖的更新导致目标被重建。这就需要用 order-only prerequisite(仅顺序依赖)。
简单一句话概括:该依赖必须存在,但同时它的更新不能影响到 target 的更新。那么你就需要 order-only prerequisite
写法:target : normal-prerequisites | order-only-prerequisites
使用管道符把 order-only-prerequisites 放在右边即可。
例如:我们想把当前目录下的 foo.c 和 bar.c 文件构建出来的 foo.o 和 bar.o 放在 obj 目录中。但这个目录可能一开始不存在。我们需要:先保证 obj/ 目录存在;但不要因为 obj/ 目录变化而导致目标重建。则 Makefile 的内容如下所示:
OBJDIR := obj
OBJS := $(OBJDIR)/foo.o $(OBJDIR)/bar.oall: $(OBJS)# 创建 obj/ 目录(order-only)
$(OBJDIR)/%.o : %.c | $(OBJDIR)@echo "Compiling $< to $@"@cp $< $@# 仅顺序依赖:保证 obj/ 目录存在,但不影响重建判断
$(OBJDIR):@echo "Creating directory $@"@mkdir -p $@
第一次执行 make
,输出如下:
Creating directory obj
Compiling foo.c to obj/foo.o
Compiling bar.c to obj/bar.o
即便我们在 obj
目录下通过命令touch obj/temp
创建文件,执行 make
依旧不会有任何更新:make: 对“all”无需做任何事。
文件名中的通配符
在 Makefile 里,一个文件名可以使用通配符(wildcard characters)来匹配多个文件。支持的通配符和 Linux shell 里的一样:*
匹配 任意长度的任意字符,?
匹配任意单个字符,[abc]
匹配括号中任意一个字符。~
(波浪号)在文件名开头(单独出现或后跟 /)代表当前用户根目录。
一条规则分 target,prerequisite 和 recipe。target 和 prerequisite 中的 通配符由 make 来展开。而 recipe 中的通配符会把命令传递给 shell 由 shell 来处理。
例如:以下 Makefile 执行 make 之后会打印出所有更新之后的 .c 文件内容。$? = 依赖中比目标 print 新的文件。
print: *.c@echo $?
但变量中的通配符不会自动展开,需要使用函数 $(wildcard)。 例如:objects = *.o
中变量赋值并不会展开,objects 只是 “*.o” 字符串。
此时,当我们通过以下 Makefile 来执行 make 命令:
objects = *.ofoo : $(objects)cc -o foo $(CFLAGS) $(objects)
若当前目录下没有任何 .o 文件,则 *.o 就不会展开,则会出错:make: *** 没有规则可制作目标“*.c”,由“*.o” 需求。 停止。
为了解决这个问题,则需要在变量定义时则进行通配符展开为文件名列表。使用 wildcard 函数,格式:$(wildcard pattern...)
。
objects := $(wildcard *.o)
foo : $(objects) cc -o foo $(CFLAGS) $(objects)
若当前目录下没有任何 .o 文件,则 *.o 仍旧会展开,只是变量 objects 为空字符串。报错:cc -o foo
和 cc: fatal error: no input files
伪目标 Phony Targets
所谓 伪目标(phony target),指的是根本就不是某个实际文件名的目标,而是一个用于明确执行某个配方(recipe)的名字。
使用伪目标的两个主要原因是:避免与同名文件冲突 和 提高性能。例如,如下 Makefile 内容:
clean:rm *.o temp
上面例子中,clean 不是实际会被创建的文件。因此,每次运行 make clean 时,都会执行 rm 命令。
如果当前目录里真有一个名字叫 clean 的文件,make clean 就会误以为 clean 已是最新,不执行删除操作
使用 .PHONY: clean
后,clean
的配方 总是会执行,不会被任何文件阻止。
没有 Prerequisites 和 recipe
如果一个规则 既没有依赖(prerequisite)也没有配方(recipe),并且它的目标文件不存在,那么每次 make 运行时都会认为这个目标已经“更新”。这意味着:所有依赖于这个目标的规则,每次都会执行它们自己的配方。
这个机制常用于强制重新构建(不论文件是否真正变化)。例如以下示例:
clean: FORCErm $(objects)FORCE:
内建特殊目标名
有一些目标名称如果出现在 Makefile 中,会具有特殊的含义。这些名字通常以 .
开头。
.PHONY
(伪目标声明),.PHONY
后面列出的所有目标都被认为是伪目标(phony targets)。
.SUFFIXES
目标后面列出的是用于后缀规则 suffix rules的后缀列表。可以用 .SUFFIXES:
清除所有后缀规则(这样就不会自动匹配如 .c → .o 这些规则了)。
.DEFAULT
(没有匹配规则时的最后手段),如果一个目标没有任何匹配的规则(无显式也无隐式规则),就会使用 .DEFAULT
提供的配方。
.DEFAULT:@echo "No rule for $@"
等等等等
一个目标多条规则或多个目标共享 prerequisites
例如:kbd.o command.o files.o: command.h
这条 Makefile 内容中,多个目标同时依赖同一个头文件。此时可以用 $@
来区分当前到底是哪个目标。
一个文件可以同时是多条规则的目标(target)。来自不同规则的所有依赖(prerequisites)会合并成一个总的依赖列表。只要目标比任何一个依赖旧,就会执行配方(recipe)。
例如以下 Makefile 的内容:
objects = foo.o bar.ofoo.o : defs.hbar.o : defs.h test.h$(objects) : config.h
foo.o
的依赖会合并成一个列表:config.h + defs.h
自动生成依赖
在程序的 Makefile 中,你通常需要写很多这种规则:main.o: defs.h
。main.c
里用了 #include "defs.h"
,那么你就得写 main.o: defs.h
。
你写这个规则是为了让 make 知道:如果 defs.h
变了,就得重新编译 main.o
。如果你的程序很大,#include
很多,每次增加或删掉头文件都得手动改 Makefile,非常麻烦,容易出错。
为了解决这个问题,现代 C 编译器可以自动帮你生成这些依赖规则。它们会扫描你的源码里的 #include 语句,通常使用 -M 选项来实现。例如:cc -M main.c
它会输出:main.o : main.c defs.h
所以你不再需要自己写这些规则,编译器会帮你搞定。
总结
完结撒花!!