LV02-05-Makefile-01-基础知识

本文主要是makefile——基础知识相关笔记,若笔记中有错误或者不合适的地方,欢迎批评指正😃。

点击查看使用工具及版本
Windows windows11
Ubuntu Ubuntu16.04的64位版本
VMware® Workstation 16 Pro 16.2.3 build-19376536
SecureCRT Version 8.7.2 (x64 build 2214) - 正式版-2020年5月14日
开发板 正点原子 i.MX6ULL Linux阿尔法开发板
uboot NXP官方提供的uboot,NXP提供的版本为uboot-imx-rel_imx_4.1.15_2.1.0_ga(使用的uboot版本为U-Boot 2016.03)
linux内核 linux-4.15(NXP官方提供)
STM32开发板 正点原子战舰V3(STM32F103ZET6)
点击查看本文参考资料
参考方向 参考原文
Makefile 跟我一起写Makefile
点击查看相关文件下载
--- ---

一、 Make 是什么?

在一个大型工程项目中,我们一定会希望有工具自动识别修改的文件,而且不需要输入冗长的命令,就可以进行编译链接等操作,像 keil 就有,一键编译,不需要像在 Linux 中一样,使用 gcc 曲边义链接各个文件,于是 Make 工程管理器应运而生。

  • Make 可以自动识别文件时间戳,只处理修改的文件,从而避免完全编译,减少编译的工作量。
  • Make 动作的依据是 makefile 文件,这也是 Make 的唯一配置文件。

【注意】 makefile 是一个名叫 makefile 的文件,文件名必须是这个,或者首字母大写的 Makefile 或者 GNUmakefile 。

二、 Makefile 是什么?

1. Makefile 简介

Makefile 可以简单的认为是一个工程文件的编译规则,描述了整个工程的编译和链接等规则。其中包含需要编译的文件和不需要编译的文件,哪些文件需要先编译,哪些文件需要后编译,哪些文件需要重建等。编译整个工程需要涉及到的,都可以在 Makefile 中进行描述。从而避免每次都手动输入一堆源文件和参数。

那么为什么要使用 Makefile 文件呢?我自己在 Linux 中使用 C语言 开发,我直接用 gcc 命令编译不行吗?当然可以,说白了, Makefile 其实就是这些编译命令的集合,那不使用 Makefile 会有哪些可能的问题呢?

点击查看我们可能遇到的麻烦

一般来说,在 Linux 中进行 C语言 开发,我们编译文件的编译命令如下:

1
gcc -o outfile name1.c name2.c ...

其中 outfile 要生成的可执行程序的名字, nameN.c 是源文件的名字。

(1)链接库

如果 name1.c 用到了数学计算库 math 中的函数,我们得手动添加参数 -Im ; name5.c 使用到了线程,我们需要去手动添加参数 -lpthread 。

这个时候如果有大量的源文件,都涉及了第三方库的话,编译的时候命令就会很长,并且在编译的时候我们可能会涉及到文件链接的顺序问题,这个时候我们怎么办,写一份文档倒是很不错的办法,但是还是需要 CV 大法来粘贴命令不是。要是我的话,我估计直接当场自闭🤣。

(2)大工程

如果我们一个完整的工程项目,免不了要去修改工程项目的源文件,每次修改后都要去重新编译。一个大的工程项目可不仅仅只有几个源文件,多的甚至可能有成百上千个。例如一个内核,或者是一个软件的源码包。

但是如果写了 Makefile 文件的话, make 命令只会编译我们修改过的文件,没有修改的文件不用重新编译,也极大的解决了我们耗费时间的问题。

在 Windows 下的集成开发环境( IDE )已经内置了 Makefile ,或者说会自动生成 Makefile ,所以我们不用去手动编写。但是 Linux 中却不能这样,需要我们去手动的完成这项工作。

2. 第一个 makefile

接下来,先不管 Makefile 文件内容,只是写一下基本使用方式。

2.1 编写 Makefile

  • (1)创建一个 Makefile 文件
1
vim Makefile

然后在命令模式下执行 :w ,将文件写入硬盘保存起来。

  • (2)编写 Makefile 文件内容

在文件中添加以下内容:

1
2
3
4
main: main.o
gcc main.o -o main
main.o: main.c
gcc -c main.c -o main.o

【说明】 main 和 main.o 都被称之为规则的目标,后边会再详细说明。

点击查看 main.c 文件内容
1
2
3
4
5
6
7
8
#include <stdio.h>

int main(int argc, const char *argv[])
{
printf("Hello, World!\n");

return 0;
}

2.2 安装 make

刚开始使用的时候可能会提醒没有安装 make 命令,如果有这样的情况的话,按提示安装即可。

1
2
3
sudo apt-get update
sudo apt-get install ubuntu-make
sudo apt-get install make

可以通过以下命令查看 make 版本,也可以检测是否安装成功:

1
make --version

2.3 运行 Makefile

在 Makefile 文件所在目录执行以下命令:

1
make

