Regra GNU Makefile gerando alguns alvos a partir de um único arquivo fonte

106

Estou tentando fazer o seguinte. Existe um programa, chame-o foo-bin, que recebe um único arquivo de entrada e gera dois arquivos de saída. Uma regra Makefile burra para isso seria:

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

No entanto, isso não indica de makeforma alguma que ambos os alvos serão gerados simultaneamente. Isso é bom quando executado makeem série, mas provavelmente causará problemas se alguém tentar make -j16ou algo igualmente maluco.

A questão é se existe uma maneira de escrever uma regra Makefile adequada para tal caso. Claramente, isso geraria um DAG, mas de alguma forma o manual do GNU make não especifica como este caso poderia ser tratado.

Executar o mesmo código duas vezes e gerar apenas um resultado está fora de questão, porque o cálculo leva tempo (pense: horas). A saída de apenas um arquivo também seria bastante difícil, porque freqüentemente é usado como uma entrada para o GNUPLOT, que não sabe como lidar com apenas uma fração de um arquivo de dados.

makeaurus
fonte
1
Automake tem documentação sobre isso: gnu.org/software/automake/manual/html_node/…
Frank

Respostas:

12

Eu iria resolver da seguinte forma:

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

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

Neste caso, o make paralelo 'serializará' criando aeb, mas como criar b não faz nada, não leva tempo.

Peter Tillemans
fonte
16
Na verdade, há um problema com isso. Se file-b.outfoi criado como declarado e, em seguida, misteriosamente excluído, makenão será possível recriá-lo porque file-a.outainda estará presente. Alguma ideia?
makeaurus
Se você excluir misteriosamente file-a.out, a solução acima funcionará bem. Isso sugere um hack: quando apenas um de a ou b existe, as dependências devem ser ordenadas de forma que o arquivo ausente apareça como saída de input.in. Alguns $(widcard...)s, $(filter...)s, $(filter-out...)s etc. devem resolver o problema. Ugh.
bobbogo
3
O PS foo-binobviamente criará um dos arquivos primeiro (usando resolução de microssegundos), portanto, você deve garantir que tenha a ordem de dependência correta quando os dois arquivos existirem.
bobbogo
2
@bobbogo ou adicione touch file-a.outcomo um segundo comando após a foo-bininvocação.
Connor Harris
Aqui está uma solução potencial para isso: adicione touch file-b.outcomo um segundo comando na primeira regra e duplique os comandos na segunda regra. Se um dos arquivos estiver faltando ou for mais antigo que input.in, o comando será executado apenas uma vez e irá regenerar os dois arquivos com file-b.outmais recente que file-a.out.
chqrlie
128

O truque é usar uma regra de padrão com vários alvos. Nesse caso, o make assumirá que ambos os destinos são criados por uma única invocação do comando.

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

Essa diferença de interpretação entre as regras de padrão e as regras normais não faz exatamente sentido, mas é útil para casos como esse e está documentada no manual.

Este truque pode ser usado para qualquer número de arquivos de saída, desde que seus nomes tenham alguma substring comum para a %correspondência. (Neste caso, a substring comum é ".")

cachorro lento
fonte
3
Na verdade, isso não está correto. Sua regra especifica que os arquivos que correspondem a esses padrões devem ser feitos a partir de input.in usando o comando especificado, mas em nenhum lugar ela diz que eles são feitos simultaneamente. Se você realmente executá-lo em paralelo, makeo mesmo comando será executado duas vezes simultaneamente.
makeaurus
47
Por favor, tente antes de dizer que não funciona ;-) O manual do GNU make diz: "As regras de padrão podem ter mais de um alvo. Ao contrário das regras normais, isso não atua como muitas regras diferentes com os mesmos pré-requisitos e comandos. Se uma regra padrão tem vários alvos, faça saber que os comandos da regra são responsáveis ​​por fazer todos os alvos. Os comandos são executados apenas uma vez para fazer todos os alvos. " gnu.org/software/make/manual/make.html#Pattern-Intro
slowdog
4
Mas e se eu tiver entradas completamente diferentes ?, Diga foo.in e bar.src? As regras de padrão suportam correspondência de padrão vazia?
grwlf
2
@dcow: Os arquivos de saída precisam apenas compartilhar uma substring (mesmo um único caractere, como ".", é o suficiente), não necessariamente um prefixo. Se não houver substring comum, você pode usar um destino intermediário, conforme descrito por richard e deemer. @grwlf: Ei, isso significa que "foo.in" e "bar.src" podem ser feitos: foo%in bar%src: input.in:-)
slowdog
1
Se os respectivos arquivos de saída forem mantidos em variáveis, a receita pode ser escrita como: $(subst .,%,$(varA) $(varB)): input.in(testado com GNU make 4.1).
stefanct
55

O make não tem uma maneira intuitiva de fazer isso, mas existem duas soluções alternativas decentes.

Primeiro, se os alvos envolvidos têm um radical comum, você pode usar uma regra de prefixo (com GNU make). Ou seja, se você quiser corrigir a seguinte regra:

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

Você poderia escrever desta forma:

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

(Usando a variável de regra de padrão $ *, que representa a parte correspondente do padrão)

Se você deseja ser portátil para implementações não GNU Make ou se seus arquivos não podem ser nomeados para corresponder a uma regra de padrão, há outra maneira:

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

