use clap::{Parser, Subcommand};
use std::{env, ffi::OsString, fs, path::Path, path::PathBuf, process};
use tectonic::{
config::PersistentConfig,
errors::{Result, SyncError},
status::{termcolor::TermcolorStatusBackend, ChatterLevel, StatusBackend},
tt_note,
};
use tectonic_errors::prelude::anyhow;
use tectonic_status_base::plain::PlainStatusBackend;
use tracing::level_filters::LevelFilter;
use self::commands::{
build::BuildCommand,
bundle::BundleCommand,
dump::DumpCommand,
new::{InitCommand, NewCommand},
show::ShowCommand,
watch::WatchCommand,
};
mod commands;
#[derive(Debug, Parser)]
#[command(
name = "tectonic -X",
version,
about = "Process (La)TeX documents",
no_binary_name(true)
)]
struct V2CliOptions {
#[arg(long = "chatter", short, default_value = "default")]
chatter_level: ChatterLevel,
#[arg(long = "color", default_value = "auto")]
cli_color: crate::CliColor,
#[command(subcommand)]
command: Commands,
}
#[derive(Debug, Default)]
struct CommandCustomizations {
always_stderr: bool,
minimal_chatter: bool,
}
pub fn v2_main(effective_args: &[OsString]) {
tectonic::test_util::maybe_activate_test_mode();
let config = match PersistentConfig::open(false) {
Ok(c) => c,
Err(ref e) => {
e.dump_uncolorized();
process::exit(1);
}
};
let args = V2CliOptions::parse_from(effective_args);
tracing_subscriber::fmt()
.with_max_level(LevelFilter::INFO)
.with_target(false)
.without_time()
.with_ansi(args.cli_color.should_enable())
.init();
let mut customizations = CommandCustomizations::default();
match &args.command {
Commands::Build(o) => o.customize(&mut customizations),
Commands::Bundle(o) => o.customize(&mut customizations),
Commands::Compile(_) => {} Commands::Dump(o) => o.customize(&mut customizations),
Commands::New(o) => o.customize(&mut customizations),
Commands::Init(o) => o.customize(&mut customizations),
Commands::Show(o) => o.customize(&mut customizations),
Commands::Watch(o) => o.customize(&mut customizations),
Commands::External(_) => {}
}
let chatter_level = if customizations.minimal_chatter {
ChatterLevel::Minimal
} else {
args.chatter_level
};
let mut status = if args.cli_color.should_enable() {
let mut sb = TermcolorStatusBackend::new(chatter_level);
sb.always_stderr(customizations.always_stderr);
Box::new(sb) as Box<dyn StatusBackend>
} else {
let mut sb = PlainStatusBackend::new(chatter_level);
sb.always_stderr(customizations.always_stderr);
Box::new(sb) as Box<dyn StatusBackend>
};
tt_note!(
status,
"\"version 2\" Tectonic command-line interface activated"
);
let r = match args.command {
Commands::Build(o) => o.execute(config, &mut *status),
Commands::Bundle(o) => o.execute(config, &mut *status),
Commands::Compile(o) => o.execute(config, &mut *status),
Commands::Dump(o) => o.execute(config, &mut *status),
Commands::New(o) => o.execute(config, &mut *status),
Commands::Init(o) => o.execute(config, &mut *status),
Commands::Show(o) => o.execute(config, &mut *status),
Commands::Watch(o) => o.execute(config, &mut *status),
Commands::External(all_args) => do_external(all_args),
};
process::exit(match r {
Ok(c) => c,
Err(e) => {
status.report_error(&SyncError::new(e).into());
1
}
})
}
trait TectonicCommand {
fn customize(&self, cc: &mut CommandCustomizations);
fn execute(self, config: PersistentConfig, status: &mut dyn StatusBackend) -> Result<i32>;
}
#[allow(clippy::large_enum_variant)]
#[derive(Debug, Subcommand)]
enum Commands {
#[command(name = "build")]
Build(BuildCommand),
#[command(name = "bundle")]
Bundle(BundleCommand),
#[command(name = "compile")]
Compile(crate::compile::CompileOptions),
#[command(name = "dump")]
Dump(DumpCommand),
#[command(name = "new")]
New(NewCommand),
#[command(name = "init")]
Init(InitCommand),
#[command(name = "show")]
Show(ShowCommand),
#[command(name = "watch")]
Watch(WatchCommand),
#[command(external_subcommand)]
External(Vec<String>),
}
#[cfg(unix)]
fn exec_or_spawn(cmd: &mut process::Command) -> Result<i32> {
use std::os::unix::process::CommandExt;
Err(cmd.exec().into())
}
#[cfg(not(unix))]
fn exec_or_spawn(cmd: &mut process::Command) -> Result<i32> {
Ok(cmd.status()?.code().unwrap())
}
#[cfg(unix)]
fn is_executable<P: AsRef<Path>>(path: P) -> bool {
use std::os::unix::prelude::*;
fs::metadata(path)
.map(|metadata| metadata.is_file() && metadata.permissions().mode() & 0o111 != 0)
.unwrap_or(false)
}
#[cfg(windows)]
fn is_executable<P: AsRef<Path>>(path: P) -> bool {
fs::metadata(path)
.map(|metadata| metadata.is_file())
.unwrap_or(false)
}
fn search_directories() -> Vec<PathBuf> {
let mut dirs = Vec::new();
if let Some(val) = env::var_os("PATH") {
dirs.extend(env::split_paths(&val));
}
dirs
}
fn do_external(all_args: Vec<String>) -> Result<i32> {
let (cmd, args) = all_args.split_first().unwrap();
let command_exe = format!("tectonic-{}{}", cmd, env::consts::EXE_SUFFIX);
let path = search_directories()
.iter()
.map(|dir| dir.join(&command_exe))
.find(|file| is_executable(file));
let command = path.ok_or_else(|| {
anyhow!(
"no internal or external subcommand `{0}` is available (install `tectonic-{0}`?)",
cmd
)
})?;
exec_or_spawn(process::Command::new(command).args(args))
}