码迷,mamicode.com
首页 > 其他好文 > 详细

Makefile脚本快速入门

时间:2015-08-05 14:49:57      阅读:221      评论:0      收藏:0      [点我收藏+]

标签:makefile

一、读懂Makefile

1、Makefile简介

简单来说,Makefile就是帮助我们编译工程并生成可执行文件。现在的IDE基本上都做了Makefile的事情,并不需要我们自己去手动编写Makefile,但是在Linux环境下,对于一个大型工程,我们常常要定制自己的编译规则,要编译哪些文件,该如何编译,通过Makefile就可以搞定。有了Makefile,只需要执行一个简单的make命令,即可实现自动化编译,当然有时候执行make命令之前还需要做一些配置工作,比如在android环境下开发时,首先要执行“envsetup.sh”并lunch一个SDK版本,这个需要根据实际情况而定。

在Linux下通过gcc进行编译时,包括四个阶段:预编译、编译、汇编和链接,分别生成.i、.s、.o和可执行文件,前三个阶段可归结为一个过程,即编译过程以生成中间文件.o,链接过程有两种情况,静态链接和动态链接,也就是所谓的库文件,静态库后缀为.a,动态库后缀为.so。了解gcc的编译过程,有助于理解Makefile,下面先来看一个简单的例子。

2、第一个Makefile

以c代码为例,这里有四个文件,分别如下。
main.c——调用circle.h头文件声明的length()和area()函数

#include "circle.h" 
#include "stdio.h" 

int main(void) 
{ 
    printf("length = %lf\n", length(5)); 
    printf("area = %lf\n", area(5)); 

    return 0; 
}

circle.h——声明length()和area()函数

#ifndef CIRCLE_H 
#define CIRCLE_H 

double length(int r); 
double area(int r); 

#endif // CIRCLE_H

circle.c——实现length()和area()函数

#include "circle.h" 
#include "def.h" 

double length(int r) 
{ 
    return 2 * PI * r; 
} 

double area(int r) 
{ 
    return PI * r * r; 
}

def.h——圆周率PI的定义

#ifndef DEF_H 
#define DEF_H 

#define PI 3.1415926 

#endif // DEF_H

使用gcc编译以上四个文件,假设目标文件为main,命令如下:

$gcc -o main main.c circle.c

当工程中有n个文件时,还是像上面的命令一样在gcc后面跟着一大堆文件吗,这样就太笨拙了。即便如此,如果只是某个文件修改了一点代码,也要编译整个工程吗,想想就不必这样,有时候我们还要单独编译某个功能模块,这时候就该Makefile发挥作用了。编译上面的四个文件,可使用如下Makefile。
Makefile——

main: main.o circle.o 
    gcc -o main main.o circle.o 

main.o: main.c circle.h 
    gcc -c main.c 

circle.o: circle.c circle.h def.h 
    gcc -c circle.c 

clean: 
    rm main main.o circle.o

上面的Makefile是一个非常简单的文件,通过这个文件,只需要执行命令“make”就可以生成目标文件main,执行命令“make clean”就可以删除目标文件main和一些.o中间文件,当工程庞大时Makefile的好处就显现出来了,其神奇功效如下:

(1)如果这个工程没有编译过,那么所有的c文件都要被编译和链接。
(2)如果这个工程的某几个c文件被修改了,那么只编译被修改的这几个c文件并链接目标程序。
(3)如果这个工程的某几个头文件被修改了,那么只编译引用了这几个头文件的c文件并链接目标程序。

从上面的Makefile可以看出其一般格式如下:

target: prerequisites 
    commands
# or
target: prerequisites; commands

target是目标文件,后面跟着一个冒号,prerequisites是生成target所需要的文件,当文件较多时,可使用续行符号“\”进行续行,另起一行commands是make执行的shell命令,有一点必须注意,commands前要留一个Tab键的距离。如果它们在同一行的话commands前以分号进行分隔。Makefile支持通配符,例如“*”,用法同bash。

3、Makefile工作过程

