Ошибка в GNU make: переменные, специфичные для цели, не раскрываются в неявных правилах? - PullRequest
13 голосов
/ 01 ноября 2010

Я работал над созданием Makefile с несколькими конфигурациями (тот, который поддерживает отдельные цели 'debug' и 'release'), и столкнулся со странной проблемой, которая кажется ошибкой в ​​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 $@

При указании цели 'debug' для CONFIG устанавливается значение 'debug', а 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

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

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

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

Ответы [ 3 ]

9 голосов
/ 01 ноября 2010

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

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

И" контекст рецепта цели "означает команды, а не предварительные требования:

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 $@ from $^
    @mkdir -p $(dir $@)                                                        
    $(CC) -c $< -o $@ 
7 голосов
/ 02 ноября 2010

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

2 голосов
/ 05 декабря 2014

Вот как можно решить проблему без 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

Теперь обратите внимание на преимущество: я могу создать цели как 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 [$$@]
        @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 и отфильтровать режимы сборки, которые там не указаны. Если вызывается 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 [$$@]
        @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
...