Skip to main content

Maintain/Build/Rhai/
ScriptRunner.rs

1//=============================================================================//
2// Module: ScriptRunner - Executes Rhai scripts for dynamic configuration
3//=============================================================================//
4
5use std::{collections::HashMap, path::Path};
6
7use rhai::{AST, Dynamic, Engine, Scope};
8
9//=============================================================================
10// Result Types
11//=============================================================================
12
13#[derive(Debug, Clone)]
14pub struct ScriptResult {
15	/// Environment variables generated by the script
16	pub env_vars:HashMap<String, String>,
17
18	/// Whether the script executed successfully
19	pub success:bool,
20
21	/// Error message if execution failed
22	pub error:Option<String>,
23
24	/// Whether to continue with pre-build steps
25	pub pre_build_continue:bool,
26
27	/// Output from post-build steps
28	pub post_build_output:Option<String>,
29
30	/// Feature flags generated by the script
31	pub features:HashMap<String, bool>,
32
33	/// Workbench type recommended by the script
34	pub workbench:Option<String>,
35}
36
37#[derive(Debug, Clone)]
38pub struct ScriptContext {
39	/// Name of the profile being executed
40	pub profile_name:String,
41
42	/// Current working directory
43	pub cwd:String,
44
45	/// Manifest directory
46	pub manifest_dir:String,
47
48	/// Target triple for cross-compilation
49	pub target_triple:Option<String>,
50
51	/// Workbench type for this profile
52	pub workbench_type:Option<String>,
53
54	/// Feature flags for this profile
55	pub features:HashMap<String, bool>,
56}
57
58//=============================================================================
59// Public API
60//=============================================================================
61
62/// Executes a profile's Rhai script and returns the results.
63///
64/// # Arguments
65///
66/// * `engine` - The Rhai engine instance
67/// * `script_path` - Path to the Rhai script
68/// * `context` - Script execution context
69///
70/// # Returns
71///
72/// Result containing the script execution results
73pub fn ExecuteProfileScript(engine:&Engine, script_path:&str, context:&ScriptContext) -> Result<ScriptResult, String> {
74	let Ast = LoadScript(engine, script_path)?;
75
76	let mut Scope = CreateScope(context);
77
78	// Execute the script
79	let ExecutionResult = engine.run_ast_with_scope(&mut Scope, &Ast);
80
81	let mut Result = ScriptResult {
82		env_vars:HashMap::new(),
83
84		success:ExecutionResult.is_ok(),
85
86		error:None,
87
88		pre_build_continue:true,
89
90		post_build_output:None,
91
92		features:HashMap::new(),
93
94		workbench:None,
95	};
96
97	if let Err(Error) = ExecutionResult {
98		Result.error = Some(Error.to_string());
99
100		Result.pre_build_continue = false;
101
102		return Ok(Result);
103	}
104
105	// Extract environment variables from script result
106	if let Ok(EnvMap) = engine.call_fn(&mut Scope, &Ast, "get_env_vars", ()) {
107		Result.env_vars = ExtractEnvMap(EnvMap);
108	}
109
110	// Extract feature flags if the function exists
111	if let Ok(FeatureMap) = engine.call_fn(&mut Scope, &Ast, "get_features", ()) {
112		Result.features = ExtractFeatureMap(FeatureMap);
113	}
114
115	// Extract workbench type if the function exists
116	if let Ok(Workbench) = engine.call_fn::<String>(&mut Scope, &Ast, "get_workbench", ()) {
117		Result.workbench = Some(Workbench);
118	}
119
120	// Check if pre-build should continue
121	if let Ok(ContinueResult) = engine.call_fn::<bool>(&mut Scope, &Ast, "pre_build_continue", ()) {
122		Result.pre_build_continue = ContinueResult;
123	}
124
125	// Get post-build output if available
126	if let Ok(Output) = engine.call_fn::<String>(&mut Scope, &Ast, "post_build_output", ()) {
127		Result.post_build_output = Some(Output);
128	}
129
130	Ok(Result)
131}
132
133/// Loads and compiles a Rhai script.
134///
135/// # Arguments
136///
137/// * `engine` - The Rhai engine instance
138/// * `script_path` - Path to the script file
139///
140/// # Returns
141///
142/// Result containing the compiled AST
143pub fn LoadScript(engine:&Engine, script_path:&str) -> Result<AST, String> {
144	if !Path::new(script_path).exists() {
145		return Err(format!("Script file not found: {}", script_path));
146	}
147
148	let Content = std::fs::read_to_string(script_path).map_err(|Error| format!("Failed to read script: {}", Error))?;
149
150	let Ast = engine
151		.compile(&Content)
152		.map_err(|Error| format!("Failed to compile script: {}", Error))?;
153
154	Ok(Ast)
155}
156
157/// Creates a Rhai engine configured for build scripts.
158///
159/// # Returns
160///
161/// Configured Rhai engine instance
162pub fn CreateEngine() -> Engine {
163	let mut Engine = Engine::new();
164
165	// Register custom functions for build scripts
166	Engine.register_fn("env", |name:&str| -> String { std::env::var(name).unwrap_or_default() });
167
168	Engine.register_fn("env_or", |name:&str, default:&str| -> String {
169		std::env::var(name).unwrap_or_else(|_| default.to_string())
170	});
171
172	Engine.register_fn("set_env", |name:&str, value:&str| {
173		// Safety: set_var is now unsafe in recent Rust versions
174		// In a build context, setting environment variables during script execution
175		// is acceptable as it doesn't violate memory safety - it just modifies
176		// the process environment map.
177		unsafe {
178			std::env::set_var(name, value);
179		}
180	});
181
182	Engine.register_fn("log", |message:&str| {
183		println!("[Rhai] {}", message);
184	});
185
186	Engine.register_fn("log_error", |message:&str| {
187		eprintln!("[Rhai ERROR] {}", message);
188	});
189
190	Engine.register_fn("log_warn", |message:&str| {
191		eprintln!("[Rhai WARN] {}", message);
192	});
193
194	// Path manipulation functions
195	Engine.register_fn("path_join", |base:&str, suffix:&str| -> String {
196		Path::new(base).join(suffix).to_string_lossy().to_string()
197	});
198
199	Engine.register_fn("path_exists", |path:&str| -> bool { Path::new(path).exists() });
200
201	// String utilities
202	Engine.register_fn("to_uppercase", |s:&str| -> String { s.to_uppercase() });
203
204	Engine.register_fn("to_lowercase", |s:&str| -> String { s.to_lowercase() });
205
206	Engine
207}
208
209//=============================================================================
210// Helper Functions
211//=============================================================================
212
213/// Creates a Rhai scope with the script context.
214fn CreateScope(context:&ScriptContext) -> Scope<'_> {
215	let mut Scope = Scope::new();
216
217	Scope.push("profile_name", context.profile_name.clone());
218
219	Scope.push("cwd", context.cwd.clone());
220
221	Scope.push("manifest_dir", context.manifest_dir.clone());
222
223	Scope.push("target_triple", context.target_triple.clone().unwrap_or_default());
224
225	Scope.push("workbench_type", context.workbench_type.clone().unwrap_or_default());
226
227	// Add features as a map
228	let mut FeaturesMap = rhai::Map::new();
229
230	for (Key, Value) in &context.features {
231		FeaturesMap.insert(Key.into(), (*Value).into());
232	}
233
234	Scope.push("features", FeaturesMap);
235
236	Scope
237}
238
239/// Extracts environment variables from a Rhai dynamic value.
240fn ExtractEnvMap(Dynamic:Dynamic) -> HashMap<String, String> {
241	let mut EnvMap = HashMap::new();
242
243	if let Some(Map) = Dynamic.try_cast::<rhai::Map>() {
244		for (Key, Value) in Map {
245			if Value.is_string() {
246				EnvMap.insert(Key.to_string(), Value.to_string());
247			} else if Value.is_int() {
248				EnvMap.insert(Key.to_string(), Value.as_int().unwrap_or(0).to_string());
249			} else if Value.is_bool() {
250				EnvMap.insert(Key.to_string(), Value.as_bool().unwrap_or(false).to_string());
251			} else {
252				EnvMap.insert(Key.to_string(), Value.to_string());
253			}
254		}
255	}
256
257	EnvMap
258}
259
260/// Extracts feature flags from a Rhai dynamic value.
261fn ExtractFeatureMap(Dynamic:Dynamic) -> HashMap<String, bool> {
262	let mut FeatureMap = HashMap::new();
263
264	if let Some(Map) = Dynamic.try_cast::<rhai::Map>() {
265		for (Key, Value) in Map {
266			if Value.is_bool() {
267				FeatureMap.insert(Key.to_string(), Value.as_bool().unwrap_or(false));
268			}
269		}
270	}
271
272	FeatureMap
273}
274
275//=============================================================================
276// Tests
277//=============================================================================
278
279#[cfg(test)]
280mod tests {
281
282	use super::*;
283
284	#[test]
285	fn test_create_engine() {
286		let Engine = CreateEngine();
287
288		// Verify engine is created successfully
289		assert!(Engine.compile("let x = 1;").is_ok());
290	}
291
292	#[test]
293	fn test_extract_env_map() {
294		let mut map = rhai::Map::new();
295
296		map.insert("KEY1".into(), "value1".into());
297
298		map.insert("KEY2".into(), 42.into());
299
300		map.insert("KEY3".into(), true.into());
301
302		let Dynamic = Dynamic::from(map);
303
304		let Env = ExtractEnvMap(Dynamic);
305
306		assert_eq!(Env.get("KEY1"), Some(&"value1".to_string()));
307
308		assert_eq!(Env.get("KEY2"), Some(&"42".to_string()));
309
310		assert_eq!(Env.get("KEY3"), Some(&"true".to_string()));
311	}
312
313	#[test]
314	fn test_extract_feature_map() {
315		let mut map = rhai::Map::new();
316
317		map.insert("feature1".into(), true.into());
318
319		map.insert("feature2".into(), false.into());
320
321		let Dynamic = Dynamic::from(map);
322
323		let Features = ExtractFeatureMap(Dynamic);
324
325		assert_eq!(Features.get("feature1"), Some(&true));
326
327		assert_eq!(Features.get("feature2"), Some(&false));
328	}
329}