3. Makefile 基础:基本语法、变量、规则、函数

说到 Android 构建系统,Makefile 是绕不开的根基。虽然现在 Android 已经用 Soong(Blueprint + Ninja)替代了传统的 Android.mk,但理解 Makefile 依然是读懂构建流程的关键。我自己刚接触 Android 源码时,第一反应就是——这 Makefile 怎么这么长?后来才发现,核心语法其实就那么几条。

3.1 规则:Makefile 的灵魂

Makefile 最核心的东西就是规则。说白了,就是告诉 make 命令:你要生成什么文件,依赖哪些文件,怎么生成。

目标文件: 依赖文件列表
    命令

举个例子,我经常用这个来演示:

hello.o: hello.c hello.h
    gcc -c hello.c -o hello.o

这里 hello.o 是目标,hello.chello.h 是依赖,下面缩进的那行就是命令。注意,命令前面必须是一个 Tab 键,不能用空格。嗯,这个坑我踩过不止一次。

⚠️ 注意: 命令前的缩进必须是 Tab,不是空格。很多编辑器默认把 Tab 转成空格,写 Makefile 时记得关掉这个功能。

3.2 变量:别重复自己

写 Makefile 最怕什么?重复。一个项目里源文件几十上百个,总不能一个个写吧?变量就是用来解决这个问题的。

CC = gcc
CFLAGS = -Wall -O2
SRCS = main.c utils.c network.c
OBJS = $(SRCS:.c=.o)

$(OBJS): %.o: %.c
    $(CC) $(CFLAGS) -c $< -o $@

这里我用了几个变量:CC 指定编译器,CFLAGS 是编译选项,SRCS 是源文件列表。最妙的是 $(SRCS:.c=.o) 这种替换写法——把 .c 后缀全换成 .o,省事吧?

变量引用用 $()${},我个人习惯用 $(),看着更清晰。还有几个自动变量特别常用:

变量 含义 例子
$@ 目标文件名 上面例子中就是 hello.o
$< 第一个依赖文件 上面例子中就是 hello.c
$^ 所有依赖文件 用空格分隔的完整列表
$? 比目标新的依赖 常用于增量编译
💡 小技巧: 调试 Makefile 时,可以用 $(info ...) 打印变量值。比如 $(info OBJS = $(OBJS)),比猜变量值靠谱多了。

3.3 函数:让 Makefile 活起来

Makefile 里的函数,说白了就是一些内置的字符串处理工具。我刚开始觉得没啥用,后来发现没有它们还真不行。

最常用的几个:

  • wildcard:匹配文件。比如 $(wildcard src/*.c) 能拿到 src 目录下所有 .c 文件。
  • patsubst:模式替换。比如 $(patsubst %.c,%.o,$(SRCS)),跟前面那个 $(SRCS:.c=.o) 效果一样。
  • foreach:循环处理。比如遍历多个目录收集源文件。
  • shell:执行 shell 命令。比如 $(shell find . -name "*.c")

看个实际例子:

SRC_DIRS = src lib test
SRCS = $(foreach dir,$(SRC_DIRS),$(wildcard $(dir)/*.c))
OBJS = $(patsubst %.c,%.o,$(SRCS))

all: app

app: $(OBJS)
    $(CC) $^ -o $@

%.o: %.c
    $(CC) $(CFLAGS) -c $< -o $@

这段代码里,foreach 遍历三个目录,wildcard 在每个目录里找 .c 文件,patsubst 把 .c 换成 .o。三行代码搞定几十个文件的编译规则,是不是很爽?

3.4 伪目标:别跟文件重名了

你有没有遇到过这种情况:写了个 clean 目标,结果目录里恰好有个叫 clean 的文件,然后 make 就说「目标已是最新」?

这就是伪目标要解决的问题。用 .PHONY 声明一下,make 就知道这个目标不是文件:

.PHONY: clean all

all: app

clean:
    rm -f *.o app

我曾经在一个项目里,因为没加 .PHONY,导致 make clean 怎么都删不掉文件。查了半天才发现目录里有个叫 clean 的空文件。从那以后,我所有非文件目标都会加上 .PHONY

3.5 条件判断:让构建更智能

有时候需要根据条件决定编译哪些文件,或者用不同的编译选项。Makefile 支持条件判断:

ifeq ($(DEBUG), 1)
    CFLAGS += -g -O0
else
    CFLAGS += -O2
endif

ifeq ($(ARCH), arm)
    CC = arm-linux-gcc
else
    CC = gcc
endif

条件判断的语法有 ifeqifneqifdefifndef 四种。我个人最常用的是 ifeqifdef,简单直观。

🔑 核心要点: Makefile 的本质就是「规则 + 变量 + 函数」。规则告诉 make 做什么,变量避免重复,函数处理复杂逻辑。掌握了这三样,大部分 Makefile 你都能看懂。

3.6 知识体系总览

下面这张图总结了 Makefile 的核心知识结构,你可以对照着回顾一下:

Makefile 核心 规则 (Rules) 目标 : 依赖 命令 (Tab缩进) 变量 (Variables) $(VAR) 引用 自动变量 $@ $< 函数 (Functions) wildcard / patsubst foreach / shell 条件 & 伪目标 ifeq / ifdef .PHONY 声明 规则是骨架,变量是血肉,函数是工具

这张图把 Makefile 的核心要素都串起来了。规则是骨架,变量是血肉,函数是工具,条件判断和伪目标是辅助。你在 Android 源码里看到的 Android.mk,本质上就是这些东西的组合。

好了,Makefile 基础就讲到这里。下一章我们会把这些知识用到 Android 构建系统中,看看 Android.mk 到底是怎么写的。


公众号:蓝海资料掘金营,微信deep3321