Ошибка в 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