makefile 学习笔记 六:makefile 写规则

详细内容见 《GNU make》 4 Writing Rules 章节。

一个规则出现在 makefile 中,并说明何时以及如何重制某些文件,称为规则的目标(通常每个规则只有一个目标)它列出了作为目标先决条件的其他文件,以及用于创建或更新目标的配方

规则的顺序并不重要,除了确定默认目标:make 要考虑的目标(如果未以其他方式指定目标)默认目标是第一个 makefile 中的第一个规则的目标。如果第一个规则有多个目标,则只有第一个目标作为默认值。有两个例外:以句号开头的目标不是默认值,除非它包含一个或多个斜杠 ‘/’。并且,定义模式规则的目标对默认目标没有影响。

因此,我们通常在编写makefile时,第一条规则是用来编译整个程序或makefile所描述的所有程序的(通常有一个叫做 ‘all’ 的目标)。

一、规则语法

一般来说,规则是这样的:

targets : prerequisites
        recipe
        …

或者像这样:

targets : prerequisites ; recipe
        recipe
        …

目标是文件名,用空格分隔可以使用通配符,形式为 a(m) 的名称表示存档文件 a 中的成员 m。通常每个规则只有一个目标,但偶尔也有理由设置更多目标

配方行以一个制表符(或 .RECIPEPREFIX 变量值中的第一个字符)开始。第一个配方行可能出现在先决条件之后的行上,带有制表符,或者可能显示在同一行上,带有分号。无论哪种方式,效果都是一样的。配方的语法还有其他差异。

因为 ‘$’ 符号用于开始进行变量引用,所以如果您确实希望在目标或先决条件中使用 ‘$’ 符号,则必须编写两个 ‘$’ 符号,’$$。如果启用了二次展开,并且希望在先决条件列表中有一个 ‘$’ 符号,则必须实际编写 4 个 $ 符号( $$$$ )。

你可以通过插入一个反斜杠后跟一个换行符分割一个长行,但这不是必需的,因为在 makefile 中对行长度没有限制。

规则告诉制定两件事:目标何时过期,以及如何在必要时更新它们

过期的标准是根据先决条件指定的,先决条件由用空格分隔的文件名组成(这里也允许通配符和存档成员)。如果目标不存在,或者目标比任何先决条件都早(通过最后修改时间的比较),那么该目标就是过时的。其想法是,目标文件的内容是根据先决条件中的信息计算的,因此,如果任何先决条件发生更改,则现有目标文件的内容不一定有效。

如何更新由配方指定。这是由 shell 执行的一行或多行(通常为"sh"),但具有一些额外的功能。

二、先决条件的类型

1、概述

实际上,GNU make 理解了两种不同类型的先决条件正常先决条件(如上一节所述)和仅顺序先决条件。正常的先决条件会做出两个语句:首先,它强加一个调用配方的顺序:在运行目标的配方之前,目标的所有先决条件的配方都将完成。其次,它强加了依赖关系如果任何先决条件比目标新,则目标被视为已过期,必须重新构建

通常,这正是您想要的:如果目标的先决条件已更新,则目标也应更新

但是,有时会遇到这样的情况,即您希望对要调用的规则施加特定的顺序,而不强制在执行其中一个规则时更新目标。在这种情况下,您需要定义仅顺序的先决条件。可以通过在先决条件列表中放置管道符号(|)来指定仅顺序先决条件:管道符号左侧的任何先决条件都是正常的;

argets : normal-prerequisites | order-only-prerequisites

当然,正常的先决条件部分可能是空的。此外,您仍然可以为同一目标声明多行先决条件:它们被适当地追加(正常先决条件将附加到正常先决条件列表中;仅顺序先决条件将附加到仅顺序先决条件列表中)。请注意,如果您将同一个文件声明为普通先决条件和仅顺序先决条件,则普通先决条件优先(因为它们具有仅顺序先决条件行为的严格超集)。

考虑一个示例,其中您的目标将放置在单独的目录中,并且在运行 make 之前该目录可能不存在。在这种情况下,您希望在将任何目标放入目录之前创建目录,但是,由于每当添加、删除或重命名文件时,目录上的时间戳都会更改,因此我们当然不希望在目录的时间戳更改时重新生成所有目标。一种管理方法是使用仅顺序先决条件:在所有目标上使目录成为仅顺序先决条件。

OBJDIR := objdir
OBJS := $(addprefix $(OBJDIR)/,foo.o bar.o baz.o)

$(OBJDIR)/%.o : %.c
        $(COMPILE.c) $(OUTPUT_OPTION) $<

all: $(OBJS)

$(OBJS): | $(OBJDIR)

$(OBJDIR):
        mkdir $(OBJDIR)

如果需要,创建objdir目录的规则将在创建 ‘.o’ 之前运行,但不会创建 '.o,因为objdir目录的时间戳改变了。

2、总结

通常,如果规则中依赖文件中的任何一个被更新,则规则的目标相应地也应该被更新。有时,需要定义一个这样的规则,在更新目标(目标已经存在)时只需要根据依赖文件中的部分来决定目标是否需要被重建,而不是在依赖文件的任何一个被修改后都重建目标。为了实现这一目标,对依赖文件进行分类:

  • 依赖文件被更新后,需要更新规则的目标;这类目标命名为 normal-prerequisites。
  • 依赖文件被更新后,可不更新规则的目标;这类目标命名为 order-only-prerequisites。

语法格式如下:

argets : normal-prerequisites | order-only-prerequisites

3、测试

测试思路:通过 main.c 和 a.c、a.h 生成 main。如果 main.c 更改,则重新生成 main;如果 a.c 更改,则不重新生成 main。

makefile 文件内容

main: main.c | a.c
	gcc main.c a.c -o main

测试:

onlylove@ubuntu:~/My/makefile/05$ ls -l
total 16
-rw-rw-r-- 1 onlylove onlylove 92 Mar 29 02:27 a.c
-rw-rw-r-- 1 onlylove onlylove 30 Mar 29 02:26 a.h
-rw-rw-r-- 1 onlylove onlylove 99 Mar 29 02:26 main.c
-rw-rw-r-- 1 onlylove onlylove 43 Mar 29 02:27 makefile
onlylove@ubuntu:~/My/makefile/05$ cat main.c 
#include "a.h"

int main(int argc, char *argv[])
{
    a_print("hello world");
    
    return 0;
}onlylove@ubuntu:~/My/makefile/05$ cat a.c
#include <stdio.h>

int a_print(unsigned char *s)
{
    printf("%s\r\n",s);

    return 0;
}onlylove@ubuntu:~/My/makefile/05$ cat a.h 
int a_print(unsigned char *s);onlylove@ubuntu:~/My/makefile/05$ 
onlylove@ubuntu:~/My/makefile/05$ 
onlylove@ubuntu:~/My/makefile/05$ make
gcc main.c a.c -o main
onlylove@ubuntu:~/My/makefile/05$ ./main 
hello world
onlylove@ubuntu:~/My/makefile/05$ 
onlylove@ubuntu:~/My/makefile/05$ 


# 修改 a.c 内容
onlylove@ubuntu:~/My/makefile/05$ 
onlylove@ubuntu:~/My/makefile/05$ cat a.c
#include <stdio.h>

int a_print(unsigned char *s)
{
    printf("%s\r\n",s);
    printf("%s\r\n",s);

    return 0;
}onlylove@ubuntu:~/My/makefile/05$ make
make: 'main' is up to date.
onlylove@ubuntu:~/My/makefile/05$ ./main 
hello world
onlylove@ubuntu:~/My/makefile/05$ 

三、在文件名中使用通配符

单个文件名可以使用通配符指定多个文件make 中的通配符为 ‘*’、’?’ 和 ‘[…]’,与 Bourne shell 中的通配符相同。例如,*.c 指定名称以 ‘.c’ 结尾的所有文件的列表(在工作目录中)。

文件名开头的字符"~"也具有特殊意义。如果单独使用,或者后跟斜杠,则表示您的主目录。例如,~/bin 扩展为 /home/you/bin。如果"~"后跟一个单词,则字符串表示由该单词命名的用户的主目录。例如,~john/bin 扩展为 /home/john/bin。在没有每个用户的主目录的系统(如 MS-DOS 或 MS-Windows)上,可以通过设置环境变量 HOME 来模拟此功能。

通配符扩展是通过在目标和先决条件中自动执行的在配方中,shell负责通配符展开。在其他上下文中,仅当您使用通配符函数显式请求通配符扩展时,才会发生通配符扩展。

通配符的特殊意义可以通过在其前面加上反斜杠来关闭。因此,foo\*bar 将指向一个名称由 ‘foo’、’*’ 和 ‘bar’ 组成的特定文件。

1、通配符示例

通配符可以在规则的配方中使用,它们由shell展开。例如,这里有一个删除所有对象文件的规则:

clean:
        rm -f *.o

通配符在规则的先决条件中也很有用。在 makefile 中使用以下规则,‘make print’ 将打印自上次打印以来更改过的所有 ‘.c’ 文件:

print: *.c
        lpr -p $?
        touch print

该规则使用 print 作为空的目标文件(自动变量 ‘$?’ 用于只打印那些已更改的文件);

在定义变量时,不会发生通配符展开。因此,如果你这样写:

objects = *.o

那么变量对象的值就是实际的字符串 ‘*.o’。但是,如果在目标或先决条件中使用对象的值,则将在那里进行通配符扩展。如果在配方中使用对象的值,则在配方运行时,shell 可能会执行通配符展开。若要将对象设置为展开,请使用:

objects := $(wildcard *.o)

2、使用通配符的缺陷

下面是一个使用通配符展开的简单方法的例子,它没有达到您的目的。假设您想说可执行文件 foo 是由目录中的所有对象文件组成的,并编写了下面的代码:

objects = *.o

foo : $(objects)
        cc -o foo $(CFLAGS) $(objects)

对象的值是实际的字符串 ‘*.o’。通配符扩展发生在 foo 的规则中,因此每个现有的 ‘.o’ 文件都成为 foo 的先决条件,并在必要时重新编译。

但是如果你删除了所有的 ‘.o’ 文件?当通配符不匹配任何文件时,它就保持原样,因此 foo 将依赖于名称奇怪的文件 *.o。由于没有这样的文件可能存在,make 会给你一个错误,说它无法弄清楚如何制作 *.o。这不是你想要的!

实际上,可以通过通配符扩展获得所需的结果,但需要更复杂的技术,包括通配符函数和字符串替换。

微软操作系统(MS-DOS和MS-Windows)使用反斜杠来分隔路径名中的目录,就像这样:

 c:\foo\bar\baz.c

这相当于 Unix 风格的 c:/foo/bar/baz.c(c:部分是所谓的驱动器号)。当 make 在这些系统上运行时,它支持路径名中的反斜杠和 Unixstyle 正斜杠。但是,此支持不包括通配符展开,其中反斜杠是引号字符。因此,在这些情况下,您必须使用unix风格的斜杠。

3、通配符函数

见 makefile 函数章节。

四、在目录中搜索先决条件

对于大型系统,通常希望将源代码与二进制文件放在单独的目录中。目录搜索功能通过自动搜索多个目录来查找先决条件来促进这一点。当您在目录之间重新分发文件时,您不需要更改单个规则,只需要更改搜索路径。

1、VPATH:所有先决条件的搜索路径

make 变量 VPATH 的值指定 make 应搜索的目录列表。大多数情况下,期望这些目录包含不在当前目录中的先决条件文件;但是,请使用 VPATH 作为规则的先决条件和目标的搜索列表。

