Makefile modulaire

Sommaire :

 

Un outil très puissant pour compiler et gérer ces projets : les Makefile.

Assez facile à prendre en main pour une utilisation toute somme basique, c’est lorsqu’on feuillette un peu la doc qu’on se rend compte de la puissance du Makefile. Je dois avouer avoir passé quelques jours rien que sur mon Makefile pour maitriser la bête (il me reste encore quelques zones d’ombre qui je suis sûr s’éclairciront avec le temps). Mais lorsqu’on possède un Makefile sympa, on peut copier-coller ce dernier pour l’adapter à tous ces projets 😉

Je remercie fdaudre-, agadiffe, jlagneau, mtassett, apachkof et ncoden de 42 qui m’ont permis d’y voir plus clair, en partageant ici et là leur propre Makefile et conseils. Voici donc le mien, que je vous explique ci-dessous… et pour éviter à ceux qui connaissent déjà le principe de se taper toute la lecture de l’article.

Makefile
# **************************************************************************** #
#                                                                              #
#                                                         :::      ::::::::    #
#    Makefile                                           :+:      :+:    :+:    #
#                                                     +:+ +:+         +:+      #
#    By: qfremeau <qfremeau@student.42.fr>          +#+  +:+       +#+         #
#                                                 +#+#+#+#+#+   +#+            #
#    Created: 2016/08/02 11:44:08 by qfremeau          #+#    #+#              #
#    Updated: 2016/11/15 17:57:53 by qfremeau         ###   ########.fr        #
#                                                                              #
# **************************************************************************** #

# Compilation
CC =		clang
CFLAGS =	-Wall -Wextra -Werror
ADDFLAGS =	

# Default rule
DEFRULE =	all

# Binary
NAME =		rtv1
DST =		

# Directories
SRCDIR =	srcs
OBJDIR =	objs
INCDIR =	includes\
			minilibx\
			libft/includes\
			/usr/X11/include

# Sources
SRC = \
			main.c\
			rt_vector.c\
			rt_vector_op.c\
			rt_raytrace.c\
			fract_list.c\
			fract_pixel.c\
			fract_color_rgb.c\
			fract_image.c\
			fract_draw_point.c\
			fract_hook.c\
			fract_exit.c

OBJ =		$(SRC:.c=.o)

# Prefixes
MLIBX =		-L/usr/local/lib -lmlx -lm -framework OpenGL -framework AppKit
LIBFT =		-Llibft/ -lft


# Paths foreach
LIBP =		$(addprefix -L, $(LIBNAME)/)
OBJP =		$(addprefix $(OBJDIR)/, $(SRC:.c=.o))
INCP =		$(foreach dir, $(INCDIR), -I$(dir))

# **************************************************************************** #
# SPECIAL CHARS

