在撰写本页所用的 TeX Live 2024 上,kpsewhich -expand-path='$TEXINPUTS' 会打印出 8,798 个目录,约五十万个字符的路径。然而 \usepackage{amsmath} 却瞬间就能解析。原因很简单:LaTeX 平时几乎不去看那些目录。本页用真实的命令输出拆解这套机制的两半——TDS(TeX 目录结构),也就是每个 texmf 文件所在位置的地图,以及在它上面奔跑的搜索引擎 kpathsea。哪棵树会在升级时被整个换掉,哪棵能活下来?为什么唯独放进 TEXMFHOME 的文件不需要 mktexlsr?
TDS:为什么一个宏包会散落在九个目录里
TDS 按种类而非按宏包整理文件,因此一个宏包的文件从不集中在一处。在本文所用的 TeX Live 2024 上清点提供 amssymb 的 amsfonts,它在 texmf-dist 下占据了 九个目录:宏在 tex/latex/amsfonts/,带注释的源码 .dtx 在 source/latex/amsfonts/,手册在 doc/fonts/amsfonts/,字体本身则按格式进一步分散到 fonts/tfm/、fonts/type1/、fonts/afm/、fonts/map/ 和 fonts/source/。plain TeX 版本另有 tex/plain/amsfonts/。
$ 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为什么要这样安排?答案是可移植性。TeX 运行在 macOS、Unix 和 Windows 上,而 CTAN(综合 TeX 档案网)汇集了数千个宏包。如果每个发行方各摆各的,发包的人和找包的工具每次都要栽跟头。TeX 用户组(TUG)在 1990 年代制定的 TDS 让一套规则通行全球:宏放在 tex/ 下,字体放在 fonts/<类型>/<供应者>/<字体名>/ 下。于是在任何操作系统、任何发行版上,都可以仅凭规则推断出文件的位置。tex/ 下还有一层 tex/<格式>/<宏包>/,其中 <格式> 是 latex、plain、generic 等。
| 目录 | 内容 | 实测大小(TeX Live 2024 的 texmf-dist) |
|---|---|---|
doc/ | 宏包手册,texdoc 打开的正是这里 | 3.7 GB,仅 PDF 就有 10,099 份 |
fonts/ | 全部字体文件,按格式分:tfm、vf、type1、opentype、enc、map | 2.9 GB |
tex/ | 宏、类、样式(.tex .sty .cls),如 tex/latex/... | 594 MB |
source/ | 带注释的源码 .dtx 及其 .ins 抽取脚本——可读的实现 | 426 MB |
scripts/ | 与操作系统无关的可执行脚本(mktexlsr、latexmk 的本体) | 133 MB |
bibtex/ | 文献数据库 bib/ 与著录样式 bst/ | 26 MB |
web2c/ | 引擎配置;texmf.cnf 与格式清单 fmtutil.cnf 的所在 | 248 KB |
如果表中有哪一行让人意外,应该是这一条:文档比软件本身还重。texmf-dist 总计 7.9 GB,其中 doc/ 占 3.7 GB,而宏本体所在的 tex/ 只有 594 MB。这正是 TeX Live 安装程序提供“不安装文档”选项的原因,也是 Docker 镜像分成带 -doc 与不带 -doc 两类的原因。把这套布局记在心里还有实际用处:当某个宏包的行为解释不通时,可以直接去读 source/latex/<宏包>/*.dtx;而 texdoc 打开的手册,实体就是 doc/ 里的一个文件。
升级之后哪棵树还在
只有 texmf-dist 会被整个替换。 TeX Live 为每一年建立一个目录——/usr/local/texlive/2024——并把发行本体 texmf-dist 放在里面。到了第二年,旁边会出现 2025,而 texmf-dist 换成全新的一份。因此往发行树里加自己的文件无异于自毁;反过来说,年份目录之外的东西毫发无损。TEXMFLOCAL 位于 /usr/local/texlive/texmf-local——在 2024 的外面——绝非偶然,这正是设计使然。TEXMFHOME 还要更外一层,就在你的主目录里。
# 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| 变量 | 角色 | 升级时会怎样 |
|---|---|---|
TEXMFDIST | 发行本体,数千宏包都在这里。不要手动改动 | 整个被替换。你加进去的东西会消失 |
TEXMFLOCAL | 面向整台机器的追加内容,所有用户共享 | 保留。因为它位于年份目录之外 |
TEXMFHOME | 你的个人树;自制类文件、期刊样式放这里 | 保留。它在主目录里,不受影响 |
TEXMFVAR | 自动生成的缓存:格式、字体映射、LuaTeX 缓存 | 按年重建;删掉只会触发重新生成 |
TEXMFCONFIG | 每用户配置的存放处,由 updmap 与 fmtutil 写入 | 保留,但位于按年划分的目录下 |
TEXMFSYSVAR | 上述 VAR / CONFIG 的全系统版,由带 -sys 的命令写入 | TEXMFSYSCONFIG 同理;两者都在年份目录内 |
TEXMFROOT | 整个安装的根,/usr/local/texlive/2024 | 换一年就是另一个目录 |
当同名文件出现在多棵树里时,哪一个胜出?这由一个变量 TEXMF 决定,它的值不过是按顺序写出的搜索优先级。在本文的 TeX Live 2024 上它是下面这样:最左边优先,先是你自己的配置与缓存,然后是个人树 TEXMFHOME,接着是全机器的 TEXMFLOCAL,最后才是发行树 TEXMFDIST。也就是说,把 mystyle.sty 放进 TEXMFHOME 就能遮住发行树里的同名文件——不是覆盖,而是「个人 → 站点 → 发行」这一自然次序。某些条目前面的 !! 标记留到下一节解释。
$ 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.可以删掉 texmf-var 吗
里面的东西全是生成物,所以原则上删掉不会丢失任何东西。不过在念叨「删了就好」之前,值得先看看里面究竟有什么。在本文的 TeX Live 2024 上,系统侧的 texmf-var 为 259 MB,其中 233 MB 是 web2c/,里面躺着 53 个 .fmt 文件——仅 pdflatex.fmt 就有 7.8 MB。格式文件是一份「罐装的内存映像」,免得每次都重新读一遍 latex.ltx 和类文件。用户侧的 texmf-var 更大,293 MB,其中 257 MB 是 luatex-cache/,也就是 LuaTeX 解析字体的结果。updmap 写出的 psfonts.map 也在这里。
$ 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由此得出实用判断。如果是格式文件过期导致行为异常,定式是用 fmtutil-sys --all 重建,而不是删目录。整棵删掉只适用于范围有限的场合,例如 LuaTeX 字体缓存损坏、luaotfload 抛出奇怪错误的时候。全删的代价不过是下一次编译慢上几十秒,但别把 texmf-var 和 texmf-config 混为一谈:连后者一起删掉,updmap 的设置也会一并飞走。重建命令的细节由宏包与字体管理那一页负责。
kpathsea 究竟是怎么找到文件的
负责搜索的是一个名为 kpathsea(kpath search)的共享库。pdftex、xetex、luatex、dvipdfmx、bibtex 都不自己找文件,而是统统去问 kpathsea:「amsmath.sty 在哪里?」kpathsea 接收的是一条带规则的字符串。要记的符号有三个:$VAR 展开变量,结尾的 // 表示「这之下全部递归」,开头的 !! 表示「不要扫描磁盘,只查下一节要讲的文件名数据库」。把用于查找 LaTeX 源文件的 TEXINPUTS 打印出来,三者一次全都现身。
$ 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读法是这样:先看 .(放稿件的目录),找不到再按 latex → generic → 其余的顺序,递归遍历各 texmf 树的 tex/ 分支。稿件旁边的文件优先,这完全符合直觉——本节的陷阱也正在这里。另一处值得注意:{latex,generic,} 的第一项会随正在运行的程序名而变。以 pdflatex-dev 调用时它变成 {latex-dev,latex,generic,},于是先查开发版的树。kpathsea 会按「谁在问」给出不同的答案。此外,-expand-path 报出的 8,798 也是一句警告:如果没有 ls-R 这个索引,每一次查找都要打开这么多目录。
ls-R 与 TEXMFDBS:为什么唯独 TEXMFHOME 不需要 mktexlsr
答案一行就够:列出「哪些树带索引」的 TEXMFDBS 里没有 TEXMFHOME。每次都打开上一节那 8,798 个目录是不可想象的,所以 kpathsea 在每棵树的根部放一个叫 ls-R 的文件名数据库,改查它。哪些树有索引由 TEXMFDBS 写明,在本文的 TeX Live 2024 上恰好四棵——正是在 TEXMF 中带 !! 的那几棵。TEXMFHOME 不在其列。所以 TEXMFHOME 每次都真去扫磁盘,文件一放进去立刻就能找到。
$ 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-onlyls-R 本身是一份朴素的纯文本。第一行永远是 % ls-R -- filename database for kpathsea; do not change this line.,其后按目录逐一列出所含文件名。本机的 texmf-dist/ls-R 为 5.2 MB、276,953 行,索引了 16,063 个目录中的 228,764 个文件。重建索引的命令是 mktexlsr,而 texhash 是指向它的符号链接——同一个程序的另一个名字。实用判断很干净:手动把文件放进 TEXMFLOCAL 或系统树,就需要 mktexlsr;放进 TEXMFHOME 则不需要。上面的实验就是全部理由。命令的具体用法见宏包与字体管理页。
kpsewhich --all:找出被旧副本遮住的文件
kpsewhich --all NAME 会按搜索顺序列出全部匹配。不带选项的 kpsewhich 只返回第一条,也就是真正会被读取的那个文件,因此要看第二条及以后就得用 --all。「同名文件有两份,旧的那份赢了」这类事故,一条命令就现形。哪怕在干净的 TeX Live 2024 上,amsmath.sty 也确实存在两份:稳定版在 tex/latex/amsmath/,开发版在 tex/latex-dev/amsmath/。
$ 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这两次运行是一个不必改动任何设置就能重复的实验,也印证了「第一条胜出」的规则。而在实际工作中,这条规则咬人的地方几乎总是稿件旁边。由于 TEXINPUTS 以 . 开头,几年前不知从哪里拷来、混进项目文件夹的旧 amsmath.sty 或 article.cls,会先于发行树中的最新副本被读取。更糟的是它以最难缠的形式发作:文档在你的机器上能编译,在合作者那里不行。当你遇到莫名其妙的错误——! LaTeX Error: Command \... already defined. 之类——或是两台机器结果不一致时,先敲 kpsewhich --all。这是最短的排查路径。
kpsewhich 用法:-var-value 与 -expand-path 有何不同
-var-value 显示配置说了什么,-expand-path 显示磁盘上实际有什么。这道缝隙正是诊断时的着力点。在本文的 TeX Live 2024 上分别打印 TEXMF:-var-value 列出七棵树,连 !! 标记一并给出;而 -expand-path 只回了五棵。掉队的两棵——~/Library/texlive/2024/texmf-config 和 ~/Library/texmf——只是还没有被创建。也就是说,如果某棵树在配置里有、在展开结果里没有,那个目录就不存在。当你确信放进 TEXMFHOME 的文件却找不到时,这是第一个该怀疑的地方。
| 命令 | 回答什么 | 什么时候用 |
|---|---|---|
kpsewhich NAME | 第一条匹配,也就是真正会被读取的文件 | 先用这个:确认它就是你以为的那个文件 |
kpsewhich --all NAME | 按搜索顺序列出全部匹配 | 查看是否被旧副本遮住 |
kpsewhich -var-value=TEXMFHOME | 配置赋予该变量的值,连 !! 一并显示 | 不靠猜测地确认树应在何处 |
kpsewhich -expand-path=$TEXMF | 只展开到实际存在的目录 | 找出配置与现实之间的落差(忘了创建) |
kpsewhich -show-path=tex | 该文件类型所用的有序目录列表 | 追查「为什么是按这个顺序找到的」 |
texmf.cnf:这些变量的值从哪里来
前面出现的 TEXMF、TEXINPUTS 以及各棵树的位置,统统写在一个叫 texmf.cnf 的配置文件里。kpathsea 在做任何事之前先读它,取得搜索路径、各树位置、内存上限等运行参数。有意思的是 texmf.cnf 未必只有一个。kpathsea 会沿着专用的搜索路径 TEXMFCNF 依次读取多个 texmf.cnf,对某个变量采用最先找到的定义(后读的文件不会覆盖先前的定义)。本机上叠了两份。
$ 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上面那份细瘦的 texmf.cnf(TeX Live 写出的仅含差异的文件)先被读取,下面那份厚重的默认值文件在后。因此,想永久改变某个值时的定式是:不要去编辑发行树里的文件,而是把需要的那几行写在优先级更高的位置。TEXMFLOCAL/web2c/texmf.cnf 就是那个位置。这样做,设置能挺过发行版升级,改了什么也只需看几行就明白。总结起来:texmf.cnf 决定树在哪里、搜索路径长什么样,kpathsea 再按那个顺序(多半经由 ls-R)找到目标——\usepackage{...} 一行悄无声息地完成解析,靠的就是这两层。
PATH 找的是程序,kpathsea 找的是文件
这是两套完全不同的机制,混为一谈就会把排查带偏。kpathsea 找的是 TeX 要读取的文件——.sty、.cls、字体——但在此之前,shell 必须先找到可执行程序本身,也就是 pdflatex。那是操作系统的活儿,无非是按顺序遍历环境变量 PATH 中列出的目录。TeX Live 把可执行程序集中在按操作系统与架构划分的单个 bin 目录里,而 macOS 上的 MacTeX 还提供了与年份无关的稳定链接 /Library/TeX/texbin。所以 pdflatex: command not found 不是 kpathsea 的问题,几乎必然是 PATH 的问题;反过来 ! LaTeX Error: File 'foo.sty' not found. 与 PATH 无关。具体设置步骤归桌面安装那一页。
$ which pdflatex
/Library/TeX/texbin/pdflatex
$ readlink /Library/TeX/texbin
Distributions/Programs/texbin自制的 .sty 该放在哪里
个人的放 TEXMFHOME,实验室共用的放 TEXMFLOCAL,两种情况都要遵守 TDS 的层次。规则就这些。但绝不要凭猜测确定位置——TEXMFHOME 的默认值因系统而异:Linux 上是 ~/texmf,而 macOS 的 MacTeX 是 ~/Library/texmf。所以每次都从 kpsewhich -var-value=TEXMFHOME 开始。反过来,只属于某一份投稿的文件,比如会议的 myconf.cls、期刊的 journal.sty,放在稿件旁边即可,因为 TEXINPUTS 会先看 .。但把 article.cls 这类通用名字放在稿件旁边,等于亲手制造上一节那种遮蔽事故。
# 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一旦 kpsewhich 返回了你预期的路径,稿件里只需写 \usepackage{thesisstyle} 即可。若它什么也不返回,请按顺序怀疑三件事。(1) 文件是否位于 tex/latex/<宏包名>/ 之下?TEXINPUTS 只看 tex/ 以下。(2) 文件名的大小写是否一致?(3) 如果放进了系统树,有没有运行 mktexlsr?按这个顺序核对,症状就从「TeX 坏了」变成「我把它放在搜索地图的哪一格」——一个有答案的问题。