Makefile简介
Makefile是什么?
gcc hello.c -o hello
gcc aa.c bb.c cc.c dd.c …
一个工程中的源文件不计其数,其按类型、功能、模块分别放在若干个目录中,makefile定义了一系列的规则来指定哪些文件需要先编译,哪些文件需要后编译,哪些文件需要重新编译,甚至于进行更复杂的功能操作,因为 makefile就像一个Shell脚本一样,也可以执行操作系统的命令。
make和Makefile是什么关系?
make工具:找出修改过的文件,根据依赖关系,找出受影响的相关文件,最后安装规则单独编译这些文件。
Makefile文件:记录依赖关系和编译规则。
必须要学精Makefile吗?
项目(Uboot、Kernel、…)和底层机制(编译、链接、库加载)通过Makelife进行项目管理,学精Makelife很有必要。
怎么学习Makefile?
Makefile的本质:无论多么复杂的语法,都是为了更好地解决项目文件之间的依赖关系。
Makefile三要素
目标、依赖、命令。
描述三要素的关系
目标:依赖的文件或者是其他的目标
命令1
命令2
…
实验演示
.PHONY:可以指定伪目标
引入Makefile管理项目
首先打开Linux命令行,我们先创建一个目录,用来存放项目,我这里用的目录名是 study
,创建好后并进入目录:
$ sudo mkdir study
$ cd study
在引入Makefile之前,我们先编写一个小项目,随后再引入Makefile进行管理项目。例如在目录下,我们使用 vi
编辑器创建一个 game.c
文件并写入以下内容:
$ sudo vi game.c
并写入以下内容后保存退出:
#include<stdio.h>
void play()
{
printf("Play a game!!!\n\r");
}
void stop()
{
printf(("Stop playing game!!!\r\n");
}
保存退出后,再使用 vi
编辑器创建一个 main.c
文件,用来编写主函数:
$ sudo vi main.c
并写入以下内容后保存退出:
#include<stdio.h>
int main()
{
play();
stop();
return 0;
}
使用 gcc
编译这两个文件:
$ sudo gcc main.c game.c -o play //用gcc编译生成可执行程序的命令
//-o选项指定可执行程序的名字为play
编译成功后,我们使用 ls
命令查看目录,可以发现目录下多了一个名为 play 的可执行文件。
./play
执行这个可执行程序,成功看到以下内容:
$ ./play
Play a game!!!
Stop playing game!!!
至此一个简单的小项目就编写好了,现在我们开始引入Makefile进行管理项目。首先创建Makefile并打开文件:
$ sudo vi Makefile
然后编写以下内容:
play:main.c game.c //创建目标play,依赖于main.c文件和game.c文件
gcc main.c game.c -o play //指定对应目标执行命令,我们同样使用刚刚用gcc编译生成可执行程序的命令
.PHONY:clean //定义伪目标,包含目标了clean
clean: //创建伪目标clean
rm play //指定目标执行命令,删除应用程序play
保存退出后我们来到命令行,删除刚刚我们生成的 play 可执行文件,然后执行 make
命令:
$ sudo rm play
$ sudo make
编译成功后,我们使用 ls
命令查看目录,同样可以发现目录下多了名为 play 的可执行文件。
./play
执行这个可执行程序,同样可以看到以下内容:
$ ./play
Play a game!!!
Stop playing game!!!
到这里我们已经成功引入了Makefile对我们的小项目进行了管理,但是呢现在我们的Makefile还有比较多的问题。比如我们每次编译应用程序 play ,都需要重新编译 main.c 和 play.c ,浪费了许多编译的时间,具体原理我们可以参阅GCC编译器基础。
现在我们打开Makefile文件来进行一些修改:
$ sudo vi Makefile
play:main.o game.o
gcc main.o game.o -o play
main.o:main.c
gcc -c main.c -o main.o
play.o:play.c
gcc-c play.c -o play.c
.PHONY:clean
clean:
rm play
经过改造过后,我们的应用程序 play 不再直接依赖于 game.c,main.c 文件,而是依赖于 game.o,main.o 文件。也就是说如果我们修改了 main.c 文件, game.o 文件是不受影响的,也就不需要重新编译 game.c 文件,只需要编译 main.c 文件就可以了。那么在编译 play 应用程序的时候,我们不用再去编译game.c,main.c 文件,而是直接编译 game.o,main.o 文件。
保存退出后,我们先执行 make clean
命令删除可执行文件 play ,然后执行 make
命令:
$ sudo make clean
rm play
$ sudo make
gcc main.o game.o -o play
编译成功后,我们使用 ls
命令查看目录,可以看到生成了 game.o,main.o 和 play 可执行文件三个文件:
./play
执行这个可执行程序,同样成功看到以下内容:
$ ./play
Play a game!!!
Stop playing game!!!
现在我们的Makefile文件已经达到了预期的目的,通过它可以解决这些文件的依赖性问题,从而优化编译的时间。但是现在Makefile只能针对此项目来用,要是我们想对大多数项目使用Makefile文件,就需要把Makefile文件改造成能通用的文件。
Makefile的变量、模式匹配
变量
系统变量
在当前目录下创建一个 Makefile_text 文件进行测试,并编辑以下内容保存退出:
$ sudo vim Makefile_text
.PHONY:all //定义尾目标all
all: //目标all
echo "$(CC)" //打印系统变量CC,CC一般指代编译器
echo "$(AS)" //打印系统变量AS,AS一般指代汇编器
echo "$(MAKE)" //打印系统变量MAKE,MAKE一般指代MAKE工具
像CC
,AS
,MAKE
这样的系统变量有很多,用的比较少,只需要记住一些常用的就可以了,现在我们来对新编辑的文件使用一下make工具,可以看到:
$ sudo make -f Makefile_text //-f 指定Make工具找到文件Makefile_text
echo "cc" //否则Make工具会默认使用原来的makefile文件
cc
echo "as"
as
echo "make"
make
自定义变量
=
,延时赋值
:=
,立即赋值
?=
,空赋值
+=
,追加赋值
我们先来看看延时赋值是什么吧,接着打开刚创建的 Makefile_text 文件并编辑以下内容保存退出:
$ sudo vim Makefile_text
A=123
B=$(A)
A=456
.PHONY:all
all:
echo "$(B)"
再对新更改的文件使用make工具,可以看到:
$ sudo make -f Makefile_text
echo "456"
456
然后我们再来看一下立即赋值是什么:
$ sudo vim Makefile_text
A=123
B:=$(A)
A=456
.PHONY:all
all:
echo "$(B)"
使用make工具,可以看到:
$ sudo make -f Makefile_text
echo "123"
123
可以看到延时赋值和立即赋值是相反的,要记住它,接下来我们再看一下空赋值:
$ sudo vim Makefile_text
A?=123
A?=456
.PHONY:all
all:
echo "$(A)"
使用make工具,可以看到:
$ sudo make -f Makefile_text
echo "123"
123
空赋值就是只有当前变量的值是空的时,赋值才有效,最后我们来看追加赋值:
$ sudo vim Makefile_text
A?=123
#B:=$(A)
A+=456
.PHONY:all
all:
echo "$(A)"
使用make工具,可以看到:
$ sudo make -f Makefile_text
echo "123 456"
123 456
在给变量追加赋值时,追加赋值不会覆盖掉原来的值,而是在原来的值后面加上新的值
自动化变量
$<
:第一个依赖文件
$^
:全部的依赖文件
$@
:目标
同样我们先来看$<
,打开 Makefile_text 文件编辑以下内容并保存退出:
$ sudo vim Makefile_text
all:targeta targetb
echo "$<"
targeta:
targetb:
使用make工具,可以看到:
sudo make -f Makefile_text
echo "targeta"
targeta
$<
就代表第一个依赖文件,我们再来看看$^
:
$ sudo vim Makefile_text
all:targeta targetb
echo "$^"
targeta:
targetb:
使用make工具,可以看到:
sudo make -f Makefile_text
echo "targeta targetb"
targeta targetb
$^
就表示全部的依赖文件,最后我们来看$@
:
$ sudo vim Makefile_text
all:targeta targetb
echo "$@"
targeta:
targetb:
使用make工具,可以看到:
sudo make -f Makefile_text
echo "all"
all
$@
就表示当前目标
接下来我们就可以修改优化一下我们前面的小项目了,我们打开文件Makefile并修改为以下内容并保存退出:
$ sudo vim Makefile
CC=gcc
TARGET=play
OBJS=main.o game.o
$(TARGET):$(OBJS)
$(CC) $^ -o $@
main.o:main.c
$(CC) -c main.c -o main.o
play.o:play.c
$(CC) -c play.c -o play.c
.PHONY:clean
clean:
rm play
我们执行 sudo rm play
命令删除可执行文件 play ,然后执行 make
命令:
$ sudo rm play
$ sudo make
gcc main.o game.o -o play
./play
执行这个可执行程序,同样成功看到以下内容,说明我们优化成功,大大提高了可移植性:
$ ./play
Play a game!!!
Stop playing game!!!
模式匹配
%
:匹配任意多个非空字符
shell:*通配符
我们继续打开 Makefile_text 文件并修改为以下内容并保存退出:
$ sudo vim Makefile_text
%:
echo "$@"
使用make工具,可以看到:
$ sudo make -f Makefile_text hsne
echo "hsne"
hsne
可以发现无论你输入啥就打印啥,%
的作用就是匹配任意多个非空字符
接下来我们又可以继续改造我们的小项目了,我们打开文件Makefile并修改为以下内容并保存退出:
$ sudo vim Makefile
CC=gcc
TARGET=play
OBJS=main.o game.o
$(TARGET):$(OBJS)
$(CC) $^ -o $@
%.o:%.c
$(CC) -c $(<) -o $(@)
.PHONY:clean
clean:
rm play
我们先执行 make clean
命令删除可执行文件 play ,然后执行 make
命令:
$ sudo make clean
rm play
$ sudo make
gcc main.o game.o -o play
./play
执行这个可执行程序,同样成功看到以下内容,说明我们又改造成功了,又提高了可移植性:
$ ./play
Play a game!!!
Stop playing game!!!
默认规则
.o文件默认使用.c文件来进行编译
因此不需要模式匹配也同样可以,例如我们继续打开 Makefile 文件,将模式匹配的内容删除:
$ sudo vim Makefile
CC=gcc
TARGET=play
OBJS=main.o game.o
$(TARGET):$(OBJS)
$(CC) $^ -o $@
.PHONY:clean
clean:
rm play
我们执行 make clean
命令删除可执行文件 play ,然后执行 make
命令:
$ sudo make clean
rm play
$ sudo make
gcc main.o game.o -o play
可以发现也是可以正常编译的, ./play
执行这个可执行程序,同样是可以正常执行的:
$ ./play
Play a game!!!
Stop playing game!!!