make 计算表达式的方式与命令行管理程序不同



>我正在尝试检查地址 A 处的文件副本是否比地址 B 处的原始文件旧。因此,例如,考虑一个名为scratch的文件,该文件与保存makefile的文件位于同一文件夹中。让这些文件的地址address_a/。现在让我们有另一个地址,比如我想安装scratchaddress_b/bin。但是,我打算在两种情况下执行安装:

  • address_b/bin不包含scratch
  • 或者每当address_b/bin中存在的scratch的副本比address_a/中存在的副本旧时。

为了做到这一点,我正在使用以下shell命令:

[ address_b/bin/scratch -ot address_a/scratch ] && echo 1 || echo 0

现在直接在终端上执行此命令将导致 1 作为输出。但是,通过makefile执行此命令会导致 0 作为输出。我使用此命令的配方如下所示:

.PHONY : install
INSTALL = install
source = scratch
destination = address_b/bin/
install : capture_status != [ $(destination) -ot $(source) ] && echo 1 || echo 0
install :
@echo $(capture_status)
ifeq ($(capture_status), 1)
$(INSTALL) $(source) $(destination)
else
@echo 'Installation has already been done.'
endif

那么,问题究竟在哪里?

注意:我在 Ubuntu 18.04.3 上使用 GNU Make 4.1

您是否知道make的主要作用是比较目标文件和先决条件文件的上次修改时间,以确定是否必须重新制作目标文件?在您的情况下,我认为以下内容以更自然的方式完全符合您的要求:

.PHONY : install
INSTALL = install
source = scratch
destination = address_b/bin/
install: $(destination)/$(source)
$(destination)/$(source): $(source)
$(INSTALL) $< $(dir $@)

总之,它说要做:

  1. install$(destination)/$(source)的别名,而不是真实的文件。

  2. $(destination)/$(source)取决于$(source),也就是说,如果$(destination)/$(source)早于$(source)则必须重新制作,否则它是最新的。

  3. 如果必须重新制作$(destination)/$(source)(因为它早于$(source)),要执行的 shell 命令是:

    $(INSTALL) $< $(dir $@)
    

$<是一个 make 自动变量,它作为规则的第一个先决条件进行扩展(在本例中$(source)),$@作为目标扩展 ($(destination)/$(source)),$(dir ...)是一个返回父目录的 make 函数。因此,该命令等效于:

$(INSTALL) $(source) $(destination)

但更通用。

编辑 1(安装未完成时添加自定义消息)

如果您希望在$(destination)/$(source)是最新的时发送特定消息或操作,则可以使用空文件将信息从一个规则传递到另一个规则(例如.done):

.PHONY : install
INSTALL = install
source = scratch
destination = address_b/bin/
DONE    := .done
MESSAGE := custom message
install: $(destination)/$(source)
@[ -f $(DONE) ] && rm $(DONE) || echo "$(MESSAGE)"
$(destination)/$(source): $(source)
$(INSTALL) $< $(dir $@)
@touch $(DONE)

编辑 2(添加对观察到的行为的解释)

您观察到的问题是由于何时使做什么。当 make 解析 Makefile 时,由 make 评估ifeq条件,而稍后扩展特定于目标的变量,当更改条件的条件为时已晚时......示范:

$ cat Makefile
.PHONY: c
c: foo := bar
c:
ifeq ($(foo),bar)
@echo "foo = bar and foo = $(foo)"
else
@echo "foo != bar and foo = $(foo)"
endif
$ make c
foo != bar and foo = bar

编辑 3(制作if条件和外壳条件)

下面的另外两个解决方案根本不是我最喜欢的,因为使用 shell 而不是 make 来测试一个文件是否比另一个文件旧会冒犯我的敏感性(而且我很敏感)。他们在这里只是为了提供信息。

GNU make 还有其他条件,比如$(if ...)函数,你可以按照你想要的方式使用它们,因为它们只有在 make 将配方传递给 shell 之前,在特定于目标的变量被扩展之后才会被扩展。

注意:在测试空性$(if ...)时,我修改了分配capture_status的方式,使其值为 1 或空字符串,而不是 1 或 0。

注意:我用$(destination)/$(source)替换了测试中的$(destination),因为您的原始版本不起作用,我认为:目录修改时间与文件修改时间不同。

.PHONY : install
INSTALL = install
source = scratch
destination = address_b/bin/
install : capture_status != [ $(destination)/$(source) -ot $(source) ] && echo 1
install :
@echo $(capture_status)
$(if $(capture_status),$(INSTALL) $(source) $(destination),@echo 'Installation has already been done.')

但是外壳条件的工作方式几乎相同:

.PHONY : install
INSTALL = install
source = scratch
destination = address_b/bin/
install : capture_status != [ $(destination)/$(source) -ot $(source) ] && echo 1 || echo 0
install :
@echo $(capture_status)
@if [ $(capture_status) -eq 1 ]; then 
echo "$(INSTALL) $(source) $(destination)"; 
$(INSTALL) $(source) $(destination); 
else 
echo 'Installation has already been done.'; 
fi

最新更新