Forum Programmation.autre Makefile générique pour les petits projets

Posté par  . Licence CC By‑SA.
Étiquettes :
16
3
juil.
2013

Sommaire

J'ai plein de petits projets (en C) et quand j'en commence un nouveau, ce qui prend du temps c'est de refaire un Makefile. Pourquoi ne pas utiliser une alternative comme CMake alors ? Et bien parce que j'aime tout ce qui est rustique.

J'ai ainsi conçu un Makefile qui répond à mes besoins en matière de compilation, qui sont, pour un même projet :

  • Gérer la construction de plusieurs binaires
  • Avoir la possibilité de définir plusieurs "modes" de compilation (Release, debug, …)
  • Générer automatiquement les dépendances, et en une seule fois (pas de make depends)
  • Créer les fichiers objets dans un répertoire dédié pour avoir une arborescence lisible
  • Être Capable d'utiliser des fichiers en C et C++ pour un même binaire
  • Avoir une cible make clean pour chaque binaire
  • Pouvoir changer facilement de compilateur
  • Et enfin, que la configuration de la compilation soit aisée

Je voulais aussi accessoirement que mon Makefile utilise le plus possible les possibilités offerte par make, et qu'il reste court, c'est-à-dire ne pas dépasser les 100 lignes.

Je souhaitais aussi le rendre compatible avec d'autre version de Make comme celle utilisée par les BSD. Mais je ne m'y suis pas encore penché pour l'instant. Néanmoins, il est compatible GNU Make 3.81 et GNU Make 3.82.

Ah oui, une dernière broutille, mon Makefile ne devait pas être récursif.

La configuration de la compilation

J'ai placé la configuration dans un fichier appelé conf.mk, qui pilote le fichier qui contient les instructions pour construire les binaires : Makefile.
Techniquement, la première ligne du Makefile est :

include conf.mk

Le but est de ne pas avoir à modifier le Makefile lorsque l'on configure la compilation. (Vous comprendrez un peu plus bas pourquoi il vaut mieux pas trop le modifier ;) )
Le fichier conf.mk indique quels sont les binaires à construire, à partir de quel fichiers. Voici le template que je copie-colle à côté de mon Makefile à chaque nouveau projet :

#
# Configuration de la compilation
#

# Options pour le compilateur :
CFLAGS := -Wall

# Il est possible de définir plusieurs mode de compilation.
# Utilisez la macro "MODE" pour sélectionner le mode.
# CFLAGS_RELEASE := -02
# CFLAGS_DEBUG := -g -Wall
# MODE := RELEASE

# Compilateur, par défaut "gcc"
# CC := g++

# Nom du dossier ou sont placé les fichiers objets, par défaut ".build"
# BUILDDIR := .objs

#
# Binaires
#

# Noms de tout les binaires construit par le Makefile
BINS := prog1

