0%

Makefile 入门

原来的笔记写于 2015年11月23日,当时使用的是为知笔记默认编辑器。十个月过去了,对于 make & Makefile 也有了更多的认识。今天重新整理一下,不过仍然定位在入门的帖子,所以不会添加新的内容,只是修改表述不当的地方,对原有内容作出删减,并用 markdown 格式重写。

这篇本来是打算写成阅读笔记的。但是所知不多,刚开始学习应该博览,求入门,求上手使用。再加上看的东西越来愈多,单纯的记录一篇帖子中的重点、难点,较真于一处在实践中基本不会碰到的细节,格局太小,意义也不大。

在正式讲解之前,我们先介绍一下使用的文件:

1
2
3
4
5
6
7
8
9
10
11
vimer@debian8light:~/see-the-world/code/make_learn/example$ tree
.
└── prj
├── abc.c
├── abc.h
├── main.c
├── xyz.c
└── xyz.h

1 directory, 5 files
vimer@debian8light:~/see-the-world/code/make_learn/example$

其中 main.c 包含头文件 abc.hxyz.habc.c 包含头文件 abc.hxyz.c 包含头文件 xyz.h

完整的手写 Makefile

明明白白、完完整整地写 Makefile 文件,是这个样子的:

1
2
3
4
5
6
7
8
9
10
11
main: main.o abc.o xyz.o
gcc main.o abc.o xyz.o -o main

main.o: main.c abc.h xyz.h
gcc -c -o main.o main.c

abc.o: abc.c abc.h
gcc -c -o abc.o abc.c

xyz.o: xyz.c xyz.h
gcc -c -o xyz.o xyz.c

但事实上,我们在实际项目中并不这样写。从例子可以看到其中有相似的重复的操作,而当代码文件增加几倍后,管理这些重复的命令将会是“费力不讨好”的事情。好在 Makefile 提供了隐含规则和自动推导帮我们“省力”。

隐含规则和自动推导

我们可以将以上的 Makefile 文件写得更简单,但功能相同:

1
2
3
4
5
6
CC=gcc

main: abc.o xyz.o
main.o: abc.h xyz.h
abc.o: abc.h
xyz.o: xyz.h

CC 是 Makefile 的预定义变量之一,用于指定编译 C 项目时所用编译器。如果我们不指定 CC 变量,则调用其默认值 cc 编译器。这在Linux上没有问题,因为 cc 常常会链接到 gcc 程序。

Makefile 预定义变量用于隐含规则生成的每条编译命令中,所以只要为这些预定义变量指定新的值,就可以改变隐含规则的默认动作。除了 CC 之外,常用的还有 CFLAGS CPPFLAGS LDLIBS LDFLAGS 等等。

这个 Makefile 文件只描述了部分依赖关系,源文件的编译命令和目标文件的链接命令也都被省略了。这正是 Makefile 的自动推导功能:它可以将目标文件自动依赖于同名的源文件(可执行文件自动依赖于同名的目标文件或源文件),根据扩展后的依赖关系使用隐含规则生成目标文件。所以其功能完全等价于第一节中我们完整手写的 Makefile 文件。

隐含规则使用的变量

在隐含规则中的命令中,基本上都是使用了一些预先设置的变量。你可以:

  1. 在你的 makefile 中改变这些变量的值,
  2. 或是在 make 的命令行中传入这些值,
  3. 或是在你的环境变量中设置这些值,

无论怎么样,只要设置了这些特定的变量,那么其就会对隐含规则起作用。当然,你也可以利用 make 的“-R”或“–no–builtin-variables”参数来取消你所定义的变量对隐含规则的作用。

例如,编译 C 程序的隐含规则的命令是“$(CC) –c $(CFLAGS) $(CPPFLAGS)”。Make默认的编译命令是“cc”,如果你把变量“$(CC)”重定义成“gcc”,把变量“$(CFLAGS)”重定义成“-g”,那么,隐含规则中的命令全部会以 “gcc –c -g $(CPPFLAGS)”的样子来执行了。

