2. Makefile基础语法:目标、依赖、命令、注释、变量赋值

说实话,很多人学Makefile卡在第一关,就是被这些基础语法给唬住了。其实说白了,Makefile就是一套规则——告诉编译器「先做什么、后做什么、用什么做」。我当年刚接触时也觉得这玩意儿玄乎,后来发现,你只要搞懂五个核心要素,剩下的都是锦上添花。

2.1 目标(Target)——你要造什么

目标就是Makefile的「终点站」。你敲make clean,clean就是目标;你敲make all,all就是目标。每个目标告诉make:「嘿,我要生成这个文件,或者执行这个动作。」

# 最简单的目标
hello: hello.c
    gcc -o hello hello.c

这里hello就是目标文件。我个人习惯把第一个目标命名为all,这样直接敲make就能构建整个项目。

小技巧: 目标名不要和文件名冲突。如果你有个文件叫clean,那make clean就会说「已经是最新了」,啥也不干。我踩过这个坑,后来都用.PHONY声明伪目标。

2.2 依赖(Prerequisites)——你需要什么

依赖就是目标的「原材料」。目标能不能造出来,得看依赖是不是准备好了。Makefile会递归检查依赖——如果依赖比目标新,那就重新生成目标。

# 目标: 依赖
main.o: main.c header.h
    gcc -c main.c

这里main.o依赖main.c和header.h。只要这两个文件任何一个变了,main.o就得重新编译。嗯,这就是增量编译的核心逻辑。

核心思想: 依赖关系决定了构建的「最短路径」。你改了一个头文件,make只会重新编译依赖它的源文件,不会全量编译。这就是Makefile比shell脚本高明的地方。

2.3 命令(Commands)——怎么做

命令就是具体的执行步骤。注意,命令前面必须是一个Tab键,不能是空格。我见过太多新手在这里翻车——复制粘贴代码时Tab变成了空格,然后make报错「missing separator」。

target: dependency
    @echo "正在编译..."   # @符号表示不打印命令本身
    gcc -o target dependency

命令可以有多行,每行都会在一个独立的shell中执行。如果你想在同一行shell里执行多条命令,用分号隔开:

target:
    cd build; make; echo "完成"
注意: 每行命令都是独立的shell进程。所以cd命令只对当前行有效。我曾经写过一个Makefile,在cd src后直接gcc,结果gcc还是在原目录执行——因为cd的效果在下一行就没了。

2.4 注释(Comments)——给自己留个记号

Makefile的注释用#开头。我建议在复杂逻辑前加注释,比如「这个目标用来清理临时文件」「这个变量控制调试开关」。三个月后你回来看代码,会感谢现在的自己。

# 这是注释
CC = gcc  # 编译器
CFLAGS = -Wall -O2  # 编译选项

2.5 变量赋值——四种方式,各有千秋

变量是Makefile的灵魂。我见过有人把同一个路径写了十几遍,改一次目录要改所有地方——这就是没用好变量。Makefile有四种赋值方式,我一个个说。

2.5.1 = 递归展开赋值

这是最常用的赋值方式。变量在使用时才展开,不是定义时展开。什么意思呢?看个例子:

A = $(B)
B = hello
all:
    @echo $(A)  # 输出 hello

这里A定义时B还没定义,但make不报错。等到echo $(A)时,make才去查找B的值。这种「延迟求值」的特性,在某些场景下很方便,但也容易造成循环引用。

避坑指南: 我曾经写过一个Makefile,A = $(B),B = $(A),结果make直接报错「递归变量引用」。所以用=时,注意别搞成死循环。

2.5.2 := 简单赋值

:=是立即展开赋值。变量在定义时就计算好值,后面再改依赖变量也没用。

B = hello
A := $(B)
B = world
all:
    @echo $(A)  # 输出 hello,不是 world

我个人习惯用:=,因为行为更可预测。你想想看,如果变量值在定义时就固定了,调试时就不用猜「这个变量到底什么时候展开」。

2.5.3 ?= 条件赋值

?=的意思是「如果变量没定义过,才赋值」。如果已经定义过了,就忽略。

CC ?= gcc
CC = clang
all:
    @echo $(CC)  # 输出 clang,因为 CC 已经定义过了

这个在写通用Makefile时特别有用。用户可以在命令行覆盖默认值:make CC=clang,如果用户没指定,就用默认的gcc。

2.5.4 += 追加赋值

+=就是在原有值后面追加内容。注意,它会自动加一个空格分隔。

CFLAGS = -Wall
CFLAGS += -O2
all:
    @echo $(CFLAGS)  # 输出 -Wall -O2

这个在分模块定义编译选项时很好用。比如基础选项放在一个地方,特定模块的选项再追加。

2.6 知识体系总览

下面这张图把Makefile基础语法的核心逻辑串起来了。你看一眼,心里就有谱了。

Makefile 基础语法 目标 (Target) 要生成的文件或动作 依赖 (Prerequisites) 构建所需的原材料 命令 (Commands) Tab开头,执行步骤 注释 (#) 代码说明,方便维护 变量赋值 = := ?= += 核心逻辑:目标依赖依赖,依赖触发命令,命令生成目标

2.7 实战建议

学完这些基础语法,我建议你马上动手写一个简单的Makefile。不用复杂,就编译一个hello.c,然后试试不同的变量赋值方式。我在项目中见过太多人「眼高手低」——看文档觉得都会了,一写就报错。

我的习惯: 写Makefile时,先用:=定义所有路径和工具链,用+=追加编译选项,用?=给用户留个覆盖入口。这样结构清晰,别人接手也容易看懂。

嗯,基础语法就这些。你把这些搞明白了,Makefile就算入门了。下一节我们会把这些语法串起来,写一个真正能用的工程化Makefile。