当前位置:网站首页 > Java教程 > 正文

java makefile教程



我建立了这个指南,因为我以前总也搞不清Makefiles。 它们似乎充斥着隐藏的规则和深奥的符号,网上提问简单的问题也得不到简单的答案。 为了解决这个问题,我花了了几个周末,阅读了有关Makefiles的所有内容。 我已经将最关键的知识浓缩到了这本指南中。 每个主题都有一个简短的描述和一个你可以自己运行的独立示例。

如果你已经大体理解Make,可以考虑阅读 Makefile Cookbook ,其中有一个针对中型项目的模板,且包含有关Makefile每个部分所做工作的大量注释。

祝你好运,希望你能够摆平令人困惑的Makefiles世界!

Makefiles用于帮助确定大型程序的哪些部分需要重新编译。 在绝大多数情况下,C或C++文件都是需要编译的。 其他语言通常都有自己的工具,它们的用途与Make类似。 在编译之外Make也可用于当你需要根据文件更改情况运行一系列指令时。 本教程将重点介绍C/C++编译用例。

下面是一个示例依赖关系图,你可能将Make用于类似的情况。 如果任何文件的依赖关系发生更改,则将重新编译该文件:

流行的C/C++替代构建系统是 SCons , CMake , Bazel ,和 NINJA 。 一些代码编辑器,如 Microsoft Visual Studio 有自己的内置构建工具。 对于JAVA,有 Ant , Maven ,和 Gradle 。 像Go和Rust这样的其他语言都有自己的构建工具。

像Python、Ruby和Javascript这样的解释语言不需要类似Makefile的工具。 Makefiles的目标是根据更改的文件来编译需要编译的任何文件。 但当解释语言的文件发生变化时,不需要重新编译。 那些程序运行时,会直接使用文件的最新版本。

有各种各样的实现,但本指南的大部分将适用于你使用的任何版本。 但这个教程还是专门为GNU Make编写的,GNU Make是Linux和MacOS上的标准实现。 所有的例子都适用于Make版本3和4,除了一些深奥的差异之外,它们几乎是等同的。

要运行这些示例,你需要安装一个终端并安装好“make”。 对于每个示例,请将内容放在一个名为的文件中,并在该目录中运行命令。 让我们从最简单的Makefiles开始:

 

注意:Makefile 必须 使用制表符而不是空格进行缩进,否则将失败。

下面是运行上述示例的输出:

 

就这样! 如果你有点困惑,这里有一个视频,它演示了这些步骤,并描述了makefile的基本结构。

Makefile由一组 rules 组成。 rule通常如下所示:

 

  • targets (目标) 是文件名,用空格分隔。 通常,每个rule只有一个。
  • commands (命令) 是通常用于创建目标的一系列步骤。 这些 需要以制表符 开头,不可以是空格。
  • prerequisites(先决条件) 也是文件名,用空格分隔。 在运行目标的命令之前,这些文件需要存在。 这些也称为 dependencies(依赖项)

让我们从一个hello world的例子开始:

 