(1)一般情况下,当文件名为“Makefile”或“makefile”时,也可以是“GNUmakefile”,只需要输入命令“make”即可,但有时候我们要起一个一目了然的文件名,以清楚地知道这个文件是干什么用的,这时执行make命令时就要使用“-f”参数了,以指定具体的文件,例如“make -f main.mk”。

(2)在Makefile中可以引用外部的Makefile,语法是“include filename”,类似于C/C++的“#include”,支持绝对路径和相对路径。make命令执行时,默认在当前目录下查找Makefile,还可以通过“-I”参数指定。

(3)找到Makefile后,它会找文件中的第一个目标文件,即文件中最顶端的target,这个target编译完成后则整个编译过程结束,如果还有其他的target没有编译也不再编译,所以文件中的第一个target格外重要。但是,有时候我们偏偏只要编译文件中的某个target,这也是可以的,只需要在make命令后输入这个target即可,其它不相关的 target将不再编译。

(4)整个编译过程其实就是文件依赖的查找与编译的过程。当目标文件不存在或者依赖文件比目标文件新时,将执行后面的commands命令,然后当前的依赖文件又会去查找它自己的依赖文件,就这样一层一层执行下去。

(5)上面例子中Makefile的clean,它没有被第一个target关联,也没有被其它的target关联,是不会被自动执行的,不过可以像第“(2)”点提到的,使用命令“make clean”来完成。

Makefile执行过程可以简单的概括为:Makefile读取>include的Makefile读取>变量初始化>隐晦规则自动推导>目标文件创建依赖关系>依据依赖关系决定哪些目标文件需要重新生成>执行生成命令。

4、使用变量来优化Makefile

在上面的例子中,main这个target用到了main.o和circle.o两次,clean这个target也用到了main.o和circle.o一次,这还算好的,如果依赖文件多时,写多次就太麻烦了,我们可以使用变量來简化这个过程,类似于shell脚本中的变量,修改上面的Makefile如下。

objects = main.o circle.o 

main: $(objects) 
    gcc -o main $(objects) 

main.o: main.c circle.h 
    gcc -c main.c 

circle.o: circle.c circle.h def.h 
    gcc -c circle.c 

clean: 
    rm main $(objects)

上面例子中,我们把main.o和circle.o赋值给了变量objects,这个变量名字是可以随意起的,访问变量时使用美元符号$,并把变量用圆括号括起来,使用花括号也是可以的。

5、使用make的自动推导功能来优化Makefile

make可以自动推导文件及文件依赖关系后面的命令,也就是常说的隐晦规则,与显式规则对比,这样就又可以少写几行代码,以circle.o为例,make会自动推导出依赖文件circle.c和命令“gcc -c circle.c”,再次修改上面的Makefile如下。

objects = main.o circle.o 

main: $(objects) 
    gcc -o main $(objects) 

main.o: circle.h 
circle.o: circle.h def.h 

clean: 
    rm main $(objects)

6、伪目标的使用

每个Makefile都要有一个clean,用于清除文件,一般在文件最下面,常见的做法是使用“.PHONY”把clean变为一个伪目标,因为clean并不是一个真正的目标文件,也可以理解为一个标签,修改上面Makefile如下。

objects = main.o circle.o 

main: $(objects) 
    gcc -o main $(objects) 

main.o: circle.h 
circle.o: circle.h def.h 

.PHONY: clean 
clean: 
    -rm main $(objects)

仔细观察可以发现rm命令前面有个减号,意思是命令执行过程中如果遇到某些文件有问题将忽略,继续执行。

伪目标的另一种功能:执行“make”时,编译的是文件最顶端的目标文件,如果目标文件有多个,最终目标文件也是只有一个,即第一个目标文件,如果我们想一口气编译多个目标文件,就可以使用伪目标,把伪目标放到文件顶端,伪目标的依赖文件为我们真正想要的目标文件。

7、多目标与静态模式

通过上面的Makefile可以发现,目标文件都是一个,依赖文件则有多个,也就是一对一或一对多的关系,其实多个目标文件也可以写在一起,也就是多对一或多对多的关系。
静态模式可以更加容易地定义多目标的规则,语法如下:

targets: <targets-pattern>: <prerequisites-pattern>

targets指定了一系列的目标文件,是一个目标文件的集合。
targets-pattern指明了targets的模式。
prerequisites-pattern指明了依赖文件的模式,与targets-pattern相关。

下面以一个例子说明,假设我们有dog.c和cat.c两个文件,想要编译生成dog.o和cat.o,Makefile如下:

objects = dog.o cat.o 

all: $(objects) 

$(objects): %.o: %.c 
    gcc -c $< -o $@ 

.PHONY: clean 
clean: 
    -rm *.o

上面的Makefile,最终目标是all,为多目标,“%.o”表示目标集中以“.o”结尾的文件即dog.o和cat.o,“%.c”则是将“%.o”表示的文件的后缀从“.o”替换为“.c”,“$<”和“$@”是两个自动化变量,分别表示依赖文件集和目标文件集。

8、“.d”文件

在Makefile中,依赖文件可能会包含多个头文件,在一个大型的工程中,我们必须知道源文件中包含了哪些头文件,这样才能编写依赖关系,数量庞大时就显得工作量繁重了,而且可维护性差,一个好的做法是使用gcc编译的“-M”或者“-MM”选项,它会自动生成依赖关系,这两个选项的区别是前者包含了标准库的头文件而后者没有。以我们一开始的main.c为例,结果如下:

$gcc -M main.c
main.o: main.c circle.h /usr/include/stdio.h /usr/include/features.h \ 
 /usr/include/stdc-predef.h /usr/include/x86_64-linux-gnu/bits/predefs.h \ 
 /usr/include/x86_64-linux-gnu/sys/cdefs.h \ 
 /usr/include/x86_64-linux-gnu/bits/wordsize.h \ 
 /usr/include/x86_64-linux-gnu/gnu/stubs.h \ 
 /usr/include/x86_64-linux-gnu/gnu/stubs-64.h \ 
 /usr/lib/gcc/x86_64-linux-gnu/4.6/include/stddef.h \ 
 /usr/include/x86_64-linux-gnu/bits/types.h \ 
 /usr/include/x86_64-linux-gnu/bits/typesizes.h /usr/include/libio.h \ 
 /usr/include/_G_config.h /usr/include/wchar.h \ 
 /usr/lib/gcc/x86_64-linux-gnu/4.6/include/stdarg.h \ 
 /usr/include/x86_64-linux-gnu/bits/stdio_lim.h \ 
 /usr/include/x86_64-linux-gnu/bits/sys_errlist.h 

$gcc -MM main.c
main.o: main.c circle.h 

从上面的例子可以看出“-M”与“-MM”的区别,我们一般使用后者。自动生成的依赖关系放在“.d”文件中,例如main.c的依赖关系对应于main.d,且看下面的Makefile。

sources:=$(wildcard *.c) 
objects=$(sources:.c=.o) 

all: main 

%.d: %.c 
    rm -f $@ 
    gcc -MM $< > tmp 
    sed ‘s,\($*\)\.o,\1.o $@,g‘ < tmp > $@ 
    rm -f tmp 

sinclude $(sources:.c=.d) 

main: $(objects) 
    gcc -o main $^ 

.PHONY: clean 
clean: 
    -rm -f main *.o *.d

上面的Makefile涉及的知识点较多,稍微有点负责,下面详细解释一下。

