Is it expected behavior for Make to not evaluate a target's timestamp on the first invocation when the rule has no recipe?

huangapple go评论62阅读模式
英文:

Is it expected behavior for Make to not evaluate a target's timestamp on the first invocation when the rule has no recipe?

问题

关于您提到的Makefile行为,第一个示例中的行为是Make工具的预期行为,而不是错误。Make工具使用时间戳(或者更准确地说是文件的最后修改时间)来确定目标是否需要重新构建。在第一次运行DB_TYPE=tidb make时,db_type文件的时间戳与run-only-if-env-var-updated的时间戳相同,因此Make不会重新构建run-only-if-env-var-updated,因为它认为它是最新的。

当您添加了额外的echo命令到db_type目标后,db_type目标实际上具有了一个非空的配方,这导致了其最后修改时间被更新,因此在第一次运行DB_TYPE=tidb make时,db_type的时间戳更新,从而使run-only-if-env-var-updated的时间戳旧于db_type的时间戳,触发了run-only-if-env-var-updated的重新构建。

这是Make工具的一种工作方式,根据目标和其依赖项的时间戳来判断是否需要重新构建。如果您希望确保run-only-if-env-var-updated在第一次运行DB_TYPE=tidb make时也运行,可以像您所示的那样将一个非空的命令添加到db_type目标中,以确保其时间戳被更新。

至于为什么Make的这种行为是预期行为,Make工具的设计原则和行为在GNU Make的官方文档中有详细描述,您可以在GNU Make手册的相关部分中找到更多信息。 Make的行为是基于时间戳和目标依赖关系的标准构建系统的工作方式。

英文:

The Makefile documentation on force targets states the following:

> If a rule has no prerequisites or recipe, and the target of the rule
> is a nonexistent file, then make imagines this target to have been
> updated whenever its rule is run. This implies that all targets
> depending on this one will always have their recipe run.

My interpretation of the above claim is that the target of the rule must be a non-existent file for a target to be a Force target. Thus, I do not think the force target is what is happening in the example below, but I'm happy to learn of evidence to the contrary.

Consider the following Makefile:

.PHONY: update-db-type
.DEFAULT: run-only-if-env-var-updated

ifeq ("$(shell echo $$DB_TYPE)","mysql")
DB_TYPE := mysql
else
DB_TYPE := tidb
endif

run-only-if-env-var-updated: db_type
	echo "Do an expensive operation"
	sleep 1
	touch $@

# Note that db_type is a real file.
db_type: update-db-type

update-db-type:
	echo "DB_TYPE: $(DB_TYPE)"
ifneq ("$(shell cat db_type)","$(DB_TYPE)")
	echo "$(DB_TYPE)" > db_type
endif

Now consider the following commands and their output:

echo mysql > db_type
DB_TYPE=mysql make
# Output
# echo "DB_TYPE: mysql"
# DB_TYPE: mysql
# echo "Do an expensive operation"
# Do an expensive operation
# sleep 1
# touch run-only-if-env-var-updated

DB_TYPE=tidb make
# Output
# echo "DB_TYPE: tidb"
# DB_TYPE: tidb
# echo "tidb" > db_type

DB_TYPE=tidb make
# echo "DB_TYPE: tidb"
# DB_TYPE: tidb
# echo "Do an expensive operation"
# Do an expensive operation
# sleep 1
# touch run-only-if-env-var-updated

Observe that the expensive operation run-only-if-env-var-updated is only run on the second invocation of DB_TYPE=tidb make.

This implies that, in the first invocation, Make did not observe that the timestamp of the file db_type was updated, and newer than the file run-only-if-env-var-updated.

Note that this is NOT the behavior I was expecting; my mental model was that Make would always compare the timestamp of the dependency to timestamp of the target to determine whether the target needed to run.


Now, if we make a small modification to the Makefile, by giving the target db_type a non-empty recipe, as follows, then we get the expected behavior of running the expensive operation on the first invocation of DB_TYPE=tidb make.

db_type: update-db-type
	echo "Running DB_TYPE"

