Makefile基础教程 2

一、实验介绍--Makefile 基础规则(1)

上次实验介绍了对不同源代码文件进行编译、链接生成可执行文件的基本过程,有了这些前导知识作为基础之后,

就可以开始学习makefile的基础规则了。

首先,我们已经知道makefile作为工程管理文件可以提供工程下各个源代码的编译、链接规则。

GNU make 工具可以读入makefile并解析其中的规则,并自动对工程进行编译链接,提高项目开发的效率。

那么,makefile到底如何实现对工程编译、链接的管理呢?本实验将通过介绍makefile的基础规则来回答这个问题。

1.1 实验内容

1.makefile基本规则。

2.makefile时间戳检验测试。

  1. 验证makefile依赖文件的执行顺序。
  2. 变量,PHONY和“-”功能测试。5.makefile文件命名规则。
  3. 编写一段程序的makefile文件。

1.2 实验知识点

1.makefile的基本编译规则。

2.make更新目标的依据:时间戳。

3.makefile目标依赖的执行顺序为从左至右。

4.makefile变量的赋值与使用。

  1. .PHONY的作用:声明伪目标。
  2. “-”的作用:让make忽略该命令的错误。
  3. make搜寻makefile的命名规则:"GNUmakefile" > "makefile" > "Makefile"

1.3 实验环境

Ubuntu系统, GNU gcc工具,GNU make工具

1.4 适合人群

本课程难度为简单,属于入门级别课程,适合有代码编写能力的用户,熟悉和掌握make的一般用法。

1.5 代码获取

可以通过以下命令获取代码:

$ git clone https://github.com/darmac/make_example.git

二、实验原理

依据 makefile 的基本规则设计相应的正反示例,验证规则。

三、开发准备

进入实验楼课程即可。

四、项目文件结构

main.c:主要的 C 语言源代码。

makefile:make工程文件。

五、实验步骤

5.1 makefile 基本规则。

5.1.1 抓取源代码

使用如下 cmd 获取 GitHub 源代码:

cd ~/Code/
git clone https://github.com/darmac/make_example.git
cd make_example/chapter1
5.1.2 编写 main.c 源文件

实验中将用“hello world!”程序来验证makefile的基本规则,因此先编写一段小程序main.c

源代码中已有main.c文件,代码如下:

#include <stdio.h>

int main(void)
{
    printf("hello world!\n");
    return 0;
}
5.1.3 熟悉makefile的基础规则

makefile 是为了自动管理编译、链接流程而存在的。

makefile 的基本书写规则如下:

TARGET... : PREREQUISITES...
COMMAND

TARGET:规则目标,可以为文件名或动作名

PREREQUISITES:规则依赖

COMMAND:命令行,必须以[TAB]开始,由shell执行

5.1.4 编写简单的makefile文件管理main.c的编译

源代码中已有makefile文件,内容如下:

  1 #this is a makefile example
  2 
  3 main:main.o
  4     gcc -o main main.o
  5 
  6 main.o:main.c
  7     gcc -c main.c

line1:“#”为注释符号,后面接注释文本。

line3 - line4:声明目标 main 的依赖文件 main.o 及链接 command。

line6 - line7:声明目标 main.o 的编译command。

5.1.5 测试make命令

make工具的基本使用方法为:make TARGET

在终端输入命令:

make main.o

可以看见shell会执行:

gcc -c main.c

接下来输入:

make main

可以看见shell执行:

gcc -o main main.o

执行main文件:

./main

终端会打印:

hello world!

说明程序正常执行。

5.1.6 自动化编译目标

清除掉main.omain文件:

rm main.o main

由于我们的最终的目标是main文件,实际上我们并不关心中间目标“main.o”。

现在尝试只运行一次make编译出我们需要的最终目标。

make main

终端会打印出 make 实际执行的命令:

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

可见make还是先生成makefilemain的依赖文件main.o,再链接生成main文件。

5.1.7 让make自动寻找目标

再次清除掉main.omain文件:

rm main.o main

并执行 make,但不输入目标:

make

终端打印出 make 的执行命令还是一样:

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

这是因为默认情况下,make 会以第一条规则作为其“终极目标”。

现在我们尝试修改makefile,在目标“main”之前再增加一条规则:

dft_test:middle_file
    mv middle_file dft_test
middle_file:
    touch middle_file

执行:

make

可以看见终端印出:

touch middle_file
mv middle_file dft_test

当前文件夹下会多出一个dft_test文件。

实验过程如下图所示:

5.1A

5.1B

5.1C

5.2makefile时间戳检验测试。

5.2.1 文件的时间戳检测规则

