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


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 не вказує, як можна обробляти цей випадок.

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


1
Automake має документацію щодо цього: gnu.org/software/automake/manual/html_node/…
Франк

Відповіді:


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 вважатиме, що обидві цілі створюються одним викликом команди.

всі: 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
Це насправді не правильно. Ваше правило визначає, що файли, що відповідають цим шаблонам, повинні бути створені з input.in за допомогою вказаної команди, але ніде не сказано, що вони створюються одночасно. Якщо ви дійсно запускаєте його паралельно, makeвиконайте одну і ту ж команду двічі одночасно.
makeaurus

47
Будь ласка, спробуйте, перш ніж ви скажете, що це не працює ;-) Посібник GNU говорить: "Правила шаблону можуть мати більше однієї цілі. На відміну від звичайних правил, це не діє стільки різних правил з однаковими передумовами та командами. Якщо правило шаблону має кілька цілей, дайте знати, що команди правила відповідають за створення всіх цілей. Команди виконуються лише один раз, щоб зробити всі цілі. " gnu.org/software/make/manual/make.html#Pattern-Intro
slowdog

4
Але що робити, якщо у мене зовсім інші входи? Скажіть foo.in і bar.src? Чи підтримує правила шаблону відповідність порожнього шаблону?
grwlf

2
@dcow: Вихідні файли мають лише спільний рядок (достатньо навіть одного символу, наприклад "."), не обов'язково префікса. Якщо немає загальної підрядки, ви можете використовувати проміжну ціль, як описано Річардом та Демером. @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

(Використовуючи змінну правила шаблону $ *, яка означає узгоджену частину шаблону)

Якщо ви хочете бути переносними для не-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

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

Точка з комою на першому рядку важлива. Це створює порожній рецепт цього правила, так що Make Know знає, що ми дійсно будемо оновлювати файл-a.out та file-b.out (спасибі @siulkiulki та інші, хто вказав на це)


7
Приємно, виглядає .INTERMEDIATE підхід працює добре. Дивіться також статтю Automake, що описує проблему (але вони намагаються вирішити її портативно).
grwlf

3
Це найкраща відповідь, яку я бачив на це. Цікавими можуть бути й інші спеціальні цілі: gnu.org/software/make/manual/html_node/Special-Targets.html
Арн

2
@deemer Це не працює для мене на OSX. (GNU Make 3.81)
Хорхе Букаран

2
Я намагався це зробити, і помітив тонкі проблеми при паралельних побудовах - залежності не завжди поширюються належним чином через проміжну ціль (хоча з -j1побудовими все добре ). Крім того, якщо makefile переривається, і він залишає 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'.

9

Після 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 говорить:

Нова функція: Групування явних цілей

Правила шаблону завжди мали можливість генерувати кілька цілей за допомогою одного виклику рецепта. Тепер можна заявити, що явне правило генерує кілька цілей за допомогою одного виклику. Щоб скористатися цим, замініть маркер ":" на "&:" у праві. Для виявлення цієї функції знайдіть "згрупована ціль" у спеціальній змінній. Реалізація сприяла Kaz Kylheku <kaz@kylheku.com>


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, але манекен-а-і-b.out потрібно зробити спочатку, тому зробимо запуск рецепта «манекен-а-і-б».
2. Ми намагаємося створити файл-b.out, dummy-a-and-b.out.

Другий і наступний час:
1. Ми намагаємося створити файл-a.out: перегляньте передумови, нормальні передумови оновлені, вторинні передумови відсутні, тому їх ігнорують.
2. Ми намагаємося створити файл-b.out: перегляньте передумови, нормальні передумови оновлені, вторинні передумови відсутні, тому їх ігноруйте.


1
Це порушено. Якщо ви видалите file-b.outmake, неможливо відновити її.
bobbogo

додано все: file-a.out file-b.out як перше правило. Як би файл-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 Зробити, від чого ця функція надихнута.


0

Щоб запобігти паралельному багаторазовому виконанню правила з кількома виходами 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
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.