Makefile

声明: 本篇博客的学习途径主要为以下网站和课堂讲解,发博客目的仅为学习使用,在该博客的基础上做了一定程序的简略和修改。
参考博客 :
原文链接:http://c.biancheng.net/makefile/

【注意】Makefile文件创建的时候,就叫Makefile,再shell中调用make命令,让Makefile文件自动执行。

什么是Makefile

Makefile 可以使得我们的项目工程的编译变得自动化,不需要每次都手动输入一堆源文件和参数,主要应用于Linux环境


【常见问题】
1)编译的时候需要链接库的的问题。拿C语言来说,编译的时候 gcc 只会默认链接一些基本的C语言标准库,很多源文件依赖的标准库都需要我们手动链接。

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

2)编译大的工程会花费很长的时间。
修改工程项目的源文件,每次修改后都要去重新编译

一个大的工程项目可不止有几个的源文件,里面的源文件个数可能有成百上千个。例如一个内核,或者是一个软件的源码包


【Makefile解决方法】:

  • 把要链接的库文件放在 Makefile 中,制定相应的规则和对应的链接顺序

这样只需要执行 make 命令,工程就会自动编译。每次想要编译工程的时候就执行 make ,省略掉手动编译中的参数选项和命令,非常的方便。

  • Makefile 支持多线程并发操作

极大的缩短我们的编译时间,并且当我们修改了源文件之后,编译整个工程的时候,make 命令只会编译我们修改过的文件,没有修改的文件不用重新编译


Makefile的内容

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

Makefile的显式规则

规则主要是两个部分组成,分别是

  • 依赖的关系
  • 执行的命令
targets : prerequisites
    command

或者

targets : prerequisites; command
    command
  • targets目标文件(结果),可以是 Object File(一般称它为中间文件),也可以是可执行文件,还可以是一个标签;
  • prerequisites:是我们的依赖文件,要生成 targets 需要的文件或者是目标。可以是多个,也可以是没有;
  • command:make 需要执行的命令(任意的 shell 命令)。可以有多条命令,每一条命令占一行。

注意:我们的目标和依赖文件之间要使用冒号分隔开,命令的开始一定要使用Tab键。


【举例】

test:test.c
    gcc -o test test.c
  • 其中 test 是的目标文件,也是我们的最终生成的可执行文件。
  • 依赖文件就是 test.c 源文件
  • 重建目标文件需要执行的操作是gcc -o test test.c

使用 Makefile 的方式:首先需要编写好 Makefile 文件,然后在 shell 中执行 make 命令,程序就会自动执行,得到最终的目标文件。


Makefile的工作流程

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
	
.PHONY:clean    #.PHONY后面的target表示的也是一个伪造的target, 而不是真实存在的文件target,
clean:
    rm -rf *.o test #清除中间文件.o

在这里插入图片描述
它的具体工作顺序是:当在 shell 提示符下输入 make 命令以后。 make 读取当前目录下的 Makefile 文件,并将 Makefile 文件中的第一个目标作为其执行的“终极目标”,开始处理第一个规则(终极目标所在的规则)。

在我们的例子中,第一个规则就是目标 “main” 所在的规则。规则描述了 “main” 的依赖关系,并定义了链接 “.o” 文件生成目标 “main” 的命令;
make 在执行这个规则所定义的命令之前,首先处理目标 “main” 的所有的依赖文件(例子中的那些 “.o” 文件)的更新规则(以这些 “.o” 文件为目标的规则)。

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

  • 目标 “.o” 文件不存在,使用其描述规则创建它;
  • 目标 “.o” 文件存在,
    • 目标 “.o” 文件所依赖的 “.c” 源文件 “.h” 文件中的任何一个比目标 “.o” 文件“更新”(在上一次 make 之后被修改)。则根据规则重新编译生成它;
    • 目标 “.o” 文件比它的任何一个依赖文件(".c" 源文件、".h" 文件)“更新”(它的依赖文件在上一次 make 之后没有被修改),则什么也不做。

