一个简单的Makefile教程

在网上找关于 Makefile 的资料时,发现了一篇简短的英文教程,于是将它翻译了一下。如果你想使用快速使用 Makefile,而不是要熟练掌握它,你可以看看这篇文章(参考的第一条)。当然,如果你想深入了解它并达到熟练使用的程度,那么还需要看看其它的文档。

一个简单的 Makefile 教程

Makefile 是一种简单的组织代码编译的方法。本教程甚至不会涉及到什么时候可能用到 make,而是想作为初学者的指南,以便你可以快速轻松地为中小型项目创建自己的 makefile。

一个简单的例子

让我们从下面三个文件开始,hellomake.c,hellofunc.c 和 hellomake.h,它们分别代表着一个典型的主程序,一个有一些方法代码的独立文件和包含文件。

1
2
3
4
5
6
7
8
9
//hellomake.c
#include <hellomake.h>
int main() {
// call a function in another file
myPrintHelloMake();
return(0);
}
1
2
3
4
5
6
7
8
9
10
//hellofunc.c
#include <stdio.h>
#include <hellomake.h>
void myPrintHelloMake(void) {
printf("Hello makefiles!\n");
return;
}
1
2
3
4
5
6
//hellomake.h
/*
example include file
*/
void myPrintHelloMake(void);

通常,您将通过执行以下命令来编译此代码集合:

1
gcc -o hellomake hellomake.c hellofunc.c -I.

这将编译两个 .c 文件并命名可执行文件为 hellomake。-I 指包含,使 gcc 在当前目录中寻找 hellomake.h。如果没有 makefile,测试/修改/调试循环的典型方法是利用终端的向上箭头返回你最近编译的命令,让你不必每次输入,特别是一旦你添加了几个 .c 文件。

不幸的是,这种编译方法有两个缺陷。首先,如果你失去了编译命令或切换计算机,你必须从头重新键入,这是最低效率的。其次,如果您只是更改一个.c文件,每次重新编译所有这些文件也是耗时且低效的。所以,是时候看看我们用一个 makefile 能做什么。

你可以创建的最简单的makefile看起来像:
Makefile1

1
2
hellomake: hellomake.c hellofunc.c
gcc -o hellomake hellomake.c hellofunc.c -I.

如果你把这个规则保存到一个名为 Makefile 或 makefile 的文件中,然后再命令行输入 make ,它将如你写入 makefile 的那样执行编译命令。注意 make 不带参数时,执行文件中的第一条规则。此外,命令所依赖的文件被放在第一行 : 之后,使其知道 如果被依赖的任一文件发生改变,hellomake 规则需要被重新执行。随即,你已经解决了问题 #1 ,能避免重复用向上方向键寻找你最近的编译命令。然而,就只编译最近修改而言,这还不是高效的办法。

注意一件非常重要的事情:makefile 中,有一个 tab 在 gcc 命令之前。在任何命令的开头都必须有 tab ,否则 make 将不高兴。

Makefile 2

1
2
3
4
5
CC=gcc
CFLAGS=-I
hellomake: hellomake.o hellofunc.o
$(CC) -o hellomake hellomake.o hellofunc.o -I

好的,现在我们定义了一些常量,CC 和 CFLAGS。事实表明这些是特殊的常量,它们和 make 沟通我们想怎样编译 hellomake.c 和 hellofunc.c。严格来讲, 宏 CC 是 C 编译器用到的,而 CFLAGS 是传递给编译命令的标志列表。通过在依赖列表和规则中放置 object 文件,make 知道它必须先逐个编译 .c 文件,然后再来编译可执行文件 hellomake。
使用这种形式的 makefile 适用于大部分小规模的项目。然而,有一件事情漏掉了:依赖 include 文件。例如:如果你修改了 hellomake.h,make 不会再编译这些 .c 文件,即使它需要这么做。为了修复这个问题,我们需要告诉 make ,所有的 .c 文件依赖的某些 .h 文件。我们可以在写一个简单的规则添加到 makefile 来实现修复这个问题。

