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就得重新编译。嗯,这就是增量编译的核心逻辑。
2.3 命令(Commands)——怎么做
命令就是具体的执行步骤。注意,命令前面必须是一个Tab键,不能是空格。我见过太多新手在这里翻车——复制粘贴代码时Tab变成了空格,然后make报错「missing separator」。
target: dependency
@echo "正在编译..." # @符号表示不打印命令本身
gcc -o target dependency
命令可以有多行,每行都会在一个独立的shell中执行。如果你想在同一行shell里执行多条命令,用分号隔开:
target:
cd build; make; echo "完成"
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的值。这种「延迟求值」的特性,在某些场景下很方便,但也容易造成循环引用。
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基础语法的核心逻辑串起来了。你看一眼,心里就有谱了。
2.7 实战建议
学完这些基础语法,我建议你马上动手写一个简单的Makefile。不用复杂,就编译一个hello.c,然后试试不同的变量赋值方式。我在项目中见过太多人「眼高手低」——看文档觉得都会了,一写就报错。
:=定义所有路径和工具链,用+=追加编译选项,用?=给用户留个覆盖入口。这样结构清晰,别人接手也容易看懂。
嗯,基础语法就这些。你把这些搞明白了,Makefile就算入门了。下一节我们会把这些语法串起来,写一个真正能用的工程化Makefile。