Il y a quelques années, j'ai lu l'article Récursif Rendre considéré comme nocif et j'ai implémenté l'idée dans mon propre processus de construction. Récemment, j'ai lu un autre article avec des idées sur la façon de implémenter non récursif make
. J'ai donc quelques points de données sur lesquels make
non récursif fonctionne pour au moins quelques projets.
Mais je suis curieux de connaître les expériences des autres. Avez-vous essayé make
non récursif? Cela a-t-il rendu les choses meilleures ou pires? Cela valait-il le temps?
Après avoir lu l'article de RMCH, je me suis lancé dans le but d'écrire un Makefile non récursif approprié pour un petit projet sur lequel je travaillais à l'époque. Après avoir terminé, j'ai réalisé qu'il devrait être possible de créer un "framework" Makefile générique qui peut être utilisé pour dire très simplement et de manière concise quelles cibles finales vous aimeriez construire, quel type de cibles elles sont (par exemple des bibliothèques ou des exécutables ) et quels fichiers source doivent être compilés pour les créer.
Après quelques itérations, j'ai finalement créé exactement cela: un seul Makefile standard d'environ 150 lignes de GNU Faites une syntaxe qui ne nécessite aucune modification - cela fonctionne juste pour tout type de projet que je souhaite utiliser il est activé et est suffisamment flexible pour créer plusieurs cibles de différents types avec suffisamment de granularité pour spécifier des indicateurs de compilation exacts pour chaque fichier source (si je le souhaite) et des indicateurs de liens précis pour chaque exécutable. Pour chaque projet, tout ce que j'ai à faire est de fournir avec de petits Makefiles séparés qui contiennent des bits similaires à ceci:
TARGET := foo
TGT_LDLIBS := -lbar
SOURCES := foo.c baz.cpp
SRC_CFLAGS := -std=c99
SRC_CXXFLAGS := -fstrict-aliasing
SRC_INCDIRS := inc /usr/local/include/bar
Un projet Makefile tel que ci-dessus ferait exactement ce que vous attendez: construire un exécutable nommé "foo", en compilant foo.c (avec CFLAGS = -std = c99) et baz.cpp (avec CXXFLAGS = -fstrict-aliasing) et en ajoutant "./inc" et "/ usr/local/include/bar" au #include
chemin de recherche, avec le lien final incluant la bibliothèque "libbar". Il remarquerait également qu'il existe un fichier source C++ et qu'il sait utiliser l'éditeur de liens C++ au lieu de l'éditeur de liens C. Le cadre me permet de spécifier beaucoup plus que ce qui est montré ici dans cet exemple simple.
Le Makefile passe-partout fait toute la construction de règles et la génération automatique de dépendances nécessaires pour créer les cibles spécifiées. Tous les fichiers générés par la construction sont placés dans une hiérarchie de répertoires de sortie distincte, de sorte qu'ils ne sont pas mélangés avec les fichiers source (et cela se fait sans utiliser VPATH, il n'y a donc aucun problème à avoir plusieurs fichiers source qui portent le même nom).
J'ai maintenant (ré) utilisé ce même Makefile sur au moins deux douzaines de projets différents sur lesquels j'ai travaillé. Certaines des choses que j'aime le plus dans ce système (mis à part la facilité avec laquelle il est de créer un bon Makefile pour tout nouveau projet) sont:
Enfin, je mentionnerais simplement qu'avec les problèmes inhérents à la création récursive, je ne pense pas qu'il m'aurait été possible de réussir cela. J'aurais probablement été condamné à réécrire encore et encore des makefiles défectueux, essayant en vain d'en créer un qui fonctionnait correctement.
Permettez-moi de souligner un argument de l'article de Miller: lorsque vous commencez à résoudre manuellement les relations de dépendance entre différents modules et que vous avez du mal à garantir l'ordre de construction, vous réimplémentez efficacement la logique que le système de construction a été conçu pour résoudre en premier lieu. Construire des systèmes make build récursifs fiables est très difficile. Les projets réels ont de nombreuses parties interdépendantes dont l'ordre de construction n'est pas trivial à comprendre et donc, cette tâche devrait être laissé au système de construction. Cependant, il ne peut résoudre ce problème que s'il a une connaissance globale du système.
En outre, les systèmes de construction de fabrication récursifs sont susceptibles de s'effondrer lorsqu'ils sont construits simultanément sur plusieurs processeurs/cœurs. Bien que ces systèmes de construction puissent sembler fonctionner de manière fiable sur un seul processeur, de nombreuses dépendances manquantes ne sont pas détectées jusqu'à ce que vous commenciez à créer votre projet en parallèle. J'ai travaillé avec un système de fabrication récursif qui fonctionnait sur jusqu'à quatre processeurs, mais qui s'est soudainement écrasé sur une machine avec deux quad-cœurs. Ensuite, j'étais confronté à un autre problème: ces problèmes de concurrence sont presque impossibles à déboguer et j'ai fini par dessiner un organigramme de l'ensemble du système pour comprendre Qu'est ce qui ne s'est pas bien passé.
Pour revenir à votre question, j'ai du mal à trouver de bonnes raisons pour lesquelles on veut utiliser le make récursif. Les performances d'exécution des systèmes non récursifs GNU Make build systems sont difficiles à battre et, bien au contraire, de nombreux systèmes make récursifs ont de sérieux problèmes de performances (un support de construction parallèle faible fait à nouveau partie du problème ). Il y a un papier dans lequel j'ai évalué un système de construction Make spécifique (récursif) et je l'ai comparé à un port SCons. Les résultats de performance ne sont pas représentatifs car le système de construction n'était pas standard, mais dans ce cas particulier, le port SCons était en fait plus rapide.
Bottom line: Si vous voulez vraiment utiliser Make pour contrôler vos versions de logiciels, optez pour Make non récursif, car cela vous facilite la vie à long terme. Personnellement, je préférerais utiliser SCons pour des raisons de convivialité (ou Rake - essentiellement tout système de construction utilisant un langage de script moderne et prenant en charge les dépendances implicites).
J'ai tenté sans enthousiasme mon travail précédent de rendre le système de construction (basé sur GNU make) complètement non récursif, mais j'ai rencontré un certain nombre de problèmes:
Une caractéristique de GNU make qui simplifie l'utilisation non récursive est les valeurs de variables spécifiques à la cible ):
foo: FOO=banana
bar: FOO=orange
Cela signifie que lors de la construction de la cible "foo", $ (FOO) s'étendra en "banane", mais lors de la construction de la "barre" cible, $ (FOO) s'étendra en "orange".
Une limitation de ceci est qu'il n'est pas possible d'avoir des définitions VPATH spécifiques à la cible, c'est-à-dire qu'il n'y a aucun moyen de définir de manière unique VPATH individuellement pour chaque cible. Cela était nécessaire dans notre cas pour trouver les bons fichiers source.
La principale caractéristique manquante de GNU make nécessaire pour supporter la non-récursivité est qu'il manque espaces de noms . Target- des variables spécifiques peuvent être utilisées d'une manière limitée pour "simuler" des espaces de noms, mais ce dont vous auriez vraiment besoin est de pouvoir inclure un Makefile dans un sous-répertoire en utilisant une portée locale.
EDIT: Une autre fonctionnalité très utile (et souvent sous-utilisée) de GNU make dans ce contexte est les fonctionnalités de macro-expansion (voir la fonction eval , par exemple)) Ceci est très utile lorsque vous avez plusieurs cibles qui ont des règles/buts similaires, mais qui diffèrent de manières qui ne peuvent pas être exprimées en utilisant la syntaxe régulière GNU make.
Je suis d'accord avec les déclarations de l'article référencé, mais il m'a fallu beaucoup de temps pour trouver un bon modèle qui fasse tout cela et qui reste facile à utiliser.
Actuellement, je travaille sur un petit projet de recherche, où j'expérimente l'intégration continue; test unitaire automatiquement sur PC, puis exécutez un test système sur une cible (intégrée). Ce n'est pas anodin dans make, et j'ai cherché une bonne solution. Trouver cette marque est toujours un bon choix pour les builds multiplateformes portables, j'ai finalement trouvé un bon point de départ en http://code.google.com/p/nonrec-make
C'était un vrai soulagement. Maintenant mes makefiles sont
Je vais certainement l'utiliser aussi pour le prochain (gros) projet (en supposant C/C++)
J'ai développé un système make non récursif pour un projet C++ de taille moyenne, destiné à être utilisé sur des systèmes de type Unix (y compris les macs). Le code de ce projet se trouve dans une arborescence de répertoires enracinée dans un répertoire src /. Je voulais écrire un système non récursif dans lequel il est possible de taper "make all" à partir de n'importe quel sous-répertoire du répertoire src/de niveau supérieur afin de compiler tous les fichiers source dans l'arborescence des répertoires enracinés dans le répertoire de travail, comme dans un système make récursif. Parce que ma solution semble être légèrement différente des autres que j'ai vues, j'aimerais la décrire ici et voir si j'obtiens des réactions.
Les principaux éléments de ma solution étaient les suivants:
1) Chaque répertoire de l'arborescence src/a un fichier nommé sources.mk. Chacun de ces fichiers définit une variable makefile qui répertorie tous les fichiers source dans l'arborescence enracinée dans le répertoire. Le nom de cette variable est de la forme [répertoire] _SRCS, dans laquelle [répertoire] représente une forme canonique du chemin du répertoire src/de niveau supérieur vers ce répertoire, avec des barres obliques inverses remplacées par des traits de soulignement. Par exemple, le fichier src/util/param/sources.mk définit une variable nommée util_param_SRCS qui contient une liste de tous les fichiers source dans src/util/param et ses sous-répertoires, le cas échéant. Chaque fichier sources.mk définit également une variable nommée [répertoire] _OBJS qui contient une liste des cibles du fichier objet correspondant * .o. Dans chaque répertoire qui contient des sous-répertoires, le fichier sources.mk inclut le fichier sources.mk de chacun des sous-répertoires et concatène les variables _SRCS [sous-répertoire] pour créer sa propre variable _SRCS [répertoire].
2) Tous les chemins sont exprimés dans les fichiers sources.mk comme des chemins absolus dans lesquels le répertoire src/est représenté par une variable $ (SRC_DIR). Par exemple, dans le fichier src/util/param/sources.mk, le fichier src/util/param/Componenent.cpp serait répertorié comme $ (SRC_DIR) /util/param/Component.cpp. La valeur de $ (SRC_DIR) n'est définie dans aucun fichier sources.mk.
3) Chaque répertoire contient également un Makefile. Chaque Makefile inclut un fichier de configuration globale qui définit la valeur de la variable $ (SRC_DIR) sur le chemin absolu vers le répertoire racine src /. J'ai choisi d'utiliser une forme symbolique de chemins absolus car cela semblait être le moyen le plus simple de créer plusieurs fichiers makefiles dans plusieurs répertoires qui interpréteraient les chemins des dépendances et des cibles de la même manière, tout en permettant de déplacer l'ensemble de l'arborescence des sources si vous le souhaitez , en modifiant la valeur de $ (SRC_DIR) dans un fichier. Cette valeur est définie automatiquement par un simple script que l'utilisateur est invité à exécuter lorsque le package est téléchargé ou cloné à partir du référentiel git, ou lorsque toute l'arborescence des sources est déplacée.
4) Le makefile dans chaque répertoire inclut le fichier sources.mk pour ce répertoire. La cible "tous" pour chacun de ces Makefile répertorie le fichier _OBJS [répertoire] pour ce répertoire comme une dépendance, nécessitant ainsi la compilation de tous les fichiers source de ce répertoire et de ses sous-répertoires.
5) La règle de compilation des fichiers * .cpp crée un fichier de dépendance pour chaque fichier source, avec un suffixe * .d, comme effet secondaire de la compilation, comme décrit ici: http: // mad-scientist. net/make/autodep.html . J'ai choisi d'utiliser le compilateur gcc pour la génération de dépendances, en utilisant l'option -M. J'utilise gcc pour la génération de dépendances même lorsque j'utilise un autre compilateur pour compiler les fichiers source, car gcc est presque toujours disponible sur les systèmes de type Unix, et parce que cela aide à standardiser cette partie du système de construction. Un compilateur différent peut être utilisé pour compiler les fichiers source.
6) L'utilisation de chemins absolus pour tous les fichiers dans les variables _OBJS et _SRCS exigeait que j'écrive un script pour éditer les fichiers de dépendance générés par gcc, qui crée des fichiers avec des chemins relatifs. J'ai écrit un script python à cette fin, mais une autre personne a peut-être utilisé sed. Les chemins des dépendances dans les fichiers de dépendances résultants sont des chemins absolus littéraux. Cela convient dans ce contexte car les fichiers de dépendances (contrairement aux fichiers sources.mk) sont générés localement et non distribués dans le cadre du package.
7) Le Makefile dans chaque directeur inclut le fichier sources.mk du même répertoire, et contient une ligne "-include $ ([répertoire] _OBJS: .o = .d)" qui tente d'inclure un fichier de dépendance pour chaque fichier source dans le répertoire et ses sous-répertoires, comme décrit dans l'URL donnée ci-dessus.
La principale différence entre cela et les autres schémas que j'ai vus qui permettent à "make all" d'être invoqué à partir de n'importe quel répertoire est l'utilisation de chemins absolus pour permettre aux mêmes chemins d'être interprétés de manière cohérente lorsque Make est appelé à partir de différents répertoires. Tant que ces chemins sont exprimés à l'aide d'une variable pour représenter le répertoire source de niveau supérieur, cela n'empêche pas de déplacer l'arborescence source et est plus simple que certaines méthodes alternatives pour atteindre le même objectif.
Actuellement, mon système pour ce projet fait toujours une construction "sur place": le fichier objet produit en compilant chaque fichier source est placé dans le même répertoire que le fichier source. Il serait simple d'activer les compilations déplacées en changeant le script qui édite les fichiers de dépendance gcc afin de remplacer le chemin absolu vers le répertoire src/par une variable $ (BUILD_DIR) qui représente le répertoire de construction dans l'expression pour le cible du fichier objet dans la règle pour chaque fichier objet.
Jusqu'à présent, j'ai trouvé ce système facile à utiliser et à entretenir. Les fragments de makefile requis sont courts et relativement faciles à comprendre pour les collaborateurs.
Le projet pour lequel j'ai développé ce système est écrit en ANSI C++ complètement autonome sans dépendances externes. Je pense que ce genre de système makefile non récursif fait maison est une option raisonnable pour un code autonome et hautement portable. Je considérerais un système de construction plus puissant tel que CMake ou gnu autotools, cependant, pour tout projet qui a des dépendances non triviales sur des programmes ou bibliothèques externes ou sur des fonctionnalités non standard du système d'exploitation.
Je connais au moins un projet à grande échelle ( ROOT ), qui fait de la publicité en utilisant [lien PowerPoint] le mécanisme décrit dans Recursive Make Considéré comme nocif. Le framework dépasse un million de lignes de code et se compile assez intelligemment.
Et, bien sûr, tous les grands projets avec lesquels je travaille et qui utilisent make récursif sont douloureusement lents à compiler. ::soupir::
J'ai écrit un système make build non récursif pas très bon, et depuis lors un système make build modulaire récursif très propre pour un projet appelé Pd-extended . C'est essentiellement un peu comme un langage de script avec un tas de bibliothèques incluses. Maintenant, je travaille également sur le système non récursif d'Android, c'est donc le contexte de mes réflexions sur ce sujet.
Je ne peux pas vraiment dire grand-chose sur les différences de performances entre les deux, je n'y ai pas vraiment prêté attention car les builds complets ne sont vraiment effectués que sur le serveur de build. Je travaille généralement soit sur le langage de base, soit sur une bibliothèque particulière, donc je ne suis intéressé que par la construction de ce sous-ensemble de l'ensemble du paquet. La technique de création récursive a l'énorme avantage de rendre le système de construction à la fois autonome et intégré dans un ensemble plus large. Ceci est important pour nous car nous voulons utiliser un système de construction pour toutes les bibliothèques, qu'elles soient intégrées ou écrites par un auteur externe.
Je travaille maintenant sur la création d'une version personnalisée de Android internes, par exemple une version des classes SQLite d'Android qui sont basées sur le sqlite chiffré SQLCipher. Je dois donc écrire Android.mk non récursif fichiers qui encapsulent toutes sortes de systèmes de construction étranges, comme sqlite. Je ne peux pas comprendre comment faire exécuter un script arbitraire par Android.mk, alors que ce serait facile dans un système de création récursif traditionnel, d'après mon expérience.