【ps:红色的为清理掉的.o文件,记得清理中间文件】


Makefile变量的定义和使用

变 量 的 定 义 变量的定义
Makefile 文件中定义变量的基本语法如下:

变量的名称=值列表

调用变量的时候可以用 “$(VALUE_LIST)” 或者是 “${VALUE_LIST}” 来替换,这就是变量的引用。实例:

OBJ=main.o test.o test1.o test2.o
mytest:$(OBJ)
      gcc -o mytest $(OBJ)

这里用OBJ代替了main.o test.o test1.o test2.o,简化书写


变 量 的 赋 值 − − − S h e l l 变量的赋值 ---Shell Shell
知道了如何定义,下面我们来说一下 Makefile 的变量的四种基本赋值方式:

  • 简单赋值 ( := ) 编程语言中常规理解的赋值方式,只对当前语句的变量有效
  • 递归赋值 ( = ) 赋值语句可能影响多个变量,所有目标变量相关的其他变量都受影响。
  • 条件赋值 ( ?= ) 如果变量未定义,则使用符号中的值定义变量。如果该变量已经赋值,则该赋值语句无效。
  • 追加赋值 ( += ) 原变量用空格隔开的方式追加一个新值

简 单 赋 值 简单赋值

x:=foo
y:=$(x)b
x:=new
mytest:
	@echo "y=>$(y)"
	@echo "x=>$(x)"

在 shell 命令行执行make test我们会看到:

y=>foob
x=>new

递 归 赋 值 递归赋值

x=foo
y=$(x)b
x=new
test:
      @echo "y=>$(y)"
      @echo "x=>$(x)"

在 shell 命令行执行make test我们会看到:

y=>newb
x=>new

条 件 赋 值 条件赋值

x:=foo
y:=$(x)b
x?=new
test:
      @echo "y=>$(y)"
      @echo "x=>$(x)"

在 shell 命令行执行make test我们会看到:

y=>foob
x=>foo

追 加 赋 值 追加赋值

x:=foo
y:=$(x)b
x+=$(y)
test:
      @echo "y=>$(y)"
      @echo "x=>$(x)"

在 shell 命令行执行make test我们会看到:

y=>foob
x=>foo foob

Makefile自动化变量 : $@ $^ $< $?

自动化变量说明
$@表示目标文件。如果目标是一个静态的库文件(.a),那么它代表这个文档的文件名;如果在多目标模式规则中,它代表的是触发规则被执行的文件名
$%当目标文件是一个静态库文件时,代表静态库的一个成员名。
$^表示所有的依赖文件如果目标是静态库文件,它所代表的只能是所有的库成员(.o 文件)名。
$<表示第一个依赖文件如果是一个目标文件使用隐含的规则来重建,则它代表由隐含规则加入的第一个依赖文件。
$?表示比目标还要新的依赖文件列表如果目标文件时静态库文件,代表的是库文件(.o 文件)。

【实例1】

test:test.o test1.o test2.o
         gcc -o $@ $^
test.o:test.c test.h
         gcc -o $@ $<
test1.o:test1.c test1.h
         gcc -o $@ $<
test2.o:test2.c test2.h
         gcc -o $@ $<

“$@” 代表的是目标文件test,“$^”代表的是依赖的文件,“$<”代表的是依赖文件中的第一个。
我们在执行 make 的时候,make 会自动识别命令中的自动化变量,并自动实现自动化变量中的值的替换,这个类似于编译C语言文件的时候的预处理的作用。


【实例2】

lib:test.o test1.o test2.o
    ar r $?

假如我们要做一个库文件,库文件的制作依赖于这三个文件。
当修改了其中的某个依赖文件,在命令行执行 make 命令,库文件 “lib” 就会自动更新。"$?" 表示修改的文件。

ar 此命令列出了 lib.a 库的目录


Makefile与Shell 通配符(* ? [ ])的使用

