Règle GNU Makefile générant quelques cibles à partir d'un seul fichier source

106

J'essaye de faire ce qui suit. Il existe un programme, appelez-le foo-bin, qui prend un seul fichier d'entrée et génère deux fichiers de sortie. Une règle Makefile stupide pour cela serait:

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

Cependant, cela n'indique makeen aucun cas que les deux cibles seront générées simultanément. C'est bien lors de l'exécution makeen série, mais cela causera probablement des problèmes si l'on essaie make -j16ou quelque chose d'aussi fou.

La question est de savoir s'il existe un moyen d'écrire une règle Makefile appropriée pour un tel cas? Clairement, cela générerait un DAG, mais d'une manière ou d'une autre, le manuel de fabrication de GNU ne spécifie pas comment ce cas pourrait être traité.

Exécuter le même code deux fois et générer un seul résultat est hors de question, car le calcul prend du temps (pensez: heures). La sortie d'un seul fichier serait également assez difficile, car il est fréquemment utilisé comme entrée dans GNUPLOT qui ne sait pas gérer seulement une fraction d'un fichier de données.

makeaurus
la source
1
Automake a de la documentation à ce sujet: gnu.org/software/automake/manual/html_node
Frank

Réponses:

12

Je le résoudrais comme suit:

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

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

Dans ce cas, la création parallèle «sérialisera» la création de a et b mais puisque la création de b ne fait rien, cela ne prend pas de temps.

Peter Tillemans
la source
16
En fait, il y a un problème avec cela. Si a file-b.outété créé comme indiqué, puis mystérieusement supprimé, makeil sera impossible de le recréer car il file-a.outsera toujours présent. Des pensées?
makesaurus
Si vous supprimez mystérieusement, file-a.outla solution ci-dessus fonctionne bien. Cela suggère un hack: quand un seul de a ou b existe, alors les dépendances doivent être ordonnées de telle sorte que le fichier manquant apparaisse comme sortie de input.in. Quelques $(widcard...)s, $(filter...)s, $(filter-out...)s etc. devraient faire l'affaire. Pouah.
bobbogo
3
PS foo-bincréera évidemment l'un des fichiers en premier (en utilisant une résolution de microseconde), vous devez donc vous assurer que vous avez le bon ordre des dépendances lorsque les deux fichiers existent.
bobbogo
2
@bobbogo soit ça, soit ajouter touch file-a.outcomme deuxième commande après l' foo-bininvocation.
Connor Harris
Voici une solution potentielle pour cela: ajoutez touch file-b.outcomme deuxième commande dans la première règle et dupliquez les commandes dans la deuxième règle. Si l'un des fichiers est manquant ou plus ancien que input.in, la commande ne sera exécutée qu'une seule fois et régénérera les deux fichiers avec file-b.outplus récent que file-a.out.
chqrlie
128

L'astuce consiste à utiliser une règle de modèle avec plusieurs cibles. Dans ce cas, make supposera que les deux cibles sont créées par une seule invocation de la commande.

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

Cette différence d'interprétation entre les règles de modèle et les règles normales n'a pas vraiment de sens, mais elle est utile pour des cas comme celui-ci, et elle est documentée dans le manuel.

Cette astuce peut être utilisée pour n'importe quel nombre de fichiers de sortie tant que leurs noms ont une sous-chaîne commune à laquelle le %doit correspondre. (Dans ce cas, la sous-chaîne commune est ".")

