打开一个 LaTeX 宏包的发行包,你可能根本找不到 .sty。取而代之的是 .dtx 与 .ins 两个文件——一种把代码与解说共置于同一文件的文学式编程实现。实际把 booktabs 的 .dtx 交给 docstrip,会看到 Lines processed: 1053 / Comments removed: 882 / Codelines passed: 161。1053 行里真正的代码只有 161 行,其余 85% 都是散文。把同一个文件交给 pdflatex,那些散文就变成排好版的 PDF 手册。本页从「把你反复粘进导言区的宏整理成自己的 .sty」讲起,一直讲到编写 .dtx、测试,以及发布到 CTAN。
.sty 的骨架——写上日期就能做版本检查
宏包就是一个 .sty 文件,或在你的项目里,或已装在某处。开头两行是自我声明:\NeedsTeXFormat{LaTeX2e} 说明所需格式,\ProvidesPackage{name}[date version description] 声明宏包名与版本。名称必须与文件的 basename 一致。方括号里的内容可以省略,但写上会有实实在在的回报:用户此后可以用 \usepackage{mypackage}[2027/01/01] 指定最低所需日期,若手头的版本较旧就会出现 LaTeX Warning: You have requested, on input line 2, version '2027/01/01' of package mypackage, but only version '2026/08/17 v1.0 ...' is available. 日期须写成 YYYY/MM/DD 形式。这一行,会让若干年后「怎么就是跑不起来」的来信少一封。
正文部分,用 \RequirePackage{...} 引入依赖——它相当于 .sty 内部的 \usepackage。要用颜色就加载 xcolor,要绘图就加载 tikz,依此类推。想告诉用户什么,就用 \PackageWarning{name}{message};若已无法继续,则用 \PackageError{name}{message}{help}。二者都以宏包名作为第一个参数,读日志的人一眼就能看出消息来自何处。至于接收 \usepackage[option]{name} 形式选项的机制——\DeclareOption 配 \ProcessOptions,以及当前内核提供的 \DeclareKeys / \ProcessKeyOptions——与文档类是共通的,已整理在类那一页。写宏包时步骤完全相同。你在较旧的 .sty 中会遇到的 kvoptions 宏包,是同一思路的前一代工具。
\NeedsTeXFormat{LaTeX2e}
\ProvidesPackage{mypackage}[2026/08/17 v1.0 My helpers]
\RequirePackage{xcolor}
\newcommand{\greet}[1][world]{Hello, \textcolor{blue}{#1}!}
\endinput自己动手写 .dtx 与 .ins——%<*package> 是什么意思
.dtx 的机关简单得近乎朴素:行首的 % 把解说与代码分开。以 % 开头的行是解说,其余是代码。所以同一个文件可以有两种读法。运行 tex mypackage.ins,docstrip 丢掉解说并写出 .sty;运行 pdflatex mypackage.dtx,解说则被排为正文,代码带行号引出。要抽取哪些片段,由 %<*package> 与 %</package> 这样的 卫标 标出,而 .ins 中的 \generate{\file{mypackage.sty}{\from{mypackage.dtx}{package}}} 表示「把 package 卫标内的内容写进 mypackage.sty」。卫标名字可以自取,所以一个 .dtx 能一并生成 .sty、.cls 和配置文件。写在 \preamble … \endpreamble 之间的文字,会以注释形式加到每个生成文件的开头——那里正是放许可声明的位置。
% \iffalse meta-comment
% Copyright (C) 2026 Example Author
% This work may be distributed and/or modified under the conditions of
% the LaTeX Project Public License, version 1.3c or later.
% \fi
%
% \iffalse
%<*driver>
\documentclass{ltxdoc}
\usepackage{mypackage}
\EnableCrossrefs
\CodelineIndex
\begin{document}
\DocInput{mypackage.dtx}
\PrintIndex
\end{document}
%</driver>
%<package>\NeedsTeXFormat{LaTeX2e}
%<package>\ProvidesPackage{mypackage}
%<package> [2026/08/17 v1.0 A demonstration package]
% \fi
%
% \title{The \textsf{mypackage} package}
% \author{Example Author}
% \maketitle
%
% \section{Usage}
% \DescribeMacro{\greet}
% |\greet| prints a greeting; the optional argument sets the name.
%
% \StopEventually{}
%
% \section{Implementation}
% \begin{macrocode}
%<*package>
\RequirePackage{xcolor}
\newcommand{\greet}[1][world]{Hello, \textcolor{blue}{#1}!}
%</package>
% \end{macrocode}
% \Finale
\endinput解说一侧要用的命令屈指可数。\DocInput{文件} 是把 .dtx 自身读回来的主力,而 %<*driver> … %</driver> 围起来的部分就是为此准备的小型文档设置(使用 ltxdoc)。实现代码用 \begin{macrocode} … \end{macrocode} 括起,惯例是围栏行缩进四个空格。\DescribeMacro{\命令} 在面向用户的正文中突出该命令,并登记进索引。\StopEventually{} 标出「从这里开始是实现」的分界,在生成只给用户看的简短版本时会起作用。最后的 \Finale 收拾索引与变更历史。把上面的 .dtx 配一个六行的 .ins 实际跑一遍,会显示 Lines processed: 41 / Comments removed: 24 / Codelines passed: 10,而生成的 .sty 开头会自动加上 %% This is file 'mypackage.sty', generated with the docstrip utility. 这样一句说明。
\input docstrip.tex
\keepsilent
\preamble
Generated from mypackage.dtx -- do not edit this file directly.
\endpreamble
\generate{\file{mypackage.sty}{\from{mypackage.dtx}{package}}}
\endbatchfile.dtx 会自行老化——booktabs 已经排不出来了
写 .dtx 之前有个值得知道的坑:.sty 可以照常工作,而 .dtx 却排不出来了。在 TeX Live 2024 上运行 pdflatex booktabs.dtx,会抛出 127 个错误、完全不生成 PDF。第一个错误是 ! Improper alphabetic constant,停在 \CharacterTable 附近。而 booktabs.sty 本身完全正常。原因在于文档一侧的地基换了:TeX Live 2024 的 doc.sty 是 v3.0m(2022/11/13),即 Frank Mittelbach 重写的「V3」,而 booktabs.dtx 仍停留在 2020 年 1 月。发行包中随附的 booktabs.pdf 也是 2020 年 1 月的日期,此后从未重新生成。作为对照,在同一个 TeX Live 2024 上,multirow.dtx(30 页)、tabularx.dtx(12 页)、array.dtx(35 页)都能毫无警告地排出,所以这不是机制的缺陷,而是维护问题。自己的 .dtx,每次发布都要真的排一遍。
不必从白纸开始。TeX Live 随附一份名为 dtxtut 的入门文档,并附有 skeleton.dtx 与 skeleton.ins 这一对模板。许多宏包由抄写它起步的痕迹,留在了意想不到的地方:skeleton.ins 第 18 行写着 \usedir{tex/latex/skeleton},而完全相同的一行至今仍留在 booktabs.ins 第 36 行——模板里的 skeleton 从未被改名。检索 TeX Live 2024 中 source/latex 下全部 1402 个 .ins 文件,仍带着这处残留的只有 booktabs 一个。这细节颇为可爱,但教训很清楚:若从模板起步,发布前先搜一遍模板的名字。
要用 expl3 来写,就用 \ProvidesExplPackage
若打算用 expl3 来写实现,就把声明行换成 \ProvidesExplPackage{name}{date}{version}{description}。除了参数拆成四个之外,它还有一个重要性质:该命令在最后会执行 \ExplSyntaxOn。也就是说,从声明的下一行起,不必写一次 \ExplSyntaxOn 就能使用 expl3 语法。下面的 .sty 里一处 \ExplSyntaxOn 也没有,但 \tl_new:N 与 \NewDocumentCommand 都照常工作。expl3 本身的读法见 expl3 一页。
\NeedsTeXFormat{LaTeX2e}
% four arguments, and it turns on expl3 syntax by itself
\ProvidesExplPackage{expldemo}{2026/08/17}{1.0}{Expl demo}
\tl_new:N \l_expldemo_tl
\tl_set:Nn \l_expldemo_tl { from~expl3 }
\NewDocumentCommand \shout { } { \tl_use:N \l_expldemo_tl }测试与打包发布:l3build
与其每次手敲 tex mypackage.ins 和 pdflatex mypackage.dtx,不如交给 LaTeX Project 维护的 l3build(TeX Live 2024 随附的是 2024-02-08 版)。在项目根目录放一个 build.lua,写上模块名与文件清单,l3build unpack 就会运行 .ins 并把 .sty 生成到 build/unpacked/,l3build doc 则排出 .dtx 并在 build/doc/ 下生成 PDF。好处是生成物不会弄脏工作目录。l3build check 是测试机制:它把 testfiles/ 中的 .lvt(测试文档)与 .tlg(预期日志)逐一比对,输出一有变化就给出差异。当排版结果由机器而非肉眼把关,重构就不再可怕。
module = "mypackage"
sourcefiles = {"mypackage.dtx", "mypackage.ins"}
installfiles = {"mypackage.sty"}
uploadconfig = { pkg = "mypackage" }
-- l3build unpack -> build/unpacked/mypackage.sty
-- l3build doc -> build/doc/mypackage.pdf
-- l3build check -> run testfiles/*.lvt against *.tlg
-- l3build ctan -> mypackage-ctan.zip, ready to upload发布到 CTAN:zip 里放什么,以及许可证
运行 l3build ctan,就会得到一个可直接上传的 zip。里面是一个以宏包命名的目录 mypackage/,装着源文件(.dtx 与 .ins)以及编译好的 PDF 手册。不放 .sty 是这个圈子的惯例,接收方会自行用 .ins 生成。再加上 README 与变更日志,形制就齐整了。不过要当心:l3build ctan 会把工作目录里找到的 PDF 一并收拢,所以散落的试排 PDF 也会被打包进去——打包后用 unzip -l 列一遍内容确认。上传本身通过 CTAN 的网页表单完成,但 TeX Live 也随附了 ctan-o-mat,可以用一个写有描述、联系方式与许可证的配置文件,从命令行完成校验与上传。
许可证必须事先定下来。 CTAN 要求明确写出分发条件,TeX Live 的宏包信息里也会显示。这一圈子的事实标准是 LPPL(LaTeX Project Public License);如何选择、各版之间有何差异——尤其是「改动后须改名」那一条究竟出现在哪一版的哪里——在许可证一页有详述。定下的条件请写在三处:.dtx 开头的元注释、.ins 的 \preamble(它会进入每个生成文件的开头,因此只拿到 .sty 的人也能看到),以及 README。做到这一步,你的宏包才会成为若干年后某个陌生人能用 texdoc 打开的东西。
\ProvidesPackage的日期务必写成YYYY/MM/DD。 没有它,用户就无法指定版本。- 每次都要真的排一遍
.dtx。.sty照常而.dtx落败,是真实发生过的事(见 booktabs)。 - 若从模板起步,就搜一遍模板的名字。
skeleton的残留在 TeX Live 2024 里还活着一处。 - 用
unzip -l检查l3build ctan生成的 zip。 它会捡走工作目录里的 PDF。 - 把许可证写在三处:
.dtx、.ins的\preamble、README。 它必须能传达给只拿到.sty的人。