因此,如果当前目录中不存在作为目标或先决条件列出的文件,则在VPATH中列出的目录中搜索具有该名称的文件。如果在其中一个文件中找到了文件,则该文件可能成为先决条件(参见下面的内容)。然后,规则可以指定先决条件列表中的文件名称,就好像它们都存在于当前目录中一样。

在VPATH变量中,目录名用冒号或空格分隔。目录的列出顺序是搜索中后跟 make 的顺序(在MS-DOS和MS-Windows上,分号在VPATH中用作目录名的分隔符,因为冒号可以在路径名本身中,在驱动器号之后使用)。

例如:

VPATH = src:../headers

指定包含两个目录 src 和 …/headers 的路径,这两个目录按该顺序进行搜索。

对于VPATH的值,遵循以下规则:

foo.o : foo.c

被解释为好像它是这样写的:

foo.o : src/foo.c

假设文件 foo.c 在当前目录中不存在,但在目录 src 中找到。

2、vpath 指令

类似于VPATH变量,但更具选择性的是VPATH指令(注意小写),它允许您为特定的文件名类指定搜索路径:那些匹配特定模式的文件名。因此,您可以为一类文件名提供特定的搜索目录,为其他文件名提供其他目录(或不提供)。

vpath 指令有三种形式:

  • vpath pattern directories:为匹配模式的文件名指定搜索路径目录。搜索路径,目录,是要搜索的目录列表,用冒号(MS-DOS和MS-Windows上的分号)或空格分隔,就像在VPATH变量中使用的搜索路径一样。

  • vpath pattern:清除与模式关联的搜索路径。

  • vpath:清除之前用vpath指令指定的所有搜索路径。

vpath模式是一个包含 ‘%’ 字符的字符串。该字符串必须匹配正在搜索的先决条件的文件名,即匹配任何由零个或多个字符组成的序列的 ‘%’ 字符。例如,%.h 匹配以 .h 结尾的文件。如果没有 ‘%’,则模式必须与先决条件完全匹配,这通常没有用处。

vpath指令模式中的 ‘%’ 字符可以用前面的反斜杠(’’)括起来。可以用更多的反斜杠来引用 ‘%’ 字符。在与文件名比较之前,引用 ‘%’ 字符的反斜杠或其他反斜杠将从模式中删除。不存在引用 ‘%’ 字符危险的反斜杠不会受到干扰。

当先决条件在当前目录中不存在时,如果vpath指令中的模式与先决条件文件的名称相匹配,则搜索该指令中的目录就像(和之前)搜索vpath变量中的目录一样。

例如:

vpath %.h ../headers

如果当前目录中找不到该文件,则指示 make 在目录 …/headers 中查找名称以 .h 结尾的任何先决条件。

如果多个 vpath 模式与先决条件文件的名称匹配,则逐个处理每个匹配的 vpath 指令,搜索每个指令中提到的所有目录。make 按照多个 vpath 指令在 make 文件中出现的顺序处理它们;具有相同模式的多个指令相互独立。

因此:

vpath %.c foo
vpath %   blish
vpath %.c bar

将查找以 foo 中的 ‘.c’ 结尾的文件,然后是 blish,然后是 bar,而:

vpath %.c foo:bar
vpath %   blish

会在 foo 中查找以 ‘.c’ 结尾的文件,然后 bar,然后 blish。

3、如何执行目录搜索

当通过目录搜索找到先决条件时,无论类型如何(常规或选择性),定位的路径名可能不是使先决条件列表中实际为您提供的路径名。有时,通过目录搜索发现的路径会被丢弃。

该算法用于决定是通过目录搜索找到的路径是保留还是放弃,如下所示:

1、如果在 makefile 中指定的路径上不存在目标文件,则执行目录搜索。

2、如果目录搜索成功,则保留该路径,并将此文件暂时存储为目标。

3、使用相同的方法检查该目标的所有先决条件。

4、在处理了先决条件之后,目标可能需要重建,也可能不需要重建:

  • 如果目标不需要重新构建,目录搜索中找到的文件的路径将用于包含此目标的任何先决条件列表。简而言之,如果 make 不需要重建目标,则可以使用通过目录搜索找到的路径。
  • 如果目标确实需要重新构建(已经过时),则丢弃在目录搜索期间找到的路径名,并使用makefile中指定的文件名重新构建目标。简而言之,如果 make 必须重建,则目标将在本地重建,而不是在通过目录搜索找到的目录中重建。

这个算法可能看起来很复杂,但在实践中,它往往正是您想要的。

其他版本的算法更简单:如果文件不存在,并且通过目录搜索找到它,那么无论是否需要构建目标,都始终使用该路径名。因此,如果目标被重建,它将在目录搜索中发现的路径名处创建。

实际上,如果这是您希望对部分或所有目录执行的行为,您可以使用GPATH变量来指示要执行的操作。

GPATH具有与VPATH相同的语法和格式(即,用空格或冒号分隔的路径名列表)。如果通过目录搜索在GPATH中也出现的目录中找到一个过期的目标,那么该路径名不会被丢弃。使用扩展路径重新构建目标。

4、使用目录搜索编写配方

当通过目录搜索在另一个目录中找到一个先决条件时,这不能改变规则的配方;他们将按规定执行。因此,您必须小心编写配方,以便它将在 make 找到它的目录中查找先决条件。

这是通过自动变量如 '$^’ 来完成的。例如,’$^’ 的值是规则的所有先决条件的列表,包括找到它们的目录的名称,而 ‘$@’ 的值是目标。因此:

foo.o : foo.c
        cc -c $(CFLAGS) $^ -o $@

变量 CFLAGS 存在,所以你可以通过隐式规则为 C 编译指定标志;我们在这里使用它是为了保持一致性,所以它会一致地影响所有的 C 编译;