chien lent
la source
3
Ce n'est en fait pas correct. Votre règle spécifie que les fichiers correspondant à ces modèles doivent être créés à partir de input.in en utilisant la commande spécifiée, mais nulle part il n'est dit qu'ils sont créés simultanément. Si vous l'exécutez réellement en parallèle, makeexécutera la même commande deux fois simultanément.
makesaurus
47
Veuillez l'essayer avant de dire que cela ne fonctionne pas ;-) Le manuel de création de GNU dit: "Les règles de modèle peuvent avoir plus d'une cible. Contrairement aux règles normales, cela n'agit pas autant de règles différentes avec les mêmes prérequis et commandes. Si une règle de modèle a plusieurs cibles, faites savoir que les commandes de la règle sont responsables de la création de toutes les cibles. Les commandes ne sont exécutées qu'une seule fois pour créer toutes les cibles. " gnu.org/software/make/manual/make.html#Pattern-Intro
slowdog
4
Mais que faire si j'ai des entrées complètement différentes ?, Dites foo.in et bar.src? Les règles de modèle prennent-elles en charge la correspondance de modèle vide?
grwlf
2
@dcow: Les fichiers de sortie n'ont besoin que de partager une sous-chaîne (même un seul caractère, tel que ".", suffit) pas nécessairement un préfixe. S'il n'y a pas de sous-chaîne commune, vous pouvez utiliser une cible intermédiaire comme décrit par Richard et Deemer. @grwlf: Hey, ça veut dire que "foo.in" et "bar.src" peuvent être faits: foo%in bar%src: input.in:-)
slowdog
1
Si les fichiers de sortie respectifs sont conservés dans des variables, la recette peut être écrite comme suit: $(subst .,%,$(varA) $(varB)): input.in(testé avec GNU make 4.1).
stefanct
55

Make n'a aucun moyen intuitif de le faire, mais il existe deux solutions de contournement décentes.

Premièrement, si les cibles impliquées ont une racine commune, vous pouvez utiliser une règle de préfixe (avec GNU make). Autrement dit, si vous souhaitez corriger la règle suivante:

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

Vous pouvez l'écrire de cette façon:

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

(En utilisant la variable de règle de modèle $ *, qui correspond à la partie correspondante du modèle)

Si vous voulez être portable vers des implémentations non-GNU Make ou si vos fichiers ne peuvent pas être nommés pour correspondre à une règle de modèle, il existe un autre moyen:

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

Cela indique à make que input.in.intermediate n'existera pas avant que make ne soit exécuté, donc son absence (ou son horodatage) ne provoquera pas une exécution erronée de foo-bin. Et que file-a.out ou file-b.out ou les deux soient obsolètes (par rapport à input.in), foo-bin ne sera exécuté qu'une seule fois. Vous pouvez utiliser .SECONDARY au lieu de .INTERMEDIATE, ce qui demandera à make NOT de supprimer un nom de fichier hypothétique input.in.intermediate. Cette méthode est également sûre pour les builds parallèles.

Le point-virgule sur la première ligne est important. Cela crée une recette vide pour cette règle, de sorte que Make sache que nous allons vraiment mettre à jour file-a.out et file-b.out (merci @siulkiulki et d'autres qui l'ont signalé)

deemer
la source
7
Nice, ressemble à l'approche .INTERMEDIATE fonctionne bien. Voir aussi l' article d'Automake décrivant le problème (mais ils essaient de le résoudre de manière portable).
grwlf
3
C'est la meilleure réponse que j'ai vue à cela. Les autres cibles spéciales pourraient également être intéressantes: gnu.org/software/make/manual/html_node/Special-Targets.html
Arne Babenhauserheide
2
@deemer Cela ne fonctionne pas pour moi sur OSX. (GNU Make 3.81)
Jorge Bucaran
2
J'ai essayé cela, et j'ai remarqué des problèmes subtils avec les constructions parallèles - les dépendances ne se propagent pas toujours correctement à travers la cible intermédiaire (bien que tout va bien avec les -j1versions). De plus, si le makefile est interrompu et qu'il laisse le input.in.intermediatefichier autour, il devient vraiment confus.
David donné le
3
Cette réponse a une erreur. Vous pouvez avoir des constructions parallèles fonctionnant parfaitement. Il vous suffit de passer file-a.out file-b.out: input.in.intermediateà file-a.out file-b.out: input.in.intermediate ;Voir stackoverflow.com/questions/37873522/… pour plus de détails.
siulkilulki
10

