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}