第一行:“:=”表示覆盖赋值,另外,“=”是最基本的赋值,“+=”是追加赋值,“?=”是变量没有被赋值时才进行等号后面的赋值,Makefile支持通配符,但是给变量赋值时“*.c”并不会被展开,表示的还是其字面值,所以要用“wildcard”进行转换。
第二行:字符串替换,把“sources”中的“.c”替换为“.o”,同样的功能可以使用Makefile中的patsubst函数实现,这里不做详细介绍。
第三行:空白行。
第四行:目标文件为“all”,依赖文件为“main”。
第五行:空白行。
第六行:目标文件为“%.d”,依赖文件为“.c”,“sources”中所有的“.c”文件都会生成一个对应的“.d”文件。
第七行:删除旧的目标文件,目的是准备生成新的目标文件,自动变量“$@”表示目标文件。
第八行:把上面提到的自动生成的文件依赖关系输入到“tmp”临时文件,“-MM”就是用来自动生成依赖关系但不包括标准库,“$<”表示第一个依赖文件,“>”是数据流重定向符号。
第九行:使用了sed命令,功能是把临时文件“tmp”中的“xxx.o”替换为“xxx.o xxx.d”,即增加一个“.d”文件,xxx是一个“.c”文件的名字,sed命令的用法教为简单,不做详细介绍,这里还用到了shell的数据流输入输出重定向。
第十行:删除tmp临时文件。
第十一行:空白行。
第十二行:使用了“sinclude”,用法同“-include”,include前面的减号表示出错时继续执行,这里使用include也可以,sinclude是为了提高Makefile的兼容性。
第十三行:空白行。
第十四行:“main”是最后生成的可执行文件,依赖于“objects”。
第十五行:使用了自动变量“$^”,表示依赖文件列表。
第十六行:空白行。
第十七行:伪目标。
第十八行:clean,没有依赖。
第十九行:使用rm命令执行一些真正的清除工作。

9、Makefile注释

Makefile注释以“#”开始,shell脚本的注释也是如此,如果在文件中使用“#”字符就要进行转义“#”。

二、Makefile高级特性

1、巧用“@”符号

执行make命令时,默认会把Makefile中的命令输出到屏幕,如果不想要这样的输出时,可以在Makefile文件中的命令前添加“@”符号,对应的命令就不会输出到屏幕。

2、使用make命令的参数

make命令的参数众多,可在shell中通过“man”来查看,这里简单介绍几个。

-n:只显示Makefile中的命令,但不真正执行,可查看Makefile的执行过程。
-s:不显示Makefile中的任何命令,但这些命令会执行到。
-i:忽略所有命令的错误。
-k:当前命令有错误时,继续执行其它的。
-w:执行Makefile时,输出目录信息。

3、命令结果传递

如果想要上一条命令的结果作用下一条命令,那么这两条命令就应该写在一行,以分号分隔,如果这两条命令独立成行的话,结果就大不同了,如下Makefile:

.PHONY: all 
all: 

showcur: 
    cd /home 
    pwd 

showhome: 
    cd /home; pwd

当我们“make showcur”时,pwd命令显示的当前路径。
当我们“make showhome”时,pwd命令显示的home路径。

4、妙用减号“-”

在bash中执行命令时,都有一个回传码,0表示目录正确执行。前面我们介绍过减号“-”的用法,在一个命令的前面,表示这个命令出错时忽略出错信息,继续执行其它的。

5、嵌套执行make命令

大型工程中,代码一般会以功能模块划分,每个功能模块都会有自己的Makefile,例如当前目录有一个Makefile和subdir目录,subdir目录中又有一个Makefile,那么我们如何在当前目录的 Makefile中直接执行subdir目录中的Makefile呢?下面两种方法都是可以的。

submodule: 
    $(MAKE) -C subdir
# or
submodule: 
    # cd subdir && $(MAKE) 

subdir这个子目录中的Makefile如下:

sub: 
    @echo "Makefile in subdir"  

当我们“make submodule”时,subdir这个子目录中的 Makefile就会执行。

6、变量传递

变量传递类似于bash中把当前shell的变量传递到其子shell,这里是传递到子目录中的Makefile,使用“export”关键字,不想传递时使用“unexport”。如下例子所示。
当前目录Makefile:

fathervar = "father" 
export fathervar 
submodule:   
    $(MAKE) -C subdir

subdir这个子目录的Makefile:

sub: 
    @echo "Makefile in subdir" 
    @echo $(fathervar)

如果想传递所有的变量,export后不加任何东西即可。

7、美元符号“$”

在Makefile中美元符号$随处可见,访问变量或者调用函数,当使用真实的美元符号时,类似于转义字符,形式为$$

8、变量嵌套与拼接

