Spaces:
Sleeping
Sleeping
| #!/usr/bin/env python | |
| """ | |
| Async LaTeX compilation handler | |
| Works efficiently on Linux/HF Spaces with forking | |
| Falls back to sequential on Windows | |
| """ | |
| import os | |
| import sys | |
| import subprocess | |
| import platform | |
| from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor | |
| from pathlib import Path | |
| import time | |
| def is_linux(): | |
| """Check if running on Linux/Unix""" | |
| return platform.system() in ['Linux', 'Darwin'] | |
| def compile_latex_file(tex_path, output_dir=None, timeout=30): | |
| """ | |
| Compile a single LaTeX file to PDF | |
| Args: | |
| tex_path: Path to .tex file | |
| output_dir: Output directory (default: same as tex file) | |
| timeout: Compilation timeout in seconds | |
| Returns: | |
| tuple: (success: bool, pdf_path: str or None, error_msg: str or None) | |
| """ | |
| tex_path = Path(tex_path) | |
| if not tex_path.exists(): | |
| return False, None, f"File not found: {tex_path}" | |
| output_dir = output_dir or tex_path.parent | |
| pdf_path = output_dir / tex_path.with_suffix('.pdf').name | |
| # Remove old PDF if exists | |
| if pdf_path.exists(): | |
| try: | |
| pdf_path.unlink() | |
| except: | |
| pass | |
| # Compile command | |
| cmd = [ | |
| 'pdflatex', | |
| '-interaction=nonstopmode', | |
| '-halt-on-error', | |
| f'-output-directory={output_dir}', | |
| str(tex_path) | |
| ] | |
| try: | |
| # Run compilation | |
| result = subprocess.run( | |
| cmd, | |
| capture_output=True, | |
| text=True, | |
| timeout=timeout, | |
| cwd=str(tex_path.parent) | |
| ) | |
| # Check if PDF was created | |
| if pdf_path.exists(): | |
| return True, str(pdf_path), None | |
| else: | |
| # Extract error from log | |
| error_msg = "Compilation failed" | |
| if result.stdout: | |
| lines = result.stdout.split('\n') | |
| for i, line in enumerate(lines): | |
| if 'Error' in line or '!' in line[:2]: | |
| error_msg = '\n'.join(lines[i:i+5]) | |
| break | |
| return False, None, error_msg | |
| except subprocess.TimeoutExpired: | |
| return False, None, f"Timeout after {timeout} seconds" | |
| except FileNotFoundError: | |
| return False, None, "pdflatex not found - install texlive" | |
| except Exception as e: | |
| return False, None, str(e) | |
| def compile_latex_batch(tex_files, output_dir=None, max_workers=4, timeout=30): | |
| """ | |
| Compile multiple LaTeX files in parallel | |
| Args: | |
| tex_files: List of .tex file paths | |
| output_dir: Output directory for PDFs | |
| max_workers: Number of parallel workers | |
| timeout: Timeout per file | |
| Returns: | |
| dict: {tex_path: (success, pdf_path, error_msg)} | |
| """ | |
| results = {} | |
| if not tex_files: | |
| return results | |
| # Use ProcessPoolExecutor on Linux for true parallelism | |
| # Use ThreadPoolExecutor on Windows (less efficient but works) | |
| if is_linux(): | |
| executor_class = ProcessPoolExecutor | |
| print(f"Using process-based parallelism ({max_workers} workers)") | |
| else: | |
| executor_class = ThreadPoolExecutor | |
| print(f"Using thread-based parallelism ({max_workers} workers)") | |
| with executor_class(max_workers=max_workers) as executor: | |
| # Submit all compilation tasks | |
| futures = { | |
| executor.submit(compile_latex_file, tex_file, output_dir, timeout): tex_file | |
| for tex_file in tex_files | |
| } | |
| # Collect results as they complete | |
| for future in futures: | |
| tex_file = futures[future] | |
| try: | |
| success, pdf_path, error = future.result(timeout=timeout+5) | |
| results[tex_file] = (success, pdf_path, error) | |
| if success: | |
| print(f" ✓ Compiled: {Path(tex_file).name}") | |
| else: | |
| print(f" ✗ Failed: {Path(tex_file).name}") | |
| except Exception as e: | |
| results[tex_file] = (False, None, str(e)) | |
| print(f" ✗ Error: {Path(tex_file).name}: {e}") | |
| return results | |
| def compile_latex_async(tex_path, output_dir=None, callback=None): | |
| """ | |
| Compile LaTeX file asynchronously (fire-and-forget) | |
| Args: | |
| tex_path: Path to .tex file | |
| output_dir: Output directory | |
| callback: Optional callback function(success, pdf_path, error) | |
| """ | |
| if is_linux(): | |
| # On Linux, fork a subprocess | |
| pid = os.fork() | |
| if pid == 0: | |
| # Child process | |
| try: | |
| success, pdf_path, error = compile_latex_file(tex_path, output_dir) | |
| if callback: | |
| callback(success, pdf_path, error) | |
| finally: | |
| os._exit(0) | |
| else: | |
| # Parent process continues immediately | |
| print(f" → Compiling {Path(tex_path).name} in background (PID: {pid})") | |
| else: | |
| # On Windows, use threading | |
| from threading import Thread | |
| def compile_thread(): | |
| success, pdf_path, error = compile_latex_file(tex_path, output_dir) | |
| if callback: | |
| callback(success, pdf_path, error) | |
| thread = Thread(target=compile_thread, daemon=True) | |
| thread.start() | |
| print(f" → Compiling {Path(tex_path).name} in background thread") | |
| def check_latex_available(): | |
| """Check if pdflatex is available""" | |
| try: | |
| result = subprocess.run( | |
| ['pdflatex', '--version'], | |
| capture_output=True, | |
| text=True, | |
| timeout=5 | |
| ) | |
| if result.returncode == 0: | |
| # Extract version | |
| for line in result.stdout.split('\n'): | |
| if 'TeX' in line: | |
| print(f"LaTeX available: {line.strip()}") | |
| return True | |
| return False | |
| except: | |
| return False | |
| # Integration with universal_validator.py | |
| def setup_async_latex_compilation(): | |
| """ | |
| Setup async LaTeX compilation for the validator | |
| Returns a function that can be used to compile LaTeX files | |
| """ | |
| if not check_latex_available(): | |
| print("Warning: LaTeX not available, PDF compilation disabled") | |
| return None | |
| def compile_reconciliation(tex_path): | |
| """Compile reconciliation document asynchronously""" | |
| compile_latex_async( | |
| tex_path, | |
| callback=lambda s, p, e: print(f" [PDF] {'Success' if s else 'Failed'}: {Path(tex_path).name}") | |
| ) | |
| return compile_reconciliation | |
| if __name__ == "__main__": | |
| # Test the compiler | |
| import tempfile | |
| print("Testing LaTeX compilation...") | |
| print(f"Platform: {platform.system()}") | |
| print(f"Async support: {'Yes' if is_linux() else 'Limited (Windows)'}") | |
| if check_latex_available(): | |
| # Create a test document | |
| with tempfile.NamedTemporaryFile(mode='w', suffix='.tex', delete=False) as f: | |
| f.write(r"""\documentclass{article} | |
| \begin{document} | |
| \title{Test Document} | |
| \author{Validator} | |
| \maketitle | |
| This is a test: $x^2 + y^2 = z^2$ | |
| \end{document}""") | |
| test_file = f.name | |
| print(f"\nCompiling test file: {test_file}") | |
| success, pdf_path, error = compile_latex_file(test_file) | |
| if success: | |
| print(f"✓ Success! PDF created: {pdf_path}") | |
| print(f" Size: {os.path.getsize(pdf_path)} bytes") | |
| else: | |
| print(f"✗ Failed: {error}") | |
| # Clean up | |
| try: | |
| os.unlink(test_file) | |
| if pdf_path and os.path.exists(pdf_path): | |
| os.unlink(pdf_path) | |
| except: | |
| pass | |
| else: | |
| print("✗ LaTeX not installed") | |
| print(" On Linux: apt-get install texlive-latex-base") | |
| print(" On Windows: Install MiKTeX") | |
| print(" On macOS: brew install --cask mactex") |