Quelle est votre expérience avec la marque non récursive?

Il y a quelques années, j’ai lu le document Récursif sur la prise en compte de la fabrication et mis en œuvre cette idée dans mon propre processus de création. Récemment, j’ai lu un autre article avec des idées sur la façon de mettre en œuvre la création non récursive . J’ai donc quelques points de données que 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é la make non récursive? Cela a-t-il amélioré ou aggravé les choses? Cela valait-il la peine?

Nous utilisons un système GNU Make non récursif dans la société pour laquelle je travaille. Il est basé sur l’article de Miller et en particulier sur le lien “Implémentation de marques non récursives” que vous avez donné. Nous avons réussi à affiner le code de Bergen dans un système où il n’y a pas de plaque de chaudière dans les fichiers makefile des sous-répertoires. En gros, cela fonctionne très bien et est bien meilleur que notre système précédent (une chose récursive faite avec GNU Automake).

Nous prenons en charge tous les systèmes d’exploitation “majeurs” (commerciaux): AIX, HP-UX, Linux, OS X, Solaris, Windows et même le mainframe AS / 400. Nous compilons le même code pour tous ces systèmes, avec les parties dépendantes de la plate-forme isolées dans les bibliothèques.

Il y a plus de deux millions de lignes de code C dans notre arborescence dans environ 2000 sous-répertoires et 20000 fichiers. Nous avons sérieusement envisagé d’utiliser des SCons, mais nous ne pouvions tout simplement pas le faire assez rapidement. Sur les systèmes plus lents, Python utilisait quelques dizaines de secondes pour parsingr les fichiers SCons où GNU Make faisait la même chose en une seconde environ. C’était il y a environ trois ans, alors les choses ont peut-être changé depuis. Notez que nous conservons généralement le code source sur un partage NFS / CIFS et construisons le même code sur plusieurs plates-formes. Cela signifie qu’il est encore plus lent pour l’outil de génération d’parsingr l’arborescence source des modifications.

Notre système GNU Make non récursif n’est pas sans problèmes. Voici quelques-uns des plus grands obstacles auxquels vous pouvez vous attendre:

  • Le rendre portable, en particulier à Windows, est très compliqué.
  • Bien que GNU Make soit presque un langage de programmation fonctionnel utilisable, il ne convient pas à la programmation dans le grand format. En particulier, il n’y a pas d’espaces de noms, de modules ou quelque chose comme ça pour vous aider à isoler les morceaux les uns des autres. Cela peut causer des problèmes, mais pas autant que vous ne le pensez.

Les principales victoires sur notre ancien système makefile récursif sont les suivantes:

  • C’est rapide Il faut environ deux secondes pour vérifier l’arborescence complète (2 k répertoires, 20 000 fichiers) et décider si elle est à jour ou commencer à comstackr. L’ancien récursif prendrait plus d’une minute pour ne rien faire.
  • Il gère correctement les dépendances. Notre ancien système basé sur les sous-répertoires de commande a été construit, etc. Comme pour la lecture de l’article de Miller, le traitement de l’arborescence complète en tant qu’entité unique est le meilleur moyen de résoudre ce problème.
  • Il est portable sur tous nos systèmes pris en charge, après tout le dur travail que nous y avons consacré. C’est vraiment cool.
  • Le système d’abstraction nous permet d’écrire des fichiers de rendu très concis. Un sous-répertoire typique qui ne définit qu’une bibliothèque est constitué de deux lignes seulement. Une ligne donne le nom de la bibliothèque et l’autre liste les bibliothèques dont celle-ci dépend.

En ce qui concerne le dernier élément de la liste ci-dessus. Nous avons fini par mettre en place une sorte d’installation d’extension de macros dans le système de build. Les sous-répertoires makefiles répertorient les programmes, sous-répertoires, bibliothèques et autres éléments communs dans des variables telles que PROGRAMS, SUBDIRS, LIBS. Ensuite, chacun d’eux est développé en “vraies” règles GNU Make. Cela nous permet d’éviter une grande partie des problèmes d’espace de noms. Par exemple, dans notre système, il est bon d’avoir plusieurs fichiers sources portant le même nom, sans problème.

En tout cas, cela a fini par être un gros travail. Si vous pouvez obtenir des SCons ou des outils similaires pour votre code, je vous conseille de regarder cela en premier.

