【问题标题】:Make overflow¹, or “How to override a target?”使溢出¹,或“如何覆盖目标?”
【发布时间】:2024-01-10 05:03:01
【问题描述】:

我有一系列(数十个)项目,其中包含 git 存储库中的大量内容。每个存储库都有一个通用工具包的 git 子模块。该工具包包含处理内容存储库和构建可发布结果所需的库和脚本。所有存储库都被推送到运行 CI 并发布结果的主机。这个想法是将重复的代码保持在绝对最低限度,并且大部分内容都在存储库中,并且依赖于工具包以将每个项目以相同的方式组合在一起。

每个项目都有一个* Makefile,通常只有几行,例如:

STAGE = stage

include toolkit/Makefile

stage 变量有一些关于这个特定的阶段的信息,决定了构建哪些格式。几乎所有其他内容都由工具包中的 600 行 Makefile 处理。构建某些输出格式可能需要一长串依赖关系:源的过程可能会触发目标规则,但要到达目标可能需要 8-10 个中间依赖关系,其中在最终目标生成之前会生成各种文件制作。

我遇到过几种情况,我想完全替换(而不仅仅是扩展)一个项目中的特定目标规则。目标在一系列依赖关系的中间被触发,但我想为这一步做一些完全不同的事情。

我尝试只替换顶层 Makefile 中的目标:

STAGE = stage

%-target.fmt:
    commands

include toolkit/Makefile

这是专门记录在要支持的,但很吸引人的是它有时会起作用。我尝试更改声明自定义目标和包含的顺序,但这似乎并没有显着影响。如果重要的话,是的,在目标中使用模式很重要。

有时,拥有一个类似于另一个 makefile 的 makefile 是很有用的。您通常可以使用“包含”指令将一个包含在另一个中,并添加更多目标或变量定义。但是,对于同一个目标,两个 makefile 给出不同的配方是无效的。

有趣的是,如果我将自定义函数放在顶层 Makefile 下面包含我可以覆盖工具包中的函数,这样$(call do_thing) 将使用我的覆盖:

STAGE = stage

include toolkit/Makefile

define do_thing
    commands
endef

但是,目标似乎并非如此。我知道two colon syntax,但我不想只是扩展具有更多依赖项的现有目标,我想用生成相同文件的不同方式完全替换目标。

我曾考虑过使用递归调用来生成as suggested in the documentation,但随后包含在工具包 Makefile 中广泛设置的辅助函数的环境将不适用于* Makefile 中的任何目标。这将是一个表演的终结者。

有什么方法可以让 make 为我破例吗?可以强制它成为压倒一切的目标吗?我只使用最新版本的 GNU-Make,并且不太关心可移植性。如果做不到这一点,还有另一种实现相同目的的概念方法吗?

¹ 我的大脑今天没有喝足够的咖啡。在尝试打开 Stack Overflow 来询问这个问题时,我在浏览器中输入了makeoverflow.com,并且很困惑为什么没有启动自动完成功能。

【问题讨论】:

  • 我说对了吗,您的工具包还包含%-target.fmt 规则以及您想要覆盖的一些默认命令?
  • @valir 是的。先决条件和配方都不同,但规则名称相同,我想用顶层的那个完全覆盖工具包中的那个。

标签: makefile gnu-make


【解决方案1】:

更新答案: 如果您的配方具有依赖项,则默认情况下不能覆盖这些依赖项。那么 $(eval) 可能会像这样拯救你:

在工具包中有一个带有通用规则的宏定义:

ifndef TARGET_FMT_COMMANDS
define TARGET_FMT_COMMANDS
    command1 # note this these commands should be prefixed with TAB character
    command2
endef
endif

define RULE_TEMPLATE
%-target.fmt: $(1)
    $$(call TARGET_FMT_COMMANDS)

endef

# define the default dependencies, notice the ?= assignment
TARGET_DEPS?=list dependencies here

# instantiate the template for the default case
$(eval $(call RULE_TEMPLATE,$(TARGET_DEPS)))

然后进入调用代码,只需在包含工具包之前定义 TARGET_FMT_COMMANDS 和 TARGET_DEPS 就可以了。

(请原谅定义/变量的名称,它们只是一个示例)

初步答案: 好吧,我会在工具包中写这个:

define TARGET_FMT_COMMANDS
    command1 # note this these commands should be prefixed with TAB character
    command2
endef

%-target.fmt:
    $(call TARGET_FMT_COMMANDS)

你可以简单地在include toolkit/Makefile之后重新定义TARGET_FMT_COMMANDS

诀窍是系统地在定义中的命令之前使用 TAB 字符,否则会出现奇怪的错误。

您也可以像往常一样为定义提供参数。

【讨论】:

  • 我想过。他的,但不幸的是,走这条路你放弃了 make 的正常功能。这不允许我重新定义先决条件,从而触发其他可能需要调用的目标。它可以欺骗配方使其具有不同的内容,但目标尚未被覆盖。
  • 哦,问题中没有提到先决条件。你对这些不走运,因为 GNU Make 在遇到规则定义时会立即扩展它们,所以如果你使用一些 $(DEPS) 然后稍后覆盖 DEPS,那么你会错过后者的 AFAICT。跨度>
  • 但是等等,可能会有适合您的解决方案。使用 $(eval) 宏扩展。我将编辑我的答案,因为它比评论更长,
【解决方案2】:

我遇到了同样的问题,我最终是如何解决覆盖目标不会覆盖先决条件的问题的,即覆盖先决条件的规则以及强制其中一些为空命令。

在工具包/Makefile中:

test: depend depend1 depend2
   @echo test toolkit
...

在 Makefile 中:

include toolkit/Makefile

depend1 depend2: ;

test: depend
   @echo test

注意depend1 和depend2 现在有空目标,因此测试目标的命令被覆盖,依赖关系也被有效覆盖。

【讨论】:

  • 我去年的测试表明这某些时间有效,但并不一致(请参阅我在问题中的评论)。关于 make 解析自身多次的方式它可能会或可能不会解析事物以覆盖您的目标,它最终可能会做相反的事情。触发这种情况的原因可能很简单,就像在不相关的目标中扩展其他变量或函数一样简单,所以虽然这个 hack 可能会让你通过几个固定的目标,但在任何复杂或动态的情况下(尤其是 .SECONDEPANSION :)它都会崩溃并做意想不到的事情发生在你身上。
  • 感谢您的提醒,很遗憾 Makefile 并不是真的打算以这种方式使用,因为这似乎是一个常见的用例。
  • 是的,make 解析器的处理方式实际上引入了一些疯狂的行为。例如,我发现如果我在 之前复制了我在包含之前和 之后完成的覆盖,它们有更好的机会被覆盖,因为这取决于它解析自身的传递次数(也许确定存在多少层变量扩展?)有时前一个有时后一个实际上会使覆盖发生。但以这种方式在生产中使用太果味了。
  • 无法从 makefile 中更改作业数量是另一个问题。您可以设置正确的变量并对其进行解析,然后重新解析 makefile,但到那时,即使设置了变量,每个目标在第一次传递时都被分配了一个作业队列。甚至不要让我开始在先决条件列表中进行二次扩展。
  • 是的,看起来只要你保持简单,Make 就会非常有用,尤其是不必键入/记住常用命令来安装依赖项和运行测试。你用什么语言制作,你考虑过alternatives吗?