Ошибка в GNU make: целевые переменные не расширены в неявных правилах?

Я работаю над созданием 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 [[email protected]]
    @mkdir -p $(dir [email protected])
    $(CC) -c $< -o [email protected]

При указании цели "debug" make, CONFIG устанавливается на "debug", а BUILDDIR и TARGET также правильно расширяются. Однако в неявном правиле для создания исходного файла из объекта, [email protected]расширяется, как будто 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 расширяется отлично, но полученный [email protected]не является. Если я затем буду комментировать спецификацию целевой переменной и вручную установить 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

Я тестировал это как с make-3.81 на Gentoo, так и с MinGW, а make-3.82 на Gentoo. Все проявляют одинаковое поведение.

Мне трудно поверить, что я первый столкнулся с этой проблемой, поэтому я предполагаю, что, вероятно, я просто делаю что-то неправильно, но я буду честен: я не вижу, как я может быть.:)

Есть ли какие-нибудь гуру, которые могли бы пролить свет на эту проблему? Спасибо!

Ответы

Ответ 1

Как указывала 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 [[email protected]]
        @mkdir -p $(dir [email protected])
        $(CC) -c $< -o [email protected]

Это дает правильный результат:

$ 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) содержит список, разделенный пробелами), но иметь дело с этим не слишком много проблемы.

Ответ 2

В принципе, Make выполняет DAG зависимостей и создает список правил, которые должны быть выполнены до запуска любого правила. Назначение целевого значения - это то, что Make делает при запуске правила, которое приходит позже. Это серьезное ограничение (о котором я и другие ранее жаловались), но я бы не назвал его ошибкой, поскольку он описан в документации. Согласно руководству GNUMake:

6.11 Целевые значения переменных:" Как и с автоматическими переменными, эти значения доступны только в контексте целевого рецепта (и в другие целевые задания).

И "контекст целевого рецепта" означает команды, а не prereqs:

10.5.3 Автоматические переменные: "[Автоматические переменные] не могут быть доступны непосредственно в списке обязательных условий для правила".

Есть несколько способов обойти это. Вы можете использовать Secondary Expansion, если ваша версия 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 [email protected] from $^
    @mkdir -p $(dir [email protected])                                                        
    $(CC) -c $< -o [email protected] 

Ответ 3

Вот как решить проблему без интроспекции MAKECMDGOALS. Проблема в том, что правила, которые вы указываете в Makefile, составляют статический граф. Назначения для конкретных целей используются во время выполнения органов правил, но не во время их компиляции.

Решение этого - захватить контроль над компиляцией правил: используйте GNU Создайте макроподобные конструкции для генерации правил. Затем мы имеем полный контроль: мы можем вставлять переменный материал в цель, предпосылку или рецепт.

Вот моя версия вашего 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 [[email protected]]
        @mkdir -p $$(dir [email protected])
        $$(CC) -c -DMODE=$(1) $$< -o [email protected]
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

Теперь обратите внимание на преимущество: я могу создать как debug, так и release цели за один раз, потому что я создал оба правила из шаблона!

$ 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 [[email protected]]
        @mkdir -p $$(dir [email protected])
-       $$(CC) -c -DMODE=$(1) $$< -o [email protected]
+       $$(CC) -c $$(CFLAGS_$(1)) -DMODE=$(1) $$< -o [email protected]
 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

Наконец, мы можем объединить это с MAKECMDGOALS. Мы можем проверить MAKECMDGOALS и отфильтровать режимы сборки, которые там не указаны. Если вызывается make release, нам не нужны правила debug, которые нужно развернуть. 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 [[email protected]]
        @mkdir -p $$(dir [email protected])
        $$(CC) -c $$(CFLAGS_$(1)) -DMODE=$(1) $$< -o [email protected]
 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