make在执行命令时会检测依赖文件的时间戳:

  1. 若依赖文件不存在或者依赖文件的时间戳比目标文件新,则执行依赖文件对应的命令。
  2. 若依赖文件的时间戳比目标文件老,则忽略依赖文件对应的命令。
5.2.2 文件时间戳测试

还原makefile文件,并打上v1.0补丁:

git checkout makefile
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

终端会输出:

gcc -c main.c
touch testa
touch testb
gcc -o main main.o

make 会分别生成main.otestatestb这三个中间文件。这验证了5.2.1中说明的第一条特性。

现在删除testb文件,再看看make会如何执行:

rm testb
make

终端打印:

touch testb
gcc -o main main.o

可见make分别执行了testbmain两条规则,main.otesta规则对应的命令没有被执行到。

这验证了 5.2.1 中说明的第二条特性。

实验过程如下图所示:

此处输入图片的描述

5.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

清除上次编译过程中产生的中间文件:

rm main.o testa testb

执行 make:

make

终端有如下打印:

touch testb
touch testa
gcc -c main.c
gcc -o main main.o

可见make的确是按照从左到右的规则分别执行依赖文件对应的命令。

5.4 变量,PHONY和“-”功能测试。

5.4.1makefile的变量定义

makefile 也可以使用变量,它类似于 C 语言中的宏定义。

变量可以直接使用“vari=string”的写法来定义,并以“$(vari)”格式来使用。

我们用变量来定义目标的依赖项,使makefile保持良好的扩展性。

5.4.2 在makefile中添加变量并使用

先还原makefile文件到v1.0补丁,并清除上一次编译的中间文件。

git checkout makefile
patch -p2 < v1.0.patch
rm main.o testa testb

在目标“main”之前定义一个变量“depen”

depen=main.o testa testb

修改 main 目标的依赖项声明:

main:$(depen)

执行 make :

make

终端打印:

gcc -c main.c
touch testa
touch testb
gcc -o main main.o

可见makefile还是能够正常执行。

之后main目标的依赖项有变化时,只需修改“depen”变量即可。

5.4.3 为makefile添加clean规则

每次测试makefile的时候我们都要清除中间文件,为了使得编译工程更加自动化,我们在makefile中添加规则让其自动清除。

makefile中修改depen变量,增加clean依赖:

depen=clean main.o testa testb

增加clean规则及其命令:


clean:
    rm main.o testa testb

当前目录下是存在main.o testa testb三个中间文件的,执行make看看效果:

make

可以看见终端打印:

rm main.o testa testb
gcc -c main.c
touch testa
touch testb
gcc -o main main.o

说明现在make会先清除掉上次编译的中间文件并重建。

5.4.4 让clean规则也使用变量

makefile 中定义了depen变量来声明各个依赖项。

但新增的clean规则没有使用这个变量,这会让makefile的维护产生麻烦:当依赖项变更的时候需要同时修改depen变量和clean规则。

因此,我们让clean规则的rm命令也使用depen变量。

修改clean规则下的rm命令行:

rm $(depen)

再次执行make猜猜会发生什么?

make

终端打印:

rm clean main.o testa testb
rm: cannot remove 'clean': No such file or directory
make: *** [clean] Error 1

原来是因为depen变量指明clean为依赖项,因此rm命令也会试图删除clean文件时出现错误。

而 make 在执行命令行过程中出现错误后会退出执行。

5.4.5 让clean命令出错后make也能继续执行

rm 某个不存在的文件是很常见的错误,在大部分情况下我们也不将其真正作为错误来看待。

如何让make忽略这个错误呢?

我们需要用到“-”符号。

“-”:让make忽略该指令的错误。

修改makefile中的clean规则:

clean:
    -rm $(depen)

再次执行make

make

终端打印:

rm clean main.o testa testb
rm: cannot remove 'clean': No such file or directory
rm: cannot remove 'main.o': No such file or directory
rm: cannot remove 'testa': No such file or directory
rm: cannot remove 'testb': No such file or directory
makefile:18: recipe for target 'clean' failed
make: [clean] Error 1 (ignored)
gcc -c main.c
touch testa
touch testb
gcc -o main main.o

看起来效果不错,虽然 rm 指令报出错误,make 却依然可以生成我们的最终目标:main 文件。

5.4.6 使用伪目标

前面提到makefile依赖文件的时间戳若比目标文件旧,则对应规则的命令不会执行。

我们现在定义了一个clean规则,但如果文件夹下正好有一个clean文件会发生什么样的冲突呢?

先在当前目录下新建一个clean文件:

touch clean

再执行make命令:

make

终端打印:

gcc -o main main.o

看来由于clean文件已经存在,make不会再执行clean目标对应的规则了。