Après avoir lu le papier RMCH, je me suis fixé comme objective d’écrire un Makefile non récursif approprié pour un petit projet sur lequel je travaillais à l’époque. Après avoir terminé, je me suis rendu compte qu’il devrait être possible de créer un “framework” Makefile générique qui peut être utilisé de manière très simple et concise pour déterminer les cibles finales que vous souhaitez construire, les types de cibles (bibliothèques ou exécutables par exemple). ) et quels fichiers sources doivent être compilés pour les créer.

Après quelques itérations, j’ai fini par créer: un simple Makefile d’environ 150 lignes de syntaxe GNU Make qui n’a jamais besoin de modification – il fonctionne simplement pour tout type de projet que je souhaite utiliser et est suffisamment flexible pour être construit. 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 lui fournir de petits Makefiles séparés contenant des bits similaires à ceci:

 TARGET := foo TGT_LDLIBS := -lbar SOURCES := foo.c baz.cpp SRC_CFLAGS := -std=c99 SRC_CXXFLAGS := -fssortingct-aliasing SRC_INCDIRS := inc /usr/local/include/bar 

Un Makefile de projet tel que ci-dessus ferait exactement ce que vous attendiez: construire un exécutable nommé “foo”, en compilant foo.c (avec CFLAGS = -std = c99) et baz.cpp (avec CXXFLAGS = -fssortingct-aliasing) et en ajoutant “./inc” et “/ usr / local / include / bar” au chemin de recherche #include , avec la liaison finale incluant la bibliothèque “libbar”. Il notera également qu’il existe un fichier source C ++ et que vous savez utiliser l’éditeur de liens C ++ à la place de l’éditeur de liens C. Le framework me permet de spécifier beaucoup plus que ce qui est montré ici dans cet exemple simple.

Le Makefile standard effectue toutes les constructions de règles et la génération automatique de dépendances requirejses pour construire 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 ceci sans utiliser VPATH, il n’y a donc aucun problème à avoir plusieurs fichiers sources portant le même nom).

J’ai maintenant (re) 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 (à part la facilité de créer un Makefile approprié pour tout nouveau projet) sont:

  • C’est rapide Il peut dire instantanément si quelque chose est obsolète.
  • Des dépendances fiables à 100%. Il n’y a aucune chance que des builds parallèles se brisent mystérieusement, et cela crée toujours exactement le minimum requirejs pour tout remettre à jour.
  • Je n’aurai jamais besoin de réécrire un makefile complet à nouveau: D

Enfin, je mentionnerai simplement que, avec les problèmes inhérents à la fabrication récursive, je ne pense pas que cela aurait été possible pour moi. J’aurais probablement été condamné à réécrire encore et encore des makefiles imparfaits, essayant en vain d’en créer un qui fonctionnait correctement.

Permettez-moi d’insister sur 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éinstallez efficacement la logique du système de construction. Construire des systèmes de compilation 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 sortingvial à comprendre et, par conséquent, cette tâche doit être laissée au système de génération. 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 génération récursifs peuvent se désintégrer lors de la construction simultanée sur plusieurs processeurs / cœurs. Bien que ces systèmes de génération puissent sembler fonctionner de manière fiable sur un seul processeur, de nombreuses dépendances manquantes ne sont pas détectées avant que vous ne commenciez à construire votre projet en parallèle. J’ai travaillé avec un système de génération récursif qui fonctionnait sur quatre processeurs au maximum, mais qui s’est soudainement écrasé sur une machine dotée de deux processeurs. Ensuite, j’ai été 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 déterminer ce qui n’allait pas.

Pour revenir à votre question, j’ai du mal à penser aux bonnes raisons pour lesquelles on veut utiliser la marque récursive. Les performances d’exécution des systèmes de génération GNU Make non récursifs sont difficiles à battre et, bien au contraire, de nombreux systèmes récursifs ont de sérieux problèmes de performances (la prise en charge de la construction parallèle faible est à nouveau une partie du problème). Il y a un article dans lequel j’ai évalué un système de génération Make (récursif) spécifique et l’ai comparé à un port SCons. Les résultats de performance ne sont pas représentatifs car le système de construction était très non 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 builds de logiciels, optez pour non-récursif Make car cela rend votre vie beaucoup plus facile à long terme. Personnellement, je préfère utiliser les SCons pour des raisons d’utilisabilité (ou Rake – essentiellement tout système de compilation utilisant un langage de script moderne et prenant en charge les dépendances implicites).