我们可以把隐含规则中使用的变量分成两种:一种是命令相关的,如“CC”;一种是参数相关的,如“CFLAGS”。在此只描述最常用的几个变量,更多的请移步 Variables Used by Implicit Rules

  1. 关于命令的变量。

    • AR 函数库打包程序。默认命令是“ar”。
    • CC C 语言编译程序。默认命令是“cc”。
    • CXX C++ 语言编译程序。默认命令是“g++”。
    • CPP(the C preprocessor,C P re P rocessor) C程序的预处理器(输出是标准输出设备)。默认命令是“$(CC) –E”。
    • RM 删除文件命令。默认命令是“rm –f”。
  2. 关于命令参数的变量:如果没有指明其默认值,那么其默认值都是空。

    • ARFLAGS 函数库打包程序 AR 命令的参数。默认值是“rv”。

    • CFLAGS C 语言编译器参数。

    • CXXFLAGS C++ 语言编译器参数。

    • CPPFLAGS C 预处理器参数。( C 和 Fortran 编译器也会用到)。

      CPPFLAGS
      Extra flags to give to the C preprocessor and programs that use it (the C and Fortran compilers).

    • LDFLAGS 链接器参数。(如:“ld”)

      LDFLAGS
      Extra flags to give to compilers when they are supposed to invoke the linker, ‘ld’, such as -L. Libraries (-lfoo) should be added to the LDLIBS variable instead.

    • LDLIBS

      Library flags or names given to compilers when they are supposed to invoke the linker, ‘ld’. LOADLIBES is a deprecated (but still supported) alternative to LDLIBS. Non-library linker flags, such as -L, should go in the LDFLAGS variable.

有几组容易混淆的变量,需要重点区分:

  1. 区分 CFLAGS CXXFLAGS CPPFLAGS

    首先 CPPFLAGS 作用于预处理器 CPP,所以能够同时用于 c代码和c++代码,eg -I./;CFLAGS CXXFLAGS 作用于编译器,分别针对C 编译器器、C++编译器,eg -g --std=c++11。编译器参数并不会传给链接器使用。

  2. 区分 LDFLAGSLDLIBS

    LDFLAGS 告诉链接器从哪里寻找库文件,LIBS 告诉链接器要链接哪些库文件,eg -L ../ -lpthread

自动生成依赖关系

此节参考自:自动处理头文件的依赖关系

上一节中如果不添加目标文件对头文件的依赖关系,如下:

1
2
3
CC=gcc

main:abc.o xyz.o

导致的直接问题就是:修改项目的头文件之后,执行 make 命令不会自动更新可执行文件。(在这半年多时间里,我写的 Makefile 大多都缺少针对头文件的依赖关系)

可是在写 main.o、abc.o 和 xyz.o 这三个目标的规则时要查看源代码,找出它们依赖于哪些头文件,这很容易出错,一是因为有的头文件包含在另一个头文件中,在写规则时很容易遗漏,二是如果以后修改源代码改变了依赖关系,很可能忘记修改 Makefile 的规则。

能不能让 make & Makefile 帮我们做了这个工作?很不幸,不能。Makefile 的自动推导依赖同名,从源文件到目标文件到可执行文件。而且一个源文件往往依赖很多个头文件,所以 Makefile 无法帮我们做自动依赖。必须由我们来告诉 Makefile 详细的依赖关系。好在我们可以用 gcc 的 -M 选项自动生成目标文件和源文件的依赖关系,然后把这些规则包含到 Makefile 中。

-M 选项把 stdio.h 以及它所包含的系统头文件也找出来了,如果我们不需要输出系统头文件的依赖关系,可以用 -MM 选项:

1
2
3
4
5
6
7
vimer@debian8light:~/see-the-world/code/make_learn/example/prj$ gcc -MM main.c 
main.o: main.c abc.h xyz.h
vimer@debian8light:~/see-the-world/code/make_learn/example/prj$ gcc -MM *.c
abc.o: abc.c abc.h
main.o: main.c abc.h xyz.h
xyz.o: xyz.c xyz.h
vimer@debian8light:~/see-the-world/code/make_learn/example/prj$

gcc -MM 自动生成的规则包含到 Makefile 中,GNU make的官方手册 - Generating Prerequisites Automatically 建议这样写:

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
CC=gcc

TARGET=main
SRCS=main.c abc.c xyz.c
#SRCS=$(wildcard *.c)
OBJS=${SRCS:.c=.o}
DEPS=${SRCS:.c=.d}

.PHONY:all clean

all:${TARGET}

${TARGET}:${OBJS}

clean:
${RM} ${TARGET} ${OBJS} ${DEPS}

# 自动生成依赖关系
include ${DEPS}

