深入了解GNU Make标准库
1. DEBUG设置检查
逻辑运算符的一个实用场景是确保makefile的使用者将 DEBUG 变量设置为 Y 或 N ,避免因遗忘该调试选项而引发问题。GNU Make标准库(GMSL)中的 assert 函数可发挥作用,如果其参数为假,它会输出致命错误。以下代码可用于确保 DEBUG 只能为 Y 或 N :
include gmsl
$(call assert,$(call or,$(call seq,$(DEBUG),Y),$(call seq,$(DEBUG),N)),DEBUG must be Y or N)
示例:
$ make DEBUG=Oui
Makefile:1: *** GNU Make Standard Library: Assertion failure: DEBUG must be Y
or N. Stop.
当用户错误地将 DEBUG 设置为 Oui 时,断言会触发错误。
2. 预处理中使用逻辑运算符
GNU make的预处理器(包含 ifeq 、 ifneq 和 ifdef 指令)缺乏逻辑运算功能,编写复杂语句较为困难。例如,要在GNU make中当 DEBUG 设置为 Y 或 Yes 时定义makefile的一部分,通常有两种不佳的做法:一是复制代码段,二是编写难以理解的语句:
ifeq ($(DEBUG),$(filter $(DEBUG),Y Yes))
--snip--
endif
此代码通过用 $(DEBUG) 的值过滤 Y Yes 列表来工作。若 $(DEBUG) 不是 Y 或 Yes ,返回空列表;若是,则返回 $(DEBUG) 的值。 ifeq 再将结果与 $(DEBUG) 比较。这种方式不仅难看、难维护,还存在微妙的错误。修复该错误的代码如下:
ifeq (x$(DEBUG)x,$(filter x$(DEBUG)x,xYx xYesx))
--snip--
endif
而GMSL的 or 运算符让代码更加清晰:
include gmsl
ifeq ($(true),$(call or,$(call seq,$(DEBUG),Y),$(call seq,$(DEBUG),Yes)))
--snip--
endif
这种方式更易于维护,它通过对两个 seq 调用进行逻辑或运算,并将结果与 $(true) 比较来实现。
3. 去除列表中的重复项
GMSL的 uniq 函数可去除列表中的重复项。GNU make自带的 sort 函数能对列表排序并去除重复项,而 uniq 则在不排序的情况下去除重复项,这在列表顺序很重要时非常有用。
例如, $(sort c b a a c) 返回 a b c ,而 $(call uniq,c b a a c) 返回 c b a 。
若要简化 PATH 变量,去除重复项并保留顺序,可使用以下代码:
include gmsl
simple-path := $(call merge,:,$(call uniq,$(call split,:,$(PATH))))
这里使用了三个GMSL函数: uniq 、 split (将字符串按指定分隔符拆分为列表,这里是冒号)和 merge (将列表合并为字符串,用指定字符分隔列表项,这里也是冒号)。
4. 自动递增版本号
软件发布时,自动递增版本号很方便。假设项目中有一个名为 version.c 的文件,其中包含当前版本号的字符串:
char * ver = "1.0.0";
理想情况下,只需输入 make major-release 、 make minor-release 或 make dot-release ,版本号的三个部分之一就能自动更新, version.c 文件也会相应改变。实现代码如下:
VERSION_C := version.c
VERSION := $(shell cat $(VERSION_C))
space :=
space +=
PARTS := $(call split,",$(subst $(space),,$(VERSION)))
VERSION_NUMBER := $(call split,.,$(word 2,$(PARTS)))
MAJOR := $(word 1,$(VERSION_NUMBER))
MINOR := $(word 2,$(VERSION_NUMBER))
DOT := $(word 3,$(VERSION_NUMBER))
major-release minor-release dot-release:
u @$(eval increment_name := $(call uc,$(subst -release,,$@)))
v @$(eval $(increment_name) := $(call inc,$($(increment_name))))
w @echo 'char * ver = "$(MAJOR).$(MINOR).$(DOT)";' > $(VERSION_C)
VERSION 变量包含 version.c 文件的内容,如 char * ver = "1.0.0"; 。 PARTS 变量是通过先去除 VERSION 中的所有空白字符,再按双引号拆分得到的列表,将 VERSION 拆分为 char*ver= 1.0.0 ; 。
所以 PARTS 是一个包含三个元素的列表,第二个元素是当前版本号,提取到 VERSION_NUMBER 中并转换为包含三个元素的列表: 1 0 0 。
接着,从 VERSION_NUMBER 中提取出 MAJOR 、 MINOR 和 DOT 变量。若 version.c 中的版本号是 1.2.3 ,则 MAJOR 为 1 , MINOR 为 2 , DOT 为 3 。
最后,为主要、次要和点版本发布定义了三个规则。这些规则使用 $(eval) 技巧,根据命令行指定的 major-release 、 minor-release 或 dot-release ,使用相同的规则体来更新主要、次要或点版本号。
以 make minor-release 且现有版本号为 1.0.0 为例:
- $(eval increment_name := $(call uc,$(subst -release,,$@))) 首先使用 $(subst) 从目标名中移除 -release ( minor-release 变为 minor )。
- 然后调用GMSL的 uc 函数(将字符串转换为大写)将 minor 变为 MINOR ,并将其存储在 increment-name 变量中。这里的关键是, increment-name 将用作要递增的变量名( MAJOR 、 MINOR 或 DOT 之一)。
- $(eval $(increment_name) := $(call inc,$($(increment_name)))) 实际执行递增操作。它使用GMSL的 inc 函数递增 increment-name 变量所代表的变量的值,并将该值设置为递增后的值。
- 最后,创建一个包含新版本号的新 version.c 文件。例如:
$ make -n major-release
echo 'char * ver = "2.0.0";' > version.c
$ make -n minor-release
echo 'char * ver = "1.1.0";' > version.c
$ make -n dot-release
echo 'char * ver = "1.0.1";' > version.c
这是从版本 1.0.0 开始,使用 -n 选项请求不同可能发布时的结果。
5. GMSL参考
GMSL 1.1.7版本涵盖逻辑运算符、整数函数、列表、字符串和集合操作函数、关联数组和命名栈等。对于每个GMSL函数类别,都有函数介绍以及列出参数和返回值的快速参考部分。最新完整参考可查看 GMSL网站 。
逻辑运算符
GMSL有布尔值 $(true) (非空字符串,实际为单个字符 T )和 $(false) (空字符串)。可使用以下运算符与这些变量或返回这些值的函数配合使用:
| 函数 | 参数 | 返回值 | 示例 |
| ---- | ---- | ---- | ---- |
| not | 单个布尔值 | 若布尔值为 $(false) 则返回 $(true) ,反之亦然 | $(call not,$(true)) 返回 $(false) |
| and | 两个布尔值 | 若两个参数都为 $(true) 则返回 $(true) | $(call and,$(true),$(false)) 返回 $(false) |
| or | 两个布尔值 | 若任一参数为 $(true) 则返回 $(true) | $(call or,$(true),$(false)) 返回 $(true) |
| xor | 两个布尔值 | 若恰好一个布尔值为真则返回 $(true) | $(call xor,$(true),$(false)) 返回 $(true) |
| nand | 两个布尔值 | not and 的值 | $(call nand,$(true),$(false)) 返回 $(true) |
| nor | 两个布尔值 | not or 的值 | $(call nor,$(true),$(false)) 返回 $(false) |
| xnor | 两个布尔值 | not xor 的值 | - |
需要注意的是,GMSL的逻辑函数 and 和 or 不是短路求值的,在执行逻辑与或或之前,这两个函数的参数都会被展开。GNU make 3.81引入的内置 and 和 or 函数是短路求值的,它们先计算第一个参数,再决定是否需要计算第二个参数。
以下是逻辑运算符的使用流程:
graph TD;
A[输入布尔值] --> B{选择逻辑运算符};
B -->|not| C[执行not操作];
B -->|and| D[执行and操作];
B -->|or| E[执行or操作];
B -->|xor| F[执行xor操作];
B -->|nand| G[执行nand操作];
B -->|nor| H[执行nor操作];
B -->|xnor| I[执行xnor操作];
C --> J[输出结果];
D --> J;
E --> J;
F --> J;
G --> J;
H --> J;
I --> J;
整数算术函数
GMSL使用将非负整数表示为 x 列表的方式进行算术运算,例如 4 表示为 x x x x 。算术库函数有两种形式:一种以整数为参数,另一种以编码参数(通过 int_encode 调用创建的 x )为参数。
| 函数 | 参数 | 返回值 | 说明 |
| ---- | ---- | ---- | ---- |
| int_decode | x表示的数字 | 该字符串表示的十进制整数 | - |
| int_encode | 人类可读的整数形式的数字 | 编码为 x 字符串的整数 | - |
| int_plus | 两个x表示的数字 | 两个数字之和的x表示 | - |
| plus | 两个整数 | 两个整数之和 | 转换为x表示并调用 int_plus |
| int_subtract | 两个x表示的数字 | 两个数字之差的x表示,若差小于0则输出错误 | - |
| subtract | 两个整数 | 两个整数之差,若差小于0则输出错误 | 转换为x表示并调用 int_subtract |
| int_multiply | 两个x表示的数字 | 两个数字之积的x表示 | - |
| multiply | 两个整数 | 两个整数之积 | 自动转换为x表示并调用 int_multiply |
| int_divide | 两个x表示的数字 | 整数除法结果的x表示 | - |
| divide | 两个整数 | 第一个参数除以第二个参数的整数结果 | 自动转换为x表示并调用 int_divide |
| int_max 、 int_min | 两个x表示的数字 | 参数的最大值或最小值的x表示 | - |
| max 、 min | 两个整数 | 整数参数的最大值或最小值 | 自动转换为x表示 |
| int_inc | x表示的数字 | 该数字加1后的x表示 | - |
| inc | 整数 | 参数加1后的结果 | - |
| int_dec | x表示的数字 | 该数字减1后的x表示 | - |
| dec | 整数 | 参数减1后的结果 | - |
| int_double | x表示的数字 | 该数字加倍后的x表示 | - |
| double | 整数 | 整数乘以2的结果 | 内部转换为x表示并调用 int_double |
| int_halve | x表示的数字 | 该数字减半后的x表示 | - |
| halve | 整数 | 整数除以2的结果 | - |
对于复杂计算,建议使用 int_* 形式,对输入进行一次编码,对输出进行一次解码;对于简单计算,可使用直接形式。
整数算术函数的使用流程如下:
graph TD;
A[输入整数或x表示的数字] --> B{选择函数类型};
B -->|直接形式| C[执行对应直接函数];
B -->|int_*形式| D[编码输入];
D --> E[执行对应int_*函数];
C --> F[输出结果];
E --> G[解码结果];
G --> F;
整数比较函数
所有整数比较函数都返回 $(true) 或 $(false) :
| 函数 | 参数 | 返回值 | 说明 |
| ---- | ---- | ---- | ---- |
| int_gt 、 int_gte 、 int_lt 、 int_lte 、 int_eq 、 int_ne | 两个x表示的数字 | $(true) 或 $(false) | 比较x表示的数字 |
| gt 、 gte 、 lt 、 lte 、 eq 、 ne | 两个整数 | $(true) 或 $(false) | 比较十进制整数 |
这些函数可与GNU make和GMSL函数以及期望布尔值的指令(如GMSL逻辑运算符)一起使用。
其他整数函数
- sequence :用于生成数字序列。
- 参数 :两个整数
- 返回值 :若
arg1 >= arg2,返回[arg1 arg2];若arg2 > arg1,返回[arg2 arg1]。 - 示例 :
$(call sequence,10,15)返回10 11 12 13 14 15;$(call sequence,15,10)返回15 14 13 12 11 10。
- dec2hex、dec2bin、dec2oct :用于在十进制数与十六进制、二进制和八进制形式之间进行转换。
- 参数 :一个整数
- 返回值 :十进制参数转换后的十六进制、二进制或八进制形式。
- 示例 :
$(call dec2hex,42)返回2a。
这些杂项函数可进行基数转换和数字序列生成,在某些情况下很有用。
深入了解GNU Make标准库
6. 逻辑运算符的实际应用
在实际应用中,逻辑运算符可以帮助我们实现各种条件判断。例如,我们可以使用 or 运算符来检查两个文件是否存在:
$(call or,$(wildcard /tmp/foo),$(wildcard /tmp/bar))
这里使用 $(wildcard) 函数来检查 /tmp/foo 和 /tmp/bar 文件是否存在。如果文件存在, $(wildcard) 会返回文件名;如果不存在,则返回空字符串。 or 运算符会将文件名视为 $(true) ,空字符串视为 $(false) 。
如果我们希望只使用 $(true) 和 $(false) 这样的值,可以定义一个 make-bool 函数:
make-bool = $(if $(strip $1),$(true),$(false))
这个函数会将任何非空字符串(去除空白后)转换为 $(true) ,空字符串(或仅含空白的字符串)转换为 $(false) 。例如,判断当前月份是否为一月:
january-now := $(call make-bool,$(filter Jan,$(shell date)))
这里先运行 date 命令,提取其中的 Jan 单词,再使用 make-bool 函数将其转换为布尔值。
我们还可以定义一个通用函数来判断列表中是否包含某个单词:
contains-word = $(call make-bool,$(filter $1,$2))
january-now := $(call contains-word,Jan,$(shell date))
使用 contains-word 函数可以重新定义 january-now 。
逻辑运算符在实际应用中的操作步骤如下:
1. 确定需要判断的条件和涉及的元素。
2. 根据条件选择合适的逻辑运算符。
3. 若需要,使用 make-bool 函数将结果转换为标准布尔值。
4. 根据逻辑运算结果进行相应操作。
7. 整数函数的应用案例
整数函数在很多场景下都有实际应用,下面通过几个案例来详细说明。
版本号递增案例
前面已经介绍了如何自动递增版本号,这里再详细梳理一下操作步骤:
1. 读取版本文件 :
VERSION_C := version.c
VERSION := $(shell cat $(VERSION_C))
- 解析版本号 :
space :=
space +=
PARTS := $(call split,",$(subst $(space),,$(VERSION)))
VERSION_NUMBER := $(call split,.,$(word 2,$(PARTS)))
MAJOR := $(word 1,$(VERSION_NUMBER))
MINOR := $(word 2,$(VERSION_NUMBER))
DOT := $(word 3,$(VERSION_NUMBER))
- 定义递增规则 :
major-release minor-release dot-release:
u @$(eval increment_name := $(call uc,$(subst -release,,$@)))
v @$(eval $(increment_name) := $(call inc,$($(increment_name))))
w @echo 'char * ver = "$(MAJOR).$(MINOR).$(DOT)";' > $(VERSION_C)
具体流程如下:
graph TD;
A[读取版本文件] --> B[解析版本号];
B --> C{选择版本递增类型};
C -->|major-release| D[递增MAJOR];
C -->|minor-release| E[递增MINOR];
C -->|dot-release| F[递增DOT];
D --> G[更新版本文件];
E --> G;
F --> G;
数值计算案例
假设我们需要计算两个数的和、差、积、商等,可以使用GMSL的整数算术函数。以下是一个简单的示例:
include gmsl
num1 := 5
num2 := 3
sum := $(call plus,$(num1),$(num2))
diff := $(call subtract,$(num1),$(num2))
product := $(call multiply,$(num1),$(num2))
quotient := $(call divide,$(num1),$(num2))
$(info Sum: $(sum))
$(info Difference: $(diff))
$(info Product: $(product))
$(info Quotient: $(quotient))
操作步骤如下:
1. 定义需要计算的整数。
2. 根据计算需求选择合适的整数算术函数。
3. 调用函数进行计算。
4. 输出计算结果。
8. 总结与建议
GNU Make标准库提供了丰富的功能,包括逻辑运算符、整数函数等,这些功能可以帮助我们更高效地编写Makefile。在使用过程中,我们可以根据具体需求选择合适的函数。
对于逻辑运算符,在编写复杂条件判断时,使用GMSL的逻辑运算符可以使代码更清晰、易维护。同时, make-bool 函数可以帮助我们统一布尔值的表示。
对于整数函数,在进行数值计算时,如果是简单计算,可以直接使用以整数为参数的函数;如果是复杂计算,建议使用 int_* 形式的函数,对输入进行一次编码,对输出进行一次解码,以提高性能。
在实际应用中,我们可以结合这些函数实现各种功能,如版本号自动递增、文件存在性检查等。通过合理运用GNU Make标准库,我们可以提高开发效率,减少错误。
以下是使用GMSL的一些建议列表:
1. 深入理解函数的参数和返回值,确保正确使用。
2. 对于复杂计算,优先考虑使用 int_* 形式的函数。
3. 使用 make-bool 函数统一布尔值表示,避免因空白字符等问题导致的错误。
4. 在编写Makefile时,合理组织代码,提高代码的可读性和可维护性。
超级会员免费看
4

被折叠的 条评论
为什么被折叠?



