Makefile之显式规则
下一篇:练习5 Makefile之配方规则,上一篇:练习3 Makefile之简单介绍,目录|首页
温故知新
经过以上的练习,我们已经对Makefile
有了一个初步的了解,本文将会对Makefile
的显示规则进行一个详细的介绍
显式规则是指在Makefile
中显式定义的规则,用于指导构建系统如何生成一个或多个目标文件。这些规则明确指定了目标文件、先决条件以及生成目标所需的操作命令。
每个显式规则通常包含以下部分:
- 目标(Target):目标是要生成的文件的名称。这可以是可执行文件、目标库、目标对象文件等。显式规则的目标是明确的构建目标。
- 先决条件(Prerequisites):先决条件是目标生成所依赖的文件或其他目标。如果先决条件中的文件比目标文件新,或者先决条件中的文件不存在,make将确定目标是过期的,需要重新生成。
- 配方(Recipe):配方是一个包含操作命令的部分,用于生成目标文件。这些命令通常是 Shell 命令,例如编译源代码、链接目标文件等。配方定义了如何从前提条件生成目标。
以下是一个示例的显式规则:
my_program : main.cpp functions.cpp
g++ -o my_program main.cpp functions.cpp
在这个示例中:
- 目标是
my_program
,表示要生成的可执行文件的名称。 - 先决条件是
main.cpp
和functions.cpp
,这些文件是构建my_program
所必需的。 - 配方包括
g++ -o my_program main.cpp functions.cpp
,这是编译和链接的命令。
显式规则允许构建系统准确了解如何生成目标,以及如何处理目标的依赖关系。当执行 make
命令时,make将根据这些规则来决定哪些目标需要重新构建以及如何构建它们。
下一篇:练习5 Makefile之配方规则,上一篇:练习3 Makefile之简单介绍,目录|首页
先决条件
在Makefile中,先决条件(prerequisites)指的是构建目标所依赖的文件或其他目标。先决条件可以分为两种类型:常规先决条件和仅限顺序先决条件。
-
常规先决条件(General Prerequisites):常规先决条件是最常见的先决条件类型,它们定义了构建目标所需的依赖项。当目标的任何一个常规先决条件发生变化(即文件被修改),或者如果它们中的任何一个不存在,目标将被认为是过期的,需要重新构建。
例如,以下是一个具有常规前提条件的Makefile规则:
my_program: main.cpp functions.cpp g++ -o my_program main.cpp functions.cpp
在这个示例中,
main.cpp
和functions.cpp
是常规先决条件,如果它们中的任何一个被修改或不存在,my_program
将被重新构建。 -
仅限顺序先决条件(Order-Only Prerequisites):仅限顺序先决条件是一种特殊类型的先决条件,用于指定目标的依赖项必须存在,但它们的修改不会触发目标的重新构建。当你需要确保依赖项的存在,但不希望每次依赖项发生变化时都重新构建目标是有用的,。
例如,以下是一个具有仅限顺序先决条件的Makefile规则:
my_program: | data_directory g++ -o my_program main.cpp functions.cpp data_directory: mkdir data_directory
在这个示例中,
data_directory
是一个仅限顺序先决条件,确保my_program
构建之前会创建data_directory
目录,但目录的修改不会导致my_program
的重新构建。
下一篇:练习5 Makefile之配方规则,上一篇:练习3 Makefile之简单介绍,目录|首页
通配符
在Makefile中,通配符(wildcard)是一种用于匹配文件名的模式匹配机制,允许你在规则中使用通配符来匹配一组文件,而不需要逐个列出文件的名称。通配符可以用于简化Makefile中的规则,特别是在构建多个目标或处理一组文件时非常有用。
Makefile中常用的通配符是*
和?
:
*
:匹配零个或多个字符。?
:匹配单个字符。
以下是一些示例使用通配符的Makefile规则:
# 使用通配符匹配所有.cpp文件
sources := $(wildcard *.cpp)
# 生成所有.cpp文件的目标.o文件
objects := $(patsubst %.cpp,%.o,$(sources))
# 编译所有.o文件生成可执行文件
my_program: $(objects)
g++ -o $@ $^
# 清理操作,删除所有.o文件
clean:
rm -f $(objects) my_program
在上述示例中:
$(wildcard *.c)
使用通配符*
匹配当前目录下的所有.cpp
文件,并将它们赋给变量sources
。$(patsubst %.cpp,%.o,$(sources))
使用$(patsubst ...)
函数将.cpp
文件的扩展名替换为.o
,生成目标文件的列表。my_program: $(objects)
规则中使用了通配符生成的目标文件列表。$@
是一种变量,意思是目标文件的完整名称$^
是一种变量,意思是所有不重复的依赖文件,以空格隔开
通配符和通配符函数使Makefile更灵活,可以适应动态变化的文件集合,而不需要手动列出每个文件的名称。这对于管理大型项目或处理大量源文件非常有用。
下一篇:练习5 Makefile之配方规则,上一篇:练习3 Makefile之简单介绍,目录|首页
通配符的陷阱
在使用通配符的Makefile规则时,有一些潜在的陷阱需要注意。以下是一些可能会引发问题的情况以及如何避免它们:
-
不匹配文件路径:通配符通常只匹配当前目录下的文件,不会递归搜索子目录中的文件。如果你的源文件分布在多个子目录中,通配符可能不会捕获到所有文件。解决办法是使用递归通配符或手动列出文件路径。
# 使用递归通配符 sources := $(wildcard src/**/*.cpp) # 手动列出文件路径 sources := src/file1.cpp src/file2.cpp
-
通配符顺序问题:通配符展开的顺序可能不是你期望的顺序,这可能导致生成的目标顺序不正确。确保你的规则不依赖于文件的顺序,或者显式指定目标的依赖关系。
# 不好的示例:依赖于通配符的展开顺序 my_program: $(wildcard *.cpp) g++ -o $@ $^ # 好的示例:显式指定目标的依赖关系 sources := $(wildcard *.cpp) my_program: $(sources) g++ -o $@ $^
-
文件名中包含空格或特殊字符:如果文件名中包含空格或其他特殊字符,通配符可能无法正确处理这些文件。为了避免问题,最好避免在文件名中使用特殊字符。
-
通配符不支持文件筛选:通配符通常不支持高级的文件筛选或排除规则。如果需要更复杂的文件选择,可能需要使用
find
命令等工具。
总之,使用通配符时要谨慎,确保理解它们的行为并在需要时采取适当的措施来解决潜在的问题。通常,显式指定目标和前提条件,以确保构建的正确性和可维护性更可取。
下一篇:练习5 Makefile之配方规则,上一篇:练习3 Makefile之简单介绍,目录|首页
函数通配符
在Makefile中,你可以使用函数来执行各种操作,包括处理文件名、路径和通配符匹配。以下是一些与通配符相关的Makefile函数:
-
wildcard函数:
wildcard
函数用于匹配文件名,返回匹配的文件列表。它通常用于获取一组文件,然后进行进一步的操作。示例:
sources := $(wildcard src/*.c)
-
patsubst函数:
patsubst
函数用于替换文件名的一部分,通常用于更改文件扩展名。示例:
objects := $(patsubst %.c,%.o,$(sources))
-
filter函数:
filter
函数用于筛选符合特定模式的文件名。示例:
sources := file1.c file2.cpp file3.c file4.cpp c_sources := $(filter %.cpp,$(sources))
-
foreach函数:
foreach
函数用于遍历文件名列表并执行操作。示例:
sources := file1.c file2.c file3.c objs := $(foreach src,$(sources),$(src:.c=.o))
-
notdir函数:
notdir
函数用于从文件路径中提取文件名部分。示例:
files := src/file1.c src/file2.c filenames := $(notdir $(files))
-
dir函数:
dir
函数用于从文件路径中提取目录部分。示例:
files := src/file1.c src/file2.c directories := $(dir $(files))
这些函数可用于处理文件名、文件列表以及文件路径,以帮助你更灵活地管理构建规则和目标。它们可以与通配符一起使用,使Makefile更加强大和可维护。
下一篇:练习5 Makefile之配方规则,上一篇:练习3 Makefile之简单介绍,目录|首页
搜索目录
对于大型系统,通常希望将源代码与二进制文件分开放置在不同的目录中。make的目录搜索功能通过自动搜索多个目录来查找前提条件,有助于实现这一目标。当你将文件重新分配到不同的目录时,你无需更改各个规则,只需更改搜索路径即可。
vpath
是Makefile中的一个特殊变量,用于指定Make工具在搜索依赖文件(先决条件)时应该搜索的文件路径。它允许你为不在当前目录中的文件指定搜索路径,以便make工具可以找到它们。
vpath
的基本语法如下:
vpath <pattern> <directories>
<pattern>
是一个用于匹配文件名的模式,通常使用通配符(例如,%.c
匹配所有以.c
结尾的文件)。<directories>
是一个以空格分隔的目录列表,表示Make工具在搜索<pattern>
匹配的文件时应该查找的目录。
以下是一个示例,展示如何使用 vpath
:
# 指定以.c结尾的文件应该从src目录中搜索
vpath %.cpp src
my_program: main.cpp functions.cpp
g++ -o $@ $^
在这个示例中,vpath %.cpp src
表示以 .cpp
结尾的文件应该从 src
目录中搜索。因此,main.cpp
和 functions.cpp
将从 src
目录中查找,并作为 my_program
的依赖条件。
使用 vpath
可以方便地管理非当前目录中的依赖文件,特别是当项目具有多个子目录时。这有助于简化Makefile并提高构建的可维护性。
下一篇:练习5 Makefile之配方规则,上一篇:练习3 Makefile之简单介绍,目录|首页
如何执行目录搜索
目录搜索是make用来查找前提条件的过程,无论前提条件是一般前提条件还是选择性前提条件,都遵循相同的过程。在目录搜索中找到的路径不一定是最终出现在前提条件列表中的路径,有时通过目录搜索找到的路径会被丢弃。
在Makefile中,执行目录搜索是通过使用vpath
或VPATH
来指定Make工具在搜索依赖文件(前提条件)时应该搜索的目录来实现的。这允许Make工具在不同的目录中查找依赖文件,以确保能够找到它们。
-
使用
vpath
:vpath
变量允许你为特定的文件模式指定搜索路径。示例如下:# 指定以.cpp结尾的文件应该从src目录中搜索 vpath %.cpp src my_program: main.cpp functions.cpp g++ -o $@ $^
在这个示例中,
.cpp
文件会从src
目录中搜索。 -
使用
VPATH
环境变量:你还可以使用
VPATH
环境变量来设置搜索路径。这允许你在命令行中或Makefile中设置一个或多个目录,使Make工具在搜索文件时也查找这些目录。例如,在命令行中设置
VPATH
:VPATH=src:include make my_program
或在Makefile中设置:
VPATH = src include my_program: main.cpp functions.cpp g++ -o $@ $^
这将使Make工具在
src
和include
目录中查找依赖文件。
通过使用vpath
或VPATH
,你可以方便地指定搜索路径,以确保Make工具可以找到依赖文件,即使它们位于不同的目录中。这对于大型项目和多目录项目的构建非常有用。
下一篇:练习5 Makefile之配方规则,上一篇:练习3 Makefile之简单介绍,目录|首页
链接库的目录搜索
在Makefile中,你可以使用 -L
选项来指定链接库的目录搜索路径,以确保链接器能够找到所需的库文件。这通常用于指定库文件的搜索路径,使链接器能够找到和链接你的程序所需的库。
以下是一个示例Makefile规则,演示如何使用 -L
选项来指定库的搜索路径:
my_program: main.o functions.o
g++ -o $@ main.o functions.o -L/path/to/library/directory -lmylib
main.o: main.cpp
g++ -c main.cpp
functions.o: functions.cpp
g++ -c functions.cpp
在上述示例中:
-
-L/path/to/library/directory
指定了库文件的搜索路径,你需要将/path/to/library/directory
替换为实际的库文件目录。 -
-lmylib
指定了要链接的库文件libmylib.so
(或libmylib.a
)。链接器将在指定的搜索路径中查找这个库文件。
请注意,如果你的库文件命名为 libmylib.so
或 libmylib.a
,那么 -lmylib
会自动匹配这个库文件。
如果你需要指定多个库目录,你可以使用多个 -L
选项,以列出它们:
my_program: main.o functions.o
g++ -o $@ main.o functions.o -L/path/to/library/directory1 -L/path/to/library/directory2 -lmylib1 -lmylib2
这样可以确保链接器能够在多个目录中搜索库文件。
通过使用 -L
选项,你可以方便地管理库文件的搜索路径,以确保链接器能够找到所需的库文件。这在构建依赖于外部库的项目时非常有用。
下一篇:练习5 Makefile之配方规则,上一篇:练习3 Makefile之简单介绍,目录|首页
伪目标
如果你编写一个规则的命令部分不会创建目标文件,那么每次需要重新生成目标时,该规则的命令部分都会被执行。以下是一个示例:
clean:
rm *.o temp
因为rm命令不会创建一个名为clean的文件,所以很可能永远不会存在这样的文件。因此,每当执行make clean
命令时,rm命令都会被执行。
在这个示例中,如果在此目录中创建了一个名为clean的文件,clean目标将无法正常工作。由于它没有前提条件,clean始终被认为是最新的,其命令部分将不会被执行。为了避免这个问题,你可以通过将其作为特殊目标.PHONY的前提条件来明确声明目标为虚拟目标,如下所示:
.PHONY: clean
clean:
rm *.o temp
一旦完成这个操作,无论是否存在名为clean的文件,make clean
都会运行命令。
.PHONY
的前提条件始终被解释为字面的目标名称,而不是模式(即使它们包含’%'字符)。如果你想要始终重建一个模式规则,请考虑使用force
目标。
伪目标在与make的递归调用结合使用时也非常有用。在这种情况下,Makefile通常包含一个变量,列出了要构建的多个子目录。处理这种情况的一种简单方法是定义一个带有循环子目录的命令的规则,如下所示:
SUBDIRS = foo bar baz
subdirs:
for dir in $(SUBDIRS); do \
$(MAKE) -C $$dir; \
done
然而,这种方法存在问题。首先,在子make中检测到的任何错误都将被此规则忽略,因此即使其中一个失败,它也将继续构建其他目录。这可以通过添加shell命令来记录错误并退出来解决,但即使使用-k选项调用make,它也会这样做,这是不幸的。其次,也许更重要的是,你无法充分利用make并行构建目标的能力(见并行执行),因为只有一个规则。每个单独的Makefile的目标将并行构建,但一次只能构建一个子目录。
通过将子目录声明为.PHONY目标(你必须这样做,因为子目录显然总是存在的;否则它将不会被构建),你可以消除这些问题:
SUBDIRS = foo bar baz
.PHONY: subdirs $(SUBDIRS)
subdirs: $(SUBDIRS)
$(SUBDIRS):
$(MAKE) -C $@
这里我们还声明foo子目录在baz子目录完成之后才能构建;这种关系声明在尝试并行构建时尤为重要。
虚拟目标的隐式规则搜索(见使用隐式规则)会被跳过。这就是声明目标为.PHONY对性能有好处的原因,即使你不担心实际文件是否存在。
虚拟目标可以有前提条件。当一个目录包含多个程序时,最方便的方法是在一个Makefile中描述所有的程序,如./Makefile。由于默认情况下,要重新生成的目标是Makefile中的第一个目标,因此通常将其设置为一个名为’all’的虚拟目标,并将其作为前提条件,列出所有的个别程序,例如:
all : prog1 prog2 prog3
.PHONY : all
prog1 : prog1.o utils.o
g++ -o prog1 prog1.o utils.o
prog2 : prog2.o
g++ -o prog2 prog2.o
prog3 : prog3.o sort.o utils.o
g++ -o prog3 prog3.o sort.o utils.o
现在你可以只执行’make’来重新构建所有三个程序,或者将要重新构建的程序作为参数传递(例如’make prog1 prog3’)。
虚拟目标不会继承其他目标的虚拟性质:虚拟目标的前提条件不会自动成为虚拟目标,除非明确声明为虚拟目标。
当一个虚拟目标是另一个虚拟目标的前提条件时,它将作为另一个虚拟目标的子例程。例如,这里make cleanall
将删除对象文件、差异文件和程序文件:
.PHONY: cleanall cleanobj cleandiff
cleanall : cleanobj cleandiff
rm program
cleanobj :
rm *.o
cleandiff :
rm *.diff
最后,需要注意的是虚拟目标不应该是一个真实目标文件的前提条件,否则它的命令部分将每次make考虑到该文件时都会被执行。只要虚拟目标永远不是真实目标的前提条件,虚拟目标的命令部分将仅在虚拟目标作为特定目标时才会被执行。
另外,你不应该将包含的Makefile声明为虚拟目标。虚拟目标并不是用来表示真实文件的,因为目标始终被视为过期,make将永远重建它然后重新执行自身。为了避免这种情况,如果包含的文件被标记为虚拟目标,则make将不会重新执行自己。
虚拟目标可用于帮助组织和优化构建过程,使构建系统更清晰和高效。通过避免与实际文件名称发生冲突并明确声明虚拟目标,你可以确保make执行所需的操作,而无需多余的操作。
下一篇:练习5 Makefile之配方规则,上一篇:练习3 Makefile之简单介绍,目录|首页
没有配方或先决条件的规则
如果一个规则没有前提条件或者配方,而规则的目标是一个不存在的文件,那么make会假定在每次运行该规则时,目标已经被更新。这意味着所有依赖于这个目标的其他目标将总是执行其配方。
下面是一个示例:
clean: FORCE
rm $(objects)
FORCE:
在这里,目标FORCE
满足特殊条件,因此依赖于它的目标clean
被强制执行其配方。FORCE
的名称没有特殊意义,但这是通常以这种方式使用的一个常见名称。
正如你所看到的,使用FORCE
这种方式具有与使用.PHONY: clean
相同的结果。
使用.PHONY
更加明确和高效。然而,其他版本的make可能不支持.PHONY
,因此在许多Makefile中会看到FORCE
的使用。
下一篇:练习5 Makefile之配方规则,上一篇:练习3 Makefile之简单介绍,目录|首页
清空目标文件来记录事件
空目标文件是伪目标的一种变体;它用于保存你不时明确请求的操作的配方。与伪目标不同,这个目标文件实际上可以存在;但文件的内容不重要,通常为空。
空目标文件的目的是记录其上次修改的时间,以记录规则的配方上次何时执行。它这样做是因为配方中的一个命令是用于更新目标文件的 touch 命令。
空目标文件应该具有一些前提条件(否则没有意义)。当你请求重新生成空目标时,如果任何前提条件比目标更新,那么将执行配方;换句话说,如果一个前提条件自上次重新生成目标以来发生了更改。以下是一个示例:
print: foo.c bar.c
lpr -p $?
touch print
使用此规则,make prin
将执行lpr命令,如果自上次make print
以来任一源文件发生了更改。自动变量$?
用于仅打印自上次更改以来已更改的文件
下一篇:练习5 Makefile之配方规则,上一篇:练习3 Makefile之简单介绍,目录|首页
特殊的内置目标名称
这些特殊目标在Makefile
中有特定的含义或效果:
.PHONY
:.PHONY
特殊目标的前提被视为虚拟目标。在考虑执行这种目标时,make
会无条件运行其规则,不管是否存在与该名称的文件或最后修改时间是什么。.SUFFIXES
:.SUFFIXES
特殊目标的前提是用于检查后缀规则的后缀列表。此目标与旧式的后缀规则相关。.DEFAULT
:.DEFAULT
的规则适用于找不到规则的任何目标(无论是显式规则还是隐式规则)。.PRECIOUS
:.PRECIOUS
依赖的目标受到特殊对待。如果make
在执行其规则期间被中断或终止,目标不会被删除,如果目标是一个中间文件,它在不再需要时也不会被删除。.INTERMEDIATE
:.INTERMEDIATE
依赖的目标被视为中间文件。.NOTINTERMEDIATE
:.NOTINTERMEDIATE
特殊目标的前提永远不会被视为中间文件。.SECONDARY
:.SECONDARY
依赖的目标被视为中间文件,但它们永远不会被自动删除。.SECONDEXPANSION
:如果在Makefile
中提到了.SECONDEXPANSION
作为目标,那么在所有Makefile
都被读取后,之后定义的前提列表将再次扩展。这用于辅助扩展。.DELETE_ON_ERROR
:如果在Makefile
中提到,当目标因其规则的非零退出状态而更改并退出时,make
将删除目标。.IGNORE
:如果为.IGNORE
指定了前提条件,make
将忽略执行这些特定文件的规则时出现的错误。如果没有前提条件地提到,.IGNORE
表示忽略所有文件的规则执行中的错误。.LOW_RESOLUTION_TIME
:如果为.LOW_RESOLUTION_TIME
指定了前提,make
会假设这些文件是由生成低分辨率时间戳的命令创建的。它用于处理某些系统上的低分辨率时间戳。.SILENT
:如果为.SILENT
指定了前提,make
在执行这些特定文件的规则之前不会打印用于重新生成它们的规则。它用于消除特定目标的规则。.EXPORT_ALL_VARIABLES
:只要提到这个作为一个目标,这告诉make
默认情况下将所有变量导出给子进程。.NOTPARALLEL
:如果没有提到任何前提条件,那么在这个make
调用中,所有目标将被串行运行,即使使用-j
选项。如果.NOTPARALLEL
具有前提,那么列出的目标的所有前提都将被串行运行。这在禁用并行执行时用到。.ONESHELL
:如果提到它作为一个目标,那么当构建目标时,规则的所有行都将传递给一个shell的单次调用,而不是单独调用每一行。.POSIX
:如果提到它作为一个目标,make
将以符合POSIX标准的模式解析和运行Makefile
。这意味着规则将被执行,就好像shell已被传递了-e
标志。- 后缀规则:这些是带有特定后缀(例如
.c.o
)的目标,用于定义隐式规则。
这些特殊目标在Makefile
中有不同的效果,用于增强make
的行为以及如何处理和构建目标。
下一篇:练习5 Makefile之配方规则,上一篇:练习3 Makefile之简单介绍,目录|首页
规则中的多个目标
在make
中,当一个显式规则具有多个目标时,可以将它们视为独立目标或分组目标。目标的处理方式由目标列表后的分隔符决定。
独立目标规则
使用标准目标分隔符:
定义独立目标规则。这等同于为每个目标分别编写相同的规则,具有相同的前提条件和配方。通常,配方将使用自动变量,例如$@
来指定正在构建的目标。
独立目标规则在以下两种情况下很有用:
-
只需要前提条件,没有配方。例如:
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
这里假设假设性程序
generate
会生成两种类型的输出,一个给定-big
,另一个给定-little
。
如果要根据目标的不同来变化前提条件,就像变化配方一样,那就不能使用普通规则中的多个目标,但可以使用静态模式规则。
分组目标规则
如果您有一个规则,它会生成多个文件,您可以将此关系表示为使用分组目标的规则。分组目标规则使用分隔符&:
(这里的&
用来表示所有
)。
当make
构建分组目标中的任何一个时,它了解到组中的所有其他目标也作为调用规则的结果而更新。此外,如果只有分组目标中的一些目标已过期或丢失,make
会意识到运行规则将更新所有目标。最后,如果分组目标中的任何一个目标已过期,那么所有分组目标都会被视为已过期。
举个例子,以下规则定义了一个分组目标:
foo bar biz &: baz boz
echo $^ > foo
echo $^ > bar
echo $^ > biz
在执行分组目标的规则期间,自动变量$@
会设置为触发规则的组中的特定目标的名称。在分组目标规则的配方中使用此变量时必须小心。与独立目标不同,分组目标规则必须包含一个配方。但是,分组目标的成员也可以出现在没有配方的独立目标规则定义中。
每个目标只能与一个规则关联。如果分组目标出现在独立目标规则中或另一个分组目标规则中并具有配方,您将会收到警告,并且后者的规则将替代前者的规则。此外,目标将从以前的组中删除,只会出现在新的组中。
如果您希望一个目标出现在多个组中,那么必须使用双冒号分隔符&::
来声明包含该目标的所有组。分组双冒号目标各自独立考虑,每个分组双冒号规则的配方在需要更新其多个目标中至多执行一次。
下一篇:练习5 Makefile之配方规则,上一篇:练习3 Makefile之简单介绍,目录|首页
一个目标的多个规则
一个文件可以成为多个规则的目标。所有规则中提到的前提条件将合并成一个前提条件列表,用于目标。如果目标比来自任何规则的任何前提条件都旧,将执行该规则。
对于一个文件只能有一个配方。如果多于一个规则为同一文件提供了配方,make
将使用最后一个规则并打印错误消息(作为特殊情况,如果文件的名称以点开头,将不会打印错误消息。这种奇怪的行为只是为了与其他make
实现兼容…您应该避免使用它)。偶尔,有多个不同部分的Makefile中的同一目标会调用多个不同的配方,您可以使用双冒号规则来实现这一点。
额外的只包含前提条件的规则可以用来为多个文件一次提供一些额外的前提条件。例如,Makefile经常包含一个变量,比如objects,其中包含正在制作的系统中所有编译器输出文件的列表。要表示如果config.h更改,则必须重新编译它们,一个简单的方法是编写以下内容:
objects = foo.o bar.o
foo.o : defs.h
bar.o : defs.h test.h
$(objects) : config.h
这可以随时插入或删除,而不会改变真正规定如何制作对象文件的规则,这是一个方便的形式,如果您希望间歇性地添加额外的前提条件。
另一个技巧是,可以使用设置为make命令行参数的变量来指定额外的前提条件。例如,
extradeps=
$(objects) : $(extradeps)
意味着命令make extradeps=foo.h
将认为foo.h是每个对象文件的前提条件,但纯粹的make
则不会。
如果一个目标的所有显式规则都没有配方,那么make
会搜索适用的隐式规则来查找一个.
下一篇:练习5 Makefile之配方规则,上一篇:练习3 Makefile之简单介绍,目录|首页
静态模式规则
静态模式规则在Make中允许您指定一个模式,匹配多个目标,并根据目标的名称构建每个目标的先决条件。它们比具有多个目标的普通规则更灵活,因为每个目标的先决条件不必相同,尽管它们应该是类似的。
静态模式规则的语法如下:
targets: target-pattern: prereq-patterns
recipe
这是语法的各个部分的含义:
targets
:这是与目标模式匹配的目标列表。通过将目标模式应用于stem(下面会详细介绍),可以生成这些目标。target-pattern
:这是用于匹配目标的模式。通常它包含一个模式字符,%
,代表stem。stem是与%
匹配的目标的子字符串,并用于构建先决条件。prereq-patterns
:这些是与每个目标对应的先决条件的模式。与target-pattern一样,它们也可以包含%
,以表示相同的stem。recipe
:这是用于构建每个目标的配方,就像常规规则中一样。
静态模式规则比隐含规则更灵活,因为它们允许您指定目标和它们的先决条件的模式,而这些模式不必完全匹配。因此,您可以使用它们以有序的方式构建具有不同先决条件的多个目标。
以下是一个示例,用于说明静态模式规则:
objects = foo.o bar.o biz.o
# 此规则匹配对象和它们对应的源文件。
$(objects): %.o: %.c
$(CC) -c $< -o $@
在此示例中,$(objects)
是目标对象的列表,规则指定每个.o
文件依赖于对应的.c
文件。目标模式和先决条件模式中的%
字符是用于stem的占位符,当构建每个目标时,它将被替换为foo
、bar
和biz
。
静态模式规则可以是一种强大的方式来构建具有不同先决条件的多个目标,特别在处理具有许多相似文件的复杂项目时非常有用。[索引]
下一篇:练习5 Makefile之配方规则,上一篇:练习3 Makefile之简单介绍,目录|首页
静态模式规则的语法
静态模式规则的语法如下:
targets …: target-pattern: prereq-patterns …
recipe
…
这是静态模式规则的各个部分:
targets
:指定规则适用于的目标列表。这些目标可以包含通配符字符,就像普通规则的目标一样。target-pattern
:用于匹配目标的模式。通常包含一个%
字符,该字符在匹配时会被替换为目标的一部分,称为stem。stem随后用于生成每个先决条件的名字。prereq-patterns
:这些部分模式定义了每个目标的先决条件。每个目标都会根据target-pattern
与target
匹配,从中提取stem。然后stem会替换prereq-patterns
中的%
,以生成每个先决条件的名字。
通常,模式中只包含一个%
字符。target-pattern
与目标匹配时,%
可以匹配目标名称的任何部分。这一部分被称为stem。其余的模式必须完全匹配。例如,目标foo.o
匹配模式%.o
,foo
是stem。而foo.c
和foo.out
不匹配该模式。
每个目标的先决条件是通过将stem替换为%
模式中的%
来生成的。例如,如果某个先决条件模式是%.c
,那么替换stem foo
将生成先决条件名foo.c
。您可以编写不包含%
的先决条件模式;在这种情况下,这个先决条件对所有目标都是相同的。
模式规则中的%
字符可以通过前面的反斜杠\
进行转义。在转义过程中,可能会有多个反斜杠\\
来转义前面的反斜杠和%
字符。但在与文件名进行比较或在stem中进行替换之前,不会删除转义的反斜杠。这是因为反斜杠通常用于保留特殊字符,如%
。
以下是一个示例,用于说明静态模式规则:
objects = foo.o bar.o
all: $(objects)
$(objects): %.o: %.c
$(CC) -c $(CFLAGS) $< -o $@
在此示例中,$(objects)
是目标对象的列表,规则指定每个.o
文件依赖于对应的.c
文件。$<
是自动变量,它保存了先决条件的名称,$@
是自动变量,它保存了目标的名称。
每个指定的目标必须与target-pattern
匹配,否则将发出警告。如果您有一个文件列表,其中只有一部分匹配模式,您可以使用filter
函数来删除非匹配的文件名。
另一个示例展示了如何在静态模式规则中使用$*
:
bigoutput littleoutput : %output : text.g
generate text.g -$* > $@
在generate
命令运行时,$*
将扩展为stem,要么是’big’,要么是’little’。这允许您在一个规则中构建多个目标,这些目标的stem将在配方中引用。
下一篇:练习5 Makefile之配方规则,上一篇:练习3 Makefile之简单介绍,目录|首页
静态模式规则与隐式规则
静态模式规则与定义为模式规则(Pattern Rule)的隐式规则(Implicit Rule)有很多共同之处。两者都具有用于目标的模式以及用于构建先决条件名称的模式。它们的区别在于make决定何时应用规则的方式。
隐式规则可以适用于与其模式匹配的任何目标,但仅当目标没有另外指定配方(recipe)时才适用,且只有在可以找到先决条件时才适用。如果有多个隐式规则看似适用,只有一个会生效;选择取决于规则的顺序。
相比之下,静态模式规则适用于您在规则中明确指定的目标列表。它不能适用于其他目标,而且它一定适用于指定的每个目标。如果有两个冲突的规则适用,并且两者都有配方,那将是一个错误。
静态模式规则可能比隐式规则更好,原因如下:
- 您可能希望为一些无法按语法分类的文件重写默认的隐式规则,但可以将它们列在显式列表中。
- 如果您不能确定正在使用的目录的确切内容,那么您可能不确定其他哪些无关的文件可能会导致make使用错误的隐式规则。选择可能取决于隐式规则搜索的顺序。使用静态模式规则,就没有不确定性:每个规则都精确适用于指定的目标。
下一篇:练习5 Makefile之配方规则,上一篇:练习3 Makefile之简单介绍,目录|首页
双冒号规则
双冒号规则(Double-colon rules)是显式规则,其目标名称后面使用::
而不是:
。与普通规则相比,在相同目标出现在多个规则中时,它们的处理方式不同。带有双冒号的模式规则具有完全不同的含义。
当一个目标出现在多个规则中时,所有规则必须是相同类型:要么都是普通规则,要么都是双冒号规则。如果它们是双冒号规则,那么它们中的每一个都是相互独立的。如果某个双冒号规则的目标比该规则的任何先决条件都要旧,那么该双冒号规则的配方将被执行。如果没有适用于该规则的先决条件,那么它的配方将始终被执行(即使目标已经存在)。这可能导致执行零个、一个或所有的双冒号规则。
具有相同目标的双冒号规则实际上是完全独立的。每个双冒号规则都会被单独处理,就像具有不同目标的规则一样。
双冒号规则为目标提供了一种不同的更新方法,具体取决于哪些先决条件文件引发了更新,这类情况比较罕见。
每个双冒号规则都应该指定一个配方;如果没有指定,将使用隐式规则(如果适用)。
下一篇:练习5 Makefile之配方规则,上一篇:练习3 Makefile之简单介绍,目录|首页
自动推导
在程序的Makefile中,你通常需要编写许多规则,这些规则仅指出一些目标文件依赖于一些头文件。例如,如果main.c通过#include使用hello.h,你会编写以下规则:
main.o: hello.h
你需要这个规则,以便make知道必须在hello.h更改时重新生成main.o。你可以看到,对于大型程序,你必须在Makefile中编写数十个这样的规则。而且,每当添加或删除#include时,你都必须非常小心地更新Makefile。
为了避免这种麻烦,大多数现代编译器可以为你编写这些规则,通过查看源文件中的#include行。通常,可以通过编译器的-MM
选项来实现这一点。例如,以下命令:
g++ -MM main.cpp
将生成如下输出:
main.o : main.cpp defs.h
因此,你不再需要亲自编写所有这些规则。编译器将为你完成这项工作。
需要注意的是,这样的规则构成了在Makefile中提到main.o,因此它永远不能被隐式规则搜索视为中间文件。这意味着make永远不会在使用后删除该文件。
在旧的make程序中,惯例做法是使用此编译器功能按需生成先决条件,例如使用“make depend”命令。该命令将创建一个名为depend的文件,其中包含所有自动生成的先决条件;然后,Makefile可以使用include命令将它们读取。
我们建议用于自动生成先决条件的做法是为每个源文件创建一个对应的Makefile。对于每个源文件name.cpp,都有一个名为name.d的Makefile,其中列出了目标文件name.o依赖的文件。这样,只需要重新扫描已更改的源文件以生成新的先决条件。
以下是用于从名为name.cpp的源文件生成先决条件文件(即Makefile)name.d的模式规则:
%.d: %.c
@set -e; rm -f $@; \
$(CC) -MM $(CPPFLAGS) $< > $@.$$$$; \
sed 's,\($*\)\.o[ :]*,\1.o $@ : ,g' < $@.$$$$ > $@; \
rm -f $@.$$$$
shell中的“-e”标志会导致它立即退出,如果$(CC)命令(或任何其他命令)失败(退出时带有非零状态)。
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)
下一篇:练习5 Makefile之配方规则,上一篇:练习3 Makefile之简单介绍,目录|首页