上次实验介绍了对不同源代码文件进行编译、链接生成可执行文件的基本过程,有了这些前导知识作为基础之后, 就可以开始学习makefile
的基础规则了。
首先,我们已经知道makefile
作为工程管理文件可以提供工程下各个源代码的编译、链接规则。 GNU make
工具可以读入makefile
并解析其中的规则,并自动对工程进行编译链接,提高项目开发的效率。 那么,makefile
到底如何实现对工程编译、链接的管理呢?本实验将通过介绍makefile
的基础规则来回答这个问题。
知识点
Makefile
的基础规则与命名方式Makefile
更新目标文件的依据makefile
时间戳检验测试Makefile
依赖执行顺序Makefile
变量的使用- 「.PHONY」与「-」的作用与使用方法
- 编写一段程序的
makefile
文件。
代码获取
通过在 Terminal 中输入以下命令可以将本课程所涉及到的所有源代码下载到在线环境中,作为参照对比进行学习。
wget http://labfile.oss.aliyuncs.com/courses/849/make_example-master.zip && unzip make_example-master.zip && rm make_example-master.zip
本章节的源代码位于 /home/project/make_example-master/chapter1
目录下,请在 Terminal 中通过 cd 命令切换至该目录后再进行实验学习。
项目涉及到的代码文件:
main.c
:C 语言主程序代码makefile
:Makefile 源代码
1️⃣ Makefile 的基础规则与命名方式
❗ 1.1 makefile 的基础规则
编写 main.c
文件,在本次实验中将用简单的 hello world!
程序来验证 makefile 的基本规则。
#include <stdio.h>
int main(void) {
printf("hello world!\n");
return 0;
}
因为 makefile 是为了自动管理编译、链接流程而存在的,因此 makefile 的书写需要遵照一定的书写规则。
TARGET... : PREREQUISITES...
COMMAND
TARGET
:规则目标,可以是一个 object file (目标文件),也可以是一个执行文件,还可以是一个标签(label)。PREREQUISITES
:要生成那个 target 所需要的文件或是目标,即规则依赖。COMMAND
:也就是 make 需要执行的命令,必须以 [TAB] 开始,由 shell 执行。
1.2 编写makefile
文件管理main.c
的编译
编写一个简单的 makefile 文件用来管理 main.c
文件的编译。
#this is a makefile example
main: main.o
gcc -o main main.o
main.o: main.c
gcc -c main.c
line1:“#”为注释符号,后面接注释文本。
line3 – line4:声明目标 main 的依赖文件 main.o 及链接 command。
line6 – line7:声明目标 main.o 的编译command。
1.3 测试make命令
make
工具的基本使用方法为:make TARGET
。 在终端输入命令:
make main.o
Terminal 的输出结果如图所示:
说明 shell 执行了命令 gcc -c main.c
。
接下来输入以下命令:
make main
Terminal 的输出结果如图所示:
说明 shell 执行了命令 gcc -o main main.o
。
最后在 Terminal 中输入以下命令:
./main
说明程序正常执行。
1.4 自动化编译目标
清除掉main.o
和main
文件:
rm main.o main
由于我们的最终的目标是 main
文件,实际上我们并不关心中间目标文件 main.o
。现在尝试只运行一次 make
编译出我们需要的最终目标。
make main
可以看出 make
还是先生成 makefile 中 main
的依赖文件 main.o
,再链接生成 main
文件。
1.5 make
自动寻找目标
让 make
自动寻找目标,即在 make
命令后不添加任何参数直接运行。
首先删除之前产生的 main
和 main.o
文件。
rm main main.o
然后在 Terminal 中输入命令:
make
Terminal 的输出内容跟之前的输出内容相同:
❗❗❗ 这是因为默认情况下,
现在我们尝试修改 makefile,在目标 main
之前再增加一条规则:
dft_test:middle_file
mv middle_file dft_test
middle_file:
touch middle_file
注意:在将这段代码复制到 WebIDE 中时需要观察一下此时 tab 的默认格式是 space:4 表示使用 4 个空格代替 制表符。
这样的话在执行 make 命令会出现以下错误:
Makefile:2 *** missing separator 停止
解决办法是依次点击 Space:4->Indent Using Tabs->4 Configured Tab Size 然后删除
Command
那一行的缩进,重新使用 tab 缩进,保存之后重新执行make
命令,问题将得以解决。
执行命令:make
说明规则得到了正确的执行,同时在当前文件夹下会产生一个新的文件 dft_test。
创建文件middle_file,改名dft_test
❗ 1.6 makefile 的命名规则
目前为止,我们写的自动编译规则都放在 makefile 中,通过实验也可证明了 make
工具会自动调用 makefile 文件。 是否文件名必须命名为「makefile」呢? 然而并不是,GNU make 会按默认的优先级查找当前文件夹的文件,查找的优先级为: 「GNUmakefile」> 「makefile」> 「Makefile」
。
接下来对 makefile 名称的优先级进行测试。
新建 GNUmakefile 文件,添加以下内容:
#this is a GNUMakefile
.PHONY: all
all:
@echo "this is GNUMakefile"
新建 Makefile 文件,添加以下内容:
#this is a Makefile
.PHONY: all
all:
@echo "this is Makefile"
查看一下当前目录文件,现在应该有三个 make
能够识别到的文件。
ls *file* -hl
- ls 显示文件信息
-h
,–human-readable
以容易理解的格式列出文件大小 (例如 1K 234M 2G)
-l
除了文件名之外,还将文件的权限、所有者、文件大小等信息详细列出来。
ls *file* -hl
表示 输出* file * (中间有file的文件)详细信息
Terminal 输出结果如图:
执行 make
命令,查看 Makefile 的执行情况。
make
Terminal 的输出结果如图:
说明 make
调用的是 GNUmakefile。 删除 GNUmakefile 再执行一次 make
命令。
输出结果如图所示:
说明 make
调用的是 makefile。 删除 makefile 再执行一次 make
命令。
输出结果如图所示:
说明 Makefile 是三者中优先级最低。
然而在创建 makefile 文件时推荐以 makefile 或者 Makefile 进行命名,而不使用 GNUmakefile ,因为 GNUmakefile 只能被 GNU 的 make 工具识别到。
2️⃣ Makefile 之时间戳
make
命令在执行时会自动检测依赖文件的时间戳,具体规则如下:
- 若依赖文件不存在或者依赖文件的时间戳比目标文件新,则执行依赖文件对应的命令。
- 若依赖文件的时间戳比目标文件老,则忽略依赖文件对应的命令。
接下来对 makefile 时间戳
规则进行验证。
将 makefile 文件内容还原,还原后的内容应该与下面给出的代码一致:
main: main.o
gcc -o main main.o
main.o: main.c
gcc -c main.c
为 makefile 打上 v1.0.patch
补丁,增加一些新的内容。
patch -p2 < v1.0.patch
此时 makefile 的内容如下:
#this is a makefile example
main:main.o testa testb
gcc -o main main.o
main.o:main.c
gcc -c main.c
testa:
touch testa
testb:
touch testb
清除可能存在的中间文件。
rm main.o testa testb
执行 make
命令。
make
Terminal 输出内容如下:
make
命令执行后分别生成 main.o
、testa
、testb
这三个中间文件。这验证了时间戳规则中的第一条。
现在删除 testb
文件,再观察 make
会如何执行。
make
Terminal 输出结果如图:
可见 make
分别执行了 testb
和 main
两条规则,main.o
和 testa
规则对应的命令没有被执行。这验证了 makefile 时间戳规则中的第二条。
3️⃣ Makefile 依赖执行顺序
从上述实验可以看出make
目标文件的依赖文件是按照需要从左到右
的顺序生成的。 对应规则“main”:
main:main.o testa testb
gcc -o main main.o
make
按照顺序分别执行 main.o
、testa
、testb
所对应的规则。
现在我们调换 main.o
、testa
、testb
的顺序。 修改 makefile 文件的 main
规则的依赖顺序:
main: testb testa main.o
输出结果如图:
可见 make
的确是按照从左到右的规则分别执行依赖文件对应的命令。
4️⃣ 变量、「.PHONY」与「-」
makefile 也可以使用变量,它类似于 C 语言中的宏定义。 变量可以直接使用··「vari=string」
的写法来定义,并以「$(vari)」
格式来使用。 我们用变量来定义目标的依赖项,使 makefile 保持良好的扩展性。
4.1 在 makefile 中定义并使用变量
将 makefile 还原至以下内容:
main: main.o
gcc -o main main.o
main.o: main.c
gcc -c main.c
为 makefile 打上v1.0.patch
补丁,并移除旧的中间文件。
patch -p2 < v1.0.patch
rm main.o testa testb
在目标 main
之前定义一个变 depen
:
#this is a makefile example
depen=main.o testa testb
main:main.o testa testb
gcc -o main main.o
main.o:main.c
gcc -c main.c
testa:
touch testa
testb:
touch testb
修改 main
的依赖声明。
main:$(depen)
执行 make
命令,并观察输出结果。
输出结果如图:
可以看出 makefile 依然能够正常执行。 就算之后 main
目标的依赖项有变化时,也只需修改 depen
变量的值即可。
4.2 为makefile
添加clean
规则
每次测试makefile
的时候我们都要清除中间文件,为了使得编译工程更加自动化,我们在makefile
中添加规则让其自动清除。
在makefile
中修改depen
变量,增加clean
依赖:
depen=clean main.o testa testb
增加 clean
规则及其命令:
clean:
rm main.o testa testb
此时 makefile 的内容如下:
#this is a makefile example
depen=clean main.o testa testb
main:$(depen)
gcc -o main main.o
main.o:main.c
gcc -c main.c
testa:
touch testa
testb:
touch testb
clean:
rm main.o testa testb
当前目录下是存在main.o testa testb
三个中间文件的,执行make
看看效果:
说明现在make
会先清除掉上次编译的中间文件并重建。
4.3 clean
规则中也使用 depen
变量。
makefile 中定义了depen
变量来声明各个依赖项。但新增的clean
规则没有使用这个变量,这会让makefile
的维护产生麻烦:当依赖项变更的时候需要同时修改depen
变量和clean
规则。 因此,我们让clean
规则的rm
命令也使用depen
变量。
修改clean
规则下的rm
命令行:
clean:
rm $(depen)
然而当我们执行了 make
命令之后,发现问题产生了。
原来是因为 depen
变量指明 clean
为依赖项,因此执行 rm
命令时也会试图删除 clean
文件,这时就会出现错误。 而 make
在执行命令行过程中出现错误后会退出执行。
4.4 「-」的使用
- 让
clean
命令出错后make
也能继续执行
rm 某个不存在的文件是很常见的错误,在大部分情况下我们也不将其真正作为错误来看待。 如何让make
忽略这个错误呢? 我们需要用到“-”符号。 “-”:让make
忽略该指令的错误。 修改makefile
中的clean
规则:
现在修改 makefile 中的 clean 规则:
clean:
-rm $(depen)
执行 make
命令并观察输出结果:
从输出结果来看,虽然 rm
指令报出错误,make
却依然可以生成 main
文件。
4.5 伪目标的使用
前面提到 makefile 依赖文件的时间戳若比目标文件旧,则对应规则的命令不会执行。 我们现在定义了一个 clean
规则,但如果文件夹下正好有一个 clean
文件会发生什么样的冲突呢? 我们先在当前目录下先新建一个 clean 文件:
touch clean
执行 make
命令,并观察输出结果。
发现 clean
规则没有得到执行,因为 clean
文件已经存在。然而 clean
实际上是一个伪目标,我们并不期望它会与真正 clean
文件有任何关联。 因此需要使用 .PHONY
来声明伪目标。
接下来修改 makefile 在变量 depen
之前加入一条伪目标声明。
.PHONY: clean
执行 make
命令并观察输出结果。
可以看到 makefile 又能得到正常执行了,并且所有流程都符合我们的预期。
现在删除掉 main
依赖项 testa
和 testb
因为在生成 main
文件过程中并不需要用到这两个文件。 修改 makefile 的 depen
变量:
depen=clean main.o
执行 make
命令并观察输出结果。
5️⃣ 编写一段程序的 makefile 文件
现在我们已经掌握了makefile
的基本规则,可以尝试自己写一个makefile
进行工程管理。
在make_example/chapter0
目录下有一段简单的计算器示例程序,现在要为它建立一个makefile
文件。 切换到chapter0
目录,查看目录下的文件:
简单介绍一下程序的需求:
add_minus.c
要求被编译成静态链接库libadd_minus.a
。multi_div.c
要求被编译成动态链接库libmulti_div.so
。main.c
是主要的源文件,会调用上述两个代码文件中的API,main.c
要求被编译为main.o
。将main.o libadd_minus.a libmulti_div.so
链接成可执行文件 main。- 每次编译前要清除上次编译时产生的文件。
打上补丁 v3.0 并增加库文件路径,export
环境变量LD_LIBRARY_PATH
为当前路径:
patch -p2 < v3.0.patch
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:当前文件路径
这时候的main.c文件如下;
#include <stdio.h>
#include "add_minus.h"
#include "multi_div.h"
int main(void) {
int rst;
printf("Hello Cacu!\n");
rst = add(3,2);
printf("3 + 2 = %d\n",rst);
rst = minus(3,2);
printf("3 - 2 = %d\n",rst);
rst = multi(3,2);
printf("3 * 2 = %d\n",rst);
rst = div(6,2);
printf("6 / 2 = %d\n",rst);
return 0;
}
makefile
文件示例
makefile_for_chapter0
的内容:
# this is a chapter0 makefile
.PHONY:main clean depen
depen=clean main.o add_minus.o libadd_minus.a libmulti_div.so
main:$(depen)
-gcc -o main main.o -L./ -ladd_minus -lmulti_div
main.o:main.c
gcc -c main.c
add_minus.o:add_minus.c
gcc -c add_minus.c
libadd_minus.a:add_minus.o
ar rc libadd_minus.a add_minus.o
libmulti_div.so:
gcc multi_div.c -fPIC -shared -o libmulti_div.so
clean:
-rm $(depen)
最后运行结果如下图:
今天的文章2. Makefile 基础规则分享到此就结束了,感谢您的阅读。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/17515.html