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

Я пытаюсь сделать следующее. Существует программа, назовите ее 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 не укажет, как этот случай мог обрабатываться.

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

Ответ 1

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

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 ничего не делает, это не занимает времени.

Ответ 2

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

all: file-a.out file-b.out
file-a%out file-b%out: input.in
    foo-bin input.in file-a$*out file-b$*out

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

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

Ответ 3

У 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

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

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

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

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

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

Ответ 4

Это основано на следующем ответе @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'.

Ответ 5

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

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, но сначала нужно выполнить фиктивный-a-and-b.out, чтобы make запускал рецепт dummy-a-and-b.out.
2. Мы пытаемся создать файл-b.out, dummy-a-and-b.out в актуальном состоянии.

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

Ответ 6

В качестве дополнения к ответу @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, на которых основана эта функция.