这里已经包含很多东西了。 让我们分解一下:

  • 我们有一个 targe 叫做‘hello`
  • 这个目标有两个 commands
  • 这个目标没有prerequisites(先决条件)

然后我们运行 。 只要文件不存在,命令就会运行。 如果 已存在,则不会运行任何命令。

需要注意的是,我所说的既是一个目标,也是一个文件*。 那是因为两者直接绑在一起。 通常,当目标运行时(也就是运行目标的命令时),这些命令将创建一个与目标同名的文件。 目前 *target 还不会创建 文件

让我们创建一个更典型的Makefile - 一个编译单个C文件的Makefile。 但是在执行此操作之前,请创建一个名为 “blah.c” 的文件,其中包含以下内容:

 

然后创建Makefile(和往常一样称为):

 

这次,尝试简单地运行 “make”。 由于没有将目标作为参数提供给命令,因此会运行第一个目标。 在这种情况下,只有一个目标 ()。 第一次运行此命令时,将创建。 第二次运行,你会看到 。 这是因为文件已经存在。 但是有一个问题: 如果我们修改 ,然后运行 ,还是什么都不会被重新编译。

我们通过添加一个先决条件来解决这个问题:

 

当我们再次运行 时,会发生以下一组步骤:

  • 第一个目标被选中,因为第一个目标是默认目标
  • 这有一个 “blah.c” 的先决条件
  • make决定是否应该运行目标。 只有当 不存在,或者 比 * 更新时,它才会运行

这最后一步是至关重要的,也是 make的本质。 它试图做的是确定自上一次编译以来,的先决条件是否发生了变化。 也就是说,如果修改了 ,运行 应该重新编译文件。 反之,如果没有变化,则无需重新编译。

为了实现这一目标,它使用文件系统时间戳作为代理来确定某些内容是否已更改。 这是一个合理的启发式方法,因为文件时间戳通常只有在文件被修改时才会更改。 但重要的是要意识到情况并不总是如此。 例如,你可以修改一个文件,然后将该文件修改后的时间戳更改为旧的时间戳。 如果这样做,Make则会错误地猜测文件没有更改,因此可以忽略。

哇,真是脑洞大开。请确保你理解这一点。 这是makefile的关键所在,可能需要你几分钟才能正确理解。 如果事情仍然令人困惑,请进行上述示例或观看上面的视频。

以下Makefile最终运行所有三个目标。 当你在终端运行时,它会按一系列步骤构建一个名为的程序:

  • make选择目标,因为第一个目标是默认目标
  • 需要, 因此搜索目标
  • 需要 ,所以搜索 目标
  • 没有依赖关系,所以运行命令
  • 然后运行 命令,因为所有 依赖关系都查询完成了
  • 运行顶部的命令,因为所有的依赖都已完成
  • 就是这样得到: 一个编译完成的c程序

 

如果删除 ,则将重新运行所有三个目标。 如果你对其进行编辑(从而将时间戳更改为比更新),则前两个目标将运行。 如果你运行 'touch blah.o' (并因此将时间戳更改为比 'blah' 更新),那么只有第一个目标将运行。 如果不更改任何内容,则不会运行任何目标。 试试看!

下一个示例没有什么新意,但却是一个很好的附加示例。 它将始终运行两个目标,因为 依赖于从未创建的 。

 

通常用作删除其他目标的输出的目标,但它在make中并不是一个专有的词。 你可以在此运行 和 来创建和删除 。

请注意,在这里做了两件新的事情:

  • 它不是第一个目标 (默认目标),也不是先决条件。 这意味着除非显式调用,否则它永远不会运行
  • 它不是一个文件名。 如果你碰巧有一个名为 “clean” 的文件,则此目标将无法运行,这不是我们想要的。 请参阅本教程后面的 ,了解如何解决此问题

 

变量只能是字符串。 你通常会希望使用,但也可以。 参见 [变量 第2部分](#变量 第2部分)。

以下是使用变量的示例:

 

使用 或 引用变量

 

要制作多个目标,而你想让所有目标都运行? 你可以制作一个目标。 由于这是列出的第一个规则,因此如果调用 而未指定目标,则默认情况下它将运行。

 

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

 

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

 

“*” 可以在目标,先决条件或 函数中使用。

危险:不能在变量定义中直接使用

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

 

确实很有用,但由于它可以在多种情况下使用,所以有点令人困惑。

  • 在“匹配”模式下使用时,匹配字符串中的一个或多个字符。 这种匹配被称为词干(stem)。
  • 在“替换”模式下使用时,它会获取匹配的词干,并替换字符串中的词干。
  • 最常用于规则定义和某些特定函数中。

有关使用它的示例,请参阅以下各节:

  • 静态模式规则
  • 模式规则
  • 字符串替换
  • vpath指令

有很多个 自动变量 ,但通常只有几个出现:

 

Make偏爱c编译。 而每次它出现这种偏爱的时候,事情就会变得混乱。 也许Make中最令人困惑的部分是Make的魔术/自动规则(magic/automatic rules)。 Make会调用这些“隐含的”规则。 我个人不同意这个设计决定,也不建议使用它们,但是它们经常被使用,因此很有用。 以下是隐含规则的列表:

  • 编译C程序: 从 自动生成 ,命令形式为
  • 编译C++程序:由或自动生成,命令形式为
  • 链接单个目标文件: 自动从构造, 通过运行命令

隐式规则使用的重要变量包括:

  • : 编译C程序的程序;默认
  • : 编译C++程序的程序; 默认 “g++”
  • : 要提供给C编译器的额外标志
  • : 给C++编译器的额外标志
  • : 要提供给C预处理器的额外标志
  • : 当编译器应该调用链接器时,会给他们额外的标志

让我们看看我们现在如何构建一个C程序,而无需明确地告诉制造如何进行编译:

 

静态模式规则是在Makefile中减少编写量的另一种方式,但我会说更有用,并且 “魔术” 技巧更少。 它们的语法如下:

 

本质是给定的 与 匹配 (通过 通配符)。 任何匹配的东西都被称为 词干。 然后将词干替换为 “prereq-pattern”,以生成目标的prereq。

一个典型的用例是将文件编译成文件。 这里是 手动方式:

 

这是使用静态模式规则的更高效的方式:

 

当我稍后介绍函数时,我将预示你可以使用它们来做什么。 过滤器函数可以在静态模式规则中使用,以匹配正确的文件。 在本例中,我编造了和扩展名。

 

模式规则经常被使用,但非常令人困惑。你可以从两个方面来看待它们:

  • 定义自己的隐含规则的方法
  • 静态模式规则的更简单形式

让我们首先从一个例子开始:

 

模式规则在目标中包含‘%’。 此 '%' 匹配任何非空字符串,其他字符匹配自己。 模式规则先决条件中的‘%’代表与目标中的‘%’匹配的同一词干。

这里是另一个例子:

 

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

 

 

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

 

默认Shell是 '/bin/sh'。你可以通过更改变量SHELL来更改此设置:

 

在运行时添加 ,即使面对错误也要继续运行。 如果你想一次查看Make的所有错误,这将非常有用。
在命令前添加 以抑制错误
添加以使每个命令都会发生这种情况。

 

备注:如果你对make按下,它将删除它刚刚创建的较新的目标。

要递归调用makefile,请使用特殊的 而不是 ,因为这样它才能为你传递make标志,并且本身不会受到它们的影响。

 

export指令采用一个变量,并使sub-make命令可以访问它。 在本例中,导出了,以便subdir中的Makefile可以使用它。

注意: export具有与sh相同的语法,但它们不相关 (尽管在功能上相似)

 

你需要导出变量,以便让它们也在shell中运行。

 

为你导出所有变量。

 

有一个很好的可以从Make运行的 options 列表。 查看 ,,。

你可以有多个目标来make,例如 ,运行,然后,然后。

有两种类型的变量:

  • 递归 (使用 ) - 在使用命令时才会查找变量,而不在定义命令时查找变量。
  • 简单展开 (使用 ) - 就像普通的命令式编程一样 -- 只有到目前为止定义的变量才会得到展开

 

简单的说展开(使用)允许你追加到一个变量。 递归定义可能会给出无限循环错误。

 

仅设置尚未设置的变量

 

行尾的空格不会被去掉,但行首的空格会被去掉。 要使用单个空格制作变量,请使用

 

未定义的变量实际上是空字符串!

 

使用 追加

 

字符串替换 也是一种真正常见且有用的修改变量的方法。 另请查看 Text Functions 和 Filename Functions 。

你可以使用覆盖来自命令行的变量。 在这里,我们用 运行 make

 

“define”实际上只是一个命令列表。 它与函数无关。 请注意,这与命令之间的分号有点不同,因为每个命令都在单独的shell中运行,如预期的那样。

 

可以为特定目标中赋值变量

 

你可以为特定目标 模式 中分配变量

 

 

 

ifdef不展开变量引用;它只查看是否定义了某些内容

 

此示例向你展示了如何使用 和 测试make标志。 使用运行此示例,以查看它打印出echo语句。

 

函数主要用于文本处理。 用 或 调用函数。 你可以使用 call 内置函数来创建自己的代码。 Make有相当数量的 内置函数 。

 

如果要使用变量替换空格或逗号

 

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

 

执行以下操作:

"在与pattern匹配的text中查找空格分隔的单词,并将其替换为replacement。 在这里,模式可以包含充当通配符的‘%’,匹配单词中任意数量的任何字符。 如果替换还包含 ‘%’,则将 ‘%’ 替换为与模式中的 ‘%’ 匹配的文本。 只有模式和替换中的第一个‘%’会以这种方式处理;任何后续的‘%’都不会改变。" (GNU docs)

替换引用 是这方面的简写。

还有一个只替换后缀的简写:。 这里不使用 通配符。

注意:不要为此简写添加额外的空格。 它将被视为搜索或替换术语。

 

 

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

 

Make支持创建基本函数。 你只需创建一个变量即可定义函数,但需要使用、等参数。 然后,你使用特殊的 'call' 函数调用该函数。 语法为。 是变量,而 ,... 等是参数。

 

shell - 这调用shell,但它用空格替换换行符!

 

include指令告诉make读取一个或多个其他多个makefiles。 一行makefile中makefile,看起来像这样:

 

当你使用诸如 之类的编译器标志基于源创建Makefiles时,这特别有用。 例如,如果某些c文件包含头文件,则该头文件将被添加到由GCC编写的Makefile中。 我在 Makefile Cookbook 中有更多探讨

使用vpath指定一些先决条件的存在位置。 格式为
可以有一个 ,它匹配任何零个或多个字符。
你还可以使用变量VPATH在全局范围内执行此操作

 

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

 

将 添加到目标将防止Make将phony(假)目标与文件名混淆。 在此示例中,如果创建了文件 “clean”,则仍将运行 make clean。 从技术上讲,我应该在每个带有或的示例中使用它,但我没有保持clean示例。 此外,"phony" 目标通常具有很少是文件名的名称,实际上,许多人忽略了它。

 

 

让我们来看看一个非常丰富的例子,它适用于中型项目。

这个Makefile的巧妙之处在于它会自动为你确定依赖项。 你所要做的就是将C/C++文件放在 “src/” 文件夹中。


                            

  • 上一篇: aws-java-sdk教程
  • 下一篇: 尚学堂java入门教程
  • 版权声明


    相关文章:

  • aws-java-sdk教程2024-12-17 11:50:05
  • java技巧视频教程2024-12-17 11:50:05
  • redis教程java代码2024-12-17 11:50:05
  • 网上java教程2024-12-17 11:50:05
  • java 系列教程2024-12-17 11:50:05
  • 尚学堂java入门教程2024-12-17 11:50:05
  • java爬虫高级教程2024-12-17 11:50:05
  • 微信公众号开发教程 java2024-12-17 11:50:05
  • 免费领java全套教程2024-12-17 11:50:05
  • java sklearn教程2024-12-17 11:50:05