21.1 基本规则
源文件编译过程通常用 Makefile 管理。
书上例子代码太多,这里搞个简单例子:
main.c:
/* main.c */#include <stdio.h>#include "my_test.h"int main(void){my_test();printf("hello world\n");return 0;}
my_test.c:
/* my_test.c */#include <stdio.h>void my_test(void){printf("test\n");}
my_test.h:
/* my_test.h */#ifndef TEST#define TESTextern void my_test(void);#endif
Makefile:
# Makefilemain: main.o my_test.ogcc main.o my_test.o -o mainmain.o: main.c my_test.hgcc -c main.cmy_test.o: my_test.cgcc -c my_test.cclean:@echo "cleanning project" # 命令前面的 @字符使得不显示命令本身而只显示它的输出结果-rm main *.o # -字符使得即使这条命令出错,make 也会继续执行后续命令而非立刻终止@echo "clean completed".PHONY: clean # .PHONY 是 make 内建的特殊目标,声明 clean 是伪目标,否则若存在 clean 文件,# make 会误认为 clean 存在且无依赖条件,便不再执行 clean 目标
Makefile 文件和源代码放在同一目录下,make 命令第一个参数是目标,缺省时把 Makefile 中第一条规则当做缺省目标。缺省目标也是目标,make 命令所做的工作都是为该目标而做,达到目标即停。
Makefile 由一组规则组成,每条规则的格式是:
目标 ...: 条件 ...
命令1
命令2
...
欲更新目标,必须先更新它的所有条件;所有条件中只要有一个条件被更新了,目标也必须随之被更新。
粗浅理解:make 执行一条规则的大致步骤就是先检查目标的条件,如果条件又是另外的目标,则要递归去执行另外的目标,直到某条件不再是别的目标且该条件是个存在的文件为止,然后若发现该条件文件修改时间比目标还要新,则往回递归的时候对应目标需要更新,如果发现某个条件既不存在另外的目标去生成,且这个条件文件又不存在,则报错退出。
然后再检查最初的目标,若文件不存在,或存在但某条件有更新,或某条件未必是文件但更新过,则最初目标都要更新,之后无论是否生成最初目标文件,都认为目标更新过了。
make 处理 Makefile 的过程分两阶段:
- 从前到后读取所有规则,建立完成依赖关系图。

