用最美味的例子学习Makefiles

2022-02-22 00:00:59

我之所以创建这个指南,是因为我从来都不太了解Makefiles。他们似乎充斥着隐藏的规则和深奥的符号,问简单的问题并不能得到简单的答案。为了解决这个问题,我花了几个周末坐下来,读了所有关于Makefiles的书。我';我把最重要的知识浓缩到这本指南中。每个主题都有一个简短的描述和一个独立的示例,您可以自己运行。

如果您主要理解Apple,请考虑查看MaxFoeCooBook,它有一个中等大小的项目的模板,对MaX文件的每个部分做了充分的注释。

祝你好运,我希望你能够杀掉这个令人困惑的Makefiles世界!

Makefiles用于帮助决定需要重新编译大型程序的哪些部分。在绝大多数情况下,C或C++文件被编译。其他语言通常有自己的工具,其用途与Make类似。它也可以在程序之外使用,当你需要一系列的指令来运行时,这取决于文件发生了什么变化。本教程将重点介绍C/C++编译用例。

这里';这是一个可以用Make构建的依赖关系图示例。如果有任何文件';s依赖项更改,则文件将重新编译:

流行的C/C++替代构建系统有SCons、CMake、Bazel和Ninja。一些代码编辑器,如Microsoft Visual Studio,有自己的内置生成工具。对于Java,有';s Ant,Maven和Gradle。Go和Rust等其他语言都有自己的构建工具。

Python、Ruby和Javascript等解释语言;不需要模拟来生成文件。Makefiles的目标是根据更改的文件来编译任何需要编译的文件。但是,当解释语言中的文件发生更改时,不需要重新编译任何内容。程序运行时,将使用文件的最新版本。

Make有多种实现方式,但本指南的大部分内容都适用于任何版本';重复使用。然而,它';它是专门为GNU Make编写的,GNU Make是Linux和MacOS上的标准实现。所有的例子都适用于Make版本3和版本4,除了一些深奥的差异之外,它们几乎是等价的。

要运行这些示例,您';我需要一个终端和";制作";安装。对于每个示例,将内容放在一个名为Makefile的文件中,并在该目录中运行make命令。让';让我们从最简单的makefile开始:

那';就这样!如果你';你有点困惑,在这里';这是一个视频,介绍了这些步骤,以及Makefiles的基本结构。

目标是文件名,用空格分隔。通常,每条规则只有一条。

这些命令是一系列通常用于生成目标的步骤。它们需要以制表符开始,而不是空格。

先决条件也是文件名,用空格分隔。在运行目标命令之前,这些文件必须存在。这些也被称为依赖项

下面的Makefile有三个单独的规则。在终端中运行make blah时,它将通过一系列步骤构建一个名为blah的程序:

然后运行cc-c命令,因为所有这些都是废话。o依赖项已完成

等等。抄送废话。o-o废话#跑第三废话。o:废话。等等。等等。o#是第二个废话。c:echo";int main(){return 0;}" >;废话。c#先跑

这个makefile只有一个目标,叫做some_file。默认目标是第一个目标,因此在本例中,将运行一些_文件。

这个文件第一次会生成一些_文件,第二次会注意到它';s已经制造,导致制造:';一些文件';是最新的。

在这里,目标是一些#文件#34;取决于";在另一个文件上。当我们运行make时,将调用默认目标(一些#u文件,因为它是第一个文件)。它将首先查看其依赖项列表,如果其中任何依赖项比较旧,它将首先运行这些依赖项的目标,然后自行运行。第二次运行时,两个目标都不会运行,因为两个目标都存在。

一些_文件:其他_文件回声#34;这将以秒为单位运行,因为它取决于其他#文件#34;触摸其他文件:echo";这将首先运行";触摸其他文件

这将始终运行两个目标,因为某些_文件依赖于另一个_文件,而另一个永远不会创建。

clean通常用作删除其他目标的输出的目标,但它在make中不是一个特殊的词。

变量只能是字符串。你';我通常希望使用:=,但=也可以。参见变量第2部分。

文件:=file1 file2一些文件:$(文件)echo";看看这个变量:"$(文件)触摸一些文件文件1:触摸文件1文件2:触摸文件2清洁:rm-f文件1文件2一些文件

制定多个目标,你想让所有目标都运行吗?制定一个全面的目标。

全部:一二三一:触摸一二:触摸二三:触摸三清洁:rm-f一二三

当规则有多个目标时,将为每个目标运行命令。$@是一个包含目标名称的自动变量。

*和%在Make中都被称为通配符,但它们的含义完全不同。*在文件系统中搜索匹配的文件名。我建议您总是将其包装在通配符函数中,否则您可能会陷入下面描述的常见陷阱。

