本文主要是为了讲述如何能够编写更专业的Makefile,而不是仅仅通过一项简单的依赖规则生成几个固定目标。(重点参考李云老师编写的《专业嵌入式软件开发——全名走向高质量高效编程》一书,该书是我在图书馆偶然看到,发现原来长期一直使用的Makefile中居然还有这么多需要注意的细节,所以特地整理记录下来。)
文章目录
Makefile,开发环境全能管家
基本规则
假目标的用处
运用“变量”提高Makefile可维护性
自动变量
特殊变量
变量的类型与赋值
变量及其值的来源
避免变量被覆盖的方法
借助“模式”精简规则
通过“函数”增强功能
abspath函数
addprefix函数
addsuffix函数
eval函数
filter函数
filter-out函数
notdir函数
patsubst函数
strip函数
wildcard函数
提高编译环境的实用性
让编译环境更加有序
目录的自动创建与删除
通过目录管理文件
提升依赖关系管理
自动生成文件依赖关系
使用依赖关系文件
为依赖关系文件建立依赖关系
打造更专业的编译环境
规划项目目录结构
增进复用性
支持头文件目录的指定
实现库链接
增加一个bar模块
增强可使用性
管理对库的依赖关系
总结
Makefile,开发环境全能管家
Linux 环境下的程序员如果不会使用GNU make来构建和管理自己的工程,应该不能算是一个合格的专业程序员,至少不能称得上是 Unix程序员。在 Linux(unix )环境下使用GNU 的make工具能够比较容易的构建一个属于你自己的工程,整个工程的编译只需要一个命令就可以完成编译、连接以至于最后的执行。不过这需要我们投入一些时间去完成一个或者多个称之为Makefile 文件的编写。
所要完成的Makefile 文件描述了整个工程的编译、连接等规则。其中包括:工程中的哪些源文件需要编译以及如何编译、需要创建哪些库文件以及如何创建这些库文件、如何最后产生我们想要的可执行文件。尽管看起来可能是很复杂的事情,但是为工程编写Makefile 的好处是能够使用一行命令来完成“自动化编译”,一旦提供一个(通常对于一个工程来说会是多个)正确的 Makefile。编译整个工程你所要做的事就是在shell 提示符下输入make命令。整个工程完全自动编译,极大提高了效率。
make是一个命令工具,它解释Makefile 中的指令。在Makefile文件中描述了整个工程所有文件的编译顺序、编译规则。Makefile 有自己的书写格式、关键字、函数。像C 语言有自己的格式、关键字和函数一样。而且在Makefile 中可以使用系统shell所提供的任何命令来完成想要的工作。Makefile在绝大多数的IDE 开发环境中都在使用,已经成为一种工程的编译方法。
基本规则
基本规则
让我们先来粗略地看一看Makefile的规则。
target ... : prerequisites ... command ... ...
以上是Makefile的基本规则,解释如下:
目标:依赖
执行指令 …
target也就是一个目标文件,可以是Object File,也可以是执行文件。还可以是一个标签(Label)。
① prerequisites就是,要生成那个target所需要的文件或是目标。
② command也就是make需要执行的命令。(任意的Shell命令)
这里关于最基本的Makefile就不详细解释了,执行make命令后Makefile会首先生成描述的第一个目标,查看依赖是否成立,如果依赖不存在再去依次生成依赖。。。。最终成功生成第一个目标。
假目标的用处
假目标采用.PHONY关键字来定义,注意它必须是大写字母,通常最基本会将clean修改为假目标,这样避免了当前文件下存在“clean”同名的文件导致make执行出现偏差。如下:
.PHONY: clean simple: main.o foo.o gcc -o simple main.o foo.o main.o : main.c gcc -o main.o -c main.c foo.o : foo.c gcc -o foo.o -c foo.c clean: rm -rf simple main.o foo.o
- 由于假目标并不会与文件相关联,所以每次构建假目标时它所在的规则中的命令都一定会被执行,也就是上面每次执行“make clean”都会执行文件清除操作。
运用“变量”提高Makefile可维护性
编写专业的Makefile同样离不开运用变量,通过使用变量可以使得Makefile更具有可维护性。如下:
.PHONY:clean CC = gcc RM = rm EXE = simple OBJS = main.o foo.o $(EXE) : $(OBJS) $(CC) -o $(EXE) $(OBJS) main.o : main.c $(CC) -o main.o -c main.c foo.o : foo.c $(CC) -o foo.o -c foo.c clean: $(RM) -rf $(EXE) $(OBJS)
- 这里,我们定义了CC,RM,EXE,OBJS四个变量,定义变量时其值可以为空(即无右值),引用变量需要采用“( 变 量 名 ) ” 或 者 “ (变量名)”或者“(变量名)”或者“{变量名}”的形式。
- 引用变量的好处很明显,比如进入CC变量以后,如果需要修改编译器,只需要修改赋值变量这一处即可。Makefile中变量的数据类型可以理解为C语言中的字符串。
自动变量
在上面的Makefile中,存在目标名和先决条件在规则的命令中重复出现。如果目标名或者先决条件名发生改变,那得在相应的命令中都去修改,这很麻烦,为了省去这种麻烦,我们可以借助以下自动变量。
- $@:用于表示一个规则中的目标,当一个规则中有多个目标时,指其中任何造成规则命令被运行的目标。
- $^:表示的是规则中的所有先决条件。
- $<:表示的是规则中的第一个先决条件。
.PHONY : all all: first second third @echo "\$$@ = $@" @echo "$$^ = $^" @echo "$$< = $<" first second third:
除了这三个自动变量外,在Makefile中还可以使用其他的自动变量,后面我们会说到,以上三个自动变量是最常用的。使用上述自动变量来简化之前的Makefile:
.PHONY:clean CC = gcc RM = rm EXE = simple OBJS = main.o foo.o $(EXE) : $(OBJS) $(CC) -o $@ $^ main.o : main.c $(CC) -o $@ -c $^ foo.o : foo.c $(CC) -o $@ -c $^ clean: $(RM) -rf $(EXE) $(OBJS)
特殊变量
在Makefile中,有两个特殊变量会经常用到:MAKE和MAKECMDGOALS。
MAKE:表示当前处理Makefile的命令名是什么。
MAKECMDGOALS:表示是是当前构建的目标名。
如下:
.PHONY = all clean all clean: echo "MAKE = $(MAKE)" echo "MAKECMDGOALS = $(MAKECMDGOALS)"
- 从结果上看,MAKECMDGOALS变量指的是用户输入的目标,当只允许make命令不带参数时,根据Makefile的语法规则会将Makefile中的第一个目标作为默认目标,即上面的all目标,但是MAKECMDGOALS却仍是空而不是“all”。
- 另外,从结果中能看到,运行make时可以同时指定多个目标,make在获得了多个目标后,将从左往右依次地构建目标。
变量的类型与赋值
变量的类型有递归扩展变量和简单扩展变量。
最简单的方式是使用“=”进行变量的定义和赋值,这种只用一个“=”符号定义的变量被称为递归扩展变量。如下:
- 递归扩展变量
.PHONY = all foo = $(bar) bar = $(ugh) ugh = HUH? all: @echo $(foo)
这种递归扩展变量最需要注意的一点就是要防止对变量进行循环扩展,容易造成一个死循环。
- 简单扩展变量
简单扩展变量是用 “ := ”操作符来定义的。这种变量,make只会对其进行一次展开,如下示例:
.PHONY = all x = foo y = $(x) b x = later xx := foo yy := $(xx) b xx := later all: @echo "x = $(y), xx = $(yy)"
- 另外,在Makefile中还可以实现条件赋值,“?=”操作符实现,当变量没有被定义时就定义它,并且将右边的值赋值给它;如果变量已经定义了,则不改变其原值。条件赋值操作可以用于为变量赋值默认值。
.PHONY = all foo = x foo ?= y bar ?=y all: @echo "foo=$(foo), bar=$(bar)"
- 还有一个非常有用的赋值方法是通过“+=”实现追加赋值。
.PHONY:all objects = main.o foo.o bar.o utils.o objects += another.o all: @echo $(objects)
变量及其值的来源
从前面的示例可以看出,在Makefile中可以对变量进行定义。此外,还有其他方式使得make获得变量。比如:
- 对于自动变量,其值是在每一个规则中根据规则的上下文自动获得的。
- 在运行make时,通过命令参数定义变量。例如下面的Makefile,如果使用“make bar=x”来运行它,得到结果就不一样了。
.PHONY : all foo = x foo ?= y bar ?= y all: @echo "foo = $(foo), bar = $(bar)"
- 变量还可以来自shell环境,如下:
### 高级变量引用功能
如下的Makefile说明了变量引用的一种高级功能,即在赋值的同时完成文件名后缀替换操作。
.PHONY = all foo = a.c b.c c.c bar := $(foo:.c=.o) all: @echo "bar = $(bar)"
从截图中的运行结果来看,bar变量中的文件名由.c后缀变成了.o。与使用函数相比,这种方式根据简洁。当然,这种功能也可以采用后面将要介绍的patsubst函数来实现。
避免变量被覆盖的方法
上面介绍了make命令行上定义变量的方式能够使得Makefile文件中定义的变量值被覆盖。但是如果在设计Makefile时不希望发生这种覆盖现象,则需要使用override指令进行预防。具体如下:
.PHONY: all override foo = x all: @echo "foo = $(foo)"
借助“模式”精简规则
对于之前使用到的Makefile,其中存在多个规则用于构建目标文件。比如,main.o和foo.o,都是采用不同的规则进行描述,如果对于每个目标文件,都得写一个不同的规则来描述,那就变成了很麻烦的事了,虽然也能实现想要的效果。Makefile中的模式就是用来解决这种烦恼的。如下的Makefile就运用到了模式。
.PHONY: clean CC=gcc RM=rm EXE=simple OBJS=main.o foo.o $(EXE) : $(OBJS) $(CC) -o $@ $^ %.o : %.c $(CC) -o $@ -c $^ clean: $(RM) -rf $(EXE) $(OBJS)
通过“函数”增强功能
函数是Makefile中的另一个离奇,通过使用函数能显著增强Makefile的功能。对于之前的项目的Makefile,尽管使用了模式规则,但还有一件比较麻烦的事——在Makefile中要指明每一个项目源文件。
如下是采用了wildcard和patsubst函数修改后的Makefile:
.PHONY: clean CC = gcc RM = rm EXE = simple SRCS = $(wildcard *.c) OBJS = $(patsubst %.c, %.o, $(SRCS)) $(EXE) : $(OBJS) $(CC) -o $@ $^ %.o : %.c $(CC) -o $@ -c $^ clean: $(RM) -rf $(EXE) $(OBJS)
执行结果如下:
当使用函数编辑Makefile后,在增加需要编译的源文件后不需要再修改Makefile,直接就能对新增的文件进行编译。如下:
下面就来详细介绍一下Makefile中经常用到的几个函数。更多函数的使用方法可以参考官方手册《GNU Make》。
abspath函数
abspath函数被用于将_names中的各路径名转换成绝对路径,并将转换后的结果返回。其形式:
$(abspath _names)
如下示例:
.PHONY: all ROOT := $(abspath /usr/../lib) all: @echo $(ROOT)
执行结果如下: