Правило GNU Makefile, генерирующее несколько целей из одного исходного файла

106

Я пытаюсь сделать следующее. Есть программа, назовите ее foo-bin, которая принимает один входной файл и генерирует два выходных файла. Глупое правило Makefile для этого будет:

file-a.out file-b.out: input.in
    foo-bin input.in file-a.out file-b.out

Однако это никоим образом не означает, makeчто обе цели будут созданы одновременно. Это нормально при makeпоследовательном запуске , но, скорее всего, вызовет проблемы, если кто-то попытается make -j16или что-то столь же безумное.

Вопрос в том, существует ли способ написать правильное правило Makefile для такого случая? Ясно, что это сгенерирует DAG, но почему-то в руководстве GNU make не указано, как этот случай может быть обработан.

Выполнить один и тот же код дважды и получить только один результат не может быть и речи, потому что вычисление требует времени (подумайте: часы). Вывод только одного файла также был бы довольно трудным, потому что часто он используется как вход для GNUPLOT, который не знает, как обрабатывать только часть файла данных.

маказавр
источник
1
У Automake есть документация по этому поводу
Фрэнк,

Ответы:

12

Я бы решил это следующим образом:

file-a.out: input.in
    foo-bin input.in file-a.out file-b.out   

file-b.out: file-a.out
    #do nothing
    noop

В этом случае параллельная программа make будет «сериализовать», создавая a и b, но поскольку создание b ничего не делает, это не требует времени.

Питер Тиллеманс
источник
16
Собственно, с этим есть проблема. Если file-b.outбыл создан, как указано, а затем загадочно удален, makeего невозможно будет воссоздать, потому что file-a.outон все еще будет присутствовать. Есть предположения?
Makeaurus
Если вы загадочно удалили, file-a.outто вышеуказанное решение работает хорошо. Это предполагает взлом: когда существует только один из a или b, тогда зависимости должны быть упорядочены так, чтобы отсутствующий файл отображался как результат input.in. Несколько $(widcard...)s, $(filter...)s, $(filter-out...)s и т. Д. Должны помочь. Тьфу.
bobbogo
3
PS foo-bin, очевидно, сначала создаст один из файлов (с использованием разрешения в микросекундах), поэтому вы должны убедиться, что у вас есть правильный порядок зависимостей, когда существуют оба файла.
bobbogo
2
@bobbogo либо это, либо добавить touch file-a.outв качестве второй команды после foo-binвызова.
Коннор Харрис,
Вот возможное исправление этого: добавьте touch file-b.outв качестве второй команды в первое правило и продублируйте команды во втором правиле. Если какой-либо файл отсутствует или старше input.in, команда будет выполнена только один раз и восстановит оба файла с file-b.outболее поздней версией, чем file-a.out.
chqrlie
128

Уловка состоит в том, чтобы использовать шаблонное правило с несколькими целями. В этом случае make предположит, что обе цели созданы одним вызовом команды.

все: файл-a.out файл-b.out
файл-a% out файл-b% out: input.in
    foo-bin input.in файл-a $ * out файл-b $ * out

Эта разница в интерпретации между шаблонными и обычными правилами не совсем понятна, но она полезна для подобных случаев и задокументирована в руководстве.

Этот прием можно использовать для любого количества выходных файлов, если в их именах есть какая-то общая подстрока для %сопоставления. (В этом случае общая подстрока - ".")

тупица
источник
3
На самом деле это не так. В вашем правиле указано, что файлы, соответствующие этим шаблонам, должны быть созданы из input.in с использованием указанной команды, но нигде не сказано, что они создаются одновременно. Если вы действительно запустите его параллельно, makeодна и та же команда будет запущена дважды одновременно.
madeaurus
47
Пожалуйста, попробуйте это, прежде чем вы скажете, что это не работает ;-) В руководстве GNU make сказано: «Шаблонные правила могут иметь более одной цели. В отличие от обычных правил, это не действует как множество разных правил с одинаковыми предпосылками и командами. Если шаблонное правило имеет несколько целей, make знает, что команды правила отвечают за создание всех целей. Команды выполняются только один раз, чтобы создать все цели ». gnu.org/software/make/manual/make.html#Pattern-Intro
slowdog
4
Но что, если у меня совершенно разные входы? Скажем, foo.in и bar.src? Поддерживают ли правила шаблона соответствие пустому шаблону?
grwlf
2
@dcow: выходные файлы должны разделять только подстроку (достаточно даже одного символа, такого как "."), не обязательно префикса. Если нет общей подстроки, вы можете использовать промежуточную цель, как описано richard и deemer. @grwlf: Эй, это значит, что "foo.in" и "bar.src" можно сделать: foo%in bar%src: input.in:-)
slowdog
1
Если соответствующие выходные файлы хранятся в переменных, рецепт можно записать как: $(subst .,%,$(varA) $(varB)): input.in(проверено с GNU make 4.1).
stefanct
55

У Make нет интуитивно понятного способа сделать это, но есть два достойных обходных пути.

Во-первых, если задействованные цели имеют общую основу, вы можете использовать правило префикса (с GNU make). То есть, если вы хотели исправить следующее правило:

object.out1 object.out2: object.input
    foo-bin object.input object.out1 object.out2

Вы могли бы написать это так:

%.out1 %.out2: %.input
    foo-bin $*.input $*.out1 $*.out2

(Используя переменную правила шаблона $ *, которая обозначает совпадающую часть шаблона)

Если вы хотите быть переносимым на реализации Make, отличные от GNU, или если ваши файлы не могут быть названы в соответствии с шаблоном, есть другой способ:

file-a.out file-b.out: input.in.intermediate ;

.INTERMEDIATE: input.in.intermediate
input.in.intermediate: input.in
    foo-bin input.in file-a.out file-b.out

Это сообщает программе make, что input.in.intermediate не будет существовать до запуска make, поэтому его отсутствие (или его временная метка) не приведет к ложному запуску foo-bin. И вне зависимости от того, устарели ли либо file-a.out, либо file-b.out, либо оба (относительно input.in), foo-bin будет запущен только один раз. Вы можете использовать .SECONDARY вместо .INTERMEDIATE, чтобы программа make НЕ удаляла гипотетическое имя файла input.in.intermediate. Этот метод также безопасен для параллельных сборок make.

Точка с запятой в первой строке важна. Он создает пустой рецепт для этого правила, так что Make знает, что мы действительно будем обновлять file-a.out и file-b.out (спасибо @siulkiulki и другим, указавшим на это)

Demer
источник
7
Приятно, похоже, подход .INTERMEDIATE работает нормально. См. Также статью Automake с описанием проблемы (но они пытаются решить ее портативным способом).
grwlf
3
Это лучший ответ, который я видел. Также могут быть интересны другие специальные цели: gnu.org/software/make/manual/html_node/Special-Targets.html
Арне Бабенхаузерхайде
2
@deemer Это не работает для меня в OSX. (GNU Make 3.81)
Хорхе Букаран
2
Я пробовал это и заметил тонкие проблемы с параллельными сборками - зависимости не всегда правильно распространяются по промежуточной цели (хотя со -j1сборками все в порядке ). Кроме того, если make-файл прерывается и оставляет input.in.intermediateфайл вокруг, это действительно сбивается с толку.
Дэвид Гивен
3
В этом ответе есть ошибка. У вас могут отлично работать параллельные сборки. Вам просто нужно перейти file-a.out file-b.out: input.in.intermediateнаfile-a.out file-b.out: input.in.intermediate ; См. Stackoverflow.com/questions/37873522/… для получения более подробной информации.
siulkilulki
10

Это основано на втором ответе @ deemer, который не полагается на правила шаблона, и исправляет проблему, с которой я столкнулся с вложенным использованием обходного пути.

file-a.out file-b.out: input.in.intermediate
    @# Empty recipe to propagate "newness" from the intermediate to final targets

.INTERMEDIATE: input.in.intermediate
input.in.intermediate: input.in
    foo-bin input.in file-a.out file-b.out

Я бы добавил это как комментарий к ответу @ deemer, но я не могу, потому что я только что создал эту учетную запись и не имею никакой репутации.