通常前提条件还包括头文件,这是您不希望在配方中提到的。自动变量 ‘$<’ 只是第一个先决条件:

VPATH = src:../headers
foo.o : foo.c defs.h hack.h
        cc -c $(CFLAGS) $< -o $@

5、目录搜索和隐式规则

在考虑隐式规则时,也会对 VPATH 或 VPATH 中指定的目录进行搜索

例如,当文件 foo.o 没有显式规则时,make 会考虑隐式规则,例如,如果该文件存在,则编译 foo.c 的内置规则。如果当前目录中没有这样的文件,则会在相应的目录中搜索它。如果 foo.c 在任何目录中存在(或在 makefile 中提到),则应用 C 编译的隐式规则。

隐式规则的配方通常需要使用自动变量;因此,他们将使用通过目录搜索找到的文件名,而不需要额外的努力。

6、目录搜索链接库

目录搜索以一种特殊的方式应用于与链接器一起使用的库。当您编写名称为形式 ‘-lname’ 的先决条件时,这个特殊特性就会发挥作用。您可以看出这里发生了一些奇怪的事情,因为先决条件通常是文件的名称,而库的文件名通常看起来像 libname.a,而不是 ‘-lname’。

当先决条件的名称具有格式 ‘-lname’ 时,make 通过搜索文件 libname.so 来专门处理它,如果未找到,则在当前目录中,在匹配的 vpath 搜索路径和 VPATH 搜索路径指定的目录中搜索文件 libname.a,然后在目录 /lib、/usr/lib 和 prefix/lib 中搜索文件 libname.a(通常为 /usr/local/lib,但 MSDOS/MS-Windows 版本的 make 的行为就像前缀被定义为 DJGPP 安装树的根一样)。

例如,如果您的系统上有一个 /usr/lib/libcurses.a 库(没有 /usr/lib/libcurses.so 文件),那么:

foo : foo.c -lcurses
        cc $^ -o $@

当foo比 foo.c 或 /usr/lib/libcurses.a 更老时,会导致命令 ‘cc foo.c /usr/lib/libcurses.a -o foo’ 被执行。

虽然要搜索的默认文件集是 libname.so 和 libname.a,但这是可以通过 .LIBPATTERNS 变量定制的。该变量值中的每个单词都是一个模式字符串。当看到像 A 这样的先决条件时,make 会将列表中每个模式中的百分比替换为 name,并使用每个库文件名执行上述目录搜索。

.LIBPATTERNS 的默认值是 ‘lib%.so lib%.a’,它提供了上面描述的默认行为。

通过将此变量设置为空值,可以完全关闭链接库展开

五、phony 目标

伪目标不是真正的文件名;更确切地说,它只是在发出显式请求时要执行的配方的名称。使用伪目标有两个原因避免与同名文件冲突,以及提高性能

如果您编写的规则的配方不创建目标文件,那么每当目标出现进行重做时,配方就会被执行。下面是一个例子:

clean:
        rm *.o temp

由于 rm 命令不会创建名为 clean 的文件,因此可能永远不会存在此类文件。因此,每次执行 ‘make clean’ 时都将执行 rm 命令。

在本例中,如果在此目录中创建了名为 clean 的文件,则 clean 目标将无法正常工作。因为它没有先决条件,所以 clean 总是被认为是最新的,它的配方将不会被执行。要避免此问题,您可以通过将其作为特殊目标 .PHONY 的先决条件来显式声明目标为假目标,如下所示:

.PHONY: clean
clean:
        rm *.o temp

完成此操作后,'make clean’将运行配方,不管是否有名为clean的文件。

虚假目标在与 make 的递归调用结合使用时也很有用。在这种情况下,makefile通常包含一个变量,该变量列出了许多要构建的子目录。处理此问题的一种简单方法是定义一个规则,其中包含一个配方,循环遍历子目录,如下所示:

SUBDIRS = foo bar baz

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

但是,此方法存在问题。首先,此规则会忽略在子 make 中检测到的任何错误,因此即使一个目录失败,它也会继续生成其余的目录。这可以通过添加shell命令来记录错误并退出来克服,但是即使使用-k选项调用make,它也会这样做,这很不幸。其次,也许更重要的是,你不能利用make并行构建目标的能力,因为只有一个规则。

通过将子目录声明为 .PHONY 目标,可以消除以下问题(你必须这样做,因为子目录显然总是存在的;否则它将无法建造):

SUBDIRS = foo bar baz

.PHONY: subdirs $(SUBDIRS)

subdirs: $(SUBDIRS)

$(SUBDIRS):
        $(MAKE) -C $@

foo: baz

在这里,我们还声明在 baz 子目录完成之前不能构建 foo 子目录;在尝试并行构建时,这种关系声明尤其重要。

对于 .PHONY 目标,将跳过隐式规则搜索。这就是为什么将目标声明为 .PHONY 可以提高性能的原因,即使您不担心实际存在的文件。

假目标不应该是真实目标文件的先决条件;如果是,则每次 make 去更新该文件时都会运行其配方。只要虚假目标不是真实目标的先决条件,那么只有当虚假目标是一个指定的目标时,才会执行虚假目标配方。

虚假目标可以有先决条件。当一个目录包含多个程序时,在一个 makefile ./Makefile中描述所有的程序是最方便的。由于默认情况下重做的目标将是 makefile 中的第一个目标,因此通常将其设置为名为 ‘all’ 的伪目标,并将所有单独的程序作为先决条件赋给它。例如:

all : prog1 prog2 prog3
.PHONY : all

prog1 : prog1.o utils.o
        cc -o prog1 prog1.o utils.o

prog2 : prog2.o
        cc -o prog2 prog2.o

prog3 : prog3.o sort.o utils.o
        cc -o prog3 prog3.o sort.o utils.o

