语言: 日本語 | Español | Français | Português | 中文 | English
上一章 | 下一章 | 目录 | 英文原版(gnu.org)

12 GNU make 的扩展

GNU make 内置了大量便利的函数,并提供许多高级功能。但它并不包含一门完整的编程语言,因此存在固有的局限。这些局限有时可以通过使用 shell 函数调用外部程序来克服,不过这种方式往往效率不高。

当 GNU make 的内置功能无法满足你的需求时,有两种扩展 make 的途径。其一,在提供了相应支持的系统上,可以把 GNU Guile 作为内嵌的脚本语言来使用(参见与 GNU Guile 的集成)。其二,在支持动态加载对象的系统上,可以用任意语言(只要能够编译成这样的对象)编写你自己的扩展,并加载它来提供扩展功能(参见加载动态对象)。

12.1 与 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 脚本中使用。

12.1.1 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 的条件判断中,任何非空字符串都被视为真。

symbol
number

符号或数值被转换为该符号或数值的字符串表示。

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 中出现语法错误。

12.1.2 从 Guile 到 make 的接口

除了 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 函数展开。

12.1.3 在 make 中使用 Guile 的示例

下面给出一个非常简单的示例,使用 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 变量中,这样就能同时处理多个输出文件。

12.2 加载动态对象

警告: 在本版本的 GNU Make 中,load 指令 (directive) 及扩展能力被定位为「技术预览(technology preview)」。我们鼓励你试用此功能,并非常欢迎你提供反馈。但我们无法保证在下一个版本中维持向后兼容性。要扩展 GNU Make,请考虑改用 GNU Guile(参见 guile 函数)。

许多操作系统都提供了动态加载已编译对象的机制。如果你的系统提供了这种机制,GNU make 就能利用它在运行时加载动态对象,从而提供可由你的 makefile 调用的新功能。

加载动态对象使用 load 指令 (directive)。对象一旦被加载,就会调用一个「setup」函数,使该对象能够初始化自身,并向 GNU make 注册新的功能。例如,某个动态对象可能包含新的 make 函数,而「setup」函数会把它们注册到 GNU make 的函数处理系统中。

12.2.1 load 指令

要把对象加载到 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 变量中,因此可以通过查看该变量来判断加载是否成功。

12.2.2 已加载对象的重新生成方式

已加载的对象会经历与 makefile 相同的重新生成过程(参见 Makefile 的重新生成方式)。如果任何已加载对象被重新生成,make 就会从头开始,重新读取所有 makefile,并再次重新加载这些对象文件。为支持这一点,被加载的对象方面无需做任何特殊处理。

提供重新构建被加载对象所需的规则,是 makefile 作者的责任。

12.2.3 已加载对象的接口

警告: 要让此功能发挥作用,你的扩展将需要调用 GNU make 内部的各种函数。本版本所提供的编程接口不应被视为稳定的:在未来版本的 GNU make 中,函数可能会被添加、移除,或者其调用签名和实现发生变化。

要发挥作用,已加载对象必须能够与 GNU make 交互。这种交互既包括已加载对象向 makefile 提供的接口,也包括 make 向已加载对象提供的接口(用于操纵 make 的运行)。

已加载对象与 make 之间的接口由 C 语言头文件 gnumake.h 定义。所有用 C 编写的已加载对象都应当包含这个头文件。任何不用 C 编写的已加载对象,都需要实现该头文件中定义的接口。

通常,已加载对象会在其 setup 函数中使用 gmk_add_function 例程注册一个或多个新的 GNU make 函数。这些 make 函数的实现可以利用 gmk_expandgmk_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 提供的机制

GNU make 公开了一些机制,供已加载对象使用。它们通常在 setup 函数和/或通过 gmk_add_function 注册的函数中运行,用于获取或修改 make 所处理的数据。

gmk_expand

该函数接受一个字符串,并使用 make 的展开规则将其展开。展开的结果以一个以 null 结尾的字符串缓冲区返回。处理完毕后,调用方有责任以指向所返回缓冲区的指针为参数调用 gmk_free

gmk_eval

该函数接受一个缓冲区,并将其作为一段 makefile 语法来求值。该函数可用于定义新变量、新规则等。它等同于使用 makeeval 函数。

请注意,gmk_eval 与配合 eval 函数对字符串调用 gmk_expand 之间存在差别。在后者的情况下,字符串会被展开两次:一次由 gmk_expand 展开,另一次由 eval 函数展开。而使用 gmk_eval 时,缓冲区至多只会被展开一次(在 make 的解析器读取它时)。

内存管理

某些系统可能采用不同的内存管理方案。因此,你绝不应当把自己直接分配的内存传递给任何 make 函数,也不应当试图直接释放任何 make 函数返回给你的内存。相反,请使用 gmk_allocgmk_free 函数。

尤其是,用 gmk_add_function 注册的函数返回给 make 的字符串,必须使用 gmk_alloc 来分配;而 makegmk_expand 函数返回的字符串,(在不再需要时)必须使用 gmk_free 来释放。

gmk_alloc

返回指向新分配缓冲区的指针。该函数总是返回有效的指针;如果内存不足,make 会退出。gmk_alloc 不会初始化所分配的内存。

gmk_free

释放 make 返回给你的缓冲区。gmk_free 函数一旦返回,该字符串就不再有效。如果向 gmk_free 传入 NULL,则不执行任何操作。

12.2.4 已加载对象的示例

假设我们想编写一个新的 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

上一章 | 下一章 | 目录 | 英文原版(gnu.org)