Makefile 3

1
2
3
4
5
6
7
8
9
CC=gcc
CFLAGS=-I.
DEPS = hellomake.h
%.o: %.c $(DEPS)
$(CC) -c -o $@ $< $(CFLAGS)
hellomake: hellomake.o hellofunc.o
gcc -o hellomake hellomake.o hellofunc.o -I.

这次增加首先创建了宏 DEPS,它是 .c 文件依赖的 .h 文件集合。然后,我们定义了一个应用到以 .o 后缀结尾的文件的规则。这条规则说的是 .o 文件依赖 .c 文件和包括在宏 DEPS 中的 .h 文件。

这条规则然后又说了生成 .o 文件,make 需要使用在宏 CC 中定义的编译器来编译 .c 文件。-c 标识表示生成 object 文件,-o $@ 表示把编译的输出放在:左边的文件中,$< 是依赖列表中的第一项,宏 CFLAGS 如上定义。
作为最后的简化,我们用特殊的宏 $@ 和 $^,分别在 : 的左边和右边,使编译总体上变得更普遍。在下面的例子中,所有的 include 文件都应作为宏 DEPS 的一部分列出,所有的 object 文件都应作为宏 OBJ 的一部分列出。

$@ 指代当前目标,就是Make命令当前构建的那个目标。
$< 指代第一个前置条件。
$^ 指代所有前置条件,之间以空格分隔。

Makefile 4

1
2
3
4
5
6
7
8
9
10
CC=gcc
CFLAGS=-I.
DEPS = hellomake.h
OBJ = hellomake.o hellofunc.o
%.o: %.c $(DEPS)
$(CC) -c -o $@ $< $(CFLAGS)
hellomake: $(OBJ)
gcc -o $@ $^ $(CFLAGS)

那么,如果我们想要开始将 .h 文件放入到一个 include 目录下,我们的源代码放到 src 目录下,还有一个局部库放到 lib 目录下呢?此外,我们还可以将恼人而到处都是的 .o 文件隐藏起来吗?答案是当然可以。下面的 makefile 定义了 include 和 lib 目录的路径,并且将 object 文件放在 src 目录下的 obj 子目录中。它也为了你想包含的任何库而定义了一个宏,例如:数学库 -lm。这个 makefile 应该放在 src 目录下。请注意,它还包含了一条规则,如果你输入 make clean,将删除 source 和 object 目录下的文件。.PHONY 规则避免 make 用名为 clean 的文件执行某些操作。

如果当前目录中,正好有一个文件叫做 clean,那么这个命令不会执行。因为 Make 发现 clean 文件已经存在,就认为没有必要重新构建了,就不会执行指定的 rm 命令。声明 clean 是”伪目标”之后,make 就不会去检查是否存在一个叫做 clean 的文件,而是每次运行都执行对应的命令。

Makefile 5

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
IDIR =../include
CC=gcc
CFLAGS=-I$(IDIR)
ODIR=obj
LDIR =../lib
LIBS=-lm
_DEPS = hellomake.h
DEPS = $(patsubst %,$(IDIR)/%,$(_DEPS))
_OBJ = hellomake.o hellofunc.o
OBJ = $(patsubst %,$(ODIR)/%,$(_OBJ))
$(ODIR)/%.o: %.c $(DEPS)
$(CC) -c -o $@ $< $(CFLAGS)
hellomake: $(OBJ)
gcc -o $@ $^ $(CFLAGS) $(LIBS)
.PHONY: clean
clean:
rm -f $(ODIR)/*.o *~ core $(INCDIR)/*~

现在你有一个完美的 makefile,修改和管理中小型软件项目。你可以向 makefile 中添加多个规则,你甚至可以创建调用其它规则的规则。有关 makefile 和 make 函数的更多信息,请查看 GNU Make Manual,它会告诉你更多(真的)。

参考

  1. A Simple Makefile Tutorial
  2. Make 命令教程
  3. 跟我一起写Makefile