%.d: %.c
set -e; rm -f $@; \
$(CC) -MM $(CPPFLAGS) $< > $@.$$$$; \
sed 's,\($*\)\.o[ : ]*,\1.o $@ : ,g' < $@.$$$$ > $@; \
rm -f $@.$$$$

${SRCS:.c=.d} 表示将 SRC 中的以 *.c 结尾的源文件名替换为 *.d 的形式,比如 main.c 对应着文件 main.d,这就是 main.c 的依赖关系文件。也可以替换成 .*.d 的形式用来隐藏文件。

.PHONY 伪目标的概念请移步下一节了解。

为了生成每个源文件的依赖文件,建立了目标依赖关系 %.d: %.c,该关系表示,对于目标集合,通过 $@ 可以访问一个依赖文件,通过 $< 则访问对应的同名源文件。其他更多的语法特性请自行 google 学习。

将该文件应用于任何单目录的 C/C++ 工程(C++ 需要修改部分细节,不作赘述)都能正常工作。

另一种方式

前者针对每个源文件单独生成对应的依赖文件,而下面是将所有的依赖规则放到了一个文件中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
EXE=main
CC=gcc
SRCS=$(wildcard *.c) #如果看不懂,自行google
OBJS=$(SRCS:.c=.o)
CFLAGS=-g

.PHONY: all
all: $(EXE)
#all: $(EXE) .depend #这种写法没有意义。详见“include调用方式&Makefile执行过程”

.depend: $(SRCS)
@$(CC) -MM $(SRCS) > .depend

-include .depend #和其它版本 make 兼容的相关命令是 sinclude

$(EXE): $(OBJS)
$(CC) $^ -o $@

.PHONY: clean
clean:
rm $(EXE) $(OBJS) .depend -f

网上有 一种模棱两可的说法,认为此种方式是过时的,被淘汰的。原因是在 Generating Prerequisites Automatically 中有如下描述:

With old make programs, it was traditional practice to use this compiler feature to generate prerequisites on demand with a command like ‘make depend’. That command would create a file depend containing all the automatically-generated prerequisites; then the makefile could use include to read them in.

In GNU make, the feature of remaking makefiles makes this practice obsolete(废弃的;过时的)—you need never tell make explicitly to regenerate the prerequisites, because it always regenerates any makefile that is out of date.

我一开始被误导,先入为主也认为此种方法不合适。然而始终未明白此方法哪里不好。

  1. 官方手册 How Makefiles Are Remade

    after reading in all makefiles, make will consider each as a goal target and attempt to update it. If a makefile has a rule which says how to update it (found either in that very makefile or in another one) or if an implicit rule applies to it (see Using Implicit Rules), it will be updated if necessary. After all makefiles have been checked, if any have actually been changed, make starts with a clean slate and reads all the makefiles over again. (It will also attempt to update each of them over again, but normally this will not change them again, since they are already up to date.)

  2. 另外,自动处理头文件的依赖关系 中描述:

    不管是Makefile本身还是被它包含的文件,只要有一个文件在make过程中被更新了,make就会重新读取整个Makefile以及被它包含的所有文件

  3. 我自己也另有笔记 《Makefile 执行过程 & include 调用方式》

仔细阅读上述3个链接之后,我认为:两种方式并无本质区别,是一样的。官方手册中提及的“obsolete”是针对以前的非 GNU make 的 make 版本,“old make programs” 和 GNU make 的一个显著区别是不会 “remaking makefiles”。即 include 操作只会单纯的包含目标文件进来,而不关心其内容是否是待更新的(即,在 include 之后其目标更新了),所以“old make programs” 总是要先手动执行 make depend 保证依赖文件是最新的,然后再执行 make 命令。区别是由 “old make programs” 和 GNU make 带来的,而不是上述两种书写方式带来的。

更多的特性

Makefile 文件最核心的内容就是上面这些。但是为了使用上方便,扩展更多的功能,比如 make clean make install,Makefile 文件的包含引用 include 等等,延伸出来了好些东西。有些特性作为惯例、最佳实践被开发者普遍使用。

.PHONY 伪目标

开发过程中肯定有清理目标文件、可执行文件的需求,但是频频使用 rm abc.o xyz.o main.o main 命令手工删除不是我们风格。所以就有

1
2
clean:
rm abc.o xyz.o main.o main

