首先为makefile创建哪个文件有关系吗?

时间:2018-11-08 21:08:53

标签: c gcc makefile

我很困惑 所以说我有以下

all: one

one: two

two: three

three: 

   echo Hello

然后 创建文件“ all”的时间比文件“一个”的时间最近,或者创建文件“三”的时间比文件“一个”的时间最近。

2 个答案:

答案 0 :(得分:1)

allonetwothree不是文件。它们是目标

您的任何规则都不会创建与这些目标相对应的任何文件。 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

然后我们可以更进一步,并意识到该操作是关联的,因为ThreeOne()·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.oall: 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来人为地触发重建,而无需修改任何依赖项。

希望这会有所帮助。