Skip to main content

ninja/scripting/
mod.rs

1//! Lua scripting engine and DSL support.
2//!
3//! Provides a Lua-based scripting environment for executing Shuriken startup, stop,
4//! configuration, and custom scripts. Includes:
5//!
6//! - [`dsl`]: Domain-specific language for command execution
7//! - [`templater`]: Template rendering for configuration files
8//! - Lua modules: fs, env, shell, time, json, http, log, proc
9//!
10//! The engine compiles scripts to bytecode and manages execution context.
11
12pub mod dsl;
13pub mod templater;
14
15mod modules;
16use crate::{manager::ShurikenManager, utils::resolve_path};
17use log::info;
18use mlua::{Error as LuaError, Lua};
19use modules::{
20    make_env_module, make_fs_module, make_modules, make_ninja_module, make_proc_module,
21    make_shell_module,
22};
23use regex::Regex;
24use std::{
25    fs,
26    path::{Path, PathBuf},
27};
28
29#[derive(Clone, Debug)]
30pub struct NinjaEngine {
31    preload_dir: Option<PathBuf>,
32    #[cfg(feature = "testing")]
33    pub lua: Lua,
34    #[cfg(not(feature = "testing"))]
35    lua: Lua,
36}
37
38impl NinjaEngine {
39    fn load_preloads(&self) -> Result<(), LuaError> {
40        let Some(dir) = &self.preload_dir else {
41            return Ok(());
42        };
43
44        let lua = &self.lua;
45        let globals = lua.globals();
46
47        if !dir.exists() {
48            return Ok(());
49        }
50
51        for entry in std::fs::read_dir(dir)? {
52            let path = entry?.path();
53
54            if path.extension().and_then(|s| s.to_str()) != Some("ns") {
55                continue;
56            }
57
58            let re = Regex::new(r"export\s+function\s+([a-zA-Z_][a-zA-Z0-9_]*)")
59                .map_err(|e| LuaError::RuntimeError(e.to_string()))?;
60
61            let name = path.file_stem().unwrap().to_string_lossy();
62            let raw_script =
63                std::fs::read_to_string(&path)?.replace("export function", "function __mod.");
64            let transformed = re.replace_all(&raw_script, "function __mod.$1").to_string();
65            let script = format!("local __mod = {{}}; {}\nreturn __mod", transformed);
66
67            let module: mlua::Table = lua.load(&script).eval()?;
68
69            globals.set(name.as_ref(), module)?;
70        }
71
72        Ok(())
73    }
74    /// Default constructor: modules are created with no fixed cwd.
75    /// All existing call sites can keep using this.
76    pub async fn new() -> Result<Self, Box<dyn std::error::Error>> {
77        let lua = Lua::new_with(mlua::StdLib::ALL_SAFE, mlua::LuaOptions::default())?;
78        let globals = lua.globals();
79
80        let (fs, env, shell, time, json, http, log, proc) = make_modules(&lua, None).await?;
81
82        globals.set("fs", fs)?;
83        globals.set("env", env)?;
84        globals.set("shell", shell)?;
85        globals.set("time", time)?;
86        globals.set("json", json)?;
87        globals.set("http", http)?;
88        globals.set("log", log)?;
89        globals.set("proc", proc)?;
90
91        let engine = Self {
92            lua,
93            preload_dir: Some(PathBuf::from(".ninja/preloads")),
94        };
95
96        engine.load_preloads()?;
97
98        Ok(engine)
99    }
100
101    /// Execute a raw Lua script in the global environment.
102    pub async fn execute(
103        &self,
104        script: &str,
105        cwd: Option<&Path>,
106        mgr: Option<ShurikenManager>,
107    ) -> Result<(), LuaError> {
108        let globals = self.lua.globals();
109
110        if let Some(mgr) = mgr {
111            let ninja = make_ninja_module(&self.lua, mgr)?;
112            globals.set("ninja", ninja)?;
113        }
114
115        let fs = make_fs_module(&self.lua, cwd)?;
116        let env = make_env_module(&self.lua, cwd)?;
117        let shell = make_shell_module(&self.lua, cwd)?;
118        let proc = make_proc_module(&self.lua, cwd)?;
119        globals.set("fs", fs)?;
120        globals.set("env", env)?;
121        globals.set("shell", shell)?;
122        globals.set("proc", proc)?;
123
124        info!("Executing lua script.");
125        self.lua.load(script).exec_async().await
126    }
127
128    /// Execute a file in the global environment, resolving path optionally against `cwd`.
129    pub async fn execute_file(
130        &self,
131        path: &PathBuf,
132        cwd: Option<&Path>,
133        mgr: Option<ShurikenManager>,
134    ) -> Result<(), LuaError> {
135        info!("Executing file: {:#?}", path);
136        let globals = self.lua.globals();
137
138        if let Some(mgr) = mgr {
139            let ninja = make_ninja_module(&self.lua, mgr)?;
140            globals.set("ninja", ninja)?;
141        }
142
143        let script = if let Some(cwd) = cwd {
144            let fs = make_fs_module(&self.lua, Some(cwd))?;
145            let env = make_env_module(&self.lua, Some(cwd))?;
146            let shell = make_shell_module(&self.lua, Some(cwd))?;
147            let proc = make_proc_module(&self.lua, Some(cwd))?;
148            globals.set("fs", fs)?;
149            globals.set("env", env)?;
150            globals.set("shell", shell)?;
151            globals.set("proc", proc)?;
152            fs::read_to_string(resolve_path(cwd, path))?
153        } else {
154            fs::read_to_string(path)?
155        };
156
157        self.lua.load(script).exec_async().await
158    }
159
160    /// Execute a specific function from a script in an isolated environment.
161    /// The script is loaded from `path` (optionally resolved against `cwd`),
162    /// its globals live in a fresh env that inherits from `lua.globals()`,
163    /// and then `function` is retrieved from that env and called.
164    pub async fn execute_function(
165        &self,
166        function: &str,
167        path: &PathBuf,
168        cwd: Option<&Path>,
169        mgr: Option<ShurikenManager>,
170    ) -> Result<(), LuaError> {
171        let lua = &self.lua;
172        let globals = self.lua.globals();
173
174        if let Some(mgr) = mgr {
175            let ninja = make_ninja_module(&self.lua, mgr)?;
176            globals.set("ninja", ninja)?;
177        }
178
179        let script = if let Some(cwd) = cwd {
180            let fs = make_fs_module(&self.lua, Some(cwd))?;
181            let env = make_env_module(&self.lua, Some(cwd))?;
182            let shell = make_shell_module(&self.lua, Some(cwd))?;
183            let proc = make_proc_module(&self.lua, Some(cwd))?;
184            globals.set("fs", fs)?;
185            globals.set("env", env)?;
186            globals.set("shell", shell)?;
187            globals.set("proc", proc)?;
188            fs::read_to_string(resolve_path(cwd, path))?
189        } else {
190            fs::read_to_string(path)?
191        };
192
193        // Create isolated env for the script
194        let env = lua.create_table()?;
195
196        // Make environment inherit standard globals
197        let globals = lua.globals();
198        env.set_metatable(Some(lua.create_table_from([("__index", globals)])?))?;
199
200        // Load script into the isolated environment
201        let chunk = lua.load(&script).set_environment(env.clone());
202
203        // Execute and capture the return value
204        let result: mlua::Value = chunk.eval_async().await?;
205
206        // Try to get the function from the returned value first (if it's a table)
207        let func: mlua::Function = if let mlua::Value::Table(table) = result {
208            // Script returned a table, try to get the function from it
209            table.get(function)?
210        } else {
211            // Script didn't return a table, try to get the function from the environment
212            env.get(function)?
213        };
214
215        // Call the function with no arguments
216        func.call_async::<()>(()).await
217    }
218}