# Pour chaque programme, indiquez les sources, et les options
# pour l'éditeur de liens (optionnel). Il est possible d'utiliser '*'.
SRCS_prog1 := src/*.c
LDFLAGS_prog1 :=

J'utilise intentionnellement := au lieu de = pour des question de performances : utiliser := permet d'évaluer immédiatement l'expression qui suit. Si j'utilise =, alors make considère que c'est une macro et remplacera bêtement chaque occurrences par son contenu, et l'évaluation ne se fera qu'à postériori. Si cette macro contenait une commande, cette dernière serait alors plusieurs fois évaluée, ce qui est rarement l'effet voulut. Cela dit, il n'y a pas de commandes dans cet exemple. Cet article¹ (Section 5.2) explique probablement mieux que moi la différence.

Comment l'utiliser

Placez le template ci-dessus à la racine de votre projet, avec le Makefile que je vais décrire un peu plus bas.
Voici ce que je peux faire avec le fichier de configuration que j'ai montré ci-dessus :

  • make ou make all pour construire tout les binaires (ici un seul : prog1)
  • make prog1 pour construire uniquement un binaire en particulier
  • make clean pour nettoyer tous les fichiers objets
  • make clean_prog1 pour nettoyer uniquement les fichiers objets d'un binaire (ici prog1)

Les dépendances, les fichiers objets, tout est généré automatiquement. Soyez prudent lorsque vous utilisez make clean et faites un backup de votre projet : il n'est pas certain que les fichiers supprimés soient les bons, l'erreur est humaine.

Le makefile

Voici à présent le Makefile à placer à coté de votre conf.mk… Ah bon, c'est illisible ? :D

include conf.mk

CC ?= gcc
BUILDDIR ?= .build
CFLAGS ?= $(CFLAGS_$(MODE))

ifeq ($(MAKECMDGOALS),)
    MAKECMDGOALS = $(BINS)
endif

all: $(BINS)

# Génère les dépendances et la compilation d'un fichier source
define COMP_template
  OBJ = $$(subst $$(suffix $(1)),.o,./$$(BUILDDIR)/$(1))
  DEP = $$(OBJ:.o=.d)

  $$(OBJ): $(1)
    @$(CC) -o $$@ -c $(1) $(CFLAGS)

  $$(DEP): $(1)
    @echo -n "$$(dir $$@)" > $$@
    @if ! $(CC) -MM $(1) 2> /dev/null >> $$@; then > $$@; fi;
endef

# Génère la compilation d'un binaire
define BIN_template
  SRCS := $$(wildcard $$(SRCS_$(1)))
  ALL_SRCS  := $$(ALL_SRCS) $$(SRCS)
  OBJS_$(1) := $$(filter %.o,$$(foreach s,$$(sort $$(suffix $$(SRCS))),$$(patsubst %$$(s),./$$(BUILDDIR)/%.o,$$(SRCS))))
  DEPS_$(1) := $$(OBJS_$(1):.o=.d)

  $(1): $$(OBJS_$(1))
    @$(CC) $$^ $$(LDFLAGS_$(1)) -o $$@ 

  ifeq ($$(filter $(1), $(MAKECMDGOALS)),$(1))
    $$(shell mkdir -p $$(sort $$(dir $$(OBJS_$(1)))))
    -include $$(DEPS_$(1))
  else
    clean_$(1):
    @rm -f $$(OBJS_$(1)) $$(DEPS_$(1))
    @rm -f $(1)
    ALL_CLEANS += clean_$(1)
  endif
endef

$(foreach b,$(BINS),$(eval $(call BIN_template,$(b))))
$(foreach s,$(sort $(ALL_SRCS)),$(eval $(call COMP_template,$(s))))

.PHONY: all clean $(ALL_CLEANS)
clean: $(ALL_CLEANS)
    $(foreach d,$(sort $(dir $(addprefix $(BUILDDIR)/,$(ALL_SRCS)))),$(shell rmdir -p $(d) 2> /dev/null))

Pour ceux qui aimerait bien y voir plus clair (c'est compréhensible), voici quelques informations :

  • MACRO ?= valeur, assigne 'valeur' à MACRO uniquement si MACRO n'a pas été définit avant, une sorte de valeur par défaut.
  • Make propose quelques fonctions bien utiles, que se soit pour manipuler du texte ou faire des boucles.
  • J'utilise à deux reprise l'astuce montré dans la doc sur eval pour générer dynamiquement les cibles avec leurs dépendances. (On pourrait considérer les macros COMP_template et BIN_template comme des fonctions)
  • $(MAKECMDGOALS) contient la liste des cibles à construire. Par exemple, si j’exécute make prog1, $(MAKECMDGOALS) contiendra prog1
  • Les $$ que vous voyez à plusieurs reprise, permettes d'échapper le $. Voir la documentation sur la fonction eval qui explique pourquoi c'est utile.

J'utilise beaucoup les fonctions proposées par make, allez jeter un coup d’œil à la documentation si vous voulez en apprendre plus.

Conclusion

En guise de conclusion, je dirais que les Makefiles, c'est comme le Perl : illisible mais puissant.
Nan, plus sérieusement, j'espère que ce Makefile vous sera utile. C'est un projet qui n'est pas terminé, je reviens dessus quelque fois pour corriger des bugs ou l'améliorer. La syntaxe de make fait peur, mais dieu que c'est puissant !

C'est plus facile d'écrire un Makefile que de ne pas faire de fautes d'orthographe ;)

(1): Rien à voir, mais ce site contient une flopée d'articles qui en intéressera plus d'un !

  • # Bienvenue

    Posté par  (site web personnel) . Évalué à 6. Dernière modification le 03 juillet 2013 à 22:33.

    J'aime bien les Makefile aussi, et c'est moi qui ai fait ça:

    http://home.gna.org/bsdmakepscripts

    http://svn.gna.org/viewcvs/bsdmakepscripts/

    Des scripts pour BSD Make, qu'on peut donc utiliser sous BSD, sous Mac OS X, et sous Linux (avec bmake). Essentiellement ils servent pour les documents TeX/LaTeX (avec intégration METAPOST, la classe quoi!) et les développement OCaml (plus très utile avec Opam, Oasis et ocamlbuild).

    J'avais commencé avec GNU Make, mais je trouve la syntaxe et la documentation vraiment toutes pourries — défaut récurrent de la doc GNU: très bavard et manque de substence — donc j'ai essayé autre chose: le make de FreeBSD. L'intérêt est qu'il y a aussi une infrastructure bien solide, puisque c'est ce Make qui orchestre la compilation du système, qu'on peut utiliser comme exemple. Quand on programme, rien n'est plus utile pour apprendre que de lire les programmes de autres.

    J'utilise intentionnellement := au lieu de = pour des question de performances : utiliser := permet d'évaluer immédiatement l'expression qui suit.

    Ça, c'est loin d'être une bonne idée. Pour GNU Make, je ne sais pas, mais en général il y a des intéractions subtiles entre les variables définies dans l'environnement, les variables définies sur la ligne de commande, etc.

    La morale de l'histoire est qu'il ne faut jamais utiliser le := pour les variables faisant partie de l'interface. Pour la performance, tu peux laisser tomber, sauf si effectivement tu sauvegardes le résultat d'évaluation d'une commande shell.

    Je te recommande chaudement de passer à bmake, pmake, bsdmake, c'est nettement plus facile à programmer que GNU Make. Si tu as envie de jouer avec les BSD Make Pallàs Scripts, fais toi plaisir, je pourrais répondre à tes questions.

  • # création de dossiers

    Posté par  (site web personnel) . Évalué à 2. Dernière modification le 04 juillet 2013 à 17:50.

    Merci pour toutes les idées.

    Un truc qui m'arrive d'avoir besoin dans un makefile est de créer, au besoin des dossiers qui ne sont pas forcément là. Pour ma part, j'utilise ce genre de magouille.

    CODE := $(shell pwd)
    
    # create folders and files
    BINDIR = $(CODE)/build/bin
    OBJDIR = $(CODE)/build/obj
    
    
    # Create folders if need
    target:
        test -d $(BINDIR) || mkdir $(BINDIR)
        test -d $(OBJDIR) || mkdir $(OBJDIR)
    

    La réalité, c'est ce qui continue d'exister quand on cesse d'y croire - Philip K. Dick

    • [^] # Re: création de dossiers

      Posté par  (site web personnel) . Évalué à 3. Dernière modification le 04 juillet 2013 à 18:55.

      Pourquoi est-ce que tu n'utilises pas install -d ? Ça t'évite le teste et tu peux donner les permissions.

      Chez-moi c'est fait comme ça:

      http://svn.gna.org/viewcvs/*checkout*/bsdmakepscripts/trunk/bps/bps.files.mk?revision=345

      Tu définis le groupes de fichiers BIN en donnant les variables
      BINDIR, BINMODE, BINOWN, BINGRP (dossier, mode, owner et group) et énumère tes binaires dans BIN. Avec mon makefile, tu a des cibles qui te permettent de tout installer d'un coup (dossiers, et fichiers), ou bien de travailler par groupe ou par fichier. En plus tu peux définir des attributs spéciaux pour chaque fichier, si certains fichiers ont des particularités.

      ### SYNOPSIS
      
      # TYPE1+= file1.type1
      # TYPE1+= file2.type1
      # TYPE2 = file.type
      #
      # TYPE1MODE.file1.type1 = 444
      # TYPE1NAME.file2.type1 = fancyname
      #
      # FILESGROUPS = TYPE1 TYPE2
      # TYPE1OWN = owner
      # TYPE1GRP = group
      # TYPE1DIR = ${X11PREFIX}/directory     # Will respect ${DESTDIR}
      # TYPE1MODE = 400
      #
      # .include "bps.init.mk"
      # .include "bps.files.mk"
      # .include "bps.usertarget.mk"
      
    • [^] # Re: création de dossiers

      Posté par  . Évalué à 2.

      Je pense que tu peux juste utiliser mkdir -p, si le dossier est déjà créé, il ne fera rien, de plus il créera les dossiers parent si nécessaire.

  • # Sympa !

    Posté par  (site web personnel) . Évalué à 6.

    Tu aurais dû faire un journal pour ce très bon article, c'est moins volatile que le forum !

    Personnellement, le projet sur lequel je travail au boulot est très gros (plusieurs milliers de fichiers dont des centaines de makefiles, récursif bien sûr !)
    C'est une vrai horreur à faire marcher :
    - illisible, des milliers de lignes qui s'appellent dans tous les sens…
    - utilisation massive de variables global qui vont rester dans le shell (donc aucune garantie sur l'état
    - scripts de nettoyage trop vieux (nettoient pas tout voir plante carrément !)
    - lenteur (des dizaines de secondes juste pour vérifier les dépendances)
    - Et bien sûr le moindre changement tient de la gageure !

    À côté de ça, mon expérience sur les autotools m'a définitivement convaincu que ce remède est pire que le mal :
    - extrêmement bordélique (pan ! voila 10 fichiers en plus dans la racine de ton projet…)
    - très lent (mention spéciale au configure)
    - mal documenté et complexe à utiliser (la semaine dernière j'ai cherché à modifier un script autotools d'un vieux projet… j'ai laissé tombé après 1h de tâtonnement…)

    Au final j'en suis arrivé à la conclusion suivante (complètement opposée à la tienne ! ) : La simplicité avant tout !
    - Autant que possible : pas de système de build ! De nos jours, combien de projet on vraiment besoin d'être écrit en C/C++ plutôt qu'en Python ?
    - Pour les tout petit projets : Makefile tout seul avec grosso modo trois règles toutes simples (build, clean, %.o:%c)
    - Pour le reste : CMake (avec Clang et ninja, mais c'est une question de goûts et ça se change en 2s). Un seul fichier de configuration simple et ça roule tout seul !

Suivre le flux des commentaires

Note : les commentaires appartiennent à celles et ceux qui les ont postés. Nous n’en sommes pas responsables.