GNU make: генерация автоматических зависимостей с помощью сгенерированных заголовочных файлов

Таким образом, я следовал документу Advanced Auto-Dependency Generation -

Makefile:

SRCS := main.c foo.c

main: main.o foo.o

%.o: %.c
    $(CC) -MMD -MG -MT '$@ $*.d' -c $< -o $@
    cp $*.d $*.tmp
    sed -e 's;#.*;;' -e 's;^[^:]*: *;;' -e 's; *\\$$;;' \
        -e '/^$$/d' -e 's;$$; :;' < $*.tmp >> $*.d
    rm $*.tmp

clean::
    -rm *.o *.d main

-include $(SRCS:.c=.d)

main.c:

#include "foo.h"

int main(int argc, char** argv) {
  foo() ;
  return 0 ;
}

foo.h:

#ifndef __FOO_H__
#define __FOO_H__

void foo() ;

#endif

- и это работает как шарм.


Но когда foo.h становится сгенерированным файлом -

Makefile:

...

HDRS := foo.h

$(HDRS):
    mk_header.sh $*

clean::
    -rm $(HDRS)
...

mk_header.sh:

#!/bin/bash
UP=$(tr "[:lower:]" "[:upper:]" <<< $1)

cat <<EOF > $1.h
#ifndef __${UP}_H__
#define __${UP}_H__

void $1() ;

#endif
EOF

1-й раз я бегу make, main.d еще не генерируется, и, таким образом, foo.h не считается обязательным условием и, следовательно, не генерируется:

$ ls
foo.c  main.c  Makefile  mk_header.sh*

$ make
cc -MMD -MG -MT 'main.o main.d' -c main.c -o main.o
cp main.d main.tmp
sed -e 's;#.*;;' -e 's;^[^:]*: *;;' -e 's; *\\$;;' \
    -e '/^$/d' -e 's;$; :;' < main.tmp >> main.d
rm main.tmp
cc -MMD -MG -MT 'foo.o foo.d' -c foo.c -o foo.o
cp foo.d foo.tmp
sed -e 's;#.*;;' -e 's;^[^:]*: *;;' -e 's; *\\$;;' \
    -e '/^$/d' -e 's;$; :;' < foo.tmp >> foo.d
rm foo.tmp
cc   main.o foo.o   -o main

$ ls
foo.c  foo.d  foo.o  
main*  main.c  main.d  main.o  
Makefile  mk_header.sh*

Только во 2-м вызове make, foo.h генерируется, и в результате строятся другие каскады.

$ make
./mk_header.sh foo
cc -MMD -MG -MT 'main.o main.d' -c main.c -o main.o
cp main.d main.tmp
sed -e 's;#.*;;' -e 's;^[^:]*: *;;' -e 's; *\\$;;' \
    -e '/^$/d' -e 's;$; :;' < main.tmp >> main.d
rm main.tmp
cc   main.o foo.o   -o main

$ ls
foo.c  foo.d  foo.h  foo.o  
main*  main.c  main.d  main.o  
Makefile  mk_header.sh*

И только после этого make понимает, что:

$ make
make: `main' is up to date.

Таким образом, мой вопрос: существует ли способ расширить рецепт, предложенный вышеупомянутым документом, чтобы обеспечить возможность создания сгенерированных заголовочных файлов, без устранения увеличения производительности, возникающего из-за того, что нет необходимости переоценивать все дерево сборки при включении *.d фрагменты?

4 ответа

Решение

Проблема в том, что *.d Генерация Makefile-фрагментов должна быть выполнена после того, как завершена генерация всех заголовков. Говоря так, можно использовать зависимости make для наведения правильного порядка:

SRCS := main.c foo.c
HDRS := foo.h

main: main.o foo.o

%.o: %.c | generated_headers
    $(CC) -MMD -MG -MT '$@ $*.d' -c $< -o $@
    cp $*.d $*.tmp
    sed -e 's;#.*;;' -e 's;^[^:]*: *;;' -e 's; *\\$$;;' \
        -e '/^$$/d' -e 's;$$; :;' < $*.tmp >> $*.d
    rm $*.tmp

-include $(SRCS:.c=.d)

$(HDRS):
    mk_header.sh $*

generated_headers: $(HDRS)

clean:
    -rm $(HDRS) *.o *.d main

.PHONY: clean generated_headers

Заметки:

  1. Я использую зависимость только от заказа.

  2. Это решение достаточно масштабируемо: каждое правило генерирования заголовка должно быть лишь обязательным условием generated_headers.PHONY цель. Предполагая, что правило генерации заголовка написано правильно, как только оно было сгенерировано правильно, удовлетворяя generated_headers цель должна быть неоперативной.

  3. Невозможно скомпилировать отдельный объект, даже если этот объект не требует каких-либо сгенерированных заголовков, без предварительной генерации всех сгенерированных заголовков проекта. Хотя это технически обоснованно, ваши разработчики будут жаловаться.

    Так что вы должны подумать о FAST_AND_LOOSE флаг, который отключит эту функцию:

    %.o: %.c | $(if $(FAST_AND_LOOSE),,generated_headers)
        ...
    

    Таким образом, разработчик может выпустить:

    make FAST_AND_LOOSE=1 main.o
    

Makefile в исходном вопросе не работает для меня с gcc 4.8.2:

cc -MMD -MG -MT main.d -c main.c -o main.o
cc1: error: -MG may only be used with -M or -MM

Я думаю, что gcc изменил поведение -MG в какой-то момент в последние 4 года.

Похоже, что если вы хотите поддерживать сгенерированные файлы заголовков, больше не существует способа генерировать файл ".d" и файл ".o" одновременно, не вызывая препроцессор C дважды.

Поэтому я обновил рецепт:

%.o: %.c
    $(CC) -MM -MG -MP -MT $*.o -MF $*.d $<
    $(CC) -c $< -o $@

(Обратите внимание, что теперь gcc имеет -MP генерировать фальшивые цели для каждого заголовка, поэтому вам больше не нужно запускать sed для вывода gcc.)

У нас все еще та же проблема, что и в первоначальном вопросе - бег make первый раз не удается сгенерировать foo.h:

$ make
cc -MM -MG -MP -MT main.o -MF main.d main.c
cc -c main.c -o main.o
main.c:1:17: fatal error: foo.h: No such file or directory
 #include "foo.h"
                 ^
compilation terminated.
Makefile:7: recipe for target 'main.o' failed
make: *** [main.o] Error 1

Запуск снова работает:

$ make
./mk_header.sh foo
cc -MM -MG -MP -MT main.o -MF main.d main.c
cc -c main.c -o main.o
cc   main.o   -o main

Так как мы все равно должны дважды запустить препроцессор C, давайте сгенерируем .dфайл в отдельном правиле:

%.d: %.c
    $(CC) -MM -MG -MP -MT $*.o -MF $@ $<

%.o: %.c
    $(CC) -c $< -o $@

Теперь он правильно генерирует заголовочный файл:

$ make clean
rm -f *.o *.d main foo.h
$ make
cc -MM -MG -MP -MT main.o -MF main.d main.c
./mk_header.sh foo
cc -c main.c -o main.o
cc   main.o   -o main

Не страдает ли это от проблемы с производительностью, которой пытался избежать исходный вопрос? По сути, это решение "Базовые автозависимости", описанное в документе " Advanced Auto-Dependency Generation".

Эта статья утверждает 3 проблемы с этим решением:

  1. Мы повторно исполняем make если что-то изменится.
  2. Уродливое, но безобидное предупреждение: "main.d: такого файла или каталога нет"
  3. Неустранимая ошибка "нет правила для создания цели foo.h", если файл foo.h удален, даже если упоминание об этом удалено из файла.c.

Проблема 2 решается с помощью -include вместо include, Насколько я могу судить, это ортогонально методике статьи, позволяющей избежать повторного выполненияmake, По крайней мере, я не смог вызвать какие-либо проблемы с помощью -includeвместо include,

Проблема 3 решена GCC -MP (или эквивалентный сценарий sed) - это также ортогонально технике, позволяющей избежать повторного выполнения make,

Проблема 1 может быть несколько улучшена чем-то вроде этого:

%.d: %.c
    $(CC) -MM -MG -MP -MT $*.o -MF $@.new $<
    cmp $@.new $@ 2>/dev/null || mv $@.new $@; rm -f $@.new

До этого изменения:

$ make clean
rm -f *.o *.d main foo.h
$ make -d 2>&1 | grep Re-executing
Re-executing[1]: make -d
$ make -d 2>&1 | grep Re-executing
$ touch main.c; make -d 2>&1 | grep Re-executing
Re-executing[1]: make -d

После этого изменения:

$ make clean
rm -f *.o *.d main foo.h
$ make -d 2>&1 | grep Re-executing
Re-executing[1]: make -d
$ make -d 2>&1 | grep Re-executing
$ touch main.c; make -d 2>&1 | grep Re-executing

Немного лучше. Конечно, если новая зависимость вводится, make все равно нужно будет заново выполнить. Возможно, ничего нельзя сделать, чтобы улучшить это; похоже, это компромисс между правильностью и скоростью.

Все вышеперечисленное было протестировано с маркой 3.81.

Краткий ответ: нет. Рецепт, описанный в статье, очень умный, один из моих любимых, но это сложное использование грубого инструмента. Он использует обычную схему, в которой существуют все необходимые заголовки; он пытается решить проблему определения того, какие заголовки, если они были недавно изменены, требуют перестройки данного объектного файла. В частности, если объектный файл не существует, он должен быть восстановлен - и в этом случае нет причин беспокоиться о заголовочных файлах, потому что компилятор обязательно их найдет.

Теперь заголовочные файлы генерируются. Так foo.h может не существовать, поэтому кто-то должен будет запустить скрипт для его генерации, и только Make может сделать это. Но Маке не может этого знать foo.h необходимо без проведения некоторого анализа main.c, Но это действительно не может произойти, пока Make не начнет выполняться main связанные правила (например, main.o или же main.o.d), которую он не может выполнить, пока не решит, какие цели он собирается построить.

Так что нам придется использовать... рекурсивный make! [ Дун-Дун-Даннн! ]

Мы не можем достичь цели статьи избежать повторного вызова Make, но мы, по крайней мере, можем избежать (некоторых) ненужных перестроений. Вы можете сделать что-то вроде "Базовых автозависимостей", описанных в статье; В документе описываются проблемы такого подхода. Или вы можете использовать команду, подобную той, что приведена в "Расширенном" рецепте, для генерации списка заголовков, а затем передать ее $(MAKE); этот подход аккуратен, но может вызывать Make много раз в одном и том же заголовке, в зависимости от того, как выглядит ваше кодовое дерево.

Вы можете создать явное правило зависимости для вашего сгенерированного заголовка:

main.o: foo.h

Если сгенерированный заголовок непосредственно включен в небольшое количество файлов, это может быть осуществимым подходом.

Другие вопросы по тегам