Isso diz ao make que input.in.intermediate não existirá antes de make ser executado, então sua ausência (ou seu registro de data e hora) não fará com que foo-bin seja executado espúrio. E se o file-a.out ou file-b.out ou ambos estiverem desatualizados (em relação ao input.in), o foo-bin será executado apenas uma vez. Você pode usar .SECONDARY em vez de .INTERMEDIATE, que instruirá o make NOT a deletar um nome de arquivo hipotético input.in.intermediate. Este método também é seguro para builds paralelos.

O ponto-e-vírgula na primeira linha é importante. Ele cria uma receita vazia para essa regra, para que Make saiba que realmente atualizaremos file-a.out e file-b.out (obrigado @siulkiulki e outros que apontaram isso)

deemer
fonte
7
Legal, parece que a abordagem .INTERMEDIATE funciona bem. Veja também o artigo do Automake que descreve o problema (mas eles tentam resolvê-lo de forma portátil).
grwlf
3
Esta é a melhor resposta que vi para isso. Também podem ser de interesse os outros alvos especiais: gnu.org/software/make/manual/html_node/Special-Targets.html
Arne Babenhauserheide
2
@deemer Isso não está funcionando para mim no OSX. (GNU Make 3.81)
Jorge Bucaran
2
Tenho tentado fazer isso e percebi problemas sutis com compilações paralelas - as dependências nem sempre se propagam corretamente no destino intermediário (embora tudo esteja bem com -j1compilações). Além disso, se o makefile for interrompido e deixar o input.in.intermediatearquivo ao redor, ele ficará muito confuso.
David Dado
3
Esta resposta contém um erro. Você pode ter compilações paralelas funcionando perfeitamente. Você apenas precisa mudar file-a.out file-b.out: input.in.intermediatepara file-a.out file-b.out: input.in.intermediate ;Ver stackoverflow.com/questions/37873522/… para mais detalhes.
siulkilulki
10

Isso se baseia na segunda resposta de @deemer, que não depende de regras de padrão e corrige um problema que estava ocorrendo com o uso aninhado da solução alternativa.

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

Eu teria adicionado isso como um comentário à resposta de @deemer, mas não posso porque acabei de criar esta conta e não tenho nenhuma reputação.

Explicação: A receita vazia é necessária para permitir que Make faça a contabilidade apropriada para marcar file-a.oute file-b.outcomo tendo sido reconstruída. Se você tiver outro destino intermediário do qual depende file-a.out, o Make escolherá não construir o intermediário externo, alegando:

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

Após o GNU Make 4.3 (19 de janeiro de 2020), você pode usar “alvos explícitos agrupados”. Substitua :por &:.

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

O arquivo NEWS do GNU Make diz:

Novo recurso: alvos explícitos agrupados

As regras de padrão sempre tiveram a capacidade de gerar vários destinos com uma única invocação da receita. Agora é possível declarar que uma regra explícita gera vários destinos com uma única invocação. Para usar isso, substitua o token ":" por "&:" na regra. Para detectar esse recurso, pesquise por 'destino agrupado' na variável especial .FEATURES. Implementação fornecida por Kaz Kylheku <[email protected]>

kakkun61
fonte
2

É assim que eu faço. Em primeiro lugar, sempre separo os pré-requisitos das receitas. Então, neste caso, um novo alvo para fazer a receita.

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

A primeira vez:
1. Tentamos construir file-a.out, mas dummy-a-and-b.out precisa primeiro, então make executa a receita dummy-a-and-b.out.
2. Tentamos construir file-b.out, dummy-a-and-b.out está atualizado.

A segunda e subsequente vez:
1. Tentamos construir file-a.out: olhamos os pré-requisitos, os pré-requisitos normais estão atualizados, os pré-requisitos secundários estão faltando, então são ignorados.
2. Tentamos construir file-b.out: olhamos os pré-requisitos, os pré-requisitos normais estão atualizados, os pré-requisitos secundários estão faltando, então são ignorados.

ctrl-alt-delor
fonte
1
Isto está quebrado. Se você excluir, o file-b.outmake não terá como recriá-lo.
bobbogo
adicionado all: file-a.out file-b.out como primeira regra. Como se file-b.out tivesse sido removido, e make fosse executado sem um alvo, na linha de comando, ele não iria reconstruí-lo.
ctrl-alt-delor
@bobbogo Corrigido: veja o comentário acima.
ctrl-alt-delor
0

Como uma extensão da resposta de @deemer, generalizei em uma função.

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

Demolir:

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

Remova qualquer espaço em branco à esquerda / à direita do primeiro argumento

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

Crie um destino variável para um valor exclusivo para usar como o destino intermediário. Nesse caso, o valor será .inter.file-a.out_file-b.out.

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

Crie uma receita vazia para as saídas com o destino exclusivo como uma dependência.

$(eval .INTERMEDIATE: $(target) ) 

Declare o destino único como intermediário.

$(target)

Termine com uma referência ao destino exclusivo para que esta função possa ser usada diretamente em uma receita.

Observe também que o uso de eval aqui é porque eval se expande para nada, então a expansão completa da função é apenas o destino exclusivo.

Deve-se dar crédito às Regras Atômicas no GNU Make, das quais esta função foi inspirada.

Lewis R
fonte
0

Para evitar a execução múltipla paralela de uma regra com várias saídas com make -jN, use .NOTPARALLEL: outputs. No seu caso :

.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
guilloptero
fonte