Skip to main content

rusty_stack/installers/common/
env_validation.rs

1//! Environment validation — `.mlstack_env` checking with sensible defaults.
2//!
3//! Ports functionality from `scripts/env_validation_utils.sh`:
4//! - Validates `.mlstack_env` file exists and contains required variables
5//! - Provides sensible defaults for missing values
6//! - Validates ROCM_CHANNEL value
7//! - Auto-detects GPU_ARCH and ROCM_VERSION when possible
8//!
9//! # Validation Assertions
10//!
11//! - **VAL-INFRA-017**: .mlstack_env validation with sensible defaults
12//! - **VAL-INFRA-022**: Graceful handling of missing dependencies (no panics)
13
14use crate::installers::common::utils::command_exists;
15use std::collections::HashMap;
16use std::fs;
17use std::path::{Path, PathBuf};
18
19// ===========================================================================
20// Constants
21// ===========================================================================
22
23/// Valid ROCm channel names.
24const VALID_CHANNELS: &[&str] = &["legacy", "stable", "latest", "preview"];
25
26/// Default ROCm version if not detected.
27const DEFAULT_ROCM_VERSION: &str = "7.2.0";
28
29/// Default ROCm channel.
30const DEFAULT_ROCM_CHANNEL: &str = "latest";
31
32/// Default GPU architecture.
33const DEFAULT_GPU_ARCH: &str = "gfx1100";
34
35/// Default venv base directory (relative to home).
36const DEFAULT_VENV_BASE_SUBDIR: &str = ".mlstack/venvs";
37
38// ===========================================================================
39// Types
40// ===========================================================================
41
42/// Result of validating the `.mlstack_env` file.
43#[derive(Debug, Clone)]
44pub struct EnvValidationResult {
45    /// Whether the env file existed.
46    pub file_existed: bool,
47    /// Whether any defaults were applied.
48    pub defaults_applied: bool,
49    /// The resolved environment variables.
50    pub vars: EnvVars,
51    /// Warnings generated during validation.
52    pub warnings: Vec<String>,
53}
54
55/// Resolved environment variables from `.mlstack_env`.
56#[derive(Debug, Clone)]
57pub struct EnvVars {
58    /// ROCm version string (e.g., "7.2.0").
59    pub rocm_version: String,
60    /// ROCm channel (legacy, stable, latest, preview).
61    pub rocm_channel: String,
62    /// GPU architecture (e.g., "gfx1100").
63    pub gpu_arch: String,
64    /// ML Stack venv base directory.
65    pub venv_base: String,
66    /// Python binary path (if set).
67    pub python_bin: Option<String>,
68}
69
70impl Default for EnvVars {
71    fn default() -> Self {
72        Self {
73            rocm_version: DEFAULT_ROCM_VERSION.to_string(),
74            rocm_channel: DEFAULT_ROCM_CHANNEL.to_string(),
75            gpu_arch: DEFAULT_GPU_ARCH.to_string(),
76            venv_base: String::new(),
77            python_bin: None,
78        }
79    }
80}
81
82// ===========================================================================
83// Validation Functions
84// ===========================================================================
85
86/// Get the default path for `.mlstack_env`.
87pub fn default_env_path() -> PathBuf {
88    dirs::home_dir()
89        .unwrap_or_else(|| PathBuf::from("/tmp"))
90        .join(".mlstack_env")
91}
92
93/// Get the default venv base directory.
94pub fn default_venv_base() -> String {
95    let home = dirs::home_dir().unwrap_or_else(|| PathBuf::from("/tmp"));
96    home.join(DEFAULT_VENV_BASE_SUBDIR)
97        .to_string_lossy()
98        .to_string()
99}
100
101/// Validate the `.mlstack_env` file and return resolved variables.
102///
103/// If the file does not exist, auto-detects values from the system.
104/// Missing values are filled with sensible defaults.
105/// Invalid channel values are corrected to "latest".
106///
107/// # Validation
108///
109/// - **VAL-INFRA-017**: .mlstack_env validation with sensible defaults
110/// - **VAL-INFRA-022**: Graceful handling of missing dependencies
111///
112/// This function never panics. If detection fails, defaults are used.
113pub fn validate_mlstack_env(script_name: Option<&str>) -> EnvValidationResult {
114    let env_path = default_env_path();
115    let mut warnings = Vec::new();
116    let mut defaults_applied = false;
117
118    if !env_path.exists() {
119        warnings.push(format!(
120            "Environment file not found: {} - using auto-detection",
121            env_path.display()
122        ));
123        let vars = auto_detect_vars(&mut defaults_applied);
124        return EnvValidationResult {
125            file_existed: false,
126            defaults_applied: true,
127            vars,
128            warnings,
129        };
130    }
131
132    // Parse the env file
133    let contents = match fs::read_to_string(&env_path) {
134        Ok(c) => c,
135        Err(e) => {
136            warnings.push(format!("Failed to read {}: {e}", env_path.display()));
137            let vars = auto_detect_vars(&mut defaults_applied);
138            return EnvValidationResult {
139                file_existed: true,
140                defaults_applied: true,
141                vars,
142                warnings,
143            };
144        }
145    };
146
147    let parsed = parse_env_file(&contents);
148
149    // Build EnvVars from parsed values with defaults
150    let mut vars = EnvVars {
151        rocm_version: String::new(),
152        rocm_channel: String::new(),
153        gpu_arch: String::new(),
154        venv_base: String::new(),
155        python_bin: None,
156    };
157    let _ = script_name; // used for warning context
158
159    // ROCM_VERSION
160    if let Some(v) = parsed.get("ROCM_VERSION") {
161        vars.rocm_version = v.clone();
162    } else {
163        vars.rocm_version = detect_rocm_version_or_default();
164        defaults_applied = true;
165        warnings.push("ROCM_VERSION not set, using auto-detection".to_string());
166    }
167
168    // ROCM_CHANNEL
169    if let Some(v) = parsed.get("ROCM_CHANNEL") {
170        let lower = v.to_lowercase();
171        if VALID_CHANNELS.contains(&lower.as_str()) {
172            vars.rocm_channel = lower;
173        } else {
174            warnings.push(format!(
175                "Unknown ROCM_CHANNEL '{v}', defaulting to 'latest'"
176            ));
177            vars.rocm_channel = DEFAULT_ROCM_CHANNEL.to_string();
178            defaults_applied = true;
179        }
180    } else {
181        vars.rocm_channel = DEFAULT_ROCM_CHANNEL.to_string();
182        defaults_applied = true;
183        warnings.push("ROCM_CHANNEL not set, defaulting to 'latest'".to_string());
184    }
185
186    // GPU_ARCH
187    if let Some(v) = parsed.get("GPU_ARCH") {
188        vars.gpu_arch = v.clone();
189    } else {
190        vars.gpu_arch = detect_gpu_arch_or_default();
191        defaults_applied = true;
192        warnings.push("GPU_ARCH not set, using auto-detection".to_string());
193    }
194
195    // MLSTACK_VENV_BASE
196    vars.venv_base = parsed
197        .get("MLSTACK_VENV_BASE")
198        .cloned()
199        .unwrap_or_else(default_venv_base);
200
201    // MLSTACK_PYTHON_BIN
202    vars.python_bin = parsed.get("MLSTACK_PYTHON_BIN").cloned();
203
204    EnvValidationResult {
205        file_existed: true,
206        defaults_applied,
207        vars,
208        warnings,
209    }
210}
211
212/// Require `.mlstack_env` validation — always succeeds with defaults.
213///
214/// This is the equivalent of `require_mlstack_env` in the shell scripts.
215/// It never fails; missing values are always filled with defaults.
216pub fn require_mlstack_env(script_name: Option<&str>) -> EnvValidationResult {
217    validate_mlstack_env(script_name)
218}
219
220/// Load the `.mlstack_env` file into the current process environment.
221///
222/// Equivalent to `source ~/.mlstack_env` in the shell scripts.
223/// Returns `true` if the file was loaded successfully.
224///
225/// Handles both direct exports (`export VAR=value`) and if-guarded exports
226/// (`if [ -z "${VAR:-}" ]; then export VAR=value; fi`). Shell suffixes are
227/// stripped to prevent `; fi` from appearing in environment variable values.
228pub fn load_mlstack_env() -> bool {
229    let env_path = default_env_path();
230    if !env_path.exists() {
231        return false;
232    }
233
234    let contents = match fs::read_to_string(&env_path) {
235        Ok(c) => c,
236        Err(_) => return false,
237    };
238
239    for line in contents.lines() {
240        let trimmed = line.trim();
241        if trimmed.is_empty() || trimmed.starts_with('#') {
242            continue;
243        }
244
245        if let Some((key, raw_value)) = extract_export_assignment(trimmed) {
246            let value = strip_shell_suffixes(&raw_value);
247            // Remove surrounding quotes if present
248            let value = value
249                .strip_prefix('"')
250                .and_then(|v| v.strip_suffix('"'))
251                .unwrap_or(&value);
252            // Skip values with shell variable expansions ($PATH, etc.)
253            if !value.contains('$') {
254                std::env::set_var(&key, value);
255            }
256        }
257    }
258
259    true
260}
261
262// ===========================================================================
263// Helpers
264// ===========================================================================
265
266/// Parse an env file into a HashMap of key-value pairs.
267///
268/// Handles two formats:
269/// 1. Direct exports: `export VAR=value`
270/// 2. If-guarded exports: `if [ -z "${VAR:-}" ]; then export VAR=value; fi`
271///
272/// Shell suffixes (`; fi`, `&&`, etc.) are stripped from values to prevent
273/// shell script remnants from appearing in parsed environment variable values.
274fn parse_env_file(contents: &str) -> HashMap<String, String> {
275    let mut map = HashMap::new();
276
277    for line in contents.lines() {
278        let trimmed = line.trim();
279        if trimmed.is_empty() || trimmed.starts_with('#') {
280            continue;
281        }
282
283        // Extract any "export KEY=VALUE" assignments from the line.
284        // This handles both direct exports and if-guarded exports.
285        if let Some(key_value) = extract_export_assignment(trimmed) {
286            let (key, value) = key_value;
287            let value = strip_shell_suffixes(&value);
288            let value = value
289                .strip_prefix('"')
290                .and_then(|v| v.strip_suffix('"'))
291                .unwrap_or(&value)
292                .to_string();
293            map.insert(key, value);
294        }
295    }
296    map
297}
298
299/// Extract a `KEY=VALUE` pair from a line containing `export KEY=VALUE`.
300///
301/// Handles:
302/// - `export KEY=VALUE`
303/// - `if [ -z "${KEY:-}" ]; then export KEY=VALUE; fi`
304/// - `    export KEY=VALUE` (indented inside if block)
305fn extract_export_assignment(line: &str) -> Option<(String, String)> {
306    // Find the last occurrence of "export " in the line (handles indented exports)
307    let export_idx = line.rfind("export ")?;
308    let rest = &line[export_idx + 7..]; // skip "export "
309
310    if let Some((key, value)) = rest.split_once('=') {
311        let key = key.trim().to_string();
312        if key.is_empty() || key.contains(' ') || key.contains('$') {
313            return None;
314        }
315        let value = value.trim().to_string();
316        Some((key, value))
317    } else {
318        None
319    }
320}
321
322/// Strip shell command suffixes from a value string.
323///
324/// Removes trailing:
325/// - `; fi` (from if-guard lines)
326/// - `&& ...` (from chained commands)
327/// - Any remaining `;` followed by shell keywords
328fn strip_shell_suffixes(value: &str) -> String {
329    let mut value = value.to_string();
330
331    // Strip trailing "; fi" (possibly with whitespace)
332    if let Some(idx) = value.find("; fi") {
333        value.truncate(idx);
334    }
335    if let Some(idx) = value.find(";& fi") {
336        value.truncate(idx);
337    }
338
339    // Strip trailing "&& ..." (chained commands)
340    if let Some(idx) = value.find("&& ") {
341        value.truncate(idx);
342    }
343
344    // Strip trailing "; then" (shouldn't normally appear in values but safety net)
345    if let Some(idx) = value.find("; then") {
346        value.truncate(idx);
347    }
348
349    value.trim().to_string()
350}
351
352/// Auto-detect environment variables from the system.
353fn auto_detect_vars(defaults_applied: &mut bool) -> EnvVars {
354    *defaults_applied = true;
355    EnvVars {
356        rocm_version: detect_rocm_version_or_default(),
357        rocm_channel: DEFAULT_ROCM_CHANNEL.to_string(),
358        gpu_arch: detect_gpu_arch_or_default(),
359        venv_base: default_venv_base(),
360        python_bin: None,
361    }
362}
363
364/// Detect ROCm version or return default.
365fn detect_rocm_version_or_default() -> String {
366    // Try platform::linux first
367    #[cfg(unix)]
368    {
369        let version = crate::platform::linux::get_rocm_version();
370        if !version.is_empty() {
371            return version;
372        }
373    }
374
375    // Fallback: read version file directly
376    let version_file = Path::new("/opt/rocm/.info/version");
377    if let Ok(contents) = fs::read_to_string(version_file) {
378        let v = contents.trim().to_string();
379        if !v.is_empty() {
380            return v;
381        }
382    }
383
384    DEFAULT_ROCM_VERSION.to_string()
385}
386
387/// Detect GPU architecture or return default.
388fn detect_gpu_arch_or_default() -> String {
389    // Try rocminfo with resolved full path (avoids "command not found" under sudo)
390    let rocminfo_path = crate::installers::common::utils::resolve_rocminfo_path();
391    if command_exists(&rocminfo_path) || command_exists("rocminfo") {
392        if let Ok(output) = std::process::Command::new(&rocminfo_path).output() {
393            if output.status.success() {
394                let stdout = String::from_utf8_lossy(&output.stdout);
395                for line in stdout.lines() {
396                    let trimmed = line.trim();
397                    if trimmed.starts_with("Name:") || trimmed.contains("gfx") {
398                        // Extract gfx version
399                        if let Some(idx) = trimmed.find("gfx") {
400                            let rest = &trimmed[idx..];
401                            let gfx: String = rest
402                                .chars()
403                                .take_while(|c| c.is_ascii_alphanumeric())
404                                .collect();
405                            if gfx.starts_with("gfx") && gfx.len() >= 5 {
406                                return gfx;
407                            }
408                        }
409                    }
410                }
411            }
412        }
413    }
414
415    DEFAULT_GPU_ARCH.to_string()
416}
417
418// ===========================================================================
419// Tests
420// ===========================================================================
421
422#[cfg(test)]
423mod tests {
424    use super::*;
425
426    // --- parse_env_file tests ---
427
428    #[test]
429    fn test_parse_env_file_basic() {
430        let contents = r#"
431# Comment
432export ROCM_VERSION=7.2.0
433export ROCM_CHANNEL=latest
434export GPU_ARCH=gfx1100
435"#;
436        let map = parse_env_file(contents);
437        assert_eq!(map.get("ROCM_VERSION").unwrap(), "7.2.0");
438        assert_eq!(map.get("ROCM_CHANNEL").unwrap(), "latest");
439        assert_eq!(map.get("GPU_ARCH").unwrap(), "gfx1100");
440    }
441
442    #[test]
443    fn test_parse_env_file_quoted_values() {
444        let contents = r#"
445export PATH="/opt/rocm/bin:$PATH"
446export ROCM_PATH=/opt/rocm
447"#;
448        let map = parse_env_file(contents);
449        assert_eq!(map.get("PATH").unwrap(), "/opt/rocm/bin:$PATH");
450        assert_eq!(map.get("ROCM_PATH").unwrap(), "/opt/rocm");
451    }
452
453    #[test]
454    fn test_parse_env_file_empty() {
455        let map = parse_env_file("");
456        assert!(map.is_empty());
457    }
458
459    #[test]
460    fn test_parse_env_file_comments_only() {
461        let contents = "# just comments\n# another comment\n";
462        let map = parse_env_file(contents);
463        assert!(map.is_empty());
464    }
465
466    // --- validate_mlstack_env tests (VAL-INFRA-017) ---
467
468    #[test]
469    fn test_validate_missing_env_file() {
470        // Use a temp dir to ensure no .mlstack_env exists
471        let _temp_dir = tempfile::tempdir().unwrap();
472        // We can't easily override the home dir, but we can test the auto-detect path
473        let result = validate_mlstack_env(Some("test_script"));
474        // Should always succeed (never panic)
475        assert!(
476            result.vars.rocm_channel == "latest"
477                || result.vars.rocm_channel == "stable"
478                || result.vars.rocm_channel == "legacy"
479                || result.vars.rocm_channel == "preview"
480        );
481    }
482
483    #[test]
484    fn test_validate_always_returns_defaults() {
485        let result = validate_mlstack_env(None);
486        // Should always have non-empty values
487        assert!(!result.vars.rocm_version.is_empty());
488        assert!(!result.vars.rocm_channel.is_empty());
489        assert!(!result.vars.gpu_arch.is_empty());
490    }
491
492    #[test]
493    fn test_require_mlstack_env_always_succeeds() {
494        let result = require_mlstack_env(None);
495        // Should always return a result with valid defaults
496        assert!(!result.vars.rocm_version.is_empty());
497    }
498
499    #[test]
500    fn test_valid_channels() {
501        for ch in VALID_CHANNELS {
502            assert!(VALID_CHANNELS.contains(ch));
503        }
504    }
505
506    // --- EnvVars default ---
507
508    #[test]
509    fn test_env_vars_default() {
510        let vars = EnvVars::default();
511        assert_eq!(vars.rocm_version, "7.2.0");
512        assert_eq!(vars.rocm_channel, "latest");
513        assert_eq!(vars.gpu_arch, "gfx1100");
514        assert!(vars.python_bin.is_none());
515    }
516
517    // --- default_venv_base ---
518
519    #[test]
520    fn test_default_venv_base() {
521        let base = default_venv_base();
522        assert!(base.contains(".mlstack/venvs"));
523    }
524
525    // --- load_mlstack_env ---
526
527    #[test]
528    fn test_load_mlstack_env_nonexistent() {
529        // Should return false without panicking
530        // This tests the actual default path which may or may not exist
531        let _ = load_mlstack_env();
532    }
533
534    // --- detect_rocm_version_or_default ---
535
536    #[test]
537    fn test_detect_rocm_version_no_panic() {
538        let version = detect_rocm_version_or_default();
539        assert!(!version.is_empty());
540    }
541
542    // --- detect_gpu_arch_or_default ---
543
544    #[test]
545    fn test_detect_gpu_arch_no_panic() {
546        let arch = detect_gpu_arch_or_default();
547        assert!(!arch.is_empty());
548        assert!(arch.starts_with("gfx"));
549    }
550
551    // --- Integration: parse + validate with known content ---
552
553    #[test]
554    fn test_validate_with_valid_channel() {
555        // This tests the channel validation logic indirectly
556        let vars = EnvVars::default();
557        assert!(VALID_CHANNELS.contains(&vars.rocm_channel.as_str()));
558    }
559
560    #[test]
561    fn test_gpu_arch_format() {
562        let arch = detect_gpu_arch_or_default();
563        // Should be gfx followed by digits
564        assert!(arch.starts_with("gfx"));
565        assert!(arch.len() >= 5); // gfx + at least 2 digits
566    }
567
568    // --- Shell suffix stripping tests (fix-persistent-env-detection) ---
569
570    #[test]
571    fn test_parse_env_file_if_guard_single_line() {
572        // The env_setup.rs generates lines like:
573        // if [ -z "${HIP_VISIBLE_DEVICES:-}" ]; then export HIP_VISIBLE_DEVICES=0,1,2; fi
574        let contents =
575            r#"if [ -z "${HIP_VISIBLE_DEVICES:-}" ]; then export HIP_VISIBLE_DEVICES=0,1,2; fi"#;
576        let map = parse_env_file(contents);
577        let value = map.get("HIP_VISIBLE_DEVICES").unwrap();
578        assert_eq!(value, "0,1,2", "Should strip '; fi' suffix, got: {value}");
579        assert!(
580            !value.contains("; fi"),
581            "Value must not contain shell suffixes"
582        );
583    }
584
585    #[test]
586    fn test_parse_env_file_rocm_version_if_guard() {
587        let contents = r#"if [ -z "${ROCM_VERSION:-}" ]; then export ROCM_VERSION=7.2.2; fi"#;
588        let map = parse_env_file(contents);
589        let value = map.get("ROCM_VERSION").unwrap();
590        assert_eq!(value, "7.2.2", "Should strip '; fi' suffix, got: {value}");
591    }
592
593    #[test]
594    fn test_parse_env_file_rocm_home_if_guard() {
595        let contents = r#"if [ -z "${ROCM_HOME:-}" ]; then export ROCM_HOME=/opt/rocm; fi"#;
596        let map = parse_env_file(contents);
597        let value = map.get("ROCM_HOME").unwrap();
598        assert_eq!(
599            value, "/opt/rocm",
600            "Should strip '; fi' suffix, got: {value}"
601        );
602    }
603
604    #[test]
605    fn test_parse_env_file_mixed_formats() {
606        // Mix of if-guard and direct export lines, like the real .mlstack_env
607        let contents = r#"
608# ML Stack Environment File
609if [ -z "${HIP_VISIBLE_DEVICES:-}" ]; then export HIP_VISIBLE_DEVICES=0,1,2; fi
610if [ -z "${ROCM_VERSION:-}" ]; then export ROCM_VERSION=7.2.2; fi
611export GPU_ARCH=gfx1100
612export HSA_OVERRIDE_GFX_VERSION=11.0.0
613if [ -z "${HSA_ENABLE_SDMA:-}" ]; then export HSA_ENABLE_SDMA=0; fi
614"#;
615        let map = parse_env_file(contents);
616
617        assert_eq!(map.get("HIP_VISIBLE_DEVICES").unwrap(), "0,1,2");
618        assert_eq!(map.get("ROCM_VERSION").unwrap(), "7.2.2");
619        assert_eq!(map.get("GPU_ARCH").unwrap(), "gfx1100");
620        assert_eq!(map.get("HSA_OVERRIDE_GFX_VERSION").unwrap(), "11.0.0");
621        assert_eq!(map.get("HSA_ENABLE_SDMA").unwrap(), "0");
622
623        // Verify no shell suffixes in any value
624        for (key, value) in &map {
625            assert!(
626                !value.contains("; fi"),
627                "{key}: Value contains '; fi': {value}"
628            );
629            assert!(!value.contains("&&"), "{key}: Value contains '&&': {value}");
630        }
631    }
632
633    #[test]
634    fn test_parse_env_file_multiline_if_guard() {
635        // Some if-guards span multiple lines
636        let contents = r#"if [ -z "${HSA_TOOLS_LIB:-}" ]; then
637    if [ -f "/opt/rocm/lib/rocprofiler-sdk/librocprofiler-sdk-tool.so" ]; then
638        export HSA_TOOLS_LIB="/opt/rocm/lib/rocprofiler-sdk/librocprofiler-sdk-tool.so"
639    else
640        export HSA_TOOLS_LIB=0
641    fi
642fi"#;
643        let map = parse_env_file(contents);
644        // Should extract the last export in the if block
645        let value = map.get("HSA_TOOLS_LIB");
646        assert!(
647            value.is_some(),
648            "Should find HSA_TOOLS_LIB in multiline if-guard"
649        );
650        let val = value.unwrap();
651        assert!(!val.contains("fi"), "Value should not contain 'fi': {val}");
652    }
653
654    #[test]
655    fn test_parse_env_file_no_shell_suffixes_in_values() {
656        let contents = r#"
657if [ -z "${CUDA_VISIBLE_DEVICES:-}" ]; then export CUDA_VISIBLE_DEVICES=0,1,2; fi
658if [ -z "${PYTORCH_ROCM_DEVICE:-}" ]; then export PYTORCH_ROCM_DEVICE=0,1,2; fi
659if [ -z "${ROCM_CHANNEL:-}" ]; then export ROCM_CHANNEL=latest; fi
660"#;
661        let map = parse_env_file(contents);
662        for (key, value) in &map {
663            assert!(!value.contains(';'), "{key}: Value contains ';': {value}");
664            assert!(!value.contains("fi"), "{key}: Value contains 'fi': {value}");
665        }
666    }
667
668    #[test]
669    fn test_parse_env_file_direct_export_unmodified() {
670        // Direct exports (no if-guard) should still work correctly
671        let contents = r#"
672export ROCM_VERSION=7.2.2
673export GPU_ARCH=gfx1100
674export ROCM_HOME=/opt/rocm
675"#;
676        let map = parse_env_file(contents);
677        assert_eq!(map.get("ROCM_VERSION").unwrap(), "7.2.2");
678        assert_eq!(map.get("GPU_ARCH").unwrap(), "gfx1100");
679        assert_eq!(map.get("ROCM_HOME").unwrap(), "/opt/rocm");
680    }
681
682    #[test]
683    fn test_parse_env_file_permanent_env_format() {
684        // Content generated by permanent_env.rs (direct export, no if-guards)
685        let contents = r#"# Permanent ROCm Environment Setup (Generated by rusty-stack)
686export ROCM_VERSION=7.2.0
687export ROCM_CHANNEL=latest
688export GPU_ARCH=gfx1100
689export PYTORCH_ROCM_ARCH=gfx1100
690export ROCM_HOME=/opt/rocm
691export ROCM_PATH=/opt/rocm
692export HIP_VISIBLE_DEVICES=0
693export CUDA_VISIBLE_DEVICES=0
694export MLSTACK_PYTHON_BIN=python3
695export HSA_OVERRIDE_GFX_VERSION=11.0.0
696"#;
697        let map = parse_env_file(contents);
698        assert_eq!(map.get("ROCM_VERSION").unwrap(), "7.2.0");
699        assert_eq!(map.get("HIP_VISIBLE_DEVICES").unwrap(), "0");
700        assert_eq!(map.get("HSA_OVERRIDE_GFX_VERSION").unwrap(), "11.0.0");
701    }
702}