Structure des répertoires et chemins de TeX

Sur l'installation TeX Live 2024 qui a servi à rédiger cette page, kpsewhich -expand-path='$TEXINPUTS' affiche 8 798 répertoires, soit environ un demi-million de caractères de chemin. Et pourtant \usepackage{amsmath} se résout instantanément. La raison est simple : LaTeX ne va presque jamais regarder dans ces répertoires. Cette page démonte les deux moitiés de ce tour de force à l'aide de véritables sorties de commandes : la TDS (TeX Directory Structure), la carte de tous les fichiers texmf, et kpathsea, le moteur de recherche qui la parcourt. Quel arbre survit à une mise à niveau et lequel est jeté ? Et pourquoi un fichier déposé dans TEXMFHOME est-il le seul cas qui se passe de mktexlsr ?

La TDS : pourquoi un package est éparpillé dans neuf répertoires

La TDS classe les fichiers par type, non par package : un package ne se trouve donc jamais en un seul endroit. Recensez amsfonts, le package qui fournit amssymb, sur cette installation TeX Live 2024 : il occupe neuf répertoires sous texmf-dist. Les macros sont dans tex/latex/amsfonts/, les sources commentées .dtx dans source/latex/amsfonts/, les manuels dans doc/fonts/amsfonts/, et les polices elles-mêmes se répartissent encore par format entre fonts/tfm/, fonts/type1/, fonts/afm/, fonts/map/ et fonts/source/. La version plain TeX dispose de son propre tex/plain/amsfonts/.

terminal
$ find /usr/local/texlive/2024/texmf-dist -maxdepth 4 -type d -path '*amsfonts*' | sort
/usr/local/texlive/2024/texmf-dist/doc/fonts/amsfonts
/usr/local/texlive/2024/texmf-dist/fonts/afm/public/amsfonts
/usr/local/texlive/2024/texmf-dist/fonts/map/dvips/amsfonts
/usr/local/texlive/2024/texmf-dist/fonts/source/public/amsfonts
/usr/local/texlive/2024/texmf-dist/fonts/tfm/public/amsfonts
/usr/local/texlive/2024/texmf-dist/fonts/type1/public/amsfonts
/usr/local/texlive/2024/texmf-dist/source/latex/amsfonts
/usr/local/texlive/2024/texmf-dist/tex/latex/amsfonts
/usr/local/texlive/2024/texmf-dist/tex/plain/amsfonts

Pourquoi une telle organisation ? La réponse est la portabilité. TeX tourne sous macOS, Unix et Windows, et le CTAN (Comprehensive TeX Archive Network) rassemble des milliers de packages. Si chaque distributeur disposait les fichiers à sa façon, ceux qui publient des packages comme les outils qui les cherchent trébucheraient à chaque fois. La TDS, établie par le TeX Users Group (TUG) dans les années 1990, a rendu un jeu de règles universel : les macros sous tex/, les polices sous fonts/<type>/<fournisseur>/<fonte>/. L'emplacement de n'importe quel fichier se déduit donc des seules règles, sur tout système et toute distribution. Sous tex/ s'ajoute un niveau, tex/<format>/<package>/, où <format> vaut latex, plain, generic, etc.

RépertoireContenuTaille mesurée (texmf-dist, TeX Live 2024)
doc/Manuels des packages — ce qu'ouvre texdoc3,7 Go, dont 10 099 PDF
fonts/Toutes les polices, par format : tfm, vf, type1, opentype, enc, map2,9 Go
tex/Macros, classes, styles (.tex .sty .cls), p. ex. tex/latex/...594 Mo
source/Sources commentées .dtx et scripts d'extraction .ins — l'implémentation, lisible426 Mo
scripts/Scripts exécutables indépendants du système (le corps de mktexlsr, latexmk, …)133 Mo
bibtex/Bases bibliographiques bib/ et styles bst/26 Mo
web2c/Configuration des moteurs ; siège de texmf.cnf et de la liste fmtutil.cnf248 Ko

