Reguła GNU Makefile generująca kilka celów z jednego pliku źródłowego


106

Próbuję wykonać następujące czynności. Istnieje program, nazwij go foo-bin, który pobiera pojedynczy plik wejściowy i generuje dwa pliki wyjściowe. Głupia reguła Makefile byłaby następująca:

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

Nie oznacza to jednak makew żaden sposób, że oba cele zostaną wygenerowane jednocześnie. Jest to dobre, gdy działa makew trybie seryjnym, ale prawdopodobnie spowoduje problemy, jeśli spróbujesz make -j16lub coś równie szalonego.

Pytanie brzmi, czy istnieje sposób na napisanie właściwej reguły Makefile dla takiego przypadku? Oczywiście wygenerowałoby to DAG, ale w jakiś sposób podręcznik GNU make nie precyzuje, jak można by obsłużyć ten przypadek.

Uruchomienie tego samego kodu dwa razy i wygenerowanie tylko jednego wyniku nie wchodzi w grę, ponieważ obliczenia wymagają czasu (pomyśl: godziny). Wyprowadzenie tylko jednego pliku również byłoby raczej trudne, ponieważ często jest on używany jako dane wejściowe do GNUPLOT, który nie wie, jak obsłużyć tylko ułamek pliku danych.


1
Automake ma dokumentację na ten temat: gnu.org/software/automake/manual/html_node/ ...
Frank

Odpowiedzi:


12

Rozwiązałbym to w następujący sposób:

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

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

W tym przypadku make równoległe „serializuje” tworząc a i b, ale ponieważ tworzenie b nic nie robi, nie zajmuje to czasu.


16
Właściwie jest z tym problem. Jeśli file-b.outzostał utworzony zgodnie z opisem, a następnie w tajemniczy sposób usunięty, nie makebędzie można go odtworzyć, ponieważ file-a.outnadal będzie obecny. jakieś pomysły?
makeaurus

Jeśli w tajemniczy sposób usuniesz, file-a.outpowyższe rozwiązanie działa dobrze. Sugeruje to włamanie: gdy istnieje tylko jedna z a lub b, zależności należy uporządkować w taki sposób, aby brakujący plik pojawił się jako wynik input.in. Kilka $(widcard...)s, $(filter...)s, $(filter-out...)s itd. Powinno załatwić sprawę. Fuj.
bobbogo

3
PS foo-binoczywiście najpierw utworzy jeden z plików (używając rozdzielczości mikrosekund), więc musisz upewnić się, że masz poprawną kolejność zależności, gdy oba pliki istnieją.
bobbogo

2
@bobbogo albo to, albo dodaj touch file-a.outjako drugie polecenie po foo-binwywołaniu.
Connor Harris

Oto potencjalne rozwiązanie tego problemu: dodaj touch file-b.outjako drugie polecenie w pierwszej regule i zduplikuj polecenia w drugiej regule. Jeśli brakuje któregokolwiek pliku lub jest starszy niż input.in, polecenie zostanie wykonane tylko raz i ponownie wygeneruje oba pliki z file-b.outnowszymi niż file-a.out.
chqrlie

128

Sztuczka polega na zastosowaniu reguły wzorcowej z wieloma celami. W takim przypadku make przyjmie, że oba cele są tworzone przez pojedyncze wywołanie polecenia.

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

Ta różnica w interpretacji między regułami wzorców a normalnymi regułami nie ma sensu, ale jest przydatna w takich przypadkach i jest udokumentowana w podręczniku.

Ta sztuczka może być użyta dla dowolnej liczby plików wyjściowych, o ile ich nazwy mają jakiś wspólny podciąg dla %dopasowania. (W tym przypadku wspólnym podciągiem jest „.”)


3
To właściwie nie jest poprawne. Twoja reguła określa, że ​​pliki pasujące do tych wzorców mają być tworzone z input.in przy użyciu określonego polecenia, ale nigdzie nie mówi, że są tworzone jednocześnie. Jeśli faktycznie uruchomisz go równolegle, makeuruchomi to samo polecenie dwa razy jednocześnie.
makeaurus

47
Spróbuj, zanim powiesz, że to nie działa ;-) W podręczniku GNU make jest napisane: „Reguły wzorców mogą mieć więcej niż jeden cel. W przeciwieństwie do zwykłych reguł, nie działa to tak wiele różnych reguł z tymi samymi wymaganiami i poleceniami. Jeśli reguła wzorca ma wiele celów, wie, że polecenia reguły są odpowiedzialne za tworzenie wszystkich celów. Polecenia są wykonywane tylko raz, aby utworzyć wszystkie cele. " gnu.org/software/make/manual/make.html#Pattern-Intro
slowdog

4
Ale co, jeśli mam zupełnie inne wejścia ?, Powiedz foo.in i bar.src? Czy reguły wzorca obsługują dopasowanie pustego wzorca?
grwlf

2
@dcow: Pliki wyjściowe muszą mieć tylko wspólny podciąg (wystarczy nawet pojedynczy znak, taki jak "."), niekoniecznie prefiks. Jeśli nie ma wspólnego podłańcucha, możesz użyć pośredniego celu zgodnie z opisem richarda i deemera. @grwlf: Hej, to znaczy "foo.in" i "bar.src" można zrobić: foo%in bar%src: input.in:-)
slowdog

1
Jeśli odpowiednie pliki wyjściowe są przechowywane w zmiennych, przepis można zapisać jako: $(subst .,%,$(varA) $(varB)): input.in(przetestowany z GNU make 4.1).
stefanct

55

Make nie ma na to żadnego intuicyjnego sposobu, ale istnieją dwa przyzwoite obejścia.

Po pierwsze, jeśli zaangażowane cele mają wspólny rdzeń, możesz użyć reguły przedrostka (z GNU make). To znaczy, jeśli chcesz naprawić następującą regułę:

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

Możesz to napisać w ten sposób:

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

(Używając zmiennej $ * wzorca reguły, która oznacza dopasowaną część wzorca)

Jeśli chcesz być przenośny do implementacji innych niż GNU Make lub jeśli twoje pliki nie mogą być nazwane tak, aby pasowały do ​​reguły wzorca, jest inny sposób:

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

To mówi make, że input.in.intermediate nie będzie istniał przed uruchomieniem make, więc jego brak (lub jego znacznik czasu) nie spowoduje fałszywego uruchomienia foo-bin. I niezależnie od tego, czy plik-a.out, czy plik-b.out, czy oba są nieaktualne (w stosunku do input.in), foo-bin zostanie uruchomione tylko raz. Możesz użyć .SECONDARY zamiast .INTERMEDIATE, co poinstruuje make NOT o usuwaniu hipotetycznej nazwy pliku input.intermediate. Ta metoda jest również bezpieczna w przypadku kompilacji równoległych.

Ważny jest średnik w pierwszym wierszu. Tworzy pustą receptę dla tej reguły, dzięki czemu Make wie, że naprawdę będziemy aktualizować file-a.out i file-b.out (dzięki @siulkiulki i innym, którzy to wskazali)


7
Ładnie, w stylu .INTERMEDIATE działa dobrze. Zobacz także artykuł Automake opisujący problem (ale próbują rozwiązać go w sposób przenośny).
grwlf

3
To najlepsza odpowiedź, jaką na to widziałem. Interesujące mogą być również inne cele specjalne: gnu.org/software/make/manual/html_node/Special-Targets.html
Arne Babenhauserheide,

2
@deemer To nie działa dla mnie na OSX. (GNU Make 3.81)
Jorge Bucaran,

2
Próbowałem tego i zauważyłem subtelne problemy z równoległymi kompilacjami - zależności nie zawsze rozprzestrzeniają się prawidłowo w pośrednim celu (chociaż wszystko jest w porządku z -j1kompilacjami). Ponadto, jeśli makefile zostanie przerwany i pozostawi input.in.intermediateplik wokół, będzie naprawdę zdezorientowany.
David Given

3
Ta odpowiedź zawiera błąd. Możesz mieć równoległe kompilacje działające idealnie. file-a.out file-b.out: input.in.intermediateAby file-a.out file-b.out: input.in.intermediate ;uzyskać więcej informacji, wystarczy przejść na stronę stackoverflow.com/questions/37873522/… .
siulkilulki

10

