LaTeX 格式

若有人问 pdflatex 是什么,准确的答案并不是「一个程序」。它是以预先载入 LaTeX 格式的方式启动的 pdfTeX 引擎pdflatexlualatexplatex 这些命令名并排摆着,看上去像是各自独立的软件;实际上,每一个都不过是两条独立轴线的组合——引擎(真正运行的程序)与格式(预先编译好的宏集合)。看清这两条轴,好几件事就一并有了解释:.fmt 文件是什么、为什么运行 latex 得到的是 DVI 而不是 PDF,以及 LaTeX3 最终究竟如何了。

引擎与格式的区别

引擎是可执行文件,格式是数据。 引擎就是读取 .tex 并排版的那个程序——texpdftexxetexluahbtexeuptex 等等。格式则是把 \documentclass\section 之类的大量宏收集起来,预先展开并固化成一份转储,也就是 .fmt 文件。启动时引擎瞬间读入这个 .fmt,因此不必每次运行都重新定义成千上万行宏。

因此,「LaTeX」是格式的名字,不是引擎的名字。 同一个 LaTeX 格式可以跑在不同引擎之上,而你敲下的命令名指定的正是某个 引擎+格式的搭配。这张对应表无需猜测:它在 TeX Live 里实实在在地存在,就是那份名为 fmtutil.cnf 的配置文件。它有四列——格式名、引擎名、断词模式文件、参数——TeX Live 2024 版共 54 行,也就是登记了 54 种组合。

terminal
$ grep -E '^(tex|latex|pdflatex|lualatex|xelatex|uplatex|amstex|pdfcsplain) ' \
      $(kpsewhich fmtutil.cnf)

# format      engine     hyphenation                 arguments
tex           tex        -                           tex.ini
latex         pdftex     language.dat                *latex.ini
pdflatex      pdftex     language.dat                *pdflatex.ini
xelatex       xetex      language.dat                -etex xelatex.ini
lualatex      luahbtex   language.dat,language.dat.lua lualatex.ini
uplatex       euptex     language.dat                *uplatex.ini
amstex        pdftex     -                           *amstex.ini
pdfcsplain    luatex     -                           csplain.ini
pdfcsplain    pdftex     -                           csplain-utf8.ini
pdfcsplain    xetex      -                           csplain.ini

从这张表里可以读出两件事。第一,同一个格式名可以出现在多个引擎之下——pdfcsplain 就有三行,分别对应 luatexpdftexxetex。要证明格式与引擎彼此正交,没有比这更好的例子了。第二,参数前面的那个 *:它指示引擎在启用 e-TeX 扩展的状态下转储,而该文件自己的开头注释说明,这本质上等同于 -etex 选项。latex 那一行有 *,Knuth 的 tex 那一行没有——现代 LaTeX 以 e-TeX 为前提这件事,就浓缩在这一个字符里。

.fmt 文件里究竟装着什么

它是把引擎的全部内部状态整个保存下来的内存转储。 既不是源码,也不是文本。宏定义、类别码、断词模式、数学字体分配——latex.ltx 读到最后那一刻引擎记忆中的一切,被原样写成二进制。TeX Live 2024 的 latex.fmt 约 8.2 MB。重新读入这 8.2 MB 与每次重新解释 latex.ltx 之间的时间差,正是 LaTeX 能瞬间启动的原因。

有一个地方,引擎与格式的关系直接呈现在文件系统的形态里.fmt 并不按「格式名」存放,而是放进以引擎命名的目录中。在 TeX Live 2024 的 texmf-var/web2c/ 之下,并排着 tex/pdftex/xetex/luahbtex/euptex/ 等目录,各自装着由该引擎转储出的格式。tex/tex.fmt 是 plain TeX,pdftex/latex.fmt 是 LaTeX,euptex/uplatex.fmt 是 upLaTeX——同样是 .fmt 扩展名,但取决于由哪个引擎生成,它们是不同的东西。

terminal
# Formats are filed by ENGINE, not by format name.
$ ls /usr/local/texlive/2024/texmf-var/web2c/
aleph  euptex  hitex  luahbtex  luajittex  luatex  metafont  pdftex  tex  xetex

$ ls /usr/local/texlive/2024/texmf-var/web2c/euptex/
eptex.fmt  euptex.fmt  platex.fmt  ptex.fmt  uplatex.fmt  ...

# Ask kpathsea which .fmt a given engine would load.
$ kpsewhich -engine pdftex latex.fmt
/usr/local/texlive/2024/texmf-var/web2c/pdftex/latex.fmt

$ ls -l $(kpsewhich -engine pdftex latex.fmt)
-rw-r--r--  1 root  wheel  8221690 May  4  2024 .../pdftex/latex.fmt

生产端的机制同样简单。给引擎加上 -ini 启动,它就进入不载入任何 .fmt 的「初始化模式」。在这个状态下让它读取 latex.ltx 这样的源文件,最后执行原语 \dump,当时的内部状态便被写成一个 .fmt。曾经是独立可执行文件的 initex,不过是这种 -ini 启动的别名,如今只作为指向 tex 的符号链接留存下来。