但实际上clean是一个伪目标,我们不期望它会与真正clean文件有任何关联。

此时需要使用“.PHONY”来声明伪目标。

修改makefile在变量depen之前加入一条伪目标声明:

.PHONY: clean

执行 make:

make

终端打印:

rm clean main.o testa testb
gcc -c main.c
touch testa
touch testb
gcc -o main main.o

makefile 又能得到正常执行了,所有流程都符合我们的预期。

现在减除掉依赖项testa testb,因为实际上main文件并不需要用到这两个文件。

修改makefiledepen变量:

depen=clean main.o

执行 make:

make

终端打印:

rm clean main.o
rm: cannot remove 'clean': No such file or directory
makefile:20: recipe for target 'clean' failed
make: [clean] Error 1 (ignored)
gcc -c main.c
gcc -o main main.o

我们已经可以随心所欲的定制main文件的依赖规则了。

实验过程如下图所示:

5.4A

5.4B

5.4C

5.4D

5.4E

5.4F

5.4G

5.5makefile文件命名规则。

5.5.1make默认调用的文件名

迄今为止,我们写的自动编译规则都放在makefile中,通过实验也可以明确了解到make工具会自动调用makefile文件。

是否文件名必须命名为“makefile”呢?

不是的,GNU make 会按默认的优先级查找当前文件夹的文件,查找的优先级为:

“GNUmakefile”> “makefile”> “Makefile”

5.5.2 测试make调用的文件优先级

新建GNUmakefile文件,添加以下内容:

#this is a GNUmakefile

.PHONY: all

all:
        @echo "this is GNUmakefile"

新建 Makefile 文件,添加以下内容:

#this is a Makefile

.PHONY: all

all:
        @echo "this is Makefile"

查看以下当前目录文件,现在应该有三个 makefile 能够识别到的文件。

ls *file* -hl

终端打印:

-rw-r--r-- 1 root root  71 Jun 25 12:22 GNUmakefile
-rw-r--r-- 1 root root 192 Jun 25 09:18 makefile
-rw-r--r-- 1 root root  65 Jun 25 12:23 Makefile

执行一次make看看哪个文件被调用:

make

终端打印:

this is GNUmakefile

说明make调用的是GNUmakefile

删除GNUmakefile再执行一次make

rm GNUmakefile
make

终端打印:

rm clean main.o
rm: cannot remove 'clean': No such file or directory
makefile:20: recipe for target 'clean' failed
make: [clean] Error 1 (ignored)
gcc -c main.c
gcc -o main main.o

说明make调用的是makefile

删除makefile,执行 make

rm makefile
make

终端打印:

this is Makefile

说明Makefile属于三者中优先级最的文件。

建议:推荐以makefile或者Makefile进行命名,而不使用GNUmakefile,因为GNUmakefile只能被GNUmake工具识别到。

实验过程如下图所示:

5.5

5.6 编写一段程序的makefile文件。

5.6.1 小型计算程序说明

现在我们已经掌握了makefile的基本规则,可以尝试自己写一个makefile进行工程管理。

make_example/chapter0目录下有一段简单的计算器示例程序,现在要为它建立一个makefile文件。

切换到chapter0目录,查看目录下的文件:

cd ../chapter0
ls

终端打印:

add_minus.c  add_minus.h main.c  multi_div.c  multi_div.h  readme.md  v1.0.patch  v2.0.patch  v3.0.patch

简单介绍一下程序的需求:

1.add_minus.c要求被编译成静态链接库libadd_minus.a

2.multi_div.c要求被编译成动态链接库libmulti_div.so

3.main.c是主要的源文件,会调用上述两个代码文件中的API,main.c要求被编译为main.o

4.将main.o libadd_minus.a libmulti_div.so链接成可执行文件 main。

  1. 每次编译前要清除上次编译时产生的文件。

打上补丁 v3.0 并增加库文件路径,export环境变量LD_LIBRARY_PATH为当前路径:

patch -p2 < v3.0.patch
export LD_LIBRARY_PATH=$PWD
5.6.2makefile文件示例

请参照 5.6.1 的要求完成makefile文件,完成后可参考文件makefile_for_chapter0的内容:

# this is a chapter0 makefile
.PHONY:all clean depen

depen=clean main.o add_minus.o libadd_minus.a libmulti_div.so

all:$(depen)
    gcc -o main main.o -L./ -ladd_minus -lmulti_div

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

add_minus.o:
    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)

实验过程如下图所示:

5.6

六、实验总结

本实验测试了makefile的基础规则和一些简单的特性。

七、课后习题

请自行设计一段包含多个源文件的小型工程,并使用makefile进行管理。

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值