一个超级小的 LaTeX 发行版

01 Mar 2025 7036 words 24 minutes BY-SA 4.0
develop tex

“Every time I read a LaTeX document, I think, wow, this must be correct!”

– Prof. Christos Papadimitriou, CS 170 Spring 2015

TeX Live, 绝大多数 LaTeX 用户使用的 LaTeX 发行版,在最近的版本 2024 中总体积已经达到了惊人的 11 GB 。对此:

600M的安装大小,好像还是很大。这是因为 TeX Live 中某些包的依赖关系比较模糊,还有一些优化的空间。

而笔者希望有这样一个 LaTeX 发行版:

下面介绍针对这一问题的一些技术选型和实验。

编译器

TeX Live 提供了以上除了 TeX 3 和 eTeX 外所有的编译器。笔者认为这没必要。就像 Python 的解释器有 CPython, PyPy, Jython, GraalPython, IronPython, rustpython 等等,但官方的 Python 发行版或者第三方的 AnaConda, miniconda 也只提供了一个 CPython 一样。

笔者选择 LuaHBTeX 。它支持 LaTeX ,而且提供了一个名为 texlua 的 lua 解释器。目前 LaTeX 的打包工具 l3build 和文档搜索工具 texdoc 都运行在 texlua 上。所以 texlua 是必不可少的。

格式文件

TeX 是一门宏语言。所有的宏语言都有一个特点:很容易创造一门新的语言。以 C 语言的宏为例:

习.h:

#define 整数 int
#define 字符 char
#define 返回 return

于是我们创造了一门新的语言,不妨称为习语言:

#include "习.h"

#include <stdio.h>
#include <stdlib.h>
整数 main(整数 argc, 字符 *argv[]) {
  puts("你好,世界!");
  返回 EXIT_SUCCESS;
}

以此类推,我们还可以有喜语言、戏语言等等,这些语言都共用相同的编译器,只需要加载不同的 喜.h, 戏.h 等。 plainTeX, LaTeX, ConTeXt, TeXinfo 等的关系类似。 它们都使用相同的编译器,但需要加载不同的初始化文件。其中最广受欢迎的 LaTeX 也只是数学家 Leslie Lamport 最初为满足自己需求设计的一种“习语言”而已。

相较 plainTeX 和 TeXinfo ,在笔者看来 LaTeX 最大的优点是在检测到编译器是 eTeX 和 LuaTeX 时,会充分利用更多的寄存器。例如 plainTeX 的 \newcount 会从小到大分配寄存器。\newinsert 会从 256 到小分配寄存器。当两个分配的范围重叠时就会报错没有足够空间。而 LaTeX 先根据是否是 eTeX 和 LuaTeX 将 \e@alloc@top 赋值为 255/32767/65535 。再增加了一个条件判断,当 \newcount\newinsert 都小于 255 且重叠时,\newcount 从 256 到大开始分配寄存器, \newinsert\e@alloc@top 到小开始分配寄存器。只有再次重叠时才会报错。故在笔者看来,在绝大多数 TeX 编译器兼容 eTeX 的如今,继续使用 plainTeX 类似在 64 位电脑上运行 32 位程序,是一种不能充分利用资源的行为。

对三种编译器 pdfTeX, XeTeX, LuaTeX 和三种 TeX 方言 plainTeX, LaTeX, ConTeXt, 我们可以造出九种组合 pdfTeX, pdfLaTeX, pdfConTeXt, 以此类推。这样的组合称为格式。 一般用编译器决定格式名的前缀,用语言决定格式名的后缀。对 plainTeX ,就用编译器名作为格式名。那么对于 Alpha, Omega 这样名字不带 TeX 的编译器,它们和 LaTeX 等组合的格式会使用专门的名字,比如 Lamed, Lambda 。

C/C++ 支持将包含的头文件预编译成 pch 文件加快编译速度:

类似的,使用后缀名 ini 的 TeX 初始化文件也可以被预编译为后缀名 fmt 的二进制格式文件:

luatex --ini XXX.ini
luatex --fmt XXX.fmt main.tex

每次编译 main.tex 都需要通过命令行传入 --fmt XXX.fmt,太麻烦了。我们知道所有可执行文件都有一个文件名:

main.c:

#include <libgen.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(int argc, char *argv[]) {
  char *name = basename(argv[0]);
  if (strcmp(name, "luatex") == 0) {
    puts("now use luatex.fmt");
  } else if (strcmp(name, "lualatex") == 0) {
    puts("now use lualatex.fmt");
  } else {
    puts("not support!");
    return EXIT_FAILURE;
  }
  return EXIT_SUCCESS;
}

我们可以通过检测文件名执行不同的代码逻辑:

$ cc main.c -o luatex
$ ./luatex
now use luatex.fmt
$ cp luatex lualatex
$ ./lualatex
now use lualatex.fmt

于是,所有 TeX 编译器约定:

不过 texlua 会直接运行一个 lua 的解释器,不会调用 texlua.fmt

笔者提供了 plainTeX, LaTeX, TeXinfo 与 LuaTeX 组合的格式文件,没有提供 ConTeXt 是因为这种语言笔者从未用过。