fmtutil 重建格式

重建 .fmt 的命令是 fmtutil。它读取 fmtutil.cnf,按其中登记的引擎和参数,把格式逐个重新转储。它通常在发行版更新时自动运行,所以很少需要手动敲;但当你换掉了内核文件或断词模式却毫无变化时,原因多半是格式还是旧的。实际会用到的两种写法是 fmtutil-sys --all(全部重建)和 fmtutil-sys --byfmt latex(只重建一个)。

terminal
# Rebuild one format (writes into the system tree; needs write permission).
$ fmtutil-sys --byfmt latex

# Rebuild every format listed in fmtutil.cnf.
$ fmtutil-sys --all

# Rebuild only the formats that use a given engine.
$ fmtutil-sys --byengine luahbtex

# Personal tree instead of the system tree (no root needed).
$ fmtutil-user --byfmt pdflatex

# Where did the format end up, and when was it built?
$ kpsewhich -engine pdftex latex.fmt

-sys-user 的差别在于写到哪里:系统树(TEXMFSYSVAR)还是各用户自己的树(TEXMFVAR。有管理员权限时用 -sys 最自然;-user 则适合在共用机器上只替换自己那一份。两者混用是制造「明明改好了却没变」的可靠办法,所以项目里应统一使用其中一种。另外,fmtutil.cnf 本身归发行版管理——它开头的注释明确写着,手工编辑这个文件的改动会在更新时丢失。若要永久更改,请使用它提供的途径,例如 fmtutil-sys --enablefmt

LaTeX2e:你机器上真正在跑的那个格式

今天说「LaTeX」,几乎无一例外指的是 LaTeX2e。它是 Leslie Lamport 所写 LaTeX 宏集的现行版本,1994 年问世,如今由 LaTeX Project 维护。「2e」表示相对于此前 LaTeX 2.09 的一次小改订,在当时被视为通往筹划已久的 LaTeX3 的过渡步骤。你手上的格式究竟是什么,可以自己核对:内核文件 latex.ltx 自称 \def\fmtname{LaTeX2e},而在 TeX Live 2024 附带的版本中,\fmtversion2023-11-01\patch@level1

内核如今按 每年约两次的定期发布 更新。把「最新版是哪个」当成数字背下来毫无意义——几个月就过时了。不如养成习惯:在自己的安装里读一读 latex.ltx\fmtversion,那才是当前实际转储的内核日期。为想提前试用下一版的人,TeX Live 还并行提供了 开发版格式fmtutil.cnf 中有 latex-devpdflatex-devxelatex-devlualatex-devuplatex-dev 等行,敲 pdflatex-dev main.tex 就会用下一版内核排版。它用于检验文档类与宏包的兼容性,不用于正式稿件。

LaTeX3 最终怎么样了:答案是 expl3

名为 LaTeX3 的「格式」,最终没有面世。 面世的是 LaTeX3 的编程语言。这一层叫作 expl3(L3 编程层),它并非以独立的新内核出现,而是被吸收进 LaTeX2e 内部。所以「该不该迁移到 LaTeX3」这个问题没有答案——不是因为无处可迁,而是因为你早就在用了

转折点是有记录的。LaTeX News 31(2020 年 2 月 2 日)指出,expl3 在此前十年间「已从基本属实验性质转为广泛稳定」,并报告说在那次发布中,团队调整了内核,使其在构建格式时预先载入 expl3 的相当一部分。动机是速度:在 XeLaTeX 或 LuaLaTeX 下载入 fontspec 的文档,每次运行都要为读取 Unicode 数据付出沉重代价。

如今 expl3 已是格式的一部分。读一读内核文件 latex.ltx,就能看到这段逻辑:若 expl3 已经存在,它会报出 Skipping: expl3 code already part of the format 并继续;若不存在,则以 \input expl3.ltx 载入;若连 expl3.ltx 都找不到,就以 \errmessage{LaTeX requires expl3} 停止。换言之,expl3 不再是可选宏包,而是 LaTeX2e 的必需部件。在文档或宏包里写 \RequirePackage{expl3},仍为照顾使用旧格式的人而被推荐,但在当前格式上实际上什么也不做。expl3 本身的语法——\ExplSyntaxOn\cs_new:Npnl3kernel 的带类型变量——另有专页讲述。

LaTeX 之外的格式

LaTeX 是使用最广的格式,但并非唯一。fmtutil.cnf 中的 54 行里,属于 LaTeX 一系的只是少数,其余都是别的宏体系。这里值得提一下 amstex。作为可执行文件,amstex 不过是指向 pdftex 的符号链接,而 fmtutil.cnfamstex pdftex - *amstex.ini 这一行,指定的是载入 AMS-TeX 宏的 pdfTeX 引擎。AMS-TeX 是美国数学会在 plain TeX 之上建立的数学排版宏集,而对使用 LaTeX 的人来说,它实际上已被 amsmath 宏包取代。用 AMS-TeX 开始写新文档,几乎没有理由。

格式转储它的引擎它是什么
textexKnuth 的 plain TeX,唯一不带 e-TeX 扩展的格式
latexpdftexLaTeX2e,由 DVI 模式的 pdfTeX 运行,输出 .dvi
pdflatexpdftex同一个 LaTeX2e,直接输出 PDF;西文的事实默认
xelatexxetexLaTeX2e 跑在 XeTeX 之上;系统字体与 Unicode
lualatexluahbtexLaTeX2e 跑在 LuaHBTeX 之上;可用 Lua 脚本与 HarfBuzz
platex / uplatexeuptex面向日文排版的 LaTeX2e;输出 DVI,再由 dvipdfmx 转为 PDF
amstexpdftexAMS-TeX,建于 plain TeX 上的数学宏;对 LaTeX 用户已被 amsmath 取代
cont-enpdftex / xetexConTeXt(MkII),与 LaTeX 分属不同体系的完整格式;登记在两个引擎下
csplainpdftex面向捷克语与斯洛伐克语的 plain TeX 衍生格式;构建时显式指定 -etex

pLaTeX 与 upLaTeX:面向日文的 LaTeX 格式

pLaTeX 与 upLaTeX 是把 LaTeX 格式装到面向日文排版的引擎上。 竖排、注音假名、和文行组版等日文特有的处理,由最初在 ASCII 公司扩展出来的 pTeX 系引擎 承担。查看 TeX Live 2024 的 fmtutil.cnf 可以发现,platexuplatex 都登记在 euptex 引擎 之下,区分它们的是格式的初始化文件(*platex.ini*uplatex.ini)。因此二者之别不在可执行文件,而在格式如何处理字符platex 保持传统的 pTeX 兼容行为,uplatex 则以内部 Unicode 为前提。新的日文文档,uplatex 是标准选择。

这一系必定输出 DVI,再把含日文的 DVI 用 dvipdfmx 转成 PDF,是两段式流程(uplatex file.tex,然后 dvipdfmx file.dvi)。索引则配合 upmendex。文档类常用奥村晴彦的 jsarticlejsbook(用于 pLaTeX)及其 Unicode 版本如 ujarticle,以及较新的 jlreq,后者遵循 JIS X 4051 的日文排版要求。引擎内部的细节——JFM 度量、禁则处理、竖排的实现——由「pTeX 家族」那一页负责。

哪条命令对应哪种组合

即便是同一个 LaTeX 格式,命令名也决定了背后的引擎、输出格式以及字体的处理方式。下表请从命令名出发,自左向右阅读。实际工作中最要紧的是最右一列:能否使用 fontspec,直接对应着引擎的选择。

命令引擎输出Unicode / 系统字体
latexpdfTeX(DVI 模式)DVI——需另用工具转换不支持;依赖 inputenc 之类
pdflatexpdfTeX直接输出 PDF不支持;依赖 inputenc 之类
platexe-upTeX(pLaTeX 兼容配置)DVI → dvipdfmx日文走传统字体配置
uplatexe-upTeXDVI → dvipdfmx内部 Unicode;日文字体仍用传统方式配置
lualatexLuaHBTeX直接输出 PDF支持——fontspec / luatexja
xelatexXeTeXPDF(内部经由 xdvipdfmx支持——fontspec
terminal
# Engines that write PDF finish in one step.
$ pdflatex main.tex      # -> main.pdf
$ lualatex main.tex      # -> main.pdf
$ xelatex  main.tex      # -> main.pdf

# DVI-based routes add a conversion step.
$ latex    main.tex      # -> main.dvi   (pdfTeX in DVI mode)
$ uplatex  main.tex      # -> main.dvi   (Japanese)
$ dvipdfmx main.dvi      # -> main.pdf

读日志的第一行,然后把组合钉死

日志的第一行同时写着引擎与格式。This is pdfTeX, Version 3.141592653-2.6-1.40.26 (TeX Live 2024) (preloaded format=latex) 拆开看:前半是引擎及其版本,(preloaded format=...) 则是所载入的格式。This is LuaHBTeX ...This is XeTeX ...This is e-upTeX ... 也一样读。当协作者报告「同一条命令,输出却不同」时,只需比对这一行,就能立刻区分是编辑器配置的问题,还是 TeX Live 年份或格式的差异。

而一旦组合确定,就把它写进项目里并固定下来。若是学位论文,等到扉页、目录、图表、参考文献都能顺利生成,就把命令记进 README 或 .latexmkrc。中途把文档从 lualatex 换成 uplatex,字体选择、日文处理、生成 PDF 的路径乃至参考文献处理都会一次性改变。若非换不可,请在回到正式稿件之前,用一个小的试验文件把所有功能都验证一遍。至于当初该如何选择的依据——速度、宏包兼容性、日文的具体情况——都汇总在「如何选择引擎」那一页。