请问,下面的常规编译命令改用 l3build 的方式要怎么操作最方便啊?
我的目的是得到一本完整的 PDF 手册。
下面的代码是使用传统的 DocStrip 的方式所用到的编译命令。
latex hello.ins
xelatex hello.dtx
makeindex -s gglo.ist -o hello.gls hello.glo
makeindex -s gind.ist -o hello.ind hello.idx
xelatex hello.dtx
xelatex hello.dtx不知道为什么命令 l3build unpack 不能释放出 hello.sty 文件,会卡在 \usepackage{xeCJK} 这一行。然后我就问了 AI ,并用了下面的命令来编译:
latex hello.ins
l3build doc然后因为我看不懂 l3build 文档,所以 build.lua 文件(上传失败)的代码也是 AI 生成的:
module = "hello"
-- 关键:禁止 l3build 尝试管理打包过程
packtds = 0
-- 关键:清空 unpackfiles,表示“不需要解包”
unpackfiles = {}
-- 明确源文件目录
sourcefiledir = "."
sourcefiles = {"hello.dtx", "hello.sty"} -- 将已存在的 .sty 也列为源文件
-- 文档编译设置
docfiledir = "."
docfiles = {module..".dtx"}
typesetfiles = docfiles
typesetexe = "xelatex"
typesetopts = "-interaction=nonstopmode"
typesetindexes = {
["hello.glo"] = {"hello.gls", "makeindex -s gglo.ist -o hello.gls hello.glo"},
["hello.idx"] = {"hello.ind", "makeindex -s gind.ist -o hello.ind hello.idx"}
}
typesetruns = 3
-- 可选:在清理时保留 .sty 文件
function clean_init()
-- 不删除 hello.sty
rm("hello.log", "hello.aux", "hello.glo", "hello.idx", "hello.gls", "hello.ind", "hello.ilg", "hello.out")
end
Hello~ l3build 重度使用者来咯~
给你简单写了个最基础的 build.lua,你执行 l3build doc 即可顺利编译
(注意到你加载了一些 Windows 特色字体,由于我是 macOS,所以注释了这些行,用 fandol 字体代替)
完整的 build.lua 在末尾,我现在给你一行行解释
module = "hello"这行表示声明你这个项目的名称,也就是包名字,这个变量会被 l3build 内部很多函数调用.
ctanzip = module这个表示上传CTAN打包时的压缩包名称,那么 module 定义为 hello,那么最后压缩文件名为 hello.zip
cleanfiles = {"*.log", "*.pdf", "*.zip", "*.curlopt"}l3build 存在一个选项 clean,这个就是打包 上传等过程中会生成的缓存文件,执行 l3build clean 会清理掉
excludefiles = {"*~"}这个嘛...就是不包含的文件,意思是打包上传 CTAN 时不会被塞入压缩包,原来的值是
excludefiles = {"*~","build.lua","config-*.lua"} 但是我把后面俩去掉了——我有个习惯,就是把打包文件也上传 CTAN,这个是被 CTAN 允许的
textfiles = {"*.md", "LICENSE", "*.lua"}这个就是很没用的那种纯文本文档,但是能够起声明版权等作用,最后会被包含到 zip 里.
上一行我说我想把 build.lua 塞进去,那我就在这里用了个 dirty trick.
typesetexe = "latexmk -xelatex"嗯 很明显这行是排版的程序,其实官方手册并不允许这样做,你使用 latexmk 会报错,但是我在 build.lua 末尾做了些手段 :-),我就想用 latexmk(傲娇)
typesetruns = 1那么既然我们用 latexmk,运行一次就够啦(l3build 足足运行了 3回啊3回!!!)
好啦!完整文件如下,并附上完成 build 的结果
module = "hello"
ctanzip = module
cleanfiles = {"*.log", "*.pdf", "*.zip", "*.curlopt"}
excludefiles = {"*~"}
textfiles = {"*.md", "LICENSE", "*.lua"}
typesetdemofiles = {module .. "-demo.tex"}
typesetexe = "latexmk -xelatex"
typesetruns = 1
--[== "Hacks" to `l3build` | Do not Modify ==]--
function docinit_hook()
cp(ctanreadme, unpackdir, currentdir)
return 0
end
function tex(file,dir,cmd)
dir = dir or "."
cmd = cmd or typesetexe
if os.getenv("WINDIR") ~= nil or os.getenv("COMSPEC") ~= nil then
upretex_aux = "-usepretex=\"" .. typesetcmds .. "\""
makeidx_aux = "-e \"$makeindex=q/makeindex -s " .. indexstyle .. " %O %S/\""
sandbox_aux = "set \"TEXINPUTS=../unpacked;%TEXINPUTS%;\" &&"
else
upretex_aux = "-usepretex=\'" .. typesetcmds .. "\'"
makeidx_aux = "-e \'$makeindex=q/makeindex -s " .. indexstyle .. " %O %S/\'"
sandbox_aux = "TEXINPUTS=\"../unpacked:$(kpsewhich -var-value=TEXINPUTS):\""
end
return run(dir, sandbox_aux .. " " .. cmd .. " " ..
upretex_aux .. " " .. makeidx_aux .. " " .. file)
end
其实后边 hack 也很简单:
function docinit_hook()
cp(ctanreadme, unpackdir, currentdir)
return 0
end钩子,见过没?LaTeX里也有 \AddToHook 😋l3build 提供了钩子 docinit_hook(),l3build ctan 的过程分大概三部:unpack (就是抽离 sty), typeset (编译手册), 和 tar (压缩包);那么 docinit_hook() 就发生在 typeset 前,或者说 unpack 后,因为我喜欢把譬如 README.md 打包到 dtx 里,但是它又需要在GitHub首页展示,所以我一旦更改 README,我会在 dtx 里改,然后这样每次 build 时就会覆盖掉当前仓库根目录下的 README. cp 命令的用法是 cp(文件, 源路径, 新路径). unpackdir 就是每次执行 l3build unpack 等命令后目录出现的 ./build/unpacked, currentdir 就是仓库的根目录.
这段有点小难... 也是我想很久写出来的, 就是强制 latexmk
这里相当于重新定义 l3build 里原有的 tex 函数了,吃仨参数:文件,路径,命令
function tex(file,dir,cmd)如果 dir 被定义了,那么这里临时变量 dir 就是等于被定义的 dir 的值;否则就是根目录;cmd 也同样
dir = dir or "."
cmd = cmd or typesetexeWindows 系统的黑锅(发怒)
if os.getenv("WINDIR") ~= nil or os.getenv("COMSPEC") ~= nil then如果是Windows系统,那么有几个地方就是双引号
upretex_aux = "-usepretex=\"" .. typesetcmds .. "\""
makeidx_aux = "-e \"$makeindex=q/makeindex -s " .. indexstyle .. " %O %S/\""
sandbox_aux = "set \"TEXINPUTS=../unpacked;%TEXINPUTS%;\" &&"
else否则就是单引号,记得转译斜杠(这个跟 l3build 无关,不会的恶补下 Linux)
好了,介绍下这些分别是啥,请打开 latexmk 手册
upretex_aux: 比如哈,你的项目里有 hook (最简单的:隐藏答案!) 你希望不改变文件比如 \documentclass[hideanswer] 这种窝囊的方式,那么可以在编译时加料, 例如你可以 latexmk -usepretex='\\AtBeginDocument\\DisableImplementation' (这是 l3doc 里的例子,注意 \\ 转译),那么这时你可以在前面定义 typesetcmds 这个变量,实现加料,此时 upretex_aux 这个临时变量被定义为 -usepretex=\'\\AtBeginDocument\\DisableImplementation\'. upretex_aux = "-usepretex=\'" .. typesetcmds .. "\'"众所周知,bib 参考文献有 bst 格式文件,那么索引同样. 逆天 l3doc 偏要使用 gind.ist 索引文件,所以 latexmk 也有设置索引文件style的接口,剩下的 makeidx_aux 和 upretex_aux 同理,自己对着 latexmk 手册慢慢看吧... 对了 makeidx_aux 和 upretex_aux 这俩变量名是我瞎写的,只要后面调用时一致即可.
makeidx_aux = "-e \'$makeindex=q/makeindex -s " .. indexstyle .. " %O %S/\'"沙河机制,是的,你会发现每次 unpack 后 .sty 都跑到了 ./build/unpacked 里,而 ./build/doc 里却没有,这是因为 l3build 官方也采用了沙河机制,默认情况下比如 latexmk 也好,或者单独 pdflatex 也好,都会优先检索当前文件夹,然后在检索系统 texmf(貌似是?反正就是你 tlmgr update 后包安装的位置), 那么这个路径是可以改(往上叠加的,把 ./build/unpack 也给他加进去!)
sandbox_aux = "TEXINPUTS=\"../unpacked:$(kpsewhich -var-value=TEXINPUTS):\""
end好了,这几个临时变量定义完了,接下来开始调用他们!
这个函数返回什么呢?run 是官方的一个借口(你可以理解为元语吧),在 argument dir (回忆:函数定义为 function tex(file, dir, cmd),这个 dir 就好比 #2)下,然后向命令行输出这些内容,这些内容已经被定义好.
return run(dir, sandbox_aux .. " " .. cmd .. " " ..
upretex_aux .. " " .. makeidx_aux .. " " .. file)
end这只是简单的例子,正常情况下还有其他的东西
你可以看 install-LaTeX-guide-zh-cn 仓库的 build.lua (是的,也是我写的),但是那个因为我和啸行老师的习惯不同,所以一些过于 "超前" 的功能我没加进去,你可以看下我的 litetable 包的 build.lua,
https://github.com/myhsia/litetable/blob/main/build.lua
这个包情况复杂:dtx 文件是纯英文写的,那么 latexmk -pdf 编译;但是还有普通话和粤语手册,自然要用 xe. 这个包是个很好的例子,你可以看下他的 build.lua.
同时这个包还提供了 demo 文件,并且还要保证 demo 文件先被 typeset 并丢到 ./buiild/doc 里从而三语言文档会用 pdfpages 的 \includepdf 去插入 demo ...
同时,还有 tag 功能,一键修改 dtx 文件里的日期和版本号...
还有上传 CTAN 功能,输入CTAN包管理员个人信息等
所以略微复杂,但是我觉得你如果能学完这个包的 l3build,你就基本上能在 l3build 横着走了,接下来你就要去官方 GitHub repo去看源代码,从而指导如果想实现一些自定义功能要改什么函数
感谢大佬耐心解答!不过目前,我还需要慢慢消化下第一段知识才行。hack部分和litetable这些准备一起看,这部分太难了...
@u101077 加油~其实你也可以不用 latexmk 就用默认的 3 次
xelatex, 使用latexmk这个主意是曾祥东最初提出来的(那时候我甚至还在上小学😊)https://github.com/latex3/l3build/issues/113
见群里 2026-01-25 原文:
还有就是不要用 Ai,很多接口随时都在更新,而且 Ai 会给一些冗余的东西,比如 packtds,这个本来就是默认禁掉的(除非你写 ctex-kit 那么大的项目😄,需要打包 tds);当然,我的 litetable 有自动 tag 更新日期版本号的功能,这个可以用 Ai,毕竟要用到很多正则表达式,我也记不全
通常,我的顺序是:经验->文档->AI->提问。
用您给的配置文件编译了几次,有两点疑问:
(1)作为最终结果的 hello.pdf 在文档末尾缺少 Change History 这一部分内容,latexmk 似乎没有调用两次 makeindex 处理?
(2)能不能在构建完成后自动把 ./build/unpacked/hello.sty 拷贝到当前根目录中?就像命令 latex hello.ins 所做的那样。
l3build官方其实不推荐用latexmk. 除非latexmk官方支持,否则只能编译一次. 当然如果你懂latexmk知道有方法实现,你可以给我截图那段手册,然后我会给你在build.lua中 hack.目前解决办法是

typesetruns = 2即可,但是这样有些"低能",因为 xelatex 也会被 double,即跑4次 :-)hack,很容易实现,先上截图要更改的部分是
docinit_hook这个钩子请看
l3build, 有个installfiles这个变量,默认值为{*.sty, *.cls}, 其中*为正则,意思是所有的sty和cls文件;而且这个变量数据类型是个数组.所以,你需要把这个变量里的文件一个个从
unpackdir复制到currentdir,就需要写循环,我觉得这个循环没啥要解释了... 很 Python, 很容易理解 😊@u79794 附:解决问题2后的完整
build.lua