危险:当*不匹配任何文件时,它保持原样(除非在通配符函数中运行)

有问题:=*。o#Don和#39;别这样'*' 无法展开正确的内容:=$(通配符*.o)全部:一二三四失败,因为$(出错)是字符串";*。o";第一:$(有错)#保持为*。o如果没有与此模式匹配的文件:(两个::.o#按照您的预期工作!在本例中,它什么也不做。三:$(正确的东西)#与规则三四相同:$(通配符*.o)

%是非常有用的,但由于它可以用于多种情况,因此有点令人困惑。

在"中使用时;匹配";模式,它匹配字符串中的一个或多个字符。这场比赛被称为干。

在"中使用时;更换";模式下,它将匹配的词干替换为字符串中的词干。

嘿:一两个输出";嘿";,因为这是第一个目标echo$@#输出比目标echo$更新的所有先决条件?#输出所有先决条件$^touch hey one:touch one two:touch two clean:rm-f hey one two

用c语言编译。每一次它表达爱的时候,事情都会变得扑朔迷离。也许Make中最令人困惑的部分是所制定的魔法/自动规则。打这些电话";隐式";规则。我不';我个人不同意这个设计决定,我也不';我不建议使用它们,但它们';我们经常使用,因此了解它们很有用。这里';这是一个隐式规则列表:

编译C程序:n.o由n.C自动生成,命令格式为$(CC)-C$(CPPFLAGS)$(CFLAGS)

编译C++程序:N.O由N.CC或N.CPP自动生成,并带有表单$(CXX)-C$(CPPPFLAGS)$(CXFLAGS)的命令。

链接单个对象文件:n通过运行命令$(CC)$(LDFLAGS)n.o$(LOADLIBES)$(LDLIBS)从n.o自动生成

LDFLAGS:当编译器应该调用链接器时,给它们的额外标志

让';让我们看看现在如何构建一个C程序,而不必明确告诉Make如何编译:

CC=gcc#隐式规则的标志scflags=-g#隐式规则的标志。打开调试信息#隐式规则#1:blah是通过C链接器隐式规则#隐式规则#2:blah构建的。o是通过C编译隐式规则构建的,因为诸如此类。存在等等。哦,废话。c:echo";int main(){return 0;}" >;废话。c清洁:rm-f等等*

静态模式规则是在Makefile中写得更少的另一种方式,但我';d说它们更有用,但有点少#34;魔法";。这里';这是他们的语法:

本质上,给定的目标与目标模式匹配(通过%通配符)。匹配的东西叫做茎。然后将stem替换为prereq模式,以生成目标';这是先决条件。

典型的用例是编译。c文件进入。o文件。这里';这是手动方式:

objects=foo。哦,酒吧。哦,好的。o所有:$(对象)#这些文件通过隐式规则foo编译。o:福。c酒吧。o:酒吧。c全部。o:好的。c全部。c:echo";int main(){return 0;}" >;全部的c%。c:touch$@clean:rm-f*。c*。哦,所有

objects=foo。哦,酒吧。哦,好的。o所有:$(对象)#这些文件通过隐式规则编译#语法-目标…:目标模式:预请求模式…#对于第一个目标,foo。o、 目标模式与foo匹配。o并设置";茎和#34;34岁;foo";#然后它将替换';%' 在带有干$(对象)的prereq模式中:%。o:%。c全部。c:echo";int main(){return 0;}" >;全部的c%。c:touch$@clean:rm-f*。c*。哦,所有

在我稍后介绍函数时,我';I’我将为你能用它们做些什么做铺垫。可以在静态模式规则中使用filter函数来匹配正确的文件。在这个例子中,我编写了。生的和生的。结果扩展。

obj_files=foo。结果栏。哦,输了。osrc_files=foo。生酒吧。c输了。c全部:$(obj_文件)$(过滤器%.o,$(obj_文件)):%。o:%。c echo和#34;目标:$@prereq:$<" $(筛选器%.result,$(obj_文件)):%。结果:%。原始回声";目标:$@prereq:$<" %.c%。原始:touch$@clean:rm-f$(src_文件)

模式规则经常被使用,但相当混乱。你可以从两个方面来看待它们:

#定义一个模式规则,该规则编译每个。c文件转换成一个。o文件%。o:%。c$(CC)-c$(CFLAGS)$(CPPFLAGS)$<-o$@

模式规则包含一个';%' 在目标中。这个';%' 匹配任何非空字符串,其他字符匹配它们自己。“%”在模式的先决条件中,规则代表与目标中“%”匹配的同一个干。

