use anyhow::{Context, Result};
use std::fs;
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use which::which;
pub enum LatexCompiler {
Tectonic(PathBuf),
PdfLatex(PathBuf),
XeLatex(PathBuf),
LuaLatex(PathBuf),
}
impl LatexCompiler {
pub fn detect() -> Result<Self> {
if let Ok(path) = which("tectonic") {
return Ok(LatexCompiler::Tectonic(path));
}
if let Some(home) = dirs::home_dir() {
let cargo_tectonic = home.join(".cargo/bin/tectonic");
if cargo_tectonic.is_file() {
return Ok(LatexCompiler::Tectonic(cargo_tectonic));
}
}
if let Ok(path) = which("xelatex") {
return Ok(LatexCompiler::XeLatex(path));
}
if let Ok(path) = which("pdflatex") {
return Ok(LatexCompiler::PdfLatex(path));
}
if let Ok(path) = which("lualatex") {
return Ok(LatexCompiler::LuaLatex(path));
}
anyhow::bail!(
"No LaTeX engine found!\n\
Recommended: Install Tectonic (modern standalone TeX engine):\n\
curl --proto '=https' --tlsv1.2 -fsSL https://drop-sh.tectonic-typesetting.github.io | sh\n\
Or install via package manager: 'sudo pacman -S tectonic' or 'sudo apt install tectonic'."
)
}
pub fn name(&self) -> &'static str {
match self {
LatexCompiler::Tectonic(_) => "Tectonic",
LatexCompiler::PdfLatex(_) => "pdflatex",
LatexCompiler::XeLatex(_) => "xelatex",
LatexCompiler::LuaLatex(_) => "lualatex",
}
}
}
pub fn compile_latex(input_path: &Path, output_pdf: &Path) -> Result<()> {
let compiler = LatexCompiler::detect()?;
let abs_input = fs::canonicalize(input_path)
.with_context(|| format!("Failed to canonicalize LaTeX path: {}", input_path.display()))?;
let parent_dir = abs_input
.parent()
.ok_or_else(|| anyhow::anyhow!("Input file has no parent directory"))?;
let file_stem = abs_input
.file_stem()
.ok_or_else(|| anyhow::anyhow!("Invalid file name"))?
.to_string_lossy();
let abs_output = if output_pdf.is_absolute() {
output_pdf.to_path_buf()
} else {
std::env::current_dir()?.join(output_pdf)
};
if let Some(parent) = abs_output.parent() {
fs::create_dir_all(parent)?;
}
match &compiler {
LatexCompiler::Tectonic(tectonic_bin) => {
let output = Command::new(tectonic_bin)
.arg("-o")
.arg(parent_dir)
.arg(&abs_input)
.current_dir(parent_dir)
.stderr(Stdio::piped())
.stdout(Stdio::piped())
.output()
.with_context(|| "Failed to run tectonic")?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
let relevant_errors: Vec<&str> = stderr
.lines()
.filter(|l| !l.starts_with("Fontconfig warning:"))
.collect();
anyhow::bail!(
"Tectonic compilation failed:\n{}",
relevant_errors.join("\n")
);
}
let generated_pdf = parent_dir.join(format!("{}.pdf", file_stem));
if generated_pdf.exists() && generated_pdf != abs_output {
fs::copy(&generated_pdf, &abs_output)?;
}
}
LatexCompiler::PdfLatex(bin) | LatexCompiler::XeLatex(bin) | LatexCompiler::LuaLatex(bin) => {
for pass in 1..=2 {
let output = Command::new(bin)
.arg("-interaction=nonstopmode")
.arg("-file-line-error")
.arg(&abs_input)
.current_dir(parent_dir)
.output()
.with_context(|| format!("Failed to run {}", compiler.name()))?;
if pass == 2 && !output.status.success() {
let log_path = parent_dir.join(format!("{}.log", file_stem));
let log_hint = if log_path.exists() {
format!("\nCheck log file: {}", log_path.display())
} else {
String::new()
};
anyhow::bail!("{} compilation failed.{}", compiler.name(), log_hint);
}
}
let generated_pdf = parent_dir.join(format!("{}.pdf", file_stem));
if generated_pdf.exists() && generated_pdf != abs_output {
fs::copy(&generated_pdf, &abs_output)?;
}
}
}
if !abs_output.exists() {
anyhow::bail!("Expected PDF was not found at {}", abs_output.display());
}
Ok(())
}