Makefile 是可以使用 shell 命令的

通配符使用说明
*匹配0个或者是任意个字符
匹配任意一个字符
[]我们可以指定匹配的字符放在 “[]” 中
Makefile自动化变量:
$@  表示目标文件
$^  表示所有的依赖文件
$<  表示第一个依赖文件
$?  表示比目标还要新的依赖文件列表
mytest:*.c
    gcc -o $@ $^

【注意】不能通过引用变量的方式来使用通配符
【经典错误】

OBJ=*.c
test:$(OBJ)
    gcc -o $@ $^

实例中我们相要表示的是当前目录下所有的 “.c” 文件,但是我们在使用的时候并没有展开,而是直接识别成了一个文件。文件名是 “*.c”。
【正确写法】使用一个函数 “wildcard”,这个函数在我们引用变量的时候,会帮我们展开

OBJ=$(wildcard *.c)
test:$(OBJ)
    gcc -o $@ $^

Makefile目标文件搜索(VPATH和vpath)

【问题】一个工程文件中的源文件有很多,并且存放的位置可能不相同(工程中的文件会被放到不同的目录下),所以按照之前的方式去编写 Makefile 会有问题。
【解决】 Makefile 中为我们提供的目录搜索文件的功能

  • 一般搜索VPATH
  • 选择搜索vpath

【区别】

  • VPATH 是变量,更具体的说是环境变量,Makefile 中的一种特殊变量,使用时需要指定文件的路径
  • vpath 是关键字,按照模式搜索,也可以说成是选择搜索。搜索的时候不仅需要加上文件的路径,还需要加上相应限制的条件

V P A T H 的 使 用 VPATH的使用 VPATH使

VPATH := src

【理解】把 src 的值赋值给变量 VPATH,所以在执行 make 的时候会从 src 目录下找我们需要的文件。

  • 当存在多个路径的时候我们可以这样写:
VPATH := src car
或
VPATH := src:car

拿上面的例子来说,我们应该先搜索 src 目录下的文件,再搜索 car 目录下的文件。

【注意】多个路径之间要使用空格或者是冒号隔开,表示在多个路径下搜索文件。搜索的顺序为我们书写时的顺序

无论你定义了多少路径,make 执行的时候会先搜索当前路径下的文件,当前目录下没有我们要找的文件,才去 VPATH 的路径中去寻找


【实例】

VPATH=src car
test:test.o
    gcc -o $@ $^

【解释】假设 test.c 文件没有在当前的目录而在当前文件的子目录 “src” 或者是 “car” 下,程序执行是没有问题的,但是生成的 test 的文件没有在定义的子目录文件中而是在当前的目录下


v p a t h 的 使 用 vpath的使用 vpath使
关键字搜索 vpath ,这种搜索方式一般被称作选择性搜索。

VPATH 是搜索路径下所有的文件,而 vpath 更像是添加了限制条件,会过滤出一部分再去寻找。

1) vpath PATTERN DIRECTORIES 
2) vpath PATTERN
3) vpath
  • PATTERN:可以理解为要寻找的条件
  • DIRECTORIES:寻找的路径

用 法 一 用法一

vpath test.c src   # 单路径
vpath test.c src car   #多路径       
或者是       
vpath test.c src : car

多路径的用法其实和 VPATH 差不多,都是使用空格或者是冒号分隔开,搜索路径的顺序是先 src 目录,然后是 car 目录。


用 法 二 用法二

vpath test.c

用法二的意思是清除符合文件 test.c 的搜索目录。


用 法 三 用法三

vpath

vpath 单独使的意思是清除所有已被设置的文件搜索路径。


路 径 搜 索 实 例 路径搜索实例
【原实例】

main:main.o list1.o list2.o
    gcc -o $@ $<
main.o:main.c
    gcc -o $@ $^
list1.o:list1.c list1.h
    gcc -o $@ $<
list2.o:list2.c list2.h
    gcc -o $@ $<
