import os import shutil import subprocess import tempfile from fastapi import FastAPI, HTTPException from fastapi.responses import Response from pydantic import BaseModel app = FastAPI(title="Tectonic LaTeX Compiler API") TECTONIC = "/app/tectonic" class CompileRequest(BaseModel): tex: str @app.get("/") def health(): return { "status": "ok", "service": "tectonic", "tectonic_exists": os.path.exists(TECTONIC), "tectonic_executable": os.access(TECTONIC, os.X_OK), "endpoint": "POST /compile", } @app.post("/compile") def compile_tex(request: CompileRequest): if not request.tex.strip(): raise HTTPException(400, "No LaTeX code provided") if len(request.tex) > 2_000_000: raise HTTPException(413, "LaTeX source too large") workdir = tempfile.mkdtemp(prefix="tectonic_") try: tex_path = os.path.join(workdir, "resume.tex") with open(tex_path, "w", encoding="utf-8") as f: f.write(request.tex) result = subprocess.run( [ TECTONIC, "-X", "compile", "--untrusted", "resume.tex", ], cwd=workdir, capture_output=True, text=True, timeout=120, ) print("TECTONIC STDOUT:") print(result.stdout) print("TECTONIC STDERR:") print(result.stderr) pdf_path = os.path.join(workdir, "resume.pdf") if result.returncode != 0: raise HTTPException( status_code=500, detail={ "returncode": result.returncode, "stdout": result.stdout[-5000:], "stderr": result.stderr[-5000:], }, ) if not os.path.exists(pdf_path): raise HTTPException( status_code=500, detail="Tectonic finished but resume.pdf was not created", ) with open(pdf_path, "rb") as f: pdf_data = f.read() return Response( content=pdf_data, media_type="application/pdf", headers={ "Content-Disposition": 'attachment; filename="resume.pdf"' }, ) except subprocess.TimeoutExpired: raise HTTPException( status_code=408, detail="Tectonic compilation timed out", ) except HTTPException: raise except Exception as e: raise HTTPException( status_code=500, detail=str(e), ) finally: shutil.rmtree(workdir, ignore_errors=True)