Maintain/Build/Rhai/
ScriptRunner.rs

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