Utiliser make pour compiler ses documents LaTeX

Dans mon précédent article, nous avons parlé du fonctionnement de make et des Makefile. Comme promis, voici le premier exemple d’utilisation : la construction de documents PDF à partir de fichiers sources LaTeX, de fichiers SVG et d’images matricielles.

Nous allons avancer progressivement tout au long de cet article, ainsi nous commencerons avec un Makefile des plus basique pour l’améliorer au fur et à mesure.

Avant de commencer

Vous trouverez des fichiers d’exemples pour chacune des étapes dans cette archive. Les corrections sont disponibles dans le répertoire Makefile/.

Faites attention aux copiés/collés : les tabulations sont transformées en espaces dans les blocs de code de cet article. Or make se sert des tabulations pour déterminer les actions relatives aux cibles. Lors de l’exécution de make, vous aurez l’erreur Makefile:23: *** missing separator. Stop., il suffira de revoir votre indentation.

Permier pas, simple mais pas efficace

Notre base de départ est simple, elle permet de compiler un document appelé document.tex dans le répertoire courant. Voici le code de Makefile:

LC = lualatex
LCFLAGS = --interaction=nonstopmode

default: document.pdf

document.pdf: document.tex
    $(LC) $(LCFLAGS) $<

.PHONY: clean
clean:
    @rm document.aux document.log documents.pdf

Nous utilisons ce qui se fait déjà pour la compilation de programme en C, à savoir placer le compilateur et ses paramètres dans des variables. Nous avons donc $(LC) pour LaTeX Compiler — j’utilise LuaLatex — et $(LCFLAGS) pour les paramètres.

La cible default sera lancée automatiquement si aucune autre est spécifiée dans la ligne de commande. Ainsi la compilation se lance avec un simple make.

un répertoire pour les compiler tous

À la premièrer compilation, on s’aperçoit vite que plusieurs fichiers apparaissent dans notre répertoire (fichiers aux, log, toc etc.) comme ceci:

$ ls -l
-rw-r--r-- 1  user group    186 Aug 31 13:14 document.aux
-rw-r--r-- 1  user group  19650 Aug 31 13:14 document.log
-rw-r--r-- 1  user group  41023 Aug 31 13:14 document.pdf
-rw-r--r-- 1  user group   1203 Aug 29 23:27 document.tex
-rw-r--r-- 1  user group    191 Aug 29 23:27 Makefile

Notre répertoire devient alors un peu confus, nous allons faire en sorte de placer tous ces fichiers (ainsi que le fichier PDF généré) dans un sous-répertoire.

LC = lualatex
LCFLAGS = --interaction=nonstopmode --output-directory $(OUTPUT)
OUTPUT = build

default: $(OUTPUT)/document.pdf

$(OUTPUT)/document.pdf: document.tex
    $(LC) $(LCFLAGS) $<

.PHONY: clean
clean:
    @rm -rf $(OUTPUT)

Nous définissons la variable OUTPUT qui contient le répertoire dans lequel placer tous les fichiers générés par lualatex. L’option qui va bien est ajoutée à LCFLAGS. OUTPUT est aussi utilisée pour la cible clean, notre nettoyage n’en est que plus simple!

Tous ces fichiers sont maintenant dans le répertoire build/:

$ tree
.
├── build
│   ├── document.aux
│   ├── document.log
│   └── document.pdf
├── document.tex
└── Makefile

2 directories, 5 files

Mais où est document?

Tous nos documents ne s’appellent pas document, leurs noms dépendent souvent du contexte. Rendons donc ce Makefile plus générique grâce aux variables et macros.

LC = lualatex
LCFLAGS = --interaction=nonstopmode --output-directory $(OUTPUT)
OUTPUT = build
DOCUMENTS = $(addprefix $(OUTPUT)/, $(patsubst %.tex,%.pdf,$(wildcard *.tex)))

default: $(DOCUMENTS)

$(OUTPUT)/%.pdf: %.tex
    @mkdir -p $(OUTPUT)
    $(LC) $(LCFLAGS) $<

.PHONY: clean
clean:
    @rm -rf $(OUTPUT)

