Makefile 简明指南

Makefile 简明指南

一. Makefile变量

在 Makefile 里,你可以通过不同方式来声明新变量 ne。在 Makefile 里,变量声明时等号(=:=+=?=)两边的空格是可选的,加空格或者不加空格都不会影响变量的赋值。下面是常见的几种声明方式及其特点:

1. 递归展开变量赋值(=

在 Makefile 里,递归展开变量是借助 = 符号或者 define 指令来定义的。这种变量赋值方式的显著特点是:在定义变量时,所指定的值会按原样保存,若其中包含对其他变量的引用,这些引用不会马上展开,而是在该变量被使用(也就是在扩展其他字符串的过程中进行替换)时才会展开,这一过程被称作递归展开。

基本的递归展开示例

foo = $(bar)
bar = $(ugh)
ugh = Huh?

all:
    @echo $(foo)

在这个例子中,foo 被定义为 $(bar),在定义时 $(bar) 不会展开。当 make 执行到 @echo $(foo) 时,$(foo) 会先展开为 $(bar),接着 $(bar) 展开为 $(ugh),最终 $(ugh) 展开为 Huh?。所以运行 make 后,输出结果为 Huh?

优点:组合变量值

CFLAGS = $(include_dirs) -O
include_dirs = -Ifoo -Ibar

all:
    @echo "CFLAGS: $(CFLAGS)"

此例展示了递归展开变量的一个优点。当在规则里展开 CFLAGS 时,$(include_dirs) 会展开为 -Ifoo -Ibar,所以 CFLAGS 最终展开为 -Ifoo -Ibar -O。运行 make 后,输出结果为 CFLAGS: -Ifoo -Ibar -O

缺点:无限循环

CFLAGS = $(CFLAGS) -O

all:
    @echo $(CFLAGS)

这体现了递归展开变量的一个主要缺点。CFLAGS 定义为 $(CFLAGS) -O,在展开 CFLAGS 时,由于它引用了自身,会造成无限循环。不过,make 能够检测到这种无限循环并报告错误。

递归展开变量虽然有组合变量值等优点,但也存在容易引发无限循环、函数多次执行导致结果不可预测和运行速度变慢等缺点。在编写 Makefile 时,需要根据具体需求谨慎使用递归展开变量。

2. Makefile 中的简单展开变量(:=

在 Makefile 里,简单展开变量是通过 := 或 ::= 来定义的。其核心特性为:在变量定义阶段,就会对变量值里引用的其他变量和函数进行展开,并且展开完成后,变量值便固定下来,后续使用时不会再次展开。

下面通过几个示例来深入理解简单展开变量:

基本示例

x := foo
y := $(x) bar
x := later

all:
    @echo "Value of y: $(y)"

在这个例子中,当定义 y := $(x) bar 时,$(x) 会立即展开为 foo,所以 y 的值是 foo bar。即便后续 x 被重新赋值为 latery 的值也不会受到影响。运行 make 后,输出结果为 Value of y: foo bar

结合函数的示例

files := $(wildcard *.c)
objects := $(patsubst %.c,%.o,$(files))

all:
    @echo "Source files: $(files)"
    @echo "Object files: $(objects)"

此例中,files := $(wildcard *.c) 会在定义时执行 wildcard 函数,将当前目录下所有 .c 文件的名称赋给 files 变量。接着,objects := $(patsubst %.c,%.o,$(files)) 会执行 patsubst 函数,把 files 里的 .c 文件名替换为 .o 文件名,然后赋给 objects 变量。后续使用 files 和 objects 时,它们的值不会再次展开。

避免无限循环示例

CFLAGS := $(CFLAGS) -O

all:
    @echo "CFLAGS: $(CFLAGS)"

若使用递归展开变量(=),CFLAGS = $(CFLAGS) -O 会造成无限循环。但使用简单展开变量(:=),CFLAGS 初始为空,定义时展开后 CFLAGS 的值就是 -O,避免了无限循环问题。运行 make 后,输出结果为 CFLAGS: -O

3. 追加赋值(+=

若你要在已有变量值的基础上追加内容,就可以使用追加赋值(+=)。

ne = first
ne += second
all:
    @echo $(ne)

在这个例子里,ne 变量最初的值是 first,使用 += 后追加了 second,最终执行 make all 会输出 first second

4. 条件赋值(?=

条件赋值(?=)只有在变量未被赋值时才会进行赋值操作。

ne ?= default
all:
    @echo $(ne)

在这个例子中,由于 ne 之前未被赋值,所以它会被赋值为 default。若 ne 之前已经有了值,那么使用 ?= 就不会改变其原有的值。

二. 取消/删除Makefile变量

在 Makefile 中取消一个变量的声明(即清除变量的值),可以通过几种不同的方式来实现,下面为你详细介绍:

1. 使用空赋值

最简单的方式是将变量赋值为空字符串。这样变量虽然仍然存在,但它的值为空。

# 声明变量
MY_VAR = some_value

# 取消变量的值
MY_VAR = 

all:
    @echo "MY_VAR 的值为: $(MY_VAR)"

在上述代码中,首先给 MY_VAR 变量赋了值 some_value,之后通过 MY_VAR = 这一操作将其值清空。当执行 make all 时,输出的 MY_VAR 的值就是空的。

2. 使用 undefine 关键字

undefine 关键字可以彻底移除变量的定义。与空赋值不同,使用 undefine 后,变量就不再存在于 Makefile 的作用域中。

# 声明变量
MY_VAR = some_value

# 取消变量的定义
undefine MY_VAR

all:
    @echo "MY_VAR 的值为: $(MY_VAR)"

这里,undefine MY_VAR 语句将 MY_VAR 变量的定义完全移除。当执行 make all 时,$(MY_VAR) 不会展开为任何内容,因为该变量已不存在。

3. 利用环境变量覆盖

如果变量是从环境变量中继承而来,你可以通过在 Makefile 中重新赋值为空或者使用 undefine 来处理。另外,在调用 make 命令时也可以通过传递空值来覆盖环境变量的值。

# 假设 MY_VAR 是从环境变量继承而来
all:
    @echo "MY_VAR 的值为: $(MY_VAR)"

若要取消这个环境变量对 Makefile 的影响,可以这样调用 make 命令:

make MY_VAR=

这样,在 Makefile 执行过程中,MY_VAR 的值就为空。

4. 总结

  • 空赋值:适合只是想清空变量的值,而保留变量的定义,后续还可能会重新赋值的情况。
  • undefine 关键字:用于彻底移除变量的定义,若后续不再需要该变量,使用此方法更合适。
  • 环境变量覆盖:针对从环境变量继承的变量,可在调用 make 时传递空值来取消其影响。

三. 访问makefile变量

1. 基本变量引用

使用 $(var) 或 ${var} 来引用变量 var 的值。这是最常见的变量引用方式。

CC = gcc
CFLAGS = -Wall

all:
    $(CC) $(CFLAGS) -o test test.c

在这个例子中,$(CC) 引用了 CC 变量的值 gcc$(CFLAGS) 引用了 CFLAGS 变量的值 -Wall

2. 变量的嵌套引用

可以在一个变量引用中嵌套另一个变量引用,以实现更复杂的操作。

src = main.c utils.c
obj = $(src:.c=.o)
CFLAGS = -Wall
CC = gcc

all: $(obj)
    $(CC) $(CFLAGS) -o my_program $(obj)

$(obj): %.o: %.c
    $(CC) $(CFLAGS) -c $< -o $@

这里,$(obj) 本身是通过后缀替换引用得到的,然后又在规则中被引用,同时规则里还引用了 $(CC) 和 $(CFLAGS) 变量。后面会有很多这种例子

3. 自动变量引用(默认变量)

Makefile 提供了一些自动变量,用于在规则中引用目标文件、依赖文件等信息。

  • $@:表示当前规则的目标文件。
  • $<:表示当前规则的第一个依赖文件。
  • $^:表示当前规则的所有依赖文件,以空格分隔。
  • $?:代表当前规则中所有比目标文件更新的依赖文件,以空格分隔。
src = main.c utils.c
obj = $(src:.c=.o)
CC = gcc
CFLAGS = -Wall

all: my_program

my_program: $(obj)
    $(CC) $(CFLAGS) -o $@ $^

%.o: %.c
    $(CC) $(CFLAGS) -c $< -o $@

在 my_program 规则中,$@ 代表 my_program$^ 代表 main.o utils.o;在 %.o: %.c 规则中,$< 代表对应的 .c 文件,$@ 代表对应的 .o 文件。

4. 函数调用引用

Makefile 支持使用函数来处理变量,通过函数调用的方式引用变量。

  • $(wildcard pattern):用于查找符合指定模式的文件列表。
src = $(wildcard *.c)
obj = $(src:.c=.o)
CC = gcc
CFLAGS = -Wall

all: my_program

my_program: $(obj)
    $(CC) $(CFLAGS) -o $@ $^

%.o: %.c
    $(CC) $(CFLAGS) -c $< -o $@

这里,$(wildcard *.c) 会查找当前目录下所有的 .c 文件,并将结果赋值给 src 变量。

  • $(patsubst pattern,replacement,text):用于对文本中的单词进行模式替换。
src = main.c utils.c
obj = $(patsubst %.c,%.o,$(src))
CC = gcc
CFLAGS = -Wall

all: my_program

my_program: $(obj)
    $(CC) $(CFLAGS) -o $@ $^

%.o: %.c
    $(CC) $(CFLAGS) -c $< -o $@

$(patsubst %.c,%.o,$(src)) 会将 src 变量中的 .c 后缀替换为 .o 后缀,效果和后缀替换引用类似。

5. 环境变量引用

Makefile 可以引用环境变量,使用 $(VAR) 或 ${VAR} 来引用名为 VAR 的环境变量。

all:
    @echo $(HOME)

在这个例子中,$(HOME) 引用了系统环境变量 HOME 的值,通常是用户的主目录。

这些变量引用方式在 Makefile 中非常实用,可以帮助你更灵活地编写和管理项目的编译规则。

6. Makefile 后缀替换引用

Makefile 中用于批量替换变量里单词后缀的语法。

格式:$(var:a=b)

  • var:操作变量名
  • a:待替换后缀
  • b:替换后后缀
src = file1.c file2.c
obj = $(src:.c=.o)
all:
    @echo $(obj)

解释:将 src 里 .c 后缀替换为 .o,输出 file1.o file2.o

项目示例

src = main.c utils.c
obj = $(src:.c=.o)
CC = gcc
CFLAGS = -Wall
all: my_program
my_program: $(obj)
    $(CC) $(CFLAGS) -o my_program $(obj)
%.o: %.c
    $(CC) $(CFLAGS) -c $< -o $@
clean:
    rm -f $(obj) my_program

解释:将 src 源文件列表转为 obj 目标文件列表,用于编译和链接生成 my_program 可执行文件。当然,%.o: %.c可以省略掉。

7. Makefile 中变量值的解析与处理

在 Makefile 中,SUBDIRS = subdir1 subdir2 这样的赋值会将 subdir1 和 subdir2 作为一个整体字符串赋值给变量 SUBDIRS,多个值之间用空格分隔。但在后续使用中,Makefile 会根据具体规则和上下文对其进行解析和处理:

  • 静态模式规则:如在 all: $(SUBDIRS) 中,$(SUBDIRS) 会展开为 "subdir1 subdir2",表明 all 目标依赖于 subdir1 和 subdir2 这两个目标。而在 $(SUBDIRS): 静态模式规则中,Makefile 会将其拆分成两个独立目标,$@ 自动变量会分别代表 subdir1 和 subdir2,然后执行 make -C $@ 命令。
  • 循环处理:在类似 for dir in $(SUBDIRS); do \ make -C $$dir; \ done 的 shell 循环中,$(SUBDIRS) 展开为 "subdir1 subdir2" 后,shell 会按空格将其拆分成多个单词,依次将 subdir1 和 subdir2 赋值给变量 dir,再执行 make -C $dir 命令。

总之,Makefile 不会将 SUBDIRS 中的多个值看作不可分割的整体,而是会根据不同场景按空格进行拆分和处理,以实现对多个目标或值的分别操作。

8. 临时变量

在 Makefile 里,$(0)$(1)$(2) 等临时变量和 call 函数配合使用,目的是在调用自定义参数化函数时传递和引用参数。

变量含义

  • $(0):存储 call 函数调用时的首个参数,也就是被调用的变量名。
  • $(1):代表 call 函数调用时传入的第一个参数。
  • $(2):代表 call 函数调用时传入的第二个参数。以此类推,可传入任意数量的参数。

工作机制

当使用 call 函数时,make 会把传入的参数依次赋值给这些临时变量。之后,在被调用的变量(宏)中,就能使用这些临时变量引用对应的参数,达成参数传递与处理的目的。

示例

  • 简单参数反转
reverse = $(2) $(1)
foo = $(call reverse,a,b)

# 执行 `$(call reverse,a,b)` 时,`reverse` 赋值给 `$(0)`,`a` 赋值给 `$(1)`,`b` 赋值给 `$(2)`。
# `reverse` 宏里 `$(2) $(1)` 替换成 `b a`,所以 `foo` 值为 `b a`。

  • 字符串拼接
concat = $(1)$(3)$(2)
result = $(call concat,Hello,World, )
# 执行 `$(call concat,Hello,World, )` 时,`concat` 赋值给 `$(0)`,`Hello` 赋值给 `$(1)`,`World` 赋值给 `$(2)`,空格 赋值给 `$(3)`。
# `concat` 宏里 `$(1)$(3)$(2)` 替换成 `Hello World`,所以 `result` 值为 `Hello World`。

四. Makefile 多种规则

在 Makefile 里,模式规则和普通规则,静态规则存在明显差异,下面从定义、语法、适用场景等方面详细介绍两者的区别:

1. 定义与概念

普通规则

明确指定一个或多个具体目标文件,以及这些目标文件所依赖的文件,同时给出用于生成目标文件的命令。简单来说,普通规则是针对特定的、具体的文件来定义的。

模式规则

使用通配符 % 来定义具有相似特征的一组目标文件及其依赖关系和生成命令。它可以匹配多个文件,通过 % 通配符捕获文件名的一部分,从而实现规则的复用。

静态模式规则

静态模式规则结合了普通规则和模式规则的特点。它会明确指定一组目标文件,然后使用模式来定义这些目标文件的依赖关系和生成命令。与模式规则不同的是,它只对明确列出的目标文件生效。

2. 语法形式

普通规则

目标文件: 依赖文件1 依赖文件2 ...
    生成目标文件的命令

main.o: main.c
    gcc -c main.c -o main.o

此规则明确指出 main.o 依赖于 main.c,并给出了将 main.c 编译成 main.o 的具体命令。

模式规则

%.o: %.c
    $(CC) $(CFLAGS) -c $< -o $@

这里 %.o 表示任意以 .o 结尾的文件,%.c 表示同名的以 .c 结尾的文件。% 会匹配文件名中相同的部分,比如对于 main.o% 匹配 main,对应的依赖文件就是 main.c$< 是自动变量,代表第一个依赖文件,$@ 代表目标文件。

静态模式规则

objects = foo.o bar.o
$(objects): %.o: %.c
    gcc -c $< -o $@

明确指定了目标文件列表 objects(包含 foo.o 和 bar.o),通过模式 %.o: %.c 定义了依赖关系和生成命令,只对 foo.o 和 bar.o 这两个目标文件生效。

3. 适用场景

普通规则

适用于目标文件较少且每个目标文件的生成规则有特殊要求的情况。例如,某个目标文件的编译选项与其他文件不同,或者生成过程需要额外的步骤,就可以使用普通规则为其单独定义。

special.o: special.c
    gcc -O3 -c special.c -o special.o  # 使用特殊的编译选项 -O3

模式规则

适合处理大量具有相似特征的文件。在项目中有很多源文件需要编译成目标文件时,使用模式规则可以避免为每个文件都编写重复的规则,大大减少 Makefile 的代码量,提高编写效率。例如,一个项目中有多个 .c 文件需要编译成 .o 文件,使用模式规则可以统一处理。

静态模式规则

适用于有一组特定的目标文件,它们有相似的依赖关系和生成方式,但又不想让规则应用到所有符合模式的文件。比如,项目中只有部分 .c 文件需要按照特定规则编译成 .o 文件,就可以使用静态模式规则指定这些目标文件。

4. 灵活性与扩展性

普通规则

灵活性较高,因为可以为每个具体的目标文件定制生成规则。但扩展性较差,当项目中的文件数量增加时,需要不断添加新的规则,会使 Makefile 变得冗长复杂。

模式规则

灵活性相对较低,因为所有匹配的文件都使用相同的规则。但扩展性好,无论项目中有多少符合模式的文件,都不需要额外添加规则,模式规则会自动处理这些文件。

静态模式规则

灵活性和扩展性处于普通规则和模式规则之间。它可以针对特定的一组目标文件定制规则,具有一定的灵活性;同时,当需要添加新的目标文件到这组特定文件中时,只需要修改目标文件列表,而不需要添加新的规则,具有一定的扩展性。

5. 后缀规则

Makefile 的后缀规则是一种较为传统的定义编译规则的方式,它通过文件的后缀名来确定如何处理不同类型的文件。以下是一些常见的后缀规则及其介绍:

.c.o 规则

  • 描述:用于将 C 源文件编译为目标文件。
  • 默认动作:使用 C 编译器(通常是 cc 或 gcc)将 .c 文件编译成 .o 文件。在编译过程中,会自动包含 Makefile 中定义的 CFLAGS 变量指定的编译选项。例如:
CFLAGS = -Wall -g

.c.o:
    $(CC) $(CFLAGS) -c $< -o $@

这里 $(CC) 是 C 编译器,$< 表示依赖的 .c 文件,$@ 表示生成的 .o 目标文件。

.cpp.o 规则

  • 描述:用于将 C++ 源文件编译为目标文件。
  • 默认动作:使用 C++ 编译器(如 g++)将 .cpp 文件编译成 .o 文件,并应用 CXXFLAGS 变量指定的编译选项。示例如下:
CXXFLAGS = -std=c++11 -Wall -g

.cpp.o:
    $(CXX) $(CXXFLAGS) -c $< -o $@

其中 $(CXX) 是 C++ 编译器,$< 和 $@ 分别代表源文件和目标文件。

这些后缀规则可以根据项目的实际需求进行自定义和扩展,以满足不同的编译和链接要求。不过,现代的 Makefile 更倾向于使用模式规则(如 %.o: %.c)来定义编译规则,因为它们更加灵活和直观。后缀规则相对较为传统,在一些旧的项目或者特定的环境中仍然会被使用。

https://www.gnu.org/software/make/manual/html_node/Suffix-Rules.html

五. 常见的隐含推导规则

1. C 源文件编译为目标文件

当目标是 .o 文件,依赖是同名的 .c 文件时,GNU Make 有默认的编译规则。其默认命令如下:

$(CC) $(CFLAGS) -c $< -o $@

这里,$(CC) 代表 C 编译器(默认是 cc),$(CFLAGS) 是编译选项,$< 为第一个依赖文件(也就是 .c 文件),$@ 是目标文件(即 .o 文件)。

示例:

CC = gcc
CFLAGS = -Wall

all: main.o

# 这里没有显式定义 main.o 的规则,会使用隐含推导规则

执行 make 时,GNU Make 会自动使用 gcc -Wall -c main.c -o main.o 来编译 main.c 文件。

2. C++ 源文件编译为目标文件

当目标是 .o 文件,依赖是同名的 .cpp 文件时,默认规则的命令为:

CC = gcc
CFLAGS = -Wall

all: main.o

# 这里没有显式定义 main.o 的规则,会使用隐含推导规则

其中,$(CXX) 是 C++ 编译器(默认是 g++),$(CXXFLAGS) 是 C++ 编译选项。

3. 链接目标文件生成可执行文件

若目标是可执行文件,依赖是多个 .o 文件,默认规则的命令是:

$(CC) $(CFLAGS) $(LDFLAGS) $^ -o $@

$(LDFLAGS) 是链接选项,$^ 代表所有的依赖文件(即 .o 文件)。

4. 隐含推导规则的使用场景

简化 Makefile:在小型项目或者简单的编译任务中,使用隐含推导规则可以显著减少 Makefile 的代码量。例如,一个只有几个源文件的 C 项目,可能只需要简单定义编译器和编译选项,就可以利用隐含推导规则完成编译。

CC = gcc
CFLAGS = -Wall

all: my_program

# 利用隐含推导规则编译 .c 文件和链接生成可执行文件

5. 隐含推导规则的注意事项

自定义编译选项:若默认的编译选项无法满足需求,就需要显式地定义规则。例如,需要指定特定的头文件路径或者宏定义时,就不能单纯依赖隐含推导规则。

%.o: %.c
    $(CC) $(CFLAGS) -I/path/to/include -DDEBUG -c $< -o $@

6. 查看隐含推导规则

你可以使用 make -p 命令查看 GNU Make 的所有隐含推导规则和默认变量设置。这个命令会输出 Make 的内部规则和变量定义,有助于你了解默认的行为和进行自定义修改。

六. Makefile 隐式规则递归推导

1. 递归推导的概念

在 Makefile 中,隐式规则的递归推导是指当 Make 工具根据隐式规则处理目标文件时,若目标文件的依赖文件自身也需要通过隐式规则生成,Make 会继续对这些依赖文件的生成规则进行推导,这个过程会层层深入,直至找到可以直接处理的文件或者满足停止条件。

2. 触发递归推导的情形

间接依赖导致的推导

当一个目标文件(如 .o 文件)依赖于其他文件,而这些依赖文件又可以通过隐式规则生成时,就会触发递归推导。例如:

all: main.o

main.o: main.c header.h

# 没有显式规则,使用隐式规则

当执行 make all 时,Make 发现需要生成 main.o。根据隐式规则,它会检查 main.c 和 header.h 是否存在。若 header.h 是由另一个 .h.in 文件通过隐式规则生成的,Make 就会进一步推导如何生成 header.h。这个过程中,Make 会按照依赖关系逐层推导,先确定 main.o 的生成方式,再确定其依赖文件 header.h 的生成方式。

链式依赖引发的推导

在项目里,若存在多个 .o 文件,并且这些 .o 文件之间存在依赖关系,也会引发递归推导。比如:

all: program

program: main.o utils.o

main.o: main.c
utils.o: utils.c

# 没有显式规则,使用隐式规则

要生成 program,Make 会先推导怎样生成 main.o 和 utils.o。对于 main.o 和 utils.o,又会依据隐式规则去推导如何从 main.c 和 utils.c 生成它们。这就形成了一个链式的推导过程,从最终目标 program 开始,逐步深入到每个 .o 文件以及其对应的 .c 文件。

3. 防止无限递归的机制

虽然存在递归推导,但 Make 工具会通过一些机制避免无限递归。它会检查文件的实际情况,像文件是否存在、文件的时间戳等信息。当某个文件已经是最新的(即其修改时间晚于依赖它的文件),Make 就不会再去推导如何更新它。例如,若 header.h 文件的修改时间晚于 main.o,Make 就不会尝试重新生成 header.h,从而避免了不必要的推导和操作,确保递归推导能够在合理的范围内结束。

4. 隐含规则链与中间目标

隐含规则链

当一个目标文件的生成需要多个连续的隐含规则时,这些规则构成 “隐含规则链”。例如,.o文件的生成,可能先由 Yacc 的.y文件通过隐含规则生成.c文件,再由 C 编译器的隐含规则将.c文件编译为.o文件。若.c文件存在,直接调用 C 编译器隐含规则;若.c文件不存在但.y文件存在,则先调用 Yacc 隐含规则生成.c文件,再调用 C 编译隐含规则。

中间目标特性

中间目标在 Makefile 中有两个特殊之处:

  1. 触发条件:仅当中间目标不存在时,才会触发对应的中间规则。
  2. 文件处理:最终目标成功产生后,生成过程中产生的中间目标文件默认会被rm -f删除 。

中间目标声明与保留

  1. 强制声明中间目标:使用伪目标.INTERMEDIATE可强制声明文件为中间目标(如.INTERMEDIATE : mid)。
  2. 阻止自动删除:通过伪目标.SECONDARY强制声明(如.SECONDARY : sec),可阻止 Make 自动删除指定中间目标。
  3. 保存中间文件:将目标以模式方式(如%.o)指定为伪目标.PRECIOUS的依赖目标,可保存被隐含规则生成的中间文件。

避免无限递归

在 “隐含规则链” 中,禁止同一个目标出现两次或两次以上,以此防止 Make 自动推导时出现无限递归的情况。

规则优化

Make 会对部分特殊隐含规则进行优化,避免生成中间文件。例如从foo.c生成目标程序foo,理论上需先编译生成foo.o再链接,但实际可通过cc -o foo foo.c一条命令完成,此时优化后的规则不会生成foo.o这一中间文件。

5. 通配模式规则概述

当模式规则的目标仅为 % 时,可匹配任意文件名,此为通配规则。但 make 处理这类规则时,要对每个作为目标或先决条件的文件名考虑所有通配规则,会导致运行速度慢。例如,对于 foo.cmake 需考虑多种不合理的生成方式,如从 foo.c.o 链接、从 foo.c.c 编译链接等。

规则限制

为提升 make 运行速度,处理通配规则设置了两种限制,定义通配规则时需二选一。

终结规则

  • 定义方式:使用双冒号 :: 定义通配规则。
  • 适用条件:仅当先决条件实际存在时适用,由其他隐式规则生成的先决条件无效,即规则后不允许进一步的规则链操作。
  • 示例:从 RCS 和 SCCS 文件提取源文件的内置隐式规则为终结规则。若 foo.c,v 文件不存在,make 不会考虑从 foo.c,v.o 或 RCS/SCCS/s.foo.c,v 将其作为中间文件生成,因为 RCS 和 SCCS 文件通常是最终源文件,无需从其他文件重新生成,可节省时间。

非终结规则

  • 定义方式:未标记为终结规则的通配规则。
  • 适用限制:不能应用于隐式规则的先决条件,也不能用于表示特定数据类型的文件名(若某个非通配隐式规则的目标与文件名匹配,则该文件名表示特定数据类型)。
  • 示例foo.c 与模式规则 %.c : %.y(运行 Yacc 的规则)的目标匹配,无论该规则是否实际适用(有 foo.y 文件才适用),只要目标匹配,make 就不会对 foo.c 考虑任何非终结通配规则,不会尝试从 foo.c.ofoo.c.cfoo.c.p 等将其作为可执行文件生成。

七. 条件判断

在 Makefile 中,是有条件判断语句的,类似于其他编程语言里的 if 语句,它能依据不同条件来执行不同的规则或者赋值操作。下面为你详细介绍 Makefile 里的条件判断语句:

1. ifeq 和 ifneq

  • ifeq:用于判断两个参数是否相等,若相等则执行 ifeq 和 endif 之间的内容。
  • ifneq:用于判断两个参数是否不相等,若不相等则执行 ifneq 和 endif 之间的内容。

语法格式

ifeq (参数1, 参数2)
    # 当参数1和参数2相等时执行的内容
else
    # 当参数1和参数2不相等时执行的内容
endif

示例

DEBUG = 1

ifeq ($(DEBUG), 1)
    CFLAGS = -g -Wall
else
    CFLAGS = -O2 -Wall
endif

all:
    @echo "CFLAGS: $(CFLAGS)"

在这个例子中,若 DEBUG 的值为 1,CFLAGS 会被赋值为 -g -Wall;若 DEBUG 的值不为 1,CFLAGS 会被赋值为 -O2 -Wall

2. ifdef 和 ifndef

  • ifdef:用于判断一个变量是否已经定义,若已定义则执行 ifdef 和 endif 之间的内容。
  • ifndef:用于判断一个变量是否未定义,若未定义则执行 ifndef 和 endif 之间的内容。

语法格式

ifdef 变量名
    # 当变量已定义时执行的内容
else
    # 当变量未定义时执行的内容
endif`
ifndef 变量名
    # 当变量未定义时执行的内容
else
    # 当变量已定义时执行的内容
endif`

示例

ifdef DEBUG
    CFLAGS = -g -Wall
else
    CFLAGS = -O2 -Wall
endif

all:
    @echo "CFLAGS: $(CFLAGS)"

在这个例子中,若 DEBUG 变量已定义,CFLAGS 会被赋值为 -g -Wall;若 DEBUG 变量未定义,CFLAGS 会被赋值为 -O2 -Wall

3. 注意事项

  • 空格问题:在 ifeqifneqifdefifndef 等关键字后面的括号内,参数之间的逗号前后不能有空格,否则会影响判断结果。
  • 嵌套使用:条件判断语句可以嵌套使用,以实现更复杂的条件判断逻辑。
DEBUG = 1
PLATFORM = linux

ifeq ($(DEBUG), 1)
    CFLAGS = -g -Wall
    ifeq ($(PLATFORM), linux)
        CFLAGS += -D LINUX
    else
        CFLAGS += -D OTHER_PLATFORM
    endif
else
    CFLAGS = -O2 -Wall
endif

all:
    @echo "CFLAGS: $(CFLAGS)"

在这个嵌套使用的例子中,先根据 DEBUG 的值进行判断,然后在 DEBUG 为 1 的情况下,再根据 PLATFORM 的值进行进一步的判断。

综上所述,Makefile 中的条件判断语句能让你根据不同的条件来灵活控制编译过程,提高 Makefile 的通用性和可维护性。

八. Makefile 隐含规则与变量设置

1. 变量影响隐含规则

在 Makefile 里,隐含规则的命令大多运用预先设定的变量。这些变量可通过以下方式设置,且一旦设置,就会对隐含规则产生作用:

  1. 在 Makefile 中修改变量值。
  2. 在 make 命令行传入变量值。
  3. 在环境变量里设置变量值。

若要取消自定义变量对隐含规则的影响,可使用 make 的 -R 或 --no–builtin-variables 参数。

示例:编译 C 程序隐含规则

编译 C 程序的隐含规则命令为 $(CC) –c $(CFLAGS) $(CPPFLAGS) ,Make 默认编译命令是 cc。若将 $(CC) 重新定义为 gcc$(CFLAGS) 重新定义为 -g,则隐含规则中的命令会以 gcc –c -g $(CPPFLAGS) 的形式执行。

2. 隐含规则使用的变量分类

隐含规则使用的变量可分为两类:

  1. 命令相关变量:例如 CC,用于指定编译器等命令。
  2. 参数相关变量:例如 CFLAGS,用于指定编译选项等参数。

关于命令的变量。

  • AR : 函数库打包程序。默认命令是 ar
  • AS : 汇编语言编译程序。默认命令是 as
  • CC : C语言编译程序。默认命令是 cc
  • CXX : C++语言编译程序。默认命令是 g++
  • CO : 从 RCS文件中扩展文件程序。默认命令是 co
  • CPP : C程序的预处理器(输出是标准输出设备)。默认命令是 $(CC) –E
  • FC : Fortran 和 Ratfor 的编译器和预处理程序。默认命令是 f77
  • GET : 从SCCS文件中扩展文件的程序。默认命令是 get
  • LEX : Lex方法分析器程序(针对于C或Ratfor)。默认命令是 lex
  • PC : Pascal语言编译程序。默认命令是 pc
  • YACC : Yacc文法分析器(针对于C程序)。默认命令是 yacc
  • YACCR : Yacc文法分析器(针对于Ratfor程序)。默认命令是 yacc –r
  • MAKEINFO : 转换Texinfo源文件(.texi)到Info文件程序。默认命令是 makeinfo
  • TEX : 从TeX源文件创建TeX DVI文件的程序。默认命令是 tex
  • TEXI2DVI : 从Texinfo源文件创建军TeX DVI 文件的程序。默认命令是 texi2dvi
  • WEAVE : 转换Web到TeX的程序。默认命令是 weave
  • CWEAVE : 转换C Web 到 TeX的程序。默认命令是 cweave
  • TANGLE : 转换Web到Pascal语言的程序。默认命令是 tangle
  • CTANGLE : 转换C Web 到 C。默认命令是 ctangle
  • RM : 删除文件命令。默认命令是 rm –f

关于命令参数的变量

下面的这些变量都是相关上面的命令的参数。如果没有指明其默认值,那么其默认值都是空。

  • ARFLAGS : 函数库打包程序AR命令的参数。默认值是 rv
  • ASFLAGS : 汇编语言编译器参数。(当明显地调用 .s 或 .S 文件时)
  • CFLAGS : C语言编译器参数。
  • CXXFLAGS : C++语言编译器参数。
  • COFLAGS : RCS命令参数。
  • CPPFLAGS : C预处理器参数。( C 和 Fortran 编译器也会用到)。
  • FFLAGS : Fortran语言编译器参数。
  • GFLAGS : SCCS “get”程序参数。
  • LDFLAGS : 链接器参数。(如: ld )
  • LFLAGS : Lex文法分析器参数。
  • PFLAGS : Pascal语言编译器参数。
  • RFLAGS : Ratfor 程序的Fortran 编译器参数。
  • YFLAGS : Yacc文法分析器参数。

九. Makefile include指令

在 Makefile 里,include 指令是一个非常实用的功能,它能让你在一个 Makefile 中包含其他 Makefile 文件的内容,从而提升代码的可维护性和复用性。下面为你详细介绍 include 的相关内容:

基本语法

include 指令的基本语法如下:

include filename1 filename2 ...

这里的 filename1filename2 等是要包含的 Makefile 文件的名称。你可以同时包含多个文件,文件名之间用空格分隔。

工作原理

当 Make 读取到 include 指令时,它会暂停当前 Makefile 的解析,转而读取并解析指定的文件内容。读取完成后,会将这些文件的内容插入到 include 指令所在的位置,然后继续解析当前的 Makefile。

示例

假设我们有两个 Makefile 文件:main.mk 和 utils.mk

utils.mk 文件内容

CFLAGS = -Wall -O2
CC = gcc

clean:
    rm -f *.o

main.mk 文件内容

include utils.mk

all: main.o
    $(CC) $(CFLAGS) main.o -o main

main.o: main.c
    $(CC) $(CFLAGS) -c main.c -o main.o

在这个例子中,main.mk 文件通过 include utils.mk 指令包含了 utils.mk 文件的内容。这样,main.mk 就可以使用 utils.mk 中定义的变量(如 CFLAGS 和 CC)和规则(如 clean 规则)。

1. 搜索路径

如果要包含的文件不在当前目录下,你可以通过设置 VPATH 变量或者 make 命令的 -I 选项来指定搜索路径。

使用 VPATH 变量

VPATH = path/to/include

include utils.mk

这里的 VPATH 变量指定了一个搜索路径,Make 会在这个路径下查找 utils.mk 文件。

使用 I 选项

make -I path/to/include -f main.mk

在执行 make 命令时,使用 -I 选项指定搜索路径,Make 会在该路径下查找包含的文件。

2. 处理文件不存在的情况

如果 include 指定的文件不存在,Make 默认会给出警告信息,但不会停止执行。你可以使用 sinclude 或者 -include 来忽略文件不存在的错误。

sinclude utils.mk
# 或者
-include utils.mk

使用 sinclude 或 -include 时,如果文件不存在,Make 不会给出警告,会继续执行后续的解析。

3. 适用场景

  • 模块化开发:将不同功能的规则和变量分别放在不同的 Makefile 文件中,通过 include 指令在主 Makefile 中组合这些文件,使代码结构更清晰,便于维护和管理。
  • 代码复用:多个项目可能会有一些共同的编译规则和变量,将这些内容放在一个公共的 Makefile 文件中,其他项目的 Makefile 可以通过 include 指令引用这个公共文件,避免代码重复编写。

十. Makefile 执行子目录 Makefile

1. 使用 make -C 命令

示例代码

SUBDIRS = subdir1 subdir2

all: $(SUBDIRS)

$(SUBDIRS):
    make -C $@

clean:
    for dir in $(SUBDIRS); do \
        make -C $$dir clean; \
    done

代码解释

  1. SUBDIRS 变量:定义包含子目录名称的列表。
  2. all 目标:依赖于 $(SUBDIRS),执行 make all 时依次处理各子目录。
  3. $(SUBDIRS) 规则make -C $@ 切换到子目录并执行其中的 Makefile,$@ 代表当前子目录名。
  4. clean 目标:使用 for 循环遍历子目录,执行 make -C $dir clean 清理文件。

2. 使用递归调用

示例代码

SUBDIRS = subdir1 subdir2

all:
    @for dir in $(SUBDIRS); do \
        $(MAKE) -C $$dir; \
    done

clean:
    @for dir in $(SUBDIRS); do \
        $(MAKE) -C $$dir clean; \
    done

代码解释

  1. all 目标for 循环遍历子目录,$(MAKE) -C $dir 递归调用 make 执行子目录的 Makefile,$(MAKE) 是特殊变量,代表当前 make 命令。
  2. clean 目标:同理,遍历子目录执行 $(MAKE) -C $dir clean 清理文件。

3. 传递变量和参数

示例代码

SUBDIRS = subdir1 subdir2
CFLAGS = -Wall -O2

all:
    @for dir in $(SUBDIRS); do \
        $(MAKE) -C $$dir CFLAGS="$(CFLAGS)"; \
    done

clean:
    @for dir in $(SUBDIRS); do \
        $(MAKE) -C $$dir clean; \
    done

代码解释

通过 $(MAKE) -C $dir CFLAGS="$(CFLAGS)" 将 CFLAGS 变量传递给子目录的 Makefile 用于编译。

4. 注意事项

  1. 错误处理:递归调用 make 时要注意错误处理,如某个子目录 Makefile 执行失败,需考虑处理方式(停止或继续构建)。
  2. 并行执行:若子目录无依赖关系,可使用 make 的 j 选项并行执行提高构建速度。

十一. Makefile 转义符号笔记

1.$$转义$

  • 作用:在 Makefile 中,$用于引用变量。当在 Makefile 的 shell 命令中需要使用$让 shell 解析变量时,使用$$进行转义,Makefile 会将$$解析为单个$传递给 shell。

示例

SUBDIRS = subdir1 subdir2

all:
    for dir in $(SUBDIRS); do \
        make -C $$dir; \
    done

在上述代码中,$(SUBDIRS)被 Makefile 展开为subdir1 subdir2$$dir经解析后传递给 shell 的是$dir,shell 在循环中会将$dir依次赋值为subdir1subdir2

二、\\转义\

  • 作用:在 Makefile 中,反斜杠\通常用于行延续。若要在字符串或命令中使用反斜杠本身,需用\\进行转义。

示例

PATH = C:\\Windows\\System32

这里\\被解析为单个反斜杠,PATH变量的值为C:\Windows\System32

三、\转义换行符

  • 作用:编写多行命令时,使用反斜杠\转义换行符,使命令延续到下一行。

示例

all:
    gcc -o program \
        main.c \
        utils.c

通过\将多行命令连接成一个完整的命令。

十二. Makefile 与 Shell 混合使用笔记

1. 基本原理

Makefile 用于自动化构建项目,通过定义规则描述文件依赖关系和构建步骤。Shell 是命令行解释器,提供丰富命令和功能。Makefile 允许在规则命令部分直接使用 Shell 命令,借助 Shell 功能完成复杂构建任务。

2. 具体体现

2.1. 直接嵌入 Shell 命令

在 Makefile 规则中可直接编写 Shell 命令,如:

all:
    echo "Starting the build process..."
    ls -l
    mkdir build

执行 make all 时,Makefile 会依次执行这些 Shell 命令。

2.2. 使用 Shell 变量和语法

可在 Makefile 中使用 Shell 的变量和语法,例如使用 Shell 循环:

SUBDIRS = subdir1 subdir2

all:
    for dir in $(SUBDIRS); do \
        echo "Building in directory: $$dir"; \
        make -C $$dir; \
    done

for 循环是 Shell 语法,dir 是 Shell 变量,可对多个子目录进行批量操作。

2.3. 结合 Shell 脚本

除嵌入简单 Shell 命令,还能调用外部 Shell 脚本,如:

all:
    ./build_script.sh

Makefile 会执行 build_script.sh 脚本。

3. 注意事项

1. 变量解析冲突

Makefile 和 Shell 都用 $ 引用变量,可能导致解析冲突。在 Makefile 的 Shell 命令中,需用 $$ 转义 $,确保 Shell 正确解析其变量。

2. 命令执行环境

Makefile 中的 Shell 命令在新的 Shell 进程中执行,每个规则的命令默认在独立 Shell 中执行。若需在同一 Shell 中执行多个命令,可用反斜杠 \ 连接成一行,如:

all:
    cd build; \
    make

确保 make 命令在 build 目录下执行。

十三. makefile中函数调用

函数使你能够在 Makefile 中进行文本处理,以计算要操作的文件,或者确定在规则(recipe)中要使用的命令。你可以在函数调用中使用函数,在函数调用时,你需要给出函数的名称以及一些供函数进行处理的文本(即参数)。函数处理的结果会在调用函数的位置被替换到 Makefile 中,这就如同变量被替换一样。有点类似于Apache commoncollections

1. 函数调用语法笔记

  1. 调用形式:类似变量引用,有 $(function arguments) 和 ${function arguments} 两种形式。函数名是 make 内置或用 call 创建的自定义函数,参数与函数名用空格 / 制表符分隔,多参数用逗号分隔。参数中分隔符要成对,嵌套调用尽量用同一种分隔符。
  2. 参数扩展:参数在函数调用前按顺序扩展。
  3. 特殊字符:使用特殊字符(逗号、首参空格、不匹配括号等)作参数时,不能用反斜杠转义,可存入变量隐藏,如用 subst 函数替换字符。

例如

comma:= ,
empty:=
space:= $(empty) $(empty)
foo:= a b c
bar:= $(subst $(space),$(comma),$(foo))
# bar is now ‘a,b,c’.

2. 字符串处理

1. 字符串替换函数

  • $(subst from,to,text):在 text 中,将所有 from 替换为 to。例:$(subst ee,EE,feet on the street) 输出 fEEt on the strEEt
  • $(patsubst pattern,replacement,text):在 text 中,匹配 pattern 的单词替换为 replacementpattern 中的 % 为通配符,replacement 中的 % 会被 pattern 匹配内容替换 。例:$(patsubst %.c,%.o,x.c.c bar.c) 输出 x.c.o bar.o
  • 替换引用语法$(var:pattern=replacement) 等价于 $(patsubst pattern,replacement,$(var))$(var:suffix=replacement) 等价于 $(patsubst %suffix,%replacement,$(var)) 。

2. 字符串处理函数

  • $(strip string):去除 string 首尾空格,内部连续空格替换为单个空格。常用于条件判断前处理字符串 。
  • $(findstring find,in):在 in 中查找 find,找到返回 find,否则返回空字符串。
  • $(filter pattern…,text):返回 text 中匹配 pattern 的单词,去除不匹配项。
  • $(filter-out pattern…,text):返回 text 中不匹配 pattern 的单词,与 filter 功能相反。
  • $(sort list):按字典序排序 list 中的单词并去重。

3. 字符串提取函数

  • $(word n,text):返回 text 中第 n 个单词(n 从 1 开始),越界返回空字符串。
  • $(wordlist s,e,text):返回 text 中从第 s 个到第 e 个单词(包含 s 和 e) 。
  • $(words text):返回 text 中单词的数量。
  • $(firstword names…):返回 names 中第一个单词。
  • $(lastword names…):返回 names 中最后一个单词。

4. 应用示例

通过 subst 和 patsubst 处理 VPATH 变量,将目录列表转为 -I 编译选项添加到 CFLAGS 中:

override CFLAGS += $(patsubst %,-I%,$(subst :, ,$(VPATH)))

上述代码先将 VPATH 中的冒号替换为空格,再将每个目录名转为 -I 标志并追加到 CFLAGS 中 。

3. 文件名处理函数

在 Makefile 中,有几个内置的展开函数专门用于处理文件名或文件名列表,这些函数对一系列以空格分隔的文件名(忽略首尾空格)进行特定转换,转换结果以单个空格连接。具体函数如下:

  1. $(dir names…):提取文件名中的目录部分,即包含最后一个斜杠(/)及之前的所有内容。若文件名无斜杠,目录部分为./。如$(dir src/foo.c hacks)结果是src/ ./
  2. $(notdir names…):提取文件名中除目录部分之外的内容。无斜杠的文件名不变;有斜杠的去掉最后斜杠及之前部分。以斜杠结尾的文件名会变为空字符串,可能导致结果文件名数量与参数不同。如$(notdir src/foo.c hacks)结果是foo.c hacks
  3. $(suffix names…):提取文件名的后缀。若文件名有.,后缀是最后一个.及之后的内容;否则后缀为空字符串。结果可能比参数文件名数量少。如$(suffix src/foo.c src-1.0/bar.c hacks)结果是.c .c
  4. $(basename names…):提取文件名中除后缀之外的内容。有.时,取最后一个.之前的部分(不包括.),目录部分的.会被忽略;无.时,basename 为整个文件名。如$(basename src/foo.c src-1.0/bar hacks)结果是src/foo src-1.0/bar hacks
  5. $(addsuffix suffix,names…):将suffix添加到names中的每个文件名后面,结果用单个空格连接。如$(addsuffix .c,foo bar)结果是foo.c bar.c
  6. $(addprefix prefix,names…):将prefix添加到names中的每个文件名前面,结果用单个空格连接。如$(addprefix src/,foo bar)结果是src/foo src/bar
  7. $(join list1,list2):将list1list2按单词逐个连接,第n个结果单词由两个参数的第n个单词连接而成。参数单词数量不同时,多出的单词原样复制到结果中。单词间原有的空格不保留,会被单个空格替换。可合并dirnotdir函数的结果。如$(join a b,.c .o)结果是a.c b.o
  8. $(wildcard pattern)pattern是文件名模式(通常含通配符),返回与模式匹配且存在的文件名,以空格分隔。
  9. $(realpath names…):返回names中每个文件名的规范绝对名称,不包含...、重复路径分隔符(/)和符号链接。失败时返回空字符串,可能的失败原因参考realpath(3)文档。
  10. $(abspath names…):返回names中每个文件名的绝对名称,不包含...、重复路径分隔符(/)。与realpath不同,abspath不解析符号链接,也不要求文件名指向的文件或目录存在,可使用wildcard函数检测文件是否存在。

4. 条件判断函数

在 Makefile 中,有四个函数可实现条件展开,这些函数的关键特点是并非所有参数都会在初始时展开,只有那些需要展开的参数才会被展开。具体函数如下:

  1. $(if condition,then-part[,else-part])if函数在函数环境中提供条件展开支持(与 GNU make 的 Makefile 条件语句,如ifeq不同)。
    • 第一个参数condition先去除首尾空格再展开。若展开后为非空字符串,条件为真;若为空字符串,条件为假。
    • 条件为真时,计算第二个参数then-part,其结果作为if函数的最终结果。
    • 条件为假时,计算第三个参数else-part(若存在),作为if函数的结果;若不存在第三个参数,if函数结果为空字符串。
    • then-partelse-part只会有一个被计算,可包含副作用操作(如 shell 函数调用等)。
  2. $(or condition1[,condition2[,condition3…]])or函数提供 “短路” 或运算。按顺序逐个展开参数,若某个参数展开后为非空字符串,处理停止,该展开结果作为or函数结果。若所有参数展开后都为假(空字符串),则函数结果为空字符串。
  3. $(and condition1[,condition2[,condition3…]])and函数提供 “短路” 与运算。按顺序逐个展开参数,若某个参数展开后为空字符串,处理停止,函数结果为空字符串。若所有参数展开后都为非空字符串,则函数结果为最后一个参数的展开值。
  4. $(intcmp lhs,rhs[,lt-part[,eq-part[,gt-part]]])intcmp函数用于整数的数值比较,在 GNU make 的 Makefile 条件语句中无对应形式。
    • 先展开并将左右参数lhsrhs解析为十进制整数,其余参数的展开取决于左右参数的数值比较结果。
    • 若无更多参数,若左右参数不相等,函数展开结果为空字符串;若相等,结果为它们的数值。
    • lhs严格小于rhsintcmp函数结果为第三个参数lt-part的展开值;若相等,结果为第四个参数eq-part的展开值;若lhs严格大于rhs,结果为第五个参数gt-part的展开值。
    • gt-part缺失,默认取eq-part;若eq-part缺失,默认取空字符串。如$(intcmp 9,7,hello)$(intcmp 9,7,hello,world,)结果为空字符串,$(intcmp 9,7,hello,world)world后无逗号)结果为world 。

例子

# $(if condition,then-part[,else-part])
# 定义变量
VAR := value

# 使用 if 函数进行条件判断
RESULT := $(if $(VAR),$(VAR) exists, $(VAR) does not exist)

all:
    @echo $(RESULT)

# 在上述示例中,变量 VAR 有值,因此 condition 为真,then-part 会被计算,最终输出 value exists。

# $(or condition1[,condition2[,condition3…]])
# 定义变量
VAR1 := 
VAR2 := value2
VAR3 := value3

# 使用 or 函数进行条件判断
RESULT := $(or $(VAR1), $(VAR2), $(VAR3))

all:
    @echo $(RESULT)
    
# 这里 VAR1 为空字符串,VAR2 为非空字符串,所以 or 函数在计算到 VAR2 时就停止,并返回 VAR2 的值,最终输出 value2。

# $(and condition1[,condition2[,condition3…]])
# 定义变量
VAR1 := value1
VAR2 := value2
VAR3 := 

# 使用 and 函数进行条件判断
RESULT := $(and $(VAR1), $(VAR2), $(VAR3))

all:
    @echo $(RESULT)
    
# 在这个例子中,VAR3 为空字符串,and 函数在计算到 VAR3 时停止,由于 VAR3 为空,所以最终输出为空字符串。

# $(intcmp lhs,rhs[,lt-part[,eq-part[,gt-part]]])
# 定义变量
NUM1 := 5
NUM2 := 3

# 使用 intcmp 函数进行数值比较
RESULT := $(intcmp $(NUM1), $(NUM2), less, equal, greater)

all:
    @echo $(RESULT)
# 因为 NUM1 大于 NUM2,所以 intcmp 函数返回 gt-part 的值,即 greater。

5. let 函数

  1. 函数功能let 函数用于限制变量作用域,在 let 表达式中对命名变量的赋值仅在该表达式的文本范围内有效,不影响外部作用域中同名变量。同时,let 函数支持列表解包,可将未分配的值都赋给最后一个命名变量。
  2. 函数语法$(let var [var ...],[list],text) 。先展开前两个参数 var 和 list,最后一个参数 text 不一同展开。然后将 list 展开值的每个单词依次绑定到变量名 var 上,最后一个变量名绑定 list 展开后的剩余部分。若 var 变量名数量多于 list 单词数,剩余变量名设为空字符串;若 var 变量名数量少于 list 单词数,最后一个 var 设为 list 剩余所有单词。在 let 执行期间,var 中的变量按简单展开变量进行赋值。
  3. 示例
    • 定义宏 reverse 用于反转给定列表中单词顺序:
reverse = $(let first rest,$1,\
            $(if $(rest),$(call reverse,$(rest)) )$(first))

all: ; @echo $(call reverse,d c b a)

  • 调用 reverse 宏时,let 先将 $1 展开为 d c b a,将 first 赋值为 drest 赋值为 c b a 。接着展开 if 语句,因 $(rest) 非空,递归调用 reverse 函数处理 c b a 。递归中 let 又将 first 赋值为 crest 赋值为 b a 。递归持续到 let 处理只有一个值 a 时,此时 first 为 arest 为空,不再递归,直接展开 $(first) 为 a 并返回,逐步添加前面的值,最终输出 a b c d 。
  • reverse 调用完成后,first 和 rest 变量不再设置,若之前存在同名变量,不受 reverse 宏展开影响。

6. foreach 函数笔记

  1. 函数特点与功能foreach 函数与 let 函数类似,但与其他函数差异较大。它能使一段文本被重复使用,每次对其进行不同的替换操作,类似于 shell 中 sh 的 for 命令和 csh 的 foreach 命令。可让文本按列表中单词数量多次展开,并将多次展开结果用空格连接作为 foreach 函数结果。
  2. 函数语法$(foreach var,list,text) 。先展开前两个参数 var 和 list,最后一个参数 text 不一同展开。然后对于 list 展开值的每个单词,将 var 展开值命名的变量设为该单词,并展开 texttext 中通常包含对该变量的引用,每次展开结果不同。
  3. 示例
dirs := a b c d
files := $(foreach dir,$(dirs),$(wildcard $(dir)/*))

text 为 $(wildcard $(dir)/*),每次循环 dir 分别取值 abc 等,依次执行 $(wildcard a/*)$(wildcard b/*) 等,最终 files 为所有目录下文件列表,效果与 files := $(wildcard a/* b/* c/* d/*) 类似(除了 dirs 设置情况)。

  • 提高可读性示例
find_files = $(wildcard $(dir)/*)
dirs := a b c d
files := $(foreach dir,$(dirs),$(find_files))

通过定义变量 find_files 来提高复杂 text 的可读性,使用 = 定义递归展开变量,使 find_files 值包含实际函数调用,能在 foreach 控制下重新展开,简单展开变量无法实现此效果(因 wildcard 在定义 find_files 时只会调用一次)。4. 变量影响:与 let 函数类似,foreach 函数对变量 var 无永久影响,调用前后 var 的值和类型不变。从 list 中获取的值仅在 foreach 执行期间临时有效,执行期间 var 是简单展开变量。若调用前 var 未定义,调用后仍未定义。5. 注意事项:使用生成变量名的复杂变量表达式时需谨慎,因为很多奇怪的变量名虽有效但可能非预期,如 files := $(foreach Esta-escrito-en-espanol!,b c ch,$(find_files)) 这种情况可能是错误的,除非 find_files 引用的变量名确实为 Esta-escrito-en-espanol! 。

7. file 函数

  1. 函数功能file 函数支持在 Makefile 中对文件进行读写操作。写操作有两种模式:覆盖(overwrite),即新文本写入文件开头,覆盖原有内容;追加(append),新文本写入文件末尾,保留原有内容。若文件不存在,两种模式下均会创建文件。写操作失败(如文件无法打开)将导致致命错误。写文件时,file 函数返回空字符串。读文件时,函数返回文件内容(去除末尾换行符,若有),读取不存在的文件返回空字符串。
  2. 函数语法$(file op filename[,text]) 。函数执行时,先展开所有参数,再根据 op 指定的模式打开 filename 对应的文件。op 为操作符,> 表示覆盖写入,>> 表示追加写入,< 表示读取文件内容;filename 为目标文件名;操作符与文件名间可包含空格。读文件时不能提供 text 参数;写文件时,text 内容将写入文件,若 text 末尾无换行符会自动添加(text 为空字符串时也会添加),不提供 text 则不写入内容 。
  3. 应用示例:当构建系统命令行长度受限,且命令支持从文件读取参数时,file 函数十分有用。许多命令约定以 @ 开头的参数指定包含更多参数的文件。例如:
    • 简单写入并使用文件参数
program: $(OBJECTS)
        $(file >$@.in,$^)
        $(CMD) $(CMDFLAGS) @$@.in
        @rm $@.in

上述代码将 $^(所有先决条件)写入 $@.in 文件(覆盖原有内容),然后命令 $(CMD) 通过 @$@.in 读取参数执行,最后删除临时文件。

  • 按行写入并使用文件参数
program: $(OBJECTS)
        $(file >$@.in) $(foreach O,$^,$(file >>$@.in,$O))
        $(CMD) $(CMDFLAGS) @$@.in
        @rm $@.in

此代码先清空创建 $@.in 文件,再通过 foreach 循环将每个先决条件逐行追加写入文件,后续命令执行和文件删除操作与上例类似。

8. call 函数

  1. 函数功能call 函数可用于创建新的参数化函数。能将复杂表达式作为变量值,再用 call 函数传入不同值进行展开,实现类似自定义带参数函数的功能。
  2. 函数语法$(call variable,param,param,…) 。make 展开此函数时,会将每个 param 赋值给临时变量 $(1)$(2) 等,$(0) 包含 variable 。参数数量无最大和最小限制,但无参数调用无意义。然后在这些临时赋值的上下文中,variable 作为 make 变量展开,variable 值中对 $(1) 等的引用会解析为 call 调用时的对应参数。
  3. 使用要点
    • variable 是变量名,书写时一般不使用 $ 或括号(若希望变量名不是常量,可在其中使用变量引用)。
    • 若 variable 是内置函数名,即使存在同名 make 变量,也总是调用内置函数。
    • call 函数在将 param 参数赋值给临时变量前会先展开它们,这可能导致包含 foreachif 等有特殊展开规则的内置函数引用的变量值,运行结果与预期不符。
  4. 示例
    • 简单参数反转示例
reverse = $(2) $(1)
foo = $(call reverse,a,b)

foo 的值为 b a 。

  • 在路径中搜索程序示例
pathsearch = $(firstword $(wildcard $(addsuffix /$(1),$(subst :, ,$(PATH)))))
LS := $(call pathsearch,ls)

LS 变量包含类似 /bin/ls 的值。

  • 函数嵌套(实现 map 函数)示例
map = $(foreach a,$(2),$(call $(1),$(a)))
o = $(call map,origin,o map MAKE)

o 最终包含类似 file file default 的值。

注意事项:给 call 函数参数添加空格时需谨慎,与其他函数一样,第二个及后续参数中的任何空格都会保留,可能导致意外结果。提供参数时,最好去除所有多余空格。

9. value 函数笔记

  1. 函数功能value 函数提供了一种使用变量值而不进行展开的方式。但它无法撤销已经发生的展开,例如对于简单展开变量,其值在定义时已展开,此时 value 函数返回的结果与直接使用该变量相同。
  2. 函数语法$(value variable) 。variable 是变量名,书写时一般不使用 $ 或括号(若希望变量名不是常量,可在其中使用变量引用)。
  3. 返回值特点:函数结果是一个包含 variable 值的字符串,且不进行任何展开。例如在以下 Makefile 中:
FOO = $PATH

all:
        @echo $(FOO)
        @echo $(value FOO)
  • 第一个 echo 语句输出 ATH,因为 $P 会被当作 make 变量展开($PATH 中 $P 被错误展开)。
  • 第二个 echo 语句输出当前 $PATH 环境变量的值,因为 value 函数避免了对 FOO 值的展开,保留了 $PATH 原样。

使用场景value 函数通常与 eval 函数结合使用(eval 函数相关内容见《The eval Function》) 。

10. eval 函数笔记

1. 函数功能

eval 函数非常特殊,它允许你定义非常量的新 Makefile 构造,这些构造是其他变量和函数求值的结果。eval 函数会先展开其参数,然后将展开结果按照 Makefile 语法进行解析,这些展开结果可以用来定义新的 Make 变量、目标、隐式或显式规则等。

2. 函数返回值

eval 函数的返回值始终为空字符串,因此它几乎可以放在 Makefile 中的任何位置而不会导致语法错误。

3. 双重展开特性

eval 函数的参数会经历两次展开:首先由 eval 函数进行第一次展开,然后展开的结果在被当作 Makefile 语法解析时会再次展开。这意味着在使用 eval 函数时,可能需要对 $ 字符进行额外的转义处理。在这种情况下,value 函数有时会很有用,可用于避免不必要的展开。

4. 示例及解释

示例代码

makefile

PROGRAMS    = server client

server_OBJS = server.o server_priv.o server_access.o
server_LIBS = priv protocol

client_OBJS = client.o client_api.o client_mem.o
client_LIBS = protocol

# Everything after this is generic

.PHONY: all
all: $(PROGRAMS)

define PROGRAM_template =
 $(1): $$($(1)_OBJS) $$($(1)_LIBS:%=-l%)
 ALL_OBJS   += $$($(1)_OBJS)
endef

$(foreach prog,$(PROGRAMS),$(eval $(call PROGRAM_template,$(prog))))

$(PROGRAMS):
        $(LINK.o) $^ $(LDLIBS) -o $@

clean:
        rm -f $(ALL_OBJS) $(PROGRAMS)

代码解释

  • 变量定义
    • PROGRAMS 定义了要构建的程序列表,这里是 server 和 client
    • 分别为 server 和 client 定义了对应的目标文件列表(_OBJS)和库文件列表(_LIBS)。
  • PROGRAM_template 模板定义
    • 使用 define 定义了一个模板 PROGRAM_template,它接受一个参数 $(1)
    • $(1): $$($(1)_OBJS) $$($(1)_LIBS:%=-l%) 定义了一个规则,目标是 $(1),依赖是对应的目标文件和库文件。
    • ALL_OBJS += $$($(1)_OBJS) 将当前程序的目标文件添加到 ALL_OBJS 变量中。
    • 注意这里使用了 $$ 进行转义,以应对 eval 的双重展开。
  • eval 和 foreach 结合使用
    • $(foreach prog,$(PROGRAMS),$(eval $(call PROGRAM_template,$(prog)))) 遍历 PROGRAMS 列表中的每个程序,调用 PROGRAM_template 模板并将程序名作为参数传递,然后使用 eval 函数对结果进行解析,动态生成规则。
  • 通用规则和清理规则
    • $(PROGRAMS): 定义了一个通用的规则,用于链接生成最终的程序。
    • clean 规则用于清理生成的目标文件和程序。

使用 eval 的好处

  • 模板定义可以非常复杂,使用 eval 可以将复杂的部分封装起来,提高代码的可维护性。
  • 可以将复杂的通用部分放在另一个 Makefile 中,然后在各个单独的 Makefile 中包含它,使各个单独的 Makefile 更加简洁明了。

11. origin 函数

1. 函数功能

origin 函数与大多数其他函数不同,它不操作变量的值,而是用于获取变量的相关信息,具体是告知变量的定义来源。

2. 函数语法

其语法为 $(origin variable),其中 variable 是要查询的变量名,并非对该变量的引用。通常书写时不用 $ 或括号(若希望变量名不是常量,可在其中使用变量引用)。

3. 函数返回值

函数返回一个字符串,表明变量的定义方式:

  • undefined:表示变量从未被定义过。
  • default:意味着变量有默认定义,像 CC 等变量通常如此。不过若重新定义了默认变量,origin 函数返回的是后续定义的来源。
  • environment:说明变量是从传递给 make 的环境中继承而来。
  • environment override:表示变量从传递给 make 的环境中继承,并且由于使用了 e 选项,它覆盖了 Makefile 中该变量的设置。
  • file:表示变量是在 Makefile 中定义的。
  • command line:说明变量是在命令行中定义的。
  • override:意味着变量是在 Makefile 中使用 override 指令定义的。
  • automatic:表示变量是为执行每个规则的命令脚本而定义的自动变量。

4. 实际应用

该信息除了满足用户的好奇心外,主要用于判断是否要采用变量的值。例如:

示例一

假设存在一个 Makefile foo 包含另一个 Makefile bar。希望在运行 make -f bar 时,若环境中已有 bletch 的定义,bar 也能对 bletch 进行定义;但如果 foo 在包含 bar 之前已定义 bletch,则不希望覆盖该定义。可以在 bar 中使用如下代码:

ifdef bletch
ifeq "$(origin bletch)" "environment"
bletch = barf, gag, etc.
endif
endif

若 bletch 是从环境中定义的,此代码会对其重新定义。

示例二

若想在 bletch 来自环境(即便使用 -e 选项)时覆盖其之前的定义,可使用如下代码:

ifneq "$(findstring environment,$(origin bletch))" ""
bletch = barf, gag, etc.
endif

当 $(origin bletch) 返回 environment 或 environment override 时,就会进行重新定义。

12. flavor 函数

1. 函数功能

flavor 函数和 origin 函数类似,它并不对变量的值进行操作,而是用于获取变量的相关特性信息。具体来说,它能够告知变量的类型(参考《The Two Flavors of Variables》)。

2. 函数语法

其语法为 $(flavor variable),这里的 variable 指的是要查询的变量名,并非对该变量的引用。一般情况下,书写时不需要使用 $ 或括号。不过,要是你希望变量名不是常量,也可以在其中使用变量引用。

3. 函数返回值

该函数会返回一个字符串,用于标识变量的类型:

  • undefined:若变量从未被定义过,函数返回此结果。
  • recursive:若变量是递归展开变量,函数返回该值。递归展开变量在定义时不会立即展开其引用的其他变量,而是在使用时才进行展开。
  • simple:若变量是简单展开变量,函数返回此值。简单展开变量在定义时就会立即展开其引用的其他变量。

flavor 函数为我们提供了一种查看变量类型的方式,有助于我们在 Makefile 编写过程中更好地理解和处理不同类型的变量,确保变量的使用符合预期,避免因变量类型导致的意外错误。

posted @ 2025-04-24 14:16  potatso  阅读(58)  评论(0)    收藏  举报