Repository navigation
🐛 Bug 修复
- 🔴
pytexmk -l一直编译到安全上限(11 次):bib_judgment()里bib_file_path = Path(self.bib_file)拿的是.aux中\bibdata{References}的裸文件名(无.bib后缀),Path("References").exists()永远为 False,于是每一轮都重新判定"文献需要 2 次编译";而_is_log_stalled()要求bib/idx/toc/aux/out全部归零才肯判定停滞,只要 bib 维度非零就一路空转到max_extra_compilations=10(合计 11 次)。修复三处:- 新增
_resolve_bib_files(),按<name>.bib→<name>逐个探测(支持\bibdata{a,b}逗号分隔多文件),self.bib_files保存真实存在的路径 - 判定改为"引用计数稳定 且
.bbl存在 且 日志无No file *.bbl/Citation undefined且.bbl不比.bib旧 → 归零";找不到任何.bib时直接归零并告警(bibtex 无法产出.bbl,继续重跑只是空转) - 删除
self.out相关的死代码(该属性只在__init__赋值"",此后从未更新,re.search(..., self.out)恒为 False),改用真实.log内容
- 新增
- 🔴 收敛判据过于脆弱,任何一个维度卡住就跑到上限:
_is_log_stalled()的"全维度归零 + 相邻两轮日志签名相同"条件在实践中几乎不可能满足(lineno 的\def\@LN@column{N}会让.aux每轮都变)。改为状态指纹不动点检测_convergence_stop_reason():- 新增
CompilationDetector.state_fingerprint():把.aux/.out/.toc的归一化内容、各索引系统输入、日志警告签名、.bbl大小合成一个 sha256 指纹 - 指纹等于上一轮 →
fixed_point;指纹等于更早轮次 →cycle(两态震荡);没有任何变更类维度非零 →log_stalled(日志仍有警告)或stable log维度被明确排除在"可推进维度"之外(ACTIONABLE_DIMS),它只是警告信号,继续编译无法消除- 结果:指纹是"整体是否原地踏步"的最终裁判,某个维度因宏包 churn 长期非零也不会再拖满 10 轮
- 新增
- 🔴 循环内从不重跑 bibtex/makeindex,
bib/idx维度必然长期非零:原实现只在循环外跑一次compile_bib/compile_index,之后每轮run_full_detection又报告"需要文献/索引处理",而循环体丢弃了返回的bib_engine/index_run_cmds。修复:循环内就地补跑,并用aux_signature()/index_signature()去重,保证"同一输入状态只调用一次外部工具" - 🔴 中断的编译留下截断辅助文件,之后每次编译都在同一处失败:Ctrl+C 或关闭终端时 LaTeX 正在写
.aux,磁盘上留下半截文件(例如 16384 字节、末尾停在\@writefile{toc}{\contentsline),下一次编译直接报! File ended while scanning use of \@writefile.+==> Fatal error occurred,且此后每次重现——因为坏文件还在。新增aux_repair.py:编译前检查.aux/.toc/.lof/.lot/.out/.bbl/.idx/.ind/.nlo/.nls/.glo/.gls/.nav/.snm,按"花括号是否配平"+"文件大小是否正好是 4 KiB 整数倍却无结尾换行"识别截断文件,删除后由本轮编译重建 - 🟡
-ls在根.pytexmkrc缺失时是空操作:write_subprojects_to_rc()遇到不存在的文件直接返回 False,_list_subprojects()只打印"请先运行 pytexmk -i",于是-ls扫描出了清单却什么都没保存,紧接着的-s <名字>必然失败。修复:-ls先用模板自动创建根配置再写入[subprojects] - 🟡
-s子项目解析脆弱:build_subprojects()只读持久化的[subprojects],别名表为空就抛ProjectNotFoundError("扫描得到却编译不了");深层子项目的别名是完整相对路径(如Section/en02),用户写en02匹配不上;条目直接指向.tex时把文件路径交给os.chdir()会抛NotADirectoryError。修复:新增discover_subprojects()兜底扫描、_match()四级匹配(精确别名 → 大小写不敏感 → 相对路径后缀 → 叶子目录名,并兼容反斜杠)、ProjectContext.work_file记录.tex条目并把work_root落到其父目录 - 🟡
abbreviations_num固定 13 元组:编译次数超过 13 会抛IndexError(v1.2.6 已从 6 扩到 13,只是把边界推远)。改为_ordinal()按需生成,上限调整不再有越界风险
✨ 新增
residual_warnings()收尾提示:编译结束后明确告诉用户"哪些警告是再编译也消不掉的"(未定义的交叉引用键名、.bib缺少条目、重复标签),避免用户误以为是编译流程的问题aux_repair独立 i18n domain(.po/.pot/.mo)
⚙️ 调整
MAX_EXTRA_COMPILATIONS从10降到8:正常情况下指纹不动点检测在 2~4 轮收敛,上限只用于防御"每轮都产生新指纹"的病态项目,把最坏情况限制在 9 次以内print_compile_report()的stalled参数保留为兼容层(等价stop_reason="log_stalled"),新增stop_reason/residual_notes参数,停止原因文案集中在STOP_REASON_MESSAGES常量表里
🧪 质量验证
pytest -q:68 passed(新增tests/test_compile_convergence.py31 例 +tests/test_subproject_resolution.py9 例)- 真实论文项目(30 页,
paper-01-dr-ym)冷启动全量构建:清空Auxiliary/+Build/后pytexmk -l→ 4 次 LuaLaTeX 收敛(bib 维度第 3 轮归零),bibtex 跑 2 次(第 1、2 轮,.aux变化后需要重跑)、makeindex 跑 1 次,随后去重生效 - 同一项目热态重建:2 次 LuaLaTeX 收敛,第 2 轮命中
fixed_point - 截断辅助文件自愈:把
main.aux截断到 16384 字节后直接pytexmk -l→ 自动删除并重建,3 次编译收敛,输出 PDF 与修复前字节完全一致(11530517 B,30 页) -s Review -l:子项目正常编译(1 次 LuaLaTeX,无额外空转)python -m compileall src/pytexmk:通过;所有新增 msgid 已同步到.po/.pot/.mo,LANGUAGE=en下译文正确加载
⚡ 收敛判定 v2:省掉确认轮 + 跨进程基线复用
承接上文:修复后冷启动仍需 4 次编译(实测第 4 轮),热态 1~2 次。多出的那一轮是纯粹的观察轮——辅助文件其实早已稳定,但因为"上一轮快照只活在内存里",进程每次都必须再编一次才能确认。本节把这一轮彻底消掉。
1. 单文件多命名空间状态(依赖 PyTeXLogs 0.4.0)
-
不新增任何辅助文件:
.pytexmk_refcache.json本来由依赖库pytexlogs写入(存引用缓存),现升级为多工具共享状态文件(schema v3): -
读写统一交给
pytexlogs.ProjectStateStore:命名空间隔离(各写各的键,不会互相覆盖)+ 原子写(临时文件 +os.replace)+ 轻量文件锁(超时降级,不阻塞编译) -
新增
src/pytexmk/build_state.py:BuildState负责产物摘要、源快照、收敛标记的业务读写;PyTeXMK 不自己碰 JSON
2. 判定矩阵:信号分类 + 省掉确认轮
核心变化是把日志信号分成两类(CompilationDetector.classify_signals()):
| 类别 | 内容 | 含义 | 决策作用 |
|---|---|---|---|
permanent |
未定义引用、Citation undefined、重复标签 |
只有改源码才能消除 | 不得作为"再编一轮"的理由 |
transient |
Label(s) may have changed、Rerun to get cross-references right、lastpage/rerunfilecheck rerun |
会随轮次消失 | 必须再编一轮让 LaTeX 自行确认 |
只有 transient 需继续编译,permanent 不能触发额外编译——这样“至少编一轮确认”就不再需要:
| 优先级 | 情形 | 判定 | 停止原因 |
|---|---|---|---|
| 1 | 本轮指纹等于上一轮 / 更早轮次 | 停 | fixed_point / cycle |
| 2 | bib/idx/toc/aux/out 全为 0 |
停 | stable |
| 3 | 仍有维度非零,但日志只剩 permanent 信号、无 transient | 停 | log_stalled(新增路径) |
| 4 | 仍有 transient 信号 / 维度非零且无信号信息 | 继续 | — |
- 第 3 条只保留为兜底:第 2 条已覆盖绝大多数情况,这里只防止“被永久信号拖着的伪需求”空转到上限
- 若
RERUN_LOG_PATTERNS顺序被调整,_PERMANENT_PATTERN_TOKENS会由正则文本自动重算,不会错位
3. 源文件未改动则整次跳过编译
- 新增
-fr / --force-rebuild参数;不指定时,满足以下全部条件则完全跳过编译:- 上次判定为收敛(
converged: true) - 所有
.tex源文件的(size, mtime_ns)与快照一致(用纳秒,避免“同秒内修改”漏判) - 产物摘要与基线一致,且输出 PDF 确实存在
- 上次判定为收敛(
- 源快照不递归别人的子项目:
Cover/、Review/这类自带.pytexmkrc的目录有自己的 PDF,改动它们不该触发主项目重编译;同时排除隐藏目录与Build/、Auxiliary/等产物目录 -dr/--draft与-fr永不跳过- 只有收敛的构建才写
converged:停在limit(安全上限用尽)不写,避免把未收敛的 PDF 当成成品
4. 清理与状态一致性
-c / -C:调用BuildState.discard(),只删pytexmk.build,保留pytexlogs.refs(引用缓存不属于 PyTeXMK)-ca / -Ca及状态文件为空骨架时自动删除文件- 所有状态读写异常一律降级(最多多编一轮),绝不阻断编译
5. 其他修正
- 首轮若所有变更维度已归零,直接判
stable,不再多编一轮(对应此前"行为退化:首轮必编 2 次") -s NAME支持按目录名 / 相对路径末段匹配(-s en02可命中Section/en02);ProjectContext新增work_file,条目直接指向.tex时会正确设置主文件-ls在根.pytexmkrc缺失时自动创建(此前只打印警告、扫描结果被丢弃,导致随后-s无处可查)
性能实测(paper-01-dr-ym,30 页)
| 场景 | 改造前 | 改造后 |
|---|---|---|
| 清除辅助文件后全量重建 | 4 次(约 16 s) | 4 次 |
| 无改动重复运行 | 2 次(约 8 s) | 0 次(0.3 s,直接跳过) |
| 修改某个章节后 | 2 次 | 1 次 |
测试
pytest -q:108 passed(新增tests/test_build_state.py24 例 +tests/test_signal_classification.py12 例,test_compile_convergence.py扩展判定矩阵用例)- 真实项目端到端:
-fr刷新快照 → 无改动 0.3 s 跳过 → 改章节立即重编 →-c只清pytexmk.build(pytexlogs.refs的 39 条引用缓存保留)→ 清理后首次编译自动重建 4 次 → 再跑即跳过 ruff check src/pytexmk tests:通过
{ "schema_version": 3, "pytexlogs": { "refs": { "keys": [...], "key_counts": {...} } }, "pytexmk": { "build": { "fingerprint": "...", "artifacts": {...}, "sources": {...}, "converged": true, "stop_reason": "stable", "signals": {...} } }, "jobs": { "main": { "updated_at": "..." } } }