Skip to main content

ninja/scripting/
mod.rs

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