konfigkoll_script/
engine.rs

1use crate::plugins::command::Commands;
2use crate::plugins::error::KError;
3use crate::plugins::package_managers::PackageManagers;
4use crate::plugins::properties::Properties;
5use crate::plugins::settings::Settings;
6use crate::types::Phase;
7use camino::Utf8Path;
8use camino::Utf8PathBuf;
9use color_eyre::Section;
10use color_eyre::SectionExt;
11use eyre::WrapErr;
12use paketkoll_types::backend::Backend;
13use paketkoll_types::backend::Files;
14use paketkoll_types::backend::PackageBackendMap;
15use paketkoll_types::backend::PackageMapMap;
16use paketkoll_types::intern::Interner;
17use rune::Diagnostics;
18use rune::Source;
19use rune::Vm;
20use rune::termcolor::Buffer;
21use rune::termcolor::ColorChoice;
22use rune::termcolor::StandardStream;
23use std::panic::AssertUnwindSafe;
24use std::panic::catch_unwind;
25use std::sync::Arc;
26use std::sync::OnceLock;
27
28/// State being built up by the scripts as it runs
29#[derive(Debug)]
30pub struct EngineState {
31    /// Properties set by the user
32    pub(crate) properties: Properties,
33    /// Commands to be applied to the system
34    pub(crate) commands: Commands,
35    /// Settings of how konfigkoll should behave.
36    pub(crate) settings: Arc<Settings>,
37    /// All the enabled package managers
38    pub(crate) package_managers: Option<PackageManagers>,
39}
40
41/// Path to the configuration directory
42pub(crate) static CFG_PATH: OnceLock<Utf8PathBuf> = OnceLock::new();
43
44impl EngineState {
45    #[must_use]
46    pub fn new(files_path: Utf8PathBuf) -> Self {
47        let settings = Arc::new(Settings::default());
48        Self {
49            properties: Default::default(),
50            commands: Commands::new(files_path, settings.clone()),
51            settings,
52            package_managers: None,
53        }
54    }
55
56    pub fn setup_package_managers(
57        &mut self,
58        package_backends: &PackageBackendMap,
59        file_backend_id: Backend,
60        files_backend: &Arc<dyn Files>,
61        package_maps: &PackageMapMap,
62        interner: &Arc<Interner>,
63    ) {
64        self.package_managers = Some(PackageManagers::create_from(
65            package_backends,
66            file_backend_id,
67            files_backend,
68            package_maps,
69            interner,
70        ));
71    }
72
73    #[must_use]
74    pub fn settings(&self) -> Arc<Settings> {
75        Arc::clone(&self.settings)
76    }
77
78    #[must_use]
79    pub const fn commands(&self) -> &Commands {
80        &self.commands
81    }
82
83    pub fn commands_mut(&mut self) -> &mut Commands {
84        &mut self.commands
85    }
86}
87
88/// The script engine that is the main entry point for this crate.
89#[derive(Debug)]
90pub struct ScriptEngine {
91    runtime: Arc<rune::runtime::RuntimeContext>,
92    sources: rune::Sources,
93    /// User scripts
94    unit: Arc<rune::Unit>,
95    /// Properties exposed by us or set by the user
96    pub(crate) state: EngineState,
97}
98
99impl ScriptEngine {
100    pub fn create_context() -> Result<rune::Context, rune::ContextError> {
101        let mut context = rune::Context::with_default_modules()?;
102
103        // Register modules
104        crate::plugins::register_modules(&mut context)?;
105        context.install(rune_modules::json::module(true)?)?;
106        context.install(rune_modules::toml::module(true)?)?;
107        context.install(rune_modules::toml::de::module(true)?)?;
108        context.install(rune_modules::toml::ser::module(true)?)?;
109
110        Ok(context)
111    }
112
113    pub fn new_with_files(config_path: &Utf8Path) -> eyre::Result<Self> {
114        CFG_PATH.set(config_path.to_owned()).map_err(|v| {
115            eyre::eyre!("Failed to set CFG_PATH to {v}, this should not be called more than once")
116        })?;
117        let context = Self::create_context()?;
118
119        // Create state
120        let state = EngineState::new(config_path.join("files"));
121
122        // Load scripts
123        let mut diagnostics = Diagnostics::new();
124
125        let mut sources = rune::Sources::new();
126        sources
127            .insert(
128                Source::from_path(config_path.join("main.rn"))
129                    .wrap_err("Failed to load main.rn")?,
130            )
131            .wrap_err("Failed to insert source file")?;
132
133        let result = rune::prepare(&mut sources)
134            .with_context(&context)
135            .with_diagnostics(&mut diagnostics)
136            .build();
137
138        if !diagnostics.is_empty() {
139            let mut writer = StandardStream::stderr(ColorChoice::Always);
140            diagnostics.emit(&mut writer, &sources)?;
141        }
142
143        // Create ScriptEngine
144        Ok(Self {
145            runtime: Arc::new(context.runtime()?),
146            sources,
147            state,
148            unit: Arc::new(result?),
149        })
150    }
151
152    /// Call a function in the script
153    #[tracing::instrument(level = "info", name = "script", skip(self))]
154    pub async fn run_phase(&mut self, phase: Phase) -> eyre::Result<()> {
155        // Update phase in relevant state
156        self.state.commands.phase = phase;
157        // Create VM and do call
158        let mut vm = Vm::new(self.runtime.clone(), self.unit.clone());
159        tracing::info!("Calling script");
160        let output = match phase {
161            Phase::SystemDiscovery => {
162                vm.async_call(
163                    [phase.as_str()],
164                    (&mut self.state.properties, self.state.settings.as_ref()),
165                )
166                .await
167            }
168            Phase::Ignores | Phase::ScriptDependencies => {
169                vm.async_call(
170                    [phase.as_str()],
171                    (&mut self.state.properties, &mut self.state.commands),
172                )
173                .await
174            }
175            Phase::Main => {
176                vm.async_call(
177                    [phase.as_str()],
178                    (
179                        &mut self.state.properties,
180                        &mut self.state.commands,
181                        self.state
182                            .package_managers
183                            .as_ref()
184                            .expect("Package managers must be set"),
185                    ),
186                )
187                .await
188            }
189        };
190        // Handle rune runtime errors
191        let output = match output {
192            Ok(output) => output,
193            Err(e) => {
194                let err_str = format!("Rune error while executing {phase}: {}", &e);
195                tracing::error!("{}", err_str);
196                let mut writer = Buffer::ansi();
197                e.emit(&mut writer, &self.sources)?;
198
199                let rune_diag =
200                    std::str::from_utf8(writer.as_slice().trim_ascii_end())?.to_string();
201
202                return Err(e)
203                    .context("Rune runtime error")
204                    .section(rune_diag.header(
205                        "  ━━━━━━━━━━━━━━━━━━━━━━━━ Rune Diagnostics and Backtrace \
206                         ━━━━━━━━━━━━━━━━━━━━━━━━\n",
207                    ));
208            }
209        };
210        tracing::info!("Returned from script");
211        // Do error handling on the returned result
212        match output {
213            rune::Value::Result(result) => match result.borrow_ref()?.as_ref() {
214                Ok(_) => (),
215                Err(e) => vm.with(|| try_format_error(phase, e))?,
216            },
217            _ => eyre::bail!("Got non-result from {phase}: {output:?}"),
218        }
219        Ok(())
220    }
221
222    #[inline]
223    #[must_use]
224    pub const fn state(&self) -> &EngineState {
225        &self.state
226    }
227
228    #[inline]
229    pub fn state_mut(&mut self) -> &mut EngineState {
230        &mut self.state
231    }
232}
233
234/// Attempt to format the error in the best way possible.
235///
236/// Unfortunately this is awkward with dynamic Rune values.
237fn try_format_error(phase: Phase, value: &rune::Value) -> eyre::Result<()> {
238    match value.clone().into_any() {
239        rune::runtime::VmResult::Ok(any) => {
240            if let Ok(mut err) = any.downcast_borrow_mut::<KError>() {
241                tracing::error!("Got error result from {phase}: {}", *err.inner());
242                let err: eyre::Error = err.take_inner();
243                return Err(err);
244            }
245            if let Ok(err) = any.downcast_borrow_ref::<std::io::Error>() {
246                eyre::bail!("Got IO error result from {phase}: {:?}", *err);
247            }
248            let ty = try_get_type_info(value, "error");
249            let formatted = catch_unwind(AssertUnwindSafe(|| format!("{value:?}")));
250            eyre::bail!(
251                "Got error result from {phase}, but it is a unknown error type: {ty}: {any:?}, \
252                 formats as: {formatted:?}",
253            );
254        }
255        rune::runtime::VmResult::Err(not_any) => {
256            tracing::error!(
257                "Got error result from {phase}, it was not an Any: {not_any:?}. Trying other \
258                 approaches at printing the error."
259            );
260        }
261    }
262    // Attempt to format the error
263    let formatted = catch_unwind(AssertUnwindSafe(|| {
264        format!("Got error result from {phase}: {value:?}")
265    }));
266    match formatted {
267        Ok(str) => eyre::bail!(str),
268        Err(_) => {
269            let ty = try_get_type_info(value, "error");
270            eyre::bail!(
271                "Got error result from {phase}, but got a panic while attempting to format said \
272                 error for printing, {ty}",
273            );
274        }
275    }
276}
277
278/// Best effort attempt at gettint the type info and printing it
279fn try_get_type_info(e: &rune::Value, what: &str) -> String {
280    match e.type_info() {
281        rune::runtime::VmResult::Ok(ty) => format!("type info for {what}: {ty:?}"),
282        rune::runtime::VmResult::Err(err) => {
283            format!("failed getting type info for {what}: {err:?}")
284        }
285    }
286}