LOG_CLEAR		= \033[2K
LOG_UP			= \033[A
LOG_NOCOLOR		= \033[0m
LOG_BOLD		= \033[1m
LOG_UNDERLINE	= \033[4m
LOG_BLINKING	= \033[5m
LOG_BLACK		= \033[1;30m
LOG_RED			= \033[1;31m
LOG_GREEN		= \033[1;32m
LOG_YELLOW		= \033[1;33m
LOG_BLUE		= \033[1;34m
LOG_VIOLET		= \033[1;35m
LOG_CYAN		= \033[1;36m
LOG_WHITE		= \033[1;37m

# **************************************************************************** #
# RULES

# Main rules
default:
	@echo -e "$(LOG_BOLD)Default execution: rule $(DEFRULE)$(LOG_NOCOLOR)"
	@make $(DEFRULE)
	@echo -e "$(LOG_BOLD)Execution finished     $(LOG_NOCOLOR)ヽ(ヅ)ノ"

all: mlibxcomp libftcomp $(OBJDIR) $(NAME)

re: fclean all

# Compilation rules
libftcomp:
	@make all -C libft/

mlibxcomp:
	@echo -e "$(LOG_CLEAR)$(LOG_BLUE)build minilibx$(LOG_NOCOLOR)"
	@make all -C minilibx/
	@echo -e "--$(LOG_CLEAR)$(LOG_VIOLET)minilibx$(LOG_NOCOLOR) compiled.......... $(LOG_GREEN)✓$(LOG_NOCOLOR)"

$(OBJDIR)/%.o: $(SRCDIR)/%.c
	@echo -e "--$(LOG_CLEAR)$(LOG_VIOLET)$(NAME)$(LOG_NOCOLOR)........................ $(LOG_YELLOW)$<$(LOG_NOCOLOR)$(LOG_UP)"
	@$(CC) $(CFLAGS) $(ADDFLAGS) -c -o $@ $^ $(INCP)

$(OBJDIR):
	@echo -e "$(LOG_CLEAR)$(LOG_BLUE)build $(NAME)$(LOG_NOCOLOR)"
	@mkdir -p $(OBJDIR)

$(NAME): $(OBJP)
	@echo -e "--$(LOG_CLEAR)$(LOG_VIOLET)$(NAME)$(LOG_NOCOLOR)....................... $(LOG_YELLOW)assembling$(LOG_NOCOLOR)$(LOG_UP)"
	@$(CC) $(CFLAGS) $(ADDFLAGS) -o $@ $^ $(INCP) $(MLIBX) $(LIBFT)
	@echo -e "--$(LOG_CLEAR)$(LOG_VIOLET)$(NAME)$(LOG_NOCOLOR) compiled........... $(LOG_GREEN)✓$(LOG_NOCOLOR)"

# MrProper's legacy
.PHONY: fclean clean glu

clean:
	@echo -e "$(LOG_CLEAR)$(LOG_BLUE)clean $(NAME)$(LOG_NOCOLOR)"
	@echo -e "--$(LOG_CLEAR)$(LOG_YELLOW)Objects$(LOG_NOCOLOR) deletion............. $(LOG_RED)×$(LOG_NOCOLOR)"
	@rm -rf $(OBJDIR)
	@echo -e "$(LOG_CLEAR)$(LOG_BLUE)fclean --force minilibx$(LOG_NOCOLOR)"
	@make clean -C minilibx/
	@echo -e "$(LOG_CLEAR)$(LOG_BLUE)clean libft$(LOG_NOCOLOR)"
	@make clean -C libft/

fclean:
	@echo -e "$(LOG_CLEAR)$(LOG_BLUE)fclean minilibx$(LOG_NOCOLOR)"
	@make clean -C minilibx/
	@echo -e "$(LOG_CLEAR)$(LOG_BLUE)fclean libft$(LOG_NOCOLOR)"
	@make fclean -C libft/
	@echo -e "$(LOG_CLEAR)$(LOG_BLUE)fclean $(NAME)$(LOG_NOCOLOR)"
	@echo -e "--$(LOG_CLEAR)$(LOG_YELLOW)Objects$(LOG_NOCOLOR) deletion............. $(LOG_RED)×$(LOG_NOCOLOR)"
	@rm -rf $(OBJDIR)
	@echo -e "--$(LOG_CLEAR)$(LOG_YELLOW)Binary$(LOG_NOCOLOR) deletion.............. $(LOG_RED)×$(LOG_NOCOLOR)"
	@rm -f $(NAME)

glu: re clean

 

Partie 1, les Dépendances :

Cette première partie regroupe les variables utiles pour votre Makefile, que vous aurez systématiquement ou presque à modifier, d’un projet à l’autre. NAME représentant bien sûr le nom de votre projet, par exemple ici « Fillit ». L’explication sera assez rapide, j’assume que vous connaissez le principe de compilation d’un projet, quand bien même j’y apporterai des précisions en 3ième partie.

  • Les Répertoires / Directories:

Une compilation se décompose en plusieurs étapes, dont la première est de transformer les fichiers sources .c en fichiers objets .o. Sans oublier d’inclure les fichier .h ou nommés includes, faisant partie de l’étape de précompilation. Vous aurez donc besoin de préciser au compilateur où se trouve ces différents fichiers dans les variables SRCDIR, OBJDIR et INCDIR, en assumant que vous n’êtes pas un gros bourrin et utilisé une méthode de travail propre et approprié pour votre projet.

  • Les Librairies / Libs:

Certains projets auront besoin d’une librairie extérieur afin de fonctionner. Une librairie où se trouve des fonctions déjà créées par d’autres ou pour le système. J’utilise ici notre fameuse libft de 42 qui regroupe une cinquantaine de fonctions système basiques, mais aussi la librairie graphique de l’école: minilibx. Chacune est spécifié dans une variable spécifique MLIBX et LIBFT que je rajoute ensuite dans mes règles de compilation.

  • Les Sources:

Votre projet ne pourra compiler sans que vous ne lui spécifier les fichiers sources à utiliser dans SRC. Certains pour aller plus vite, utiliseront les wildcard, par exemple « *.c », afin d’inclure fichier de type .c, mais je le déconseille, d’une part parce qu’à 42 leur utilisation est interdite, et d’autre part parce que vous pourriez par mégarde inclure un fichier non voulu. C’est une bonne habitude à prendre.

  • Et la Compilation:

CC et CFLAGS précise le compilateur que vous utiliserez et les options de compilations « -Wall -Wextra -Werror » pour les sado-maso du code, qui l’aime propre, voir un peu trop! Mais c’est encore une bonne habitude à prendre.

 

Partie 2, de l’Esthétique :

« Special chars » n’est juste qu’une partie esthétique qui me permet d’afficher un texte de compilation personnalisé et coloré pour mon Makefile. Ce sont des variables liés au terminal et utilisables avec la commande « echo » ou encore « printf ». Elles appliquent un format au texte. Vous pouvez en apprendre plus sur les codes de votre terminal ici Terminal codes (ANSI/VT100) introduction (English).

 

Partie 3, les « Rules » et Variables automatiques :

Cette partie regroupe les règles de compilation pour votre makefile et les variables automatiques que vous n’aurez pas besoin de modifier (ou rarement).

Les Variables automatiques:

Vous avez besoin de ces variables afin d’ajouter à chaque source le répertoire où celui-ci ce trouve. Au lieu de taper le chemin complet pour chaque fichier, votre makefile s’en chargera pour vous. De même vous aurez remarqué que je n’ai pas spécifier le noms des objets car avec une règle, je modifie l’extension .c en .o (ou disons que je copie ce qui est avant l’extension pour y ajouter après un « .o »).

Idem avec mes dossiers d’includes. J’ajoute avant un « -I » qui permet au makefile de specifier que ce sont des dossiers include où chercher les pré-processeurs (très utile pour l’étape de pré-compilation).

Différentes manière de modifier vos variables existent:

OBJP =		$(addprefix $(OBJDIR)/, $(SRC:.c=.o))

« Addprefix » ajoute $(OBJDIR)/ avant chaque $(SRC:.c=.o), séparé par un espace ou un « \ » qui représente un retour à la ligne. Ici cas un peu spécifique cette règle modifie en même temps le nom SRC pour remplacer la fin « .c » par « .o ». Comme ca on appelle les fichier objets sans avoir à tout réécrire.

INCS_DIRS = $(addsuffix /, $(INCDIR))

« Addsuffix » fonctionne de la même façon mais ajoute l’élément après la variable : le second paramètre. NB: la règle n’est pas présente dans le Makefile fourni en exemple.

INCP =		$(foreach dir, $(INCDIR), -I$(dir))

« Foreach » n’est qu’une autre façon de modifier vos variables. Cette fois-ci vous pouvez ajouter du texte avant et/ou après votre variable créé : « dir » qui est défini comme étant  $(INCDIR).

Avec ces différents exemples vous serez en mesure de comprendre le reste du code. Ainsi les règles « OBJP », « LIBP » ou « INCP » associent simplement chaque fichiers à leur répertoire. Ce sont en réalité ces variables qui sont appelées dans les règles du Makefile.

Les Rules:

Viennent ensuite les règles d’exécution. Je vais honteusement copier les propos d’un de mes camarades d’école.

DEFRULE =	all

default:
	@make $(DEFRULE)

all: mlibxcomp libftcomp $(OBJDIR) $(NAME)

« All » se contente de tout exécuter. C’est la règle qui est lancée si on tape « make » sans paramètres. Cette règle renvoie à « mlibxcomp » « libftcomp » « $(OBJDIR) » « $(NAME) » qui appelle avant son exécution d’autres règles (ce qui est après les deux points est évalué avant de traiter la règle).

J’ai aussi rajouter en tout premier une règle « default » avec sa variable « $(DEFRULE) » qui permet de définir la règle à exécuter si aucune n’est spécifiée.

libftcomp:
	@make all -C libft/

mlibxcomp:
	@make all -C minilibx/

« libftcomp » et « mlibxcomp » rentrent dans le dossier de leur lib grace l’option -C et exécute make all. Un « cd libft/ && make all » permettrait aussi d’exécuter le Makefile dans le dossier spécifié, mais reste un peu moins performant que l’option -C.

Ces règles permettent au préalable de compiler les librairies qui seront nécessaire à la compilation de notre programme.

$(OBJDIR)/%.o: $(SRCDIR)/%.c
	@$(CC) $(CFLAGS) $(ADDFLAGS) -c -o $@ $^ $(INCP)

$(OBJDIR):
	@mkdir -p $(OBJDIR)

Retour à notre règle Name, nous demandons au compilateur de créer l’exécutable avec les objets, mais auparavant, nous spécifions bien que ces objets doivent être à jour dans le dossier $(OBJP): qui renvoie à ces règles : $(OBJDIR) & $(OBJDIR)/%.o: $(SRCDIR)/%.c – du moins c’est ce que j’ai pu comprendre.

$(OBJDIR), va d’abord créer le dossier des objets et ensuite compiler chacun des objets ; la ligne va créer une règle par fichier .o et chacun de ces fichiers .o va tester le fichier .c correspondant pour voir si le fichier .o est à jour. On n’oublie pas de spécifier le dossier des fichiers .h.

Un peu plus d’explication vis à vis des symboles est fourni dans la partie suivante.

$(NAME): $(OBJP)
	@$(CC) $(CFLAGS) $(ADDFLAGS) -o $@ $^ $(INCP) $(MLIBX) $(LIBFT)

« $(NAME) » – prenant la valeur de notre programme – est alors exécuté et permet de faire le linkage entre les fichiers .o compilés précédemment et les librairie utilisées, afin de créer l’executable « rtv1.out » / « rtv1.exe ».

clean:
	@rm -rf $(OBJDIR)
	@make clean -C minilibx/
	@make clean -C libft/

« clean » supprime les objets.

fclean: clean
	rm -f $(NAME)

« fclean » qui va d’abord lancer « clean » avant de supprimer l’exécutable :

re: fclean all

« re » recompile le projet de zéro.

 

Partie 4, Bonus :

On peut ajouter la règle « .PHONY » Celle-ci empêche le Makefile de confondre un fichier et une règle, si ils ont le même nom.

Et « .SILENT » qui rend silencieux le makefile permettant alors d’utiliser un affichage personnalisé pour votre makefile. Ou il est possible d’ajouter un « @ » avant une ligne pour la rendre silencieuse. J’utilise ainsi la commande « echo » pour afficher mon propre texte de compilation avec les macros SPECIAL CHARS qu’il y avait plus haut.

$(NAME): build $(OBJS)
	echo -e "$(LOG_CLEAR)$(NAME)....... $(LOG_YELLOW)assembling...$(LOG_NOCOLOR)$(LOG_UP)"
	$(CC) $(CFLAGS) -o $(NAME) $(OBJS) $(LDFLAGS)
	echo -e "$(LOG_CLEAR)$(NAME)....... compiled $(LOG_GREEN)✓$(LOG_NOCOLOR)"

J’ai aussi créé mes propres « Extra Rules » pour automatiser des processus, puisque votre Makefile comprend très bien les commandes shell.

Pour plus d’informations sur les Makefiles n’hésitez pas à lire la doc et consulter cette page Wikipédia. Elles expliquent notamment les macros interne. Par exemple :

$(NAME): $(OBJ)
    $(CC) $(LDFLAGS) $(LDLIBS) $^ -o $@

Le $^ représente tous ce qui est après le : (c’est donc un raccourci pour $(OBJ)), tandis que le $@ représente le nom de la règle ici $(NAME).

$(OBJ_PATH)%.o: $(SRC_PATH)%.c
    @mkdir $(OBJ_PATH) 2> /dev/null || true
    $(CC) $(CFLAGS) $(CPPFLAGS) -o $@ -c $<

Le 2> /dev/null || true est là pour qu’aucun message intempestif (du genre « le dossier existe déjà ») n’apparaisse. Tandis que la dernière variable $< est le nom de la dépendance (le .c).

 

Voilà c’est tout pour les Makefile, mais vous pouvez faire bien plus. Il vous suffit maintenant de créé un alias pour votre terminal (par exemple dans le fichier .bashrc) :

Alias Makefile
alias makefile="cp ~/Makefile ./"

La prochaine fois, j’essaierai de m’attaquer aux Makefile génériques, qui permettent notamment de configurer plusieurs modes de releases ou de gérer la construction de plusieurs binaires. Disons une sorte de CMake fait maison 😀 (il faudra attendre encore un peu!)

 

Update [11/15/2016]

Le tutoriel a été mis à jour pour prendre en compte une compilation d’objet plus performante, corriger quelques erreurs et surtout permettre l’ajout de librairies plus facilement.

 

À propos de la règle PHONY: Je me rends compte après tout ce temps qu’un certain mysticisme (rite vaudou ou je ne sais quel sorcellerie) continue de perdurer chez un bon nombre de personnes à propos de cette « règle ».

En effet sur certains tuto la règle .PHONY inclura toutes les variables du Makefile faisant ainsi re-linker le Makefile alors qu’il ne devrait pas. PHONY n’est pas une règle à proprement parlé mais plutôt une liste de cibles pour votre Makefile. Ces cibles y sont généralement ajoutés pour éviter un conflit dans les règles de deletion tels que clean et fclean. Si un fichier « clean » ou « fclean » devait exister dans le dossier à nettoyer votre règle risque alors de se considérer à jour et ne s’exécuterait pas.

De manières assez simple et sans compliqué les choses, PHONY peut ainsi être utilisé pour forcer l’exécution de ces règles.

.PHONY
.PHONY: fclean clean glu

clean:
	@rm -rf $(OBJDIR)
	@make clean -C minilibx/
	@make clean -C libft/

fclean:
	@make fclean -C libft/
	@rm -rf $(OBJDIR)

glu: re clean

 

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *