Makefile学习

目录

一、Makefile的介绍

1.1 什么是Makefile

1.2 Makefile的规则与示例

二、Makefile的基本语法

2.1 通配符

2.2 假想目标

2.3 变量

 三、Makefile的常用函数

3.1 字符串替换和分析函数

3.1.1 subst

 3.1.2 patsubst

3.1.3 strip

3.1.4 findstring

3.1.5 filter/filterout

 3.1.6 sort

3.2 文件名函数

3.2.1 dir/notdir

 3.2.2 suffix 

 3.2.3 basename

 3.2.4 addsuffix/addprefix

 3.2.5 wildcard

3.3 其他函数

3.3.1 foreach

 3.3.2 if

3.3.3 origin

3.3.4 shell 


1.1 什么是Makefile

        相信在Linux系统中经常会用到make这个命令来编译程序,而执行make命令所依赖的文件便是Makefile文件,make命令通过Makefile文件编写的内容对程序进行编译。make命令根据文件更新的时间戳来决定哪些文件需要重新编译这可以避免编译已经编译过的,没有改变的文件,从而提升编译效率。

1.2 Makefile的规则与示例

        一个简单的 Makefile 文件包含一系列的“规则”,其样式如下:

目标(target)…: 依赖()…
<tab>命令(command)

当“依赖文件”比“目标文件”更加新时,或者目标文件还没有生成时,就会执行“命令”

下面就是一个简单的Makefile文件:

hello: hello.c
    gcc -o hello hello.c
clean:
    rm -f hello

在代码所在的文件夹下放入此Makefile文件,并执行make操作,便会生成目标文件hello,如下

所示:

hello.c内容为:

#include <stdio.h>

int main()
{
    printf("hello world\n");
    return 0;

}

以上述为例,介绍一下详细规则:

        目标(target)通常是要生成的文件的名称,可以是可执行文件(比如上例中的hello就是要生成的可执行文件名)或OBJ 文件, 也可以是一个执行的动作名称(如上述例子中的clean)。
        依赖(prerequiries)是用来产生目标的材料(比如源文件 ) ,一个目标经常有几个依赖。
        命令(command)是生成目标时执行的动作,一个规则可以含有几个命令,每个命令占一 行。
        每个命令行前面必须是一个Tab字符,即命令行第一个字符是Tab。通常当一个依赖发生了变化,规则就会调用命令产生新的目标。但是并非所有目标都有依赖,像上述例子中的“clean”,它只负责清除文件。
        每一个Makefile文件也可以包含规则外的其他文件。
        对于上面的Makefile,执行"make"后仅当hello.c文件比hello文件新时,才会执行cc -o hello hello.c,从而生成hello文件,如果没有hello文件时,这个命令也会被执行。运行“make clean”时,由于clean目标没有依赖,它的命令“rm -f hello”就会被强制执行。

二、Makefile的基本语法

2.1 通配符

        当一个目标文件所依赖的依赖文件有很多时,需要写很多条规则,因此可以使用通配符只需要写一行来代替多行的规则。

        首先举一个没有使用通配符的例子:共有两个.c文件,分别为a.c,b.c

a.c:

#include <stdio.h>

int main()
{
	func_b();
	return 0;
}

b.c:

#include <stdio.h>

void func_b()
{
	printf("This is B\n");
}

Makefile:

test:a.o b.o
	gcc -o test a.o b.o
	
a.o : a.c
	gcc -c -o a.o a.c

b.o : b.c
	gcc -c -o b.o b.c

上述例子中的目标文件“test”有两个依赖“a.o”,“b.o”,不难看出,生成a.o,b.o的规则有很多相似之处,因此可以使用通配符来改进Makefile。改进后的Makefile如下:

test:a.o b.o
    gcc -o test $^
    
%.o : %.c
    gcc -c -o $@ $<

其中用到的通配符的解释:

%.o:表示所有.o文件

%.c:表示所有.c文件

$@:表示目标

$<:表示第一个依赖文件

$^:表示所有依赖文件

2.2 假想目标

当我们需要清除文件时,可以在Makefile中添加如下代码:

test:a.o b.o
    gcc -o test $^
    
%.o : %.c
    gcc -c -o $@ $<
clean:
    rm *.o test

在shell中输入make clean即可清除文件:

上面的写法其实存在问题,当前目录下没有clean文件时,使用make clean即可清除文件,但是当当前目录中存在clean文件时,再执行make clean便会出现下面的问题:

出现上面提示的原因是,一个规则能够被执行需要满足两个条件中的一个:

(1)目标文件不存在

(2)依赖文件比目标文件新

这里,clean没有依赖文件,并且clean文件在当前目录已经存在,因此不会执行make clean的操作。为了解决这个问题,需要用到关键字PHONY,其具体用法如下:

test:a.o b.o
    gcc -o test $^
    
%.o : %.c
    gcc -c -o $@ $<
.PHONY : clean # 把clean定义为假象目标。他就不会判断名为“clean”的文件是否存在
clean:
    rm *.o test