- 从缺省目标或命令行指定的目标开始,根据依赖关系图选择适当的规则执行。
一些约定俗成的目标名字:
- all 执行主要的编译工作,常用作缺省目标
- install 执行编译后的安装工作,把文件拷贝到不同安装目录等
- clean 删除编译生成的二进制文件
- distclean 不仅删除二进制文件,也删除其他的生成文件,只留下源文件
21.2 隐含规则和模式规则
一个目标依赖的所有条件不一定非得写在一条规则中,也可拆开写,但此时其中只有一条规则允许有命令列表,否则 make 报警告采用最后一条规则的命令列表。 ```makefile main.o: main.c my_test.h gcc -c main.c
拆成下面两条规则
main.o: my_test.h
main.o: main.c gcc -c main.c
越拆越多更繁琐,但可删去第二条,可这样 main.o 连编译命令都无了,其实还能编译。之所以能放心省略第二条规则,是因为:<br />**如果一个目标在 Makefile 中的所有规则都没有命令列表,make 会尝试在内建的隐含规则(Implicit Rule)中查找适用的规则。**
目录下 `make -p` 可查看由 make 融合后的:内建的隐含规则 + 我们自己写的规则,上例改造中实际起作用的是下面几条隐含规则及变量定义:<br /><br />Linux 上 `cc` 是指向 gcc 的符号链接。<br />`CC = cc` 是定义变量,惯例是大写字母加下划线命名,使用时通过 `$(变量名)` 展开。<br />`$@` 和 `$<` 是特殊变量,前者取值是当前规则中的目标,后者取值是当前规则中的第一个条件。<br />`%.o: %.c` 是一种特殊的规则叫做**模式规则。**
执行结果中,第一条命令就是 make 根据内建的隐含规则推出来的:<br />
编写 Makefile 规则的目的是让 make 建立依赖关系图,所以不管怎么写,只要把依赖关系描述清楚就行,先前是**以目标为中心**,一个目标依赖于若干条件;换个角度也能**以条件为中心**,例如:<br />`main.o stack.o maze.o: main.h` # 三个目标都依赖同一个条件。<br />对于多目标规则,make 会拆成几条单目标规则来处理。
<a name="vSeVe"></a>
## 21.3 变量
通过 `=` 等号定义一个变量,如果右边有需要展开的形式,并不会定义变量时就立即展开,而是直到这个变量取值时才递归展开。<br />所以此时,多个变量定义时的前后顺序无所谓,因为 Makefile 是先被解析构建成依赖关系图之后,才开始选择适当规则来执行的。执行命令时涉及到变量取值,才会进行展开(递归展开)。
该特性坏处时可能写出无穷递归的定义,虽然 make 能检测出来这种错误不会死循环,<br />因此,定义变量还可以用 `:=` 号,这样的变量在 make 读到定义时就立即展开右边。
定义变量时使用不同等号的区别:
- = 变量取值时才展开
- := 读到定义时就立即展开变量
- ?= 若没定义过则相当于 =,否则什么也不做
- += 给变量追加值,会自动在中间添加一个空格,且延续前一种等号的性质,若无前者,则相当于 =
常用自动变量:
- $@ 规则中的目标
- $* 模式规则中的 Stem(柄)
- $< 规则中的第一个条件
- $? 规则中所有比目标新的条件,组成一个列表,以空格分隔
- $^ 规则中的所有条件,组成一个列表,以空格分隔,并会消除重复项
make -p 看到的隐含规则中用到了很多变量,有些没定义,有些定义了缺省值,写 Makefile 时可以重新定义这些变量的值或追加值,使用这些变量自然也是为了一处修改,到处生效,易于维护,部分:<br />
<a name="qIca0"></a>
## 21.4 自动处理头文件的依赖关系
手工维护目标文件依赖哪些头文件,容易出错,所以要想办法自动维护,保持 DRY(Don't Repeat Yourself)原则。<br />第一步,设法把源代码中的依赖关系信息抽出来,并自动转换成 Makefile 中的规则。使用 `gcc -M *.c` 就能自动分析目标文件和源文件的依赖关系,并以 Makefile 规则格式输出。<br />但系统头文件一般不需要我们随程序一起维护,所以通常是用 `gcc -MM *.c` 使输出结果中只包含我们自己写的头文件:<br /><br />第二步,设法把得到的规则添加到 Makefile 中,Scott McPeak 提供的方案:<br /><br />`OBJS` 变量的值是要编译生成的 .o 文件列表,然后 include 时变量替换,更改后缀。
`include` 类似于 C 的 #include 预处理指示,图中表示把那三个文件包含到当前的 Makefile 中,开头的 `-` 表示忽略不存在的文件,只包含存在的文件,否则若三个文件中有不存在的文件,则 make 会报错。<br />include 不仅仅是把文件包含进来,还会对于每个要包含进来的文件,把文件名当做目标尝试更新,该过程中若真有对应的目标且文件被更新了,则 make 会重新从头执行,重新包含更新后的文件。
`sed` 命令是在文件中做编辑、查找、替换,`fmt` 是段落排版。
具体过程略,用到时再研究。
<a name="Hgrvh"></a>
## 21.5 常用的 make 命令行选项
- -n 只打印要执行的命令,而不会真的执行命令(Dry Run)。
- -C 切换到另一个目录执行那个目录下的 Makefile,执行完后会退回到先前的目录。
- make 命令行也可用 = 或 := 定义变量,如 `make CFLAGS=-g` 为本次编译增加 gcc 调试选项 -g。
make 进程中的环境变量也可起到 Makefile 变量的作用:
```bash
export foo=2
make
但如果 Makefile 也定义了相同变量,则默认情况下采用 Makefile 中的定义,但 -e 选项能让环境变量覆盖 Makefile 中的定义:
foo = 1
all:
@echo $(foo)
变量定义优先级对比(从高到低):
- make 命令行选项中定义
- Makefile 中定义
- 环境变量中定义(若使用了 make -e 参数,则高低优先级变为 1 3 2)
shell 中执行 make 时还可以直接给 make 进程传递环境变量:
foo=3 make # 这是环境变量
大规模项目中,可在上层目录的 Makefile 里用 export 声明一些变量,会自动传给 make -C 命令做环境变量:
foo = string1
export foo
all:
$(MAKE) -C subdir
另外,如果是命令行选项中定义的变量,则不需要 export,也能直接传给 Makefile 里的 make -C 命令。
