Estructura de directorios y rutas de TeX

En la instalación de TeX Live 2024 sobre la que se escribió esta página, kpsewhich -expand-path='$TEXINPUTS' imprime 8.798 directorios, unos quinientos mil caracteres de ruta. Y aun así \usepackage{amsmath} se resuelve al instante. El motivo es sencillo: LaTeX casi nunca mira dentro de esos directorios. Esta página desarma las dos mitades de ese truco con salidas de comandos reales: la TDS (TeX Directory Structure), el mapa de dónde vive cada archivo texmf, y kpathsea, el motor de búsqueda que lo recorre. ¿Qué árbol sobrevive a una actualización y cuál se descarta? ¿Y por qué un archivo colocado en TEXMFHOME es el único caso que no necesita mktexlsr?

La TDS: por qué un paquete queda repartido en nueve directorios

La TDS ordena los archivos por tipo, no por paquete, así que un paquete nunca queda en un único sitio. Cuente amsfonts, el paquete que provee amssymb, en esta instalación de TeX Live 2024: ocupa nueve directorios bajo texmf-dist. Las macros están en tex/latex/amsfonts/, el fuente comentado .dtx en source/latex/amsfonts/, los manuales en doc/fonts/amsfonts/, y las fuentes mismas se reparten aún más por formato entre fonts/tfm/, fonts/type1/, fonts/afm/, fonts/map/ y fonts/source/. La versión plain TeX tiene su propio 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

¿Por qué organizarlo así? La respuesta es la portabilidad. TeX se ejecuta en macOS, Unix y Windows, y CTAN (Comprehensive TeX Archive Network) reúne miles de paquetes. Si cada distribuidor colocara los archivos a su manera, tanto quienes publican paquetes como las herramientas que los buscan tropezarían cada vez. La TDS, redactada por el TeX Users Group (TUG) en los años noventa, hizo universal un solo conjunto de reglas: las macros bajo tex/, las fuentes bajo fonts/<tipo>/<proveedor>/<tipografía>/. Así la ubicación de cualquier archivo se deduce solo a partir de las reglas, en cualquier sistema y cualquier distribución. Bajo tex/ hay un nivel más, tex/<formato>/<paquete>/, donde <formato> es latex, plain, generic, etcétera.

DirectorioContenidoTamaño medido (texmf-dist, TeX Live 2024)
doc/Manuales de paquetes: lo que abre texdoc3,7 GB, solo en PDF 10.099 archivos
fonts/Todas las fuentes, por formato: tfm, vf, type1, opentype, enc, map2,9 GB
tex/Macros, clases, estilos (.tex .sty .cls), p. ej. tex/latex/...594 MB
source/Fuentes comentadas .dtx y sus scripts de extracción .ins: la implementación, legible426 MB
scripts/Scripts ejecutables independientes del sistema (el cuerpo de mktexlsr, latexmk, …)133 MB
bibtex/Bases bibliográficas bib/ y estilos bst/26 MB
web2c/Configuración de los motores; sede de texmf.cnf y de la lista fmtutil.cnf248 KB

