Makefile 快速入门指南 P1
GNU Make 是一款自动化构建工具,它能够根据源代码智能生成可执行文件及其他衍生文件。
构建规则
Make 的构建规则由 Makefile 文件定义,该文件详细规定了每个目标文件的生成逻辑及依赖关系。在软件开发过程中,编写规范的 Makefile 是必不可少的,这样才能充分发挥 Make 的自动化构建优势,实现高效的程序编译与部署。
描述方式
Makefile 的依赖关系描述方式如下所示。
Target: Prerequisites
Command
Target
:规则的目标,可以是文件或标签。Prerequisites
:依赖条件,可以有多个或没有。Command
:达成目标的命令。
在 Makefile 中,制表符与空格不可以随意互换,Command 前一定要是制表符。
以下示例指示编译目标 main
时,先编译目标 main.o
和 abc.o
,然后再将两个目标文件链接到目标 main
。
main: main.o abc.o
gcc -o main main.o abc.o
工作流程
Make 默认以 Makefile 中定义的第一个目标作为终极构建目标。在执行目标时 Make 会递归处理该目标的所有依赖项,形成自底向上的构建链。对于每个目标文件,Make 根据以下条件判断是否需要重建:
- 📌 缺失触发构建:当目标文件不存在时,自动执行命令进行创建。
- 🔄 更新检测机制:若目标文件存在,但任意依赖文件的修改时间比目标文件新(即依赖项有更新),则触发重新构建。
- ✅ 构建跳过原则:若目标文件已存在且所有依赖项均未更新,则视为最新状态,跳过构建。
伪目标
Make 默认所有构建目标都是文件,但我们需要一类特殊的构建目标即伪目标,其不应与文件对应,而是用来执行一些特定的操作。为了避免与文件同名的伪目标被误认为是文件,可以在目标前加上 .PHONY
声明。
.PHONY: clean
clean:
rm -f *.o
搜索目录
Make 默认只在 Makefile 所在的目录中搜索文件,但有时我们需要的文件存在于不同的目录中,此时可以使用 VPATH
变量或 vpath
指令附加搜索目录。
即使添加了搜索路径,Make 仍然会优先在 Makefile 所在的目录中查找所需的文件,无法找到时才会在附加搜索目录中依次寻找。
VAPTH 变量
VPATH
变量指定了 Makefile 搜索文件的路径,搜索路径按空格或冒号分隔,按书写顺序搜索。
VPATH = src include
vpath 指令
vpath
指令可以为不同类型的文件添加不同的搜索路径。
vpath Pattern Directories # 为符合 Pattern 的文件添加搜索路径
vpath Pattern # 清除 Pattern 的搜索路径
vpath # 清除所有搜索路径
Pattern
:搜索条件,可以包含模式字符%
来匹配一个或多个字符,如%.o
、%.c
等,如果没有包含模式字符,则表示匹配具体的文件名。Directories
:搜索路径,多个路径之间使用空格或冒号分隔,按书写顺序搜索。
vpath %.h include # 为 .h 文件添加 include 目录作为搜索路径
vpath %.h # 清除 .h 文件的搜索路径
vpath # 清除所有搜索路径
变量
在 Makefile 中,内建的变量有默认变量、环境变量和自动变量,我们也可以覆盖内建的变量或自定义变量。
自定义变量
变量的定义语法如下所示,我们可以通过特定的语法规则来定义或覆盖变量。
# 变量名 = 变量值
id = a1
mid = $(id) # 引用变量为最后一次赋的值,mid = a2
id = a2
# 变量名 := 变量值
id = a1
mid := $(id) # 引用变量为当前位置时的值,mid = a1
id = a2
# 变量名 ?= 变量值
id ?= a1 # 如果变量未定义,则赋值
# 变量名 += 变量值
id = a1
id += a2 # 追加变量值,id = a1 a2
预定义变量
Make 预定义了很多变量,这些变量可以通过环境变量、命令行参数或 Makefile 中的变量定义语句进行覆盖。可以通过 make -p
命令查看所有预定义的变量及规则,如果当前目录存在 Makefile 文件,则其会列出解析后的变量及规则。
默认变量
默认变量可以通过 make -p
打印并通过关注 default
注释来查看。
# default
ARFLAGS = -rv
# default
AS = as
# default
AR = ar
# default
CC = cc
# default
CPP = $(CC) -E
......
环境变量
Makefile 中可以使用系统的环境变量,也可以通过命令行参数定义或覆盖环境变量,如果环境变量与默认变量名称相同,则默认变量会被覆盖。
以下示例在 Makefile 中引用环境变量并打印出来。
.PHONY: test
test:
echo $(JAVA_HOME)
PS> $env:JAVA_HOME = '/usr/lib/jvm/java-23-openjdk-amd64' # 定义环境变量
PS> make test
/usr/lib/jvm/java-23-openjdk-amd64
通过命令行参数设置环境变量,如果 JAVA_HOME
变量已被系统环境变量设置,则覆盖系统环境变量,否则定义此变量。
PS> make test JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64
/usr/lib/jvm/java-17-openjdk-amd64
自动变量
自动变量的作用域在当前规则内,其值会根据规则的依赖关系动态变化。
$@
:目标文件$<
:第一个依赖文件$^
:所有依赖文件,去重(以空格分隔)$+
:所有依赖文件,不去重(以空格分隔)$?
:所有比目标文件新的依赖文件(以空格分隔)$*
:目标模式中%
及其之前的部分(在 GNU Make 中,如果目标中没有模式的定义且文件后缀是其所能识别的,则此变量为去除后缀的文件名,否则此变量为空值。例如目标为main.c
,其中.c
可识别,则此变量为main
)
以下示例将 ddl.sql
和 dml.sql
文件合并到 new.sql
文件中。
new.sql: ddl.sql dml.sql
cat $^ > $@ # cat ddl.sql dml.sql > new.sql
变量的传递
在复杂的项目中,递归 Make 很常见,子 Makefile 可能会用到父 Makefile 中的变量,此时需要使用 export
关键字将变量传递给子 Makefile。
# 父 Makefile
FOO1 := 123
export FOO2 := 456
all:
echo $(FOO1) # 123
echo $(FOO2) # 456
$(MAKE) -C sub
# 子 Makefile
all:
echo "FOO1 = $(FOO1)" # FOO1 =
echo "FOO2 = $(FOO2)" # FOO2 = 456
模式规则
Makefile 中可以使用模式字符 %
来匹配任意非空字符串,将此应用到规则中则可以匹配多个文件并执行相应的操作。
以下示例假设当前目录中只有 ddl.txt
和 dml.txt
两个文件,则执行 make
命令后,会生成 ddl.sql
和 dml.sql
两个文件并合并为 new.sql
文件。
new.sql: ddl.sql dml.sql
cat $^ > $@
%.sql: %.txt
cp $< $@
PS > make
cp ddl.txt ddl.sql
cp dml.txt dml.sql
cat ddl.sql dml.sql > new.sql
Make 也提供了一些内置的模式规则,例如以下规则会将匹配到的 .c
文件编译为 .o
文件,可以通过 make -p
打印。
%.o: %.c
# recipe to execute (built-in):
$(COMPILE.c) $(OUTPUT_OPTION) $<
当然,我们也可以通过在 Makefile 中自定义此规则来覆盖内置规则。