use crate::plugins::command::Commands;
use crate::plugins::error::KError;
use crate::plugins::package_managers::PackageManagers;
use crate::plugins::properties::Properties;
use crate::plugins::settings::Settings;
use crate::types::Phase;
use camino::Utf8Path;
use camino::Utf8PathBuf;
use color_eyre::Section;
use color_eyre::SectionExt;
use eyre::WrapErr;
use paketkoll_types::backend::Backend;
use paketkoll_types::backend::Files;
use paketkoll_types::backend::PackageBackendMap;
use paketkoll_types::backend::PackageMapMap;
use paketkoll_types::intern::Interner;
use rune::Diagnostics;
use rune::Source;
use rune::Vm;
use rune::termcolor::Buffer;
use rune::termcolor::ColorChoice;
use rune::termcolor::StandardStream;
use std::panic::AssertUnwindSafe;
use std::panic::catch_unwind;
use std::sync::Arc;
use std::sync::OnceLock;
#[derive(Debug)]
pub struct EngineState {
pub(crate) properties: Properties,
pub(crate) commands: Commands,
pub(crate) settings: Arc<Settings>,
pub(crate) package_managers: Option<PackageManagers>,
}
pub(crate) static CFG_PATH: OnceLock<Utf8PathBuf> = OnceLock::new();
impl EngineState {
#[must_use]
pub fn new(files_path: Utf8PathBuf) -> Self {
let settings = Arc::new(Settings::default());
Self {
properties: Default::default(),
commands: Commands::new(files_path, settings.clone()),
settings,
package_managers: None,
}
}
pub fn setup_package_managers(
&mut self,
package_backends: &PackageBackendMap,
file_backend_id: Backend,
files_backend: &Arc<dyn Files>,
package_maps: &PackageMapMap,
interner: &Arc<Interner>,
) {
self.package_managers = Some(PackageManagers::create_from(
package_backends,
file_backend_id,
files_backend,
package_maps,
interner,
));
}
#[must_use]
pub fn settings(&self) -> Arc<Settings> {
Arc::clone(&self.settings)
}
#[must_use]
pub const fn commands(&self) -> &Commands {
&self.commands
}
pub fn commands_mut(&mut self) -> &mut Commands {
&mut self.commands
}
}
#[derive(Debug)]
pub struct ScriptEngine {
runtime: Arc<rune::runtime::RuntimeContext>,
sources: rune::Sources,
unit: Arc<rune::Unit>,
pub(crate) state: EngineState,
}
impl ScriptEngine {
pub fn create_context() -> Result<rune::Context, rune::ContextError> {
let mut context = rune::Context::with_default_modules()?;
crate::plugins::register_modules(&mut context)?;
context.install(rune_modules::json::module(true)?)?;
context.install(rune_modules::toml::module(true)?)?;
context.install(rune_modules::toml::de::module(true)?)?;
context.install(rune_modules::toml::ser::module(true)?)?;
Ok(context)
}
pub fn new_with_files(config_path: &Utf8Path) -> eyre::Result<Self> {
CFG_PATH.set(config_path.to_owned()).map_err(|v| {
eyre::eyre!("Failed to set CFG_PATH to {v}, this should not be called more than once")
})?;
let context = Self::create_context()?;
let state = EngineState::new(config_path.join("files"));
let mut diagnostics = Diagnostics::new();
let mut sources = rune::Sources::new();
sources
.insert(
Source::from_path(config_path.join("main.rn"))
.wrap_err("Failed to load main.rn")?,
)
.wrap_err("Failed to insert source file")?;
let result = rune::prepare(&mut sources)
.with_context(&context)
.with_diagnostics(&mut diagnostics)
.build();
if !diagnostics.is_empty() {
let mut writer = StandardStream::stderr(ColorChoice::Always);
diagnostics.emit(&mut writer, &sources)?;
}
Ok(Self {
runtime: Arc::new(context.runtime()?),
sources,
state,
unit: Arc::new(result?),
})
}
#[tracing::instrument(level = "info", name = "script", skip(self))]
pub async fn run_phase(&mut self, phase: Phase) -> eyre::Result<()> {
self.state.commands.phase = phase;
let mut vm = Vm::new(self.runtime.clone(), self.unit.clone());
tracing::info!("Calling script");
let output = match phase {
Phase::SystemDiscovery => {
vm.async_call(
[phase.as_str()],
(&mut self.state.properties, self.state.settings.as_ref()),
)
.await
}
Phase::Ignores | Phase::ScriptDependencies => {
vm.async_call(
[phase.as_str()],
(&mut self.state.properties, &mut self.state.commands),
)
.await
}
Phase::Main => {
vm.async_call(
[phase.as_str()],
(
&mut self.state.properties,
&mut self.state.commands,
self.state
.package_managers
.as_ref()
.expect("Package managers must be set"),
),
)
.await
}
};
let output = match output {
Ok(output) => output,
Err(e) => {
let err_str = format!("Rune error while executing {phase}: {}", &e);
tracing::error!("{}", err_str);
let mut writer = Buffer::ansi();
e.emit(&mut writer, &self.sources)?;
let rune_diag =
std::str::from_utf8(writer.as_slice().trim_ascii_end())?.to_string();
return Err(e)
.context("Rune runtime error")
.section(rune_diag.header(
" ━━━━━━━━━━━━━━━━━━━━━━━━ Rune Diagnostics and Backtrace \
━━━━━━━━━━━━━━━━━━━━━━━━\n",
));
}
};
tracing::info!("Returned from script");
match output {
rune::Value::Result(result) => match result.borrow_ref()?.as_ref() {
Ok(_) => (),
Err(e) => vm.with(|| try_format_error(phase, e))?,
},
_ => eyre::bail!("Got non-result from {phase}: {output:?}"),
}
Ok(())
}
#[inline]
#[must_use]
pub const fn state(&self) -> &EngineState {
&self.state
}
#[inline]
pub fn state_mut(&mut self) -> &mut EngineState {
&mut self.state
}
}
fn try_format_error(phase: Phase, value: &rune::Value) -> eyre::Result<()> {
match value.clone().into_any() {
rune::runtime::VmResult::Ok(any) => {
if let Ok(mut err) = any.downcast_borrow_mut::<KError>() {
tracing::error!("Got error result from {phase}: {}", *err.inner());
let err: eyre::Error = err.take_inner();
return Err(err);
}
if let Ok(err) = any.downcast_borrow_ref::<std::io::Error>() {
eyre::bail!("Got IO error result from {phase}: {:?}", *err);
}
let ty = try_get_type_info(value, "error");
let formatted = catch_unwind(AssertUnwindSafe(|| format!("{value:?}")));
eyre::bail!(
"Got error result from {phase}, but it is a unknown error type: {ty}: {any:?}, \
formats as: {formatted:?}",
);
}
rune::runtime::VmResult::Err(not_any) => {
tracing::error!(
"Got error result from {phase}, it was not an Any: {not_any:?}. Trying other \
approaches at printing the error."
);
}
}
let formatted = catch_unwind(AssertUnwindSafe(|| {
format!("Got error result from {phase}: {value:?}")
}));
match formatted {
Ok(str) => eyre::bail!(str),
Err(_) => {
let ty = try_get_type_info(value, "error");
eyre::bail!(
"Got error result from {phase}, but got a panic while attempting to format said \
error for printing, {ty}",
);
}
}
}
fn try_get_type_info(e: &rune::Value, what: &str) -> String {
match e.type_info() {
rune::runtime::VmResult::Ok(ty) => format!("type info for {what}: {ty:?}"),
rune::runtime::VmResult::Err(err) => {
format!("failed getting type info for {what}: {err:?}")
}
}
}