Jest to oparte na drugiej odpowiedzi @ deemer, która nie opiera się na regułach wzorców, i rozwiązuje problem, który miałem z zagnieżdżonymi zastosowaniami obejścia.

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

Dodałbym to jako komentarz do odpowiedzi @ deemer, ale nie mogę, ponieważ właśnie utworzyłem to konto i nie mam żadnej reputacji.

Objaśnienie: Pusta receptura jest potrzebna, aby umożliwić firmie Make prawidłowe prowadzenie księgowości w celu oznaczenia file-a.outi file-b.outprzebudowania. Jeśli masz jeszcze inny cel pośredni, od którego zależy file-a.out, Make zdecyduje się nie tworzyć zewnętrznego pośredniego celu, twierdząc, że:

No recipe for 'file-a.out' and no prerequisites actually changed.
No need to remake target 'file-a.out'.

9

Po GNU Make 4.3 (19 stycznia 2020) możesz używać „zgrupowanych wyraźnych celów”. Wymień :się &:.

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

Plik NEWS w GNU Make mówi:

Nowa funkcja: zgrupowane wyraźne cele

Reguły wzorców zawsze miały możliwość generowania wielu celów za pomocą jednego wywołania receptury. Teraz można zadeklarować, że jawna reguła generuje wiele celów za pomocą jednego wywołania. Aby z tego skorzystać, zamień token „:” na „&:” w regule. Aby wykryć tę cechę, wyszukaj „zgrupowany cel” w zmiennej specjalnej .FEATURES. Wdrożenie wniesione przez Kaz Kylheku <kaz@kylheku.com>


2

Tak to robię. Najpierw zawsze oddzielam wymagania wstępne od przepisów. Wtedy w tym przypadku nowy cel do wykonania przepisu.

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

Za pierwszym razem:
1. Próbujemy skompilować plik-a.out, ale najpierw trzeba wykonać atrapę-a-i-b.out, więc make uruchamia przepis atrapy-a-i-b.out.
2. Próbujemy zbudować plik-b.out, atrapy-a-i-b.out są aktualne.

Drugi i kolejny raz:
1. Próbujemy zbudować plik-a.out: sprawdź wymagania wstępne, normalne wymagania wstępne są aktualne, dodatkowe wymagania wstępne są pominięte, więc są ignorowane.
2. Próbujemy zbudować plik-b.out: przyjrzyj się wymaganiom wstępnym, normalne wymagania wstępne są aktualne, dodatkowe wymagania wstępne brakuje, więc są ignorowane.


1
To jest zepsute. Jeśli usuniesz, file-b.outmake nie ma możliwości ponownego utworzenia.
bobbogo

dodano wszystko: plik-a.out plik-b.out jako pierwszą regułę. Jakby file-b.out został usunięty, a make został uruchomiony bez celu, w linii poleceń, nie odbudowałby go.
ctrl-alt-delor

@bobbogo Naprawiono: patrz powyższy komentarz.
ctrl-alt-delor

0

Jako rozszerzenie odpowiedzi @ deemer uogólniłem ją na funkcję.

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

Awaria:

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

Usuń wszelkie początkowe / końcowe spacje z pierwszego argumentu

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

Utwórz zmienną docelową o unikalnej wartości, która będzie używana jako pośrednia wartość docelowa. W tym przypadku wartością będzie .inter.file-a.out_file-b.out.

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

Utwórz pustą recepturę dla wyników z unikalnym celem jako zależnością.

$(eval .INTERMEDIATE: $(target) ) 

Zadeklaruj unikalny cel jako pośredni.

$(target)

Zakończ z odniesieniem do unikalnego celu, aby ta funkcja mogła być używana bezpośrednio w przepisie.

Zauważ również, że użycie eval jest tutaj spowodowane tym, że eval nie rozszerza się do zera, więc pełne rozwinięcie funkcji jest tylko unikalnym celem.

Muszę przyznać uznanie Regułom Atomowym w GNU Make, z których ta funkcja jest inspirowana.


0

Aby zapobiec równoległemu wielokrotnemu wykonywaniu reguły z wieloma wyjściami make -jN, użyj .NOTPARALLEL: outputs. W Twoim przypadku :

.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
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.