rusty_stack/installers/common/
rocm_env.rs1#[cfg(unix)]
14use crate::platform::linux;
15use std::path::PathBuf;
16
17#[derive(Debug, Clone)]
31pub struct RocmEnv {
32 path: Option<PathBuf>,
34 version: String,
36}
37
38impl RocmEnv {
39 pub fn detect() -> Self {
44 #[cfg(unix)]
45 {
46 let path = linux::detect_rocm_path();
47 let version = linux::get_rocm_version();
48 Self { path, version }
49 }
50 #[cfg(not(unix))]
51 {
52 Self {
53 path: None,
54 version: String::new(),
55 }
56 }
57 }
58
59 pub fn from_known(path: Option<PathBuf>, version: String) -> Self {
61 Self { path, version }
62 }
63
64 pub fn none() -> Self {
66 Self {
67 path: None,
68 version: String::new(),
69 }
70 }
71
72 pub fn path(&self) -> Option<&PathBuf> {
74 self.path.as_ref()
75 }
76
77 pub fn version(&self) -> &str {
82 &self.version
83 }
84
85 pub fn is_detected(&self) -> bool {
87 self.path.is_some()
88 }
89
90 pub fn bin_path(&self) -> Option<PathBuf> {
95 self.path
96 .as_ref()
97 .map(|p| p.join("bin"))
98 .filter(|p| p.is_dir())
99 }
100
101 pub fn lib_path(&self) -> Option<PathBuf> {
105 self.path.as_ref().and_then(|p| {
106 let lib = p.join("lib");
107 if lib.is_dir() {
108 return Some(lib);
109 }
110 let lib64 = p.join("lib64");
111 if lib64.is_dir() {
112 return Some(lib64);
113 }
114 None
115 })
116 }
117
118 pub fn include_path(&self) -> Option<PathBuf> {
120 self.path
121 .as_ref()
122 .map(|p| p.join("include"))
123 .filter(|p| p.is_dir())
124 }
125
126 pub fn tool_exists(&self, tool_name: &str) -> bool {
130 if let Some(bin) = self.bin_path() {
132 let tool_path = bin.join(tool_name);
133 if tool_path.exists() {
134 return true;
135 }
136 }
137
138 which_exists(tool_name)
140 }
141
142 pub fn tool_path(&self, tool_name: &str) -> Option<PathBuf> {
146 if let Some(bin) = self.bin_path() {
148 let tool_path = bin.join(tool_name);
149 if tool_path.exists() {
150 return Some(tool_path);
151 }
152 }
153
154 if let Ok(output) = std::process::Command::new("which").arg(tool_name).output() {
156 if output.status.success() {
157 let path_str = String::from_utf8_lossy(&output.stdout).trim().to_string();
158 if !path_str.is_empty() {
159 return Some(PathBuf::from(path_str));
160 }
161 }
162 }
163
164 None
165 }
166
167 pub fn version_major_minor(&self) -> String {
171 extract_major_minor(&self.version)
172 }
173
174 pub fn series_candidates(&self) -> Vec<String> {
178 let detected = self.version_major_minor();
179 let mut candidates = Vec::new();
180 if !detected.is_empty() {
181 candidates.push(detected);
182 }
183 for fallback in &[
184 "7.2", "7.1", "7.0", "6.4", "6.3", "6.2", "6.1", "6.0", "5.7",
185 ] {
186 let fb = fallback.to_string();
187 if !candidates.contains(&fb) {
188 candidates.push(fb);
189 }
190 }
191 candidates
192 }
193
194 pub fn torch_index_url(&self, channel: &str, series: &str) -> String {
196 match channel {
197 "nightly" => format!("https://download.pytorch.org/whl/nightly/rocm{series}"),
198 _ => format!("https://download.pytorch.org/whl/rocm{series}"),
199 }
200 }
201
202 pub fn torch_radeon_index_url(&self, series: &str) -> String {
204 format!("https://repo.radeon.com/rocm/manylinux/rocm-rel-{series}/")
205 }
206}
207
208impl Default for RocmEnv {
209 fn default() -> Self {
210 Self::detect()
211 }
212}
213
214fn extract_major_minor(version: &str) -> String {
220 let version = version.trim();
221 if version.is_empty() {
222 return String::new();
223 }
224
225 let chars: Vec<char> = version.chars().collect();
227 let mut result = String::new();
228 let mut dot_count = 0;
229
230 for &ch in &chars {
231 if ch == '.' {
232 dot_count += 1;
233 if dot_count > 1 {
234 break;
235 }
236 result.push(ch);
237 } else if ch.is_ascii_digit() {
238 result.push(ch);
239 } else {
240 break;
241 }
242 }
243
244 if result.contains('.') && result.len() >= 3 {
246 result
247 } else {
248 String::new()
249 }
250}
251
252fn which_exists(cmd: &str) -> bool {
254 std::process::Command::new("which")
255 .arg(cmd)
256 .output()
257 .map(|o| o.status.success())
258 .unwrap_or(false)
259}
260
261#[cfg(test)]
266mod tests {
267 use super::*;
268
269 #[test]
272 fn test_extract_major_minor_standard() {
273 assert_eq!(extract_major_minor("7.2.0"), "7.2");
274 assert_eq!(extract_major_minor("6.4.3"), "6.4");
275 }
276
277 #[test]
278 fn test_extract_major_minor_two_part() {
279 assert_eq!(extract_major_minor("7.2"), "7.2");
280 }
281
282 #[test]
283 fn test_extract_major_minor_empty() {
284 assert_eq!(extract_major_minor(""), "");
285 }
286
287 #[test]
288 fn test_extract_major_minor_invalid() {
289 assert_eq!(extract_major_minor("abc"), "");
290 }
291
292 #[test]
295 fn test_rocm_env_detect_no_panic() {
296 let env = RocmEnv::detect();
298 let _ = env.version();
300 }
301
302 #[test]
303 fn test_rocm_env_from_known() {
304 let env = RocmEnv::from_known(Some(PathBuf::from("/opt/rocm")), "7.2.0".to_string());
305 assert!(env.is_detected());
306 assert_eq!(env.version(), "7.2.0");
307 assert_eq!(env.path(), Some(&PathBuf::from("/opt/rocm")));
308 }
309
310 #[test]
311 fn test_rocm_env_none() {
312 let env = RocmEnv::none();
313 assert!(!env.is_detected());
314 assert!(env.version().is_empty());
315 assert!(env.path().is_none());
316 }
317
318 #[test]
319 fn test_rocm_env_version_major_minor() {
320 let env = RocmEnv::from_known(Some(PathBuf::from("/opt/rocm")), "7.2.0".to_string());
321 assert_eq!(env.version_major_minor(), "7.2");
322 }
323
324 #[test]
325 fn test_rocm_env_version_major_minor_empty() {
326 let env = RocmEnv::none();
327 assert!(env.version_major_minor().is_empty());
328 }
329
330 #[test]
331 fn test_rocm_env_series_candidates() {
332 let env = RocmEnv::from_known(Some(PathBuf::from("/opt/rocm")), "7.2.0".to_string());
333 let candidates = env.series_candidates();
334 assert!(!candidates.is_empty());
335 assert_eq!(candidates[0], "7.2");
336 assert!(candidates.contains(&"7.1".to_string()));
338 assert!(candidates.contains(&"6.4".to_string()));
339 }
340
341 #[test]
342 fn test_rocm_env_series_candidates_no_duplicates() {
343 let env = RocmEnv::from_known(Some(PathBuf::from("/opt/rocm")), "7.2.0".to_string());
344 let candidates = env.series_candidates();
345 let mut seen = std::collections::HashSet::new();
346 for c in &candidates {
347 assert!(seen.insert(c.clone()), "Duplicate series candidate: {c}");
348 }
349 }
350
351 #[test]
352 fn test_rocm_env_torch_index_url() {
353 let env = RocmEnv::from_known(Some(PathBuf::from("/opt/rocm")), "7.2.0".to_string());
354 assert_eq!(
355 env.torch_index_url("stable", "7.2"),
356 "https://download.pytorch.org/whl/rocm7.2"
357 );
358 assert_eq!(
359 env.torch_index_url("nightly", "7.2"),
360 "https://download.pytorch.org/whl/nightly/rocm7.2"
361 );
362 }
363
364 #[test]
365 fn test_rocm_env_torch_radeon_index_url() {
366 let env = RocmEnv::from_known(Some(PathBuf::from("/opt/rocm")), "7.2.0".to_string());
367 assert_eq!(
368 env.torch_radeon_index_url("7.2"),
369 "https://repo.radeon.com/rocm/manylinux/rocm-rel-7.2/"
370 );
371 }
372
373 #[test]
374 fn test_rocm_env_default() {
375 let env = RocmEnv::default();
376 let _ = env.version();
378 }
379
380 #[test]
381 fn test_rocm_env_bin_path_with_mock() {
382 let env = RocmEnv::from_known(Some(PathBuf::from("/usr")), "1.0.0".to_string());
384 assert!(env.bin_path().is_some());
385 }
386
387 #[test]
388 fn test_rocm_env_lib_path_with_mock() {
389 let env = RocmEnv::from_known(Some(PathBuf::from("/usr")), "1.0.0".to_string());
391 assert!(env.lib_path().is_some());
392 }
393
394 #[test]
395 fn test_rocm_env_include_path_with_mock() {
396 let env = RocmEnv::from_known(Some(PathBuf::from("/usr")), "1.0.0".to_string());
398 assert!(env.include_path().is_some());
399 }
400
401 #[test]
402 fn test_rocm_env_bin_path_none() {
403 let env = RocmEnv::none();
404 assert!(env.bin_path().is_none());
405 }
406
407 #[test]
408 fn test_rocm_env_tool_exists_bash() {
409 let env = RocmEnv::from_known(Some(PathBuf::from("/usr")), "1.0.0".to_string());
410 assert!(env.tool_exists("bash"));
412 }
413
414 #[test]
415 fn test_rocm_env_tool_exists_nonexistent() {
416 let env = RocmEnv::none();
417 assert!(!env.tool_exists("nonexistent_tool_xyz_12345"));
418 }
419
420 #[test]
421 fn test_rocm_env_tool_path_bash() {
422 let env = RocmEnv::from_known(Some(PathBuf::from("/usr")), "1.0.0".to_string());
423 assert!(env.tool_path("bash").is_some());
424 }
425
426 #[test]
427 fn test_rocm_env_tool_path_nonexistent() {
428 let env = RocmEnv::none();
429 assert!(env.tool_path("nonexistent_tool_xyz_12345").is_none());
430 }
431
432 #[test]
435 fn test_rocm_env_delegates_to_platform_linux() {
436 let env = RocmEnv::detect();
438 let platform_path = linux::detect_rocm_path();
439 let platform_version = linux::get_rocm_version();
440
441 assert_eq!(env.path(), platform_path.as_ref());
442 assert_eq!(env.version(), platform_version);
443 }
444
445 #[test]
448 fn test_rocm_version_no_panic_on_malformed() {
449 let env = RocmEnv::from_known(None, "not-a-version".to_string());
450 let mm = env.version_major_minor();
452 assert!(mm.is_empty()); }
454
455 #[test]
456 fn test_rocm_version_empty_on_not_installed() {
457 let env = RocmEnv::none();
458 assert!(env.version().is_empty());
459 assert!(env.version_major_minor().is_empty());
460 }
461}