我们直接执行 make clean 就可以达到清理目录的目的。但这么写有两个问题:

  1. 当某个删除目标不存在时 rm 就会执行失败,进而 make 报错执行 clean 目标失败。为了规避此问题,我们使用 rm -f,与此等价的 Makefile 预定义变量是 ${RM}
  2. 如果目录下存在 clean 同名文件,因为 clean 不存在依赖目标,所以 clean 就是最新的,就不会再执行 rm -f 命令。此问题的解决方法是:将 clean 声明为 .PHONY 伪目标。

为了偷懒,不用在多个地方重复修改,我们引入 ${TARGET} ${OBJS}变量。按照惯例,用 all 做缺省目标。Makefile 文件变成下面这个样子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
CC=gcc

TARGET=main
OBJS=main.o abc.o xyz.o

.PHONY:all clean

all:${TARGET}

${TARGET}:${OBJS}
main.o:abc.h xyz.h
abc.o: abc.h
xyz.o: xyz.h

clean:
${RM} ${TARGET} ${OBJS}

include 和 sinclude

Makefile 中 include 的调用方式:(详见 《Makefile 执行过程 & include 调用方式》

  1. 首先在指定的目录下搜索被调用文件(如果没有路径,则为当前目录)
  2. 如果没有找到,则从 -I 所指定的 include 目录查找,如果还没找到,则从 /usr/gnu/include /usr/local/include 等目录查找
  3. 最终结果还是没找到,则 Makefile 输出异常信息 No such file or directory但此时不会立刻退出,而是继续处理 Makefile 的后续内容
  4. 当完成读取整个 Makefile 后,make 将试图使用规则来创建通过指示符 include 指定的但未找到的文件
  5. 如果没有对应规则,输出错误信息 make: *** No rule to make target 'non-exit-file'. Stop.。Makefile终止执行

通常我们在 Makefile 中可使用 -include 来代替 include,来忽略由于包含文件不存在或者无法创建时的错误提示。- 的意思是告诉 make,忽略此操作的错误,继续执行。

include 会报错导致 make 终止执行;-include 不会终止 make 的执行;而为了进一步和其它的 make 程序进行兼容,也可以使用 sinclude 来代替 -include。综合以上,优先使用 sinlude

其他

用 make 命令加 -p 选项后,可以打印出系统缺省定义的内部规则。它们包括系统预定义的宏、以及产生某些种类后缀的文件的内部相关行。内部规则涉及的文件种类很多,它不仅包括 C 源程序文件及其目标文件,还包括 SCCS 文件、yacc 文件和 lex 文件,甚至还包括 Shell 文件。

注意 gmake 和 make 的区别:

gmake 是GNU Make的缩写。 Linux系统环境下的 make 就是 GNU Make,之所以有 gmake,是因为在别的平台上,make 一般被占用,GNU make 只好叫 gmake 了。 引用来源

结语

通过前边的讨论,我们得到一个能在单目录工程下工作的通用 Makefile,至于是实现为单独一个依赖文件的形式,还是每个源文件产生一个独立的依赖文件,要根据程序作者自己的喜恶来选择。虽然每种方法都有一些细微的瑕疵,但是不影响这个通用的 Makefile 的实用性,试想一下在工程目录下拷贝一份当前的 Makefile,稍加修改便可以正确的编译开发,一定会令人心情大好。

网上关于 make & Makefile 的入门有很多不错的文章,单篇和系列的都有。整理此篇笔记时也多有参考、摘抄。列举如下:

  1. 如果之前一点也不了解 make,想快速上手写 makefile 编译小程序。推荐 《如何自己编写Makefile》 这一篇。★★★
  2. 如果想更进一步的学习,则推荐 《 跟我一起写 Makefile》 系列,总共十四篇。具体目录可以下载附件查看,另有此系列汇总的pdf版本,也一并上传在附件中。★★★★★
  3. 如果较真于语法,可以查看 Makefile 使用总结,目录也在附件中。可以用来检索,综合来说意义不大。★★
  4. 发现一篇很好的帖子,不吐不快。《说说Makefile那些事儿》,重点是看完1、2之后,读这篇帖子还能学到新的东西。★★★★★

实际项目中肯定会涉及到复杂的目录结构以及对链接库的使用,在姊妹篇 《不同场景下的 Makefile》 (也是入门篇)略有提及。关于链接库更详细的知识请移步 《共享库 & 静态库》,在大型的项目中手工编写 Makefile 都是一场挑战,Makefile 自动化生成请移步 [《》][/(ㄒoㄒ)/~~ 我还没整理呢]。