Makefile
Makefile
Linux下使用GNU make来构建和管理代码工程。
源码包的安装中经常会使用。
Makefile文件:描述了整个工程的编译、链接、安装等规则。
包括:
①:工程中的哪些源文件需要编译以及如何编译。
②:需要创建那些库文件以及如何创建这些库文件。
③:如何最后产生我们想要得可执行文件。
好处:使用一行命令来完成“自动化编译”,极大提高了效率。
make工具规则
当使用make工具进行编译时,工程中以下几种文件在执行make时将会被编译(重新编译):
1.所有的源文件没有被编译过,则对各个C源文件进行编译并进行链接,生成最后的可执行程序;
2.每一个在上次执行make之后修改过的C源代码文件在本次执行make时将会被重新编译;
3.头文件在上一次执行make之后被修改。则所有包含此头文件的C源文件在本次执行make时将会被重新编译。
Makefile语法
一个简单的Makefile描述规则组成:
TARGET... : PREREQUISITES...
COMMAND
...
TARGET
TARGET:规则的目标。
通常是最后需要生成的文件名或者为了实现这个目的而必需的中间过程文件名。
可以是.o文件,也可以是最后的可执行程序的文件名等。
另外,目标也可以是一个make执行的动作的名称,如目标“clean”,我们称这样的目标是“伪目标”。
PREREQUISITES
PREREQUISITES:规则的依赖。
生成规则目标所需要的文件名列表。通常一个目标依赖于一个或者多个文件。
COMMAND
COMMAND:规则的命令行。
是规则所要执行的动作(任意的shell命令或者是可在shell下执行的程序)。它限定了make执行这条规则时所需要的动作。
一个规则可以有多个命令行,每一条命令占一行。
注意:
每一个命令行必须以[Tab]字符开始,[Tab]字符告诉make此行是一个命令行。
make按照命令完成相应的动作。这也是书写Makefile中容易产生,而且比较隐蔽的错误。
makefile编写
最简单的makefile文件可以命名为 makefile 或 Makefile。
注意:不可以用 makeFile。
创建 main.c 文件
#include <stdio.h>
#include "print.h"
int main()
{
hello();
return 0;
}
创建 print.c 文件
#include "print.h"
void hello(void)
{
puts("hello world C++\n");
}
创建 print.h 文件
void hello(void);
创建makefile文件:
vi makefile
打开之后首先规则要填写目标。(可以是可执行程序文件,也可以是 .o 文件。)
目标后面跟 : 后面跟生成目标的所需要的依赖文件。(例如:生成 .o 文件需要依赖 .c 文件。)
all: main.c print.c
注意:如果目标没有依赖文件也可以直接不填。
all:
生成目标需要一些指令,指令在目标下一行使用 Tab 缩进之后进行编写。
添加依赖文件:
all: main.c print.c
gcc main.c print.c ‐o helloworld
# 注意:上面一行指令使用 Tab 缩进之后进行编写。(语法要求)
不添加依赖文件:
all:
gcc main.c print.c ‐o helloworld
# 注意:上面一行指令使用 Tab 缩进之后进行编写。(语法要求)
注意:makefile 文件和源码必须在同一个路径。
上面的已经完成了一个简单可用的 makefile 文件。
在当前目录执行 make
命令,命令会在当前路径查找 makefile 并且按照 makefile 的规则去编译。
使用 make 命令执行完成并执行结果:
可以在指令中执行shell命令。
all:
gcc print.c main.c -o helloworld
echo "gcc success"
上面 makefile 缺点分析:
不管源文件是否修改,整个工程都会重新编译一次。
(和上面给出的 make 工具规则不符合,而且源文件太多损耗时间会很长。)
make 编译原理:
-
当存在多个目标,只会执行第一个目标。
-
如果目标文件存在,会将 目标文件的修改时间 和 目标文件的依赖文件的修改时间 进行对比。
如果 目标文件的修改时间 比 目标文件的所有依赖文件的修改时间 靠后,不执行目标的下一行指令。
如果 目标文件的修改时间 比 目标文件的所有依赖文件的修改时间 靠前,执行目标的下一行指令。 -
如果目标文件不存在,则认为目标时间为 0,执行上面第二条。
helloworld: main.c print.c
gcc main.c print.c ‐o helloworld
# 注意:上面一行指令使用 Tab 缩进之后进行编写。(语法要求)
当已经存在的目标文件多次使用 make 命令编译时,会提示目标文件已经是最新的。
如果依赖文件进行修改,则依赖文件的修改时间会比目标文件的修改时间更新,则会执行目标文件下一个行的指令。
上面 makefile 缺点分析:
修改一个源文件,整个工程都会重新编译一次。
(和上面给出的 make 工具规则不符合,而且源文件太多损耗时间会很长。)
将上面gcc分为多个阶段:
helloworld:print.o main.o
gcc print.o main.o -o helloworld
print.o:print.c
gcc -c print.c
main.o:main.c
gcc -c main.c
执行 make 命令:
变量
上面 makefile 缺点分析:
如果工程有很多 .o 文件,每次都需要写很多,给后期维护和修改带来了很多方便。例如:添加或者修改时写错或遗漏。
那么就可以使用变量进行替换。
使用 obj变量进行替换:
定义 :
objs=print.o main.o
target=helloworld
定义之后使用变量进行替换:
objs=print.o main.o
target=helloworld
$(target):$(objs)
gcc $(objs) -o $(target)
print.o:print.c
gcc -c print.c
main.o:main.c
gcc -c main.c
使用 make 命令进行编译:
清空工作目录过程文件
上面 makefile 缺点分析:
如果重新编译,需要删除生成的中间文件。工程中间文件太多手动删除肯定是不合理的。
定义规则实现清除当前目录中编译过程中产生的临时文件:
clean :
rm helloworld $(objs)
clean 命令需要我们自己执行清理,但是如果目录中有了 clean 就会出现冲突。
注意:
①:clean规则没有依赖文件,无法进行时间对比,所以目标被认为是最新的而不去执行规则作定义的命令。
②:在命令行之前使用 -,意思是无论当前命令是否正常执行, 都继续执行后面的命令。
objs=print.o main.o
target=helloworld
$(target):$(objs)
gcc $(objs) -o $(target)
print.o:print.c
gcc -c print.c
main.o:main.c
gcc -c main.c
clean:
-rm -rf $(objs) $(target)
echo "clean success"
@的使用
“@”不显示命令本身,只显示结果。如:@echo "clean success"
上面 makefile 缺点分析:
如果目录中有一个名字 clean 的文件,则会和 make clean 命令产生冲突。
解决方法:
通过“.PHONY”特殊目标将“clean”目标声明为伪目标。避免当磁盘上存在一个名为“clean”文件时,在我们输入“make clean”时不会产生异常。
objs=print.o main.o
target=helloworld
$(target):$(objs)
gcc $(objs) -o $(target)
print.o:print.c
gcc -c print.c
main.o:main.c
gcc -c main.c
.PHONY:clean
clean:
-rm -rf $(objs) $(target)
@echo "clean success"
注意:
伪目标不需要和依赖文件的修改时间进行比较,直接执行目标的下一行指令。
没有依赖文件的目标文件修改时间和 0 进行比较。
上面 makefile 缺点分析:
上面 .c 文件生成 .o 文件,如果工程中 .c 文件很多,全部手写也是不合理的。
解决方法:
objs=print.o main.o
target=helloworld
$(target):$(objs)
gcc $(objs) -o $(target)
%.o:%.c
gcc -c $^ -o $@
.PHONY:clean
clean:
-rm -rf $(objs) $(target)
@echo "clean success"
$^
代表 .c 依赖文件。
$@
代表 .o 目标文件。
编译演示:
自动推导
在使用make编译.c源文件时,编译.c源文件规则的命令可以不用明确给出。
这是因为make本身存在一个默认的规则,能够自动完成对.c文件的编译并生成对应的.o文件。
它执行命令 cc -c
来编译.c源文件。在Makefile中我们只需要给出需要重建的目标文件名(一个.o文件),
make会自动为这个.o文件寻找合适的依赖文件(对应的.c文件。对应是指:文件名除后缀外,其余都相同的两个文件),
而且使用正确的命令来重建这个目标文件。
makefile文件:
objs=print.o main.o
target=helloworld
$(target):$(objs)
gcc $(objs) -o $(target)
.PHONY:clean
clean:
-rm -rf $(objs) $(target)
@echo "clean success"
执行 make 命令:
上面 make 编译过程默认使用 cc 命令进行编译。
make 默认规则的隐含规则:
对于上边的例子,此默认规则就使用命令“cc c main.c o main.o”来创建文件“main.o”。
对一个目标文件是“N.o”,倚赖文件是“N.c”的规则,完全可以省略其规则的命令行,而由make自身决定使用默认命令。
查看默认的隐含规则:
通过执行 make -p
命令查看:
上面框出来的规则我们自实现的 makefile 规则类型。
编译变量
上面默认规则变量:
COMPILE.c = $(CC) $(CFLAGS) $(CPPFLAGS) $(TARGET_ARCH) ‐c
$(OUTPUT_OPTION)
为空。
$^
表示依赖的所有 .c 文件。
%.o: %.c
代表通配所有的.c文件,并且指定其对应的目标.o,都执行内建命令。
$(CC)
:编译工具。
$(CFLAGS)
:编译时的参数,如g,Wall,fPIC。
$(CPPFLAGS)
:预处理时的参数,如I,D, shared。
$(LDFLAGS)
:链接时的参数。
$(TARGET_ARCH)
:代表cpu的构架,一般默认是x86。
将上面参数在 makefile 中使用:
修改 main.c 函数增加debug定义:
#include <stdio.h>
#include "print.h"
#ifdef DEBUG
#define LOG() puts("debug version");
#else
#define LOG()
#endif
int main()
{
LOG();
hello();
return 0;
}
objs=print.o main.o
target=helloworld
CFLAGS=-g
CPPFLAGS=-DDEBUG
LDFLAGS=
$(target):$(objs)
gcc $(objs) -o $(target) $(LDFLAGS)
.PHONY:clean
clean:
-rm -rf $(objs) $(target)
@echo "clean success"
make 编译出调试版本并打印调试信息:
编译过程自动添加 -g 参数,生成调试程序就可以使用 GDB 进行调试。
上面 makefile 缺点分析:
上面 objs 后面的 .o 文件如果很多,维护和修改也比较麻烦。
解决方法:使用 makefile 函数自动获取 .o 文件。
makefile函数
wildcard 函数可以用来查找对应通配符的文件。如果想找所有的.c文件则可以这样写 $(wildcard *.c)
。
找到.c文件后,我们希望能够自动生成.o文件列表。
可以使用$(patsubst %.c, %.o, $(wildcard *.c))
将所有的.c文件替换成.o。
objs=$(patsubst %.c, %.o, $(wildcard *.c))
target=helloworld
CFLAGS=-g
CPPFLAGS=-DDEBUG
LDFLAGS=
$(target):$(objs)
gcc $(objs) -o $(target) $(LDFLAGS)
.PHONY:clean
clean:
-rm -rf $(objs) $(target)
@echo "clean success"
上面的 makefile 编译规则不依赖于任何源文件。
好处:不同的工程可以使用相同的 makefile 进行编译。
附加目标
distclean一般是用来彻底清除生成的过程文件和生成的配置文件。这个目标也是要自己写的。
比如命令:
distclean:
rm ‐rf /usr/bin/app
rm ‐rf /etc/app.config
install:
cp app /usr/bin/app
缺省makefile
当没有编写makefile文件的时候,直接执行 make 指令加某个目标。
如果在当前目录中存在目标默认的依赖.c文件,则自动执行 gcc [目标].c ‐o [目标] 。
例如:当前路径有一个文件test.c,编写完代码后,直接执行
make test
如果编译通过,将在当前路径上生成一个test可执行程序。