现在,您可以只说 ‘make’ 来重制所有三个程序,或者指定要重制的程序作为参数(如 ‘make prog1 prog3’ 所示)。虚假性不会被继承:虚假目标的先决条件本身并不虚假,除非明确声明是虚假的。

当一个伪目标是另一个的先决条件时,它充当另一个的子例程。例如,这里 ‘make cleanall’ 将删除object 文件、difference 文件和 program 文件:

.PHONY: cleanall cleanobj cleandiff

cleanall : cleanobj cleandiff
        rm program

cleanobj :
        rm *.o

cleandiff :
        rm *.diff

六、没有配方或先决条件的规则

如果规则没有先决条件或配方,并且规则的目标是不存在的文件,则 make 将假定此目标在运行其规则时已更新这意味着所有依赖于此的目标都将运行它们的配方

一个例子将说明这一点:

clean: FORCE
        rm $(objects)
FORCE:

在这里,目标 ‘FORCE’ 满足特殊条件,因此依赖于它的目标清洁被强制运行它的配方。名称 'FORCE’没有什么特别之处,但这是一个常用的名称。

如你所见,以这种方式使用 ‘FORCE’ 和使用 ‘.PHONY: clean’ 有相同的结果。

使用 ‘.PHONY’ 更明确、更有效。但是,其他版本的 make 不支持 ‘.PHONY’;因此,‘FORCE’ 出现在许多 makefile 中。

七、空的目标文件记录事件

空目标是假目标的变体;它用于保存您不时明确请求的某个操作的配方。与虚假目标不同,此目标文件确实可以存在;但是文件的内容并不重要,通常是空的。

空目标文件的目的是记录规则配方上次执行的时间及其上次修改时间。它之所以这样做,是因为配方中的一个命令是用于更新目标文件的 touch 命令。

空目标文件应具有一些先决条件(否则就没有意义)。当您要求重制空目标时,如果任何先决条件比目标更新,则执行配方;换句话说,如果自上次重新创建目标以来,先决条件发生了变化。下面是一个示例:

print: foo.c bar.c
        lpr -p $?
        touch print

使用此规则,如果源文件中任何一个自上次 ‘make print’ 后发生了更改,那么 ‘make print’ 将执行lpr命令。自动变量 ‘$?’ 用于只打印那些已更改的文件。

八、特殊内置目标名称

某些名称如果作为目标出现,则具有特殊的含义

  • .PHONY:特殊目标 .PHONY 的先决条件被认为是假目标。当需要考虑这样的目标时,make 将无条件地运行其配方,无论是否存在具有该名称的文件或其上次修改时间。

  • .SUFFIXES:特殊目标 .SUFFIXES 的先决条件是用于检查后缀规则的后缀列表。

  • .DEFAULT:为A指定的配方将用于任何没有找到规则的目标(无论是显式规则还是隐式规则)。如果指定了一个配方,那么在规则中作为先决条件而不是目标提到的每个文件都将执行该配方。

  • .PRECIOUS:.PRECIOUS 所依赖的目标被给予以下特殊处理:如果在执行其配方期间杀死或中断了目标,则不会删除目标。此外,如果目标是一个中间文件,那么在不再需要它之后,它将不会被删除,就像通常所做的那样。在后一方面,它与 .SECONDARY 特殊目标重叠。

  • .INTERMEDIATE:.INTERMEDIATE 所依赖的目标被视为中间文件。没有先决条件的 .INTERMEDIATE 没有效果。

  • .SECONDARY:.SECONDARY 所依赖的目标被视为中间文件,只是它们永远不会被自动删除。没有先决条件的 .SECONDARY 会导致所有目标都被视为次要目标(即,没有目标被删除,因为它被认为是中间目标)。

  • .SECONDEXPANSION:如果在 makefile 的任何地方提到 .SECONDEXPANSION 作为目标,那么在它出现后定义的所有先决条件列表将在所有 makefile 被读取后第二次展开。

  • .DELETE_ON_ERROR:如果在 makefile 中的任何位置将 .DELETE_ON_ERROR 作为目标提及,则 make 将删除规则的目标(如果规则已更改,并且其配方以非零退出状态退出),就像它收到信号时一样。

  • .IGNORE:如果为 .IGNORE 指定先决条件,则 make 将忽略在执行这些特定文件的配方时出现的错误。.IGNORE 的配方(如果有)将被忽略。如果将它作为一个没有先决条件的目标,.IGNORE 表示忽略所有文件的配方执行过程中的错误。’.IGNORE’ 的这种用法只支持历史兼容性。因为这会影响到makefile中的每个配方,所以它不是很有用;我们建议您使用更有选择性的方法来忽略特定配方中的错误。

  • .LOW_RESOLUTION_TIME:如果为 .LOW_RESOLUTION_TIME 指定先决条件,则假定这些文件是由生成低分辨率时间戳的命令创建的。.LOW_RESOLUTION_TIME 目标的配方将被忽略。许多现代文件系统的高分辨率文件时间戳减少了错误地得出文件是最新的结论的机会。遗憾的是,某些主机不提供设置高分辨率文件时间戳的方法,因此像 ‘cp -p’ 这样显式设置文件时间戳的命令必须丢弃其亚秒部分。如果文件是由此类命令创建的,则应将其列为 A 的先决条件,以便 make 不会错误地断定该文件已过期。例如:

