113 lines
3.6 KiB
Python
113 lines
3.6 KiB
Python
#!/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())
|