GNU make 内置了大量便利的函数,并提供许多高级功能。但它并不包含一门完整的编程语言,因此存在固有的局限。这些局限有时可以通过使用 shell 函数调用外部程序来克服,不过这种方式往往效率不高。
当 GNU make 的内置功能无法满足你的需求时,有两种扩展 make 的途径。其一,在提供了相应支持的系统上,可以把 GNU Guile 作为内嵌的脚本语言来使用(参见与 GNU Guile 的集成)。其二,在支持动态加载对象的系统上,可以用任意语言(只要能够编译成这样的对象)编写你自己的扩展,并加载它来提供扩展功能(参见加载动态对象)。
GNU make 可以在构建时加入对 GNU Guile 的支持,作为内嵌的扩展语言。Guile 是 Scheme 语言的一个实现。关于 GNU Guile、Scheme 语言及其特性的介绍超出了本手册的范围,请参阅 GNU Guile 和 Scheme 的文档。
要判断你的 make 是否包含 Guile 支持,可以检查 .FEATURES 变量;如果 Guile 支持可用,该变量中会包含 guile 这个词。
与 Guile 的集成新增了一个 make 函数:guile 函数。guile 函数接受一个参数,该参数首先由 make 按常规方式展开,然后传递给 GNU Guile 的求值器。求值器的结果被转换为字符串,并用作 makefile 中 guile 函数的展开结果。
此外,GNU make 还公开了一些 Guile 过程,供 Guile 脚本中使用。
make 中只有一种「数据类型」,即字符串。而 GNU Guile 则提供了丰富多样的数据类型。make 与 GNU Guile 之间接口的一个重要方面,就是把 Guile 的数据类型转换为 make 的字符串。
这种转换在两种场合下会涉及到。其一,当 makefile 调用 guile 函数来求值一个 Guile 表达式时,求值结果必须转换为 make 字符串,以便 make 能进一步对其求值。其二,当 Guile 脚本调用 make 所公开的某个过程时,传给该过程的参数必须被转换为字符串。
Guile 类型到 make 字符串的转换规则如下:
#f假(False)被转换为空字符串:在 make 的条件判断中,空字符串被视为假。
#t真(True)被转换为字符串「#t」:在 make 的条件判断中,任何非空字符串都被视为真。
symbolnumber符号或数值被转换为该符号或数值的字符串表示。
character可打印字符被转换为相同的字符。
string仅由可打印字符组成的字符串被转换为相同的字符串。
list列表按照上述规则递归地转换。这意味着任何带结构的列表都会被展平(也就是说,「'(a b (c d) e)」这样的结果会被转换为 make 字符串「a b c d e」)。
other任何其他 Guile 类型都会导致错误。在未来版本的 make 中,可能会支持转换其他 Guile 类型。
之所以把「#f」转换为空字符串、把「#t」转换为非空字符串「#t」,是为了让你能够把 Guile 的布尔结果直接用作 make 的布尔条件。例如:
$(if $(guile (access? "myfile" R_OK)),$(info myfile exists))
由于存在这些转换规则,你必须考虑 Guile 脚本的返回结果,因为该结果会被转换成字符串并由 make 解析。如果脚本没有自然的返回结果(也就是说,脚本只是为了其副作用而存在),你应当把「#f」作为最后一个表达式加进去,以避免 makefile 中出现语法错误。
除了 makefile 中可用的 guile 函数之外,make 还公开了一些过程,供你的 Guile 脚本使用。启动时,make 会创建一个新的 Guile 模块 gnu make,并从该模块中将以下过程作为公开接口导出:
gmk-expand该过程接受一个参数,并将其转换为字符串。该字符串会按照通常的 make 展开规则由 make 展开。展开的结果被转换为 Guile 字符串,并作为该过程的结果返回。
gmk-eval该过程接受一个参数,并将其转换为字符串。该字符串会被 make 当作 makefile 来求值。这与 eval 函数(参见 eval 函数)所提供的能力相同。gmk-eval 过程的结果始终是空字符串。
请注意,gmk-eval 与配合 eval 函数使用 gmk-expand 并不完全相同。在后者的情况下,被求值的字符串会被展开两次:先由 gmk-expand 展开,接着再由 eval 函数展开。
下面给出一个非常简单的示例,使用 GNU Guile 来管理对文件的写入。这些 Guile 过程只是打开一个文件,允许向文件写入(每行写入一个字符串),然后关闭文件。请注意,由于诸如 Guile 端口之类的复杂值无法保存在 make 变量中,这里我们把端口保留为 Guile 解释器内部的一个全局变量。
你可以用 define/endef 创建一个 Guile 脚本,然后用 guile 函数将其内部化(internalize),从而轻松创建 Guile 函数:
define GUILEIO ;; GNU Make 的一个简单 Guile 输入输出库 (define MKPORT #f) (define (mkopen name mode) (set! MKPORT (open-file name mode)) #f) (define (mkwrite s) (display s MKPORT) (newline MKPORT) #f) (define (mkclose) (close-port MKPORT) #f) #f endef # 内部化 Guile 输入输出函数 $(guile $(GUILEIO))
如果你的 Guile 支持代码数量较多,可以考虑把它保存在另一个文件中(例如 guileio.scm),然后在 makefile 中使用 guile 函数加载它:
$(guile (load "guileio.scm"))
这种方法的好处是,在编辑 guileio.scm 时,你的编辑器会知道该文件包含的是 Scheme 语法,而不是 makefile 语法。
现在你就可以用这些 Guile 函数来创建文件了。假设你需要处理一个非常大的列表,它无法塞进命令行,但你所使用的工具同样能接受该列表作为输入:
prog: $(PREREQS)
@$(guile (mkopen "tmp.out" "w")) \
$(foreach X,$^,$(guile (mkwrite "$(X)"))) \
$(guile (mkclose))
$(LINK) < tmp.out
当然,你也可以编写一套更为完备的文件操作过程。例如,你可以为每个输出文件选定一个符号,并将其用作哈希表的键(以端口作为值),然后返回该符号以保存到 make 变量中,这样就能同时处理多个输出文件。
|
许多操作系统都提供了动态加载已编译对象的机制。如果你的系统提供了这种机制,GNU make 就能利用它在运行时加载动态对象,从而提供可由你的 makefile 调用的新功能。
加载动态对象使用 load 指令 (directive)。对象一旦被加载,就会调用一个「setup」函数,使该对象能够初始化自身,并向 GNU make 注册新的功能。例如,某个动态对象可能包含新的 make 函数,而「setup」函数会把它们注册到 GNU make 的函数处理系统中。
要把对象加载到 GNU make 中,需要在 makefile 中写入 load 指令。load 指令的语法如下:
load object-file …
或者:
load object-file(symbol-name) …
文件 object-file 由 GNU make 动态加载。如果 object-file 不含目录路径,则首先在当前目录中查找。如果在那里找不到,或者含有目录路径,则会搜索系统特定的路径。如果加载因任何原因失败,make 都会打印一条消息并退出。
如果加载成功,make 会调用一个初始化函数。
如果提供了 symbol-name,它将被用作初始化函数的名称。
如果没有提供 symbol-name,初始化函数的名称按如下方式生成:取 object-file 的基本文件名,截取到第一个不属于合法符号名字符的字符为止(字母数字和下划线是合法的符号名字符)。在这个前缀后面再附加后缀 _gmk_setup,即得到函数名。
一条 load 指令可以加载多个对象文件,并且 load 参数的两种书写形式也可以在同一条指令中混用。
初始化函数会被传入调用该 load 操作处的文件名和行号。它必须返回一个 int 类型的值,失败时为 0,成功时为非 0 值。如果返回值为 -1,则 GNU Make 不会尝试重新构建该对象文件(参见已加载对象的重新生成方式)。
例如:
load ../mk_funcs.so
这会加载动态对象 ../mk_funcs.so。对象加载之后,make 会调用(假定由该共享对象定义的)函数 mk_funcs_gmk_setup。
另一方面:
load ../mk_funcs.so(init_mk_func)
这会加载动态对象 ../mk_funcs.so。对象加载之后,make 会调用函数 init_mk_func。
无论某个对象文件在 load 指令中出现多少次,它只会被加载一次(其 setup 函数也只会被调用一次)。
对象成功加载之后,其文件名会被追加到 .LOADED 变量中。
如果你希望加载动态对象失败时不被报告为错误,可以使用 -load 指令来代替 load。当对象加载失败时,GNU make 不会失败,也不会产生任何消息。加载失败的对象不会被加入到 .LOADED 变量中,因此可以通过查看该变量来判断加载是否成功。
已加载的对象会经历与 makefile 相同的重新生成过程(参见 Makefile 的重新生成方式)。如果任何已加载对象被重新生成,make 就会从头开始,重新读取所有 makefile,并再次重新加载这些对象文件。为支持这一点,被加载的对象方面无需做任何特殊处理。
提供重新构建被加载对象所需的规则,是 makefile 作者的责任。
|
要发挥作用,已加载对象必须能够与 GNU make 交互。这种交互既包括已加载对象向 makefile 提供的接口,也包括 make 向已加载对象提供的接口(用于操纵 make 的运行)。
已加载对象与 make 之间的接口由 C 语言头文件 gnumake.h 定义。所有用 C 编写的已加载对象都应当包含这个头文件。任何不用 C 编写的已加载对象,都需要实现该头文件中定义的接口。
通常,已加载对象会在其 setup 函数中使用 gmk_add_function 例程注册一个或多个新的 GNU make 函数。这些 make 函数的实现可以利用 gmk_expand 和 gmk_eval 等例程来完成其工作,并可视需要返回一个字符串作为该函数展开的结果。
每个动态扩展都应当定义全局符号 plugin_is_GPL_compatible,以声明它是在与 GPL 兼容的许可证下发布的。如果该符号不存在,make 在尝试加载你的扩展时会发出致命错误并退出。
该符号声明的类型应当是 int。不过,它无需被放置在任何特定的段(section)中。代码只是声明该符号存在于全局作用域中而已。像下面这样写就足够了:
int plugin_is_GPL_compatible;
gmk_floc这个结构体表示一个文件名/位置(location)对。在定义各类项目时会提供它,以便 GNU make 在必要时能够稍后告知用户该定义是在何处进行的。
目前,makefile 调用已加载对象所提供操作的方式只有一种,即通过 make 的函数调用接口。已加载对象可以注册一个或多个新函数,之后就能像调用其他任何函数一样,从 makefile 中调用它们。
使用 gmk_add_function 来创建一个新的 make 函数。它的参数如下:
name函数名。这是 makefile 用来调用该函数的名字。名称长度必须在 1 到 255 个字符之间,且只能包含字母数字、句点(「.」)、连字符(「-」)和下划线(「_」)字符。它不能以句点开头。
func_ptr指向某个函数的指针,当 make 在 makefile 中展开该函数时会调用它。这个函数必须由已加载对象定义。
min_args该函数所接受的最小参数个数。必须在 0 到 255 之间。GNU make 会对此进行检查,如果在参数过少的情况下调用该函数,会在调用 func_ptr 之前使其失败。
max_args该函数所接受的最大参数个数。必须在 0 到 255 之间。GNU make 会对此进行检查,如果在参数过多的情况下调用该函数,会在调用 func_ptr 之前使其失败。如果该值为 0,则参数个数不受限制。如果该值大于 0,则必须大于或等于 min_args。
flags指定该函数如何运行的标志。所需的标志应当用 OR 组合在一起。如果给定 GMK_FUNC_NOEXPAND 标志,则在调用函数之前不会展开函数参数;否则会先展开参数。
注册到 make 中的函数必须符合 gmk_func_ptr 类型。它会带着三个参数被调用,即 name(函数的名称)、argc(函数的参数个数)以及 argv(指向函数各参数的指针的数组)。最后一个指针(即 argv[argc])将为空(0)。
函数的返回值就是展开该函数的结果。如果函数没有展开出任何内容,返回值可以为空。否则,它必须是指向用 gmk_alloc 创建的字符串的指针。函数一旦返回,该字符串就归 make 所有,make 会在适当的时候将其释放;已加载对象无法再访问该字符串。
GNU make 公开了一些机制,供已加载对象使用。它们通常在 setup 函数和/或通过 gmk_add_function 注册的函数中运行,用于获取或修改 make 所处理的数据。
gmk_expand该函数接受一个字符串,并使用 make 的展开规则将其展开。展开的结果以一个以 null 结尾的字符串缓冲区返回。处理完毕后,调用方有责任以指向所返回缓冲区的指针为参数调用 gmk_free。
gmk_eval该函数接受一个缓冲区,并将其作为一段 makefile 语法来求值。该函数可用于定义新变量、新规则等。它等同于使用 make 的 eval 函数。
请注意,gmk_eval 与配合 eval 函数对字符串调用 gmk_expand 之间存在差别。在后者的情况下,字符串会被展开两次:一次由 gmk_expand 展开,另一次由 eval 函数展开。而使用 gmk_eval 时,缓冲区至多只会被展开一次(在 make 的解析器读取它时)。
某些系统可能采用不同的内存管理方案。因此,你绝不应当把自己直接分配的内存传递给任何 make 函数,也不应当试图直接释放任何 make 函数返回给你的内存。相反,请使用 gmk_alloc 和 gmk_free 函数。
尤其是,用 gmk_add_function 注册的函数返回给 make 的字符串,必须使用 gmk_alloc 来分配;而 make 的 gmk_expand 函数返回的字符串,(在不再需要时)必须使用 gmk_free 来释放。
gmk_alloc返回指向新分配缓冲区的指针。该函数总是返回有效的指针;如果内存不足,make 会退出。gmk_alloc 不会初始化所分配的内存。
gmk_free释放 make 返回给你的缓冲区。gmk_free 函数一旦返回,该字符串就不再有效。如果向 gmk_free 传入 NULL,则不执行任何操作。
假设我们想编写一个新的 GNU make 函数,用于创建一个临时文件并返回其名称。我们希望该函数把一个前缀作为参数。首先,我们可以把这个函数写入文件 mk_temp.c 中:
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <gnumake.h>
int plugin_is_GPL_compatible;
char *
gen_tmpfile(const char *nm, int argc, char **argv)
{
int fd;
/* Compute the size of the filename and allocate space for it. */
int len = strlen (argv[0]) + 6 + 1;
char *buf = gmk_alloc (len);
strcpy (buf, argv[0]);
strcat (buf, "XXXXXX");
fd = mkstemp(buf);
if (fd >= 0)
{
/* Don't leak the file descriptor. */
close (fd);
return buf;
}
/* Failure. */
fprintf (stderr, "mkstemp(%s) failed: %s\n", buf, strerror (errno));
gmk_free (buf);
return NULL;
}
int
mk_temp_gmk_setup (const gmk_floc *floc)
{
printf ("mk_temp plugin loaded from %s:%lu\n", floc->filenm, floc->lineno);
/* Register the function with make name "mk-temp". */
gmk_add_function ("mk-temp", gen_tmpfile, 1, 1, 1);
return 1;
}
接下来,我们将编写一个 Makefile,用来构建这个共享对象、加载它并使用它:
all:
@echo Temporary file: $(mk-temp tmpfile.)
load mk_temp.so
mk_temp.so: mk_temp.c
$(CC) -shared -fPIC -o $@ $<
在 MS-Windows 上,由于共享对象的生成方式有其特殊之处,编译器需要扫描构建 make 时所生成的导入库(import library)(其名称通常为 libgnumake-version.dll.a,其中 version 是加载对象 API 的版本)。因此,在 Windows 上生成共享对象的命令 (recipe) 将如下所示(此处假定 API 版本为 1):
mk_temp.dll: mk_temp.c
$(CC) -shared -o $@ $< -lgnumake-1
现在,当你运行 make 时,会看到类似下面这样的输出:
$ make mk_temp plugin loaded from Makefile:4 cc -shared -fPIC -o mk_temp.so mk_temp.c Temporary filename: tmpfile.A7JEwd