Notre macro DOCUMENTS est une imbrication de 3 fonctions :

  1. wildcard <motif> pour récupérer une liste de fichiers dans le répertoire courant en fonction d’un motif;
  2. patsubst <motif>,<remplacement>,<chaine> pour substituer une partie de la chaîne dans un chemin, % fait ici office de caractère joker;
  3. addprefix <prefix>, <chaine> pour ajouter un préfixe à chacun des éléments d’une chaîne de caractères. N’oubliez pas, pour make, les éléments d’une chaîne de caractères sont délimités par des espaces.

Elle permet d’obtenir le nom d’une cible en fonction des fichiers tex contenus dans notre répertoire : document.tex deviendra alors build/document.pdf.

$(DOCUMENTS) est maintenant une dépendance de notre cible default. Et chacun de ses élémets appelleront la cible $(OUTPUT)/%.pdf: %.tex qui se charge de contruite un fichier PDF en fonction de sa source LaTeX.

Cette solution nous permet d’appeler notre source LaTeX comme bon nous semble. Mieux encore nous pouvons avoir plusieurs fichiers à la racine de notre répertoire, ils seront tous compilés indépendamment. C’est très pratique lorsque vous avez un rapport et la présentation dans un même dépôt par exemple.

Les images matricielles

Il n’est pas rare qu’un document contienne des images matricielles : des photos JPEG ou des captures d’écran PNG par exemple. Il est alors nécessaire de prendre en compte la modification de ces images pour la compilation de nos documents.

LC = lualatex
LCFLAGS = --interaction=nonstopmode --output-directory $(OUTPUT)
OUTPUT = build
IMAGES_DIR = images/bitmap