结果:
make:*** No rule to make target 'main.c',need by 'main.o'. stop.

错误原因:重建目标main.o 文件的时候,发现没有找到指定的 main.c 文件,这是错误的根本原因

【解决方法一】 开头加上VPATH=src include

VPATH=src include
main:main.o list1.o list2.o
    gcc -o $@ $<
main.o:main.c
    gcc -o $@ $^
list1.o:list1.c list1.h
    gcc -o $@ $<
list2.o:list2.c list2.h
    gcc -o $@ $<
我们使用 vpath 的话同

【解决方法二】 开头加上

vpath %.c src
vpath %.h include
vpath %.c src
vpath %.h include
main:main.o list1.o list2.o
    gcc -o $@ $<
main.o:main.c
    gcc -o $@ $^
list1.o:list1.c list1.h
    gcc -o $@ $<
list2.o:list2.c list2.h
    gcc -o $@ $<

在使用 vpath 的时候,搜索的条件中可以包含模式字符“%”,这个符号的作用是匹配一个或者是多个字符,例如“%.c”表示搜索路径下所有的 .c 结尾的文件。如果搜索条件中没有包含“%" ,那么搜索的文件就是具体的文件名称。

Makefile隐含规则

test:test.o
    gcc -o test test.o
test.o:test.c

等价于

test:test.o
    gcc -o test test.o

等价于

test:test.o
    gcc -o test test.o
test.o:test.c
    gcc -o test.o test.c

前两个都是使用了隐含规则,简化了Makefile的编写,隐含规则这块先略过,回头再写

注意:隐含条件只能省略中间目标文件重建的命令和规则,但是最终目标的命令和规则不能省略。

Makefile条件判断 - ifeq、ifneq、ifdef和ifndef

【引入】

日常使用 Makefile 编译文件时,可能会遇到需要分条件执行的情况,比如在一个工程文件中,可编译的源文件很多,但是它们的类型是不相同的,所以编译文件使用的编译器也是不同的。手动编译去操作文件显然是不可行的(每个文件编译时需要注意的事项很多),所以 make 为我们提供了条件判断来解决这样的问题。

【注意】:条件语句只能用于控制 make 实际执行的 Makefile 文件部分,不能控制规则的 shell 命令执行的过程。

关键字功能
ifeq判断参数是否相等,相等为 true,不相等为 false。
ifneq判断参数是否不相等,不相等为 true,相等为 false。
ifdef判断是否有值,有值为 true,没有值为 false。
ifndef判断是否有值,没有值为 true,有值为 false。

i f e q 和 i f n e q ifeq 和 ifneq ifeqifneq

ifeq (ARG1, ARG2)
ifeq 'ARG1' 'ARG2'
ifeq "ARG1" "ARG2"
ifeq "ARG1" 'ARG2'
ifeq 'ARG1' "ARG2"

【例】

libs_for_gcc= -lgnu
normal_libs=
foo:$(objects)
ifeq($(CC),gcc)
    $(CC) -o foo $(objects) $(libs_for_gcc)
else
    $(CC) -o foo $(objects) $(noemal_libs)
endif
libs_for_gcc= -lgnu
normal_libs=
ifeq($(CC),gcc)
    libs=$(libs_for_gcc)
else
    libs=$(normal_libs)
endif
foo:$(objects)
    $(CC) -o foo $(objects) $(libs)

i f d e f 和 i f n d e f ifdef 和 ifndef ifdefifndef
它的主要功能是判断变量的值是不是为空

ifdef VARIABLE

【例】

bar =
foo = $(bar)
all:
ifdef foo
    @echo yes
else
    @echo  no
endif
foo=
all:
ifdef foo
    @echo yes
else
    @echo  no
endif

通过两个实例对比说明:通过打印 “yes” 或 “no” 来演示执行的结果。我们执行 make 可以看到实例 1打印的结果是 “yes” ,实例 2打印的结果是 “no” 。
其原因就是在实例 1 中,变量“foo”的定义是“foo = $(bar)”。虽然变量“bar”的值为空,但是“ifdef”的判断结果为真,这种方式判断显然是有不行的,

