GNU Make 是一款自动化构建工具,它能够根据源代码智能生成可执行文件及其他衍生文件。

构建规则

Make 的构建规则由 Makefile 文件定义,该文件详细规定了每个目标文件的生成逻辑及依赖关系。在软件开发过程中,编写规范的 Makefile 是必不可少的,这样才能充分发挥 Make 的自动化构建优势,实现高效的程序编译与部署。

描述方式

Makefile 的依赖关系描述方式如下所示。

Target: Prerequisites
    Command
  • Target:规则的目标,可以是文件或标签。
  • Prerequisites:依赖条件,可以有多个或没有。
  • Command:达成目标的命令。

在 Makefile 中,制表符与空格不可以随意互换,Command 前一定要是制表符。

以下示例指示编译目标 main 时,先编译目标 main.oabc.o,然后再将两个目标文件链接到目标 main

main: main.o abc.o
    gcc -o main main.o abc.o

工作流程

Make 默认以 Makefile 中定义的第一个目标作为终极构建目标。在执行目标时 Make 会递归处理该目标的所有依赖项,形成自底向上的构建链。对于每个目标文件,Make 根据以下条件判断是否需要重建:

  1. 📌 缺失触发构建:当目标文件不存在时,自动执行命令进行创建。
  2. 🔄 更新检测机制:若目标文件存在,但任意依赖文件的修改时间比目标文件新(即依赖项有更新),则触发重新构建。
  3. ✅ 构建跳过原则:若目标文件已存在且所有依赖项均未更新,则视为最新状态,跳过构建。

伪目标

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.sqldml.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.txtdml.txt 两个文件,则执行 make 命令后,会生成 ddl.sqldml.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 中自定义此规则来覆盖内置规则。