3、Make基础回顾:Makefile基本语法、目标、依赖、变量、函数、Android.mk的Make本质
说实话,很多做Android开发的朋友,一听到Make就头大。我当年刚接触AOSP时也一样,看着那一堆Android.mk文件,完全不知道它们在干嘛。后来我才明白——不懂Make,你根本没法真正理解Android的构建系统。
这一章,我们就来扒一扒Make的老底。别怕,我会用最接地气的方式讲清楚。
3.1 Make是什么?一个简单的故事
想象一下:你写了一个App,有100个源文件。你只改了其中1个,难道要重新编译全部100个?太蠢了。Make就是来解决这个问题的——它只重新编译那些「变了」的文件。
说白了,Make是一个自动化构建工具。它根据文件的时间戳来判断:如果目标文件比依赖文件旧,就重新生成。就这么简单。
核心思想:Make的本质是一个「有向无环图(DAG)」的依赖解析器。它把构建规则组织成一张图,然后按拓扑顺序执行。
3.2 Makefile基本语法
一个Makefile的基本结构长这样:
目标(target): 依赖(dependencies)
<Tab>命令(commands)
注意那个Tab!我当年踩过这个坑——用空格代替Tab,结果Make报错说「missing separator」。嗯,这是个经典新手错误。
来看个实际例子:
hello: hello.c
gcc -o hello hello.c
这个规则的意思是:要生成hello这个文件,需要先有hello.c。如果hello.c比hello新,就执行gcc命令重新编译。
3.3 目标(Target)
目标可以是文件名,也可以是「伪目标」。伪目标不生成文件,只是执行一些操作。
.PHONY: clean
clean:
rm -f *.o hello
为什么要加.PHONY?因为如果当前目录下恰好有一个叫clean的文件,Make就会认为「clean已经是最新的了」,不执行命令。加.PHONY就是告诉Make:别管文件,直接执行。
我的习惯:所有不生成文件的目标,都加上.PHONY声明。这是好习惯,能避免很多奇怪的问题。
3.4 依赖(Dependencies)
依赖是Make的灵魂。一个目标可以有多个依赖,依赖本身也可以是其他目标。
app: main.o utils.o
gcc -o app main.o utils.o
main.o: main.c header.h
gcc -c main.c
utils.o: utils.c utils.h
gcc -c utils.c
执行make app时,Make会先检查main.o和utils.o是否需要更新,如果需要,就递归地处理它们的依赖。这就是「依赖链」的概念。
我在AOSP里见过最深的依赖链有十几层。你想想看,一个小小的改动,可能触发上百个文件的重新编译。
3.5 变量(Variables)
写Makefile不用变量?那你得累死。变量让Makefile变得灵活可维护。
CC = gcc
CFLAGS = -Wall -O2
OBJS = main.o utils.o
app: $(OBJS)
$(CC) -o app $(OBJS)
%.o: %.c
$(CC) $(CFLAGS) -c $< -o $@
这里有几个特殊变量要注意:
| 变量 | 含义 |
|---|---|
| $@ | 目标文件名 |
| $< | 第一个依赖文件名 |
| $^ | 所有依赖文件名(去重) |
| $? | 所有比目标新的依赖 |
我曾经踩过的坑:在AOSP里,变量展开有两种方式——「简单展开」(:=)和「递归展开」(=)。用=定义变量时,如果变量引用了自身,会陷入无限递归。我见过同事因为这个,Make跑了半小时还没结束。
3.6 函数(Functions)
Make内置了不少实用函数,处理字符串、文件列表什么的很方便。
SRCS = $(wildcard src/*.c)
OBJS = $(patsubst src/%.c, obj/%.o, $(SRCS))
app: $(OBJS)
gcc -o $@ $^
常用的函数有:
$(wildcard pattern)— 匹配文件模式$(patsubst pattern,replacement,text)— 模式替换$(foreach var,list,text)— 循环处理$(call func,args...)— 调用自定义函数$(shell command)— 执行shell命令
我个人最常用的是$(foreach),在AOSP里处理多架构编译时特别好用。
3.7 Android.mk的Make本质
好了,重点来了。Android.mk本质上就是一个Makefile——只不过Google给它加了一层「语法糖」。
看一个典型的Android.mk:
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := hello
LOCAL_SRC_FILES := hello.c
LOCAL_C_INCLUDES := $(LOCAL_PATH)/include
LOCAL_SHARED_LIBRARIES := libcutils
include $(BUILD_SHARED_LIBRARY)
你可能会问:这跟Makefile长得不一样啊?
其实,LOCAL_XXX都是变量赋值。include $(CLEAR_VARS)是清空这些变量。include $(BUILD_SHARED_LIBRARY)是引入一个「模板Makefile」,它会把LOCAL_XXX变量转换成真正的Make规则。
说白了,Android.mk就是「用变量来配置,用include来组装」的Makefile。
Make本质:Android.mk中的每一行,最终都会被展开成标准的Make规则。LOCAL_MODULE变成目标,LOCAL_SRC_FILES变成依赖,LOCAL_CFLAGS变成编译选项。仅此而已。
3.8 知识体系图
下面这张图,帮你理清Make的核心概念和它们之间的关系:
3.9 实战:从Make到Android.mk的转换
假设我们有一个简单的C项目:
# 普通Makefile
CC = gcc
CFLAGS = -I./include
hello: hello.c utils.c
$(CC) $(CFLAGS) -o hello hello.c utils.c
转换成Android.mk就是:
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := hello
LOCAL_SRC_FILES := hello.c utils.c
LOCAL_C_INCLUDES := $(LOCAL_PATH)/include
include $(BUILD_EXECUTABLE)
你看,LOCAL_MODULE对应目标,LOCAL_SRC_FILES对应依赖,LOCAL_C_INCLUDES对应编译选项。本质完全一样,只是换了个写法。
我的建议:如果你能写Makefile,Android.mk就是换个语法的事。别被那些LOCAL_XXX吓到,它们只是变量而已。
3.10 避坑指南
最后,分享几个我在AOSP里踩过的坑:
- 空格 vs Tab:Makefile的命令行必须以Tab开头。我见过有人用4个空格代替,结果编译报错。这问题排查起来特别隐蔽。
- 变量展开时机:用
=定义变量是延迟展开,用:=是立即展开。在AOSP里,大部分LOCAL变量用:=更安全。 - 伪目标一定要加.PHONY:否则如果目录下有同名文件,Make会跳过执行。我因为这个,有一次clean操作没生效,导致增量编译一直失败。
- include顺序:Android.mk里
include $(CLEAR_VARS)必须在最前面,否则上次的LOCAL变量会污染当前模块。
嗯,Make的基础就这些。掌握了这些,你再看AOSP里的Android.mk,就不会觉得神秘了。它们本质上就是一堆精心组织的Make规则,用变量和函数组装起来的构建系统。
公众号:蓝海资料掘金营,微信deep3321