TeX directory structure & paths

On the TeX Live 2024 install this page was written against, kpsewhich -expand-path='$TEXINPUTS' prints 8,798 directories — about half a million characters of path. And yet \usepackage{amsmath} resolves instantly. The reason is simple: LaTeX hardly ever looks at those directories. This page takes apart the two halves of that trick — the TDS (TeX Directory Structure), which is the map of where every texmf file lives, and kpathsea, the search engine that runs over it — using real command output throughout. Which tree survives an upgrade and which is thrown away? And why is a file dropped into TEXMFHOME the one case that needs no mktexlsr?

The TDS: why one package is scattered over nine directories

The TDS sorts files by kind, not by package, so a single package never sits in a single place. Count amsfonts — the package that provides amssymb — on this TeX Live 2024 install and it occupies nine directories under texmf-dist. Its macros are in tex/latex/amsfonts/, the commented source .dtx files in source/latex/amsfonts/, the manuals in doc/fonts/amsfonts/, and the fonts themselves split further by format across fonts/tfm/, fonts/type1/, fonts/afm/, fonts/map/ and fonts/source/. The plain TeX version gets its own 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

Why arrange things this way? The answer is portability. TeX runs on macOS, Unix and Windows, and CTAN (the Comprehensive TeX Archive Network) collects thousands of packages. If every distributor laid files out differently, both the people shipping packages and the tools looking for them would stumble every time. The TDS, drawn up by the TeX Users Group (TUG) in the 1990s, made one set of rules universal: macros go under tex/, fonts under fonts/<type>/<supplier>/<typeface>/. As a result the location of any file can be inferred from the rules alone, on any OS and any distribution. Under tex/ there is one more level, tex/<format>/<package>/, where <format> is latex, plain, generic and so on.

DirectoryWhat it holdsMeasured size (texmf-dist, TeX Live 2024)
doc/Package manuals — what texdoc opens3.7 GB, 10,099 PDFs alone
fonts/All font files, by format: tfm, vf, type1, opentype, enc, map2.9 GB
tex/Macros, classes, styles (.tex .sty .cls), e.g. tex/latex/...594 MB
source/Commented sources .dtx and their .ins extraction scripts — the implementation, readable426 MB
scripts/OS-independent executable scripts (the bodies of mktexlsr, latexmk, …)133 MB
bibtex/Bibliography databases bib/ and bibliography styles bst/26 MB
web2c/Engine configuration; home of texmf.cnf and the format list fmtutil.cnf248 KB

