GNU make: создание автоматических зависимостей с сгенерированными файлами заголовков

Итак, я последовал за документом Advanced Auto-Dependency Generation -

Makefile:

SRCS := main.c foo.c

main: main.o foo.o

%.o: %.c
    $(CC) -MMD -MG -MT '[email protected] $*.d' -c $< -o [email protected]
    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

В первый раз, когда я запускаю 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.

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

Ответ 1

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

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

main: main.o foo.o

%.o: %.c | generated_headers
    $(CC) -MMD -MG -MT '[email protected] $*.d' -c $< -o [email protected]
    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

Примечания:

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

  • Это решение достаточно масштабируемо: каждое правило генерации заголовка должно быть только обязательным условием для generated_headers .PHONY. Предполагая, что правило генерации заголовка написано правильно, как только оно было сгенерировано правильно, удовлетворяющее целевому объекту generated_headers должно быть no-op.

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

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

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

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

    make FAST_AND_LOOSE=1 main.o
    

Ответ 2

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

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

Итак, нам придется использовать... рекурсивный make! [ <Я > Дун Дун-dunnnn!]

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

Ответ 3

Файл make в исходном вопросе не работает для меня с 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 [email protected]

(Обратите внимание также, что 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 [email protected] $<

%.o: %.c
    $(CC) -c $< -o [email protected]

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

$ 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

Это связано с проблемой производительности, из-за которой исходный вопрос был пытаясь избежать? Это, по сути, решение "Основные автозависимости" описанный в Расширенная автозависимость Generation.

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

  • Мы запустим make, если что-то изменится.
  • Ужасное, но безобидное предупреждение: "main.d: Нет такого файла или каталога"
  • Неустранимая ошибка "no rule to make targe foo.h", если файл foo.h удален, даже если упоминание об этом удалено из файла .c.

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

Задача 3 решается GCC -MP (или эквивалентным sed script) - это также ортогонально методу, позволяющему избежать повторного выполнения make.

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

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

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

$ 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 будет по-прежнему необходимо повторно выполнить. Может быть, нет ничего, что можно было бы сделать, чтобы улучшить это; это, кажется, компромисс между правильностью и скоростью.

Все вышеперечисленное было протестировано с помощью make 3.81.

Ответ 4

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

main.o: foo.h

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