.LOW_RESOLUTION_TIME: dst
dst: src
        cp -p src dst
  • .LOW_RESOLUTION_TIME(连接上面):由于 ‘cp -p’ 丢弃了 src 时间戳的亚秒部分,因此即使 dst 是最新的,它通常也比 src 略旧。如果dst的时间戳与 src 的时间戳在同一秒的开始,则 .LOW_RESOLUTION_TIME 行使dst被认为是最新的。由于归档格式的限制,归档成员时间戳的分辨率总是很低。您无需将存档成员列为 .LOW_RESOLUTION_TIME 的先决条件,因为 make 会自动执行此操作。

  • .SILENT:如果为 .SILENT 指定先决条件,则 make 在执行这些文件之前不会打印用于重制这些特定文件的配方。.SILENT 的配方将被忽略。如果作为一个没有先决条件的目标,a说在执行之前不要打印任何配方。您还可以使用更有选择性的方法来静默特定的配方命令行。如果您想对特定运行的 make 禁用所有配方,请使用 ‘-s’ 或 ‘–silent’ 选项。

  • .EXPORT_ALL_VARIABLES:只需将其作为目标提及,这就会告诉 make 在默认情况下将所有变量导出到子进程。

  • .NOTPARALLEL:如果将 .NOTPARALLEL 作为目标提及,则即使给出了 ‘-j’ 选项,也会按顺序运行此 make 调用。任何递归调用的 make 命令仍将并行运行配方(除非其 makefile 也包含此目标)。此目标上的任何先决条件都将被忽略。

  • .ONESHELL:如果将 .ONESHELL 作为目标提到,那么在构建目标时,配方的所有行都将被交给shell的单个调用,而不是单独调用每一行。

  • .POSIX:如果将 .POSIX 作为目标提及,则将解析 makefile 并在符合 POSIX 的模式下运行。这并不意味着只有符合 POSIX 的 makefile 才会被接受:所有高级 GNU make 功能仍然可用。相反,此目标会导致 make 在 make 的默认行为不同的区域中按照 POSIX 的要求运行。特别是,如果提到此目标,则将调用配方,就好像 shell 已传递 -e 标志一样:配方中的第一个失败命令将导致配方立即失败。

任何定义的隐式规则后缀如果作为目标出现,也可以算作特殊目标,两个后缀的连接也一样,比如 ‘.c.o’。这些目标是后缀规则,一种过时的定义隐式规则的方法(但仍被广泛使用)。原则上,如果将任何目标名称一分为二并将两个部分都添加到后缀列表中,则任何目标名称都可能以这种方式是特殊的。实际上,后缀通常以 ‘.’ 开头,所以这些特殊的目标名称也以 ‘.’ 开头。

九、规则中的多个目标

当显式规则具有多个目标时,可以通过以下两种可能的方式之一处理它们:作为独立目标分组目标。处理它们的方式由出现在目标列表之后的分隔符决定。

1、独立目标的规则

使用标准目标分隔符 ‘:’ 的规则定义独立的目标。这相当于为每个目标编写相同的规则,并使用重复的先决条件和配方。通常,配方将使用自动变量 ‘$@’ 来指定要构建的目标。

具有独立目标的规则在两种情况下很有用:

1、你需要的只是先决条件,而不是配方。例如:

kbd.o command.o files.o: command.h

为上述三个目标文件中的每一个提供额外的先决条件。它相当于写:

kbd.o: command.h
command.o: command.h
files.o: command.h

类似的配方适用于所有目标。自动变量 ‘$@’ 可用于将要重制的特定目标替换为命令。例如:

bigoutput littleoutput : text.g
        generate text.g -$(subst output,,$@) > $@

相当于:

bigoutput : text.g
        generate text.g -big > bigoutput
littleoutput : text.g
        generate text.g -little > littleoutput

这里我们假设程序生成两种类型的输出,一种是给定 ‘-big’ 的输出,另一种是给定 ‘-little’ 的输出。

假设您希望根据目标改变先决条件,就像变量 ‘$@’ 允许您改变配方一样。普通规则中的多个目标不能这样做,但是可以使用静态模式规则。

2、分组目标的规则

如果您使用的配方不是独立目标,而是从单个调用生成多个文件,则可以通过将规则声明为使用分组目标来表达这种关系。分组目标规则使用分隔符 ‘&:’ (这里的 ‘&’ 表示 ‘all’)。

当 make 生成任何一个分组目标时,它会理解组中的所有其他目标也是由于调用配方而创建的。此外,如果只有一些分组目标已过期或缺少,则 make 将意识到运行配方将更新所有目标。

例如,此规则定义一个分组目标:

foo bar biz &: baz boz
        echo $^ > foo
        echo $^ > bar
        echo $^ > biz

在执行分组目标的配方时,自动变量 ‘$@’ 被设置为触发该规则的组中特定目标的名称。如果在分组目标规则的配方中依赖此变量,则必须谨慎使用。

与独立目标不同,分组目标规则必须包含配方。但是,作为分组目标成员的目标也可能出现在没有配方的独立目标规则定义中。

每个目标可能只有一个与之相关的配方。如果一个分组目标出现在一个独立的目标规则中,或者出现在另一个分组目标规则中,那么您将得到一个警告,后一个分组目标将取代前一个分组目标规则。此外,目标将从上一个组中删除,只出现在新组中。

如果您希望一个目标出现在多个组中,那么在声明包含该目标的所有组时,必须使用双冒号分组目标分隔符 ‘&::’。分组的双冒号目标被认为是独立的,并且每个分组的双冒号规则的配方最多执行一次,如果它的多个目标中至少有一个需要更新。

十、一个目标的多个规则

一个文件可以是多个规则的目标。所有规则中提到的所有先决条件都合并到目标的一个先决条件列表中。如果目标比任何规则的任何先决条件都早,则执行配方。

一个文件只能执行一个配方。如果多个规则为同一文件提供了配方,请使用最后一个给出的规则并打印错误消息(作为特殊情况,如果文件名以点开头,则不会打印任何错误消息。这种奇怪的行为只是为了与 make. . . 的其他实现兼容,你应该避免使用它)。有时,让同一目标调用在makefile的不同部分中定义的多个配方是很有用的;对此可以使用双冒号规则。