J’ai fait une tentative timide à mon travail précédent de rendre le système de compilation (basé sur GNU make) complètement non récursif, mais j’ai rencontré un certain nombre de problèmes:

  • Les artefacts (c’est-à-dire les bibliothèques et les exécutables construits) ont leurs sources réparties sur un certain nombre de répertoires, en s’appuyant sur vpath pour les trouver
  • Plusieurs fichiers sources portant le même nom existaient dans des répertoires différents
  • Plusieurs fichiers sources ont été partagés entre des artefacts, souvent compilés avec différents indicateurs de compilation
  • Différents artefacts ont souvent des indicateurs de compilation différents, des parameters d’optimisation, etc.

Une caractéristique de GNU make, qui simplifie l’utilisation non récursive, est la suivante:

 foo: FOO=banana bar: FOO=orange 

Cela signifie que quand on construit la cible “foo”, $ (FOO) se développera en “banana”, mais quand on construira la cible “bar”, $ (FOO) se développera en “orange”.

Une limitation de ceci est qu’il n’est pas possible d’avoir des définitions VPATH spécifiques à la cible, c.-à-d. Cela était nécessaire dans notre cas pour trouver les fichiers sources corrects.

La principale caractéristique manquante de GNU nécessaire pour prendre en charge la non-récursivité est l’absence d’ espaces de noms . Les variables spécifiques à la cible peuvent être utilisées de manière limitée pour “simuler” les espaces de noms, mais ce dont vous avez vraiment besoin est de pouvoir inclure un Makefile dans un sous-répertoire en utilisant une scope locale.

EDIT: Une autre fonctionnalité très utile (et souvent sous-utilisée) de GNU make dans ce contexte concerne les fonctionnalités d’expansion de macro (voir la fonction eval , par exemple). Ceci est très utile lorsque vous avez plusieurs cibles qui ont des règles / objectives similaires, mais qui diffèrent par des méthodes qui ne peuvent pas être exprimées en utilisant la syntaxe GNU standard.

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 fait tout cela et qui est toujours facile à utiliser.

Je travaille actuellement sur un petit projet de recherche où j’expérimente l’continuous integration; testez automatiquement l’unité sur PC, puis exécutez un test système sur une cible (intégrée). Ceci n’est pas sortingvial dans make, et j’ai cherché une bonne solution. Trouver cette marque est toujours un bon choix pour les builds multi-plateformes portables J’ai enfin trouvé un bon sharepoint départ dans http://code.google.com/p/nonrec-make

C’était un vrai soulagement. Maintenant, mes makefiles sont

  • très simple à modifier (même avec des connaissances limitées)
  • rapide à comstackr
  • vérifier complètement les dépendances (.h) sans effort

Je vais certainement aussi l’utiliser pour le prochain (grand) projet (en supposant que C / C ++)

Je connais au moins un projet à grande échelle ( ROOT ), qui fait de la publicité en utilisant le [lien powerpoint] le mécanisme décrit dans Récursif Make Considéré Nocif. Le framework dépasse un million de lignes de code et comstack intelligemment.


Et, bien sûr, tous les projets de grande envergure avec lesquels je travaille, qui utilisent des marques récursives, sont extrêmement lents à comstackr. ::soupir::

J’ai développé un système de création 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 dans ce projet est tout 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 / afin de comstackr tous les fichiers sources dans l’arborescence enracinée dans le répertoire de travail, comme dans un système de marque récursif. Parce que ma solution semble légèrement différente des autres que j’ai vues, j’aimerais la décrire ici et voir si je réagis.

Les principaux éléments de ma solution étaient les suivants:

1) Chaque répertoire du src / tree a un fichier nommé sources.mk. Chacun de ces fichiers définit une variable makefile qui répertorie tous les fichiers source de l’arborescence, qui sont enracinés dans le répertoire. Le nom de cette variable est de la forme [répertoire] _SRCS, dans lequel [répertoire] représente une forme canalisée du chemin depuis le 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 sources de src / util / param et de ses sous-répertoires, le cas échéant. Chaque fichier sources.mk définit également une variable nommée [répertoire] _OBJS contenant une liste des cibles du fichier object correspondant * .o. Dans chaque répertoire contenant des sous-répertoires, 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 [directory] _SRCS.

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 fichier Makefile comprend un fichier de configuration global qui définit la valeur de la variable $ (SRC_DIR) sur le chemin absolu du répertoire src / racine. 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’intégralité de l’arborescence , en modifiant la valeur de $ (SRC_DIR) dans un fichier. Cette valeur est définie automatiquement par un script simple 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 source est déplacée.