Объяснение: Пустой рецепт нужен для того, чтобы позволить Make надлежащим образом отметить file-a.outи пометить file-b.outкак восстановленный. Если у вас есть еще одна промежуточная цель, которая зависит от file-a.out, то Make решит не создавать внешнюю промежуточную цель , утверждая:

No recipe for 'file-a.out' and no prerequisites actually changed.
No need to remake target 'file-a.out'.
Ричард Ся
источник
10

После GNU Make 4.3 (19 января 2020 г.) вы можете использовать «сгруппированные явные цели». Заменить: на &:.

file-a.out file-b.out &: input.in
    foo-bin input.in file-a.out file-b.out

В файле NEWS GNU Make говорится:

Новая функция: сгруппированные явные цели

У шаблонных правил всегда была возможность сгенерировать несколько целей с помощью одного вызова рецепта. Теперь можно объявить, что явное правило генерирует несколько целей за один вызов. Чтобы использовать это, замените в правиле токен «:» на «&:». Чтобы обнаружить эту функцию, поищите 'grouped-target' в специальной переменной .FEATURES. Реализация предоставлена ​​Каз Кылхеку <kaz@kylheku.com>

kakkun61
источник
2

Вот как я это делаю. Сначала я всегда отделяю предварительные требования от рецептов. Тогда в этом случае новая цель сделать рецепт.

all: file-a.out file-b.out #first rule

file-a.out file-b.out: input.in

file-a.out file-b.out: dummy-a-and-b.out

.SECONDARY:dummy-a-and-b.out
dummy-a-and-b.out:
    echo creating: file-a.out file-b.out
    touch file-a.out file-b.out

В первый раз:
1. Мы пытаемся собрать файл a.out, но сначала нужно сделать dummy-a-and-b.out, поэтому make запускает рецепт dummy-a-and-b.out.
2. Пробуем собрать file-b.out, dummy-a-and-b.out в актуальном состоянии.

Второй и последующие разы:
1. Мы пытаемся собрать file-a.out: make просматривает предварительные требования, обычные предварительные требования актуальны, вторичные предварительные требования отсутствуют, поэтому игнорируются.
2. Мы пытаемся создать file-b.out: make просматривает предварительные требования, обычные предварительные требования актуальны, второстепенные предварительные требования отсутствуют, поэтому игнорируются.

ctrl-alt-delor
источник
1
Сломано. Если вы удалите, file-b.outmake не сможет создать его заново.
bobbogo
добавлено все: file-a.out file-b.out как первое правило. Как если бы file-b.out был удален, а программа make была запущена без цели, в командной строке она не будет перестраивать его.
ctrl-alt-delor
@bobbogo Исправлено: см. комментарий выше.
ctrl-alt-delor
0

В качестве расширения к ответу @ deemer я обобщил его в функцию.

sp :=
sp +=
inter = .inter.$(subst $(sp),_,$(subst /,_,$1))

ATOMIC=\
    $(eval s1=$(strip $1)) \
    $(eval target=$(call inter,$(s1))) \
    $(eval $(s1): $(target) ;) \
    $(eval .INTERMEDIATE: $(target) ) \
    $(target)

$(call ATOMIC, file-a.out file-b.out): input.in
    foo-bin input.in file-a.out file-b.out

Сломать:

$(eval s1=$(strip $1))

Удалите любые начальные / конечные пробелы из первого аргумента

$(eval target=$(call inter,$(s1)))

Создайте переменную цель с уникальным значением для использования в качестве промежуточной цели. В этом случае значение будет .inter.file-a.out_file-b.out.

$(eval $(s1): $(target) ;)

Создайте пустой рецепт для выходов с уникальной целью в качестве зависимости.

$(eval .INTERMEDIATE: $(target) ) 

Объявите уникальную цель как промежуточную.

$(target)

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

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

Следует отдать должное атомарным правилам в GNU Make, на которых основана эта функция.

Льюис Р
источник
-1

Чтобы предотвратить параллельное многократное выполнение правила с несколькими выходами make -jN, используйте .NOTPARALLEL: outputs. В твоем случае :

.NOTPARALLEL: file-a.out file-b.out

file-a.out file-b.out: input.in
    foo-bin input.in file-a.out file-b.out
гильоптеро
источник