将clean定义为假想目标,便可执行在当前目录已经存在clean文件的情况下执行make clean操作:

2.3 变量

在Makefile中有两种变量:

(1)即时变量(简单变量)

        A:=XXX,对于即时变量使用 “:=” 表示,它的值在定义的时候已经被确定了

(2)延时变量()

        B=XXX,对于延时变量使用“=”表示,它只有在使用到的时候才确定,在定义时并没有

确定下来。
        想使用变量的时候使用“$” 来引用,如果不想看到命令是,可以在命令的前面加上 "@" 符号,就不会显示命令本身。当我们执行make 命令的时候, make 这个指令本身,会把整个 Makefile 读进去,进行全部分析,然后解析里面的变量。常用的变量的定义如下:
: = # 即时变量
= # 延时变量
? = # 延时变量 , 如果是第 1 次定义才起效 , 如果在前面该变量已定义则忽略这句
\+ = # 附加 , 它是即时变量还是延时变量取决于前面的定义
? = # 如果这个变量在前面已经被定义了,这句话就会不会起效果

例子:

A := $(C) #即时变量
b = $(C) #延时变量
C = ABC
C +=123
D = hello
D ?= hello world # 延时变量,如果是第一次定义才有效,如果在前面该变量已经定义则忽略这句
E := $(C)
all:
    @echo A = $(A) #@表示不打印echo
    @echo b = $(b)
    @echo D = $(D)
    @echo E = $(E)

 运行结果:

分析:

A = ,由于A被定义为即时变量,在开始执行的时候C的值为空,所以A的值也为空。

 b = ABC 123,b被定义为延时变量,执行make时,会解析Makefile中所用的变量,先执行C = ABC,再执行C +=123,最终 C = ABC 123,因此b = ABC 123。

D = hello,D ?= hello world,执行时会判断前面是否已经定义D,在本例中前面已经定义D = hello了,因此D ?=  hello world便没有执行,依然输出D = hello

E =ABC 123,这里E依然为即时变量,但是与A不同的是,执行到这里,C已经被赋值为ABC 123,因此E = $(C) =ABC 123。 

 三、Makefile的常用函数

        函数调用的格式为:

$(function arguments)

        这里function是函数名字,arguments是函数的参数。参数和函数名之 间是用空格或 Tab 隔开,如果有多个参数,它们之间用逗号隔开。这些空格和逗号不是参数值的一部分。接下来介绍一些常用的函数。

3.1 字符串替换和分析函数

3.1.1 subst
$(subst from,to,text)

在文本“text”中将“from”用“to”替换

例:

A = $(subst c,o,a.c b.c)
all:
    @echo subst  = $(A)

运行结果,将a.c b.c 中的c字符替换为o字符: 

 3.1.2 patsubst
$(patsubst pattern ,replacement,text)

 将“text”中符合pattern的格式的字符用“replacement”替换,“pattern”和“replacement”可以使用通配符。

例:

files = a.c b.c c.c abc
dep_files1 = $(patsubst %.c,%.o,$(files))
all:
    @echo dep_files1 = $(dep_files1)

 运行结果:将以.c结尾的字符替换为.o

3.1.3 strip
$(strip string)

去掉前导和结尾空格,并将中间的多个空格压缩为单个空格。

例:

dep = $(strip a  d   e  f )
all:
    @echo dep = $(dep)

 运行结果,将a d  e f ,中多余的空格去除:

3.1.4 findstring

$(findstring find,in)

在字符串“in”中搜寻“find” ,若找到则返回“find”,否则返回空

例:

dep1 = $(findstring a,a b c)
dep2 = $(findstring a,b c)
all:
    @echo $(dep1)
    @echo $(dep2)

 运行结果,dep1 为a,dep为空:

3.1.5 filter/filterout
$(filter pattern ,text)

 在text中取出符合patten格式的值

$(filter-out pattern...,text)

 在text中取出不符合patten格式的值

例:

# filter函数,$(filter pattern...,text)  在text中取出符合patten格式的值
# filter-out函数,$(filter-out pattern...,text)  在text中取出不符合patten格式的值
A = a b c d/
B = $(filter %/,$(A))
C = $(filter-out %/,$(A))
all:
    @echo B = $(B)
    @echo C = $(C)

运行结果,B为以/结尾的d/,C为出去/d的其他字符:

 3.1.6 sort
$(sort list)

将“list”中的字符按字母顺序排序,并去除重复的字,输出由单个空格隔开的字的列表

例:

dep = $(sort appel cat bat egg)
all:
    @echo $(dep)

运行结果:


 

3.2 文件名函数

3.2.1 dir/notdir
$(dir names...)

抽取“names”中的每一个文件名的路径部分。文件名的路径部分包括从文件名的首字符到最后一个斜杠之前的一切字符。

$(notdir names...)

 抽取‘names...’中每一个文件名中除路径部分外一切字符(真正的文件名)。

例:

dep1 = $(dir src/hello.c hello.c)
dep2 = $(notdir src/hello.c hello.c)
all:
    @echo dep1 = $(dep1)
    @echo dep2 = $(dep2)

 运行结果:

 3.2.2 suffix 
$(suffix names...)

抽取“names”中每一个文件名的后缀

 例:

dep  = $(suffix src/foo.c src-1.0/bar.c hacks.o)
all:
    @echo dep = $(dep)

运行结果:

 3.2.3 basename
$(basename names...)

 抽取‘names...’中每一个文件名中除后缀外一切字符。

例:

dep = $(basename src/foo.c src-1.0/bar hacks.o)
all:
    @echo dep = $(dep)

 运行结果:

 3.2.4 addsuffix/addprefix
$(addsuffix suffix,names...)
参数‘ names... ’是一系列的文件名,文件名之间用空格隔开; suffix 是 一个后缀名。将 suffix( 后缀 ) 的值附加在每一个独立文件名的后面,完成后将 文件名串联起来,它们之间用单个空格隔开。
例:
dep = $(addsuffix .c,cat dog)
all:
    @echo $(dep)

运行结果:

$(addprefix prefix,names...)
参数‘ names ’是一系列的文件名,文件名之间用空格隔开; prefix 是一个前缀名。将 preffix( 前缀 ) 的值附加在每一个独立文件名的前面,完成后将文件名串联起来,它们之间用单个空格隔开。

 例:

dep = $(addprefix src/,cat dog)
all:
    @echo $(dep)

运行结果:

 

 3.2.5 wildcard
$(wildcard pattern)
参数‘ pattern ’是一个文件名格式,包含有通配符 ( 通配符和 shell 中用法一样) 。函数 wildcard 的结果是一列和格式匹配的且真实存在的文件的名称,文件名之间用一个空格隔开
例:比如当前路径下有a.c b.c c.c d.o,执行以下代码
#$(wildcard pattern)  pattern定义了文件名的格式, wildcard会在当前目录下取出其中存在的文件。
files = $(wildcard *.c)
all:
    @echo files = $(files)

运行结果:

3.3 其他函数

3.3.1 foreach

$(foreach var ,list,text)

 将list中遍历的每一个字都赋值给var,然后text引用变量var对其中保存的字进行扩展,因此text每次扩展都不相同。

例:

dirs = a b c d
files = $(foreach dir,$(dirs),$(wildcard $(dir)/*))

all:
    @echo files = $(files)

该例中files保存的是当前路径下的a/,b/,c/,d/目录下所有的文件列表,a/,b/,c/,d/路径下的文件如下图所示:

 执行Makefile后显示内容为:

 3.3.2 if

$(if condition,then-part[,else-part])
首先把第一个参数‘ condition ’的前导空格、结尾空格去掉,然后扩展。 如果扩展为非空字符串,则条件‘condition ’为‘真’;如果扩展为空字符串,则条件‘condition ’为‘假’。
如果条件‘ condition ’为‘真’ , 那么计算第二个参数‘ then-part ’的值,并将该值作为整个函数 if 的值。
如果条件‘ condition ’为‘假’ , 并且第三个参数存在,则计算第三个参数‘else-part ’的值,并将该值作为整个函数 if 的值;如果第三个参数不存在,函数 if 将什么也不计算,返回空值。

 例:

# SUBDIR = $(B)
SUBDIR := $(B)
B = a/
sub_dir = $(if $(SUBDIR),$(SUBDIR),/home/src)
all:
    @echo sub_dir  = $(sub_dir)

当SUBDIR为即时变量时,SUBDIR没有被即时赋值,因此该变量保存的内容为空,经过if语句判断后sub_dir输出的内容应该为/home/src

当SUBDIR为延时变量时,SUBDIR被即时赋值为a/,经过if语句判断后sub_dir输出的内容应该为a/:

3.3.3 origin

$(origin variable)
变量‘ variable ’是一个查询变量的名称,不是对该变量的引用。所以,不能采用‘$ ’和圆括号的格式书写该变量,当然,如果需要使用非常量的文件名,可以在文件名中使用变量引用。
函数 origin 的结果是一个字符串,该字符串变量是这样定义的:
例:
a = a/
ifdef a

ifeq ("$(origin a)", "file")

dir := $(a)

endif

endif

all:

    @echo  $(origin a)

    @echo  $(dir)

如果a变量被定义,且a变量是在Makefile中被定义的,则dir = a,结果输出file和a/

当a变量是在命令行中被定义时,输出为command line 和空:

 

3.3.4 shell 

$(shell command arguments)
函数 shell make 与外部环境的通讯工具。函数 shell 的执行结果和在控制台上执行‘command arguments ’的结果相似。不过如果‘ command arguments ’的结果含有换行符(和回车符),则在函数 shell 的返回结果中将把它们处理为单个空格,若返回结果最后是换行符(和回车符)则被去掉。
例:
c_src := $(shell ls)
all:
    @echo c_src = $(c_src)

显示当前目录下所有的文件