【Linux系统】—— make/makefile
- 1 什么是 make/makefile
- 2 第一版本makefile
- 3 依赖关系和依赖方法
- 4 清理
- 4.1 清理的基本语法
- 4.2 make 的默认执行
- 4.3 为什么要加 『.PHONY:clean』
- 4.3.1 『.PHONY:clean』的功能
- 4.3.2 如何理解总是不被执行
- 4.3.2 如何区分文件的新旧
- 5 第二版本makefile
- 6 第三版本makefile
- 7 终版makefile
1 什么是 make/makefile
一句话概括:
make
是一个命令
makefile
是一个文件会不会写 makefile,侧面说明了一个人是否具备完成大型工程的能力
一个工程中的源文件不计其数,其按类型、功能、模块分别放在若干个目录中,makefile 定义了一系列的规则来指定,哪些文件需要先编译,哪些文件需要后编译,哪些文件需要重新编译,甚至于进行更复杂的功能操作
makefile 带来的好处就是 “自动化编译”,一旦写好,只需要一个 make 命令,整个工程完全自动编译,极大提高了软件开发的效率
make 是一个命令工具,是一个解释 makefile 中指令的命令工具,一般来说,大多数的IDE都有这个命令,比如:Delphi 的 make,Visual C++ 的 nmake,Linux 下 GNU 的 make。可见,makefile 都成为了一种在工程方面的编译方法
make 是一条命令,makefile是一个文件,两个搭配使用,完成项目自动化构建。
2 第一版本makefile
在学习 make/makefile 之前,我们先来简单看看它的功能和使用方法
- 先创建一个 code.c 的源文件,输入代码:
#include<stdio.h>int main()
{printf("Hello Linux!\n");return 0;
}
2. 在当前路径下创建一个 makefile 文件
touch makefile/Makefile
注:makefile 与 Makefile 都行,不区分大小写。后文统一用 makefile 代替 makefile/Makefile
- 用vim打开makefile文件,输入以下内容
4. 编译
makefile 写好后,我们就不用再写 gcc 命令了,可以直接用 make 命令
输入make命令:
输入make命令后,它就会在当前的工作路径找 makefile 文件,再根据 makefile 文件中的内容,自动推导自动编译。
此时可执行 code.exe 就生成了
3 依赖关系和依赖方法
我们回到 makefile 文件中的那两行内容
我们将第一行称之为 依赖关系,第二行必须以 tab 键开头,后面的内容我们称之为 依赖方法。
下面举个例子帮大家更好理解依赖关系和依赖方法:
月底了,你没有生活费了,你给你父亲打电话,说:“爸,我是你儿子。”这句话就在和你的父亲表明了依赖关系。意思是爸,我是你儿子,我依赖于你。然后呢?生活费还是没要到,因为现在和你爸表面了依赖关系。这时你又说:“爸,我没生活费了,给我打点钱”。“打钱”就是依赖方法。最后,你父亲听了,很爽快地给你打了钱。
不仅仅是计算机,现实生活中,要做成一件事,都是要具备依赖关系和依赖方法的
。你和你父亲表明了依赖关系和依赖方法,你父亲才给你打钱。为什么你不给你舍友的父亲打电话呢?因为你和你舍友的父亲没有依赖关系。依赖关系错了,事就办不成。“你是我爸,你要帮我考试”,这时依赖关系是对的,但依赖方法错了,事也办不成
makefile 文件中:code.exe 说“我要形成要依赖 code.c ”,可依赖怎么依赖呢?gcc 还是 g++?所以我要用 gcc 方法来编译形成可执行,两个依赖缺一不可
makefile 最基本的语法前文已经初步介绍,这里就不再赘述,大家直接看图即可
4 清理
4.1 清理的基本语法
我们用 make/makefile 来自动化清理 code.exe 文件的格式如下:
4.2 make 的默认执行
makefile文件内容如下:
code.exe:code.cgcc -o code.exe code.c.PHONY:clean
clean:rm -f code.exe
用 make 命令生成可执行和清理:
为什么执行 make 时,默认我们的 make 就是形成 code.exe 的可执行文件,而执行clean 时,需要 make clean
呢。code.exe 是一种目标文件,而 clean 也是一种目标文件,只是它的方法是删除文件而已,他们的本质是一样的。
我们将makefile中的内容颠倒一下
.PHONY:clean
clean:rm -f code.execode.exe:code.cgcc -o code.exe code.c
此时默认就是执行 clean 了
总结:make 扫描 makefile 文件时,从上往下扫描,默认形成第一个目标文件。
所以写makefile时,我们一般喜欢把要形成的可执行程序放在最前面
4.3 为什么要加 『.PHONY:clean』
4.3.1 『.PHONY:clean』的功能
.PHONY:clean
到底是什么东西呢
假设我们将.PHONY:clean
这个符号声明去掉
code.exe:code.cgcc -o code.exe code.cclean:rm -f code.exe
再执行相应指令
发现并不影响 makefile 的具体操作,那为什么还要加上.PHONY:clean
来声明他是一个伪目标呢?
.PHONY
其实是一种建议性的关键字,来建议 clean。它的作用是:表示目标总是被执行
。谁总是被执行?对应的依赖关系和依赖方法总是被执行。
4.3.2 如何理解总是不被执行
那既然有总是被执行,肯定就有总是不被执行。我们先来理解什么叫做总是不被执行?
当前我们的 makefile 总共有两个目标文件:code.exe 和 clean
我们重复执行 code.exe 发现只有第一次可以执行
我们打开 makefile,在 code.exe 前面加上.PHONY
修饰
.PHONY:code.exe
code.exe:code.cgcc -o code.exe code.c.PHONY:clean
clean:rm -f code.exe
此时就可以code.exe就可以总是被执行了
为什么我们的可执行文件一般不建议用 .PHONY 修饰?
道理很简单,因为我们的源代码没有被更新过,没有被更新过就没有必要重新生成一份 code.exe 可执行程序,只有源文件该过了才会再次形成可执行。
比如某个工程一共依赖一千个源文件,现在修改了其中10个,我们只需要将那10个重新编译就可以了,这样可以大大提高效率。
make/makefile默认老代码不重新编译
4.3.2 如何区分文件的新旧
那问题又来了:make 是怎么知道可执行文件和源文件的新旧的呢?
曾经我们提过:文件属性有三个时间:Access
、Modify
、Change
三个时间
我们知道,文件 == 内容 + 属性。对文件的修改,要么修改文件的内容,要么修改文件的属性,要么同时修改
- 只查看文件内容:更新Access时间
- 只修改文件内容:更新Modify时间
- 只修改文件属性:更新Change时间
往往我们修改文件的内容,Modify 时间和 Change 时间都会改变。因为我们往文件中写东西,文件的大小改变了,文件的大小属于文件的属性;而且时间本身就是文件的属性,修改了文件内容 Modify 时间这个属性被修改,Change 时间也会修改
如果只想改变属性呢?可以用chmod
指令只改变文件的属性。
Access时间有点特殊,我们来查看一个文件看看
可以看到,Access时间没有任何变化。为什么呢?
因为对一个文件进行操作,查找的比重是比修改的比重大的多的。可能访问一个文件 100 下,90 都是查找,剩下的才是修改。如果 Linux 中,只要你查了一下文件内容就更新一下时间,这就会带来一个隐性的成本:带来很多的 I/O 操作,因为时间是由操作系统自动形成的,而文件是在磁盘上存的。
因此只有查看文件的次数达到若干次,Linux 系统才会将文件的 Access 时间更新
。具体次数与特定的 Linux 版本有关。
回到开头:make 是怎么知道可执行文件和源文件的新旧的呢?
很简单,只需要比较可执行文件和源文件的Modify时间
即可。如果源文件的 Modify 时间可执行的 Modify 时间晚,说明源文件被更新过。
我们前面 touch 命令,认为他是用来创建文件的,其实 touch 的核心作用是用来修改文件的时间(三个时间统一更新)
所以我们可以将 code.c 文件 touch 一下,再用 make 命令就可以再次编译了,这里就不再演示了。
现在 .PHONY 的作用就很好理解了:
被 .PHONY 修饰的符号,表示它总是被执行。本质是 make 在调用相应的命令时,忽略对比时间,直接执行
5 第二版本makefile
实践中,我们是很少像前面那样写 makefile 的,我们往往是将 .o 文件编译成可执行,而不是直接编译 .c 文件。下面我们来正式学习一下如何使用 makefile
先来看依赖链
现在有了多组依赖关系,如何将.c 变成 .i 呢,.i 如何变成 .s 呢?我们需再加上依赖方法
我们再加上 clean,第二版的 makefile 就写完了
code.exe:code.ogcc code.o -o code.exe
code.o:code.sgcc -c code.s -o code.o
code.s:code.igcc -S code.i -o code.s
code.i:code.cgcc -E code.c -o code.i.PHONY:clean
clean:rm -f *.i *.s *.o *.exe
实际上 makefile 在翻译时,在自己内部会维护一种类似于栈之类的东西。
makefile 文件会被 make 命令从上到下进行扫描。首先根据第一个依赖关系,知道code.exe 依赖 code.o,可 code.o 根本不存在,此时就会将这组依赖关系的依赖方法入栈 gcc code.o -o code.exe
;make 命令再继续向下扫描第二组依赖关系,同理,将他们的依赖方法 gcc -c code.s -o code.o
入栈,之后再将 gcc -S code.i -o code.s
入栈。
最后找到 .i,发现 .i 依赖 .c,当前目录时有 .c 的,此时 make 开始执行对应的依赖方法 gcc -E code.c -o code.i
,再将在栈中的三个依赖方法依次出栈并执行
如此便完成了从上到下扫描,从下到上执行,以形成最终的目标文件。这个过程是 makefile 进行目标文件推导的最基本规则
6 第三版本makefile
实践中,也很少人会写第二版本的 makefile,我们来写个第三版
makefile 允许我们定义变量,我们先写段测试命令来看看变量的用法
执行结果:
我们可以将变量理解成宏
有了变量后,我们就可以将各种具体的符号、指令、文件等都以变量的形式呈现
BIN=code.exe
CC=gcc
SRC=code.c
FLAGS=-o
RM=rm -f$(BIN):$(SRC)$(CC) $(FLAGS) $(BIN) $(SRC).PHONY:clean
clean:$(RM) $(BIN)
上述的写法有点不优雅,我们可以运用 @
和 ^
用make编译时,我们不想回显gcc -o code.exe code.c
命令,我们可以在依赖方法前加 @
,可是这样的话我就不知道编译完成了没,怎么办呢?依赖关系只有一条,可是依赖方法可以有多行,我们可以这样多加一个行依赖方法来进行字符串显示
7 终版makefile
前三个版本,我们都是将单一文件编成可执行,如果有多个文件怎么办呢?
根据编程习惯,我们往往会先将所有的 .c
编译形成 .o
,再将所有的 .o
一起编译形成可执行
那么这样就可以了吗,还差最后一步
上面的写法只是将所有的 .c 编成了.o,可最终编成可执行时,变量 OBJ 还是指定的 code.o,因此此时只有 code.o 单独编成可执行,我们想的是多个 .o 一起链接形成可执行。
首先,在SRC获取源文件上,我们不用自己指定 .c 文件。自动获取路径中的 .c 文件有两种方法
法一:
makefile 是可以直接执行命令行命令的,这种写法的意思是SRC获取 ls 命令在 shell 中的执行结果,即 make 扫描到的文件全部放在了SRC变量中
SRC=$(shell ls)
我们来试验一下
.PHONY:test
test:@echo $(SRC)
这样如果我们再加上通配符就可以获取当前路径中所有的 .c 文件啦
法二:
makefile 中包含类似于函数的东西。wildcard 可以将当前路径下符合条件的文件通配出来
SRC=$(wildcard *.c)
此时我们已经获取了当前路径的所有 .c,那 .o 怎么获取呢?
.o 不就是对应的源文件的 .c 换成 .o 吗?因此 .o 的获取很简单
OBJ=$(SRC:.c=.o)
此时的OBJ表示的是将所有SRC中的 .c 换成 .o
我们来测试一下:
.PHONY:test
test:@echo $(SRC)@echo $(OBJ)
这样,最终版的 makefile 就完成啦,我们还可以顺便加上 clean
大家构建100个源文件来进行测试,输入指令:count=1; while [ $count -le 100 ]; do touch code${count}.c; let count++; done
创建 100 个源文件,再输入 make 指令进行编译即可
输入 make 指令后,发现 make 自动将所有的 .c 编译成 .o 再将所有的 .o 一起链接成可执行文件。大家可以自己回去试一试。
好啦,本期关于 make/makefile 的知识就介绍到这里啦,希望本期博客能对你有所帮助。同时,如果有错误的地方请多多指正,让我们在 Linux 的学习路上一起进步!