因此当我们需要判断一个变量的值是否为空的时候需要使用“ifeq" 而不是“ifdef”。


Makefile伪目标

伪目标:它并不会创建目标文件,只是想去执行这个目标下面的命令。

伪目标的存在可以帮助我们找到命令并执行。

伪 目 标 避 免 文 件 冲 突 伪目标避免文件冲突

【作用】:

  • 避免我们的 Makefile 中定义的只执行的命令的目标和工作目录下的实际文件出现名字冲突
  • 提高执行 make 时的效率,特别是对于一个大型的工程来说,

【对比】:如果没有伪目标

clean:
    rm -rf *.o test

如果目录下不存在clean文件:
规则中 rm 命令不是创建文件 clean 的命令,而是执行删除任务,删除当前目录下的所有的 .o 结尾和文件名为 test 的文件。当工作目录下不存在以 clean 命令的文件时,在 shell 中输入 make clean 命令,命令 rm -rf *.o test 总会被执行 ,这也是我们期望的结果。
如果目录下存在clean文件:
当我们在 shell 中执行命令 make clean,由于这个规则没有依赖文件,所以目标被认为是最新的而不去执行规则所定义的命令。因此命令 rm 将不会被执行。

【改进】为了规避这种情况,所以将目标 clean 声明为伪目标

.PHONY:clean

这样 clean 就被声明成一个伪目标,无论当前目录下是否存在 clean 这个文件,当我们执行 make clean 后 rm 都会被执行。
而且当一个目标被声明为伪目标之后,make 在执行此规则时不会去试图去查找隐含的关系去创建它

完整写法:

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

伪 目 标 与 递 归 伪目标与递归
伪目标的另一种使用的场合是在 make 的并行和递归执行的过程中,此情况下一般会存在一个变量,定义为所有需要 make 的子目录。

对多个目录进行 make 的实现,可以在一个规则的命令行中使用 shell 循环来完成。如下:
【对比】如果没有伪目标

SUBDIRS=foo bar baz
subdirs:
    for dir in $(SUBDIRS);do $(MAKE) -C $$dir;done

代码表达的意思是当前目录下存在三个子文件目录,每个子目录文件都有相对应的 Makefile 文件
代码中实现的部分 是用当前目录下的 Makefile 控制其它子模块中的 Makefile 的运行
【出现问题】“

  • 当子目录执行 make 出现错误时,make 不会退出。就是说,在对某个目录执行 make 失败以后,会继续对其他的目录进行 make。
  • 另外一个问题就是使用这种 shell 循环方式时,没有用到 make 对目录的并行处理功能由于规则的命令时一条完整的 shell 命令,不能被并行处理。

【改进】有了伪目标之后,我们可以用它来克服以上方式所存在的两个问题

SUBDIRS=foo bar baz
.PHONY:subdirs $(SUBDIRS)
subdirs:$(SUBDIRS)
$(SUBDIRS):
    $(MAKE) -C $@
foo:baz  #规定编译顺序

上面的实例中有一个没有命令行的规则“foo:baz”,这个规则是用来规定三个子目录的编译顺序
因为在规则中 “baz” 的子目录被当作成了 “foo” 的依赖文件,所以 “baz” 要比 “foo” 子目录更先执行,最后执行 “bar” 子目录的编译。


伪 目 标 生 成 多 文 件 伪目标生成多文件
如果在一个文件里想要同时生成多个可执行文件,我们可以借助伪目标来实现。
(实际上去掉.PHONY以后也可以生成多个可执行文件,目前还在捣鼓中,这里暂略)

.PHONY:all
all:test1 test2 test3
test1:test1.o
    gcc -o $@ $^
test2:test2.o
    gcc -o $@ $^
test3:test3.o
    gcc -o $@ $^
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值