IMPORTANT

此文档仍需调整补充。

Make 是来源于 GNU 自由软件项目的一个通用型构建工具,是 GNU C/C++ 工具链的一部分。

构建是将多个源代码文件进行预先设定的处理并打包为最终需要的可运行程序或二进制库的过程,构建是对于一整个项目而言的,可以减少使用编译器指令逐个手动处理源文件的工作量;Make 通过 Makefile 文件了解项目该如何编译与链接。

Info

Make 通过一系列变量、条件判断和目标依赖文件的修改时间,决定该运行哪些目标,看起来像是 Shell 的强化版本;但和 Shell 又明显区别:Make 能够主动根据目标(文件)和依赖(文件)的时间戳决定是否运行它。

Make 构建文件可使用这2个文件名:makefile, Makefile,若有多个文件,前者优先级更高;

Makefile 中的符号用法概念性内容较为繁杂且较难理解,应多结合实例进行学习。

指令用法

运行以下指令,以使用工作目录下的一个 makefileMakefile 文件开始构建

make [target=all ...] [args=value ...]

选项

构建目标 紧随 make 之后给出的一个或多个在 Makefile 中存在的目标,可以有多个;

构建参数 设置在 makefile 中定义的变量的值,优先级最高,会覆盖任何变量,可以有多个,按 <变量名>=<值> 的方式编写;

-n | --dry-run 仅显示待运行的指令,而不是实际运行它们。这在检查 makefile 的编写是否有问题时很实用;

-j <数量> 使用指定数量线程并行构建。

Info

在没有提供任何构建目标时,make 会从头查找第1个可运行的目标(包括伪目标),如果存在依赖,再沿着依赖路径逐个运行。

Makefile 的基本结构

VARIABLE = Good
 
program: main.c config.c
	gcc main.c config.c -o program
	# (more...)
# 或者
.PHONY: clean
clean:
	rm program