#定义一个先决条件中没有模式的模式规则。#这只会产生空的。需要时使用c文件。%。c:触摸$@

很少使用双冒号规则,但允许为同一目标定义多个规则。如果这些是单冒号,则会打印一条警告,并且只会运行第二组命令。

在命令前添加一个@以阻止打印。您还可以使用-s运行make以在每行之前添加一个@

每个命令都在一个新的shell中运行(或者至少效果是这样的)

全部:cd。。#上面的cd不影响此行,因为每个命令都在一个新的shell echo`pwd`中有效地运行#此cd命令影响下一个命令,因为它们在同一行cd上。。;echo`pwd`#与上述cd相同\echo`pwd`

运行make时添加-k,即使遇到错误也可以继续运行。如果您想同时查看Make的所有错误,这将非常有用。在命令之前添加一个-Add-i以抑制错误,并为每个命令添加-i以使其发生这种情况。

第一:#此错误将被打印但忽略,make将继续运行-错误触摸一

只需注意:如果按住ctrl+c组合键,它将删除刚刚创建的较新目标。

要递归调用makefile,请使用特殊的$(MAKE)而不是MAKE,因为它将为您传递MAKE标志,并赢得';它本身不会受到它们的影响。

新内容=";您好:\n\t点击内部文件";全部:mkdir-p subdir printf$(新内容)|sed-e';s/^/'>;subdir/makefile cd subdir&&$(使)清洁:rm-rf细分

export指令接受一个变量,并使子make命令可以访问该变量。在本例中,将导出cooly,以便subdir中的makefile可以使用它。

注意:export的语法与sh相同,但它们不是';t相关(尽管功能相似)

新内容=";你好:\n\\techo\$$(酷)和#34;全部:mkdir-p subdir echo$(新内容)|sed-e';s/^/'>;subdir/makefile@echo"---MAKEFILE内容--";@cd-R&&;cat makefile@echo"---结束MAKEFILE内容--";cd&&$(MAKE)#注意变量和导出。它们在全球范围内受到影响。cooly=";子目录可以看到我" export cooly#这将使上面的行无效:unexport cooly clean:rm-rf subdir

one=这只在本地工作export two=我们可以使用以下所有命令运行子命令:@echo$(一)@echo$$one@echo$(二)@echo$$two

.EXPORT_ALL_变量:new_contents=";你好:\n\techo\$$(酷)和#34;cooly=";子目录可以看到我" # 这将使上面的行无效:unexport cooly all:mkdir-p subdir echo$(新内容)|sed-e';s/^/'>;subdir/makefile@echo"---MAKEFILE内容--";@cd-R&&;cat makefile@echo"---结束MAKEFILE内容--";cd&&$(使)清洁:rm-rf细分

那里';这是一个很好的选项列表,可以从make运行。检查——试运行,——触摸,——旧文件。

你可以设定多个目标,例如,让清洁运行测试运行清洁目标,然后运行,然后测试。

递归(use=)-仅在使用命令时查找变量,而不是在';这是定义。

简单扩展(使用:=)——就像普通的命令式编程一样——只有到目前为止定义的程序才能被扩展

#递归变量。这将打印";后来";belowone=one${later_variable}#简单扩展变量。这不会打印";后来";belowtwo:=two${later_variable}later_variable=later all:echo$(一)echo$(二)

简单扩展(使用:=)允许您附加到变量。递归定义将产生无限循环错误。

one=hello#one被定义为一个简单的扩展变量(:=),因此可以处理appendingone:=${one}那里all:echo$(one)

一个=你好吗?=不会是第二集吗?=将全部设置为:echo$(一个)echo$(两个)

线条末端的空间不会被剥离,但开头的空间会被剥离。要使用单个空格生成变量,请使用$(nullstring)

with_spaces=hello#with_spaces在"之后有许多空格;你好";after=$(带_空格)therenullstring=space=$(nullstring)#用一个空格生成一个变量。全部:echo"$(之后)和#34;回声启动"$(空间)和#34;终止

字符串替换也是修改变量的一种非常常见和有用的方法。还可以查看文本函数和文件名函数。

可以通过使用override覆盖来自命令行的变量。这里我们运行了make with make option_one=hi

#Overrides命令行参数override option_one=did#override#not override命令行参数option_two=not_override all:echo$(option_one)echo$(option_two)

"定义";实际上只是一个命令列表。它与函数无关。注意这里是';这与在命令之间使用分号有点不同,因为正如预期的那样,每个命令都在单独的shell中运行。