4) Le fichier makefile dans chaque répertoire inclut le fichier sources.mk pour ce répertoire. La cible “all” pour chacun de ces Makefiles répertorie le fichier _OBJS [répertoire] pour ce répertoire en tant que dépendance, ce qui nécessite la compilation de tous les fichiers source dans ce répertoire et 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 vous utilisez un autre compilateur pour comstackr les fichiers source, car gcc est presque toujours disponible sur des systèmes de type Unix, et cela aide à standardiser cette partie du système de génération. Un compilateur différent peut être utilisé pour comstackr réellement les fichiers sources.

6) L’utilisation de chemins absolus pour tous les fichiers des variables _OBJS et _SRCS nécessitait que j’écrive un script pour éditer les fichiers de dépendance générés par gcc, ce qui crée des fichiers avec des chemins relatifs. J’ai écrit un script python à cette fin, mais une autre personne aurait pu utiliser sed. Les chemins des dépendances dans les fichiers de dépendance résultants sont des chemins absolus littéraux. Cela convient parfaitement dans ce contexte car les fichiers de dépendance (contrairement aux fichiers sources.mk) sont générés localement et ne sont pas dissortingbué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 des fichiers de dépendance pour chaque fichier source dans le répertoire et ses sous-répertoires, comme décrit dans l’URL ci-dessus.

La principale différence entre les autres schémas permettant d’appeler “make all” à partir de n’importe quel répertoire est l’utilisation de chemins absolus pour que les mêmes chemins soient interprétés de manière cohérente lorsque Make est appelé depuis 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 permettant d’atteindre le même objective.

Actuellement, mon système pour ce projet effectue toujours une génération “in-situ”: le fichier object généré en compilant chaque fichier source est placé dans le même répertoire que le fichier source. Il serait facile d’activer les constructions obsolètes en modifiant le script qui édite les fichiers de dépendance gcc afin de remplacer le chemin absolu du répertoire src / dirctory par une variable $ (BUILD_DIR) qui représente le répertoire de construction dans l’expression du fichier. object fichier cible dans la règle pour chaque fichier object.

Jusqu’à présent, j’ai trouvé ce système facile à utiliser et à entretenir. Les fragments de makefile requirejs sont courts et relativement faciles à comprendre pour les collaborateurs.

Le projet pour lequel j’ai développé ce système est écrit en C ++ ANSI complètement autonome, sans aucune dépendance externe. Je pense que ce type de système de makefile non récursif fait maison est une option raisonnable pour un code autonome hautement portable. J’envisagerais un système de construction plus puissant, tel que CMake ou gnu autotools, pour tout projet comportant des dépendances non négligeables sur des programmes ou des bibliothèques externes ou sur des fonctionnalités de système d’exploitation non standard.

J’ai écrit un système de génération de build non récursif pas très bon, et depuis lors, un système de construction récursive modulaire très propre pour un projet appelé Pd-extended . C’est un peu comme un langage de script avec un tas de bibliothèques incluses. Maintenant, je travaille aussi sur le système non récursif d’Android, c’est donc le contexte de mes reflections sur ce sujet.

Je ne peux pas vraiment en dire beaucoup sur les différences de performances entre les deux, je n’ai pas vraiment fait attention car les builds complètes ne sont vraiment réalisées que sur le serveur de compilation. Je travaille habituellement soit sur la langue principale, soit sur une bibliothèque particulière, donc je ne suis intéressé que par la construction de ce sous-ensemble de l’ensemble. La technique de fabrication récursive a l’énorme avantage de rendre le système de construction à la fois autonome et intégré dans un ensemble plus vaste. Ceci est important pour nous car nous voulons utiliser un système de construction unique pour toutes les bibliothèques, qu’elles soient intégrées ou écrites par un auteur externe.

Je travaille actuellement sur la construction d’une version personnalisée des composants internes d’Android, par exemple une version des classes SQLite d’Android qui sont basées sur le sqlite chiffré SQLCipher. Donc, je dois écrire des fichiers Android.mk non récursifs qui contiennent toutes sortes de systèmes de construction étranges, tels que sqlite. Je ne peux pas comprendre comment faire en sorte que Android.mk exécute un script arbitraire, alors que cela serait facile dans un système de création récursif traditionnel, d’après mon expérience.