此外, TeX 的编译器有一个特性:当编译遇错的时候会停下来询问怎么办。在静默安装时这会无限等待下去。请添加 --interaction=nonstopmode 。(别问笔者怎么发现的 QAQ)

依赖

CTAN 时一个专门收录 TeX 相关的项目的网站,然而相比 PYPI, npmjs.org, luarocks.org, CTAN 的包没有依赖信息。笔者通过将包发布到 luarocks.org 来利用其依赖信息。注意到和 LuaTeX 相关的 TeX 包中会有大量 Lua 代码,这样做其实挺合理。

另一个例子是 NeoVim 的包管理器 rocks.nvim 。 NeoVim 是一个新的 vimscript 解释器,正如 LuaTeX 与 TeX 的关系一样, NeoVim 相比 vim 内置了一个 LuaJIT 。 rocks.nvim 通过利用 luarocks 来声明各种 Vim 插件的依赖关系。顺带一提, rocks.nvim 3.0 将用 rust 重写底层,有望成为最快的 Vim 包管理器。

Luarocks 支持声明构建时依赖和运行时依赖。对格式文件而言,生成格式文件的 TeX 源代码所在的包就是构建时依赖。对于普通的包, \RequirePackage{} 的 LaTeX 代码和 \input 的 TeX 代码所在的包就是运行时依赖。

Lua

LuaTeX 和 NeoVim 的某些相似的 API 如下:

Lua 的引入允许用户更好的看清 TeX 的一些细节。 An Introduction to LuaTeX (Part 2): Understanding \directlua 给出了一张图解释 LuaTeX 的词法分析:

token

每个 token 都会计算一个 token value, 原文给出了 token value 的计算公式。 但如果利用 LuaTeX 提供的 API ,可以直接看到计算后的结果:

REPL

这个 REPL 的代码在

更多 API 请查询 LuaTeX 文档 。确保你拥有一门 TeX 方言的先验知识。笔者推荐 plainTeX ,因为它真的是太简单了,对照 TeX 急就帖,高德纳写的 1200 行代码一天就可以读完。

标准

Lua 的包搜索路径取决于 package.pathpackage.cpath:它们是用分号连接的一组路径,用于 Lua 脚本和二进制 Lua 模块。 NeoVim 额外提供了 vim.o.runtimepath , 用逗号连接的一组路径,用于 Lua 脚本和 vimscript 脚本。 LuaTeX 则额外提供了 kpse.lookup() 用于搜索 Lua 脚本和 TeX 文件。 kpse 是 kpathsea (卡尔路径海) 的一个 Lua 模块,其配置文件 texmf.cnf 中的 LUAINPUTS, TEXINPUTS 是用分号连接的一组路径。只需要再每次安装完后更新 texmf.cnf 即可让新安装的 TeX 包被找到。

此外, Unix 有 XDG 标准,规定字体文件路径在 ${XDG_DATA_HOME:-$HOME/.local/share}/fonts 下。而 TeX 有 TDS 标准规定字体文件按文件类型路径在

可以在 texmf.cnf 中添加 XDG 字体路径到 OPENTYPEFONTS 等变量中兼容 XDG 。

同样的,对于 texmf.cnf 默认的一些路径:

TEXMFHOME = ~/texmf
TEXMFVAR = ~/.texlive2023/texmf-var
TEXMFCONFIG = ~/.texlive2023/texmf-config

参考 ArchLinux wiki 后指定:

TEXMFHOME     = $XDG_DATA_HOME/texmf
TEXMFVAR      = $XDG_CACHE_HOME/texmf
TEXMFCONFIG   = $XDG_CONFIG_HOME/texmf

笔者将一些常见的包如 PGF/TikZ, beamer 打包了。

将目前所有包安装在本地后,占据空间大概如下:

作为比较,以下是其他文档排版系统的大小(没有包含字体):

很多东西仍然是缺失的:

总之,相较 NeoVim 仅靠 Lua 就可以满足近乎所有需求,一个仅靠 LuaTeX 的生态仍然缺失大量工具软件。但一个去除 pdf 文档后仅占据 143.7 MiB 就可以绘制如下流程图的 LaTeX 发行版,也何尝不会是一个类似 rocks.nvim 的好的开始呢?

% 施法前摇
\documentclass[tikz]{standalone}
\usetikzlibrary{arrows.meta, quotes, graphs, graphdrawing, shapes.geometric}
\usegdlibrary{layered}
\usepackage{hyperref}
\usepackage{hologo}
\title{graph}
\begin{document}
\begin{tikzpicture}[rounded corners, >=Stealth, auto]
  \graph[layered layout, nodes={draw, align=center}]{

    % 开始施法
    "\TeX" -> "\hologo{eTeX}" -> "\hologo{pdfTeX}" -> "\hologo{LuaTeX}";
    "\hologo{eTeX}" -> "\hologo{XeTeX}"

    % 施法后摇
  };
\end{tikzpicture}
\end{document}

graph

https://user-images.githubusercontent.com/32936898/199681341-1c5cfa61-4411-4b67-b268-7cd87c5867bb.png https://user-images.githubusercontent.com/32936898/199681363-1094a0be-85ca-49cf-a410-19b3d7965120.png https://user-images.githubusercontent.com/32936898/199681368-c34c2be7-e0d8-43ea-8c2c-d3e865da6aeb.png