一个=出口废话=";我被安排好了"; echo$$blah define two export blah=setecho$$blah endef#一和二是不同的。全部:@echo";这张照片';我被设定为'"@echo";这不打印';我被设定为';因为每个命令都在一个单独的shell中运行#34;@$(两个)

nullstring=foo=$(nullstring)#行尾;这里有一个空间:ifeq($(strip$(foo)),)echo";脱光衣服后,foo是空的";endif ifeq($(空字符串),)echo";空字符串不';甚至没有空间";恩迪夫

ifdef不展开变量引用;它只是看看是否有什么定义

bar=foo=$(bar)all:ifdef foo echo";foo的定义是";endif ifdef bar echo";但酒吧不是";恩迪夫

此示例演示如何使用findstring和MAKEFLAGS测试make标志。用make-i运行这个例子,看它打印出echo语句。

bar=foo=$(bar)all:#搜索"-我";旗帜MAKEFLAGS只是单个字符的列表,每个标记一个字符。因此,寻找";我";在这种情况下。ifneq(,$(findstring i,$(MAKEFLAGS)))echo和#34;我被传给MAKEFLAGS";恩迪夫

函数主要用于文本处理。使用$(fn,参数)或${fn,参数}调用函数。您可以使用call builtin功能自行创建。Make有相当多的内置功能。

逗号:=,空:=空格:=$(空)$(空)foo:=abcbar:=$(subst$(空格),$(逗号),$(foo))全部:@echo$(条)

在第一个参数之后的参数中不要包含空格。这将被视为字符串的一部分。

逗号:=,空:=空格:=$(空)$(空)foo:=abcbar:=$(subst$(空格),$(逗号),$(foo))全部:#输出为";,a,b,c";。注意@echo$(条形)中引入的空格

"在匹配模式的文本中查找空格分隔的单词,并将其替换为替换。在这里,模式可能包含一个“%”作为通配符,匹配一个单词中任意数量的字符。如果替换还包含“%”,则“%”将被模式中与“%”匹配的文本替换。只有模式和替换中的第一个“%”被这样处理;任何后续“%”都将保持不变" (GNU文档)

那里';这是另一个只替换后缀的缩写:$(text:suffix=replacement)。此处未使用%通配符。

注:唐和#39;不要为这个速记增加额外的空格。它将被视为搜索或替换术语。

foo:=a.o b.o l.a c.oone:=$(patsubst%.o,%.c,$(foo))#这是上述两种语言的简写形式:=$(foo:%.o=%.c)#这是唯一的后缀简写形式,也相当于上述两种语言。三:=$(foo:.o=.c)全部:echo$(一)echo$(二)echo$(三)

foreach函数如下所示:$(foreach变量、列表、文本)。它将一个单词列表(用空格分隔)转换为另一个。var被设置为列表中的每个单词,文本被展开为每个单词。这会在每个单词后附加一个感叹号:

foo:=你是谁#每个#34;单词";在foo中,在后面加一个感叹号:=$(foreach wrd,$(foo),$(wrd)!)全部:#输出为";谁是你"@echo$(巴)

if检查第一个参数是否为非空。如果是,则运行第二个参数,否则运行第三个参数。

Make支持创建基本函数。你";定义";该函数只需创建一个变量,但使用参数$(0),$(1)等。然后使用特殊调用函数调用该函数。语法是$(call variable,param,param)$(0)是变量,而$(1),$(2)等是参数。

sweet_new_fn=变量名:$(0)第一个:$(1)第二个:$(2)空变量:$(3)所有:#输出和#34;变量名:sweet_new_fn First:go Second:tigers空变量:";@echo$(呼叫sweet_new_fn,加油,老虎队)

include指令告诉make读取一个或多个其他makefile。它';makefile makefile中的一行如下所示:

当使用编译器标志(如-M)基于源代码创建makefile时,这一点尤其有用。例如,如果一些c文件包含一个头文件,该头文件将被添加到一个生成文件中,该文件为';这是gcc写的。我在Makefile烹饪书中详细讨论了这一点

使用vpath指定某些先决条件集存在的位置。格式为vpath<;模式>&书信电报;目录,空格/冒号分隔>&书信电报;模式>;可以有一个%匹配任何零个或多个字符。您还可以使用变量VPATH全局执行此操作

vpath%。h/标题/其他目录一些二进制文件:/标题之类的。触摸一些二进制文件/标题:mkdir/标题之类的。h:触摸/标题/诸如此类。h清洁:rm-rf/头文件rm-f一些二进制文件

反斜杠(";\";)字符使我们能够在命令过长时使用多行

some_file:echo这行太长,所以被分成多行

添加。假冒目标将防止make将假冒目标与文件名混淆。在本例中,如果文件clean是c

......