Ceci est basé sur la deuxième réponse de @ deemer qui ne repose pas sur des règles de modèle, et corrige un problème que je rencontrais avec les utilisations imbriquées de la solution de contournement.

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

J'aurais ajouté ceci en commentaire à la réponse de @ deemer, mais je ne peux pas car je viens de créer ce compte et je n'ai aucune réputation.

Explication: La recette vide est nécessaire pour permettre à Make de faire la comptabilité appropriée pour marquer file-a.outet file-b.outcomme ayant été reconstruite. Si vous avez encore une autre cible intermédiaire qui dépend de file-a.out, Make choisira de ne pas construire l'intermédiaire externe, en affirmant:

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

Après GNU Make 4.3 (19 janvier 2020), vous pouvez utiliser des «cibles explicites groupées». Remplacez :par &:.

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

Le fichier NEWS de GNU Make dit:

Nouvelle fonctionnalité: cibles explicites groupées

Les règles de modèle ont toujours eu la capacité de générer plusieurs cibles avec un seul appel de la recette. Il est désormais possible de déclarer qu'une règle explicite génère plusieurs cibles avec un seul appel. Pour l'utiliser, remplacez le jeton ":" par "&:" dans la règle. Pour détecter cette fonctionnalité, recherchez «grouped-target» dans la variable spéciale .FEATURES. Mise en œuvre fournie par Kaz Kylheku <[email protected]>

kakkun61
la source
2

Voilà comment je fais. Premièrement, je sépare toujours les pré-demandes des recettes. Puis dans ce cas une nouvelle cible pour faire la recette.

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

La première fois:
1. Nous essayons de construire file-a.out, mais dummy-a-and-b.out doit d'abord le faire, alors make lance la recette dummy-a-and-b.out.
2. Nous essayons de construire file-b.out, dummy-a-and-b.out est à jour.

La deuxième fois et les suivantes:
1. Nous essayons de construire file-a.out: examinez les prérequis, les prérequis normaux sont à jour, les prérequis secondaires sont manquants donc ignorés.
2. Nous essayons de construire file-b.out: examinez les prérequis, les prérequis normaux sont à jour, les prérequis secondaires sont manquants donc ignorés.

ctrl-alt-delor
la source
1
C'est cassé. Si vous supprimez, file-b.outmake n'a aucun moyen de le recréer.
bobbogo
tout ajouté: file-a.out file-b.out comme première règle. Comme si file-b.out était supprimé et que make était exécuté sans cible, en ligne de commande, il ne le reconstruirait pas.
ctrl-alt-delor
@bobbogo Correction: voir le commentaire ci-dessus.
ctrl-alt-delor
0

Comme extension de la réponse de @ deemer, je l'ai généralisée en une fonction.

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

Panne:

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

Supprimez tous les espaces de début / fin du premier argument

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

Créez une cible variable avec une valeur unique à utiliser comme cible intermédiaire. Dans ce cas, la valeur sera .inter.file-a.out_file-b.out.

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

Créez une recette vide pour les sorties avec la cible unique comme dépendance.

$(eval .INTERMEDIATE: $(target) ) 

Déclarez la cible unique comme intermédiaire.

$(target)

Terminez par une référence à la cible unique pour que cette fonction puisse être utilisée directement dans une recette.

Notez également que l'utilisation de eval ici est due au fait que eval se développe en rien et que l'expansion complète de la fonction est juste la cible unique.

Doit donner du crédit aux règles atomiques dans GNU Make dont cette fonction est inspirée.

Lewis R
la source
0

Pour empêcher l'exécution multiple parallèle d'une règle avec plusieurs sorties avec make -jN, utilisez .NOTPARALLEL: outputs. Dans ton cas :

.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
guilloptère
la source