一个只有先决条件的额外规则可以用来一次为多个文件提供一些额外的先决条件。例如,makefile通常有一个变量(比如对象),它包含正在生成的系统中所有编译器输出文件的列表。如果config.h发生变化,说明所有这些参数都必须重新编译的一种简单方法是编写以下代码:

objects = foo.o bar.o
foo.o : defs.h
bar.o : defs.h test.h
$(objects) : config.h

这可以在不改变真正指定如何创建目标文件的规则的情况下插入或删除,使其成为一种方便的形式,如果您希望间歇地添加额外的先决条件。

另一个问题是,可以用一个变量指定额外的先决条件,这个变量是用命令行参数设置的。例如:

extradeps=
$(objects) : $(extradeps)

这意味着命令 ‘make extradeps=foo.h’ 将把 foo.h 作为每个对象文件的先决条件,但纯 ‘make’ 不会。

如果目标的显式规则中没有一个包含规则,那么搜索一个适用的隐式规则来找到规则。

十一、静态模式规则

静态模式规则是指定多个目标并基于目标名称为每个目标构造先决条件名称的规则。它们比具有多个目标的普通规则更通用,因为目标不需要具有相同的先决条件。它们的先决条件必须相似,但不一定相同。

1、静态模式规则的语法

静态模式规则的语法如下:

targets …: target-pattern: prereq-patterns …
        recipe
        …

targets 列表指定规则应用到的目标。目标可以包含通配符,就像普通规则的目标一样。

target-pattern 和 prereq-patterns 说明了如何计算每个目标的先决条件。每个目标都与 target-pattern 匹配,以提取目标名称的一部分(称为词干)。此词干被替换为每个 prereq-patterns 以生成先决条件名称(每个 prereq-pattern 一个)。

每个模式通常只包含字符 ‘%’ 一次。当 target-pattern 匹配目标时,’%’ 可以匹配目标名称的任何部分;这部分被称为词干。模式的其余部分必须完全匹配。例如,目标 foo.o 与模式 ‘%.o’ 匹配,其中 ‘foo’ 作为词干。目标 foo.c 和 foo.out 与该模式不匹配。

每个目标的先决条件名称都是通过在每个先决条件模式中用词干替换 ‘%’ 来创建的。例如,如果一个先决条件模式是 %.c,则替换词干 ‘foo’ 将给出先决条件名称 foo.c。编写不包含 ‘%’ 的先决条件模式是合法的;则此先决条件对于所有目标都是相同的。

模式规则中的 ‘%’ 字符可以用前面的反斜杠 (’’) 引用。可以用更多的反斜杠来引用 ‘%’ 字符。引用 ‘%’ 字符或其他反斜杠的反斜杠在与文件名进行比较或将其替换为一个字符之前从模式中删除。不存在引用 ‘%’ 字符危险的反斜杠不会受到干扰。例如,模式 the%weird\%pattern\ 在可操作的 ‘%’ 字符之前有 ‘the%weird’,在执行 ‘%’ 字符后面有 ‘pattern\’。最后两个反斜杠不受影响,因为它们不会影响任何 ‘%’ 字符。

下面是一个示例,它从 corresponding .c 文件编译每个 foo.o 和 bar.o:

objects = foo.o bar.o

all: $(objects)

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

此处,’$<’ 是保存先决条件名称的自动变量,’$@’ 是保存目标名称的自动变量;

指定的每个目标必须与目标模式匹配;对每个不存在的目标发出警告。如果您有一个文件列表,其中只有一些与模式匹配,您可以使用 filter 函数来删除不匹配的文件名:

files = foo.elc bar.o lose.o

$(filter %.o,$(files)): %.o: %.c
        $(CC) -c $(CFLAGS) $< -o $@
$(filter %.elc,$(files)): %.elc: %.el
        emacs -f batch-byte-compile $<

在此示例中,’$(filter %.o,$(files))’ 的结果为 bar.o lose.o,第一个静态模式规则通过编译相应的 C 源文件来更新其中每个对象文件。’$(filter %.elc,$(files))’ 的结果是 foo.elc,所以该文件由 foo.el 组成。

另一个例子展示了如何在静态模式规则中使用 $*:

bigoutput littleoutput : %output : text.g
        generate text.g -$* > $@

运行 generate 命令时,$* 将扩展到词干,即 ‘big’ 或 ‘little’。

2、静态模式规则与隐式规则

静态模式规则与定义为模式规则的隐式规则有很多共同之处。两者都有用于目标的模式和用于构造先决条件名称的模式。不同之处在于 make 如何决定规则何时适用。

隐式规则可以应用于任何匹配它的模式的目标,但它只适用于目标没有指定配方的时候,并且只有当先决条件可以找到的时候。如果有多个隐式规则适用,则只适用一个;选择取决于规则的顺序。

相反,静态模式规则适用于您在规则中指定的精确目标列表。它不能适用于任何其他目标,它总是适用于指定的每一个目标。如果有两个相冲突的规则,并且都有配方,那就是错误的。

由于以下原因,静态模式规则可能比隐式规则更好:

  • 对于一些文件,您可能希望重写通常的隐式规则,这些文件的名称不能按照语法分类,但可以在一个显式列表中给出。
  • 如果您无法确定所使用的目录的确切内容,则可能无法确定哪些其他不相关的文件可能会导致使用错误的隐式规则。选择可能取决于隐式规则搜索的顺序。使用静态模式规则,不存在不确定性:每个规则精确地应用于指定的目标。

十二、双冒号规则

双冒号规则是在目标名称后面用 ‘::’ 而不是 ‘:’ 编写的显式规则。当同一目标出现在多个规则中时,它们的处理方式与普通规则不同。带有双冒号的模式规则具有完全不同的含义。

