#!/usr/bin/env python3 """Compile a LaTeX project with cached Tectonic resources and shell escape disabled.""" from pathlib import Path import re import shutil import subprocess import sys import tempfile from _pdf_common import ( SkillArgumentParser, output_pdf, new_temp_pdf, publish_temp_file, run_cli, ) def compile_document(args): from pypdf import PdfReader source = Path(args.input).expanduser().resolve() if ( not source.is_file() or source.suffix.lower() != ".tex" or not 0 < source.stat().st_size <= 2 * 1024 * 1024 ): raise ValueError("输入需为不超过 2 MiB 的本地 .tex 文件") if not 1 <= args.timeout <= 600: raise ValueError("timeout 必须在 1–600 秒之间") executable = shutil.which("tectonic") if not executable: raise RuntimeError("基础镜像缺少预置 Tectonic,需要更新镜像") target = output_pdf(args.output, args.overwrite) temporary = new_temp_pdf(target) try: with tempfile.TemporaryDirectory(prefix="pdf-latex-") as folder: command = [ executable, "--untrusted", "--only-cached", "--keep-logs", "--outdir", folder, str(source), ] completed = subprocess.run( command, cwd=source.parent, capture_output=True, text=True, timeout=args.timeout, check=False, ) result = Path(folder) / (source.stem + ".pdf") messages = (completed.stdout + "\n" + completed.stderr).splitlines() if completed.returncode or not result.is_file(): detail = "\n".join(messages[-15:]) raise RuntimeError( "LaTeX 编译失败;缺失 TeX 包需在基础镜像构建时预置,任务中不下载:" + detail ) count = len(PdfReader(result).pages) if not count: raise ValueError("LaTeX 没有生成有效 PDF") logfile = Path(folder) / (source.stem + ".log") if logfile.exists(): messages += logfile.read_text(errors="replace").splitlines() warnings = list( dict.fromkeys( line.strip() for line in messages if re.search( r"warning:|Overfull|Missing character|undefined references", line, re.I, ) ) )[:30] shutil.copyfile(result, temporary) publish_temp_file(temporary, target, args.overwrite) finally: temporary.unlink(missing_ok=True) return { "source": str(source), "path": str(target), "page_count": count, "engine": "tectonic", "dependency_mode": "cached-only", "warnings": warnings, "requires_visual_review": True, } def main(argv=None): parser = SkillArgumentParser( description="固定 LaTeX 编译接口,禁用 shell escape,只使用镜像内缓存包" ) parser.add_argument("--input", required=True) parser.add_argument("--output", required=True) parser.add_argument("--timeout", type=int, default=180) parser.add_argument("--overwrite", action="store_true") return run_cli( lambda: compile_document( parser.parse_args(sys.argv[1:] if argv is None else argv) ) ) if __name__ == "__main__": raise SystemExit(main())