#![deny(missing_docs)]
use std::collections::{HashMap, HashSet};
use std::ffi::{OsStr, OsString};
use std::fs::File;
use std::io::Write;
use std::path::{Path, PathBuf};
use std::rc::Rc;
use std::str::FromStr;
use std::time::SystemTime;
use crate::digest::DigestData;
use crate::engines::IoEventBackend;
use crate::errors::{ErrorKind, Result, ResultExt};
use crate::io::{Bundle, InputOrigin, IoProvider, IoSetup, IoSetupBuilder, OpenResult};
use crate::status::StatusBackend;
use crate::{ctry, errmsg, tt_error, tt_note, tt_warning};
use crate::{BibtexEngine, Spx2HtmlEngine, TexEngine, TexResult, XdvipdfmxEngine};
use std::result::Result as StdResult;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum AccessPattern {
Read,
Written,
ReadThenWritten,
WrittenThenRead,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct FileSummary {
access_pattern: AccessPattern,
pub input_origin: InputOrigin,
pub read_digest: Option<DigestData>,
pub write_digest: Option<DigestData>,
got_written_to_disk: bool,
}
impl FileSummary {
fn new(access_pattern: AccessPattern, input_origin: InputOrigin) -> FileSummary {
FileSummary {
access_pattern,
input_origin,
read_digest: None,
write_digest: None,
got_written_to_disk: false,
}
}
}
pub struct IoEvents(pub HashMap<OsString, FileSummary>);
impl IoEvents {
fn new() -> IoEvents {
IoEvents(HashMap::new())
}
}
impl IoEventBackend for IoEvents {
fn output_opened(&mut self, name: &OsStr) {
if let Some(summ) = self.0.get_mut(name) {
summ.access_pattern = match summ.access_pattern {
AccessPattern::Read => AccessPattern::ReadThenWritten,
c => c,
};
return;
}
self.0.insert(
name.to_os_string(),
FileSummary::new(AccessPattern::Written, InputOrigin::NotInput),
);
}
fn stdout_opened(&mut self) {
if let Some(summ) = self.0.get_mut(OsStr::new("")) {
summ.access_pattern = match summ.access_pattern {
AccessPattern::Read => AccessPattern::ReadThenWritten,
c => c,
};
return;
}
self.0.insert(
OsString::from(""),
FileSummary::new(AccessPattern::Written, InputOrigin::NotInput),
);
}
fn output_closed(&mut self, name: OsString, digest: DigestData) {
let summ = self
.0
.get_mut(&name)
.expect("closing file that wasn't opened?");
summ.write_digest = Some(digest);
}
fn input_not_available(&mut self, name: &OsStr) {
if let Some(summ) = self.0.get_mut(name) {
summ.access_pattern = match summ.access_pattern {
AccessPattern::Written => AccessPattern::WrittenThenRead,
c => c,
};
return;
}
let mut fs = FileSummary::new(AccessPattern::Read, InputOrigin::NotInput);
fs.read_digest = Some(DigestData::of_nothing());
self.0.insert(name.to_os_string(), fs);
}
fn input_opened(&mut self, name: &OsStr, origin: InputOrigin) {
if let Some(summ) = self.0.get_mut(name) {
summ.access_pattern = match summ.access_pattern {
AccessPattern::Written => AccessPattern::WrittenThenRead,
c => c,
};
return;
}
self.0.insert(
name.to_os_string(),
FileSummary::new(AccessPattern::Read, origin),
);
}
fn input_closed(&mut self, name: OsString, digest: Option<DigestData>) {
let summ = self
.0
.get_mut(&name)
.expect("closing file that wasn't opened?");
if summ.read_digest.is_none() {
summ.read_digest = digest;
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum OutputFormat {
Aux,
Html,
Xdv,
Pdf,
Format,
}
impl FromStr for OutputFormat {
type Err = &'static str;
fn from_str(a_str: &str) -> StdResult<Self, Self::Err> {
match a_str {
"aux" => Ok(OutputFormat::Aux),
"html" => Ok(OutputFormat::Html),
"xdv" => Ok(OutputFormat::Xdv),
"pdf" => Ok(OutputFormat::Pdf),
"fmt" => Ok(OutputFormat::Format),
_ => Err("unsupported or unknown format"),
}
}
}
impl Default for OutputFormat {
fn default() -> OutputFormat {
OutputFormat::Pdf
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum PassSetting {
Default,
Tex,
BibtexFirst,
}
impl Default for PassSetting {
fn default() -> PassSetting {
PassSetting::Default
}
}
impl FromStr for PassSetting {
type Err = &'static str;
fn from_str(a_str: &str) -> StdResult<Self, Self::Err> {
match a_str {
"default" => Ok(PassSetting::Default),
"bibtex_first" => Ok(PassSetting::BibtexFirst),
"tex" => Ok(PassSetting::Tex),
_ => Err("unsupported or unknown pass setting"),
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
enum PrimaryInputMode {
Stdin,
Path(PathBuf),
Buffer(Vec<u8>),
}
impl Default for PrimaryInputMode {
fn default() -> PrimaryInputMode {
PrimaryInputMode::Stdin
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
enum OutputDestination {
Default,
Path(PathBuf),
Nowhere,
}
impl Default for OutputDestination {
fn default() -> OutputDestination {
OutputDestination::Default
}
}
#[derive(Default)]
pub struct ProcessingSessionBuilder {
primary_input: PrimaryInputMode,
tex_input_name: Option<String>,
output_dest: OutputDestination,
format_name: Option<String>,
format_cache_path: Option<PathBuf>,
output_format: OutputFormat,
makefile_output_path: Option<PathBuf>,
hidden_input_paths: HashSet<PathBuf>,
pass: PassSetting,
reruns: Option<usize>,
print_stdout: bool,
bundle: Option<Box<dyn Bundle>>,
keep_intermediates: bool,
keep_logs: bool,
synctex: bool,
build_date: Option<SystemTime>,
}
impl ProcessingSessionBuilder {
pub fn primary_input_path<P: AsRef<Path>>(&mut self, p: P) -> &mut Self {
self.primary_input = PrimaryInputMode::Path(p.as_ref().to_owned());
self
}
pub fn primary_input_buffer(&mut self, buf: &[u8]) -> &mut Self {
self.primary_input = PrimaryInputMode::Buffer(buf.to_owned());
self
}
pub fn tex_input_name(&mut self, s: &str) -> &mut Self {
self.tex_input_name = Some(s.to_owned());
self
}
pub fn output_dir<P: AsRef<Path>>(&mut self, p: P) -> &mut Self {
self.output_dest = OutputDestination::Path(p.as_ref().to_owned());
self
}
pub fn do_not_write_output_files(&mut self) -> &mut Self {
self.output_dest = OutputDestination::Nowhere;
self
}
pub fn format_name(&mut self, p: &str) -> &mut Self {
self.format_name = Some(p.to_owned());
self
}
pub fn format_cache_path<P: AsRef<Path>>(&mut self, p: P) -> &mut Self {
self.format_cache_path = Some(p.as_ref().to_owned());
self
}
pub fn output_format(&mut self, f: OutputFormat) -> &mut Self {
self.output_format = f;
self
}
pub fn makefile_output_path<P: AsRef<Path>>(&mut self, p: P) -> &mut Self {
self.makefile_output_path = Some(p.as_ref().to_owned());
self
}
pub fn pass(&mut self, p: PassSetting) -> &mut Self {
self.pass = p;
self
}
pub fn reruns(&mut self, r: usize) -> &mut Self {
self.reruns = Some(r);
self
}
pub fn print_stdout(&mut self, p: bool) -> &mut Self {
self.print_stdout = p;
self
}
pub fn hide<P: AsRef<Path>>(&mut self, p: P) -> &mut Self {
self.hidden_input_paths.insert(p.as_ref().to_owned());
self
}
pub fn bundle(&mut self, b: Box<dyn Bundle>) -> &mut Self {
self.bundle = Some(b);
self
}
pub fn keep_intermediates(&mut self, k: bool) -> &mut Self {
self.keep_intermediates = k;
self
}
pub fn keep_logs(&mut self, k: bool) -> &mut Self {
self.keep_logs = k;
self
}
pub fn synctex(&mut self, s: bool) -> &mut Self {
self.synctex = s;
self
}
pub fn build_date(&mut self, date: SystemTime) -> &mut Self {
self.build_date = Some(date);
self
}
pub fn create(self, status: &mut dyn StatusBackend) -> Result<ProcessingSession> {
let mut io = IoSetupBuilder::default();
io.bundle(self.bundle.expect("a bundle must be specified"))
.use_genuine_stdout(self.print_stdout);
for p in &self.hidden_input_paths {
io.hide_path(p);
}
let (primary_input_path, default_output_path) = match self.primary_input {
PrimaryInputMode::Path(p) => {
io.primary_input_path(&p);
let parent = match p.parent() {
Some(parent) => parent.to_owned(),
None => {
return Err(errmsg!(
"can't figure out a parent directory for input path \"{}\"",
p.to_string_lossy()
));
}
};
io.filesystem_root(&parent);
(Some(p), parent)
}
PrimaryInputMode::Stdin => {
io.primary_input_stdin();
(None, "".into())
}
PrimaryInputMode::Buffer(buf) => {
io.primary_input_buffer(buf);
(None, "".into())
}
};
let output_path = match self.output_dest {
OutputDestination::Default => Some(default_output_path),
OutputDestination::Path(p) => Some(p),
OutputDestination::Nowhere => None,
};
if let Some(ref p) = self.format_cache_path {
io.format_cache_path(p);
}
let tex_input_name = self
.tex_input_name
.expect("tex_input_name must be specified");
let mut aux_path = PathBuf::from(tex_input_name.clone());
aux_path.set_extension("aux");
let mut xdv_path = aux_path.clone();
xdv_path.set_extension(if self.output_format == OutputFormat::Html {
"spx"
} else {
"xdv"
});
let mut pdf_path = aux_path.clone();
pdf_path.set_extension("pdf");
Ok(ProcessingSession {
io: io.create(status)?,
events: IoEvents::new(),
pass: self.pass,
primary_input_path,
primary_input_tex_path: tex_input_name,
format_name: self.format_name.unwrap(),
tex_aux_path: aux_path.into_os_string(),
tex_xdv_path: xdv_path.into_os_string(),
tex_pdf_path: pdf_path.into_os_string(),
output_format: self.output_format,
makefile_output_path: self.makefile_output_path,
output_path,
tex_rerun_specification: self.reruns,
keep_intermediates: self.keep_intermediates,
keep_logs: self.keep_logs,
synctex_enabled: self.synctex,
build_date: self.build_date.unwrap_or(SystemTime::UNIX_EPOCH),
})
}
}
#[derive(Debug, Clone)]
enum RerunReason {
Bibtex,
FileChange(String),
}
pub struct ProcessingSession {
pub io: IoSetup,
pub events: IoEvents,
primary_input_path: Option<PathBuf>,
primary_input_tex_path: String,
format_name: String,
tex_aux_path: OsString,
tex_xdv_path: OsString,
tex_pdf_path: OsString,
makefile_output_path: Option<PathBuf>,
output_path: Option<PathBuf>,
pass: PassSetting,
output_format: OutputFormat,
tex_rerun_specification: Option<usize>,
keep_intermediates: bool,
keep_logs: bool,
synctex_enabled: bool,
build_date: SystemTime,
}
const DEFAULT_MAX_TEX_PASSES: usize = 6;
const ALWAYS_INTERMEDIATE_EXTENSIONS: &[&str] = &[
".snm", ".toc",
];
impl ProcessingSession {
fn is_rerun_needed<S: StatusBackend>(&self, status: &mut S) -> Option<RerunReason> {
for (name, info) in &self.events.0 {
if info.access_pattern == AccessPattern::ReadThenWritten {
let file_changed = match (&info.read_digest, &info.write_digest) {
(&Some(ref d1), &Some(ref d2)) => d1 != d2,
(&None, &Some(_)) => true,
(_, _) => {
tt_warning!(
status,
"internal consistency problem when checking if {} changed",
name.to_string_lossy()
);
true
}
};
if file_changed {
return Some(RerunReason::FileChange(name.to_string_lossy().into_owned()));
}
}
}
None
}
#[allow(dead_code)]
fn _dump_access_info<S: StatusBackend>(&self, status: &mut S) {
for (name, info) in &self.events.0 {
if info.access_pattern != AccessPattern::Read {
let r = match info.read_digest {
Some(ref d) => d.to_string(),
None => "-".into(),
};
let w = match info.write_digest {
Some(ref d) => d.to_string(),
None => "-".into(),
};
tt_note!(
status,
"ACCESS: {} {:?} {:?} {:?}",
name.to_string_lossy(),
info.access_pattern,
r,
w
);
}
}
}
pub fn run<S: StatusBackend>(&mut self, status: &mut S) -> Result<()> {
let generate_format = if self.output_format == OutputFormat::Format {
false
} else {
let fmt_result = {
let mut stack = self.io.as_stack();
stack.input_open_format(OsStr::new(&self.format_name), status)
};
match fmt_result {
OpenResult::Ok(_) => false,
OpenResult::NotAvailable => true,
OpenResult::Err(e) => {
return Err(e)
.chain_err(|| format!("could not open format file {}", self.format_name));
}
}
};
if generate_format {
tt_note!(status, "generating format \"{}\"", self.format_name);
self.make_format_pass(status)?;
}
let result = match self.pass {
PassSetting::Tex => match self.tex_pass(None, status) {
Ok(Some(warnings)) => {
tt_warning!(status, "{}", warnings);
Ok(0)
}
Ok(None) => Ok(0),
Err(e) => Err(e),
},
PassSetting::Default => self.default_pass(false, status),
PassSetting::BibtexFirst => self.default_pass(true, status),
};
if let Err(e) = result {
self.write_files(None, status, true)?;
return Err(e);
};
let mut mf_dest_maybe = match self.makefile_output_path {
Some(ref p) => {
if self.output_path.is_none() {
tt_warning!(
status,
"requested to generate Makefile rules, but no files written to disk!"
);
None
} else {
Some(File::create(p)?)
}
}
None => None,
};
let n_skipped_intermediates = self.write_files(mf_dest_maybe.as_mut(), status, false)?;
if n_skipped_intermediates > 0 {
status.note_highlighted(
"Skipped writing ",
&format!("{}", n_skipped_intermediates),
" intermediate files (use --keep-intermediates to keep them)",
);
}
if let Some(ref mut mf_dest) = mf_dest_maybe {
ctry!(write!(mf_dest, ": "); "couldn't write to Makefile-rules file");
if let Some(ref pip) = self.primary_input_path {
ctry!(mf_dest.write_all(pip.to_string_lossy().as_ref().as_bytes()); "couldn't write to Makefile-rules file");
}
let root = self.output_path.as_ref().unwrap();
for (name, info) in &self.events.0 {
if info.input_origin != InputOrigin::Filesystem {
continue;
}
if info.got_written_to_disk {
tt_warning!(
status,
"omitting circular Makefile dependency for {}",
name.to_string_lossy()
);
continue;
}
ctry!(write!(mf_dest, " \\\n {}", root.join(name).display()); "couldn't write to Makefile-rules file");
}
ctry!(writeln!(mf_dest, ""); "couldn't write to Makefile-rules file");
}
Ok(())
}
fn write_files<S: StatusBackend>(
&mut self,
mut mf_dest_maybe: Option<&mut File>,
status: &mut S,
only_logs: bool,
) -> Result<u32> {
let root = match self.output_path {
Some(ref p) => p,
None => {
return Ok(0);
}
};
let mut n_skipped_intermediates = 0;
for (name, contents) in &*self.io.mem.files.borrow() {
if name == self.io.mem.stdout_key() {
continue;
}
let sname = name.to_string_lossy();
let summ = self.events.0.get_mut(name).unwrap();
if !only_logs && (self.output_format == OutputFormat::Aux) {
if !sname.ends_with(".aux") {
continue;
}
} else if !self.keep_intermediates
&& (summ.access_pattern != AccessPattern::Written
|| ALWAYS_INTERMEDIATE_EXTENSIONS
.iter()
.any(|ext| sname.ends_with(ext)))
{
n_skipped_intermediates += 1;
continue;
}
let is_logfile = sname.ends_with(".log") || sname.ends_with(".blg");
if is_logfile && !self.keep_logs {
continue;
}
if !is_logfile && only_logs {
continue;
}
if contents.is_empty() {
status.note_highlighted("Not writing ", &sname, ": it would be empty.");
continue;
}
let real_path = root.join(name);
status.note_highlighted(
"Writing ",
&real_path.to_string_lossy(),
&format!(" ({} bytes)", contents.len()),
);
let mut f = File::create(&real_path)?;
f.write_all(contents)?;
summ.got_written_to_disk = true;
if let Some(ref mut mf_dest) = mf_dest_maybe {
ctry!(write!(mf_dest, "{} ", real_path.to_string_lossy()); "couldn't write to Makefile-rules file");
}
}
Ok(n_skipped_intermediates)
}
fn default_pass<S: StatusBackend>(
&mut self,
bibtex_first: bool,
status: &mut S,
) -> Result<i32> {
let mut warnings = None;
let mut rerun_result = if bibtex_first {
self.bibtex_pass(status)?;
Some(RerunReason::Bibtex)
} else {
warnings = self.tex_pass(None, status)?;
if self.is_bibtex_needed() {
self.bibtex_pass(status)?;
Some(RerunReason::Bibtex)
} else {
self.is_rerun_needed(status)
}
};
let (pass_count, reruns_fixed) = match self.tex_rerun_specification {
Some(n) => (n, true),
None => (DEFAULT_MAX_TEX_PASSES, false),
};
for i in 0..pass_count {
let rerun_explanation = if reruns_fixed {
"I was told to".to_owned()
} else {
match rerun_result {
Some(RerunReason::Bibtex) => "bibtex was run".to_owned(),
Some(RerunReason::FileChange(ref s)) => format!("\"{}\" changed", s),
None => break,
}
};
for summ in self.events.0.values_mut() {
summ.read_digest = None;
}
warnings = self.tex_pass(Some(&rerun_explanation), status)?;
if !reruns_fixed {
rerun_result = self.is_rerun_needed(status);
if rerun_result.is_some() && i == DEFAULT_MAX_TEX_PASSES - 1 {
tt_warning!(
status,
"TeX rerun seems needed, but stopping at {} passes",
DEFAULT_MAX_TEX_PASSES
);
break;
}
}
}
if let Some(warnings) = warnings {
tt_warning!(status, "{}", warnings);
}
if let OutputFormat::Pdf = self.output_format {
self.xdvipdfmx_pass(status)?;
} else if let OutputFormat::Html = self.output_format {
self.spx2html_pass(status)?;
}
Ok(0)
}
fn is_bibtex_needed(&self) -> bool {
const BIBDATA: &[u8] = b"\\bibdata";
self.io
.mem
.files
.borrow()
.get(&self.tex_aux_path)
.map(|data| {
data.windows(BIBDATA.len()).any(|s| s == BIBDATA)
})
.unwrap_or(false)
}
fn make_format_pass<S: StatusBackend>(&mut self, status: &mut S) -> Result<i32> {
if self.io.bundle.is_none() {
return Err(
ErrorKind::Msg("cannot create formats without using a bundle".to_owned()).into(),
);
}
if self.io.format_cache.is_none() {
return Err(ErrorKind::Msg(
"cannot create formats without having a place to save them".to_owned(),
)
.into());
}
let r: Result<&str> = self.format_name.splitn(2, '.').next().ok_or_else(|| {
ErrorKind::Msg(format!(
"incomprehensible format file name \"{}\"",
self.format_name
))
.into()
});
let stem = r?;
let result = {
let mut stack = self
.io
.as_stack_for_format(&format!("tectonic-format-{}.tex", stem));
TexEngine::new()
.halt_on_error_mode(true)
.initex_mode(true)
.process(&mut stack, &mut self.events, status, "UNUSED.fmt", "texput")
};
match result {
Ok(TexResult::Spotless) => {}
Ok(TexResult::Warnings) => {
tt_warning!(status, "warnings were issued by the TeX engine; use --print and/or --keep-logs for details.");
}
Ok(TexResult::Errors) => {
tt_error!(status, "errors were issued by the TeX engine; use --print and/or --keep-logs for details.");
return Err(ErrorKind::Msg("unhandled TeX engine error".to_owned()).into());
}
Err(e) => {
return Err(e.chain_err(|| ErrorKind::EngineError("TeX")));
}
}
let format_cache = &mut *self.io.format_cache.as_mut().unwrap();
for (name, contents) in &*self.io.mem.files.borrow() {
if name == self.io.mem.stdout_key() {
continue;
}
let sname = name.to_string_lossy();
if !sname.ends_with(".fmt") {
continue;
}
ctry!(format_cache.write_format(stem, contents, status); "cannot write format file {}", sname);
}
self.io.mem.files.borrow_mut().clear();
Ok(0)
}
fn tex_pass<S: StatusBackend>(
&mut self,
rerun_explanation: Option<&str>,
status: &mut S,
) -> Result<Option<&'static str>> {
let result = {
let mut stack = self.io.as_stack();
if let Some(s) = rerun_explanation {
status.note_highlighted("Rerunning ", "TeX", &format!(" because {} ...", s));
} else {
status.note_highlighted("Running ", "TeX", " ...");
}
TexEngine::new()
.halt_on_error_mode(true)
.initex_mode(self.output_format == OutputFormat::Format)
.synctex(self.synctex_enabled)
.semantic_pagination(self.output_format == OutputFormat::Html)
.build_date(self.build_date)
.process(
&mut stack,
&mut self.events,
status,
&self.format_name,
&self.primary_input_tex_path,
)
};
let warnings = match result {
Ok(TexResult::Spotless) => None,
Ok(TexResult::Warnings) =>
Some("warnings were issued by the TeX engine; use --print and/or --keep-logs for details."),
Ok(TexResult::Errors) =>
Some("errors were issued by the TeX engine, but were ignored; \
use --print and/or --keep-logs for details."),
Err(e) =>
return Err(e.chain_err(|| ErrorKind::EngineError("TeX"))),
};
Ok(warnings)
}
fn bibtex_pass<S: StatusBackend>(&mut self, status: &mut S) -> Result<i32> {
let result = {
let mut stack = self.io.as_stack();
let mut engine = BibtexEngine::new();
status.note_highlighted("Running ", "BibTeX", " ...");
engine.process(
&mut stack,
&mut self.events,
status,
&self.tex_aux_path.to_str().unwrap(),
)
};
match result {
Ok(TexResult::Spotless) => {}
Ok(TexResult::Warnings) => {
tt_note!(
status,
"warnings were issued by BibTeX; use --print and/or --keep-logs for details."
);
}
Ok(TexResult::Errors) => {
tt_warning!(
status,
"errors were issued by BibTeX, but were ignored; \
use --print and/or --keep-logs for details."
);
}
Err(e) => {
return Err(e.chain_err(|| ErrorKind::EngineError("BibTeX")));
}
}
Ok(0)
}
fn xdvipdfmx_pass<S: StatusBackend>(&mut self, status: &mut S) -> Result<i32> {
{
let mut stack = self.io.as_stack();
let mut engine = XdvipdfmxEngine::new().with_date(self.build_date);
status.note_highlighted("Running ", "xdvipdfmx", " ...");
engine.process(
&mut stack,
&mut self.events,
status,
&self.tex_xdv_path.to_str().unwrap(),
&self.tex_pdf_path.to_str().unwrap(),
)?;
}
self.io.mem.files.borrow_mut().remove(&self.tex_xdv_path);
Ok(0)
}
fn spx2html_pass<S: StatusBackend>(&mut self, status: &mut S) -> Result<i32> {
{
let mut stack = self.io.as_stack();
let mut engine = Spx2HtmlEngine::new();
status.note_highlighted("Running ", "spx2html", " ...");
engine.process(
&mut stack,
&mut self.events,
status,
&self.tex_xdv_path.to_str().unwrap(),
)?;
}
self.io.mem.files.borrow_mut().remove(&self.tex_xdv_path);
Ok(0)
}
pub fn into_file_data(self) -> HashMap<OsString, Vec<u8>> {
Rc::try_unwrap(self.io.mem.files)
.expect("multiple strong refs to MemoryIo files")
.into_inner()
}
}