当一个目标出现在多个规则中时,所有规则都必须是相同的类型:全部为普通规则,或全部为双冒号。如果它们是双冒号,每一个都是独立的。如果目标早于该规则的任何先决条件,则执行每个双冒号规则的配方。如果该规则没有先决条件,则始终执行它的配方(即使目标已经存在)。这可能导致不执行任何或全部双冒号规则。

具有相同目标的双冒号规则实际上彼此完全分离。每个双冒号规则都是单独处理的,就像处理具有不同目标的规则一样。

目标的双冒号规则按照它们在 makefile 中出现的顺序执行。然而,在双冒号规则真正有意义的情况下,执行配方的顺序并不重要。

双冒号规则有些晦涩,而且通常不是很有用;它们提供了一种机制,用于更新目标的方法根据导致更新的先决条件文件的不同而不同,这种情况很少见。

每个双冒号规则应该指定一个配方;否则,如果应用隐式规则,则将使用隐式规则。

十三、自动生成先决条件

在程序的makefile中,您需要编写的许多规则通常只说明某些目标文件依赖于某些头文件。例如,如果 main.c 通过 #include 使用 defs.c,则可以这样写:

main.o: defs.h

你需要这个规则,以便知道每当 defs.h 改变时它必须重新制作 main.o。您可以看到,对于大型程序,您必须在makefile中编写许多这样的规则。而且,每次添加或删除 #include 时,都必须非常小心地更新makefile。

为了避免这种麻烦,大多数现代C编译器可以通过查看源文件中的 #include 行来为您编写这些规则。这通常是通过编译器的 ‘-M’ 选项来完成的。例如,命令:

cc -M main.c

生成输出:

main.o : main.c defs.h

因此,您不再需要自己编写所有这些规则。编译器会帮你做的。

请注意,这样的规则在makefile中包含提及 main.o,因此通过隐式规则搜索,它永远不能被视为一个中间文件。这意味着 make 在使用后永远不会删除该文件;

对于旧的 make 程序,传统做法是使用此编译器功能通过 A 等命令按需生成先决条件。该命令将创建一个包含所有自动生成的先决条件的文件依赖项;然后 makefile 可以使用 include 来读取它们。

在 GNU make 中,重造 makefile 的功能使这种做法过时了 — 你永远不需要明确告诉 make 来重新生成先决条件,因为它总是会重新生成任何过期的 makefile。

对于自动生成先决条件,我们推荐的做法是每个源文件对应一个makefile。对于每个源文件 name.c,都有一个makefile name.d,它列出了对象文件 name.o 所依赖的文件。这样,只有已更改的源文件才需要重新扫描以生成新的先决条件。

下面是一个模式规则,它可以从C源文件 name.c 中生成一个具有先决条件的文件 name.d(即makefile):

%.d: %.c
        @set -e; rm -f $@; \
         $(CC) -M $(CPPFLAGS) $< > $@.$$$$; \
         sed 's,\($*\)\.o[ :]*,\1.o $@ : ,g' < $@.$$$$ > $@; \
         rm -f $@.$$$$

如果 $(CC) 命令(或任何其他命令)失败(以非零状态退出),shell的 ‘-e’ 标志将导致它立即退出。

对于GNU C编译器,您可能希望使用 ‘-MM’ 标志而不是 ‘-M’ 标志。这省略了系统头文件的先决条件。

sed 命令的用途是翻译(例如):

main.o : main.c defs.h

等价于:

main.o main.d : main.c defs.h

这使得每个 ‘.d’ 文件依赖于相应的 ‘.o’ 文件所依赖的所有源文件和头文件。然后,make 知道每当任何源文件或头文件更改时,它必须重新生成先决条件。

一旦您定义了重制 ‘.d’ 文件的规则,就可以使用include指令将它们全部读入。例如:

sources = foo.c bar.c

include $(sources:.c=.d)

这个例子使用了一个替换变量引用来将源文件 ‘foo.c bar.c’ 的列表转换为先决条件生成文件 ‘foo.d bar.d’ 的列表。由于 ‘.d’ 文件是像任何其他文件一样是生成文件,因此 make 将根据需要重新制作它们,而无需您进一步的工作。

注意,’.d’ 文件包含目标定义;你应该确保将include指令放在makefiles中的第一个默认目标之后,否则就有可能让一个随机的目标文件成为默认目标。

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
在使用VSCode编Makefile时,你可以按照以下步骤进行操作: 1. 在VSCode中创建一个新的文件,并将其命名为"Makefile"。 2. 在Makefile中,你可以使用-g选项来输出调试信息,以便进行在线调试。这可以通过在Makefile中添加"-g"来实现。\[1\] 3. 如果你的项目中有多个cpp文件,你可以使用通配符来简化Makefile的编。例如,使用"%.o: %.cpp"来表示所有的cpp文件都会生成对应的目标文件。\[2\] 4. 如果你希望将生成的目标文件放在其他位置,你可以在Makefile中指定目标文件的路径。例如,使用"obj/%.o"来表示将目标文件统一放在"obj"目录下。\[2\] 5. Makefile可以自动根据源文件的更新情况来判断是否需要重新编译。这样可以避免每次都重新编译所有文件。你可以使用make命令来执行Makefile,并根据需要进行编译。\[3\] 希望以上信息对你有所帮助! #### 引用[.reference_title] - *1* [vscode 使用makefile 在线调试运行C/C++程序的方法](https://blog.csdn.net/fhqlongteng/article/details/127388105)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insertT0,239^v3^insert_chatgpt"}} ] [.reference_item] - *2* [vscode makefile编译方法实例](https://blog.csdn.net/weixin_44523062/article/details/120284524)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insertT0,239^v3^insert_chatgpt"}} ] [.reference_item] - *3* [Tomato学习笔记-Vscode配置Makefile(使用task.jason和launch.jason)](https://blog.csdn.net/GitTomato/article/details/123170550)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insertT0,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值