use std::{
fs, io,
path::{Path, PathBuf},
process::Command,
};
use jimtcl::{Interp, JimObject, JimResult, args::ProcArgParser, tcl_error};
use log::*;
use tempfile::tempdir;
use thiserror::Error;
#[derive(Error, Debug)]
pub enum CompileError {
#[error("I/O error: {0}")]
IO(#[from] io::Error),
#[error("Cargo build failed")]
BuildFailed,
#[error("Preparation failed: {0}")]
PrepFailed(String),
#[error("Preparation script failed: {0}")]
PrepScriptError(#[from] jimtcl::JimError),
#[error("Compilation output is missing")]
OutputMissing,
}
pub struct ScriptCompiler<'interp> {
interp: &'interp Interp,
work_dir: Option<PathBuf>,
tcl_source: Option<PathBuf>,
debug: bool,
}
impl<'interp> ScriptCompiler<'interp> {
pub fn new(interp: &'interp Interp) -> ScriptCompiler<'interp> {
ScriptCompiler {
interp,
work_dir: None,
tcl_source: None,
debug: false,
}
}
pub fn working_dir<P: AsRef<Path>>(&mut self, path: P) -> &mut Self {
self.work_dir = Some(path.as_ref().to_owned());
self
}
pub fn guardian_source_dir<P: AsRef<Path>>(&mut self, path: P) -> &mut Self {
self.tcl_source = Some(path.as_ref().to_owned());
self
}
pub fn compile(&self, src: &Path, dst: &Path) -> Result<(), CompileError> {
if let Some(dir) = &self.work_dir {
info!("using existing working directory {}", dir.display());
self.compile_in_dir(dir, src, dst)
} else {
debug!("using temporary working directory");
let workdir = tempdir()?;
self.compile_in_dir(workdir.path(), src, dst)
}
}
fn compile_in_dir(&self, work: &Path, src: &Path, dst: &Path) -> Result<(), CompileError> {
debug!("preparing compilation in {}", work.display());
let name = if let Some(name) = dst.file_name() {
name
} else {
return Err(CompileError::PrepFailed("output has no filename".into()));
};
let str_name = name
.to_str()
.ok_or(CompileError::PrepFailed("name not valid UTF-8".into()))?;
self.prepare_source(work, str_name)?;
self.compile_source(work, src, str_name)?;
let out_path = if self.debug {
work.join("target/debug").join(name)
} else {
work.join("target/release").join(name)
};
if !out_path.exists() {
error!("compile output does not exist: {}", out_path.display());
}
debug!("copying executable to destination");
fs::copy(&out_path, dst)?;
Ok(())
}
fn prepare_source(&self, path: &Path, name: &str) -> Result<(), CompileError> {
debug!(
"preparing project for {} and version {}",
name,
super::GUARTCL_VERSION
);
debug!("loading compiler script");
self.interp
.eval_source("compile.tcl", 1, include_str!("compile.tcl"))?;
let command = self.interp.new_object();
command.list_append("guardian::compiler::prepare");
command.list_append(path.as_os_str());
let info = self.interp.new_object();
info.dict_set("name", name)?;
if let Some(path) = &self.tcl_source {
let dir = path.canonicalize()?;
debug!("using Guardian source from {}", dir.display());
info.dict_set("guardian_path", dir.as_os_str())?;
}
command.list_append(info);
debug!("executing compiler command {}", command);
self.interp.eval_object(&command)?;
debug!("script source prepared");
Ok(())
}
fn compile_source(&self, work: &Path, src: &Path, name: &str) -> Result<(), CompileError> {
debug!("preparing to build {} in {}", src.display(), work.display());
let mut cmd = Command::new("cargo");
cmd.current_dir(work);
cmd.args(["build"]);
if !self.debug {
cmd.arg("--release");
}
cmd.env("GUARDIAN_SCRIPT_NAME", name);
let src = src
.canonicalize()
.inspect_err(|_| error!("{}: cannot normalize path", src.display()))?;
debug!("full source path: {}", src.display());
cmd.env("GUARDIAN_SCRIPT_SOURCE", src.as_os_str());
info!("invoking Rust compiler");
let mut child = cmd.spawn()?;
let ec = child.wait()?;
if ec.success() {
Ok(())
} else {
Err(CompileError::BuildFailed)
}
}
}
pub(crate) fn compile_command(interp: &Interp, args: &[JimObject<'_>]) -> JimResult<PathBuf> {
let mut parser = ProcArgParser::new();
parser.add_option("-guardian-src", "dir");
parser.add_flag("-debug");
parser.add_option("-out", "exe");
parser.add_required_arg("script");
let args = parser.parse(interp, args)?;
let mut compiler = ScriptCompiler::new(interp);
let src = args.require_arg("script")?.as_str()?;
let src = PathBuf::from(src);
let dst: PathBuf = if let Some(out) = args.get_option("-out") {
out.as_str()?.into()
} else {
src.with_extension("")
};
if let Some(tcl) = args.get_option("-guardian-src") {
let path = PathBuf::from(tcl.as_str()?);
compiler.guardian_source_dir(&path);
}
if args.get_flag("-debug") {
compiler.debug = true;
}
info!(
"compiling Tcl script {} to {}",
src.display(),
dst.display()
);
compiler
.compile(&src, &dst)
.map_err(|e| tcl_error!("compilation failed: {}", e))?;
Ok(dst)
}