Si alguna fila de la tabla sorprende, es esta: la documentación pesa más que el propio software. De los 7,9 GB de texmf-dist, doc/ se lleva 3,7 GB, mientras que las macros de tex/ apenas suman 594 MB. Por eso el instalador de TeX Live ofrece omitir la documentación, y por eso las imágenes Docker se dividen en variantes con y sin -doc. Conocer la disposición también sirve: cuando un paquete se comporta de forma inexplicable, se va a leer directamente source/latex/<paquete>/*.dtx, y el manual que abre texdoc es un archivo real alojado en doc/.

Qué árbol sobrevive a una actualización

Solo texmf-dist se reemplaza por completo. TeX Live crea un directorio por año —/usr/local/texlive/2024— y coloca dentro la distribución propiamente dicha, texmf-dist. Al año siguiente aparece un 2025 al lado y texmf-dist se sustituye por una copia nueva. Añadir archivos propios a la distribución es, por tanto, suicida; en contrapartida, todo lo que está fuera del directorio del año queda intacto. Que TEXMFLOCAL se encuentre en /usr/local/texlive/texmf-local, fuera de 2024, no es casualidad: es exactamente ese el diseño. TEXMFHOME está aún más afuera, dentro del directorio personal.

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
VariableFunciónQué le hace una actualización
TEXMFDISTLa distribución misma; miles de paquetes. No editar a manoSe reemplaza por completo. Lo añadido desaparece
TEXMFLOCALAñadidos para toda la máquina, compartidos por todosSe conserva, por estar fuera del directorio del año
TEXMFHOMEEl árbol personal; sus clases propias y el estilo de una revista van aquíSe conserva; está en el directorio personal y no se toca
TEXMFVARCaché generado automáticamente: formatos, font maps, cachés de LuaTeXSe reconstruye cada año; borrarlo solo fuerza su regeneración
TEXMFCONFIGAlmacén de configuración por usuario, escrito por updmap y fmtutilSe conserva, pero vive bajo un directorio anual
TEXMFSYSVARContraparte de todo el sistema de VAR / CONFIG, escrita por los comandos -sysTEXMFSYSCONFIG funciona igual; ambos están dentro del directorio del año
TEXMFROOTRaíz de toda la instalación, /usr/local/texlive/2024Un año nuevo significa un directorio distinto

Cuando un archivo del mismo nombre existe en varios árboles, ¿cuál gana? Lo decide una sola variable, TEXMF, cuyo valor no es más que la prioridad de búsqueda escrita en orden. En este TeX Live 2024 se lee como abajo: gana el de más a la izquierda, así que primero su configuración y sus cachés, después el árbol personal TEXMFHOME, luego el TEXMFLOCAL de la máquina y, en último lugar, la distribución TEXMFDIST. Dicho de otro modo, poner mystyle.sty en TEXMFHOME tapa el archivo homónimo de la distribución: no por sobrescritura, sino por el orden natural personal → sitio → distribución. Las marcas !! delante de algunas entradas se explican en la sección siguiente.

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.

¿Se puede borrar texmf-var?

Todo lo que contiene está generado, así que en principio nada se pierde al borrarlo. Antes de entonar «bórralo y se arregla», conviene sin embargo ver qué hay realmente ahí. En este TeX Live 2024 el texmf-var del sistema suma 259 MB, de los cuales 233 MB corresponden a web2c/, que alberga 53 archivos .fmt; solo pdflatex.fmt ocupa 7,8 MB. Un archivo de formato es una imagen de memoria enlatada que evita releer latex.ltx y las clases en cada ejecución. El texmf-var del usuario es aún mayor, 293 MB, de los cuales 257 MB son luatex-cache/, el resultado de que LuaTeX analice las fuentes. El psfonts.map que escribe updmap también vive aquí.

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

De ahí se sigue una regla práctica. Cuando un archivo de formato caducado hace que algo se comporte raro, se reconstruye con fmtutil-sys --all en lugar de borrar el directorio. Borrar el árbol entero vale para casos más acotados: una caché de fuentes de LuaTeX corrompida que hace que luaotfload emita errores extraños, por ejemplo. El precio es solo una primera compilación lenta después; pero no confunda texmf-var con texmf-config: llevarse el segundo por delante supone perder también los ajustes de updmap. Los comandos de regeneración en sí los cubre la página de gestión de paquetes y fuentes.

Cómo encuentra kpathsea un archivo

De la búsqueda se encarga una biblioteca compartida llamada kpathsea (kpath search). Ni pdftex, ni xetex, ni luatex, ni dvipdfmx, ni bibtex buscan por su cuenta: todos preguntan a kpathsea «¿dónde está amsmath.sty?». Lo que kpathsea recibe es una sola cadena con reglas dentro. Vale la pena aprender tres símbolos: $VAR expande una variable, un // final significa «todo lo que hay debajo, recursivamente», y un !! inicial significa «no recorras el disco: consulta solo la base de nombres de archivo que describe la sección siguiente». Imprima TEXINPUTS, la ruta con la que se buscan los fuentes LaTeX, y los tres aparecen a la vez.

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

Se lee así: primero . (el directorio del manuscrito) y, si no, recorrer recursivamente la rama tex/ de cada árbol texmf en el orden latexgeneric → todo lo demás. Que gane un archivo situado junto al manuscrito es justo lo que cabe esperar, y ahí mismo está la trampa de esta sección. Lo otro que conviene notar es que el primer elemento de {latex,generic,} cambia con el nombre del programa en ejecución. Invocado como pdflatex-dev pasa a ser {latex-dev,latex,generic,}, de modo que se consulta antes el árbol de desarrollo. kpathsea responde según quién pregunte. Y los 8.798 que informó -expand-path son además una advertencia: sin el índice ls-R, esos son los directorios que habría que abrir en cada búsqueda.

ls-R y TEXMFDBS: por qué solo TEXMFHOME no necesita mktexlsr

La respuesta cabe en una línea: TEXMFDBS, la lista de árboles que llevan índice, no incluye TEXMFHOME. Abrir cada vez los 8.798 directorios de la sección anterior queda descartado, así que kpathsea coloca en la raíz de cada árbol una base de nombres de archivo llamada ls-R y la consulta en su lugar. Qué árboles tienen índice lo dice TEXMFDBS, y en este TeX Live 2024 enumera exactamente cuatro: precisamente los que llevaban !! en TEXMF. TEXMFHOME no está entre ellos. Por eso TEXMFHOME se recorre en disco cada vez, y por eso un archivo puesto ahí se encuentra en cuanto aterriza.

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

El propio archivo ls-R es texto llano y sin adornos. Su primera línea es siempre % ls-R -- filename database for kpathsea; do not change this line., y a continuación se lista cada directorio con su contenido. El texmf-dist/ls-R de esta máquina ocupa 5,2 MB y 276.953 líneas, e indexa 228.764 archivos repartidos en 16.063 directorios. El comando que lo reconstruye es mktexlsr, y texhash es un enlace simbólico a él: el mismo programa con un segundo nombre. La regla práctica sale sola: si coloca un archivo a mano en TEXMFLOCAL o en un árbol del sistema, hace falta mktexlsr; si lo coloca en TEXMFHOME, no. El experimento de arriba es toda la razón. El detalle de los comandos corresponde a la página de gestión de paquetes y fuentes.

kpsewhich --all: hallar el archivo que oculta una copia vieja

kpsewhich --all NOMBRE imprime todas las coincidencias, en orden de búsqueda. El kpsewhich a secas devuelve solo la primera —el archivo que de hecho se leerá—, así que ver la segunda y siguientes requiere --all. El accidente clásico, «hay dos archivos con este nombre y gana el más viejo», se hace visible con un solo comando. Incluso en un TeX Live 2024 recién instalado, amsmath.sty existe realmente por duplicado: la copia estable en tex/latex/amsmath/ y la de desarrollo en 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

Esas dos ejecuciones son un experimento repetible sin tocar nada, y confirman la regla de que gana el primer resultado. En la práctica, sin embargo, donde esto muerde es casi siempre junto al manuscrito. Como TEXINPUTS empieza por ., un viejo amsmath.sty o article.cls que se coló en la carpeta del proyecto hace años se lee antes que la copia actual de la distribución. Y falla del modo más incómodo posible: el documento compila en su máquina y no en la de un coautor. Ante un error que no se explica —! LaTeX Error: Command \... already defined. y parientes— o ante una discrepancia entre dos máquinas, teclee primero kpsewhich --all. Es el camino más corto a la respuesta.

Usar kpsewhich: -var-value frente a -expand-path

-var-value muestra lo que dice la configuración; -expand-path muestra lo que hay realmente en disco. Esa diferencia es lo que los vuelve útiles para diagnosticar. Imprima TEXMF de ambas formas en este TeX Live 2024: -var-value enumera siete árboles con sus marcas !!, mientras que -expand-path devuelve solo cinco. Los dos que se cayeron —~/Library/texlive/2024/texmf-config y ~/Library/texmf— sencillamente aún no se han creado. Es decir: si un árbol aparece en la configuración pero no en la expansión, ese directorio no existe. Cuando no aparece un archivo que jurarías haber puesto en TEXMFHOME, esa es la primera sospecha.

ComandoQué respondeCuándo usarlo
kpsewhich NAMELa primera coincidencia: el archivo que de verdad se leeEmpiece aquí: confirme que es el archivo que cree
kpsewhich --all NAMETodas las coincidencias, en orden de búsquedaPara ver si una copia vieja la está tapando
kpsewhich -var-value=TEXMFHOMEEl valor que la configuración da a la variable, con las marcas !!Para confirmar sin adivinar dónde debería estar un árbol
kpsewhich -expand-path=$TEXMFLa expansión limitada a los directorios que existen de verdadPara detectar el desfase entre configuración y realidad
kpsewhich -show-path=texLa lista ordenada de directorios para ese tipo de archivoPara rastrear por qué se encuentra en ese orden

texmf.cnf: de dónde salen los valores de las variables

Todas las variables vistas hasta aquí —TEXMF, TEXINPUTS, la ubicación de cada árbol— están escritas en un archivo de configuración llamado texmf.cnf. Antes que nada, kpathsea lo lee y de ahí toma sus parámetros de funcionamiento: rutas de búsqueda, dónde se sitúa cada árbol, límites de memoria y demás. Lo interesante es que puede haber más de un texmf.cnf. kpathsea los lee en orden a lo largo de una ruta dedicada, TEXMFCNF, y para cada variable toma la primera definición que encuentra; los archivos posteriores no anulan a los anteriores. En esta máquina hay dos apilados.

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

El texmf.cnf delgado de arriba —el archivo de diferencias que escribe TeX Live— se lee primero, y el grueso archivo de valores por defecto después. Así que la convención para cambiar un valor de forma permanente es no editar el archivo de la distribución, sino escribir solo las líneas necesarias en un lugar de mayor prioridad. TEXMFLOCAL/web2c/texmf.cnf es ese lugar. Hecho así, los ajustes sobreviven a una actualización de la distribución y unas pocas líneas bastan para ver qué se cambió. En resumen: texmf.cnf fija dónde están los árboles y qué forma tienen las rutas de búsqueda, y kpathsea encuentra después el archivo en ese orden, casi siempre a través del índice ls-R. Esas dos capas son todo el mecanismo detrás de una sola línea callada de \usepackage{...}.

PATH encuentra el programa; kpathsea, los archivos

Son dos mecanismos por completo distintos, y confundirlos descarrila el diagnóstico. kpathsea busca los archivos que TeX lee.sty, .cls, fuentes—, pero antes de eso el intérprete de órdenes debe encontrar el ejecutable mismo, pdflatex. Eso es cosa del sistema operativo y consiste en recorrer en orden los directorios listados en la variable de entorno PATH. TeX Live reúne sus ejecutables en un único directorio bin por sistema y arquitectura, y en macOS MacTeX ofrece un enlace estable independiente del año en /Library/TeX/texbin. Por tanto pdflatex: command not found no es un problema de kpathsea, sino casi con seguridad de PATH. A la inversa, ! LaTeX Error: File 'foo.sty' not found. no tiene nada que ver con PATH. El procedimiento para configurarlo corresponde a la página de instalación de escritorio.

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

Dónde poner su propio archivo .sty

Lo personal va a TEXMFHOME, lo compartido por el laboratorio a TEXMFLOCAL, y en ambos casos se respeta la disposición TDS. Esa es toda la regla. Lo que no debe hacerse es adivinar la ubicación: el valor por defecto de TEXMFHOME cambia según el sistema —~/texmf en Linux, pero ~/Library/texmf en MacTeX bajo macOS—. Así que empiece siempre por kpsewhich -var-value=TEXMFHOME. A la inversa, un archivo que pertenece solo a un envío —un myconf.cls de congreso, un journal.sty de revista— puede quedarse junto al manuscrito, porque TEXINPUTS mira . primero. Pero colocar un nombre genérico como article.cls junto al manuscrito es fabricar con las propias manos el accidente de ocultamiento de la sección anterior.

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

En cuanto kpsewhich devuelva la ruta esperada, al manuscrito le basta con \usepackage{thesisstyle}. Si no devuelve nada, sospeche de tres cosas por orden. (1) ¿Está el archivo bajo tex/latex/<paquete>/? TEXINPUTS solo mira por debajo de tex/. (2) ¿Coincide el nombre en mayúsculas y minúsculas? (3) Si lo puso en un árbol del sistema, ¿ejecutó mktexlsr? Comprobar en ese orden convierte el síntoma «TeX está roto» en «¿dónde lo puse en el mapa de búsqueda?», una pregunta que sí tiene respuesta.