变量可以嵌套在一起使用,变量拼接用法同shell脚本,将变量直接连在一起写即可,如下Makefile:

A := a 
B := b 
ab := "vars" 
multivar: 
    echo $($(A)$(B))

执行结果是“vars”。

9、空变量与空格

如下例子,nullstring为空变量,什么也没有,space为空格。

nullstring := 
space := $(nullstring)

10、“override”用法

如果有变量是通常make的命令行参数设置的,那么Makefile中对这个变量的赋值会被忽略,如果你想在Makefile中设置这类参数的值,那么,你可以使用“override”,基本语法如下:

override <variable> = <value>

11、局部变量

上面Makefile的例子中我们定义的变量都是全局变量,我们还可以定义局部变量,也叫目标变量,例子如下:

globalvar := "global" 
local: globalvar := "local" 
local: 
    echo $(globalvar)

local2: 
    echo $(globalvar)

globalvar是个全局变量,在local这个目标中进行了重定义,变成了局部变量,然后在local目标中访问时值为“local”,而local2中还是“global”。

三、Makefile函数

Makefile中的函数众多,可以用来处理变量、文件名等,提供变量的灵活性,返回值可以直接当变量来使用,这里只给出函数调用的一般形式及用法。

语法格式:

$(<function> <arguments>)

语法格式一目了然,函数名与参数列表以空格分开,参数列表内部的各个变量之间以逗号分隔,介绍一下subst函数,形式如下:

$(subst <from>,<to>,<text>)

表示在text中,把from全部替换为to,返回新的内容,如下例子:

funcvar := "hello Makefile" 
func: 
    echo $(subst l,L,$(funcvar))

输出结果为“heLLo Makefile”,小写l全部替换为了大些L,要注意参数列表中的各参数与逗号间不要留有空格,否则替换时空格也起了作用。

上面列举了subst函数的用法,用来处理变量,还有其它一些函数,如用来处理文件名的dir函数以及一些特殊的函数如foreach、if、call、origin、shell等,用法大同小异,这里不再赘述。

另外,还有控制make的函数,类似于C/C++的assert,它们是error和warning,error直接终止程序运行,warning产生警告但不影响运行。

四、条件编译

关键字:ifeq、inneq、indef、inndef、else、endif。前面两个关键字用于判断相等与否,中间两个变量类似于C/C++中的宏定义,else是个可选的关键字,endif放在条件编译的最后,如下例子:

ifvar := 
ifdef ifvar 
ifvar += "defined" 
else 
ifvar := "not defined" 
endif 
condition: 
    echo $(ifvar)

“make condition”时,因为ifvar变量为空,make认为是没有定义的,所以输出结果为“not undefined”。

五、Makefile自动变量

上面的Makefile例子中提到了许多自动变量,下面做个简单的总结。

$@:目标文件集。
$%:只有目标为函数库文件时,这个变量才有效,如Linux下的“.a”静态库,否则为空,有效时其值为函数库文件中的成员名。
$<:依赖文件中的第一个文件,当依赖文件使用了模式变量即“%”,上面的例子中有使用过,那么其值将是符合该模式的一系列的文件集,结果是一个一个取出来的。
$?:比目标文件新的依赖文件的集合,空格分开。
$^:依赖文件的集合,空格分开,去重。
$+:依赖文件的集合,空格分开,不去重。
$*:当使用了模式匹配即“%”,其值是%代表的值及%之前的部分,如果没有使用模式匹配,最起码文件名要有个后缀,如foo.c,那么这个值为foo,否则为空。
上面的七个自动变量还可以分别加上“D”和“F”,举个例子如下。
$(@D):目标文件集的目录部分,如“dir/foo.o”,其值为“dir”。
$(@F):目标文件集的文件部分,如“dir/foo.o”,其值为“foo.o”。

########## end ##########
Makefile介绍完毕,熟悉以上Makefile规则后,就可以搞定一个大型工程的编译法则喽。

版权声明:本文为博主原创文章,未经博主允许不得转载。

Makefile脚本快速入门

标签:makefile

原文地址:http://blog.csdn.net/ieearth/article/details/47296429

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!