Is the behavior in the first example (without the extra echo) expected behavior for Make?
If so, is there documentation that explains why the updated timestamp is being ignored on the first execution?

答案1

得分: 2

GNU make 从不考虑除了它所调用的目标之外的其他目标是否更新了目标文件。因此,如果它发现给定目标没有相关的构建规则,那么它会假定该目标没有被更新。

只有当有一个给定目标的构建规则时,make 才会重新检查目标的修改时间,以查看它是否已更改。构建规则实际上不必执行任何操作,只要它存在即可。例如,这就足够了:

db_type: update-db-type ;

这里的分号添加了一个空的构建规则。

在我看来,你的 makefile 相当令人困惑,我肯定不会这样编写它。首先,为什么要使用 $(shell echo $$DB_TYPE)?为什么不直接使用 $(DB_TYPE)?其次,在构建规则中使用 ifeq 等语句几乎总是错误的(至少非常令人困惑)。ifeq 等语句类似于预处理器语句,它们在解析 makefile 时会直接展开,而不是在执行构建规则时展开。

在我看来,下面的 makefile 可以实现你想要的功能,而且更加安全:

.PHONY: update-db-type
.DEFAULT: run-only-if-env-var-updated

ifneq ($(DB_TYPE),mysql)
DB_TYPE := tidb
endif

run-only-if-env-var-updated: db_type
        echo "Do an expensive operation"
        sleep 1
        touch $@

# 请注意,db_type 是一个真正的文件。
db_type: update-db-type ;

update-db-type:
        echo "DB_TYPE: $(DB_TYPE)"
        [ "$$(cat db_type 2>/dev/null)" = '$(DB_TYPE)' ] \
            || echo "$(DB_TYPE)" > db_type

不过,更加直接和易于理解的方法是将构建规则放在你希望更新的目标中,而不是放在其他目标中,并强制始终调用该构建规则。因此,可以像这样:

db_type: FORCE
        echo "DB_TYPE: $(DB_TYPE)"
        [ "$$(cat $@ 2>/dev/null)" = '$(DB_TYPE)' ] \
            || echo "$(DB_TYPE)" > $@

FORCE:

这就是更简单、更容易理解的方法。

英文:

GNU make it doesn't ever consider that some other target, other than the one it invoked, updates the target file. So, if it sees that there is no recipe for a given target then it assumes that the target cannot have been updated.

Only if there is a recipe for a given target, will make re-check the modification time on the target to see if it has changed or not. The recipe doesn't have to actually do anything, it just has to exist. For example, this is sufficient:

db_type: update-db-type ;

The semicolon here adds an empty recipe to this rule.

IMO your makefile is pretty confusing and I would definitely not write it like this. First, why do you use $(shell echo $$DB_TYPE)? Why not just use $(DB_TYPE)? Second, it's virtually always wrong (and at least very confusing) to use ifeq etc. inside a recipe. ifeq etc. are preprocessor-like statements and they are expanded directly when the makefile is parsed, not when the recipe is invoked.

IMO this makefile does what you want and is safer:

.PHONY: update-db-type
.DEFAULT: run-only-if-env-var-updated

ifneq ($(DB_TYPE),mysql)
DB_TYPE := tidb
endif

run-only-if-env-var-updated: db_type
        echo "Do an expensive operation"
        sleep 1
        touch $@

# Note that db_type is a real file.
db_type: update-db-type ;

update-db-type:
        echo "DB_TYPE: $(DB_TYPE)"
        [ "$$(cat db_type 2>/dev/null)" = '$(DB_TYPE)' ] \
            || echo "$(DB_TYPE)" > db_type

But, a more straightforward method that is IMO more understandable is to put the recipe in the target you want to be updated, rather than some other target, and force that recipe to always be invoked. So it would be this instead:

db_type: FORCE
        echo "DB_TYPE: $(DB_TYPE)"
        [ "$$(cat $@ 2>/dev/null)" = '$(DB_TYPE)' ] \
            || echo "$(DB_TYPE)" > $@

FORCE:

huangapple
  • 本文由 发表于 2023年3月1日 16:10:08
  • 转载请务必保留本文链接:https://go.coder-hub.com/75601001.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定