Skip to main content

Maintain/Build/Rhai/
EnvironmentResolver.rs

1//=============================================================================//
2// File Path: Element/Maintain/Source/Build/Rhai/EnvironmentResolver.rs
3//=============================================================================//
4// Module: EnvironmentResolver
5//
6// Brief Description: Resolves final environment variables from all sources.
7//
8// RESPONSIBILITIES:
9// ================
10//
11// Primary:
12// - Merge environment variables from multiple sources
13// - Add environment variable prefixes per crate
14// - Apply environment variables to current process
15// - Provide validation of required environment variables
16// - Support workbench and feature flag resolution
17//
18// Secondary:
19// - Log environment variable resolution
20// - Support variable expansion (e.g., ${VAR})
21// - Generate feature flag environment variables
22//
23//=============================================================================//
24
25use std::collections::HashMap;
26
27//=============================================================================
28// Public API
29//=============================================================================
30
31/// Resolves the final set of environment variables.
32///
33/// This function merges variables from:
34/// 1. Template defaults
35/// 2. Profile-specific static variables
36/// 3. Workbench environment variables
37/// 4. Feature flag variables
38/// 5. Rhai script output
39/// 6. Current process environment
40///
41/// # Arguments
42///
43/// * `template_env` - Environment variables from templates
44/// * `profile_env` - Environment variables from profile
45/// * `workbench_env` - Environment variables from workbench selection
46/// * `feature_env` - Environment variables from feature flags
47/// * `script_env` - Environment variables from Rhai script
48/// * `preserve_current` - Whether to preserve current process environment
49///
50/// # Returns
51///
52/// Final resolved HashMap of environment variables
53///
54/// # Example
55///
56/// ```no_run
57/// use crate::Maintain::Source::Build::Rhai::EnvironmentResolver;
58///
59/// let templates = HashMap::from([("PATH", "/usr/bin".to_string())]);
60///
61/// let profile = HashMap::from([("NODE_ENV", "development".to_string())]);
62///
63/// let workbench = HashMap::from([("Mountain", "true".to_string())]);
64///
65/// let features = HashMap::from([("FEATURE_TAURI_IPC", "true".to_string())]);
66///
67/// let script = HashMap::from([("RUST_LOG", "debug".to_string())]);
68///
69/// let final_env =
70/// 	EnvironmentResolver::resolve_full(templates, profile, workbench, features, script, true);
71/// ```
72pub fn ResolveFull(
73	template_env:HashMap<String, String>,
74
75	profile_env:HashMap<String, String>,
76
77	workbench_env:HashMap<String, String>,
78
79	feature_env:HashMap<String, String>,
80
81	script_env:HashMap<String, String>,
82
83	preserve_current:bool,
84) -> HashMap<String, String> {
85	let mut resolved = HashMap::new();
86
87	// Start with current environment if requested
88	if preserve_current {
89		for (key, value) in std::env::vars_os() {
90			if let (Ok(key_str), Ok(value_str)) = (key.into_string(), value.into_string()) {
91				resolved.insert(key_str, value_str);
92			}
93		}
94	}
95
96	// Apply template values (lowest priority)
97	for (key, value) in template_env {
98		resolved.insert(key, value);
99	}
100
101	// Apply profile values (overriding templates)
102	for (key, value) in profile_env {
103		resolved.insert(key, value);
104	}
105
106	// Apply workbench values (overriding profile)
107	for (key, value) in workbench_env {
108		resolved.insert(key, value);
109	}
110
111	// Apply feature flag values
112	for (key, value) in feature_env {
113		resolved.insert(key, value);
114	}
115
116	// Apply script values (highest priority)
117	for (key, value) in script_env {
118		resolved.insert(key, value);
119	}
120
121	// Apply environment variable prefixes per crate
122	ApplyPrefixes(&mut resolved);
123
124	// Expand variable references
125	ExpandVariables(&mut resolved);
126
127	resolved
128}
129
130/// Resolves the final set of environment variables (simplified version).
131///
132/// This function merges variables from:
133/// 1. Template defaults
134/// 2. Profile-specific static variables
135/// 3. Rhai script output
136/// 4. Current process environment
137///
138/// # Arguments
139///
140/// * `template_env` - Environment variables from templates
141/// * `profile_env` - Environment variables from profile
142/// * `script_env` - Environment variables from Rhai script
143/// * `preserve_current` - Whether to preserve current process environment
144///
145/// # Returns
146///
147/// Final resolved HashMap of environment variables
148pub fn Resolve(
149	template_env:HashMap<String, String>,
150
151	profile_env:HashMap<String, String>,
152
153	script_env:HashMap<String, String>,
154
155	preserve_current:bool,
156) -> HashMap<String, String> {
157	ResolveFull(
158		template_env,
159		profile_env,
160		HashMap::new(),
161		HashMap::new(),
162		script_env,
163		preserve_current,
164	)
165}
166
167/// Applies environment variables to the current process.
168///
169/// # Arguments
170///
171/// * `env_vars` - Environment variables to apply
172///
173/// # Example
174///
175/// ```no_run
176/// use crate::Maintain::Source::Build::Rhai::EnvironmentResolver;
177///
178/// let env = HashMap::from([
179/// 	("NODE_ENV".to_string(), "production".to_string()),
180/// 	("RUST_LOG".to_string(), "info".to_string()),
181/// ]);
182///
183/// EnvironmentResolver::apply(&env);
184/// ```
185pub fn Apply(env_vars:&HashMap<String, String>) {
186	for (key, value) in env_vars {
187		// Safety: set_var is now unsafe in recent Rust versions
188		// Setting environment variables during build orchestration is acceptable
189		// as it doesn't violate memory safety.
190		unsafe {
191			std::env::set_var(key, value);
192		}
193	}
194}
195
196/// Converts environment variables to a formatted string for logging.
197///
198/// # Arguments
199///
200/// * `env_vars` - Environment variables to format
201///
202/// # Returns
203///
204/// Formatted string representation
205pub fn format_env(env_vars:&HashMap<String, String>) -> String {
206	let mut entries:Vec<_> = env_vars.iter().collect();
207
208	entries.sort_by_key(|(k, _)| *k);
209
210	entries
211		.iter()
212		.map(|(k, v)| format!("  {}={}", k, v))
213		.collect::<Vec<_>>()
214		.join("\n")
215}
216
217/// Validates that required environment variables are set.
218///
219/// # Arguments
220///
221/// * `env_vars` - Current environment variables
222/// * `required` - List of required variable names
223///
224/// # Returns
225///
226/// Result indicating success or list of missing variables
227pub fn validate_required(env_vars:&HashMap<String, String>, required:&[&str]) -> Result<(), Vec<String>> {
228	let missing:Vec<String> = required
229		.iter()
230		.filter(|var| !env_vars.contains_key(&var.to_string()))
231		.map(|s| s.to_string())
232		.collect();
233
234	if missing.is_empty() { Ok(()) } else { Err(missing) }
235}
236
237/// Generates workbench-specific environment variables.
238///
239/// # Arguments
240///
241/// * `workbench_type` - The selected workbench type
242///
243/// # Returns
244///
245/// HashMap of workbench environment variables
246pub fn generate_workbench_env(workbench_type:&str) -> HashMap<String, String> {
247	let mut env = HashMap::new();
248
249	// Set the workbench type as an environment variable
250	env.insert(workbench_type.to_string(), "true".to_string());
251
252	// Set WORKBENCH_TYPE for use in build scripts
253	env.insert("WORKBENCH_TYPE".to_string(), workbench_type.to_string());
254
255	env
256}
257
258/// Generates feature flag environment variables from a feature map.
259///
260/// # Arguments
261///
262/// * `features` - HashMap of feature name to enabled status
263///
264/// # Returns
265///
266/// HashMap of FEATURE_* environment variables
267pub fn generate_feature_env(features:&HashMap<String, bool>) -> HashMap<String, String> {
268	features
269		.iter()
270		.map(|(name, value)| {
271			let env_key = format!("FEATURE_{}", name.to_uppercase().replace('-', "_"));
272
273			(env_key, value.to_string())
274		})
275		.collect()
276}
277
278//=============================================================================
279// Private Helper Functions
280//=============================================================================
281
282/// Applies environment variable prefixes per crate.
283fn ApplyPrefixes(_env_vars:&mut HashMap<String, String>) {
284	// Define known crate prefixes
285	let Prefixes = [
286		("air", "AIR_"),
287		("cocoon", "MOUNTAIN_"),
288		("grove", "VSCODE_"),
289		("maintain", "LAND_"),
290	];
291
292	// For now, this is a no-op as prefixes are handled in the config
293	// Future: could auto-prefix variables based on their names
294	let _ = Prefixes;
295}
296
297/// Expands variable references in environment variable values.
298///
299/// Supports ${VAR} syntax for variable expansion.
300fn ExpandVariables(env_vars:&mut HashMap<String, String>) {
301	// Collect all current values for reference
302	let Original:HashMap<String, String> = env_vars.clone();
303
304	// Expand ${VAR} references in each value
305	for Value in env_vars.values_mut() {
306		// Simple expansion - replace ${VAR} with the value from Original
307		let mut Expanded = Value.clone();
308
309		let mut Start = 0;
310
311		while let Some(Open) = Expanded[Start..].find("${") {
312			let AbsOpen = Start + Open;
313
314			if let Some(Close) = Expanded[AbsOpen..].find('}') {
315				let VarName = &Expanded[AbsOpen + 2..AbsOpen + Close];
316
317				if let Some(Replacement) = Original.get(VarName) {
318					Expanded.replace_range(AbsOpen..AbsOpen + Close + 1, Replacement);
319
320					// Continue from after the replacement
321					Start = AbsOpen + Replacement.len();
322				} else {
323					// Variable not found, skip past this reference
324					Start = AbsOpen + Close + 1;
325				}
326			} else {
327				break;
328			}
329		}
330
331		*Value = Expanded;
332	}
333}
334
335//=============================================================================
336// Tests
337//=============================================================================
338
339#[cfg(test)]
340mod tests {
341
342	use super::*;
343#[test]
344fn test_resolve() {
345    let template = HashMap::from([
346        (String::from("A"), "1".to_string()),
347        (String::from("B"), "2".to_string()),
348    ]);
349
350    let profile = HashMap::from([
351        (String::from("B"), "3".to_string()),
352        (String::from("C"), "4".to_string()),
353    ]);
354
355    let script = HashMap::from([(String::from("C"), "5".to_string())]);
356
357    let result = Resolve(template, profile, script, false);
358
359    assert_eq!(result.get("A"), Some(&"1".to_string())); // From template
360
361    assert_eq!(result.get("B"), Some(&"3".to_string())); // Profile overrides template
362
363    assert_eq!(result.get("C"), Some(&"5".to_string())); // Script overrides profile
364}
365	#[test]
366	fn test_generate_workbench_env() {
367		let env = generate_workbench_env("Mountain");
368
369		assert_eq!(env.get("Mountain"), Some(&"true".to_string()));
370
371		assert_eq!(env.get("WORKBENCH_TYPE"), Some(&"Mountain".to_string()));
372	}
373
374	#[test]
375	fn test_generate_feature_env() {
376		let mut features = HashMap::new();
377
378		features.insert("tauri-ipc".to_string(), true);
379
380		features.insert("wind-services".to_string(), false);
381
382		let env = generate_feature_env(&features);
383
384		assert_eq!(env.get("FEATURE_TAURI_IPC"), Some(&"true".to_string()));
385
386		assert_eq!(env.get("FEATURE_WIND_SERVICES"), Some(&"false".to_string()));
387	}
388
389	#[test]
390	fn test_validate_required() {
391		let env = HashMap::from([("A".to_string(), "1".to_string()), ("B".to_string(), "2".to_string())]);
392
393		assert!(validate_required(&env, &["A", "B"]).is_ok());
394
395		assert!(validate_required(&env, &["A", "C"]).is_err());
396	}
397
398	#[test]
399	fn test_expand_variables() {
400		let mut env = HashMap::new();
401
402		env.insert("BASE".to_string(), "/path/to/base".to_string());
403
404		env.insert("FULL".to_string(), "${BASE}/sub".to_string());
405
406		ExpandVariables(&mut env);
407
408		assert_eq!(env.get("FULL"), Some(&"/path/to/base/sub".to_string()));
409	}
410}