DOCUMENTS = $(addprefix $(OUTPUT)/, $(patsubst %.tex,%.pdf,$(wildcard *.tex)))
IMAGES = $(wildcard $(IMAGES_DIR)/*.*)

default: $(DOCUMENTS)

$(OUTPUT)/%.pdf: %.tex $(IMAGES)
    @mkdir -p $(OUTPUT)
    $(LC) $(LCFLAGS) $<

.PHONY: clean
clean:
    @rm -rf $(OUTPUT)

Nous avons maintenant une variable IMAGES_DIR qui nous permet de spécifier le répertoire dans lequel sont stockées les images — nous verrons plus tard que d’autres sous-dossiers d’images apparaîtront. La liste des images est récupérée grâce à la macro IMAGES qui utilise la fonction wildcard pour récupérer tous les fichiers de ce répertoire.

Le résultat de cette macro est donné en dépendance de notre cible permettant la construction des documents : $(OUTPUT)/%.pdf: %.tex. Ainsi si une image est modifiée ou ajoutée, alors la compilation du fichier PDF sera relancée.

Notre technique a cependant deux défauts.

D’abord, si nous ajoutons une image sans l’utiliser dans notre document et nous relançons la compilation alors notre document sera tout de même recompilé sans que se soit nécessaire. make ne fait pas d’analyse sytaxique, il n’est pas capable d’analyser le fichier source pour determiner s’il doit être compilé ou non.

Ensuite prenons l’exemple d’un dossier contenant trois fichiers LaTeX A, B, et C. Si une image utilisée seulement dans le document B est modifiée, A et C seront tout de même recompilés.

Dans l’exemple de code fourni (dans le répertoire 4_images), vous trouverex un script (change_image.sh) qui se charge de changer l’image du document presentation.tex. Vous pouvez tester la compilation avant et après la modification du Makefile et observer les actions effectuées.

Ce scrits se lance sans paramètres :

$ ./change_image.sh
Flip images ...
Done!

Les images SVG

J’utilise beaucoup Inkscape pour produire diverses images. Mais le format SVG n’est pas utilisable tel quel en LaTeX1. Il est nécesssaire de passer par une étape intermédiaire et le transformer en PDF.

LC = lualatex
LCFLAGS = --interaction=nonstopmode --output-directory $(OUTPUT)
SC = inkscape
SCFLAGS = --export-type=pdf --export-pdf-version=1.4

OUTPUT = build
IMAGES_DIR = images/bitmap
SVG_DIR = images/svg
SVG_EXPORTED_DIR = images/generated

DOCUMENTS = $(addprefix $(OUTPUT)/, $(patsubst %.tex,%.pdf,$(wildcard *.tex)))
IMAGES = $(wildcard $(IMAGES_DIR)/*.*)
SVG = $(wildcard $(SVG_DIR)/*.svg)

SVG_EXPORTED = $(subst $(SVG_DIR), $(SVG_EXPORTED_DIR), $(patsubst %.svg,%.pdf,$(SVG)))

default: $(DOCUMENTS)

$(OUTPUT)/%.pdf: %.tex $(IMAGES) $(SVG_EXPORTED)
    @mkdir -p $(OUTPUT)
    $(LC) $(LCFLAGS) $<

$(SVG_EXPORTED_DIR)/%.pdf : $(SVG_DIR)/%.svg
    $(SC) $(SCFLAGS) -o $@ $<

.PHONY: clean 
clean: 
    @rm -rf $(OUTPUT

Le principe est simple : nous utilisons Inkscape pour exporter les fichiers au format PDF. Pourquoi exporter en PDF? C’est un format bien supporté par les moteurs LaTeX. Il permet aussi de conserver au maximum le format vectoriel2.

Quatres variables font leur apparition :

  • SC pour SVG compiler, nous utilisons Inkscape;
  • SCFLAGS qui contient les paramètres de la commande Inkscape;
  • SVG_DIR qui contient le chemin vers les fichiers SVG;
  • SVG_EXPORTED_DIR qui contient le chemin vers les fichiers PDF exportés depuis Inkscape.

Et deux deux macros :

  • SVG liste les fichiers SVG via la fonction wildcard;
  • SVG_EXPORTED transforme la liste contenu dansSVG en liste de fichiers PDF. Deux fonctions imbriquées sont nécessaires :subst qui permet de remplacer un motif dans des chaînes et patsubst que nous avons vu précédemment. Cette macro est donnée en dépendance de la cible de compilation des documents.

Ensuite la cible qui nous permet de convertir les fichiers :
$(SVG_EXPORTED_DIR)/%.pdf: $(SVG_DIR)/%.svg.

Les cibles accessoires

Nous avons vu jusqu’ici les cibles principales, ajoutons maintenant deux cibles qui pourront nous faciliter la vie.

Afficher des informations

Notre Makefile est maintenant conséquent, il contient quelques macros qu’il est parfois utile d’afficher. Cet affichage nous permettra par exemple de vérifier les images matricielles prises en compte ou encore les fichiers SVG qui seront exportés.

LC = lualatex
LCFLAGS = --interaction=nonstopmode --output-directory $(OUTPUT)
SC = inkscape
SCFLAGS = --export-type=pdf --export-pdf-version=1.4

OUTPUT = build
IMAGES_DIR = images/bitmap
SVG_DIR = images/svg
SVG_EXPORTED_DIR = images/generated

DOCUMENTS = $(addprefix $(OUTPUT)/, $(patsubst %.tex,%.pdf,$(wildcard *.tex)))
IMAGES = $(wildcard $(IMAGES_DIR)/*.*)
SVG = $(wildcard $(SVG_DIR)/*.svg)

SVG_EXPORTED = $(subst $(SVG_DIR),$(SVG_EXPORTED_DIR),$(patsubst %.svg,%.pdf,$(SVG)))

default: $(DOCUMENTS)

$(OUTPUT)/%.pdf: %.tex $(IMAGES) $(SVG_EXPORTED)
    @mkdir -p $(OUTPUT)
    $(LC) $(LCFLAGS) $<

$(SVG_EXPORTED_DIR)/%.pdf : $(SVG_DIR)/%.svg
    $(SC) $(SCFLAGS) -o $@ $<

.PHONY: clean 
clean: 
    @rm -rf $(OUTPUT)

.PHONY: info
info:
    @echo "document.............'$(DOCUMENTS)'"
    @echo "bitmap images........'$(IMAGES)'"
    @echo "SVG images...........'$(SVG)'"
    @echo "exported SVG images..'$(SVG_EXPORTED)'"

Nous n’utilisons pas les commandes relatives aux messages (info, warning et error) que nous avons vu dans le précédent article, sinon un message make: Nothing to be done for 'info'. apparaît après les informations.

Voici le résultat de cette cible:

$ make info
document.............'build/presentation.pdf'
bitmap images........'images/bitmap/ferret.jpg'
SVG images...........'images/svg/souris.svg'
exported SVG images..'images/generated/souris.pdf'

Lancer l’application de visualisation des PDF

Lancer la compilation d’un document et l’afficher ensuite dans notre visionneur de documents permet souvent de gagner du temps. Partons du principe que nous utilisons Zathura comme visionneur de documents.

LC = lualatex
LCFLAGS = --interaction=nonstopmode --output-directory $(OUTPUT)
SC = inkscape
SCFLAGS = --export-type=pdf --export-pdf-version=1.4

VIEWER = zathura
VIEWER_FLAGS = --fork

OUTPUT = build
IMAGES_DIR = images/bitmap
SVG_DIR = images/svg
SVG_EXPORTED_DIR = images/generated

DOCUMENTS = $(addprefix $(OUTPUT)/, $(patsubst %.tex,%.pdf,$(wildcard *.tex)))
IMAGES = $(wildcard $(IMAGES_DIR)/*.*)
SVG = $(wildcard $(SVG_DIR)/*.svg)

SVG_EXPORTED = $(subst $(SVG_DIR),$(SVG_EXPORTED_DIR),$(patsubst %.svg,%.pdf,$(SVG)))

default: $(DOCUMENTS)

$(OUTPUT)/%.pdf: %.tex $(IMAGES) $(SVG_EXPORTED)
    @mkdir -p $(OUTPUT)
    $(LC) $(LCFLAGS) $<

$(SVG_EXPORTED_DIR)/%.pdf : $(SVG_DIR)/%.svg
    $(SC) $(SCFLAGS) -o $@ $<

.PHONY: clean 
clean: 
    @rm -rf $(OUTPUT)

.PHONY: info
info:
    @echo "document.............'$(DOCUMENTS)'"
    @echo "bitmap images........'$(IMAGES)'"
    @echo "SVG images...........'$(SVG)'"
    @echo "exported SVG images..'$(SVG_EXPORTED)'"

.PHONY: view
view: default
    @$(VIEWER) $(VIEWER_FLAGS) $(DOCUMENTS)

Deux nouvelles variables font leur apparition :

  • VIEWER qui contient le nom du programme utilisé comme visionneur
  • VIEWER_FLAGS qui contient les options de notre visionneur

Nous avons aussi une nouvelle cible view, comme elle ne réalise aucune création de fichiers nous la déclarons comme cible .PHONY. La seule dépendance de cette cible est default : ainsi lors de l’appel de view, notre document sera recompilé si nécessaire.

En conclusion

Partant d’un Makefile standard, nous l’avons amélioré au fur et à mesure en utilisant les fonctionnalités proposées par make. Bien entendu cet exemple est valable pour les documents simples, il sera alors nécessaire de l’adapter pour prendre en compte les compilations multi-passes pour la gestion des bibliographies par exemple.

De mon côté, j’utilise énormément LaTeX pour les courriers officiels, les rendu des différents TD pour l’Université, mes présentations. J’ai profité de l’écriture de cet article pour améliorer mon processus de compilation de tous mes documents. Au final, chaque dépôt de code contenant des documents prend la forme suivante :

├── images/
│   ├── bitmap/
│   │   └── ...
│   ├── generated/
│   └── svg
│       └── ...
├── Makefile
├── build/
├── README.md
└── document.tex

  1. Ce n’est pas tout à fait vrai, il est possible d’utiliser le package LaTeX svg et sa commande \includesvg mais les contraintes sont nombreuses. 

  2. Sauf dans quelques cas spécifiques comme l’utilisation de certains filtres, mais les éléments concernés seront convertis en matriciels dans le PDF rendu.