1.前言
最近在看Android源码中涉及到了大量的Makefile文件,想通过这篇文章的学习扫如何编写一个简单的makefile文件,在后续的学习过程中,如果还有其他问题可以直接去官网继续学习,国内的教程还有一个陈皓大神写的《跟我一起写Makefile》也是很经典的学习资料。
2.Makefile的由来
通常我们编写项目的时候,都会编写多个C文件,一个C文件我们可以编译为一个目标文件,多个目标文件可以组成一个程序。通过下面的例子,我们理解这一过程。
2.1代码用例
- main.c
#include <stdio.h>
#include <stdlib.h>
int main(int argc,char *argv[])
{
int x,y ;
sscanf(argv[1],"%u",&x);
sscanf(argv[2],"%u",&y);
printf("func1:%u\n",func1(x,y));
printf("func2:%u\n",func2(x,y));
return 0;
}
- func1.c
#include <stdio.h>
int func1(int x,int y)
{
return x+y;
}
- func2.c
#include <stdio.h>
int func2(int x,int y)
{
return x*y;
}
2.1.1 编译和执行
我们使用gcc
命令对上面的main.c
,fun1.c
,func2.c
三个文件编译为程序main
,并执行main
程序。
$ gcc main.c func1.c func2.c -o main
$ ./main 3 5
func1:8
func2:15
为了简化gcc的操作,我们通过make程序替代了gcc的编译操作,这就是Makefile的由来。
3.makefile如何编写
通过上面的用例,我们把gcc的编译操作移植到了makefile文件中去描述编译的过程。
3.1 文件命名规则
在make程序中,我们的makefile文件的命名,只有两种规则全部字母小写
或者只有首字母大写
,即makefile或者Makefile。
3.2 编写规则
在讲述这个makefile之前,还是让我们先来粗略地看一看makefile的规则。
target ... : prerequisites ...
command //前面是一个tab符号,需要注意
...
...
-
target
可以是一个object file(目标文件),也可以是一个执行文件,还可以是一个标签(label)。对 于标签这种特性,在后续的“伪目标”章节中会有叙述。 -
prerequisites
生成该target所依赖的文件和/或target -
command
该target要执行的命令(任意的shell命令)
这是一个文件的依赖关系,也就是说,target这一个或多个的目标文件依赖于prerequisites中的文件, 其生成规则定义在command中。说白一点就是说:
prerequisites中如果有一个以上的文件比target文件要新的话,command所定义的命令就会被执行。
这就是makefile的规则,也就是makefile中最核心的内容。按照上面的规则,我们来编写makefile
文件:
1 #func makefile
2 main: main.c fun1.c func2.c
3 gcc main.c func1.c func2.c -o main
执行日志如下:
$ make
gcc main.c func1.c func2.c -o main
$ ./main 5 5
func1:10
func2:25
可以看出当我们执行make
命令时,gcc的编译命令也被执行了。接下来,我们将文件改变一下。
3.3 多目标文件创建一个程序
上面的命令,我们将c文件直接编译为了main
程序,期间并没有生成目标文件,这里我们将文件修改下,生成目标文件。
main:main.o func1.o func2.o
gcc main.o func1.o func2.o -o main
main.o:main.c
gcc -c main.o
func1.o:func1.c
gcc -c func1.o
func2.o:func2.c
gcc -c func2.o
执行日志
$ make
gcc -c main.c
gcc -c func1.c
gcc -c func2.c
gcc main.o func1.o func2.o -o main
$ ./main 5 6
func1:11
func2:30
- 当我们生成main程序的时候,需要main.o,func1.o,func2.o
- 而main.o是通过main.c生成的,这是一个递归生成目标文件的过程。
由此我们就可以得出结论,Makefile生成文件的过程就是一个递归的过程。
时间戳,在执行make命令的时候,如果make没有改动是不会去编译的。
3.4 添加多个功能
平常我们再make后,会加上清理,安装,卸载的功能,都是通过伪目标的语法来编写的,主要就是target:后面不编写需要依赖的库,类似函数名称的概念。
#func makefile
main:main.o func1.o func2.o
gcc main.o func1.o func2.o -o main
main.o:main.c
gcc -c main.c
func1.o:func1.c
gcc -c func1.c
func2.o:func2.c
gcc -c func2.c
clean:
rm func1.o func2.o main.o main
install:
cp main /usr/local/main
uninstall:
rm /usr/local/main
执行日志:
$ sudo make install
cp main /usr/local/main
$ main 5 5
-bash: main: command not found
$ sudo make uninstall
rm /usr/local/main
$ make clean
rm func1.o func2.o main.o main
在上面执行日志当中,我们已经看见安装,卸载,清理功能都执行了。下面,我们来学习一些makefile的语法。
4.正式学习Makefile
在makefile的语法当中包含了一些我们在其他语言中基本包含的东西比如语法函数和控制语法,下面我们来编写一些用例。
4.1 makefile的变量
makefile的变量包含三类:
- 用户自定义变量
- 预定义变量
- 自动变量及环境变量
4.1.1 用户自定义变量
变量在声明时需要给予初值,而在使用时,需要给在变量名前加上 $ 符号,但最好用小括号 () 或是大括号 {} 把变量给包括起来。如果你要使用真实的 $ 字符,那么你需要用 $$ 来表示。
变量可以使用在许多地方,如规则中的“目标”、“依赖”、“命令”以及新的变量中。
我们接着上一个例子编写,将main.o func1.o func2.o
复制给MObj变量,使用时用$(MObj)
进行替代。
#func makefile
MObj=main.o func1.o func2.o
main:$(MObj)
gcc $(MObj) -o main
main.o:main.c
gcc -c main.c
func1.o:func1.c
gcc -c func1.c
func2.o:func2.c
gcc -c func2.c
clean:
rm $(MObj) main
install:
cp main /usr/local/main
uninstall:
rm /usr/local/main
执行日志如下:
$ make
gcc -c main.c
gcc -c func1.c
gcc -c func2.c
gcc main.o func1.o func2.o -o main
$ ./main 5 5
func1:10
func2:25
另外的变量复制方式
上面使用=
号来进行赋值,会将main.o进行递归的生成,如果我们想去掉此功能,可以使用:=
符号来进行复制。这也是我们常用的赋值方式。
4.1.2 预定义变量
4.1.3 预定义变量
根据main:main.o func1.o func2.o
这句语法看见,我们的目标文件main
的生成是依靠main.o
、func1.o
、func2.o
文件生成的,通过上表的预定义变量,我们的指令中$^
代表所有不重复的依赖文件main.o
、func1.o
、func2.o
,$@
代表生成的目标文件main
,可以改成下面图片的样子.
4.2 伪目标
最早先的一个例子中,我们提到过一个“clean”的目标,这是一个“伪目标”.
clean:
rm *.o temp
“伪目标”并不是一个文件,只是一个标签,由于“伪目 标”不是文件,所以 make 无法生成它的依赖关系和决定它是否要执行。为了避免和文件重名的这种情况,我们可以使用一个特殊的标记“.PHONY”来显式地指明一 个目标是“伪目标”,向 make 说明,不管是否有这个文件,这个目标就是“伪目标”。
.PHONY : clean
只要有这个声明,不管是否有“clean”文件,要运行“clean”这个目标,只有“make clean”这 样。于是整个过程可以这样写:
.PHONY : clean
clean:
rm $(MObj) main
4.3 引用其它make file及makefile嵌套
4.3.1 引用其它makefile
在 Makefile 使用 include 关键字可以把别的 Makefile 包含进来,这很像 C 语言的 #include ,被包含的文件会原模原样的放在当前文件的包含位置。include 的语法是:
include <filename>
在 include 前面可以有一些空字符,但是绝不能是 Tab 键开始。
- 例子:
#func makefile
include config.mk
MObj=main.o func1.o func2.o
...
在同级目录下,创建一个config.mk
文件,在表头用include config.mk
语法包含。
如果你想让 make 不理那些无法读取的文件,而继续执行,你可以 在 include 前加一个减号“-”.
4.3.2 嵌套其它文件
在一些大的工程中,我们会把我们不同模块或是不同功能的源文件放在不同的目录中,我们可以在 每个目录中都书写一个该目录的 Makefile,这有利于让我们的 Makefile 变得更加地简洁,而不至于 把所有的东西全部写在一个 Makefile 中,这样会很难维护我们的 Makefile,这个技术对于我们模块 编译和分段编译有着非常大的好处。
例如,我们有一个子目录叫 subdir,这个目录下有个 Makefile 文件,来指明了这个目录下文件的 编译规则。那么我们总控的 Makefile 可以这样书写:
subsystem:
cd subdir && $(MAKE)
其等价于:
subsystem:
$(MAKE) -C subdir
定义$(MAKE)宏变量的意思是,也许我们的make需要一些参数,所以定义成一个变量比较利于维护。这两个 例子的意思都是先进入“subdir”目录,然后执行make命令。
我们把这个Makefile叫做“总控Makefile”,总控Makefile的变量可以传递到下级的Makefile中(如果 你显示的声明),但是不会覆盖下层的Makefile中所定义的变量,除非指定了 -e 参数。
参考链接:https://seisman.github.io/how-to-write-makefile/recipes.html#make
4.4 条件判断
使用条件判断,可以让make根据运行时的不同情况选择不同的执行分支。条件表达式可以是比较变量的值, 或是比较变量和常量的值。
我们将上面main的例子改变下,判断 $(CC) 变量是否 gcc ,如果是的话,则使用GNU函数编译目标。
MObj=main.o func1.o func2.o
CFLAGS= -c
CC=gcc
main:$(MObj)
ifeq ($(CC),gcc)
gcc $^ -o $@
else
$(CC) $^ -o main2
endif
....
这里注意语法,条件判断需要顶头写。
参考链接:https://seisman.github.io/how-to-write-makefile/conditionals.html
函数
在Makefile中可以使用函数来处理变量,从而让我们的命令或是规则更为的灵活和具有智能。make 所支持的函数也不算很多,不过已经足够我们的操作了。函数调用后,函数的返回值可以当做变量来使用。
函数调用,很像变量的使用,也是以 $ 来标识的,其语法如下:
$(<function> <arguments>)
或是:
${<function> <arguments>}
这里, <function>
就是函数名,make支持的函数不多。 为函数的参数, 参数间以逗号 , 分隔,而函数名和参数之间以“空格”分隔。函数调用以 $ 开头,以圆括号 或花括号把函数名和参数括起。感觉很像一个变量,是不是?函数中的参数可以使用变量,为了风格的 统一,函数和变量的括号最好一样,如使用 $(subst a,b,$(x))
这样的形式,而不是 $(subst a,b, ${x})
的形式。因为统一会更清楚,也会减少一些不必要的麻烦。
更过细节请查看链接:https://seisman.github.io/how-to-write-makefile/functions.html
Makefile的管理命令
-C dir
:读入制定目录下面的makefile
make -C <包含makefile文件的目录>
示例
$ ls
cpp
$ make -C cpp
make: Entering directory `/home/mujf/workspace/cpp'
gcc -c main.c
gcc -c func1.c
gcc -c func2.c
gcc main.o func1.o func2.o -o main
make: Leaving directory `/home/mujf/workspace/cpp'
我们回退到cpp的同级目录中,执行命令,会自动进入cpp目录并且执行make命令,在Android源码编译中被大量使用。
-f file
读入当前目录下的file文件为makefile
make -f makefile
表示执行当前文件夹下指定的makefile
文件。
-i
忽略所有命令执行错误-I dir
指定被包含的makefile所在目录
这里是大i
结束语
这里,我们已经可以开始研究Android的源码,在源码的build/core
路径下包含了大量的makefile文件。接下来阅读源码,我们需要思考三个问题:
- 源文件或者依赖文件过多怎么办?
makefile文件分开或者分级
- output不仅仅一个文件怎么办?
用多个makefile文件,互相include 嵌套
使用伪目标 make all
- 如何嵌套执行makefile文件
可查看上面的内容