summaryrefslogtreecommitdiff
path: root/support/texlab/src/tex
diff options
context:
space:
mode:
Diffstat (limited to 'support/texlab/src/tex')
-rw-r--r--support/texlab/src/tex/compile.rs97
-rw-r--r--support/texlab/src/tex/kpsewhich.rs112
-rw-r--r--support/texlab/src/tex/miktex.rs108
-rw-r--r--support/texlab/src/tex/mod.rs154
-rw-r--r--support/texlab/src/tex/tectonic.rs36
-rw-r--r--support/texlab/src/tex/texlive.rs71
6 files changed, 578 insertions, 0 deletions
diff --git a/support/texlab/src/tex/compile.rs b/support/texlab/src/tex/compile.rs
new file mode 100644
index 0000000000..a2b0d2f37d
--- /dev/null
+++ b/support/texlab/src/tex/compile.rs
@@ -0,0 +1,97 @@
+use std::{io, process::Stdio, time::Duration};
+use tempfile::{tempdir, TempDir};
+use thiserror::Error;
+use tokio::{
+ fs,
+ process::Command,
+ time::{timeout, Elapsed},
+};
+
+#[derive(Debug, PartialEq, Eq, Clone, Copy)]
+pub enum Format {
+ Latex,
+ Pdflatex,
+ Xelatex,
+ Lualatex,
+}
+
+impl Format {
+ pub fn executable(self) -> &'static str {
+ match self {
+ Self::Latex => "latex",
+ Self::Pdflatex => "pdflatex",
+ Self::Xelatex => "xelatex",
+ Self::Lualatex => "lualatex",
+ }
+ }
+}
+
+#[derive(Debug)]
+pub struct Artifacts {
+ pub dir: TempDir,
+ pub log: String,
+}
+
+#[derive(Debug, Error)]
+pub enum CompileError {
+ #[error("an I/O error occurred: `{0}`")]
+ IO(#[from] io::Error),
+ #[error("TeX engine is not installed")]
+ NotInstalled,
+ #[error("build timeout: `{0}`")]
+ Timeout(#[from] Elapsed),
+}
+
+#[derive(Debug, PartialEq, Eq, Clone, Copy)]
+pub struct CompileParams<'a> {
+ pub format: Format,
+ pub file_name: &'a str,
+ pub code: &'a str,
+ pub timeout: Duration,
+}
+
+impl<'a> Default for CompileParams<'a> {
+ fn default() -> Self {
+ Self {
+ format: Format::Lualatex,
+ file_name: "code.tex",
+ code: "",
+ timeout: Duration::from_secs(15),
+ }
+ }
+}
+
+#[derive(Debug, Clone, Copy)]
+pub struct Compiler<'a> {
+ pub executable: &'a str,
+ pub args: &'a [&'a str],
+ pub file_name: &'a str,
+ pub timeout: Duration,
+}
+
+impl<'a> Compiler<'a> {
+ pub async fn compile<'b>(&'a self, code: &'b str) -> Result<Artifacts, CompileError> {
+ let directory = tempdir()?;
+ let tex_file = directory.path().join(self.file_name);
+ fs::write(&tex_file, code).await?;
+
+ let child = Command::new(self.executable)
+ .args(self.args)
+ .current_dir(&directory)
+ .stdout(Stdio::null())
+ .stderr(Stdio::null())
+ .status();
+
+ timeout(self.timeout, child)
+ .await?
+ .map_err(|_| CompileError::NotInstalled)?;
+
+ let log_file = tex_file.with_extension("log");
+ let log_bytes = fs::read(log_file).await?;
+ let log = String::from_utf8_lossy(&log_bytes).into_owned();
+ Ok(Artifacts {
+ dir: directory,
+ log,
+ })
+ }
+}
diff --git a/support/texlab/src/tex/kpsewhich.rs b/support/texlab/src/tex/kpsewhich.rs
new file mode 100644
index 0000000000..cb2ab89de5
--- /dev/null
+++ b/support/texlab/src/tex/kpsewhich.rs
@@ -0,0 +1,112 @@
+use super::Language;
+use futures::Future;
+use std::{
+ collections::HashMap,
+ env,
+ ffi::OsStr,
+ io,
+ path::{Path, PathBuf},
+ string::FromUtf8Error,
+};
+use thiserror::Error;
+use tokio::{fs, process::Command};
+
+#[derive(Debug, Error)]
+pub enum KpsewhichError {
+ #[error("an I/O error occurred: `{0}`")]
+ IO(#[from] io::Error),
+ #[error("an utf8 error occurred: `{0}`")]
+ Decode(#[from] FromUtf8Error),
+ #[error("invalid output from kpsewhich")]
+ InvalidOutput,
+ #[error("kpsewhich not installed")]
+ NotInstalled,
+ #[error("no kpsewhich database")]
+ NoDatabase,
+ #[error("corrupt kpsewhich database")]
+ CorruptDatabase,
+}
+
+#[derive(Debug, PartialEq, Eq, Clone, Default)]
+pub struct Resolver {
+ pub files_by_name: HashMap<String, PathBuf>,
+}
+
+impl Resolver {
+ pub fn new(files_by_name: HashMap<String, PathBuf>) -> Self {
+ Self { files_by_name }
+ }
+}
+
+pub async fn parse_database<'a, R, F>(
+ root_directories: &'a [PathBuf],
+ reader: R,
+) -> Result<Resolver, KpsewhichError>
+where
+ R: Fn(&'a Path) -> F,
+ F: Future<Output = Result<Vec<PathBuf>, KpsewhichError>>,
+{
+ let mut files_by_name = HashMap::new();
+ for directory in root_directories {
+ for path in reader(directory).await? {
+ if is_tex_file(&path) {
+ if let Some(path) = make_absolute(root_directories, &path).await {
+ if let Some(name) = path
+ .file_name()
+ .and_then(OsStr::to_str)
+ .map(ToString::to_string)
+ {
+ files_by_name.insert(name, path);
+ }
+ }
+ }
+ }
+ }
+ Ok(Resolver::new(files_by_name))
+}
+
+fn is_tex_file(path: &Path) -> bool {
+ path.extension()
+ .and_then(OsStr::to_str)
+ .and_then(Language::by_extension)
+ .is_some()
+}
+
+async fn make_absolute(root_directories: &[PathBuf], relative_path: &Path) -> Option<PathBuf> {
+ for dir in root_directories.iter().rev() {
+ if let Ok(path) = fs::canonicalize(dir.join(&relative_path)).await {
+ return Some(path);
+ }
+ }
+ None
+}
+
+pub async fn root_directories() -> Result<Vec<PathBuf>, KpsewhichError> {
+ let texmf = run(&["-var-value", "TEXMF"]).await?;
+ let expand_arg = format!("--expand-braces={}", texmf);
+ let expanded = run(&[&expand_arg]).await?;
+ let directories = env::split_paths(&expanded.replace("!", ""))
+ .filter(|path| path.exists())
+ .collect();
+ Ok(directories)
+}
+
+async fn run<I, S>(args: I) -> Result<String, KpsewhichError>
+where
+ I: IntoIterator<Item = S>,
+ S: AsRef<OsStr>,
+{
+ let output = Command::new("kpsewhich")
+ .args(args)
+ .output()
+ .await
+ .map_err(|_| KpsewhichError::NotInstalled)?;
+
+ let result = String::from_utf8(output.stdout)?
+ .lines()
+ .next()
+ .ok_or(KpsewhichError::InvalidOutput)?
+ .into();
+
+ Ok(result)
+}
diff --git a/support/texlab/src/tex/miktex.rs b/support/texlab/src/tex/miktex.rs
new file mode 100644
index 0000000000..9f1551df44
--- /dev/null
+++ b/support/texlab/src/tex/miktex.rs
@@ -0,0 +1,108 @@
+use super::{
+ compile,
+ kpsewhich::{self, KpsewhichError, Resolver},
+ Artifacts, CompileError, CompileParams, Distribution, DistributionKind,
+};
+use async_trait::async_trait;
+use byteorder::{LittleEndian, ReadBytesExt};
+use futures::lock::Mutex;
+use std::{
+ ffi::OsStr,
+ io::{self, Cursor},
+ mem,
+ path::{Path, PathBuf},
+ sync::Arc,
+};
+use tokio::fs;
+
+#[derive(Debug, Default)]
+pub struct Miktex {
+ resolver: Mutex<Arc<Resolver>>,
+}
+
+#[async_trait]
+impl Distribution for Miktex {
+ fn kind(&self) -> DistributionKind {
+ DistributionKind::Miktex
+ }
+
+ async fn compile<'a>(&'a self, params: CompileParams<'a>) -> Result<Artifacts, CompileError> {
+ compile(params).await
+ }
+
+ async fn load(&self) -> Result<(), KpsewhichError> {
+ let root_directories = kpsewhich::root_directories().await?;
+ let resolver = kpsewhich::parse_database(&root_directories, read_database).await?;
+ mem::replace(&mut *self.resolver.lock().await, Arc::new(resolver));
+ Ok(())
+ }
+
+ async fn resolver(&self) -> Arc<Resolver> {
+ let resolver = self.resolver.lock().await;
+ Arc::clone(&resolver)
+ }
+}
+
+const DATABASE_PATH: &str = "miktex/data/le";
+const FNDB_SIGNATURE: u32 = 0x42_44_4e_46;
+const FNDB_WORD_SIZE: usize = 4;
+const FNDB_TABLE_POINTER_OFFSET: usize = 4 * FNDB_WORD_SIZE;
+const FNDB_TABLE_SIZE_OFFSET: usize = 6 * FNDB_WORD_SIZE;
+const FNDB_ENTRY_SIZE: usize = 4 * FNDB_WORD_SIZE;
+
+async fn read_database(directory: &Path) -> Result<Vec<PathBuf>, KpsewhichError> {
+ let database_directory = directory.join(DATABASE_PATH);
+ if !database_directory.exists() {
+ return Ok(Vec::new());
+ }
+
+ let mut database = Vec::new();
+ let mut files: tokio::fs::ReadDir = fs::read_dir(database_directory).await?;
+ while let Some(file) = files.next_entry().await? {
+ if file.path().extension().and_then(OsStr::to_str) == Some("fndb-5") {
+ let bytes = fs::read(file.path()).await?;
+ database.extend(parse_database(&bytes).map_err(|_| KpsewhichError::CorruptDatabase)?);
+ }
+ }
+ Ok(database)
+}
+
+fn parse_database(bytes: &[u8]) -> io::Result<Vec<PathBuf>> {
+ let mut reader = Cursor::new(bytes);
+ if reader.read_u32::<LittleEndian>()? != FNDB_SIGNATURE {
+ return Err(io::ErrorKind::InvalidData.into());
+ }
+
+ reader.set_position(FNDB_TABLE_POINTER_OFFSET as u64);
+ let table_address = reader.read_u32::<LittleEndian>()?;
+
+ reader.set_position(FNDB_TABLE_SIZE_OFFSET as u64);
+ let table_size = reader.read_u32::<LittleEndian>()?;
+
+ let mut files = Vec::new();
+ for i in 0..table_size {
+ let offset = table_address + i * FNDB_ENTRY_SIZE as u32;
+ reader.set_position(offset as u64);
+ let file_name_offset = reader.read_u32::<LittleEndian>()? as usize;
+ let directory_offset = reader.read_u32::<LittleEndian>()? as usize;
+ let file_name = read_string(bytes, file_name_offset)?;
+ let directory = read_string(bytes, directory_offset)?;
+
+ let file = PathBuf::from(directory).join(file_name);
+ files.push(file);
+ }
+
+ Ok(files)
+}
+
+fn read_string(bytes: &[u8], offset: usize) -> io::Result<&str> {
+ let mut byte = bytes[offset];
+ let mut length = 0;
+ while byte != 0x00 {
+ length += 1;
+ byte = bytes[offset + length];
+ }
+
+ std::str::from_utf8(&bytes[offset..offset + length])
+ .map_err(|_| io::ErrorKind::InvalidData.into())
+}
diff --git a/support/texlab/src/tex/mod.rs b/support/texlab/src/tex/mod.rs
new file mode 100644
index 0000000000..6d60380e52
--- /dev/null
+++ b/support/texlab/src/tex/mod.rs
@@ -0,0 +1,154 @@
+mod compile;
+mod kpsewhich;
+mod miktex;
+mod tectonic;
+mod texlive;
+
+pub use self::{
+ compile::{Artifacts, CompileError, CompileParams, Format},
+ kpsewhich::{KpsewhichError, Resolver},
+};
+
+use self::{compile::Compiler, miktex::Miktex, tectonic::Tectonic, texlive::Texlive};
+use async_trait::async_trait;
+use std::{fmt, process::Stdio, sync::Arc};
+use tokio::process::Command;
+
+#[derive(Debug, PartialEq, Eq, Clone, Copy)]
+pub enum DistributionKind {
+ Texlive,
+ Miktex,
+ Tectonic,
+ Unknown,
+}
+
+impl fmt::Display for DistributionKind {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ let name = match self {
+ Self::Texlive => "TeXLive",
+ Self::Miktex => "MikTeX",
+ Self::Tectonic => "Tectonic",
+ Self::Unknown => "Unknown",
+ };
+ write!(f, "{}", name)
+ }
+}
+
+impl DistributionKind {
+ pub async fn detect() -> Self {
+ if Command::new("tectonic")
+ .arg("--version")
+ .stdout(Stdio::null())
+ .stderr(Stdio::null())
+ .status()
+ .await
+ .is_ok()
+ {
+ return Self::Tectonic;
+ }
+
+ match Command::new("latex").arg("--version").output().await {
+ Ok(output) => {
+ let stdout = String::from_utf8_lossy(&output.stdout);
+ if stdout.contains("TeX Live") {
+ Self::Texlive
+ } else if stdout.contains("MiKTeX") {
+ Self::Miktex
+ } else {
+ Self::Unknown
+ }
+ }
+ Err(_) => Self::Unknown,
+ }
+ }
+}
+
+#[derive(Debug, PartialEq, Eq, Clone, Copy)]
+pub enum Language {
+ Latex,
+ Bibtex,
+}
+
+impl Language {
+ pub fn by_extension(extension: &str) -> Option<Self> {
+ match extension.to_lowercase().as_str() {
+ "tex" | "sty" | "cls" | "def" | "lco" | "aux" => Some(Language::Latex),
+ "bib" | "bibtex" => Some(Language::Bibtex),
+ _ => None,
+ }
+ }
+
+ pub fn by_language_id(language_id: &str) -> Option<Self> {
+ match language_id {
+ "latex" | "tex" => Some(Language::Latex),
+ "bibtex" | "bib" => Some(Language::Bibtex),
+ _ => None,
+ }
+ }
+}
+
+#[async_trait]
+pub trait Distribution: Send + Sync {
+ fn kind(&self) -> DistributionKind;
+
+ async fn compile<'a>(&'a self, params: CompileParams<'a>) -> Result<Artifacts, CompileError>;
+
+ async fn load(&self) -> Result<(), KpsewhichError>;
+
+ async fn resolver(&self) -> Arc<Resolver>;
+}
+
+impl dyn Distribution {
+ pub async fn detect() -> Arc<dyn Distribution> {
+ let kind = DistributionKind::detect().await;
+ let distro: Arc<dyn Distribution + Send + Sync> = match kind {
+ DistributionKind::Texlive => Arc::new(Texlive::default()),
+ DistributionKind::Miktex => Arc::new(Miktex::default()),
+ DistributionKind::Tectonic => Arc::new(Tectonic::default()),
+ DistributionKind::Unknown => Arc::new(UnknownDistribution::default()),
+ };
+ distro
+ }
+}
+
+impl fmt::Debug for dyn Distribution {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ write!(f, "{}", self.kind())
+ }
+}
+
+async fn compile(params: CompileParams<'_>) -> Result<Artifacts, CompileError> {
+ let executable = params.format.executable();
+ let args = &["--interaction=batchmode", "-shell-escape", params.file_name];
+ let compiler = Compiler {
+ executable,
+ args,
+ file_name: params.file_name,
+ timeout: params.timeout,
+ };
+ compiler.compile(params.code).await
+}
+
+#[derive(Debug, Default)]
+pub struct UnknownDistribution {
+ resolver: Arc<Resolver>,
+}
+
+#[async_trait]
+impl Distribution for UnknownDistribution {
+ fn kind(&self) -> DistributionKind {
+ DistributionKind::Unknown
+ }
+
+ async fn compile<'a>(&'a self, _params: CompileParams<'a>) -> Result<Artifacts, CompileError> {
+ Err(CompileError::NotInstalled)
+ }
+
+ async fn load(&self) -> Result<(), KpsewhichError> {
+ Ok(())
+ }
+
+ async fn resolver(&self) -> Arc<Resolver> {
+ Arc::clone(&self.resolver)
+ }
+}
diff --git a/support/texlab/src/tex/tectonic.rs b/support/texlab/src/tex/tectonic.rs
new file mode 100644
index 0000000000..1c19466830
--- /dev/null
+++ b/support/texlab/src/tex/tectonic.rs
@@ -0,0 +1,36 @@
+use super::{
+ compile::{Artifacts, CompileError, CompileParams, Compiler},
+ kpsewhich::{KpsewhichError, Resolver},
+ Distribution, DistributionKind,
+};
+use async_trait::async_trait;
+use std::sync::Arc;
+
+#[derive(Debug, Default)]
+pub struct Tectonic;
+
+#[async_trait]
+impl Distribution for Tectonic {
+ fn kind(&self) -> DistributionKind {
+ DistributionKind::Tectonic
+ }
+
+ async fn compile<'a>(&'a self, params: CompileParams<'a>) -> Result<Artifacts, CompileError> {
+ let args = [params.file_name];
+ let compiler = Compiler {
+ executable: "tectonic",
+ args: &args,
+ file_name: params.file_name,
+ timeout: params.timeout,
+ };
+ compiler.compile(params.code).await
+ }
+
+ async fn load(&self) -> Result<(), KpsewhichError> {
+ Ok(())
+ }
+
+ async fn resolver(&self) -> Arc<Resolver> {
+ Arc::new(Resolver::default())
+ }
+}
diff --git a/support/texlab/src/tex/texlive.rs b/support/texlab/src/tex/texlive.rs
new file mode 100644
index 0000000000..64bcfebdb7
--- /dev/null
+++ b/support/texlab/src/tex/texlive.rs
@@ -0,0 +1,71 @@
+use super::{
+ compile,
+ kpsewhich::{self, KpsewhichError, Resolver},
+ Artifacts, CompileError, CompileParams, Distribution, DistributionKind,
+};
+use async_trait::async_trait;
+use futures::lock::Mutex;
+use std::{
+ io, mem,
+ path::{Path, PathBuf},
+ str::Lines,
+ sync::Arc,
+};
+use tokio::fs;
+
+#[derive(Debug, Default)]
+pub struct Texlive {
+ resolver: Mutex<Arc<Resolver>>,
+}
+
+#[async_trait]
+impl Distribution for Texlive {
+ fn kind(&self) -> DistributionKind {
+ DistributionKind::Texlive
+ }
+
+ async fn compile<'a>(&'a self, params: CompileParams<'a>) -> Result<Artifacts, CompileError> {
+ compile(params).await
+ }
+
+ async fn load(&self) -> Result<(), KpsewhichError> {
+ let root_directories = kpsewhich::root_directories().await?;
+ let resolver = kpsewhich::parse_database(&root_directories, read_database).await?;
+ mem::replace(&mut *self.resolver.lock().await, Arc::new(resolver));
+ Ok(())
+ }
+
+ async fn resolver(&self) -> Arc<Resolver> {
+ let resolver = self.resolver.lock().await;
+ Arc::clone(&resolver)
+ }
+}
+
+const DATABASE_PATH: &str = "ls-R";
+
+async fn read_database(directory: &Path) -> Result<Vec<PathBuf>, KpsewhichError> {
+ let file = directory.join(DATABASE_PATH);
+ if !file.is_file() {
+ return Ok(Vec::new());
+ }
+
+ let text = fs::read_to_string(file)
+ .await
+ .map_err(|_| KpsewhichError::NoDatabase)?;
+ parse_database(text.lines()).map_err(|_| KpsewhichError::CorruptDatabase)
+}
+
+fn parse_database(lines: Lines) -> io::Result<Vec<PathBuf>> {
+ let mut paths = Vec::new();
+ let mut directory = "";
+
+ for line in lines.filter(|x| !x.trim().is_empty() && !x.starts_with('%')) {
+ if line.ends_with(':') {
+ directory = &line[..line.len() - 1];
+ } else {
+ let path = PathBuf::from(directory).join(line);
+ paths.push(path);
+ }
+ }
+ Ok(paths)
+}