GNU make 中的错误:特定于目标的变量未在隐式规则中扩展?

发布于 2024-09-30 18:09:10 字数 1424 浏览 5 评论 0原文

我一直致力于设计一个多配置 Makefile(支持单独的“调试”和“发布”目标的文件),并且遇到了一个奇怪的问题,它似乎是 GNU make 中的一个错误。

当在隐式规则中引用这些变量时,GNU make 似乎没有正确扩展特定于目标的变量。下面是一个简化的 Makefile,它显示了这个问题:

all:
    @echo specify configuration 'debug' or 'release'

OBJS := foo.o bar.o

BUILDDIR = .build/$(CONFIG)

TARGET = $(addprefix $(BUILDDIR)/,$(OBJS))

debug: CONFIG := debug
release: CONFIG := release

#CONFIG := debug

debug: $(TARGET)
release: $(TARGET)

clean:
    rm -rf .build

$(BUILDDIR)/%.o: %.c
    @echo [$(BUILDDIR)/$*.o] should be [$@]
    @mkdir -p $(dir $@)
    $(CC) -c $< -o $@

当指定要制作的目标“调试”时,CONFIG 设置为“调试”,并且 BUILDDIR 和 TARGET 同样正确扩展。但是,在从对象构建源文件的隐式规则中,$@ 会展开,就好像 CONFIG 不存在一样。

以下是使用此 Makefile 的输出:

$ make debug
[.build/debug/foo.o] should be [.build//foo.o]
cc -c foo.c -o .build//foo.o
[.build/debug/bar.o] should be [.build//bar.o]
cc -c bar.c -o .build//bar.o

这表明 BUILDDIR 正在正常扩展,但生成的 $@ 却没有。如果我然后注释掉目标变量规范并手动设置 CONFIG := debug (上面的注释行),我会得到我所期望的:

$ make debug
[.build/debug/foo.o] should be [.build/debug/foo.o]
cc -c foo.c -o .build/debug/foo.o
[.build/debug/bar.o] should be [.build/debug/bar.o]
cc -c bar.c -o .build/debug/bar.o

我已经在 Gentoo 和 MinGW 上使用 make-3.81 进行了测试,并在 Gentoo 和 MinGW 上使用 make-3.82 进行了测试根图。所有人都表现出相同的行为。

我发现很难相信我会是第一个遇到这个问题的人,所以我猜我可能只是做错了什么——但我会说实话:我不知道我怎么会这样。 :)

是否有任何制作专家可以阐明这个问题?谢谢!

I have been working on designing a multiple configuration Makefile (one that supports separate 'debug' and 'release' targets), and have come across a strange problem which appears to be a bug in GNU make.

It seems that GNU make is not expanding target-specific variables properly when those variables are referenced in an implicit rule. Here is a simplified Makefile which shows this issue:

all:
    @echo specify configuration 'debug' or 'release'

OBJS := foo.o bar.o

BUILDDIR = .build/$(CONFIG)

TARGET = $(addprefix $(BUILDDIR)/,$(OBJS))

debug: CONFIG := debug
release: CONFIG := release

#CONFIG := debug

debug: $(TARGET)
release: $(TARGET)

clean:
    rm -rf .build

$(BUILDDIR)/%.o: %.c
    @echo [$(BUILDDIR)/$*.o] should be [$@]
    @mkdir -p $(dir $@)
    $(CC) -c 
lt; -o $@

When specifying the goal 'debug' to make, CONFIG is set to 'debug', and BUILDDIR and TARGET are likewise expanded properly. However, in the implicit rule to build the source file from the object, $@ is expanded as if CONFIG does not exist.

Here is the output from using this Makefile:

$ make debug
[.build/debug/foo.o] should be [.build//foo.o]
cc -c foo.c -o .build//foo.o
[.build/debug/bar.o] should be [.build//bar.o]
cc -c bar.c -o .build//bar.o

This shows that BUILDDIR is being expanded fine, but the resulting $@ is not. If I then comment out the target variable specification and manually set CONFIG := debug (the commented line above), I get what I would expect:

$ make debug
[.build/debug/foo.o] should be [.build/debug/foo.o]
cc -c foo.c -o .build/debug/foo.o
[.build/debug/bar.o] should be [.build/debug/bar.o]
cc -c bar.c -o .build/debug/bar.o

I've tested this with both make-3.81 on Gentoo and MinGW, and make-3.82 on Gentoo. All exhibit the same behavior.

I find it difficult to believe that I would be the first to come across this problem, so I'm guessing I'm probably just doing something wrong -- but I'll be honest: I don't see how I could be. :)

Are there any make gurus out there that might be able to shed some light on this issue? Thanks!

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

原来是傀儡 2024-10-07 18:09:10

基本上,Make 计算出依赖关系的 DAG,并创建一个必须在运行任何规则之前运行的规则列表。分配特定于目标的值是 Make 在运行规则时执行的操作(稍后进行)。这是一个严重的限制(我和其他人之前已经抱怨过),但我不会将其称为错误,因为它在文档中进行了描述。根据 GNUMake 手册:

6.11 特定于目标的变量值: “与自动变量一样,这些值仅在目标配方的上下文中可用(以及其他特定于目标的分配)。”

“目标配方的上下文”指的是命令,而不是先决条件:

10.5.3 自动变量:“无法在规则的先决条件列表中直接访问[自动变量]。”

有几种方法可以解决这个问题。您可以使用 辅助扩展,如果您的 Make 版本GNUMake 有它(3.81 没有,我不知道 3.82)。或者您可以不使用特定于目标的变量:

DEBUG_OBJS = $(addprefix $(BUILDDIR)/debug/,$(OBJS))
RELEASE_OBJS = $(addprefix $(BUILDDIR)/release/,$(OBJS))

debug: % : $(DEBUG_OBJS)
release: $(RELEASE_OBJS)

$(DEBUG_OBJS): $(BUILDDIR)/debug/%.o : %.cc
$(RELEASE_OBJS): $(BUILDDIR)/release/%.o : %.cc

$(DEBUG_OBJS) $(RELEASE_OBJS):
    @echo making $@ from $^
    @mkdir -p $(dir $@)                                                        
    $(CC) -c 
lt; -o $@ 

Basically, Make works out the DAG of dependencies and creates a list of rules that must be run before running any rule. Assigning a target-specific value is something that Make does when running a rule, which comes later. This is a serious limitation (which I and others have complained about before), but I wouldn't call it a bug since it is described in the documentation. According to the GNUMake manual:

6.11 Target-specific Variable Values: "As with automatic variables, these values are only available within the context of a target's recipe (and in other target-specific assignments)."

And "the context of a target's recipe" means the commands, not the prereqs:

10.5.3 Automatic variables: "[Automatic variables] cannot be accessed directly within the prerequisite list of a rule."

There are a couple of ways around this. You can use Secondary Expansion, if your version of Make GNUMake has it (3.81 doesn't, I don't know about 3.82). Or you can do without target-specific variables:

DEBUG_OBJS = $(addprefix $(BUILDDIR)/debug/,$(OBJS))
RELEASE_OBJS = $(addprefix $(BUILDDIR)/release/,$(OBJS))

debug: % : $(DEBUG_OBJS)
release: $(RELEASE_OBJS)

$(DEBUG_OBJS): $(BUILDDIR)/debug/%.o : %.cc
$(RELEASE_OBJS): $(BUILDDIR)/release/%.o : %.cc

$(DEBUG_OBJS) $(RELEASE_OBJS):
    @echo making $@ from $^
    @mkdir -p $(dir $@)                                                        
    $(CC) -c 
lt; -o $@ 
顾挽 2024-10-07 18:09:10

正如 Beta 所指出的,这确实不是 make 中的错误,因为文档中描述了限制(我想我一定错过了那个特定部分 - 抱歉)。

无论如何,我实际上可以通过做一些更简单的事情来解决这个问题。由于我需要的只是根据目标分配一个变量,所以我发现我可以使用 $(MAKECMDGOALS) 变量以便正确扩展构建目录。消除 $(CONFIG) 变量并按如下方式重写 Makefile 正是我所需要的:

all:
        @echo specify configuration 'debug' or 'release'

OBJS := foo.o bar.o

BUILDDIR := .build/$(MAKECMDGOALS)

TARGET := $(addprefix $(BUILDDIR)/,$(OBJS))

debug: $(TARGET)
release: $(TARGET)

clean:
        rm -rf .build

$(BUILDDIR)/%.o: %.c
        @echo [$(BUILDDIR)/$*.o] should be [$@]
        @mkdir -p $(dir $@)
        $(CC) -c 
lt; -o $@

然后给出正确的结果:

$ make debug
[.build/debug/foo.o] should be [.build/debug/foo.o]
cc -c foo.c -o .build/debug/foo.o
[.build/debug/bar.o] should be [.build/debug/bar.o]
cc -c bar.c -o .build/debug/bar.o
$ make release
[.build/release/foo.o] should be [.build/release/foo.o]
cc -c foo.c -o .build/release/foo.o
[.build/release/bar.o] should be [.build/release/bar.o]
cc -c bar.c -o .build/release/bar.o
$ make debug
make: Nothing to be done for `debug'.
$ make release
make: Nothing to be done for `release'.

如果在命令行上指定了多个目标,这当然会中断(因为 $(MAKECMDGOALS) 包含空格分隔的列表),但处理它并不是什么大问题。

As Beta has pointed out, this indeed isn't a bug in make since the limitation is described in the documentation (I guess I must have missed that particular part -- sorry).

In any case, I was actually able to work around this issue by doing something even simpler. Since all I need is to assign a variable based on the goal, I found that I can use the $(MAKECMDGOALS) variable in order to expand the build directory properly. Eliminating the $(CONFIG) variable and rewriting the Makefile as the following does exactly what I need:

all:
        @echo specify configuration 'debug' or 'release'

OBJS := foo.o bar.o

BUILDDIR := .build/$(MAKECMDGOALS)

TARGET := $(addprefix $(BUILDDIR)/,$(OBJS))

debug: $(TARGET)
release: $(TARGET)

clean:
        rm -rf .build

$(BUILDDIR)/%.o: %.c
        @echo [$(BUILDDIR)/$*.o] should be [$@]
        @mkdir -p $(dir $@)
        $(CC) -c 
lt; -o $@

This then gives the proper result:

$ make debug
[.build/debug/foo.o] should be [.build/debug/foo.o]
cc -c foo.c -o .build/debug/foo.o
[.build/debug/bar.o] should be [.build/debug/bar.o]
cc -c bar.c -o .build/debug/bar.o
$ make release
[.build/release/foo.o] should be [.build/release/foo.o]
cc -c foo.c -o .build/release/foo.o
[.build/release/bar.o] should be [.build/release/bar.o]
cc -c bar.c -o .build/release/bar.o
$ make debug
make: Nothing to be done for `debug'.
$ make release
make: Nothing to be done for `release'.

This will of course break if there are multiple goals specified on the command line (since $(MAKECMDGOALS) contains a space-separated list), but dealing with that isn't too much of a problem.

失眠症患者 2024-10-07 18:09:10

以下是如何在不进行 MAKECMDGOALS 自省的情况下解决问题。问题基本上是您在 Makefile 中指定的规则构成了静态图。特定于目标的分配在规则体的执行期间使用,但在其编译期间不使用。

解决这个问题的方法是控制规则编译:使用 GNU Make 的类似宏的结构来生成规则。然后我们就可以完全控制:我们可以将可变材料粘贴到目标、先决条件或配方中。

这是我的 Makefile 版本

all:
        @echo specify configuration 'debug' or 'release'

OBJS := foo.o bar.o

# BUILDDIR is a macro
# $(call BUILDDIR,WORD) -> .build/WORD
BUILDDIR = .build/$(1)

# target is a macro
# $(call TARGET,WORD) -> ./build/WORD/foo.o ./build/WORD/bar.o
TARGET = $(addprefix $(call BUILDDIR,$(1))/,$(OBJS))

# BUILDRULE is a macro: it builds a release or debug rule
# or whatever word we pass as argument $(1)
define BUILDRULE
$(call BUILDDIR,$(1))/%.o: %.c
        @echo [$(call BUILDDIR,$(1))/$*.o] should be [$@]
        @mkdir -p $(dir $@)
        $(CC) -c -DMODE=$(1) $< -o $@
endef

debug: $(call TARGET,debug)
release: $(call TARGET,release)

# generate two build rules from macro
$(eval $(call BUILDRULE,debug))
$(eval $(call BUILDRULE,release))

clean:
        rm -rf .build

现在,请注意优点:我可以一次性构建 debugrelease 目标,因为我已经实例化了这两条规则均来自模板!

$ make clean ; make debug release
rm -rf .build
[.build/debug/foo.o] should be [.build/debug/foo.o]
cc -c -DMODE=debug foo.c -o .build/debug/foo.o
[.build/debug/bar.o] should be [.build/debug/bar.o]
cc -c -DMODE=debug bar.c -o .build/debug/bar.o
[.build/release/foo.o] should be [.build/release/foo.o]
cc -c -DMODE=release foo.c -o .build/release/foo.o
[.build/release/bar.o] should be [.build/release/bar.o]
cc -c -DMODE=release bar.c -o .build/release/bar.o

此外,我还随意将宏参数添加到 cc 命令行中,以便模块接收一个 MODE 宏,该宏告诉它们如何编译它们。

我们可以使用变量间接来设置不同的CFLAGS或其他什么。如果我们像这样修补上面的内容,看看会发生什么:

--- a/Makefile
+++ b/Makefile
@@ -3,6 +3,9 @@

 OBJS := foo.o bar.o

+CFLAGS_debug = -O0 -g
+CFLAGS_release = -O2
+
 # BUILDDIR is a macro
 # $(call BUILDDIR,WORD) -> .build/WORD
 BUILDDIR = .build/$(1)
@@ -17,7 +20,7 @@ define BUILDRULE
 $(call BUILDDIR,$(1))/%.o: %.c
        @echo [$(call BUILDDIR,$(1))/$*.o] should be [$@]
        @mkdir -p $(dir $@)
-       $(CC) -c -DMODE=$(1) $< -o $@
+       $(CC) -c $(CFLAGS_$(1)) -DMODE=$(1) $< -o $@
 endef

 debug: $(call TARGET,debug)

运行:

$ make clean ; make debug release
rm -rf .build
[.build/debug/foo.o] should be [.build/debug/foo.o]
cc -c -O0 -g -DMODE=debug foo.c -o .build/debug/foo.o
[.build/debug/bar.o] should be [.build/debug/bar.o]
cc -c -O0 -g -DMODE=debug bar.c -o .build/debug/bar.o
[.build/release/foo.o] should be [.build/release/foo.o]
cc -c -O2 -DMODE=release foo.c -o .build/release/foo.o
[.build/release/bar.o] should be [.build/release/bar.o]
cc -c -O2 -DMODE=release bar.c -o .build/release/bar.o

最后,我们可以将其与 MAKECMDGOALS 结合起来。我们可以检查 MAKECMDGOALS 并过滤掉其中未指定的构建模式。如果调用了makerelease,我们就不需要扩展debug规则。补丁:

--- a/Makefile
+++ b/Makefile
@@ -3,6 +3,11 @@

 OBJS := foo.o bar.o

+# List of build types, but only those mentioned on command line
+BUILD_TYPES := $(filter $(MAKECMDGOALS),debug release)
+
+$(warning "generating rules for BUILD_TYPES := $(BUILD_TYPES)")
+
 CFLAGS_debug = -O0 -g
 CFLAGS_release = -O2

@@ -17,18 +22,15 @@ TARGET = $(addprefix $(call BUILDDIR,$(1))/,$(OBJS))
 # BUILDRULE is a macro: it builds a release or debug rule
 # or whatever word we pass as argument $(1)
 define BUILDRULE
+$(1): $(call TARGET,$(1))
 $(call BUILDDIR,$(1))/%.o: %.c
        @echo [$(call BUILDDIR,$(1))/$*.o] should be [$@]
        @mkdir -p $(dir $@)
        $(CC) -c $(CFLAGS_$(1)) -DMODE=$(1) $< -o $@
 endef

-debug: $(call TARGET,debug)
-release: $(call TARGET,release)
-
-# generate two build rules from macro
-$(eval $(call BUILDRULE,debug))
-$(eval $(call BUILDRULE,release))
+$(foreach type,$(BUILD_TYPES),\
+  $(eval $(call BUILDRULE,$(type))))

 clean:
        rm -rf .build

请注意,我通过将 debug:release: 目标滚动到 BUILDRULE 宏中来简化事情。

$ make clean ; make release
Makefile:9: "generating rules for BUILD_TYPES := "
rm -rf .build
Makefile:9: "generating rules for BUILD_TYPES := release"
[.build/release/foo.o] should be [.build/release/foo.o]
cc -c -O2 -DMODE=release foo.c -o .build/release/foo.o
[.build/release/bar.o] should be [.build/release/bar.o]
cc -c -O2 -DMODE=release bar.c -o .build/release/bar.o

$ make clean ; make release debug
Makefile:9: "generating rules for BUILD_TYPES := "
rm -rf .build
Makefile:9: "generating rules for BUILD_TYPES := debug release"
[.build/release/foo.o] should be [.build/release/foo.o]
cc -c -O2 -DMODE=release foo.c -o .build/release/foo.o
[.build/release/bar.o] should be [.build/release/bar.o]
cc -c -O2 -DMODE=release bar.c -o .build/release/bar.o
[.build/debug/foo.o] should be [.build/debug/foo.o]
cc -c -O0 -g -DMODE=debug foo.c -o .build/debug/foo.o
[.build/debug/bar.o] should be [.build/debug/bar.o]
cc -c -O0 -g -DMODE=debug bar.c -o .build/debug/bar.o

Here is how to solve the problem without MAKECMDGOALS introspection. Tha problem is basically that the rules you specify in Makefile constitute a static graph. The target-specific assignments are used during the execution of rule bodies, but not during their compilation.

The solution to this is to grab control over rule compilation: use GNU Make's macro-like constructs to generate the rules. Then we have full control: we can stick variable material into the target, prerequisite or recipe.

Here is my version of your Makefile

all:
        @echo specify configuration 'debug' or 'release'

OBJS := foo.o bar.o

# BUILDDIR is a macro
# $(call BUILDDIR,WORD) -> .build/WORD
BUILDDIR = .build/$(1)

# target is a macro
# $(call TARGET,WORD) -> ./build/WORD/foo.o ./build/WORD/bar.o
TARGET = $(addprefix $(call BUILDDIR,$(1))/,$(OBJS))

# BUILDRULE is a macro: it builds a release or debug rule
# or whatever word we pass as argument $(1)
define BUILDRULE
$(call BUILDDIR,$(1))/%.o: %.c
        @echo [$(call BUILDDIR,$(1))/$*.o] should be [$@]
        @mkdir -p $(dir $@)
        $(CC) -c -DMODE=$(1) $< -o $@
endef

debug: $(call TARGET,debug)
release: $(call TARGET,release)

# generate two build rules from macro
$(eval $(call BUILDRULE,debug))
$(eval $(call BUILDRULE,release))

clean:
        rm -rf .build

Now, notice the advantage: I can build both debug and release targets in one go, because I have instantiated both rules from the template!

$ make clean ; make debug release
rm -rf .build
[.build/debug/foo.o] should be [.build/debug/foo.o]
cc -c -DMODE=debug foo.c -o .build/debug/foo.o
[.build/debug/bar.o] should be [.build/debug/bar.o]
cc -c -DMODE=debug bar.c -o .build/debug/bar.o
[.build/release/foo.o] should be [.build/release/foo.o]
cc -c -DMODE=release foo.c -o .build/release/foo.o
[.build/release/bar.o] should be [.build/release/bar.o]
cc -c -DMODE=release bar.c -o .build/release/bar.o

Furthermore, I have taken liberty to add the macro argument into the cc command line also, so that the modules receive a MODE macro which tells them how they are being compiled.

We can use variable indirection to set up different CFLAGS or whatever. Watch what happens if we patch the above like this:

--- a/Makefile
+++ b/Makefile
@@ -3,6 +3,9 @@

 OBJS := foo.o bar.o

+CFLAGS_debug = -O0 -g
+CFLAGS_release = -O2
+
 # BUILDDIR is a macro
 # $(call BUILDDIR,WORD) -> .build/WORD
 BUILDDIR = .build/$(1)
@@ -17,7 +20,7 @@ define BUILDRULE
 $(call BUILDDIR,$(1))/%.o: %.c
        @echo [$(call BUILDDIR,$(1))/$*.o] should be [$@]
        @mkdir -p $(dir $@)
-       $(CC) -c -DMODE=$(1) $< -o $@
+       $(CC) -c $(CFLAGS_$(1)) -DMODE=$(1) $< -o $@
 endef

 debug: $(call TARGET,debug)

Run:

$ make clean ; make debug release
rm -rf .build
[.build/debug/foo.o] should be [.build/debug/foo.o]
cc -c -O0 -g -DMODE=debug foo.c -o .build/debug/foo.o
[.build/debug/bar.o] should be [.build/debug/bar.o]
cc -c -O0 -g -DMODE=debug bar.c -o .build/debug/bar.o
[.build/release/foo.o] should be [.build/release/foo.o]
cc -c -O2 -DMODE=release foo.c -o .build/release/foo.o
[.build/release/bar.o] should be [.build/release/bar.o]
cc -c -O2 -DMODE=release bar.c -o .build/release/bar.o

Finally, we can combine that with MAKECMDGOALS. We can inspect MAKECMDGOALS and filter out the build modes which are not specified there. If make release is called, we don't need the debug rules to be expanded. Patch:

--- a/Makefile
+++ b/Makefile
@@ -3,6 +3,11 @@

 OBJS := foo.o bar.o

+# List of build types, but only those mentioned on command line
+BUILD_TYPES := $(filter $(MAKECMDGOALS),debug release)
+
+$(warning "generating rules for BUILD_TYPES := $(BUILD_TYPES)")
+
 CFLAGS_debug = -O0 -g
 CFLAGS_release = -O2

@@ -17,18 +22,15 @@ TARGET = $(addprefix $(call BUILDDIR,$(1))/,$(OBJS))
 # BUILDRULE is a macro: it builds a release or debug rule
 # or whatever word we pass as argument $(1)
 define BUILDRULE
+$(1): $(call TARGET,$(1))
 $(call BUILDDIR,$(1))/%.o: %.c
        @echo [$(call BUILDDIR,$(1))/$*.o] should be [$@]
        @mkdir -p $(dir $@)
        $(CC) -c $(CFLAGS_$(1)) -DMODE=$(1) $< -o $@
 endef

-debug: $(call TARGET,debug)
-release: $(call TARGET,release)
-
-# generate two build rules from macro
-$(eval $(call BUILDRULE,debug))
-$(eval $(call BUILDRULE,release))
+$(foreach type,$(BUILD_TYPES),\
+  $(eval $(call BUILDRULE,$(type))))

 clean:
        rm -rf .build

Note that I simplified things by rolling the debug: and release: targets into the BUILDRULE macro.

$ make clean ; make release
Makefile:9: "generating rules for BUILD_TYPES := "
rm -rf .build
Makefile:9: "generating rules for BUILD_TYPES := release"
[.build/release/foo.o] should be [.build/release/foo.o]
cc -c -O2 -DMODE=release foo.c -o .build/release/foo.o
[.build/release/bar.o] should be [.build/release/bar.o]
cc -c -O2 -DMODE=release bar.c -o .build/release/bar.o

$ make clean ; make release debug
Makefile:9: "generating rules for BUILD_TYPES := "
rm -rf .build
Makefile:9: "generating rules for BUILD_TYPES := debug release"
[.build/release/foo.o] should be [.build/release/foo.o]
cc -c -O2 -DMODE=release foo.c -o .build/release/foo.o
[.build/release/bar.o] should be [.build/release/bar.o]
cc -c -O2 -DMODE=release bar.c -o .build/release/bar.o
[.build/debug/foo.o] should be [.build/debug/foo.o]
cc -c -O0 -g -DMODE=debug foo.c -o .build/debug/foo.o
[.build/debug/bar.o] should be [.build/debug/bar.o]
cc -c -O0 -g -DMODE=debug bar.c -o .build/debug/bar.o
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文