这个时候就可以以发现,目录下多了一个 main 和一个 main.o 的文件,这就是生成的文件,在终端中执行 ./main 可以看到打印出以下内容:

1
Hello, World!

这与我们之前使用 gcc 命令编译产生的可执行文件是一致的。

2.4 指定目标

接下来我们尝试在 make 后边加上 main 和 main.o 会有什么现象。我们先手动删除刚才生成的 main 和 main.o 文件,然后在终端执行:

1
make main

然后我们发现,在目录下重新生成了 main 和 main.o 文件,也就是说 Makefile 文件内的两条命令都执行了。然后我们再次删除刚才生成的两个文件,在终端运行:

1
make main.o

这次会发现,仅仅生成了 main.o 文件,这说明, make 后边跟上目标名称,可以运行单个规则,但是加的是最终目标名称的话,是所有规则都运行(什么是规则?什么是最终目标?请看下一节)。

三、 Makefile 基础

1. 文件内容

一般来说, Makefile 文件主要包含有五个部分:

显式规则显式规则说明了,如何生成一个或多的的目标文件。这是由 Makefile 的书写者明显指出,要生成的文件,文件的依赖文件,生成的命令。
隐晦规则由于我们的 make 命名有自动推导的功能,所以隐晦的规则可以让我们比较粗糙地简略地书写 Makefile,这是由 make 命令所支持的。
变量定义在 Makefile 中我们要定义一系列的变量,变量一般都是字符串,这个有点像C语言中的宏,当 Makefile 被执行时,其中的变量都会被扩展到相应的引用位置上。
文件指示其包括了三个部分,一个是在一个 Makefile 中引用另一个 Makefile,就像C语言中的 include 一样;另一个是指根据某些情况指定 Makefile 中的有效部分,就像C语言中的预编译 #if 一样;还有就是定义一个多行的命令。有关这一部分的内容,我会在后续的部分中讲述。
注释Makefile 中只有行注释,和 UNIX 的 Shell 脚本一样,其注释是用“#”字符。如果要在我们的 Makefile 中使用“#”字符,可以用反斜框进行转义,如:“\#”。

2. 书写规则

2.1 编写格式

Makefile 文件描述的是文件编译的相关规则,它的规则主要是两个部分组成,分别是依赖的关系执行的命令,其结构如下所示:

1
2
3
4
5
target: dependency_files
<Tab>[option]command

# 或者写成一行
target: dependency_files ; command

【注意】两边的空格总是会被 make 自动删除。

点击查看各部分说明
target规则的目标,可以是 Object File(一般称它为中间文件),也可以是可执行文件,还可以是一个标签
dependency_files是目标的依赖文件,就是要生成 target 需要的文件或者是目标。可以是多个,也可以是没有
<Tab>表示命令的开始(命令前边一定要有Tab)
option@输出的信息中,不要显示此行命令(make执行过程中,默认会显示所执行的命令)。
-忽略当前此行命令执行时候所遇到的错误。如果不忽略,make在执行命令的时候,如果遇到error,会退出执行的,加上减号后,即便此行命令执行中出错,比如删除一个不存在的文件等,那么也会继续执行make。
commandmake 需要执行的命令(任意的 shell 命令)。可以有多条命令,每一条命令占一行

【说明】如果说,命令前边没有用 Tab ,或者用了几个空格代替,那就有可能会报以下错误:

1
Makefile:8: *** 遗漏分隔符 (null)。 停止。

2.2 使用实例

这里主要是写一个多文件编译的实例,单文件编译的实例在《第一个Makefile》笔记中已经写了一个。

2.2.1 测试文件

点击查看所需测试文件

各文件所在目录的结构如下:

1
2
3
4
5
6
.
├── main.c
├── Makefile
├── test1.c
├── test2.c
└── test.h
1
2
3
4
5
6
7
8
9
10
#include <stdio.h>
#include "test.h"

int main(int argc, const char *argv[])
{
printf("This is main file!\n");
test1Fun();
test2Fun();
return 0;
}
1
2
3
4
5
6
#include <stdio.h>

void test1Fun()
{
printf("This is test1.c file\n");
}
1
2
3
4
5
6
#include <stdio.h>

void test2Fun()
{
printf("This is test2.c file\n");
}
1
2
void test1Fun();
void test2Fun();

2.2.2 Makefile

在 Makefile 中我们需要指明每个目标的依赖文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
OBJ = main.o test1.o test2.o

main: ${OBJ}
gcc $(OBJ) -o main

main.o: main.c
gcc -c main.c -o main.o

test1.o: test1.c
gcc -c test1.c -o test1.o

test2.o: test2.c
gcc -c test2.c -o test2.o

.PHONY: clean
clean:
rm -rf *.o main

然后我们在终端执行 make 命令,会发现有如下信息输出:

1
2
3
4
gcc -c main.c -o main.o
gcc -c test1.c -o test1.o
gcc -c test2.c -o test2.o
gcc main.o test1.o test2.o -o main

我们执行生成的 main 可执行文件,,在终端执行 ./main ,会发现有如下信息打印:

1
2
3
This is main file!
This is test1.c file
This is test2.c file

3. 工作流程

那么 Makefile 是怎样工作的呢?当我们在执行 make 命令的时候, make 就会去当前目录下找要执行的编译规则,也就是 Makefile 文件。

编写 Makefile 的时可以使用的文件的名称 GNUmakefile 、 makefile 、 Makefile , make 执行时会去寻找 Makefile 文件,找文件也是按照这个顺序进行的(一般推荐使用 Makefile )。执行 make 命令后,当不存在 Makefile 文件时,将会出现以下提示:

1
make: *** 没有指明目标并且找不到 makefile。 停止。

接下来我们就来分析一下, Makefile 文件工作的流程。以下边文件内容为例:

1
2
3
4
5
6
7
8
main : main.o test1.o test2.o
gcc main.o test1.o test2.o -o main
main.o : main.c test.h
gcc -c main.c -o main.o
test1.o : test1.c test.h
gcc -c test1.c -o test1.o
test2.o : test2.c test.h
gcc -c test2.c -o test2.o

在编译项目文件的时候,默认情况下, make 执行的是 Makefile 中的第一规则( Makefile 中出现的第一个依赖关系),这个规则的第一目标称之为最终目标或者是终极目标

  • 执行 make ,确定最终目标

当我们在终端输入 make 命令后, make 命令会读取当前目录下的 Makefile 文件,并将 Makefile 文件中的第一个目标作为其执行的最终目标,也就是生成 main 可执行文件。

  • 处理最终目标的规则

接下来,开始处理第一个规则,也就是最终目标所在的规则,在本例中就是:

1
2
main : main.o test1.o test2.o
gcc main.o test1.o test2.o -o main

这个规则描述了最终要生成的 main 文件的依赖关系(需要哪些文件),并定义了链接 .o 文件生成 main 文件的命令。但是这是第一个规则, main.o 、 test1.o 和 test2.o 文件可能还没有生成啊,怎么办呢?其实在处理第一个规则所定义的命令之前, make 会先处理最终目标 main 的所有的依赖文件(也就是例子中的这些 .o 文件)的更新规则(就是后边以这些 .o 文件为目标的规则)

  • 处理最终目标的依赖文件

对这些 .o 文件为目标的规则处理有下列三种情况:

(1)目标 .o 文件不存在,使用其描述规则创建相应的 .o 文件。

(2)目标 .o 文件存在,目标 .o 文件所依赖的 .c 源文件和 .h 文件中的任何一个比目标 .o 文件更新(就是说在上一次 make 之后被修改过),那么就根据规则重新编译生成相应的 .o 文件。

(3)目标 .o 文件存在,目标 .o” 文件比它的任何一个依赖文件( .c 源文件、 .h” 文件)更新(它的依赖文件在上一次 make 之后没有被修改),则什么也不做。

通过上面的更新规则我们可以了解到中间文件的作用,也就是编译时生成的 .o 文件。它们的作用是检查某个源文件是不是进行过修改,最终目标文件是不是需要重建。我们执行 make 命令时,只有修改过的源文件或者是不存在的目标文件会进行重建,而那些没有改变的文件不用重新编译,这样在很大程度上节省时间,提高编程效率。

点击查看 GNU 的 make 执行步骤

上边其实已经分析了 make 的工作流程,这里简单了解一下 GNU 的 make ,相当于做一个总结吧。大多数的 make 应该也是很类似。

  • (1) 读入所有的Makefile。
  • (2) 读入被 include 的其它 Makefile 。
  • (3) 初始化文件中的变量。
  • (4) 推导隐晦规则,并分析所有规则。
  • (5) 为所有的目标文件创建依赖关系链。
  • (6) 根据依赖关系,决定哪些目标要重新生成。
  • (7) 执行生成命令。

(1)-(5) 步为第一个阶段, (6)-(7) 为第二个阶段。第一个阶段中,如果定义的变量被使用了,那么, make 会把其展开在使用的位置。但 make 并不会完全马上展开,如果变量出现在依赖关系的规则中,那么只有当这条依赖被决定要使用了,变量才会在其内部展开。

4. 清除过程文件

但是,很多时候我们并不需要中间的 .o 文件啊,而且如果文件超级多的话,生成的文件都混合在一起,这样的文件夹看起来也太乱了吧,没关系,我们不是有个命令叫 rm 嘛,当然也可以用在 Makefile 中啦。格式如下:

1
2
3
.PHONY: clean
clean:
rm -rf *.o main
.PHONY: clean说明 clean 是伪目标(后边会解释),不是具体的文件。
*.o是执行过程中产生的中间文件
main是最终生成的目标文件(也是第一个规则生成的目标文件)

clean 的书写规则与上边一致,但是它后边是没有依赖文件的,不是具体的文件。不会与第一个目标文件相关联,所以我们在执行 make 的时候也不会执行下面的命令。但是需要指明这是一个伪目标,后边会解释。然后我们在终端执行以下命令就可以删除所有的中间文件了:

1
make clean