Makefile使用入门很简单(实例讲解)
一、引言
如开发一个小项目,在当前目录下(~/myproject),内含3个c源码文件源码和2个h头文件,依赖关系如下:
f1.c-->def1.h
f2.c-->def2.h
mian.c-->f1.cf2.c
/*f1.c*/
#include "stdio.h"
#include"def1.h"
voidfunc_one()
{
printf("This is func_one() ,it isin f1.c!\n");
}
/*f2.c*/
#include "stdio.h"
#include"def2.h"
voidfunc_two()
{
printf("This is func_two(),it isin f2.c!\n");
}
/*main.c*/
#include"stdio.h"
#include"def1.h"
#include"def2.h"
intmain()
{
printf("main() isruning!\n");
func_one();
func_two();
return 1;
}
上节我们了解gcc编译器简单使用,知道先分别编译源文件(*.c)、再链接所有目标文件(*.o)可生成执行文件,演示过程如下:
[root@localhostmyproject]# ls
def1.h def2.h f1.c f2.c main.c
//编译
[root@localhostmyproject]# gcc -c f1.c -o f1.o
[root@localhostmyproject]# gcc -c f2.c -o f2.o
[root@localhostmyproject]# gcc -c main.c -o main.o
//链接
[root@localhostmyproject]# gcc main.o f1.o f2.o -o main
[root@localhostmyproject]# ls
def1.h def2.h f1.c f1.o f2.c f2.o main main.c main.o
//执行
[root@localhostmyproject]# ./main
main()is runing!
Thisis func_one(),it is in f1.c!
Thisis func_two(),it is in f2.c!
如果软件项目工程大,有成百上千个c源代码程序,上述过程会把人累晕!这时我们介绍另外一个工具,make命令及其配置文件Makefile。
为了初步体会Makefile,我们使用它同样来实现上述小项目的编译链接。
//先清理文件
[root@localhostmyproject]# rm -rf *.o
[root@localhostmyproject]# rm -rf main
//编辑Makefile文件,如下:
[root@localhost myproject]# vim Makefile
/*Makefile*/
main:main.o f1.o f2.o
gcc main.o f1.o f2.o -o main
main.o:main.c def1.h def2.h
gcc -c main.c -o main.o
f1.o:f1.c def1.h
gcc -c f1.c -o f1.o
f2.o:f2.c def2.h
gcc -c f2.c -o f2.o
这就是Makefile文件结构,清晰简单,每项由两部分组成:依赖关系与一些命令。它们一步一步的告诉make命令如何去生成执行文件main。
[root@localhostmyproject]# ls
def1.h def2.h f1.c f2.c main.c Makefile
//make命令
[root@localhostmyproject]# make
gcc-c main.c -o main.o
gcc-c f1.c -o f1.o
gcc-c f2.c -o f2.o
gccmain.o f1.o f2.o -o main
[root@localhostmyproject]# ls
def1.h def2.h f1.c f1.o f2.c f2.o main main.c main.o Makefile
程序源代码一旦写好,只需要一个make命令,整个工程完全自动编译,提高了软件开发的效率。即依据Makefile文件“自动化编译”!
二、make命令与Makefile关系
Makefile文件,就是告诉make命令需要怎么样的去编译和链接程序,决定检查哪些模块,以及如何构建软件。
make 命令,就是用于自动编译、链接程序的实用工具。能够根据程序中模块的修改情况,自动判断应该对那些模块重新编译,从而保证软件是由最新的模块构成。
同时,make程序的命令行选项可以对Makefile进行即时配置。
三、make命令的语法与常用选项
make [options] [target]…
选项 | 含义 |
-f FILE | 以指定的FILE 文件作为makefile。 |
-n | 只打印要执行的命令,但不执行这些命令。 |
-s | 在执行命令时不显示命令。 |
-p | 输出所有宏定义和目标文件描述 |
-i | 忽略命令执行返回的出错信息。 |
d | 显示调试信息 |
-c dir | 在读取 makefile 之前改变到指定的目录dir |
-I dir | 当包含其他 makefile文件时,利用该选项指定搜索目录 |
-w | 在处理 makefile 之前和之后,都显示工作目录。 |
四、make命令的工作原理
当输入make命令之后,会默认的在当前目录下按顺序找寻文件名为“GNUmakefile”、“makefile”、“Makefile”的文件,找到就解析这个文件。
解析过程就是一层又一层地去找文件的“规则”(规则:由依赖关系和其所定义的一些命令组成),这些“规则”会指示 make 如何进行编译,直到最终编译出第一个目标文件。
在解析过程中,比如,规则的目标[ target ]冒号后面的被依赖的文件找不到(伪目标除外),那么make就会报错退出;
而对于所定义的命令的错误(或是编译不成功),make命令会忽视后继续下一行的解析。
归纳成执行步骤如下:
①、读入所有的Makefile。
②、读入被include的其它Makefile。
③、初始化文件中的变量。
④、推导隐晦规则,并分析所有规则。
⑤、为所有的目标文件创建依赖关系链。
⑥、根据依赖关系,决定哪些目标要重新生成。
⑦、执行生成命令。
五、Makefile文件的基本结构
Makefile文件的结构是由包含一系列的“规则”组成,其样式如下:
目标(target) :依赖(prerequiries)…
<tab>命令(command)
<tab>命令(command)
……
//或者
目标(target):依赖(prerequiries)…;命令(command)
<tab>命令(command)
……
//又或者
伪目标(target) :
<tab>命令(command)
<tab>命令(command)
……
基本语义如下:
规则的目标(target):通常是要产生的文件的名称或者为了实现这个目标
而必需的中间过程文件名。目标可以是可执行文件或OBJ文件,也可是一个执行的动作名称(这目标称为伪目标(PHONY),如‘clean’)。
规则的依赖(prerequiries):生成规则目标所需要的文件名列表,一个目标生成经常依赖一个或几个文件。
规则的命令(command):是规则所要执行的动作(任意的shell命令或者
是可在shell下执行的程序),一个规则可以含有几个命令,每个命令占一行。
<tab>:每一个命令行必须以[Tab]字符开始,[Tab]字符告诉make此行是一个命令行,这是不小心容易出错的地方。此外,如果在makefile文件中的行尾加上空格键的话,也会导致make命令运行失败。
六、隐式规则
Make命令会自动使用gcc -c命令,将一个扩展名为.c的c语言程序源文件编译成一个同名的.o目标文件。
因此,在上例中,我们将Makefile文件使用隐式规则重新编辑,如下:
[root@localhost myproject]#vim Makefile1
/*Makefile1*/
main:main.o f1.o f2.o
gcc main.o f1.o f2.o -o main
main.o: def1.h def2.h
gcc -c main.c
f1.o:def1.h
gcc -c f1.c
f2.o:def2.h
gcc -c f2.c
[root@localhostmyproject]# make –f Makefile1
使用了隐式规则,隐式规则只是节省了敲代码的数量,但是可读性差些,一般大项目中不建议使用。
七、伪目标
有些目标(target:)区别于普通的,它不是为了编译生成文件,而是实现一些小功能的作用,比如,清除之前编译时生成的“.o”目标文件。这种类型目标(target:)便叫做伪目标。请看下面的示例:
示例:在上述范例的Makefile1文件末尾中加入clean伪目标,清除“.o”目标文件。
[root@localhost myproject]#vim Makefile1
/*Makefile1*/
main:main.o f1.o f2.o
gcc main.o f1.o f2.o -o main
main.o: def1.h def2.h
gcc -c main.c
f1.o:def1.h
gcc -c f1.c
f2.o:def2.h
gcc -c f2.c
.PHONY clean
clean:
-rm *.o
[root@localhost myproject]#make –f Makefile1 clean
我们可以直接输入:“make clean”(有些教程使用默认的Makefile文件)来运行这个伪目标。显然,此伪目标可以清除之前生成的.o目标文件。
加入“.PHONY clean”,是为了防止命名的重复,可以使用.PHONY来显式地指明一个伪目标,这样就不会对clean这个目标的性质定义产生混乱。
项目中常见的伪目标,实现编译、安装、打包等功能,如下:
“all” 这个伪目标是所有目标的目标,其功能一般是编译所有的目标。
“clean” 这个伪目标功能是删除所有被make创建的文件。
“install” 这个伪目标功能是安装已编译好的程序,其实就是把目标执行文件拷贝到指定的目标中去。
“print” 这个伪目标的功能是例出改变过的源文件。
“tar” 这个伪目标功能是把源程序打包备份。也就是一个tar文件。
“dist” 这个伪目标功能是创建一个压缩文件,一般是把tar文件压成Z文件。或是gz文件。
“TAGS” 这个伪目标功能是更新所有的目标,以备完整地重编译使用。
“check”和“test” 这两个伪目标一般用来测试makefile的流程。
八、Makefile常用的语法
为了增强Makefile的功能与增加程序的可移植性,Makefile中使用很多语法格式,如变量(宏)、通配符、函数、条件表达式、关键字等等。
①内部定义的宏
符号 | 含义 |
$ | 宏引用,如$(CC) |
$@ | 代表目标文件 |
$^ | 代表所有的依赖文件 |
$< | 代表第一个依赖文件 |
$? | 代表比目标的修改时间更晚的那些依赖文件。 |
$* | 代表去掉后缀的当前目标名。例如,若当前目标是pro.o,则$*表示pro |
宏的作用类似于C语言中的define。Makefile文件中的宏分为两种类:
系统内部定义的宏,上表已经列出,可以直接使用。
用户自己定义的宏,必须在makefile或命令行中明确定义。
格式1:宏标识符 = 值列表 //这个格式很少用
格式2:宏标识符 := 值列表 //这个使用正常
格式3:宏标识符 ?= 值列表
上述三个方式赋值有些区别的,大家自己根据需要使用!一般就用格式2吧!
示范:用户自定义宏
main:main.o f1.o f2.o
gcc main.o f1.o f2.o -o main
上述规则可转化成如下:
OBJFS:=main.o f1.of2.o
main: $(OBJFS)
gcc main.o f1.o f2.o -omain
由于Makefile当中的宏在本质上是一个字符串,因此可以追加宏的值,用‘+=’操作符。例如:
objects := main.o foo.o
则objects +=another.o 结果为:
objects := main.o foo.o another.o
②关键符的含义
符号 | 含义 |
# | 注释以#为开头,至行尾结束。 |
\ | 将一个较长行使用反斜线(\)来分解为多行,反斜线之后不能有空格。 |
@ | 不要显示执行的指令。 |
- | 表示即使该行指令出错,也不会中断执行。 |
示范:反斜线(\)
main: main.o f1.o f2.of3.o f4.o \
f5.o f6.o f7.o
gcc mian.o f1.o f2.o f3.o f4.o \
f5.o f6.o f7.o
clean:
-rm *.o
③通配符
通配符,如’*’,’?’。它只在规则的目标、依赖关系、命令中会自动扩展。但是,其它情况下,比如,在规则中的文件名可以包含统配符,除非显式使用’wildcard’函数,否则统配符不会扩展开来的。
示范:可执行文件’main’是从当前目录中的所有’.o’文件生成的:
objects = *.o
main : $(objects)
gcc $(objects) -o main
如果当前目录删除了所有的’.o’文件,此时通配符不匹配任何文件,就保持原样(无法扩展了),则’main’依赖于一个叫做’*.o’的文件,make命令找不到这个文件就会报错,把上述代码代入Makefile1中,测试下便知!
④函数语法与常用函数(详见范例讲解)
几种常用的函数介绍,详解见《Linux_编程_02:Makefile常用函数》,一般中等项目软件够用了!
⑤目录与文件的搜索(详见范例讲解)
make只会在当前的目录中去找寻依赖文件和目标文件。但是,在大的工程中,许多的源文件分类存放在不同的目录中。关于目录与文件的搜索,Makefile 提供了两种方式:
第一种是设置全局访问路径VAPTH。
第二种是设置关键字vpath。
先介绍第一种方式:Makefile文件中有个特殊变量“VPATH”,如果定义了,则make就会在当前目录找不到的情况下,到所指定的目录中去找寻文件了。
语法:VPATH =src:../headers
上面的的定义指定两个目录,“src”和“../headers”,make会按照这个顺序进行搜索。目录由“冒号”分隔。
注意:定义了VPATH或vpath,这仅仅是对于makefile来说搜索目标和依赖文件的路径,但是对于命令行来说是无效的,也就是说在执行g++或者gcc时不会自动从VPATH 或者vpath中自动搜索要包含的头文件等信息文件。
此时要用到了 -I 或者--incude +路径。这也容易出错的地方。
第二种方式:当需要为不类型的文件指定不同的搜索目录时需要这种方式。
语法:
#为符合模式<pattern>的文件指定搜索目录<directories>。
vpath <pattern> <directories>
#清除符合模式<pattern>的文件的搜索目录。
vpath <pattern>
#清除所有已被设置好了的文件搜索目录。
vpath
<pattern>指定了要搜索的文件集,而<directories>则指定了<pattern>的文件集的搜索的目录。
示范
vpath %.h ../headers
vpath %.c foo
要求make在“../headers”目录下搜索所有以“.h”结尾的文件;在“foo”目录下搜索所有以“.c”结尾的文件。
⑥条件表达式
条件编译:如果满足一定的条件,将执行一定的动作。
常见的条件编译指令:
ifeq(arg1, arg2) .. endif 如果arg1和arg2相等的话,将执行该区域中的内容。
ifneq(arg1, arg2) .. endif 如果arg1和arg2不相等的话,将执行该区域内的内容。
ifdef ARG .. endif 判定ARG变量是否已经定义,已定义则将执行该区域内的内容。
ifndef ARG .. endif 和ifdef相反
上面的语句可以和else一起来使用,如下:
ifneq ("same", "diff")
@echo "same !=diff"
else
@echo "same ==diff"
endif
九、自动产生依赖关系表
在一个含有大量源文件的项目中,很可能每个源文件都包含一组头文件,而头文件有时又会包含其它头文件,这时正确区分依赖关系就比较难了,了防止遗漏,gcc还有一个-MM选项可用,该选项会为make生成一个依赖关系表,如下:
[root@localhost makepro]# gcc -MM main.cf1.c f2.c f3.c
main.o: main.c def1.h
f1.o: f1.c def1.h
f2.o: f2.c def1.h def2.h
f3.o: f3.c def2.h def3.h
将它们保存到一个临时文件内,然后将其插入makefile即可。