If one line of that table surprises you, it should be this: the documentation weighs more than the software. Of the 7.9 GB in texmf-dist, doc/ accounts for 3.7 GB while the macros in tex/ come to just 594 MB. That is why the TeX Live installer offers to skip documentation at all, and why the Docker images split into -doc and non--doc flavours. Once the layout is in your head you can also use it: when a package behaves in a way you cannot explain, go and read source/latex/<package>/*.dtx directly, and remember that the manual texdoc opens is a real file sitting in doc/.

Which tree survives an upgrade

Only texmf-dist is replaced wholesale. TeX Live creates a directory per year — /usr/local/texlive/2024 — and puts the distribution proper, texmf-dist, inside it. Next year a 2025 appears alongside and texmf-dist is swapped for a brand-new copy. Adding your own files to the distribution is therefore suicide; the flip side is that anything outside the year directory is untouched. It is no accident that TEXMFLOCAL sits at /usr/local/texlive/texmf-localoutside 2024. That is the whole design. TEXMFHOME is further out still, inside your home directory.

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
VariableRoleWhat an upgrade does to it
TEXMFDISTThe distribution itself; thousands of packages live here. Never edit by handReplaced wholesale. Anything you added is gone
TEXMFLOCALMachine-wide additions, shared by every userSurvives, because it sits outside the year directory
TEXMFHOMEYour personal tree; your own classes and a journal's style go hereSurvives; it is in your home directory and untouched
TEXMFVARAuto-generated cache: formats, font maps, LuaTeX cachesRebuilt per year; deleting it only forces regeneration
TEXMFCONFIGPer-user configuration store, written by updmap and fmtutilSurvives, but lives under a per-year directory
TEXMFSYSVARSystem-wide counterpart of VAR / CONFIG, written by the -sys commandsTEXMFSYSCONFIG behaves the same; both sit inside the year directory
TEXMFROOTRoot of the whole installation, /usr/local/texlive/2024A new year means a different directory entirely

When a file of the same name exists in several trees, which one wins? That is settled by a single variable, TEXMF, whose value is nothing but the search priority written out in order. On this TeX Live 2024 it reads as below: leftmost wins, so your own configuration and caches come first, then your personal TEXMFHOME, then the machine-wide TEXMFLOCAL, and the distribution TEXMFDIST last. In other words, dropping mystyle.sty into TEXMFHOME shadows the distribution's copy of the same name — not by overwriting anything, but by the natural ranking personal → site → distribution. The !! markers in front of some entries are explained in the next section.

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.

Is it safe to delete texmf-var?

Everything in it is generated, so in principle nothing is lost by deleting it. Before chanting “delete it and it will fix itself”, though, it is worth looking at what is actually there. On this TeX Live 2024 the system-side texmf-var comes to 259 MB, of which 233 MB is web2c/ holding 53 .fmt filespdflatex.fmt alone is 7.8 MB. A format file is a canned memory image that saves re-reading latex.ltx and the class files on every run. The user-side texmf-var is larger still at 293 MB, and 257 MB of that is luatex-cache/, the result of LuaTeX parsing fonts. The psfonts.map that updmap writes lives here too.

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

A practical rule follows. When a stale format file is making things behave oddly, rebuild it with fmtutil-sys --all rather than deleting the directory. Deleting the whole tree is for narrower situations — a corrupted LuaTeX font cache that makes luaotfload emit strange errors, for instance. Wiping it costs only a slow first compile afterwards, but do not confuse texmf-var with texmf-config: take the latter down with it and you lose your updmap settings as well. The regeneration commands themselves are covered on the package and font management page.

How kpathsea actually finds a file

The searching is done by a shared library called kpathsea (kpath search). None of pdftex, xetex, luatex, dvipdfmx or bibtex looks for files itself; they all ask kpathsea “where is amsmath.sty?”. What kpathsea takes in is a single string with rules in it. Three symbols are worth learning: $VAR expands a variable, a trailing // means “everything below this, recursively”, and a leading !! means “do not scan the disk — consult only the filename database described in the next section”. Print TEXINPUTS, the path used to find LaTeX sources, and all three appear at once.

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

Read it like this: . first (the directory holding your manuscript), and failing that, recurse through each texmf tree's tex/ branch in the order latexgeneric → everything else. That a file next to the manuscript wins is exactly what you would expect — and it is also where this section's pitfall lives. The other thing to notice is that the first item in {latex,generic,} changes with the name of the running program. Invoked as pdflatex-dev it becomes {latex-dev,latex,generic,}, so the development tree is consulted first. kpathsea gives different answers depending on who is asking. And the 8,798 that -expand-path reported is also a warning: without the ls-R index, that is how many directories would have to be opened on every single lookup.

ls-R and TEXMFDBS: why only TEXMFHOME needs no mktexlsr

The answer fits on one line: TEXMFDBS, the list of trees that carry an index, does not include TEXMFHOME. Opening the 8,798 directories of the previous section every time is out of the question, so kpathsea keeps a filename database called ls-R at the root of each tree and consults that instead. Which trees have an index is spelled out by TEXMFDBS, and on this TeX Live 2024 it lists exactly four — precisely the trees that carried !! in TEXMF. TEXMFHOME is not among them. That is why TEXMFHOME is walked on disk every time, and why a file put there is found the instant it lands.

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

The ls-R file itself is unglamorous plain text. Its first line is always % ls-R -- filename database for kpathsea; do not change this line., after which each directory is listed with its contents. The texmf-dist/ls-R on this machine is 5.2 MB and 276,953 lines long, indexing 228,764 files across 16,063 directories. The command that rebuilds it is mktexlsr, and texhash is a symbolic link to it — the same program under a second name. The practical rule falls out cleanly: place a file by hand in TEXMFLOCAL or a system tree and you need mktexlsr; place it in TEXMFHOME and you do not. The experiment above is the whole reason why. Which command to reach for is covered in detail on the package and font management page.

kpsewhich --all: finding the file an older copy is hiding

kpsewhich --all NAME prints every match, in search order. Plain kpsewhich returns only the first hit — the file that will actually be read — so seeing the second and beyond needs --all. The classic accident, “there are two files with this name and the older one is winning”, becomes visible in a single command. Even on a clean TeX Live 2024, amsmath.sty really does exist twice: the stable copy in tex/latex/amsmath/ and the development copy in 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

Those two runs are an experiment you can repeat without touching a thing, and they confirm the rule that the first hit wins. In practice, though, the place this bites is almost always next to the manuscript. Because TEXINPUTS starts with ., an old amsmath.sty or article.cls that drifted into the project folder years ago is read before the distribution's current copy. Worse, it fails in the nastiest possible shape: the document builds on your machine and not on a co-author's. When you meet an error you cannot account for — ! LaTeX Error: Command \... already defined. and its relatives — or a disagreement between two machines, type kpsewhich --all first. It is the shortest path to the answer.

Using kpsewhich: -var-value versus -expand-path

-var-value shows what the configuration says; -expand-path shows what is actually on disk. That gap is what makes them useful for diagnosis. Print TEXMF both ways on this TeX Live 2024 and -var-value lists seven trees complete with their !! markers, while -expand-path returns only five. The two that dropped out — ~/Library/texlive/2024/texmf-config and ~/Library/texmf — simply have not been created yet. So if a tree appears in the configuration but not in the expansion, that directory does not exist. When a file you are sure you put in TEXMFHOME cannot be found, this is the first thing to suspect.

CommandWhat it answersWhen to reach for it
kpsewhich NAMEThe first match — the file that will really be readStart here: confirm it is the file you think it is
kpsewhich --all NAMEEvery match, in search orderTo see whether an older copy is shadowing it
kpsewhich -var-value=TEXMFHOMEThe value the configuration gives a variable, !! markers includedTo confirm where a tree is meant to be, without guessing
kpsewhich -expand-path=$TEXMFThe expansion restricted to directories that actually existTo catch the gap between configuration and reality
kpsewhich -show-path=texThe ordered directory list used for that file typeTo trace why things are found in the order they are

texmf.cnf: where the variables get their values

Every variable so far — TEXMF, TEXINPUTS, the location of each tree — is written down in a configuration file called texmf.cnf. Before doing anything else kpathsea reads it and picks up its operating parameters: the search paths, where each tree sits, memory limits and the rest. The interesting part is that there can be more than one texmf.cnf. kpathsea reads them in order along a dedicated search path, TEXMFCNF, and for any given variable takes the first definition it finds — later files do not override earlier ones. On this machine, two are stacked up.

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

The thin texmf.cnf on top — the file of differences TeX Live writes out — is read first, and the fat file of defaults second. So the convention for changing a value permanently is not to edit the distribution's file but to write only the lines you need in a higher-priority location. TEXMFLOCAL/web2c/texmf.cnf is that place. Do it this way and your settings survive a distribution upgrade, and a glance at a few lines tells you what you changed. To sum up: texmf.cnf fixes where the trees are and what the search paths look like, and kpathsea then finds the target in that order, usually via the ls-R index. Those two layers are the whole mechanism behind a single quiet line of \usepackage{...}.

PATH finds the program; kpathsea finds the files

These are two entirely separate mechanisms, and confusing them sends a diagnosis off the rails. kpathsea looks for the files TeX reads.sty, .cls, fonts — but before any of that the shell has to find the executable itself, pdflatex. That is the operating system's job, and it consists of walking the directories listed in the PATH environment variable in order. TeX Live gathers its executables into a single bin directory per OS and architecture, and on macOS MacTeX provides a year-independent stable link at /Library/TeX/texbin. So pdflatex: command not found is not a kpathsea problem; it is almost certainly a PATH problem. Conversely, ! LaTeX Error: File 'foo.sty' not found. has nothing to do with PATH. The procedure for setting it up belongs to the desktop installation page.

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

Where to put your own .sty file

Personal files go in TEXMFHOME, lab-wide files in TEXMFLOCAL, and in both cases you keep the TDS layout. That is the whole rule. What you must not do is guess the location — the default TEXMFHOME differs by OS: ~/texmf on Linux but ~/Library/texmf for MacTeX on macOS. So always start from kpsewhich -var-value=TEXMFHOME. Conversely, a file that belongs only to one submission bundle — a conference myconf.cls, a journal journal.sty — may sit next to the manuscript, because TEXINPUTS looks at . first. But putting a generic name such as article.cls beside the manuscript is manufacturing the shadowing accident of the previous section with your own hands.

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

Once kpsewhich reports the path you expected, the manuscript needs nothing more than \usepackage{thesisstyle}. When it reports nothing, suspect three things in order. (1) Is the file under tex/latex/<package>/? TEXINPUTS only ever looks below tex/. (2) Does the filename match in case? (3) If you put it in a system tree, did you run mktexlsr? Checking in that order turns the symptom from “TeX is broken” into “where on the search map did I put it?” — a question that has an answer.