21.1 基本规则

源文件编译过程通常用 Makefile 管理。

书上例子代码太多,这里搞个简单例子:
main.c:

  1. /* main.c */
  2. #include <stdio.h>
  3. #include "my_test.h"
  4. int main(void)
  5. {
  6. my_test();
  7. printf("hello world\n");
  8. return 0;
  9. }

my_test.c:

  1. /* my_test.c */
  2. #include <stdio.h>
  3. void my_test(void)
  4. {
  5. printf("test\n");
  6. }

my_test.h:

  1. /* my_test.h */
  2. #ifndef TEST
  3. #define TEST
  4. extern void my_test(void);
  5. #endif

Makefile:

  1. # Makefile
  2. main: main.o my_test.o
  3. gcc main.o my_test.o -o main
  4. main.o: main.c my_test.h
  5. gcc -c main.c
  6. my_test.o: my_test.c
  7. gcc -c my_test.c
  8. clean:
  9. @echo "cleanning project" # 命令前面的 @字符使得不显示命令本身而只显示它的输出结果
  10. -rm main *.o # -字符使得即使这条命令出错,make 也会继续执行后续命令而非立刻终止
  11. @echo "clean completed"
  12. .PHONY: clean # .PHONY 是 make 内建的特殊目标,声明 clean 是伪目标,否则若存在 clean 文件,
  13. # make 会误认为 clean 存在且无依赖条件,便不再执行 clean 目标

Makefile 文件和源代码放在同一目录下,make 命令第一个参数是目标,缺省时把 Makefile 中第一条规则当做缺省目标。缺省目标也是目标,make 命令所做的工作都是为该目标而做,达到目标即停。
Makefile 由一组规则组成,每条规则的格式是:

目标 ...: 条件 ...
    命令1
    命令2
    ...

欲更新目标,必须先更新它的所有条件;所有条件中只要有一个条件被更新了,目标也必须随之被更新。

粗浅理解:make 执行一条规则的大致步骤就是先检查目标的条件,如果条件又是另外的目标,则要递归去执行另外的目标,直到某条件不再是别的目标且该条件是个存在的文件为止,然后若发现该条件文件修改时间比目标还要新,则往回递归的时候对应目标需要更新,如果发现某个条件既不存在另外的目标去生成,且这个条件文件又不存在,则报错退出。
然后再检查最初的目标,若文件不存在,或存在但某条件有更新,或某条件未必是文件但更新过,则最初目标都要更新,之后无论是否生成最初目标文件,都认为目标更新过了。

make 处理 Makefile 的过程分两阶段:

  1. 从前到后读取所有规则,建立完成依赖关系图。

image.png

  1. 从缺省目标或命令行指定的目标开始,根据依赖关系图选择适当的规则执行。

一些约定俗成的目标名字:

  • 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 />![image.png](https://cdn.nlark.com/yuque/0/2021/png/126049/1640853994772-d60bb1cb-a9d8-4a09-b694-6ae3df3d0ed6.png#clientId=u52999764-ddea-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=229&id=u68b57169&margin=%5Bobject%20Object%5D&name=image.png&originHeight=229&originWidth=529&originalType=binary&ratio=1&rotation=0&showTitle=false&size=40019&status=done&style=none&taskId=ua16a4a43-a73d-4776-bec8-66875afcb5f&title=&width=529)<br />Linux 上 `cc` 是指向 gcc 的符号链接。<br />`CC = cc` 是定义变量,惯例是大写字母加下划线命名,使用时通过 `$(变量名)` 展开。<br />`$@` 和 `$<` 是特殊变量,前者取值是当前规则中的目标,后者取值是当前规则中的第一个条件。<br />`%.o: %.c` 是一种特殊的规则叫做**模式规则。**

执行结果中,第一条命令就是 make 根据内建的隐含规则推出来的:<br />![image.png](https://cdn.nlark.com/yuque/0/2021/png/126049/1640853819661-ecfa140d-d029-4270-8683-8650a17ed695.png#clientId=u52999764-ddea-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=90&id=u22f6d92c&margin=%5Bobject%20Object%5D&name=image.png&originHeight=90&originWidth=446&originalType=binary&ratio=1&rotation=0&showTitle=false&size=14425&status=done&style=none&taskId=u40731e42-26cc-4834-bf41-26beac6019c&title=&width=446)

编写 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 />![image.png](https://cdn.nlark.com/yuque/0/2021/png/126049/1640918942336-8400de9a-a4a5-4914-a948-7aeab7e52b23.png#clientId=u52999764-ddea-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=260&id=ufe12938c&margin=%5Bobject%20Object%5D&name=image.png&originHeight=260&originWidth=792&originalType=binary&ratio=1&rotation=0&showTitle=false&size=47095&status=done&style=none&taskId=ue63a41f9-6d74-4768-87f2-2454eb7ee22&title=&width=792)
<a name="qIca0"></a>
## 21.4 自动处理头文件的依赖关系
手工维护目标文件依赖哪些头文件,容易出错,所以要想办法自动维护,保持 DRY(Don't Repeat Yourself)原则。<br />第一步,设法把源代码中的依赖关系信息抽出来,并自动转换成 Makefile 中的规则。使用 `gcc -M *.c` 就能自动分析目标文件和源文件的依赖关系,并以 Makefile 规则格式输出。<br />但系统头文件一般不需要我们随程序一起维护,所以通常是用 `gcc -MM *.c` 使输出结果中只包含我们自己写的头文件:<br />![image.png](https://cdn.nlark.com/yuque/0/2021/png/126049/1640932791241-7932fc5d-e7e5-423e-90d4-3f9cd68e8c69.png#clientId=u52999764-ddea-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=60&id=ua059b097&margin=%5Bobject%20Object%5D&name=image.png&originHeight=60&originWidth=514&originalType=binary&ratio=1&rotation=0&showTitle=false&size=12308&status=done&style=none&taskId=uceea23af-e48e-4d53-a474-bf5a2d7315a&title=&width=514)<br />第二步,设法把得到的规则添加到 Makefile 中,Scott McPeak 提供的方案:<br />![image.png](https://cdn.nlark.com/yuque/0/2021/png/126049/1640933001709-ba2f703f-352b-487a-8652-bc661315d213.png#clientId=u52999764-ddea-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=410&id=u57bdd1e6&margin=%5Bobject%20Object%5D&name=image.png&originHeight=410&originWidth=608&originalType=binary&ratio=1&rotation=0&showTitle=false&size=124478&status=done&style=none&taskId=u4849063e-22e4-4b35-b6f8-f2bc533aa9c&title=&width=608)<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)

变量定义优先级对比(从高到低):

  1. make 命令行选项中定义
  2. Makefile 中定义
  3. 环境变量中定义(若使用了 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 命令。