VARIABLE 是 Makefile 中的[变量↗](# 变量),像其他程序设计语言那样,它也使用 = 为变量赋值;这里 VARIABLE 的值是字符串 Good

program 是要构建的目标,Make 会默认把它当成目标文件,program 相当于一个文件路径;目标可以有多个,这样这些目标也可以运行一系列相同操作。

一个目标也可以没有任何依赖。有一种特殊的无依赖目标被称为伪目标 (phony target),伪目标不关联任何文件,在可运行时一定会运行。添加 .PHONY: <TARGET> 到目标之前以声明伪目标:

.PHONY: target
target:
	echo "Perfect!"

在目标后面是构建出此目标所需的数个依赖 (prerequisite),依赖可以是文件,也可以是其他目标,用于嵌套运行;如果 Make 发现目标文件比依赖文件更旧,Make 会运行目标提供的指令。

如果有些依赖更新了,但是并不会使目标文件迟于更新,应该在这些依赖的左侧添加竖线 |。竖线左侧仍是普通依赖,竖线右侧是 order-only 唯一有序依赖,Make 不检查这种依赖是否更新。

在后续带有制表符缩进的多行是为通过依赖得到目标文件或完成目标的一系列指令,被称为配方 (recipe)

目标、依赖(如果有)和配方(如果有)共同组成了一条规则 (rule)。依赖和配方必须至少有其中一个,只有目标的规则没有作用。Make 总是会先尝试检查并运行依赖(如果有),然后逐行运行目标的指令。

若依赖或配方太长而需要跨行,可在行尾添加一个反斜线 \,然后在新行编写。

通常将不必使用配方的作为最终构建目标,将没有依赖的目标作为伪目标,然后逐层细分到后续事务。请看下例:

all: test_debug test_release
debug: test_debug
release: test_release
 
test_release: $(wildcard *.cpp)
	g++ $^ -o $@ -O3
test_debug: $(wildcard *.cpp)
	g++ $^ -o $@ -g -O0
 
.PHONY: clean
clean:
	rm -f test_release test_debug

现在不必关注指令行中的特殊符号。若运行 makemake all,Make 会先运行最低层目标 test_releasetest_debug 提供的指令,然后完成 all 提供的指令(但这里没有写)。

也可以指定第2行和第3行的目标以完成特定的构建任务,在本例中将 test_debugtest_release 作为构建目标也可以。

如果希望清理构建文件,可运行 make clean

WARNING

正常情况下,指令行/配方行必须使用制表符缩进,而不能是空格;

若文件路径含有目录,需要先建立,Make 不会自动创建目录;

若一个目标并不对应一个待生成的文件,应将其注释为 .PHONY,防止后续在 makefile 目录下意外地创建一个同名文件而导致这个目标的行为不确定。

Makefile 元素

变量

Make 允许自定义一些变量并设置它的值为字符串,可以使用数字、字母、下划线、短横线及 Shell 支持的符号构成变量名,但建议使用大写字母+下划线的形式。

# ✅ Recommend
CC := gcc
CFLAGS_DEBUG := -g -O0
CFLAGS_RELEASE := -O3
TARGET := games

WARNING

若没有必要,不要在变量赋值语句后添加空格或制表符等不可见字符,这会被 Make 视为待赋值字符串的一部分。

自动变量

Make 提供一些自动变量以简化指令书写:

  • $@ 构建目标:代表上方的 TARGET
  • $< 第一个依赖文件:代表上方的 DEPT.c
  • $^ 所有依赖文件,代表 TARGET 之后的空格分隔出的全部文件
  • [仅在依赖处使用模式匹配时可用] $* 代表 % 处的内容

赋值符号

Make 根据需要提供并扩充了一般的运算符。

  • = 一般赋值符,被赋值变量是字符串或另一个已存在变量;只有在使用到变量时 Make 才会查找并递归展开;也就是说如果这个变量的值在后续发生改变,会影响到引用此变量的后续其他位置的值;
  • := 简单赋值符,与一般赋值功能上一样,但是它在赋值时就会查找并递归展开;这可以避免一般赋值符在赋值时和使用时变量值不一致而降低可读性和可理解性的问题;(建议使用,因为这符合对变量赋值一般意义上的理解,保证可阅读性)
  • ?= 条件赋值符(初始化运算符),仅在变量此前未被赋值时才会在此处对其赋值;非常适用于希望从终端获取构建参数,并在未提供参数时设置默认值; [GNU Make] 条件赋值符在完成赋值后,它的行为和一般赋值符一样;
  • += 追加赋值符,追加时会在原字符串末尾先添加空格;
  • != [仅GNU Make etc.] Shell 结果赋值符,右操作数应为一段 Shell 指令,赋值前它会立刻运行,并将运行结果字符串立刻赋值给变量。

赋值优先级

优先级从高到低排序,优先级高的会覆盖同名标识符的低优先级值。

  1. 通过 make 指令传递的参数;
  2. Makefile 定义;
  3. 当前 Shell 的环境变量 PATH
  4. 自动变量;
  5. 预定义变量 CC, CFLAGS, LIBS

Info

要防止运行参数覆盖 Makefile 中的变量,需要在变量标识符之前设置 override

通配与匹配符

  • * 通配符,表示的含义就是任意,可以使用 *.c 表示目录下所有以 .c 为扩展名的文件;被通配符匹配到的文件一定是已存在的,被匹配后实际上是一列已存在文件名的字符串,文件名之间以空格分隔。此项也适用于 Shell;

  • ? 单字符通配符,表示任意一个字符,被匹配后实际上是一列已存在文件名的字符串,文件名之间以空格分隔。此项也适用于 Shell;

    WARNING

    避免在目标和依赖处使用 * 匹配所有期望的文件;若没有符合的文件,会将带 * 的整个字符串作为字面值对待。

    例如若匹配 *.exe 文件,若当前目录下没有 .exe 文件,那么就会被当作字面意义上的 *.exe 对待。

    建议使用内置函数 $(wildcard <带通配符的文件名形式>) 避免此问题。

  • % 模式匹配符,常成对使用,主要作用就是使一组文件的名称不变的情况下逐个对应另一个不同的扩展名(例如 %.png -> %.jpg);常将一对模式分别作为目标和依赖进行文件类型转换; 具备模式的规则,在运行时 Make 将逐个文件名附加到变量 $^$< 上并循环运行规则内的指令,而不是全部文件一同附加。

    debug/%.o: %.cpp
    	g++ -c $< -o $@ -g -O0
    #           ^ 此处推荐使用 $<,如果增加依赖可避免后续产生错误。
    #           | 除非一条指令中要同时使用多个依赖,否则使用 $<。

    若在含有多个 .cpp 的目录下运行此规则,可能的输出是:

    g++ -c test.cpp -o debug/test.o -g -O0
    g++ -c test_2.cpp -o debug/test_2.o -g -O0

Info

在 Makefile 中,有时候需要针对 Shell 转义一些符号,需要在符号前添加反斜线 \ 转义;

注意$ 需要特殊对待,要使用连续2个 $ 以使 Make 将其视为1个普通字符;如果需要避免 Shell 解析它,还需要在最前方添加反斜线 \

Shell 也会解析 *,使用 \* 以直接在 Shell 输出星号。

特殊变量

具有特定用途的变量,以点号开头。

  • .RECIPEPREFIX 设置指令行的起始前缀,默认值是制表符 \t; 示例:

    .RECIPEPREFIX = >
    all:
    > echo Hello, world

条件判断

使用条件判断语句可以决定 Make 该运行哪些规则。

可以在 Makefile 中使用这些条件判断关键字:

  • 条件判断的开始,可以嵌套使用 ifeq, ifneq(值判断)ifdef, ifndef(变量存在性判断)
  • 在一个条件判断内使用,作为条件判断多分支 else (ifeq ...)
  • 上一个条件判断起始的结束标记 endif

WARNING

不能在指令(规则)行中使用 Make 提供的判断关键字,因为 Make 本身不运行指令行,指令行的内容最终会交给 Shell 运行。

函数

Make 提供了一些函数以支持更灵活简洁的 Makefile。

Info

标注了 ... 的参数表示该参数支持按空格分隔的单词逐个查找。

函数的两种基本形式为:

$(<function> <arguments ...>)
${<function> <arguments ...>}

函数可能有多个参数,使用 , 以分隔。

字符串替换

<text> 中找到的 <from> 替换为 <to>。通配符在此处无效。

$(subst <from>,<to>,<text>)

模式替换

<text> 中找到的每个以空格分隔的且符合 <pattern> 的单词替换为 <replacement>。可使用 %。常用于将一组同扩展名文件转换为其他扩展名。

$(patsubst <pattern>,<replacement>,<text>)

# 示例
SOURCE := main.cpp control.cpp window.cpp
OBJECT := $(patsubst %.cpp,%.o,$(SOURCE))
# OBJECT -> main.o control.o window.o

简易方法

$(SOURCE:.cpp=.o)

清理空格

<string> 中开头和末尾的空格移除,且将字符串中间的空格设置为最多1个。

$(strip <string>)

查找单词

<text> 中查找符合 <pattern> 的单词;单词间以空格分隔,分别检查。

$(filter <pattern ...>,<text>)

反向查找版本,查找不符合 <pattern> 的单词。

$(filter-out <pattern ...>,<text>)

查找字符串

在字符串 <string> 查找 <sub>,如果找到会返回 <sub>,未找到返回空字符串。

$(findstring <sub>,<string>)

获取多个文件

按照 <pattern> 的规则找到指定路径下的全部文件并逐个存储为单词,文件名之间以空格分隔。需要结合通配符 * 使用。

$(wildcard <pattern>)

提取为目录

<path> 中给出的路径末尾的文件名去除,并保留到最后一个 /

$(dir <path ...>)

# 示例
PATH = /etc/sample source/image-1.png
DIR = $(dir $(PATH))
# DIR -> /etc/ source/

提取为文件名

<path> 中给出的路径的目录索引部分去除,只留下文件名。

$(notdir <path ...>)

# 示例
PATH = /etc/sample source/image-1.png
FILE = $(dir $(PATH))
# FILE -> sample image-1.png

提取为扩展名

提取 <name> 中给出的路径或文件名的扩展名部分,含 .,无扩展名的会提取为空。

$(suffix <name ...>)

提取为文件名称

<name> 中给出的路径或文件名的扩展名部分(最后一个 . 及其之后)去除,无扩展名的不会改变。

$(basename <name ...>)

添加前缀或后缀

<name> 中给出的单词添加确定的前后缀。例如可以为其增添目录或扩展名。

$(addprefix <prefix>,<name ...>)
$(addsuffix <suffix>,<name ...>)

判断是否为空

判断 <condition> 是否为空字符串,如果非空就返回 <if-true>,为空就返回 <if-else>

$(if <condition>,<if-true>[,<if-else>])

检查变量来源

检查一个变量值的来源。

$(origin <variable>)

可能的返回值:

  • undefined 未定义
  • default Make 内置的默认定义(如 CC
  • environment 来自环境变量
  • environment override 来自覆盖环境变量的命令行选项(make -e
  • file 在 Makefile 中定义
  • command line 来自命令行参数(如 make CFLAGS=-O2
  • overrideoverride 指令重新定义
  • automatic 自动化变量(如 $@, $<

Make 如何判断目标是否需要运行?

Make 判断构建目标路径上,每个目标的以下几项信息以决定是否需要运行它:

  • 目标为非文件?(标注 .PHONY) ✅ 始终会运行此目标或继续检查普通依赖;

    ❌ 检查下一项;

  • 若有依赖,目标比普通依赖旧?(或无依赖,目标文件不存在?)*唯一有序依赖不参与此目标的检查。但 Make 仍会将他作为目标,检查它的依赖。 ✅ 运行此目标或继续检查依赖;

    ❌ 目标已是最新,Make 不会运行此目标。

如果目标需要运行,但此目标没有配方,且依赖都已最新(如果有普通依赖),Make 会提示:

make: Nothing to be done for 'target'.

如果 Make 认为目标是最新的,Make 会提示:

make: 'target' is up to date.

示例 Makefile

一般赋值和简单赋值

NAME = application
TARGET = $(NAME)_debug
TARGET_EASY := $(NAME)_debug
NAME = game
 
.PHONY: default
default:
	echo "NAME = $(NAME)" >> /dev/null
	echo "TARGET = $(TARGET)" >> /dev/null
	echo "TARGET_EASY = $(TARGET_EASY)" >> /dev/null

运行结果

echo "NAME = game" >> /dev/null
echo "TARGET = game_debug" >> /dev/null
echo "TARGET_EASY = application_debug" >> /dev/null

运行时机

以下是一个完整的基本 Makefile。

override PROJECT := test_app
TARGET ?= all
SOURCE := $(wildcard *.cpp)
OBJECT_DEBUG := $(patsubst %.cpp,debug/%.o,$(SOURCE))
OBJECT_RELEASE := $(patsubst %.cpp,release/%.o,$(SOURCE))
 
ifeq ($(TARGET), all)
.PHONY: all
all: app_debug app_release
else ifeq ($(TARGET), debug)
.PHONY: go-to
go-to: app_debug
else ifeq ($(TARGET), release)
.PHONY: go-to
go-to: app_release
else
.PHONY: bad-target
bad-target:
	echo "Invalid target"
endif
 
app_debug: $(PROJECT)_debug
app_release: $(PROJECT)_release
 
$(PROJECT)_debug: $(OBJECT_DEBUG)
	g++ $^ -o $(PROJECT)_debug
 
$(PROJECT)_release: $(OBJECT_RELEASE)
	g++ $^ -o $(PROJECT)_release
 
debug/%.o: %.cpp | debug
	g++ -c $< -o $@ -g -O0
 
release/%.o: %.cpp | release
	g++ -c $< -o $@ -O3
 
debug release:
	mkdir -p $@
 
.PHONY: clean
clean:
	rm -f debug/* release/* $(PROJECT)_debug $(PROJECT)_release