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	/// 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 ExecuteProfileScript(engine:&Engine, script_path:&str, context:&ScriptContext) -> Result<ScriptResult, String> {
63	let Ast = LoadScript(engine, script_path)?;
64	let mut Scope = CreateScope(context);
65
66	// Execute the script
67	let ExecutionResult = engine.run_ast_with_scope(&mut Scope, &Ast);
68
69	let mut Result = ScriptResult {
70		env_vars:HashMap::new(),
71		success:ExecutionResult.is_ok(),
72		error:None,
73		pre_build_continue:true,
74		post_build_output:None,
75		features:HashMap::new(),
76		workbench:None,
77	};
78
79	if let Err(Error) = ExecutionResult {
80		Result.error = Some(Error.to_string());
81		Result.pre_build_continue = false;
82		return Ok(Result);
83	}
84
85	// Extract environment variables from script result
86	if let Ok(EnvMap) = engine.call_fn(&mut Scope, &Ast, "get_env_vars", ()) {
87		Result.env_vars = ExtractEnvMap(EnvMap);
88	}
89
90	// Extract feature flags if the function exists
91	if let Ok(FeatureMap) = engine.call_fn(&mut Scope, &Ast, "get_features", ()) {
92		Result.features = ExtractFeatureMap(FeatureMap);
93	}
94
95	// Extract workbench type if the function exists
96	if let Ok(Workbench) = engine.call_fn::<String>(&mut Scope, &Ast, "get_workbench", ()) {
97		Result.workbench = Some(Workbench);
98	}
99
100	// Check if pre-build should continue
101	if let Ok(ContinueResult) = engine.call_fn::<bool>(&mut Scope, &Ast, "pre_build_continue", ()) {
102		Result.pre_build_continue = ContinueResult;
103	}
104
105	// Get post-build output if available
106	if let Ok(Output) = engine.call_fn::<String>(&mut Scope, &Ast, "post_build_output", ()) {
107		Result.post_build_output = Some(Output);
108	}
109
110	Ok(Result)
111}
112
113/// Loads and compiles a Rhai script.
114///
115/// # Arguments
116///
117/// * `engine` - The Rhai engine instance
118/// * `script_path` - Path to the script file
119///
120/// # Returns
121///
122/// Result containing the compiled AST
123pub fn LoadScript(engine:&Engine, script_path:&str) -> Result<AST, String> {
124	if !Path::new(script_path).exists() {
125		return Err(format!("Script file not found: {}", script_path));
126	}
127
128	let Content = std::fs::read_to_string(script_path).map_err(|Error| format!("Failed to read script: {}", Error))?;
129
130	let Ast = engine
131		.compile(&Content)
132		.map_err(|Error| format!("Failed to compile script: {}", Error))?;
133
134	Ok(Ast)
135}
136
137/// Creates a Rhai engine configured for build scripts.
138///
139/// # Returns
140///
141/// Configured Rhai engine instance
142pub fn CreateEngine() -> Engine {
143	let mut Engine = Engine::new();
144
145	// Register custom functions for build scripts
146	Engine.register_fn("env", |name:&str| -> String { std::env::var(name).unwrap_or_default() });
147
148	Engine.register_fn("env_or", |name:&str, default:&str| -> String {
149		std::env::var(name).unwrap_or_else(|_| default.to_string())
150	});
151
152	Engine.register_fn("set_env", |name:&str, value:&str| {
153		// Safety: set_var is now unsafe in recent Rust versions
154		// In a build context, setting environment variables during script execution
155		// is acceptable as it doesn't violate memory safety - it just modifies
156		// the process environment map.
157		unsafe {
158			std::env::set_var(name, value);
159		}
160	});
161
162	Engine.register_fn("log", |message:&str| {
163		println!("[Rhai] {}", message);
164	});
165
166	Engine.register_fn("log_error", |message:&str| {
167		eprintln!("[Rhai ERROR] {}", message);
168	});
169
170	Engine.register_fn("log_warn", |message:&str| {
171		eprintln!("[Rhai WARN] {}", message);
172	});
173
174	// Path manipulation functions
175	Engine.register_fn("path_join", |base:&str, suffix:&str| -> String {
176		Path::new(base).join(suffix).to_string_lossy().to_string()
177	});
178
179	Engine.register_fn("path_exists", |path:&str| -> bool { Path::new(path).exists() });
180
181	// String utilities
182	Engine.register_fn("to_uppercase", |s:&str| -> String { s.to_uppercase() });
183
184	Engine.register_fn("to_lowercase", |s:&str| -> String { s.to_lowercase() });
185
186	Engine
187}
188
189//=============================================================================
190// Helper Functions
191//=============================================================================
192
193/// Creates a Rhai scope with the script context.
194fn CreateScope(context:&ScriptContext) -> Scope<'_> {
195	let mut Scope = Scope::new();
196
197	Scope.push("profile_name", context.profile_name.clone());
198	Scope.push("cwd", context.cwd.clone());
199	Scope.push("manifest_dir", context.manifest_dir.clone());
200	Scope.push("target_triple", context.target_triple.clone().unwrap_or_default());
201	Scope.push("workbench_type", context.workbench_type.clone().unwrap_or_default());
202
203	// Add features as a map
204	let mut FeaturesMap = rhai::Map::new();
205	for (Key, Value) in &context.features {
206		FeaturesMap.insert(Key.into(), (*Value).into());
207	}
208	Scope.push("features", FeaturesMap);
209
210	Scope
211}
212
213/// Extracts environment variables from a Rhai dynamic value.
214fn ExtractEnvMap(Dynamic:Dynamic) -> HashMap<String, String> {
215	let mut EnvMap = HashMap::new();
216
217	if let Some(Map) = Dynamic.try_cast::<rhai::Map>() {
218		for (Key, Value) in Map {
219			if Value.is_string() {
220				EnvMap.insert(Key.to_string(), Value.to_string());
221			} else if Value.is_int() {
222				EnvMap.insert(Key.to_string(), Value.as_int().unwrap_or(0).to_string());
223			} else if Value.is_bool() {
224				EnvMap.insert(Key.to_string(), Value.as_bool().unwrap_or(false).to_string());
225			} else {
226				EnvMap.insert(Key.to_string(), Value.to_string());
227			}
228		}
229	}
230
231	EnvMap
232}
233
234/// Extracts feature flags from a Rhai dynamic value.
235fn ExtractFeatureMap(Dynamic:Dynamic) -> HashMap<String, bool> {
236	let mut FeatureMap = HashMap::new();
237
238	if let Some(Map) = Dynamic.try_cast::<rhai::Map>() {
239		for (Key, Value) in Map {
240			if Value.is_bool() {
241				FeatureMap.insert(Key.to_string(), Value.as_bool().unwrap_or(false));
242			}
243		}
244	}
245
246	FeatureMap
247}
248
249//=============================================================================
250// Tests
251//=============================================================================
252
253#[cfg(test)]
254mod tests {
255	use super::*;
256
257	#[test]
258	fn test_create_engine() {
259		let Engine = CreateEngine();
260		// Verify engine is created successfully
261		assert!(Engine.compile("let x = 1;").is_ok());
262	}
263
264	#[test]
265	fn test_extract_env_map() {
266		let mut map = rhai::Map::new();
267		map.insert("KEY1".into(), "value1".into());
268		map.insert("KEY2".into(), 42.into());
269		map.insert("KEY3".into(), true.into());
270
271		let Dynamic = Dynamic::from(map);
272		let Env = ExtractEnvMap(Dynamic);
273
274		assert_eq!(Env.get("KEY1"), Some(&"value1".to_string()));
275		assert_eq!(Env.get("KEY2"), Some(&"42".to_string()));
276		assert_eq!(Env.get("KEY3"), Some(&"true".to_string()));
277	}
278
279	#[test]
280	fn test_extract_feature_map() {
281		let mut map = rhai::Map::new();
282		map.insert("feature1".into(), true.into());
283		map.insert("feature2".into(), false.into());
284
285		let Dynamic = Dynamic::from(map);
286		let Features = ExtractFeatureMap(Dynamic);
287
288		assert_eq!(Features.get("feature1"), Some(&true));
289		assert_eq!(Features.get("feature2"), Some(&false));
290	}
291}