Si une ligne de ce tableau surprend, c'est celle-ci : la documentation pèse plus lourd que le logiciel. Sur les 7,9 Go de texmf-dist, doc/ en occupe 3,7 tandis que les macros de tex/ n'en font que 594 Mo. C'est pour cela que l'installateur de TeX Live propose de ne pas installer la documentation, et que les images Docker se déclinent avec et sans -doc. Connaître cette disposition sert aussi : quand un package se comporte de façon inexplicable, on va lire directement source/latex/<package>/*.dtx, et le manuel qu'ouvre texdoc est un vrai fichier posé dans doc/.

Quel arbre survit à une mise à niveau

Seul texmf-dist est remplacé en bloc. TeX Live crée un répertoire par année — /usr/local/texlive/2024 — et y place la distribution proprement dite, texmf-dist. L'année suivante, un 2025 apparaît à côté et texmf-dist est échangé contre une copie neuve. Ajouter ses propres fichiers dans la distribution est donc suicidaire ; en contrepartie, tout ce qui se trouve hors du répertoire de l'année reste intact. Que TEXMFLOCAL soit à /usr/local/texlive/texmf-local, en dehors de 2024, n'a rien d'un hasard : c'est précisément la conception retenue. TEXMFHOME est encore plus à l'écart, dans le répertoire personnel.

terminal
# Never guess these paths - ask. Values below: TeX Live 2024 on macOS.
$ kpsewhich -var-value=TEXMFROOT
/usr/local/texlive/2024
$ kpsewhich -var-value=TEXMFLOCAL      # note: OUTSIDE the year directory
/usr/local/texlive/texmf-local
$ kpsewhich -var-value=TEXMFHOME      # ~/texmf on Linux, ~/Library/texmf on macOS
/Users/you/Library/texmf
$ kpsewhich -var-value=TEXMFVAR
/Users/you/Library/texlive/2024/texmf-var
VariableRôleCe qu'une mise à niveau lui fait
TEXMFDISTLa distribution elle-même ; des milliers de packages. Ne jamais modifier à la mainRemplacée en bloc. Tout ajout disparaît
TEXMFLOCALAjouts valables pour toute la machine, partagés par tousConservé, car situé hors du répertoire de l'année
TEXMFHOMEL'arbre personnel ; vos classes et le style d'une revue vont iciConservé ; il est dans le répertoire personnel, intouché
TEXMFVARCache généré automatiquement : formats, font maps, caches LuaTeXReconstruit chaque année ; le supprimer force sa régénération
TEXMFCONFIGDépôt de configuration par utilisateur, écrit par updmap et fmtutilConservé, mais rangé sous un répertoire annuel
TEXMFSYSVARÉquivalent système de VAR / CONFIG, écrit par les commandes en -sysTEXMFSYSCONFIG se comporte pareil ; les deux sont dans le répertoire de l'année
TEXMFROOTRacine de toute l'installation, /usr/local/texlive/2024Une nouvelle année, c'est un tout autre répertoire

Quand un fichier du même nom existe dans plusieurs arbres, lequel l'emporte ? Cela se règle par une seule variable, TEXMF, dont la valeur n'est rien d'autre que la priorité de recherche écrite dans l'ordre. Sur ce TeX Live 2024 elle se lit comme ci-dessous : le plus à gauche gagne, donc d'abord votre configuration et vos caches, puis l'arbre personnel TEXMFHOME, ensuite le TEXMFLOCAL de la machine, et la distribution TEXMFDIST en dernier. Autrement dit, déposer mystyle.sty dans TEXMFHOME masque le fichier homonyme de la distribution — non par écrasement, mais par l'ordre naturel personnel → site → distribution. Les marques !! devant certaines entrées font l'objet de la section suivante.

terminal
$ kpsewhich -var-value=TEXMF
{{}/Users/you/Library/texlive/2024/texmf-config,
 /Users/you/Library/texlive/2024/texmf-var,
 /Users/you/Library/texmf,
 !!/usr/local/texlive/texmf-local,
 !!/usr/local/texlive/2024/texmf-config,
 !!/usr/local/texlive/2024/texmf-var,
 !!/usr/local/texlive/2024/texmf-dist}

# Note which entries carry "!!" - and which do not.

Peut-on supprimer texmf-var ?

Tout ce qu'il contient est engendré : en principe, rien ne se perd en le supprimant. Avant d'entonner « supprime-le et ça se répare », il vaut pourtant la peine de regarder ce qui s'y trouve. Sur ce TeX Live 2024, le texmf-var système pèse 259 Mo, dont 233 Mo pour web2c/ qui abrite 53 fichiers .fmtpdflatex.fmt à lui seul fait 7,8 Mo. Un fichier de format est une image mémoire en conserve, qui évite de relire latex.ltx et les classes à chaque exécution. Le texmf-var utilisateur est plus gros encore, 293 Mo, dont 257 Mo de luatex-cache/, résultat de l'analyse des polices par LuaTeX. Le psfonts.map écrit par updmap s'y trouve aussi.

terminal
$ du -sh /usr/local/texlive/2024/texmf-var/*
4.0K    ls-R
 36K    tex
 26M    fonts
233M    web2c          # 53 .fmt files; pdflatex.fmt alone is 7.8 MB

$ du -sh "$(kpsewhich -var-value=TEXMFVAR)"/*
 32K    fonts
2.1M    texdoc
 12M    web2c
 22M    luatexja
257M    luatex-cache   # LuaTeX font analysis, rebuilt on demand

Il en découle une règle pratique. Quand un fichier de format périmé fait dérailler quelque chose, on le reconstruit avec fmtutil-sys --all plutôt que de supprimer le répertoire. Effacer l'arbre entier vise des cas plus étroits : un cache de polices LuaTeX corrompu qui fait sortir à luaotfload des erreurs incompréhensibles, par exemple. Le prix n'est qu'une première compilation lente ensuite ; mais ne confondez pas texmf-var et texmf-config : emporter le second, c'est perdre aussi les réglages d'updmap. Les commandes de régénération elles-mêmes relèvent de la page sur la gestion des packages et des polices.

Comment kpathsea trouve réellement un fichier

La recherche est assurée par une bibliothèque partagée nommée kpathsea (kpath search). Ni pdftex, ni xetex, ni luatex, ni dvipdfmx, ni bibtex ne cherchent par eux-mêmes : tous demandent à kpathsea « où est amsmath.sty ? ». Ce que kpathsea reçoit est une seule chaîne contenant des règles. Trois symboles méritent d'être retenus : $VAR développe une variable, un // final signifie « tout ce qui est en dessous, récursivement », et un !! initial signifie « ne pas parcourir le disque — consulter uniquement la base de noms de fichiers décrite à la section suivante ». Affichez TEXINPUTS, le chemin de recherche des sources LaTeX, et les trois apparaissent d'un coup.

terminal
$ kpsewhich -progname=pdflatex -var-value=TEXINPUTS
.:{...the TEXMF list...}/tex/{latex,generic,}//

# The same query, run as a different program:
$ kpsewhich -progname=pdflatex-dev -var-value=TEXINPUTS
.:{...the TEXMF list...}/tex/{latex-dev,latex,generic,}//

# How many real directories does that string stand for?
$ kpsewhich -progname=pdflatex -expand-path='$TEXINPUTS' | tr : '\n' | wc -l
    8798

Voici comment le lire : d'abord . (le répertoire du manuscrit), sinon parcourir récursivement la branche tex/ de chaque arbre texmf dans l'ordre latexgeneric → tout le reste. Qu'un fichier voisin du manuscrit l'emporte correspond exactement à l'intuition — et c'est aussi là que se niche le piège de cette section. L'autre point remarquable est que le premier élément de {latex,generic,} change avec le nom du programme en cours. Appelé sous le nom pdflatex-dev, il devient {latex-dev,latex,generic,} : l'arbre de développement est consulté en premier. kpathsea répond différemment selon qui demande. Et les 8 798 rapportés par -expand-path sont aussi un avertissement : sans l'index ls-R, c'est le nombre de répertoires qu'il faudrait ouvrir à chaque recherche.

ls-R et TEXMFDBS : pourquoi seul TEXMFHOME se passe de mktexlsr

La réponse tient en une ligne : TEXMFDBS, la liste des arbres pourvus d'un index, ne contient pas TEXMFHOME. Ouvrir à chaque fois les 8 798 répertoires de la section précédente est exclu ; kpathsea place donc à la racine de chaque arbre une base de noms de fichiers appelée ls-R et la consulte à la place. Quels arbres possèdent un index, c'est TEXMFDBS qui le dit, et sur ce TeX Live 2024 il en énumère exactement quatre — précisément ceux qui portaient !! dans TEXMF. TEXMFHOME n'y figure pas. Voilà pourquoi TEXMFHOME est parcouru sur le disque à chaque fois, et pourquoi un fichier qu'on y dépose est trouvé sur-le-champ.

terminal
$ kpsewhich -var-value=TEXMFDBS
{!!/usr/local/texlive/texmf-local,
 !!/usr/local/texlive/2024/texmf-config,
 !!/usr/local/texlive/2024/texmf-var,
 !!/usr/local/texlive/2024/texmf-dist}
# TEXMFHOME is absent from this list.

# The experiment: the SAME file, the SAME TDS layout, two different trees.
$ mkdir -p /tmp/t/tex/latex/demo && touch /tmp/t/tex/latex/demo/demo.sty

$ TEXMFHOME=/tmp/t  kpsewhich -progname=pdflatex demo.sty
/tmp/t/tex/latex/demo/demo.sty          # found - no ls-R, no mktexlsr

$ TEXMFLOCAL=/tmp/t kpsewhich -progname=pdflatex demo.sty
$ echo $?
1                                       # NOT found: "!!" means index-only

Le fichier ls-R lui-même est un texte sans apprêt. Sa première ligne est toujours % ls-R -- filename database for kpathsea; do not change this line., puis chaque répertoire est listé avec son contenu. Le texmf-dist/ls-R de cette machine fait 5,2 Mo et 276 953 lignes ; il indexe 228 764 fichiers répartis dans 16 063 répertoires. La commande qui le reconstruit est mktexlsr, et texhash est un lien symbolique vers elle — le même programme sous un second nom. La règle pratique tombe d'elle-même : placez un fichier à la main dans TEXMFLOCAL ou un arbre système et il faut mktexlsr ; placez-le dans TEXMFHOME et ce n'est pas nécessaire. L'expérience ci-dessus en est toute la raison. Le détail des commandes revient à la page sur la gestion des packages et des polices.

kpsewhich --all : débusquer le fichier masqué par une copie plus ancienne

kpsewhich --all NOM affiche toutes les correspondances, dans l'ordre de recherche. Le kpsewhich nu ne renvoie que la première — le fichier qui sera réellement lu — ; pour voir la deuxième et les suivantes, il faut --all. L'accident classique, « il existe deux fichiers de ce nom et c'est le plus ancien qui l'emporte », devient visible en une commande. Même sur un TeX Live 2024 tout neuf, amsmath.sty existe bel et bien en double : la version stable dans tex/latex/amsmath/ et celle de développement dans tex/latex-dev/amsmath/.

terminal
$ kpsewhich --all amsmath.sty
/usr/local/texlive/2024/texmf-dist/tex/latex/amsmath/amsmath.sty
/usr/local/texlive/2024/texmf-dist/tex/latex-dev/amsmath/amsmath.sty

# Same two files, opposite order - because the program name changed the path.
$ kpsewhich --all -progname=pdflatex-dev amsmath.sty
/usr/local/texlive/2024/texmf-dist/tex/latex-dev/amsmath/amsmath.sty
/usr/local/texlive/2024/texmf-dist/tex/latex/amsmath/amsmath.sty

Ces deux exécutions constituent une expérience reproductible sans rien modifier, et elles confirment la règle : le premier résultat l'emporte. En pratique, pourtant, l'endroit où cela mord est presque toujours à côté du manuscrit. Comme TEXINPUTS commence par ., un vieux amsmath.sty ou article.cls égaré dans le dossier du projet il y a des années sera lu avant la copie à jour de la distribution. Pire, la panne prend la forme la plus désagréable qui soit : le document se compile sur votre machine et pas sur celle d'un coauteur. Devant une erreur inexplicable — ! LaTeX Error: Command \... already defined. et consorts — ou un désaccord entre deux machines, tapez d'abord kpsewhich --all. C'est le chemin le plus court vers la réponse.

Utiliser kpsewhich : -var-value contre -expand-path

-var-value montre ce que dit la configuration ; -expand-path montre ce qui se trouve réellement sur le disque. C'est cet écart qui les rend utiles au diagnostic. Affichez TEXMF des deux façons sur ce TeX Live 2024 : -var-value énumère sept arbres, marques !! comprises, alors que -expand-path n'en rend que cinq. Les deux disparus — ~/Library/texlive/2024/texmf-config et ~/Library/texmfn'ont tout simplement pas encore été créés. Si un arbre figure dans la configuration mais pas dans le développement, c'est que le répertoire n'existe pas. Quand un fichier que vous croyez avoir déposé dans TEXMFHOME reste introuvable, c'est le premier soupçon.

CommandeCe qu'elle répondQuand y recourir
kpsewhich NAMELa première correspondance — le fichier réellement luCommencer par là : vérifier que c'est bien ce fichier
kpsewhich --all NAMEToutes les correspondances, dans l'ordre de recherchePour voir si une copie plus ancienne la masque
kpsewhich -var-value=TEXMFHOMELa valeur donnée par la configuration, marques !! comprisesPour confirmer sans deviner où un arbre doit se trouver
kpsewhich -expand-path=$TEXMFLe développement restreint aux répertoires qui existent vraimentPour repérer l'écart entre configuration et réalité
kpsewhich -show-path=texLa liste ordonnée des répertoires pour ce type de fichierPour comprendre pourquoi l'ordre est celui-là

texmf.cnf : d'où viennent les valeurs des variables

Toutes les variables rencontrées jusqu'ici — TEXMF, TEXINPUTS, l'emplacement de chaque arbre — sont consignées dans un fichier de configuration nommé texmf.cnf. Avant toute chose, kpathsea le lit et y prend ses paramètres de fonctionnement : chemins de recherche, position des arbres, limites de mémoire, etc. Le point intéressant est qu'il peut exister plus d'un texmf.cnf. kpathsea les lit dans l'ordre le long d'un chemin dédié, TEXMFCNF, et retient pour chaque variable la première définition rencontrée — les fichiers lus ensuite n'écrasent pas les précédents. Sur cette machine, il y en a deux empilés.

terminal
$ kpsewhich -all texmf.cnf
/usr/local/texlive/2024/texmf.cnf                     # TeX Live's thin override, read first
/usr/local/texlive/2024/texmf-dist/web2c/texmf.cnf    # hundreds of lines of defaults

Le texmf.cnf mince du dessus — le fichier de différences qu'écrit TeX Live — est lu en premier, le gros fichier de valeurs par défaut ensuite. Pour changer une valeur durablement, la convention est donc de ne pas modifier le fichier de la distribution mais d'écrire seulement les lignes voulues à un emplacement plus prioritaire. TEXMFLOCAL/web2c/texmf.cnf est cet emplacement. Ainsi les réglages survivent à une montée de version, et quelques lignes suffisent à voir ce qui a été changé. En résumé : texmf.cnf fixe où sont les arbres et à quoi ressemblent les chemins de recherche, puis kpathsea trouve le fichier dans cet ordre, le plus souvent via l'index ls-R. Ces deux couches constituent tout le mécanisme derrière une simple ligne \usepackage{...}.

PATH trouve le programme, kpathsea trouve les fichiers

Ce sont deux mécanismes tout à fait distincts, et les confondre fait dérailler le diagnostic. kpathsea cherche les fichiers que TeX lit.sty, .cls, polices — mais avant cela le shell doit trouver l'exécutable lui-même, pdflatex. C'est l'affaire du système d'exploitation : il parcourt dans l'ordre les répertoires listés dans la variable d'environnement PATH. TeX Live rassemble ses exécutables dans un unique répertoire bin par système et architecture, et sous macOS MacTeX fournit un lien stable indépendant de l'année, /Library/TeX/texbin. Donc pdflatex: command not found n'est pas un problème kpathsea : c'est presque à coup sûr un problème de PATH. À l'inverse, ! LaTeX Error: File 'foo.sty' not found. n'a rien à voir avec PATH. La marche à suivre pour le configurer relève de la page sur l'installation de bureau.

terminal
$ which pdflatex
/Library/TeX/texbin/pdflatex
$ readlink /Library/TeX/texbin
Distributions/Programs/texbin

Où placer son propre fichier .sty

Le personnel dans TEXMFHOME, le partagé au laboratoire dans TEXMFLOCAL, et dans les deux cas on respecte la disposition TDS. Toute la règle est là. Ce qu'il ne faut pas faire, c'est deviner l'emplacement : la valeur par défaut de TEXMFHOME dépend du système — ~/texmf sous Linux, mais ~/Library/texmf pour MacTeX sous macOS. On commence donc toujours par kpsewhich -var-value=TEXMFHOME. À l'inverse, un fichier qui n'appartient qu'à une soumission — un myconf.cls de conférence, un journal.sty de revue — peut rester à côté du manuscrit, puisque TEXINPUTS regarde . en premier. Mais poser un nom générique comme article.cls à côté du manuscrit, c'est fabriquer soi-même l'accident de masquage de la section précédente.

terminal
# Ask for the tree, never hard-code it: this is ~/texmf on Linux,
# ~/Library/texmf on macOS, %USERPROFILE%\texmf on Windows.
HOME_TREE="$(kpsewhich -var-value=TEXMFHOME)"

mkdir -p "$HOME_TREE/tex/latex/thesisstyle"
cp thesisstyle.sty "$HOME_TREE/tex/latex/thesisstyle/"

# Confirm which copy TeX will pick up. No mktexlsr needed for TEXMFHOME.
kpsewhich thesisstyle.sty
kpsewhich --all thesisstyle.sty    # and check nothing else shadows it

Dès que kpsewhich renvoie le chemin attendu, le manuscrit n'a plus besoin que de \usepackage{thesisstyle}. S'il ne renvoie rien, soupçonnez trois choses dans l'ordre. (1) Le fichier est-il sous tex/latex/<package>/ ? TEXINPUTS ne regarde jamais ailleurs que sous tex/. (2) La casse du nom de fichier correspond-elle ? (3) Si vous l'avez posé dans un arbre système, avez-vous lancé mktexlsr ? Vérifier dans cet ordre transforme le symptôme « TeX est cassé » en « où l'ai-je posé sur la carte de recherche ? » — une question qui, elle, a une réponse.