extern crate aho_corasick;
extern crate clap;
#[macro_use] extern crate error_chain;
#[macro_use] extern crate tectonic;
extern crate termcolor;
use aho_corasick::{Automaton, AcAutomaton};
use clap::{Arg, ArgMatches, App};
use std::collections::HashMap;
use std::ffi::OsString;
use std::fs::File;
use std::io::Write;
use std::path::{Path, PathBuf};
use std::process;
use tectonic::config::PersistentConfig;
use tectonic::engines::{AccessPattern, FileSummary};
use tectonic::errors::{Result, ResultExt};
use tectonic::io::{FilesystemIo, GenuineStdoutIo, IoProvider, IoStack, MemoryIo};
use tectonic::io::itarbundle::{HttpITarIoFactory, ITarBundle};
use tectonic::io::zipbundle::ZipBundle;
use tectonic::status::{ChatterLevel, StatusBackend};
use tectonic::status::termcolor::TermcolorStatusBackend;
use tectonic::{BibtexEngine, TexEngine, TexResult, XdvipdfmxEngine};
struct CliIoSetup {
pub bundle: Option<Box<IoProvider>>,
pub mem: MemoryIo,
pub filesystem: FilesystemIo,
pub genuine_stdout: Option<GenuineStdoutIo>,
}
impl CliIoSetup {
pub fn new(bundle: Option<Box<IoProvider>>, use_genuine_stdout: bool) -> Result<CliIoSetup> {
Ok(CliIoSetup {
mem: MemoryIo::new(true),
filesystem: FilesystemIo::new(Path::new(""), false, true),
bundle: bundle,
genuine_stdout: if use_genuine_stdout {
Some(GenuineStdoutIo::new())
} else {
None
},
})
}
fn as_stack<'a> (&'a mut self) -> IoStack<'a> {
let mut providers: Vec<&mut IoProvider> = Vec::new();
if let Some(ref mut p) = self.genuine_stdout {
providers.push(p);
}
providers.push(&mut self.mem);
providers.push(&mut self.filesystem);
if let Some(ref mut b) = self.bundle {
providers.push(&mut **b);
}
IoStack::new(providers)
}
}
#[derive(Clone,Copy,Debug,Eq,PartialEq)]
pub enum OutputFormat {
Xdv,
Pdf,
}
#[derive(Clone,Copy,Debug,Eq,PartialEq)]
enum PassSetting {
Tex,
Default,
}
struct ProcessingSession {
io: CliIoSetup,
summaries: HashMap<OsString, FileSummary>,
pass: PassSetting,
tex_path: String,
format_path: String,
aux_path: PathBuf,
xdv_path: PathBuf,
pdf_path: PathBuf,
output_format: OutputFormat,
tex_rerun_specification: Option<usize>,
keep_intermediates: bool,
keep_logs: bool,
noted_tex_warnings: bool,
}
const DEFAULT_MAX_TEX_PASSES: usize = 6;
impl ProcessingSession {
pub fn new(args: &ArgMatches, config: &PersistentConfig, status: &mut TermcolorStatusBackend) -> Result<ProcessingSession> {
let format_path = args.value_of("format").unwrap();
let tex_path = args.value_of("INPUT").unwrap();
let output_format = match args.value_of("outfmt").unwrap() {
"xdv" => OutputFormat::Xdv,
"pdf" => OutputFormat::Pdf,
_ => unreachable!()
};
let pass = match args.value_of("pass").unwrap() {
"default" => PassSetting::Default,
"tex" => PassSetting::Tex,
_ => unreachable!()
};
let reruns = match args.value_of("reruns") {
Some(s) => Some(usize::from_str_radix(s, 10)?),
None => None,
};
let mut aux_path = PathBuf::from(&tex_path);
aux_path.set_extension("aux");
let mut xdv_path = PathBuf::from(&tex_path);
xdv_path.set_extension("xdv");
let mut pdf_path = PathBuf::from(&tex_path);
pdf_path.set_extension("pdf");
let bundle: Option<Box<IoProvider>>;
if let Some(p) = args.value_of("bundle") {
let zb = ZipBundle::<File>::open(Path::new(&p)).chain_err(|| "error opening bundle")?;
bundle = Some(Box::new(zb));
} else if let Some(u) = args.value_of("web_bundle") {
let tb = ITarBundle::<HttpITarIoFactory>::new(&u);
bundle = Some(Box::new(tb));
} else {
bundle = Some(config.default_io_provider(status)?);
}
let io = CliIoSetup::new(bundle, args.is_present("print_stdout"))?;
Ok(ProcessingSession {
io: io,
summaries: HashMap::new(),
pass: pass,
tex_path: tex_path.to_owned(),
format_path: format_path.to_owned(),
aux_path: aux_path,
xdv_path: xdv_path,
pdf_path: pdf_path,
output_format: output_format,
tex_rerun_specification: reruns,
keep_intermediates: args.is_present("keep_intermediates"),
keep_logs: args.is_present("keep_logs"),
noted_tex_warnings: false,
})
}
fn rerun_needed(&mut self, status: &mut TermcolorStatusBackend) -> Option<String> {
for (name, info) in &self.summaries {
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(name.to_string_lossy().into_owned());
}
}
}
return None;
}
#[allow(dead_code)]
fn _dump_access_info(&self, status: &mut TermcolorStatusBackend) {
for (name, info) in &self.summaries {
if info.access_pattern != AccessPattern::Read {
use std::string::ToString;
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);
}
}
}
fn run(&mut self, status: &mut TermcolorStatusBackend) -> Result<i32> {
match self.pass {
PassSetting::Tex => self.tex_pass(None, status),
PassSetting::Default => self.default_pass(status),
}?;
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.summaries.get(name).unwrap();
if summ.access_pattern != AccessPattern::Written && !self.keep_intermediates {
n_skipped_intermediates += 1;
continue;
}
if (sname.ends_with(".log") || sname.ends_with(".blg")) && !self.keep_logs {
continue;
}
if contents.len() == 0 {
status.note_highlighted("Not writing ", &sname, ": it would be empty.");
continue;
}
status.note_highlighted("Writing ", &sname, &format!(" ({} bytes)", contents.len()));
let mut f = File::create(Path::new(name))?;
f.write_all(contents)?;
}
if n_skipped_intermediates > 0 {
status.note_highlighted("Skipped writing ", &format!("{}", n_skipped_intermediates),
" intermediate files (use --keep-intermediates to keep them)");
}
Ok(0)
}
fn default_pass(&mut self, status: &mut TermcolorStatusBackend) -> Result<i32> {
self.tex_pass(None, status)?;
let mut rerun_result = self.rerun_needed(status);
let use_bibtex = {
if let Some(auxdata) = self.io.mem.files.borrow().get(self.aux_path.as_os_str()) {
let cite_aut = AcAutomaton::new(vec!["\\citation", "\\bibcite"]);
cite_aut.find(auxdata).count() > 0
} else {
false
}
};
if use_bibtex {
self.bibtex_pass(status)?;
rerun_result = Some(String::new());
}
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(ref s) => {
if s == "" {
"bibtex was run".to_owned()
} else {
format!("\"{}\" changed", s)
}
},
None => {
break;
}
}
};
for summ in self.summaries.values_mut() {
summ.read_digest = None;
}
self.tex_pass(Some(&rerun_explanation), status)?;
if !reruns_fixed {
rerun_result = self.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 OutputFormat::Pdf = self.output_format {
self.xdvipdfmx_pass(status)?;
}
Ok(0)
}
fn tex_pass(&mut self, rerun_explanation: Option<&str>, status: &mut TermcolorStatusBackend) -> Result<i32> {
let result = {
let mut stack = self.io.as_stack();
let mut engine = TexEngine::new();
engine.set_halt_on_error_mode(true);
if let Some(s) = rerun_explanation {
status.note_highlighted("Rerunning ", "TeX", &format!(" because {} ...", s));
} else {
status.note_highlighted("Running ", "TeX", " ...");
}
engine.process(&mut stack, Some(&mut self.summaries), status,
&self.format_path, &self.tex_path)
};
match result {
Ok(TexResult::Spotless) => {},
Ok(TexResult::Warnings) => {
if !self.noted_tex_warnings {
tt_note!(status, "warnings were issued by the TeX engine; use --print and/or --keep-logs for details.");
self.noted_tex_warnings = true;
}
},
Ok(TexResult::Errors) => {
if !self.noted_tex_warnings {
tt_warning!(status, "errors were issued by the TeX engine, but were ignored; \
use --print and/or --keep-logs for details.");
self.noted_tex_warnings = true;
}
},
Err(e) => {
if let Some(output) = self.io.mem.files.borrow().get(self.io.mem.stdout_key()) {
tt_error!(status, "something bad happened inside TeX; its output follows:\n");
tt_error_styled!(status, "===============================================================================");
status.dump_to_stderr(&output);
tt_error_styled!(status, "===============================================================================");
tt_error_styled!(status, "");
}
return Err(e);
}
}
Ok(0)
}
fn bibtex_pass(&mut self, status: &mut TermcolorStatusBackend) -> 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, Some(&mut self.summaries), status,
&self.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) => {
if let Some(output) = self.io.mem.files.borrow().get(self.io.mem.stdout_key()) {
tt_error!(status, "something bad happened inside BibTeX; its output follows:\n");
tt_error_styled!(status, "===============================================================================");
status.dump_to_stderr(&output);
tt_error_styled!(status, "===============================================================================");
tt_error_styled!(status, "");
}
return Err(e);
}
}
Ok(0)
}
fn xdvipdfmx_pass(&mut self, status: &mut TermcolorStatusBackend) -> Result<i32> {
{
let mut stack = self.io.as_stack();
let mut engine = XdvipdfmxEngine::new ();
status.note_highlighted("Running ", "xdvipdfmx", " ...");
engine.process(&mut stack, Some(&mut self.summaries), status,
&self.xdv_path.to_str().unwrap(), &self.pdf_path.to_str().unwrap())?;
}
self.io.mem.files.borrow_mut().remove(self.xdv_path.as_os_str());
Ok(0)
}
}
fn inner(matches: ArgMatches, config: PersistentConfig, status: &mut TermcolorStatusBackend) -> Result<i32> {
let mut sess = ProcessingSession::new(&matches, &config, status)?;
sess.run(status)
}
fn main() {
let matches = App::new("Tectonic")
.version("0.1.2")
.about("Process a (La)TeX document.")
.arg(Arg::with_name("format")
.long("format")
.value_name("PATH")
.help("The name of the \"format\" file used to initialize the TeX engine.")
.default_value("xelatex.fmt"))
.arg(Arg::with_name("bundle")
.long("bundle")
.short("b")
.value_name("PATH")
.help("Use this Zip-format bundle file to find resource files instead of the default.")
.takes_value(true))
.arg(Arg::with_name("web_bundle")
.long("web-bundle")
.short("w")
.value_name("URL")
.help("Use this URL find resource files instead of the default.")
.takes_value(true))
.arg(Arg::with_name("outfmt")
.long("outfmt")
.value_name("FORMAT")
.help("The kind of output to generate.")
.possible_values(&["pdf", "xdv"])
.default_value("pdf"))
.arg(Arg::with_name("pass")
.long("pass")
.value_name("PASS")
.help("Which engines to run.")
.possible_values(&["default", "tex"])
.default_value("default"))
.arg(Arg::with_name("reruns")
.long("reruns")
.short("r")
.value_name("COUNT")
.help("Rerun the TeX engine exactly this many times after the first."))
.arg(Arg::with_name("keep_intermediates")
.short("k")
.long("keep-intermediates")
.help("Keep the intermediate files generated during processing."))
.arg(Arg::with_name("keep_logs")
.long("keep-logs")
.help("Keep the log files generated during processing."))
.arg(Arg::with_name("print_stdout")
.long("print")
.short("p")
.help("Print the engine's chatter during processing."))
.arg(Arg::with_name("chatter_level")
.long("chatter")
.short("c")
.value_name("LEVEL")
.help("How much chatter to print when running.")
.possible_values(&["default", "minimal"])
.default_value("default"))
.arg(Arg::with_name("INPUT")
.help("The file to process.")
.required(true)
.index(1))
.get_matches ();
let chatter = match matches.value_of("chatter_level").unwrap() {
"default" => ChatterLevel::Normal,
"minimal" => ChatterLevel::Minimal,
_ => unreachable!()
};
let config = match PersistentConfig::open(false) {
Ok(c) => c,
Err(ref e) => {
e.dump_uncolorized();
process::exit(1);
}
};
let mut status = TermcolorStatusBackend::new(chatter);
tt_note!(status, "this is a BETA release; report issues at https://github.com/tectonic-typesetting/tectonic/issues");
process::exit(match inner(matches, config, &mut status) {
Ok(ret) => ret,
Err(ref e) => {
status.bare_error(e);
1
}
})
}