如果想要在某些文件更新时执行或更新任务,make
工具会令这个过程变得很方便。make
工具需要Makefile
(或makefile
)文件来定义要执行的一系列任务。你可能已经使用过make
将源码编译为程序。大部分开源项目都使用make
来编译为二进制的可执行程序,编译后的程序可以使用make install
进行安装。
本文将使用一些基本和高级的示例来对make
和makefile
进行探索。在这之前,先确保make
已经安装好了。
基础示例
下面我们在终端上以经典的打印”Hello World”作为开始。创建一个空目录 myproject,目录里创建一个包含以下内容的makefile
文件:
say_hello:
echo "Hello World"
现在在 myproject 目录里通过make
命令执行这个文件:
$ make
echo "Hello World"
Hello World
在上例中,say_hello 类似于其它编程语言中的函数名。这叫做 target。prerequisites 或 dependencies 就在 target 之后。为了简单起见,在该例中我们没有定义 prerequisites。命令 echo “Hello World” 被称为 recipe ,recipe 使用 prerequisites 来创建一个 target。target,prerequisites 和 recipes 一起组成了 rule。
总而言之,下面是一个典型的 rule 语法:
target: prerequisites
<TAB> recipe
例如,一个依赖 prerequisites(源码)的二进制 target 。另一方面,一个 prerequisite 也可以是一个依赖其它 dependencies 的 target:
final_target: sub_target final_target.c
Recipe_to_create_final_target
sub_target: sub_target.c
Recipe_to_create_sub_target
target 并不非得是一个文件,它可以仅仅是 recipe 的一个名字,正如我们的示例一样。我们称这种为 phony targets(注:伪目标
)。
回到我们上面的示例,当make
执行后,完整的命令 echo “Hello World” 就会紧接着实际的命令后显示。我们通常不希望这样。为了抑制实际的命令,需要在 echo 之前添加 @:
say_hello:
@echo "Hello World"
现在尝试再次运行make
。输出应该只显示如下内容:
$ make
Hello World
让我们添加更多 phony target 到Makefile
中:generate 和 clean:
say_hello:
@echo "Hello World"
generate:
@echo "Creating empty text files..."
touch file-{1..10}.txt
clean:
@echo "Cleaning up..."
rm *.txt
如果我们现在尝试运行make
,只有 say_hello 会被执行。这是因为makefile
中只有第一个 target 是默认的 target。通常这被称为 default goal,这就是你会在大部分项目都看到 all 作为第一个 target 的原因。all 的责任就是调用其它 target。我们也可以通过 phony target .DEFAULT_GOAL 来修改这个默认行为。
让我们在makefile
文件开头加上以下内容:
.DEFAULT_GOAL := generate
这样在运行时就会以 generate 作为默认 target:
$ make
Creating empty text files...
touch file-{1..10}.txt
正如名字暗示的意思一样,phony target .DEFAULT_GOAL 一次只能运行一个 target。这就是大部分makefile
都需要包含 all 作为调用其它 target 的 target 的原因。
让我们使用 phony target all 并把 .DEFAULT_GOAL 删除:
all: say_hello generate
say_hello:
@echo "Hello World"
generate:
@echo "Creating empty text files..."
touch file-{1..10}.txt
clean:
@echo "Cleaning up..."
rm *.txt
在运行make
之前,先添加另一个特殊的 phony target,.PHONY,这是用来定义所有非文件的 target。make
在运行它的 recipe 时就不用管它是不是文件或者它上次的修改时间了。这是一个完整的makefile
:
.PHONY: all say_hello generate clean
all: say_hello generate
say_hello:
@echo "Hello World"
generate:
@echo "Creating empty text files..."
clean:
@echo "Cleaning up..."
rm *.txt
make
会调用 say_hello 和 generate:
$ make
Hello World
Creating empty text files...
touch file-{1..10}.txt
在 all 中不调用 clean 或者不把 clean 放在第一个 target 中是一个很好的做法。clean 应该手动调用,在调用时作为make
的第一个参数:
$ make clean
Cleaning up...
rm *.txt
现在你应该了解一个基本的makefile
是如何工作以及如何写了个简单的makefile
了,接下来看了下相对高级的示例。
高级示例
变量
在以上的示例中,大部分 target 和 prerequisite 值都是硬代码,但在现实的项目中,这些应该用变量和模式来代替。
在makefile
中定义一个变量,最简单的办法是使用=
操作符。例如,把gcc
命令赋值给变量CC
:
CC = gcc
这也叫 recursive expanded variable,它在规则中的用法如下:
hello: hello.c
${CC} hello.c -o hello
可能你已经猜到了,recipe 传到终端之后展开如下:
gcc hello.c -o hello
${CC} 和 $(CC) 都是gcc
的有效引用。但如果一个变量试图把自身赋值给自己,会导致无限循环。让我们来验证一下:
CC = gcc
CC = ${CC}
all:
@echo ${CC}
执行make
会返回如下结果:
$ make
Makefile:8: *** Recursive variable 'CC' references itself (eventually). Stop.
为了避免这种情况,我们可以使用:=
操作符(这也叫做 simply expanded variable)。执行以下makefile
应该没有问题了:
CC := gcc
CC := ${CC}
all:
@echo ${CC}
模式与函数
下面的makefile
通过使用变量、模式和函数可以编译所有的 C 语言程序。让我们来一行一行地探索:
# Usage:
# make # compile all binary
# make clean # remove ALL binaries and objects
.PHONY = all clean
CC = gcc # compiler to use
LINKERFLAG = -lm
SRCS := $(wildcard *.c)
BINS := $(SRCS:%.c=%)
all: $(BINS)
%: %.o
@echo "Checking.."
${CC} ${LINKERFLAG} $< -o $@
%.o: %.c
@echo "Creating object.."
${CC} -c $<
clean:
@echo "Cleaning up..."
rm -rvf *.o ${BINS}
- 以
#
开头的是注释。 - .PHONY = all clean 定义了 phony targets all 和 clean。
- 变量 LINKERFLAG 定义了 recipe gcc 的标志。
- SRCS := $(wildcard *.c): $(wildcard pattern) 是 functions for filenames。在该例中,所有扩展为 .c 的文件都保存到变量 SRCS 中。
- BINS := $(SRCS:%.c=%): 这叫 substitution reference。在该例中,如果 SRCS 存在值 foo.c bar.c,则 BINS 则为 foo bar。
- all: ${BINS}: phony target all 以 target 的方式分别调用 ${BIN} 的值。
-
规则:
%: %.o: @echo "Checking.." ${CC} ${LINKERFLAG} $< -o $@
让我们来理解一下这个规则。假设 foo 是 ${BINS} 中的一个值,则 % 可以匹配 foo(% 可以匹配任何 target 名称)。以下是规则的展开形式:
foo: foo.o @echo "Checking.." gcc -lm foo.o -o foo
如上所示,% 被 foo 替换了,$< 被 foo.o 替换了。$< 是匹配 prerequisites 的模式,$@ 匹配了 target。这个规则会被 ${BINS} 中的每一个值调用。
-
规则:
%.o: %.c @echo "Creating object.." ${CC} -c $<
之前规则中的每个 prerequisite 在这条规则下都被认为是 target。以下是规则展开的形式:
foo.o: foo.c @echo "Creating object.." gcc -c foo.c
- 最后,我们用 clean target 把所有的二进制和对象文件删除。
下面重写一下以上的makefile
,假设这是一个只有单独一个 foo.c 文件的目录:
# Usage:
# make # compile all binary
# make clean # remove ALL binaries and objects
.PHONY = all clean
CC = gcc # compiler to use
LINKERFLAG = -lm
SRCS := foo.c
BINS := foo
all: foo
foo: foo.c
@echo "Checking.."
gcc -lm foo.o -o foo
foo.o: foo.c
@echo "Creating object.."
gcc -c foo.c
clean:
@echo "Cleaning up..."
rm -rvf foo.o foo
如果想对makefile
有更深入的了解,可以查阅 GNU Make manual,这里有完整的手册和示例。