一、 三个基本概念
(注:本文所有的测试都是在 Linux 环境下进行的)
在Makefile中,最重要的三个概念是:目标(target)、依赖关系(dependency)和命令(command)。目标是指要干什么,即运行make后生成什么;依赖是指明目标所依赖的其他目标;命令则告诉make如何生成目标,这三个概念是通过Makefile中的规则(rule)关联在一起的。
例 1 编辑一个名为 Makefile 的文件,文件内容如下:
all:
echo "Hello Lion, I love you"
然后在命令行中执行它,键入集合
make ,就能执行。编辑 makefile 文件时要注意,命令所在的行必须以
Tab 键开头。
在Makefile中,目标和命令组合在一起就形成了一个简单的规则,通过这个规则,我们告诉 make 要做什么。运行 make 命令时可以指定具体的目标加以选择。
例 2 继续编辑修改刚才的 Makefile 文件,如下:
all:
echo "Hello Lion, I love you"
test:
echo "Just for test, she is so beautiful"
综上,我们得到以下信息:一个Makefile中可以定义多个目标;调用 make 命令时,得告诉它我们希望构建的目标是什么,即要它执行哪个命令,第一个目标是默认执行的目标;当 make 得到目标后,先找到构建目标的对应规则,然后运行规则中的命令来达到构建目标的目的,一个规则中可以根据需要存在多条命令。
如果不想让 make 打印出每条要执行的命令,可以在命令前加上
@ 符号,如
all:
@ echo "Hello Lion, I love you"
test:
@echo "Just for test, she is so beautiful"
先决条件:在执行一个目标前,必须要先执行其他目标,即当前目标的执行是以其他目标的执行为条件。这个先决目标就是当前要执行的目标要依赖的目标。如把刚才的 Makefile 修改如下:
all: test
@echo "Hello Lion, I love you"
test:
@echo "Just for test, she is so beautiful"
然后再次执行命令 make ,运动结果如下:
$ make
Just for test, she is so beautiful!
Hello Lion, I love you!
从结果可以看到,test 目标先被构建了,然后才构建 all 目标,因为 test 目标是 all 目标的先决条件。出现这种目标依赖关系时, make 会从左到右(在同一规则中)和从上到下(在不同的规则中)的先后顺序先构建一个规则所依赖的每一个目标,形成一种“链式反应”。
二、 搭建基本的编译环境(实验)
我们把这个简单的项目称为
simple 项目吧,让我们先编辑项目中用到的几个文件
(1)foo.c
#include <stdio.h>
void foo()
{
printf("foo() function test makefile");
}
(2) main.c
extern void foo();
int main()
{
foo();
return 0;
}
(3) Makefile
all: 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 main.o foo.o
执行方法很简单,键入 make 命令就会生成相应文件,键入 make clean 命令就会删除相应文件。注意连续几次(大于两次)键入 make 命令,从第二次开始,就没有构建目标文件的动作,但是有构建 simple 可执行程序的动作。这是因为 make 是通过文件的时间戳来判定哪些文件需要重新编译的。make 在分析一个规则以创建目标时,如果发现先决条件中文件的时间戳大于目标的时间戳,那先决条件中的文件比目标更新,就没必要再重新构建了。
三、 让Makefile更专业
1、假目标的运用
在前面写的 Makefile中,有一个 clean 目标。假设 Makefile 所在的目录下有一个 clean 文件,那么当我们运行 make clean 时,将无法正常执行。因为些时 make 会把 clean 当成文件来处理,而不是当成命令。这种目录文件名与 Makefile 中的目标名重名的情况是很难避免的,也是我们不希望看到的。为此,引进
假目标(phony target)这个概念。假目标用
.PHONY 关键字来定义,必须大写。所以我们可以这样修改上边的Makefile文件。
.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 main.o foo.o
更改后再执行 make clean 命令,它就能正确执行了。使用
.PHONY关键声明一个目标后, make 并不会将其当做一个文件来处理。由于假目标并不与文件关联,所以每次构建假目标时它所在规则中的命令一定会被执行。
2、运用“变量”提高可维护性
为了提高 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 $(OBJS)
在这个 Makefile 中,我们定义了CC、RM、EXE、OBJS四个变量,定义变量时其值可以为空,即无右值。引用变量时可以采用
$(变量名) 或
${变量名} 的形式。引入变量后,Makefile 就很灵活了,例如如果我们想更换编译器,我们只需更改
CC 变量的值就行了。
自动变量
在上边的这个 Makefile 中,存在目标名和先决条件名在规则中的命令重复出现的情况。如果目标名或先决条件名发生了改变,那么我们就必须相应的修改所有的命令,为了省去这种麻烦,我们可以使用如下自动变量。
$@ 用于表示一个规则中的目标,当一个规则中有多个目标时,$@ 所指的是其中任何造成规则命令被运行的目标;
$^ 表示的是规则中的所有先决条件;
$< 表示的是规则中的第一个先决条件。
当然 Makefile 中还有其他的自动变量,但是现在我们只用到这三个。下边是一个测试文件
.PHONY : all
all : first second third
@echo "\$$@ = $@"
@echo "$$^ = $^"
@echo "$$ = $<"
first second third
执行结果如下
$@ = all
$^ = first second third
$< = first
注:在 Makefile 中 “$” 有特殊的意思,如果想用 echo 输出 “$” ,就必须用两个连着的 “$” ; “$@” 对于 Bash shell 也有特殊的意思,需要在 “$$@” 之前再加一个脱字符 “\” ,最后一行是一个只有目标的规则,是不能缺少的。下面我们再来使用自动变量来重写前面的 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 的命令名是什么。在这篇文章中,
$(MAKE) 的值就是 “make” 。当需要在 Makefile 中运行另一个 Makefile 时,需要用到这个变量。
MAKECMDGOALS 变量表示的是当前构建的目标名,下面分别是测试
MAKE 和
MAKECMDGOALS 的 Makefile 文件。注意
MAKECMDGOALS 是指用户输入的目标,所以当只运行 make 而不带参数时(即用户不在命令行中输入任何目标),
MAKECMDGOALS 仍为空而不是 “all” 。
测试
MAKE
.PHONY : all
all :
@echo "MAKE = $(MAKE)"
测试
MAKECMDGOALS
.PHONY : all clean
all clean :
@echo "\$$@ = $@"
@echo "MAKECMDGOALS = $(MAKECMDGOALS)"
变量的类别与赋值
变量有递归扩展变量和简单扩展变量两种。递归扩展变量只用一个 “
= ” 符号进行定义,其赋值是可递归的。
下面让我们通过一个例子来看一下
递归扩展变量的特点
.PHONY : all
first = $(lion)
lion = $(love)
love = linda
all :
@echo $(first)
执行 make 命令后,打印结果如下
linda
使用递归扩展变量的时候要注意,使用不当可能会造成死循环,如下的赋值就是一个死循环
CFLAGS = $(CFLAGS) -O
简单扩展变量,是用 “
:= ” 操作符来定义的,对于这种变量,make 只对其进行一次扩展。来看一个例子
.PHONY : all
x = lion
y = $(x) love
x = linda
xx := lion
yy := $(xx) love
xx := linda
all :
@echo "x = $(y), xx = $(yy)"
执行 make 命令后,打印结果如下
x = linda love, xx = lion love
在 Makefile 中可以对同一个变量采用不同的赋值操作,如下例子
.PHONY : all
obj = main.o foo.o
obj := $(obj) utils.o
all :
@echo $(obj)
执行 make 命令后,打印结果如下
main.o foo.o utils.o
在 Makefile 中还可以使用
条件赋值,这个操作是通过 “
?= ” 操作符来实现的,当变量没定义时就定义它,并且将右边的值赋值给它;如果变量已经定义了,则不改变其原值。条件赋值通常用于为变量赋默认值。例
.PHONY : all
x = lion
x ?= linda
y ?= linda
all :
@echo "x = $(x), y = $(y)"
执行 make 命令后,打印结果如下
x = lion, y = linda
在 Makefile 中,我们也可以通过 “
+= ” 实现追加赋值的功能,例
.PHONY : all
x = main.o
x += object.o
all :
@echo "x = $(x)"
执行 make 命令后,打印结果如下
x = main.o object.o
在 Makefile 中可以对变量进行定义,也可以通过其他方式来让 make 获得变量:对于自动变量,其值是在每一个规则的上下文件中自动获得的;在运行 make 时,可以通过命令参数定义变量,如对于下边的 Makefile
.PHONY : all
x = main.o
x += object.o
all :
@echo "x = $(x)"
如果执行命令 make x=change 则打印结果为
x = change
由此可见,在运行 make 的命令参数中定义的变量在 Makefile 中是可见的,而且这些参数可以覆盖 Makefile 文件中所定义的变量的值。
有时我们可能不希望 Makefile 文件中的变量有被覆盖的可能,这时就得用到 override 指令进行预防了。例
.PHONY : all
override x = main.o
x += object.o
all :
@echo "x = $(x)"
执行命令 make x=change 的打印结果为
x = main.o
可见使用 override 指令后,x 变量的值不可覆盖了,而且 Makefile 中对它进行追加赋值也失效了。
使用 “ 模式 ” 精简规则
在前边的 simple 项目的 Makefile 文件中,存在多个规则用于构建目标文件。如 foo.o 和 main.o ,都采用不同的规则进行描述。如果对于每个目标文件,都要写一个不同的规则来描述,那是很费力的事。所以我们有必要使用模式来减少我们写 Makefile 的工作量。借助模式,我们可以把 simple 项目的 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 $(OBJS)
改写后的 Makefile 与原来的 Makefile 的区别就是把
main.o : main.c
$(CC) -o main.o -c main.c
foo.o : foo.c
$(CC) -o foo.o -c foo.c
替换成了
%.o : %.c
$(CC) -o $@ -c $^
经过这样的修改,把多条构建瞟文件的规则变成了一条,不论有多少个源文件需要编译都可以应用同一规则,编写 Makefile 文件的工作量就大大减少了。(其中的
% 是通配符)
四、通过函数增强功能
函数是 Makefile 中经常要用到的,它们可以增强 Makefile 的功能,在 simple 项目中,其 Makefile 文件中要指明每个项目的源
文件,这样是很麻烦的。我们可以通过函数来避免这种麻烦,下面让我们先来看一下一些常用的函数(《GNU make》中有详细的解说)。下面的实验都是我在公司的服务器上做的
1、abspath 函数:用于将 _names 中的各路径名转换成绝对路径,并将转换后的结果返回,其形式是
$(abspath _names)
例
.PHONY all
ROOT := $(abspath /usr ../lib)
all :
@echo $(ROOT)
执行 make 的打印结果如下
/usr /home/lion/pratemp/pramake/lib
2、addprefix 函数:用于给名字列表 _names 中的每一个名字增加前缀,并将增加了前缀的名字列表返回,其形式是
$(addprefix _prefix, _names)
例
.PHONY : all
without_dir = foo.c bar.c main.o
with_dir := $(addprefix objs/, $(without_dir))
all :
@echo $(with_dir)
执行 make 的打印结果如下
objs/foo.c objs/bar.c objs/main.o
3、addsuffix 函数:用于给名字列表 _names 中的每一个名字增加后缀,并将增加了后缀的名字列表返回,其形式是
$(addsuffix _suffix, _names)
例
.PHONY : all
without_suffix = foo bar main
with_suffix := $(addsuffix .c, $(without_suffix))
all :
@echo $(with_suffix)
执行 make 的打印结果如下
foo.c bar.c main.c
4、filter 函数:用于将 _text 中根据模式 _pattern 得到满足需要的名字列表并返回,其形式是
$(filter _pattern, _text)
例
.PHONY : all
obj = foo.c bar.c utils.c vir.s single.h
obj := $(filter %.c %.h, $(obj))
all :
@echo $(obj)
执行 make 的打印结果如下
foo.c bar.c utils.c single.h
.s 文件被过滤掉了,可见 filter 函数起到过滤的作用
5、eval 函数:eval 函数将使得 make 再一次解析 _text语句,该函数返回空字符串,其形式是
$(eval _text)
例
.PHONY : all
obj = foo.c bar.c utils.c vir.s single.h
$(eval obj := $(filter %.c %.h, $(obj)))
all :
@echo $(obj)
执行 make 的打印结果如下
foo.c bar.c utils.c single.h
6、filter-out 函数:用于将 _text 中根据模式 _pattern 滤除一部分名字,并将滤除后的列表返回,其形式是
$(filter-out _pattern, _text)
例
.PHONY : all
obj = foo1.c foo2.c foo3.c test.c single.h
result = $(filter-out foo%.c, $(obj))
all :
@echo $(result)
执行 make 的打印结果如下
test.c single.h
7、notdir 函数:用于将 _names 中的各路径名转换成绝对路径,并将转换后的结果返回,其形式是
$(notdir _names)
例
.PHONY : all
names := $(notdir lion/mode1/src/test.c lion/mode2/src/temp.c)
all :
@echo $(nams)
执行 make 的打印结果如下
test.c temp.c
8、patsubst 函数:被用于将名字列表 _text 中符合 _pattern 模式的名字替换为 _replacement ,并将替换后的名字列表返回。其形式是
$(patsubst _pattern, _replacement, _text)
例
.PHONY : all
names = test.c temp.c code.o
result := $(patsubst %.c, %.o, $(names))
all :
@echo $(result)
执行 make 的打印结果如下
test.o temp.o code.o
9、realpath 函数:用于获取 _names 所对应的真实路径名,并将取得的结果返回,其形式是
$(realpath _names)
例
.PHONY : all
result := $(realpath ./..)
all :
@echo $(result)
我当前所在的目录是
/home/lion/pratemp/pramake/pra_2
执行 make 的打印结果如下
/home/lion/pratemp/pramake
10、strip 函数:用于将 _string 中多余的空格去除,并将所得结果返回,其形式是
$(strip _string)
例
.PHONY : all
first = test.c main.c
second := $(strip $(first))
all :
@echo "first = $(first))"
@echo "second = $(second)"
执行 make 的打印结果如下
first = test.c main.c
second = test.c main.c
11、wildcard 函数:这是个通配符函数,用于得到当前工作目录中满足 _pattern 模式的文件或目录名列表,其形式是
$(wildcard _pattern)
例
.PHONY : all
srcFile = $(wildcard *.c)
all :
@echo $(srcFile)
我当前所在目录下有以下文件
Makefile temp.c test.c
执行 make 的打印结果如下
temp.c test.c
五、提高编译环境的实用性
前面所讲的 simple 项目是一个简单的项目,下面我们来一步步实现一个更大的项目,且称之为
greater 项目吧。下面是这个项目的初始代码
greater/test.h
#ifndef __TEST_H
#define __ TEST_H
void test();
#endif
greater/test.c
#include <stdio.h>
#include "test.h"
void test()
{
printf("Take it easy, just for test\n");
}
greater/main.c
#include "test.h"
int main()
{
test();
return 0;
}
greater/Makefile
.PHONY : all
MKDIR = mkdir
DIRS = objs exes
all : $(DIRS)
$(DIRS) :
$(MKDIR) $@
执行前 make 前后,当前目录文件的变化过程如下
lion@eserver:~/pratemp/pramake/greater$ ls
main.c Makefile test.c test.h
lion@eserver:~/pratemp/pramake/greater$ make
mkdir objs
mkdir exes
lion@eserver:~/pratemp/pramake/greater$ ls
exes main.c Makefile objs test.c test.h
在编译一个项目时会产生大量的中间文件,如果中间文件与项目的源文件混在一起,就会很乱,不利于维护。所以在编译过程中应该自动生成用于存放不同类型文件的目录,如将所有的目标文件放入 objs 子目录中,将所有的可执行文件放入 exes 子目录等。接下来为我们的 greater 项目的 Makefile 增加一个 clean 目标,用于删除编译时所产生的存放于 objs 子目录中的所有中间文件,修改后的 Makefile 内容如下:
.PHONY : all clean
MKDIR = mkdir
DIRS = objs exes
RM = rm
RMFLAGS = -rf
all : $(DIRS)
$(DIRS) :
$(MKDIR) $@
clean :
$(RM) $(RMFLAGS) objs
通过目录管理文件
为了将项目编译时所创建的文件分类存放(把 .o 文件放入 objs 子目录中,把可执行文件放入 exes 子目录中),我们得借助前面介绍的常用函数。将 greater 项目的 Makefile 修改如下
.PHONY : all clean
MKDIR = mkdir
RM = rm
RMFLAGS = -rf
CC = gcc
OBJS_DIR = objs
EXES_DIR = exes
DIRS = $(OBJS_DIR) $(EXES_DIR)
EXE = greater
SRCS = $(wildcard *.c)
OBJS = $(SRCS:.c=.o)
OBJS := $(addprefix $(OBJS_DIR)/, $(OBJS))
all : $(DIRS) $(EXE)
$(DIRS) :
$(MKDIR) $@
$(EXE) : $(OBJS)
$(CC) -o $@ $^
$(OBJS_DIR)/%.o : %.c
$(CC) -o $@ -c $^
clean :
$(RM) $(RMFLAGS) $(DIRS)
对于规则中的每一条命令,make 都是在一个新的 Shell 上运行它;如果希望多个命令在同一个 Shell 中运行,可以用 “;” 将这些命令连接起来;当命令很长时,可以用 “\” 将一个命令分成多行书写。