我很困惑 所以说我有以下
all: one one: two two: three three: echo Hello
然后 创建文件“ all”的时间比文件“一个”的时间最近,或者创建文件“三”的时间比文件“一个”的时间最近。
答案 0 :(得分:1)
all
,one
,two
和three
不是文件。它们是目标。
您的任何规则都不会创建与这些目标相对应的任何文件。 Make会将目标视为文件,因为它们没有声明为 phony 。
文件中的第一个目标是all
,因此它是默认目标。因为它不是.PHONY
目标,所以Make将访问文件系统以查看是否存在与目标相对应的名为all
的文件。它会注意到all
不存在。因此,必须执行all
规则的主体(“食谱”)才能创建all
。但是,这不能立即完成。
all
的空配方无法立即执行,因为all
具有称为one
的先决条件。 Make将访问文件系统以查看one
是否存在,并发现不存在。在这种情况下,Make会搜索其余规则,以查看one
是否作为目标出现。因此,它随后处理one : two
规则以尝试更新one
目标。
以这种方式,它会遍历所有相关规则,直到达到three:
规则为止。这没有先决条件,并且不存在名为three
的文件,因此将执行配方。 echo
命令运行。
运行echo
命令后,配方以成功状态终止。此时,Make假设目标three
已被更新,而忽略了该文件仍然不存在的情况。请记住,目标与文件不是同一回事;这是Make中的一个概念,也是Make内存中的一个对象。 “目标已更新”状态仅表示由于配方已指示成功执行,使得Make在内部将其内存中目标对象标记为已更新。
由于three
目标被视为已更新,并且它是two
的唯一前提,因此two
规则现在可以被激活。它的空主体已执行并成功执行,因此two
被视为已更新。
类似地,然后执行one :
规则的空主体,最后执行all
规则的空主体。
什么是假冒目标?我们这样声明它们:
.PHONY: all one two three
这告诉Make所有这些目标均与文件不对应。伪造目标始终被视为缺少相应文件的目标,因此始终需要更新。 Make不检查是否存在用于伪造目标的文件。
声明虚假目标非常重要。例如,考虑以下规则:
clean:
rm -f $(OBJS)
如果未将clean
声明为.PHONY:
,那么如果您不小心创建了一个名为clean
的文件,make clean
将无济于事!相反,您会看到类似"make: 'clean' is up to date."
的消息。
答案 1 :(得分:0)
为了回答您的问题,我将不得不在Make中区分文件和食谱。首先,我将专门针对这个特定的Makefile回答您的问题,因为从数学的角度来讲,从学术的角度来看,有一些非常有趣的代数性质使这个问题变得微不足道。如果您确实只想解释所涉及的实际机制,请随意跳过后面的理论数学部分,但我确实鼓励您探索所举示例的数学含义,因为其基础是抽象代数和一般数学概念它是函数式编程的基础,还有其他许多方面。
如果将makefile描绘为有向图,其中每个节点都是一个依赖项,那么最终将得到线性有向图:
all: one
one: two
two: three
three:
echo "Hello, world!"
成为:
all->one->two->three
对于您的特定示例,从实际意义上来说,这意味着从依赖角度进行思考是没有意义的,因为就所有意图和目的而言,先调用three
然后再调用one
会导致{{1 }}被打印两次。
食谱"Hello, world!"
总是做同样的事情:它返回字符串“ Hello,world!”。 (IO是一个副作用,但是为了简洁起见,我们将假装而不是打印函数,而是仅返回配方的字符串,对于所有意图和目的,该字符串在这种情况下通常表示同一件事。) / p>
如果我们将这些配方视为three
中的函数,其中c
表示调用main
,则我们具有以下内容:
all
输出:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
char* Three() {
return ("Hello, world!");
}
char* Two() {
return Three();
}
char* One() {
return Two();
}
int main()
{
printf("%s\n", One());
return EXIT_SUCCESS;
}
就像在您的makefile中一样,该程序调用{{1}},后者调用Hello, world!
,后者又调用main
,后者又调用One
,并返回Two
。如果我们现在先呼叫Three
,然后再呼叫Hello, world!
,就像我们所说的那样:
Three
输出:
One
这是因为您的示例是一种特殊情况,其中调用任何函数int main()
{
printf("%s\n", Three());
printf("%s\n", One());
return EXIT_SUCCESS;
}
,Hello, world!
Hello, world!
或One
等效于简单地返回一个字符串,这意味着它们是纯函数功能(忽略IO,如我们所说)。
如果我们认为您可以定义一个二进制运算来连接函数的结果:
Two
然后我们可以更进一步,并意识到该操作是关联的,因为Three
和One()·Two() => "Hello, world!"+"Hello, world!"
。
通过建立关联组合的二进制运算,我们可以确认其为Semigroup。
对类型及其组成的抽象代数分析是类别理论,类型理论,Haskell等事物的基础。
因此,让我们通过具体回答您的问题来结束对makefile的理论分析:在调用配方One()·Two() => "Hello, world!"+"Hello, world!"
之前调用配方Two()·One() => "Hello, world!"+"Hello, world!"
是否会影响three
的打印?
否;在上面定义了半组之后,很明显,从其组成的关联特性来看,在这种情况下,我们称食谱为 的顺序是无关紧要的,从而导致结构上相同的结果。
Make使用时间戳来确定是否触发特定配方的重建。如前所述,您的构建链是文字链,因此,请考虑以下内容:
one
在这种情况下,您将只对源文件进行编辑,而不是使用十六进制编辑器来操作目标文件。构建二进制文件后,您将拥有Hello, world!
,这是不言自明的。
重要的是要注意,在make的上下文中,这些不是“功能”。您实际上不应该明确地调用这些食谱。否则,使用构建系统有什么意义?因此,您实际上不会在调用all: untitled
untitled: untitled.o
untitled.o: untitled.c
之前先调用all
,但是第二次您对源文件untitled
进行了更改并调用make之后,它将自上而下地系统检查配方(就项目->对象->源而言,而不是在文件中的位置而言),并通过比较untitled.o
早于其依赖项并需要重建的时间戳来查看。
假设您有以下内容,而您没有线性构建方法:
untitled.c
构建系统仍然可以正常运行。假设您在b中有同时在untitled.o
和all: untitled
untitled: main.o a.o b.o c.o
main.o: main.c
a.o: a.c
b.o: b.c
c.o: c.c
中使用的函数。在main
中声明所有函数后,您要做的就是在main和a中都包含该标头,并且即使main和a不知道实现是否正确,这些函数也可以正常工作a
中的“”已更改,因为他们只能访问b.h
。
请记住,函数名称本质上是指向其在内存中位置的指针。当包含另一个源文件的标头时,实际上是在声明函数原型,因为预处理程序会将标头文件的文本复制到包含该文件的文件中,这取决于指定指令的指示。 (这等效于简单地编写b.c
或任何函数的名称和签名。)您不是定义函数,只是指定这些函数具有外部链接-因此术语b.h
-,它们将在以后由链接程序映射出来。
构建完所有目标文件后,链接器将使用符号表和重定位条目来生成单个可执行文件。
这就是为什么像make这样的构建系统可以工作的原因。由于每个编译单元只需要知道函数的结构即可调用它(函数的参数必须放在特定的寄存器和/或堆栈上,具体取决于ABI ,平台等),而不是实现,对另一个编译单元的任何更改都应仅触发对该单元的重新编译,并调用链接器以重新映射可执行文件。
重申一下,通过对文件的时间戳进行比较,可以对目标文件所依赖的源文件的实际更改进行检测。这意味着您可以通过简单地在源文件上使用extern void DoSomething();
命令并重新运行make来人为地触发重建,而无需修改任何依赖项。
希望这会有所帮助。